Merge branch 'precompiled-legacy' into https-packages

This commit is contained in:
Richard Feldman 2022-11-24 04:25:54 -05:00
commit f5cb2d73a1
No known key found for this signature in database
GPG key ID: F1F21AA5B1D9E43B
96 changed files with 4063 additions and 1334 deletions

View file

@ -12,7 +12,7 @@ env:
jobs: jobs:
spell-check: spell-check:
name: spell check name: spell check
runs-on: [self-hosted] runs-on: [ubuntu-20.04]
timeout-minutes: 10 timeout-minutes: 10
env: env:
FORCE_COLOR: 1 FORCE_COLOR: 1
@ -21,5 +21,7 @@ jobs:
with: with:
clean: "true" clean: "true"
- name: do spell check with typos-cli 1.0.11 # to reproduce locally: cargo install typos-cli --version 1.0.11 - run: cargo install typos-cli --version 1.0.11
- name: do spell check with typos-cli 1.0.11
run: typos run: typos

21
Cargo.lock generated
View file

@ -4037,6 +4037,7 @@ dependencies = [
"roc_mono", "roc_mono",
"roc_std", "roc_std",
"roc_target", "roc_target",
"roc_wasm_module",
] ]
[[package]] [[package]]
@ -4384,6 +4385,7 @@ dependencies = [
"roc_target", "roc_target",
"roc_types", "roc_types",
"roc_utils", "roc_utils",
"tempfile",
"wasi_libc_sys", "wasi_libc_sys",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
@ -4545,6 +4547,24 @@ dependencies = [
"snafu", "snafu",
] ]
[[package]]
name = "roc_wasm_interp"
version = "0.1.0"
dependencies = [
"bitvec 1.0.1",
"bumpalo",
"roc_wasm_module",
]
[[package]]
name = "roc_wasm_module"
version = "0.0.1"
dependencies = [
"bitvec 1.0.1",
"bumpalo",
"roc_error_macros",
]
[[package]] [[package]]
name = "rodio" name = "rodio"
version = "0.15.0" version = "0.15.0"
@ -5368,6 +5388,7 @@ dependencies = [
"roc_types", "roc_types",
"roc_unify", "roc_unify",
"roc_utils", "roc_utils",
"roc_wasm_module",
"target-lexicon", "target-lexicon",
"tempfile", "tempfile",
"wasi_libc_sys", "wasi_libc_sys",

View file

@ -23,6 +23,8 @@ members = [
"crates/docs_cli", "crates/docs_cli",
"crates/linker", "crates/linker",
"crates/wasi-libc-sys", "crates/wasi-libc-sys",
"crates/wasm_module",
"crates/wasm_interp",
] ]
exclude = [ exclude = [
"ci/benchmarks/bench-runner", "ci/benchmarks/bench-runner",

View file

@ -4,7 +4,7 @@ use roc_build::{
legacy_host_filename, link, preprocess_host_wasm32, preprocessed_host_filename, legacy_host_filename, link, preprocess_host_wasm32, preprocessed_host_filename,
rebuild_host, LinkType, LinkingStrategy, rebuild_host, LinkType, LinkingStrategy,
}, },
program::{self, CodeGenOptions, Problems}, program::{self, CodeGenOptions},
}; };
use roc_builtins::bitcode; use roc_builtins::bitcode;
use roc_load::{ use roc_load::{
@ -13,12 +13,14 @@ use roc_load::{
}; };
use roc_mono::ir::OptLevel; use roc_mono::ir::OptLevel;
use roc_packaging::cache::RocCacheDir; use roc_packaging::cache::RocCacheDir;
use roc_reporting::report::{RenderTarget, DEFAULT_PALETTE}; use roc_reporting::{
cli::Problems,
report::{RenderTarget, DEFAULT_PALETTE},
};
use roc_target::TargetInfo; use roc_target::TargetInfo;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::{path::PathBuf, thread::JoinHandle}; use std::{path::PathBuf, thread::JoinHandle};
use target_lexicon::Triple; use target_lexicon::Triple;
use tempfile::Builder;
fn report_timing(buf: &mut String, label: &str, duration: Duration) { fn report_timing(buf: &mut String, label: &str, duration: Duration) {
use std::fmt::Write; use std::fmt::Write;
@ -110,19 +112,27 @@ pub fn build_file<'a>(
} }
}; };
let (app_extension, extension) = { let (app_extension, extension, host_filename) = {
use roc_target::OperatingSystem::*; use roc_target::OperatingSystem::*;
match roc_target::OperatingSystem::from(target.operating_system) { match roc_target::OperatingSystem::from(target.operating_system) {
Wasi => { Wasi => {
if matches!(code_gen_options.opt_level, OptLevel::Development) { if matches!(code_gen_options.opt_level, OptLevel::Development) {
("wasm", Some("wasm")) ("wasm", Some("wasm"), "host.zig".to_string())
} else { } else {
("bc", Some("wasm")) ("bc", Some("wasm"), "host.zig".to_string())
} }
} }
Unix => ("o", None), Unix => (
Windows => ("obj", Some("exe")), "o",
None,
legacy_host_filename(target, code_gen_options.opt_level).unwrap(),
),
Windows => (
"obj",
Some("exe"),
legacy_host_filename(target, code_gen_options.opt_level).unwrap(),
),
} }
}; };
@ -135,8 +145,7 @@ pub fn build_file<'a>(
let host_input_path = if let EntryPoint::Executable { platform_path, .. } = &loaded.entry_point let host_input_path = if let EntryPoint::Executable { platform_path, .. } = &loaded.entry_point
{ {
cwd.join(platform_path) cwd.join(platform_path).with_file_name(host_filename)
.with_file_name(legacy_host_filename(target, code_gen_options.opt_level).unwrap())
} else { } else {
unreachable!(); unreachable!();
}; };
@ -166,19 +175,18 @@ pub fn build_file<'a>(
}) })
.collect(); .collect();
let preprocessed_host_path = match linking_strategy { let preprocessed_host_path = if linking_strategy == LinkingStrategy::Legacy {
LinkingStrategy::Surgical | LinkingStrategy::Additive => { host_input_path
host_input_path.with_file_name(preprocessed_host_filename(target).unwrap()) .with_file_name(legacy_host_filename(target, code_gen_options.opt_level).unwrap())
} } else {
LinkingStrategy::Legacy => host_input_path host_input_path.with_file_name(preprocessed_host_filename(target).unwrap())
.with_file_name(legacy_host_filename(target, code_gen_options.opt_level).unwrap()),
}; };
// We don't need to spawn a rebuild thread when using a prebuilt host. // We don't need to spawn a rebuild thread when using a prebuilt host.
let rebuild_thread = if prebuilt || loaded.uses_prebuilt_platform { let rebuild_thread = if prebuilt {
if !preprocessed_host_path.exists() { if !preprocessed_host_path.exists() {
eprintln!( eprintln!(
"\nSince I was run with --prebuilt-platform=true, I was expecting this file to exist:\n\n {}\n\nHowever, it was not there!\n\nIf you have the platform's source code locally, you may be able to regenerate it by re-running this command with --prebuilt-platform=false\n", "\nBecause I was run with --prebuilt-platform=true, I was expecting this file to exist:\n\n {}\n\nHowever, it was not there!\n\nIf you have the platform's source code locally, you may be able to regenerate it by re-running this command with --prebuilt-platform=false\n",
preprocessed_host_path.to_string_lossy() preprocessed_host_path.to_string_lossy()
); );
@ -350,7 +358,7 @@ pub fn build_file<'a>(
problems problems
} }
(LinkingStrategy::Legacy, _) => { (LinkingStrategy::Legacy, _) => {
let app_o_file = Builder::new() let app_o_file = tempfile::Builder::new()
.prefix("roc_app") .prefix("roc_app")
.suffix(&format!(".{}", app_extension)) .suffix(&format!(".{}", app_extension))
.tempfile() .tempfile()
@ -364,10 +372,21 @@ pub fn build_file<'a>(
app_o_file.to_str().unwrap(), app_o_file.to_str().unwrap(),
]; ];
let str_host_obj_path = bitcode::get_builtins_host_obj_path(); let builtins_host_tempfile = {
#[cfg(unix)]
{
bitcode::host_unix_tempfile()
}
#[cfg(windows)]
{
bitcode::host_windows_tempfile()
}
}
.expect("failed to write host builtins object to tempfile");
if matches!(code_gen_options.backend, program::CodeGenBackend::Assembly) { if matches!(code_gen_options.backend, program::CodeGenBackend::Assembly) {
inputs.push(&str_host_obj_path); inputs.push(builtins_host_tempfile.path().to_str().unwrap());
} }
let (mut child, _) = link(target, binary_path.clone(), &inputs, link_type) let (mut child, _) = link(target, binary_path.clone(), &inputs, link_type)
@ -377,6 +396,10 @@ pub fn build_file<'a>(
.wait() .wait()
.map_err(|_| todo!("gracefully handle error after `ld` spawned"))?; .map_err(|_| todo!("gracefully handle error after `ld` spawned"))?;
// Extend the lifetime of the tempfile so it doesn't get dropped
// (and thus deleted) before the child process is done using it!
let _ = builtins_host_tempfile;
if exit_status.success() { if exit_status.success() {
problems problems
} else { } else {
@ -444,6 +467,10 @@ fn spawn_rebuild_thread(
exported_symbols, exported_symbols,
exported_closure_types, exported_closure_types,
); );
// Copy preprocessed host to executable location.
// The surgical linker will modify that copy in-place.
std::fs::copy(&preprocessed_host_path, binary_path.as_path()).unwrap();
} }
LinkingStrategy::Legacy => { LinkingStrategy::Legacy => {
rebuild_host( rebuild_host(
@ -455,14 +482,7 @@ fn spawn_rebuild_thread(
} }
} }
if linking_strategy == LinkingStrategy::Surgical { rebuild_host_start.elapsed().as_millis()
// Copy preprocessed host to executable location.
std::fs::copy(preprocessed_host_path, binary_path.as_path()).unwrap();
}
let rebuild_host_end = rebuild_host_start.elapsed();
rebuild_host_end.as_millis()
}) })
} }
@ -473,7 +493,7 @@ pub fn check_file<'a>(
emit_timings: bool, emit_timings: bool,
roc_cache_dir: RocCacheDir<'_>, roc_cache_dir: RocCacheDir<'_>,
threading: Threading, threading: Threading,
) -> Result<(program::Problems, Duration), LoadingProblem<'a>> { ) -> Result<(Problems, Duration), LoadingProblem<'a>> {
let compilation_start = Instant::now(); let compilation_start = Instant::now();
// only used for generating errors. We don't do code generation, so hardcoding should be fine // only used for generating errors. We don't do code generation, so hardcoding should be fine

View file

@ -157,9 +157,7 @@ fn parse_all<'a>(arena: &'a Bump, src: &'a str) -> Result<Ast<'a>, SyntaxError<'
let (module, state) = module::parse_header(arena, State::new(src.as_bytes())) let (module, state) = module::parse_header(arena, State::new(src.as_bytes()))
.map_err(|e| SyntaxError::Header(e.problem))?; .map_err(|e| SyntaxError::Header(e.problem))?;
let (_, defs, _) = module_defs() let (_, defs, _) = module_defs().parse(arena, state, 0).map_err(|(_, e)| e)?;
.parse(arena, state, 0)
.map_err(|(_, e, _)| e)?;
Ok(Ast { module, defs }) Ok(Ast { module, defs })
} }

View file

@ -7,12 +7,13 @@ use build::BuiltFile;
use bumpalo::Bump; use bumpalo::Bump;
use clap::{Arg, ArgMatches, Command, ValueSource}; use clap::{Arg, ArgMatches, Command, ValueSource};
use roc_build::link::{LinkType, LinkingStrategy}; use roc_build::link::{LinkType, LinkingStrategy};
use roc_build::program::{CodeGenBackend, CodeGenOptions, Problems}; use roc_build::program::{CodeGenBackend, CodeGenOptions};
use roc_error_macros::{internal_error, user_error}; use roc_error_macros::{internal_error, user_error};
use roc_load::{ExpectMetadata, LoadingProblem, Threading}; use roc_load::{ExpectMetadata, LoadingProblem, Threading};
use roc_mono::ir::OptLevel; use roc_mono::ir::OptLevel;
use roc_packaging::cache::RocCacheDir; use roc_packaging::cache::RocCacheDir;
use roc_packaging::tarball::Compression; use roc_packaging::tarball::Compression;
use roc_reporting::cli::Problems;
use std::env; use std::env;
use std::ffi::{CString, OsStr}; use std::ffi::{CString, OsStr};
use std::io; use std::io;

View file

@ -353,7 +353,12 @@ mod cli_run {
return; return;
} }
"args" => { "args" => {
custom_flags = vec![LINKER_FLAG, "legacy"]; eprintln!(
"WARNING: skipping testing example {} because it is known to be bad, pending investigation!",
roc_filename
);
return;
// custom_flags = vec![LINKER_FLAG, "legacy"];
} }
_ => {} _ => {}
} }
@ -557,6 +562,16 @@ mod cli_run {
) )
} }
// TODO: remove in favor of cli_args once mono bugs are resolved in investigation
#[test]
#[cfg_attr(windows, ignore = "missing __udivdi3 and some other symbols")]
#[serial(cli_platform)]
fn cli_args_check() {
let path = file_path_from_root("examples/cli", "args.roc");
let out = run_roc(&[CMD_CHECK, path.to_str().unwrap()], &[], &[]);
assert!(out.status.success());
}
#[test] #[test]
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]
fn interactive_effects() { fn interactive_effects() {

View file

@ -60,9 +60,14 @@ pub fn link(
} }
} }
const fn legacy_host_filename_ext(target: &Triple, opt_level: OptLevel) -> Option<&'static str> { const fn legacy_host_filename_ext(
match roc_target::OperatingSystem::new(target.operating_system) { os: roc_target::OperatingSystem,
Some(roc_target::OperatingSystem::Wasi) => { opt_level: OptLevel,
) -> &'static str {
use roc_target::OperatingSystem::*;
match os {
Wasi => {
// TODO wasm host extension should be something else ideally // TODO wasm host extension should be something else ideally
// .bc does not seem to work because // .bc does not seem to work because
// //
@ -70,50 +75,13 @@ const fn legacy_host_filename_ext(target: &Triple, opt_level: OptLevel) -> Optio
// //
// and zig does not currently emit `.a` webassembly static libraries // and zig does not currently emit `.a` webassembly static libraries
if matches!(opt_level, OptLevel::Development) { if matches!(opt_level, OptLevel::Development) {
Some("wasm") "wasm"
} else { } else {
Some("zig") "zig"
} }
} }
Some(_) => match target { Unix => "o",
Triple { Windows => "obj",
operating_system: OperatingSystem::Linux,
architecture: Architecture::X86_64,
..
} => Some("o"),
Triple {
operating_system: OperatingSystem::Linux,
architecture: Architecture::Aarch64(_),
..
} => Some("o"),
Triple {
operating_system: OperatingSystem::Darwin,
architecture: Architecture::Aarch64(_),
..
} => Some("o"),
Triple {
operating_system: OperatingSystem::Darwin,
architecture: Architecture::X86_64,
..
} => Some("o"),
Triple {
operating_system: OperatingSystem::Windows,
architecture: Architecture::X86_64,
..
} => Some("obj"),
Triple {
operating_system: OperatingSystem::Windows,
architecture: Architecture::X86_32(_),
..
} => Some("obj"),
Triple {
operating_system: OperatingSystem::Windows,
architecture: Architecture::Aarch64(_),
..
} => Some("obj"),
_ => None,
},
None => None,
} }
} }
@ -121,6 +89,10 @@ const PRECOMPILED_HOST_EXT: &str = "rh1"; // Short for "roc host version 1" (so
pub const fn preprocessed_host_filename(target: &Triple) -> Option<&'static str> { pub const fn preprocessed_host_filename(target: &Triple) -> Option<&'static str> {
match target { match target {
Triple {
architecture: Architecture::Wasm32,
..
} => Some(concatcp!("wasm32", '.', PRECOMPILED_HOST_EXT)),
Triple { Triple {
operating_system: OperatingSystem::Linux, operating_system: OperatingSystem::Linux,
architecture: Architecture::X86_64, architecture: Architecture::X86_64,
@ -162,7 +134,8 @@ pub const fn preprocessed_host_filename(target: &Triple) -> Option<&'static str>
/// Same format as the precompiled host filename, except with a file extension like ".o" or ".obj" /// Same format as the precompiled host filename, except with a file extension like ".o" or ".obj"
pub fn legacy_host_filename(target: &Triple, opt_level: OptLevel) -> Option<String> { pub fn legacy_host_filename(target: &Triple, opt_level: OptLevel) -> Option<String> {
let ext = legacy_host_filename_ext(target, opt_level)?; let os = roc_target::OperatingSystem::from(target.operating_system);
let ext = legacy_host_filename_ext(os, opt_level);
Some(preprocessed_host_filename(target)?.replace(PRECOMPILED_HOST_EXT, ext)) Some(preprocessed_host_filename(target)?.replace(PRECOMPILED_HOST_EXT, ext))
} }
@ -215,6 +188,7 @@ pub fn build_zig_host_native(
target: &str, target: &str,
opt_level: OptLevel, opt_level: OptLevel,
shared_lib_path: Option<&Path>, shared_lib_path: Option<&Path>,
builtins_host_path: &Path,
) -> Command { ) -> Command {
let mut zig_cmd = zig(); let mut zig_cmd = zig();
zig_cmd zig_cmd
@ -226,18 +200,12 @@ pub fn build_zig_host_native(
// with LLVM, the builtins are already part of the roc app, // with LLVM, the builtins are already part of the roc app,
// but with the dev backend, they are missing. To minimize work, // but with the dev backend, they are missing. To minimize work,
// we link them as part of the host executable // we link them as part of the host executable
let builtins_obj = if target.contains("windows") {
bitcode::get_builtins_windows_obj_path()
} else {
bitcode::get_builtins_host_obj_path()
};
zig_cmd.args([ zig_cmd.args([
"build-exe", "build-exe",
"-fPIE", "-fPIE",
"-rdynamic", // make sure roc_alloc and friends are exposed "-rdynamic", // make sure roc_alloc and friends are exposed
shared_lib_path.to_str().unwrap(), shared_lib_path.to_str().unwrap(),
&builtins_obj, builtins_host_path.to_str().unwrap(),
]); ]);
} else { } else {
zig_cmd.args(["build-obj", "-fPIC"]); zig_cmd.args(["build-obj", "-fPIC"]);
@ -293,6 +261,7 @@ pub fn build_zig_host_native(
target: &str, target: &str,
opt_level: OptLevel, opt_level: OptLevel,
shared_lib_path: Option<&Path>, shared_lib_path: Option<&Path>,
builtins_host_path: &Path,
) -> Command { ) -> Command {
// to prevent `clang failed with stderr: zig: error: unable to make temporary file: No such file or directory` // to prevent `clang failed with stderr: zig: error: unable to make temporary file: No such file or directory`
let env_userprofile = env::var("USERPROFILE").unwrap_or_else(|_| "".to_string()); let env_userprofile = env::var("USERPROFILE").unwrap_or_else(|_| "".to_string());
@ -309,7 +278,7 @@ pub fn build_zig_host_native(
"build-exe", "build-exe",
// "-fPIE", PIE seems to fail on windows // "-fPIE", PIE seems to fail on windows
shared_lib_path.to_str().unwrap(), shared_lib_path.to_str().unwrap(),
&bitcode::get_builtins_windows_obj_path(), builtins_host_path.to_str().unwrap(),
]); ]);
} else { } else {
zig_cmd.args(&["build-obj"]); zig_cmd.args(&["build-obj"]);
@ -352,6 +321,7 @@ pub fn build_zig_host_native(
_target: &str, _target: &str,
opt_level: OptLevel, opt_level: OptLevel,
shared_lib_path: Option<&Path>, shared_lib_path: Option<&Path>,
builtins_host_path: &Path,
// For compatibility with the non-macOS def above. Keep these in sync. // For compatibility with the non-macOS def above. Keep these in sync.
) -> Command { ) -> Command {
use serde_json::Value; use serde_json::Value;
@ -404,7 +374,7 @@ pub fn build_zig_host_native(
"build-exe", "build-exe",
"-fPIE", "-fPIE",
shared_lib_path.to_str().unwrap(), shared_lib_path.to_str().unwrap(),
&bitcode::get_builtins_host_obj_path(), builtins_host_path.to_str().unwrap(),
]); ]);
} else { } else {
zig_cmd.args(&["build-obj"]); zig_cmd.args(&["build-obj"]);
@ -508,6 +478,7 @@ pub fn build_c_host_native(
sources: &[&str], sources: &[&str],
opt_level: OptLevel, opt_level: OptLevel,
shared_lib_path: Option<&Path>, shared_lib_path: Option<&Path>,
builtins_host_path: &Path,
) -> Command { ) -> Command {
let mut clang_cmd = clang(); let mut clang_cmd = clang();
clang_cmd clang_cmd
@ -534,6 +505,7 @@ pub fn build_c_host_native(
get_target_str(target), get_target_str(target),
opt_level, opt_level,
Some(shared_lib_path), Some(shared_lib_path),
builtins_host_path,
); );
} }
_ => { _ => {
@ -544,7 +516,7 @@ pub fn build_c_host_native(
// linking the built-ins led to a surgical linker bug for // linking the built-ins led to a surgical linker bug for
// optimized builds. Disabling until it is needed for dev // optimized builds. Disabling until it is needed for dev
// builds. // builds.
// &bitcode::get_builtins_host_obj_path(), // builtins_host_path,
"-fPIE", "-fPIE",
"-pie", "-pie",
"-lm", "-lm",
@ -640,19 +612,37 @@ pub fn rebuild_host(
roc_target::OperatingSystem::Wasi => "", roc_target::OperatingSystem::Wasi => "",
}; };
let host_dest = let host_dest = if matches!(target.architecture, Architecture::Wasm32) {
if shared_lib_path.is_none() || matches!(target.architecture, Architecture::Wasm32) { if matches!(opt_level, OptLevel::Development) {
host_input_path.with_file_name(legacy_host_filename(target, opt_level).unwrap()) host_input_path.with_extension("o")
} else { } else {
host_input_path host_input_path.with_extension("bc")
.with_file_name("dynhost") }
.with_extension(executable_extension) } else if shared_lib_path.is_some() {
}; host_input_path
.with_file_name("dynhost")
.with_extension(executable_extension)
} else {
host_input_path.with_file_name(legacy_host_filename(target, opt_level).unwrap())
};
let env_path = env::var("PATH").unwrap_or_else(|_| "".to_string()); let env_path = env::var("PATH").unwrap_or_else(|_| "".to_string());
let env_home = env::var("HOME").unwrap_or_else(|_| "".to_string()); let env_home = env::var("HOME").unwrap_or_else(|_| "".to_string());
let env_cpath = env::var("CPATH").unwrap_or_else(|_| "".to_string()); let env_cpath = env::var("CPATH").unwrap_or_else(|_| "".to_string());
let builtins_host_tempfile = {
#[cfg(windows)]
{
bitcode::host_windows_tempfile()
}
#[cfg(unix)]
{
bitcode::host_unix_tempfile()
}
}
.expect("failed to write host builtins object to tempfile");
if zig_host_src.exists() { if zig_host_src.exists() {
// Compile host.zig // Compile host.zig
@ -690,6 +680,7 @@ pub fn rebuild_host(
get_target_str(target), get_target_str(target),
opt_level, opt_level,
shared_lib_path, shared_lib_path,
builtins_host_tempfile.path(),
), ),
Architecture::X86_32(_) => build_zig_host_native( Architecture::X86_32(_) => build_zig_host_native(
&env_path, &env_path,
@ -700,8 +691,8 @@ pub fn rebuild_host(
"i386-linux-musl", "i386-linux-musl",
opt_level, opt_level,
shared_lib_path, shared_lib_path,
builtins_host_tempfile.path(),
), ),
Architecture::Aarch64(_) => build_zig_host_native( Architecture::Aarch64(_) => build_zig_host_native(
&env_path, &env_path,
&env_home, &env_home,
@ -711,11 +702,12 @@ pub fn rebuild_host(
target_zig_str(target), target_zig_str(target),
opt_level, opt_level,
shared_lib_path, shared_lib_path,
builtins_host_tempfile.path(),
), ),
_ => internal_error!("Unsupported architecture {:?}", target.architecture), _ => internal_error!("Unsupported architecture {:?}", target.architecture),
}; };
run_build_command(zig_cmd, "host.zig", 0) run_build_command(zig_cmd, "host.zig", 0);
} else if cargo_host_src.exists() { } else if cargo_host_src.exists() {
// Compile and link Cargo.toml, if it exists // Compile and link Cargo.toml, if it exists
let cargo_dir = host_input_path.parent().unwrap(); let cargo_dir = host_input_path.parent().unwrap();
@ -778,6 +770,7 @@ pub fn rebuild_host(
&[c_host_src.to_str().unwrap()], &[c_host_src.to_str().unwrap()],
opt_level, opt_level,
shared_lib_path, shared_lib_path,
builtins_host_tempfile.path(),
); );
run_build_command(clang_cmd, "host.c", 0); run_build_command(clang_cmd, "host.c", 0);
@ -832,6 +825,7 @@ pub fn rebuild_host(
], ],
opt_level, opt_level,
shared_lib_path, shared_lib_path,
builtins_host_tempfile.path(),
); );
run_build_command(clang_cmd, "host.c", 0); run_build_command(clang_cmd, "host.c", 0);
} else { } else {
@ -844,6 +838,7 @@ pub fn rebuild_host(
&[c_host_src.to_str().unwrap()], &[c_host_src.to_str().unwrap()],
opt_level, opt_level,
shared_lib_path, shared_lib_path,
builtins_host_tempfile.path(),
); );
run_build_command(clang_cmd, "host.c", 0); run_build_command(clang_cmd, "host.c", 0);
@ -879,6 +874,7 @@ pub fn rebuild_host(
&[c_host_src.to_str().unwrap()], &[c_host_src.to_str().unwrap()],
opt_level, opt_level,
shared_lib_path, shared_lib_path,
builtins_host_tempfile.path(),
); );
run_build_command(clang_cmd, "host.c", 0); run_build_command(clang_cmd, "host.c", 0);
@ -900,6 +896,10 @@ pub fn rebuild_host(
run_build_command(swiftc_cmd, "host.swift", 0); run_build_command(swiftc_cmd, "host.swift", 0);
} }
// Extend the lifetime of the tempfile so it doesn't get dropped
// (and thus deleted) before the build process is done using it!
let _ = builtins_host_tempfile;
host_dest host_dest
} }
@ -1452,10 +1452,13 @@ pub fn preprocess_host_wasm32(host_input_path: &Path, preprocessed_host_path: &P
(but seems to be an unofficial API) (but seems to be an unofficial API)
*/ */
let builtins_host_tempfile =
bitcode::host_wasm_tempfile().expect("failed to write host builtins object to tempfile");
let mut zig_cmd = zig(); let mut zig_cmd = zig();
let args = &[ let args = &[
"wasm-ld", "wasm-ld",
&bitcode::get_builtins_wasm32_obj_path(), builtins_host_tempfile.path().to_str().unwrap(),
host_input, host_input,
WASI_LIBC_PATH, WASI_LIBC_PATH,
WASI_COMPILER_RT_PATH, // builtins need __multi3, __udivti3, __fixdfti WASI_COMPILER_RT_PATH, // builtins need __multi3, __udivti3, __fixdfti
@ -1472,7 +1475,11 @@ pub fn preprocess_host_wasm32(host_input_path: &Path, preprocessed_host_path: &P
// println!("\npreprocess_host_wasm32"); // println!("\npreprocess_host_wasm32");
// println!("zig {}\n", args.join(" ")); // println!("zig {}\n", args.join(" "));
run_build_command(zig_cmd, output_file, 0) run_build_command(zig_cmd, output_file, 0);
// Extend the lifetime of the tempfile so it doesn't get dropped
// (and thus deleted) before the Zig process is done using it!
let _ = builtins_host_tempfile;
} }
fn run_build_command(mut command: Command, file_to_build: &str, flaky_fail_counter: usize) { fn run_build_command(mut command: Command, file_to_build: &str, flaky_fail_counter: usize) {

View file

@ -3,15 +3,12 @@ use roc_error_macros::internal_error;
use roc_gen_llvm::llvm::build::{module_from_builtins, LlvmBackendMode}; use roc_gen_llvm::llvm::build::{module_from_builtins, LlvmBackendMode};
use roc_gen_llvm::llvm::externs::add_default_roc_externs; use roc_gen_llvm::llvm::externs::add_default_roc_externs;
use roc_load::{EntryPoint, ExpectMetadata, LoadedModule, MonomorphizedModule}; use roc_load::{EntryPoint, ExpectMetadata, LoadedModule, MonomorphizedModule};
use roc_module::symbol::{Interns, ModuleId};
use roc_mono::ir::OptLevel; use roc_mono::ir::OptLevel;
use roc_region::all::LineInfo; use roc_reporting::cli::{report_problems, Problems};
use roc_solve_problem::TypeError;
use std::ops::Deref; use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use roc_collections::all::MutMap;
#[cfg(feature = "target-wasm32")] #[cfg(feature = "target-wasm32")]
use roc_collections::all::MutSet; use roc_collections::all::MutSet;
@ -21,7 +18,7 @@ pub struct CodeGenTiming {
} }
pub fn report_problems_monomorphized(loaded: &mut MonomorphizedModule) -> Problems { pub fn report_problems_monomorphized(loaded: &mut MonomorphizedModule) -> Problems {
report_problems_help( report_problems(
loaded.total_problems(), loaded.total_problems(),
&loaded.sources, &loaded.sources,
&loaded.interns, &loaded.interns,
@ -31,7 +28,7 @@ pub fn report_problems_monomorphized(loaded: &mut MonomorphizedModule) -> Proble
} }
pub fn report_problems_typechecked(loaded: &mut LoadedModule) -> Problems { pub fn report_problems_typechecked(loaded: &mut LoadedModule) -> Problems {
report_problems_help( report_problems(
loaded.total_problems(), loaded.total_problems(),
&loaded.sources, &loaded.sources,
&loaded.interns, &loaded.interns,
@ -40,123 +37,6 @@ pub fn report_problems_typechecked(loaded: &mut LoadedModule) -> Problems {
) )
} }
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Problems {
pub errors: usize,
pub warnings: usize,
}
impl Problems {
pub fn exit_code(&self) -> i32 {
// 0 means no problems, 1 means errors, 2 means warnings
if self.errors > 0 {
1
} else {
self.warnings.min(1) as i32
}
}
}
fn report_problems_help(
total_problems: usize,
sources: &MutMap<ModuleId, (PathBuf, Box<str>)>,
interns: &Interns,
can_problems: &mut MutMap<ModuleId, Vec<roc_problem::can::Problem>>,
type_problems: &mut MutMap<ModuleId, Vec<TypeError>>,
) -> Problems {
use roc_reporting::report::{
can_problem, type_problem, Report, RocDocAllocator, Severity::*, DEFAULT_PALETTE,
};
let palette = DEFAULT_PALETTE;
// This will often over-allocate total memory, but it means we definitely
// never need to re-allocate either the warnings or the errors vec!
let mut warnings = Vec::with_capacity(total_problems);
let mut errors = Vec::with_capacity(total_problems);
for (home, (module_path, src)) in sources.iter() {
let mut src_lines: Vec<&str> = Vec::new();
src_lines.extend(src.split('\n'));
let lines = LineInfo::new(&src_lines.join("\n"));
// Report parsing and canonicalization problems
let alloc = RocDocAllocator::new(&src_lines, *home, interns);
let problems = can_problems.remove(home).unwrap_or_default();
for problem in problems.into_iter() {
let report = can_problem(&alloc, &lines, module_path.clone(), problem);
let severity = report.severity;
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
match severity {
Warning => {
warnings.push(buf);
}
RuntimeError => {
errors.push(buf);
}
}
}
let problems = type_problems.remove(home).unwrap_or_default();
for problem in problems {
if let Some(report) = type_problem(&alloc, &lines, module_path.clone(), problem) {
let severity = report.severity;
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
match severity {
Warning => {
warnings.push(buf);
}
RuntimeError => {
errors.push(buf);
}
}
}
}
}
let problems_reported;
// Only print warnings if there are no errors
if errors.is_empty() {
problems_reported = warnings.len();
for warning in warnings.iter() {
println!("\n{}\n", warning);
}
} else {
problems_reported = errors.len();
for error in errors.iter() {
println!("\n{}\n", error);
}
}
// If we printed any problems, print a horizontal rule at the end,
// and then clear any ANSI escape codes (e.g. colors) we've used.
//
// The horizontal rule is nice when running the program right after
// compiling it, as it lets you clearly see where the compiler
// errors/warnings end and the program output begins.
if problems_reported > 0 {
println!("{}\u{001B}[0m\n", Report::horizontal_rule(&palette));
}
Problems {
errors: errors.len(),
warnings: warnings.len(),
}
}
pub enum CodeObject { pub enum CodeObject {
MemoryBuffer(MemoryBuffer), MemoryBuffer(MemoryBuffer),
Vector(Vec<u8>), Vector(Vec<u8>),

View file

@ -12,6 +12,7 @@ roc_region = { path = "../region" }
roc_module = { path = "../module" } roc_module = { path = "../module" }
roc_target = { path = "../roc_target" } roc_target = { path = "../roc_target" }
roc_utils = { path = "../../utils" } roc_utils = { path = "../../utils" }
tempfile.workspace = true
[build-dependencies] [build-dependencies]
# dunce can be removed once ziglang/zig#5109 is fixed # dunce can be removed once ziglang/zig#5109 is fixed
@ -19,4 +20,4 @@ dunce = "1.0.3"
roc_utils = { path = "../../utils" } roc_utils = { path = "../../utils" }
[target.'cfg(target_os = "macos")'.build-dependencies] [target.'cfg(target_os = "macos")'.build-dependencies]
tempfile = "3.2.0" tempfile.workspace = true

View file

@ -1,7 +1,4 @@
zig-cache zig-cache
src/zig-cache src/zig-cache
benchmark/zig-cache benchmark/zig-cache
builtins.ll
builtins.bc
builtins.o
dec dec

View file

@ -134,24 +134,17 @@ fn generate_bc_file(bitcode_path: &Path, zig_object: &str, file_name: &str) {
pub fn get_lib_dir() -> PathBuf { pub fn get_lib_dir() -> PathBuf {
// Currently we have the OUT_DIR variable which points to `/target/debug/build/roc_builtins-*/out/`. // Currently we have the OUT_DIR variable which points to `/target/debug/build/roc_builtins-*/out/`.
// So we just need to shed a 3 of the outer layers to get `/target/debug/` and then add `lib`. // So we just need to add "/bitcode" to that.
let out_dir = env::var_os("OUT_DIR").unwrap(); let dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("bitcode");
let lib_path = Path::new(&out_dir) // create dir if it does not exist
.parent() fs::create_dir_all(&dir).expect("Failed to make $OUT_DIR/bitcode dir.");
.and_then(|path| path.parent())
.and_then(|path| path.parent())
.unwrap()
.join("lib");
// create dir of it does not exist dir
fs::create_dir_all(lib_path.clone()).expect("Failed to make lib dir.");
lib_path
} }
fn copy_zig_builtins_to_target_dir(bitcode_path: &Path) { fn copy_zig_builtins_to_target_dir(bitcode_path: &Path) {
// To enable roc to find the zig biultins, we want them to be moved to a folder next to the roc executable. // To enable roc to find the zig builtins, we want them to be moved to a folder next to the roc executable.
// So if <roc_folder>/roc is the executable. The zig files will be in <roc_folder>/lib/*.zig // So if <roc_folder>/roc is the executable. The zig files will be in <roc_folder>/lib/*.zig
let target_profile_dir = get_lib_dir(); let target_profile_dir = get_lib_dir();

View file

@ -1,39 +1,55 @@
use roc_module::symbol::Symbol; use roc_module::symbol::Symbol;
use roc_target::TargetInfo; use roc_target::TargetInfo;
use roc_utils::get_lib_path;
use std::ops::Index; use std::ops::Index;
use tempfile::NamedTempFile;
const LIB_DIR_ERROR: &str = "Failed to find the lib directory. Did you copy the roc binary without also copying the lib directory?\nIf you built roc from source, the lib dir should be in target/release.\nIf not, the lib dir should be included in the release tar.gz file."; pub const HOST_WASM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/bitcode/builtins-wasm32.o"));
// TODO: in the future, we should use Zig's cross-compilation to generate and store these
// for all targets, so that we can do cross-compilation!
#[cfg(unix)]
pub const HOST_UNIX: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/bitcode/builtins-host.o"));
#[cfg(windows)]
pub const HOST_WINDOWS: &[u8] = include_bytes!(concat!(
env!("OUT_DIR"),
"/bitcode/builtins-windows-x86_64.obj"
));
pub fn get_builtins_host_obj_path() -> String { pub fn host_wasm_tempfile() -> std::io::Result<NamedTempFile> {
let builtins_host_path = get_lib_path().expect(LIB_DIR_ERROR).join("builtins-host.o"); let tempfile = tempfile::Builder::new()
.prefix("host_bitcode")
.suffix(".wasm")
.rand_bytes(8)
.tempfile()?;
builtins_host_path std::fs::write(tempfile.path(), HOST_WASM)?;
.into_os_string()
.into_string() Ok(tempfile)
.expect("Failed to convert builtins_host_path to str")
} }
pub fn get_builtins_windows_obj_path() -> String { #[cfg(unix)]
let builtins_host_path = get_lib_path() pub fn host_unix_tempfile() -> std::io::Result<NamedTempFile> {
.expect(LIB_DIR_ERROR) let tempfile = tempfile::Builder::new()
.join("builtins-windows-x86_64.obj"); .prefix("host_bitcode")
.suffix(".o")
.rand_bytes(8)
.tempfile()?;
builtins_host_path std::fs::write(tempfile.path(), HOST_UNIX)?;
.into_os_string()
.into_string() Ok(tempfile)
.expect("Failed to convert builtins_host_path to str")
} }
pub fn get_builtins_wasm32_obj_path() -> String { #[cfg(windows)]
let builtins_wasm32_path = get_lib_path() pub fn host_windows_tempfile() -> std::io::Result<NamedTempFile> {
.expect(LIB_DIR_ERROR) let tempfile = tempfile::Builder::new()
.join("builtins-wasm32.o"); .prefix("host_bitcode")
.suffix(".obj")
.rand_bytes(8)
.tempfile()?;
builtins_wasm32_path std::fs::write(tempfile.path(), HOST_WINDOWS)?;
.into_os_string()
.into_string() Ok(tempfile)
.expect("Failed to convert builtins_wasm32_path to str")
} }
#[derive(Debug, Default, Copy, Clone)] #[derive(Debug, Default, Copy, Clone)]

View file

@ -88,6 +88,14 @@ impl<T: PartialEq> VecSet<T> {
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.elements.clear() self.elements.clear()
} }
/// Retains only the elements specified by the predicate.
pub fn retain<F>(&mut self, f: F)
where
F: FnMut(&T) -> bool,
{
self.elements.retain(f)
}
} }
impl<A: Ord> Extend<A> for VecSet<A> { impl<A: Ord> Extend<A> for VecSet<A> {

View file

@ -1815,7 +1815,6 @@ fn constrain_function_def(
signature_closure_type_index, signature_closure_type_index,
)) ))
}; };
let signature_index = constraints.push_type(types, signature);
let cons = [ let cons = [
constraints.let_constraint( constraints.let_constraint(
[], [],
@ -3308,8 +3307,6 @@ fn constraint_recursive_function(
let loc_pattern = Loc::at(loc_symbol.region, Pattern::Identifier(loc_symbol.value)); let loc_pattern = Loc::at(loc_symbol.region, Pattern::Identifier(loc_symbol.value));
flex_info.vars.extend(new_infer_variables);
let signature_index = constraints.push_type(types, signature); let signature_index = constraints.push_type(types, signature);
let annotation_expected = constraints.push_expected_type(FromAnnotation( let annotation_expected = constraints.push_expected_type(FromAnnotation(
@ -3433,14 +3430,38 @@ fn constraint_recursive_function(
let def_con = constraints.exists(vars, and_constraint); let def_con = constraints.exists(vars, and_constraint);
rigid_info.vars.extend(&new_rigid_variables); rigid_info.vars.extend(&new_rigid_variables);
flex_info.vars.extend(&new_infer_variables);
rigid_info.constraints.push(constraints.let_constraint( rigid_info.constraints.push({
new_rigid_variables, // Solve the body of the recursive function, making sure it lines up with the
def_pattern_state.vars, // signature.
[], // no headers introduced (at this level) //
def_con, // This happens when we're checking that the def of a recursive function actually
Constraint::True, // aligns with what the (mutually-)recursive signature says, so finish
)); // generalization of the function.
let rigids = new_rigid_variables;
let flex = def_pattern_state
.vars
.into_iter()
.chain(new_infer_variables);
constraints.let_constraint(
rigids,
flex,
// Although we will have already introduced the headers of the def in the
// outermost scope when we introduced the rigid variables, we now re-introduce
// them to force an occurs check and appropriate fixing, since we might end up
// inferring recursive types at inference variable points. E.g.
//
// f : _ -> _
// f = \F c -> F (List.map f c)
//
// TODO: I (Ayaz) believe we can considerably simplify all this.
def_pattern_state.headers.clone(),
def_con,
Constraint::True,
)
});
rigid_info.def_types.extend(def_pattern_state.headers); rigid_info.def_types.extend(def_pattern_state.headers);
} }
} }

View file

@ -33,13 +33,27 @@
//! These flags are also set in .cargo/config found at the repository root. You can modify them //! These flags are also set in .cargo/config found at the repository root. You can modify them
//! there to avoid maintaining a separate script. //! there to avoid maintaining a separate script.
#[macro_export]
macro_rules! dbg_set {
($flag:path) => {{
#[cfg(not(debug_assertions))]
{
false
}
#[cfg(debug_assertions)]
{
let flag = std::env::var($flag);
flag.is_ok() && flag.as_deref() != Ok("0")
}
}};
}
#[macro_export] #[macro_export]
macro_rules! dbg_do { macro_rules! dbg_do {
($flag:path, $expr:expr) => { ($flag:path, $expr:expr) => {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
let flag = std::env::var($flag); if $crate::dbg_set!($flag) {
if !flag.is_err() && flag.as_deref() != Ok("0") {
$expr $expr
} }
} }
@ -91,6 +105,15 @@ flags! {
/// chainging how defs are constrained. /// chainging how defs are constrained.
ROC_VERIFY_RIGID_LET_GENERALIZED ROC_VERIFY_RIGID_LET_GENERALIZED
/// Verifies that an `occurs` check indeed only contains non-recursive types that need to be
/// fixed-up.
///
/// This flag is disabled by default because an occurs check may pass through an inferred
/// partially-recursive structure if a part of that structure also has type errors. However, in
/// the presence of programs without type errors, occurs checks should always consist of only
/// non-recursive types, and this flag should pass.
ROC_VERIFY_OCCURS_RECURSION
// ===Mono=== // ===Mono===
/// Writes a pretty-printed mono IR to stderr after function specialization. /// Writes a pretty-printed mono IR to stderr after function specialization.

View file

@ -15,6 +15,7 @@ roc_mono = { path = "../mono" }
roc_target = { path = "../roc_target" } roc_target = { path = "../roc_target" }
roc_std = { path = "../../roc_std" } roc_std = { path = "../../roc_std" }
roc_error_macros = { path = "../../error_macros" } roc_error_macros = { path = "../../error_macros" }
roc_wasm_module = { path = "../../wasm_module" }
bitvec.workspace = true bitvec.workspace = true
bumpalo.workspace = true bumpalo.workspace = true

View file

@ -1,36 +1,5 @@
# Development backend for WebAssembly # Development backend for WebAssembly
## Plan
- Initial bringup
- [x] Get a wasm backend working for some of the number tests.
- [x] Use a separate `gen_wasm` directory for now, to avoid trying to do bringup and integration at the same time.
- Get the fundamentals working
- [x] Come up with a way to do control flow
- [x] Flesh out the details of value representations between local variables and stack memory
- [x] Set up a way to write tests with any return value rather than just i64 and f64
- [x] Implement stack memory
- [x] Push and pop stack frames
- [x] Deal with returning structs
- [x] Distinguish which variables go in locals, own stack frame, caller stack frame, etc.
- [x] Ensure early Return statements don't skip stack cleanup
- [x] Model the stack machine as a storage mechanism, to make generated code "less bad"
- [x] Switch vectors to `bumpalo::Vec` where possible
- [ ] Implement relocations
- Requires knowing the _byte_ offset of each call site. This is awkward as the backend builds a `Vec<Instruction>` rather than a `Vec<u8>`. It may be worth serialising each instruction as it is inserted.
- Refactor for code sharing with CPU backends
- [ ] Extract a trait from `WasmBackend` that looks as similar as possible to `Backend`, to prepare for code sharing
- [ ] Refactor to actually share code between `WasmBackend` and `Backend` if it seems feasible
- Integration
- Move wasm files to `gen_dev/src/wasm`
- Share tests between wasm and x64, with some way of saying which tests work on which backends, and dispatching to different eval helpers based on that.
- Get `build_module` in object_builder.rs to dispatch to the wasm generator (adding some Wasm options to the `Triple` struct)
- Get `build_module` to write to a file, or maybe return `Vec<u8>`, instead of returning an Object structure
## Structured control flow ## Structured control flow
One of the security features of WebAssembly is that it does not allow unrestricted "jumps" to anywhere you like. It does not have an instruction for that. All of the [control instructions][control-inst] can only implement "structured" control flow, and have names like `if`, `loop`, `block` that you'd normally associate with high-level languages. There are branch (`br`) instructions that can jump to labelled blocks within the same function, but the blocks have to be nested in sensible ways. One of the security features of WebAssembly is that it does not allow unrestricted "jumps" to anywhere you like. It does not have an instruction for that. All of the [control instructions][control-inst] can only implement "structured" control flow, and have names like `if`, `loop`, `block` that you'd normally associate with high-level languages. There are branch (`br`) instructions that can jump to labelled blocks within the same function, but the blocks have to be nested in sensible ways.
@ -41,19 +10,6 @@ This way of representing control flow is similar to parts of the Roc AST like `W
Our solution is to wrap all joinpoint/jump graphs in an outer `loop`, with nested `block`s inside it. Our solution is to wrap all joinpoint/jump graphs in an outer `loop`, with nested `block`s inside it.
### Possible future optimisations
There are other algorithms available that may result in more optimised control flow. We are not focusing on that for our development backend, but here are some notes for future reference.
The WebAssembly compiler toolkit `binaryen` has an [API for control-flow graphs][cfg-api]. We're not using `binaryen` right now. It's a C++ library, though it does have a (very thin and somewhat hard-to-use) [Rust wrapper][binaryen-rs]. Binaryen's control-flow graph API implements the "Relooper" algorithm developed by the Emscripten project and described in [this paper](https://github.com/emscripten-core/emscripten/blob/main/docs/paper.pdf).
> By the way, apparently "binaryen" rhymes with "Targaryen", the family name from the "Game of Thrones" TV series
There is also an improvement on Relooper called ["Stackifier"](https://medium.com/leaningtech/solving-the-structured-control-flow-problem-once-and-for-all-5123117b1ee2). It can reorder the joinpoints and jumps to make code more efficient. (It is also has things Roc wouldn't need but C++ does, like support for "irreducible" graphs that include `goto`).
[cfg-api]: https://github.com/WebAssembly/binaryen/wiki/Compiling-to-WebAssembly-with-Binaryen#cfg-api
[binaryen-rs]: https://crates.io/crates/binaryen
## Stack machine vs register machine ## Stack machine vs register machine
Wasm's instruction set is based on a stack-machine VM. Whereas CPU instructions have named registers that they operate on, Wasm has no named registers at all. The instructions don't contain register names. Instructions can only operate on whatever data is at the top of the stack. Wasm's instruction set is based on a stack-machine VM. Whereas CPU instructions have named registers that they operate on, Wasm has no named registers at all. The instructions don't contain register names. Instructions can only operate on whatever data is at the top of the stack.

View file

@ -1,7 +1,6 @@
use bitvec::vec::BitVec; use bitvec::vec::BitVec;
use bumpalo::collections::{String, Vec}; use bumpalo::collections::{String, Vec};
use code_builder::Align;
use roc_builtins::bitcode::{FloatWidth, IntWidth}; use roc_builtins::bitcode::{FloatWidth, IntWidth};
use roc_collections::all::MutMap; use roc_collections::all::MutMap;
use roc_error_macros::internal_error; use roc_error_macros::internal_error;
@ -15,20 +14,22 @@ use roc_mono::ir::{
use roc_mono::layout::{Builtin, Layout, LayoutIds, TagIdIntType, UnionLayout}; use roc_mono::layout::{Builtin, Layout, LayoutIds, TagIdIntType, UnionLayout};
use roc_std::RocDec; use roc_std::RocDec;
use crate::layout::{CallConv, ReturnMethod, WasmLayout}; use roc_wasm_module::linking::{DataSymbol, WasmObjectSymbol};
use crate::low_level::{call_higher_order_lowlevel, LowLevelCall}; use roc_wasm_module::sections::{
use crate::storage::{AddressValue, Storage, StoredValue, StoredVarKind};
use crate::wasm_module::linking::{DataSymbol, WasmObjectSymbol};
use crate::wasm_module::sections::{
ConstExpr, DataMode, DataSegment, Export, Global, GlobalType, Import, ImportDesc, Limits, ConstExpr, DataMode, DataSegment, Export, Global, GlobalType, Import, ImportDesc, Limits,
MemorySection, NameSection, MemorySection, NameSection,
}; };
use crate::wasm_module::{ use roc_wasm_module::{
code_builder, CodeBuilder, ExportType, LocalId, Signature, SymInfo, ValueType, WasmModule, round_up_to_alignment, Align, ExportType, LocalId, Signature, SymInfo, ValueType, WasmModule,
}; };
use crate::code_builder::CodeBuilder;
use crate::layout::{CallConv, ReturnMethod, WasmLayout};
use crate::low_level::{call_higher_order_lowlevel, LowLevelCall};
use crate::storage::{AddressValue, Storage, StoredValue, StoredVarKind};
use crate::{ use crate::{
copy_memory, round_up_to_alignment, CopyMemoryConfig, Env, DEBUG_SETTINGS, MEMORY_NAME, copy_memory, CopyMemoryConfig, Env, DEBUG_SETTINGS, MEMORY_NAME, PTR_SIZE, PTR_TYPE,
PTR_SIZE, PTR_TYPE, TARGET_INFO, TARGET_INFO,
}; };
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -55,7 +56,8 @@ pub struct WasmBackend<'a> {
module: WasmModule<'a>, module: WasmModule<'a>,
layout_ids: LayoutIds<'a>, layout_ids: LayoutIds<'a>,
pub fn_index_offset: u32, pub fn_index_offset: u32,
called_preload_fns: BitVec<usize>, import_fn_count: u32,
called_fns: BitVec<usize>,
pub proc_lookup: Vec<'a, ProcLookupData<'a>>, pub proc_lookup: Vec<'a, ProcLookupData<'a>>,
host_lookup: Vec<'a, (&'a str, u32)>, host_lookup: Vec<'a, (&'a str, u32)>,
helper_proc_gen: CodeGenHelp<'a>, helper_proc_gen: CodeGenHelp<'a>,
@ -103,10 +105,12 @@ impl<'a> WasmBackend<'a> {
} }
module.link_host_to_app_calls(env.arena, host_to_app_map); module.link_host_to_app_calls(env.arena, host_to_app_map);
module.code.code_builders.reserve(proc_lookup.len()); let import_fn_count = module.import.function_count();
let host_function_count = import_fn_count
let host_function_count = module.import.imports.len() + module.code.dead_import_dummy_count as usize
+ (module.code.dead_import_dummy_count + module.code.preloaded_count) as usize; + module.code.function_count as usize;
let mut called_fns = BitVec::repeat(false, host_function_count);
called_fns.extend(std::iter::repeat(true).take(proc_lookup.len()));
WasmBackend { WasmBackend {
env, env,
@ -114,10 +118,10 @@ impl<'a> WasmBackend<'a> {
// Module-level data // Module-level data
module, module,
layout_ids, layout_ids,
fn_index_offset, fn_index_offset,
called_preload_fns: BitVec::repeat(false, host_function_count), import_fn_count: import_fn_count as u32,
called_fns,
proc_lookup, proc_lookup,
host_lookup, host_lookup,
helper_proc_gen, helper_proc_gen,
@ -211,7 +215,7 @@ impl<'a> WasmBackend<'a> {
self.module.data.end_addr += PTR_SIZE; self.module.data.end_addr += PTR_SIZE;
self.module.reloc_code.apply_relocs_u32( self.module.reloc_code.apply_relocs_u32(
&mut self.module.code.preloaded_bytes, &mut self.module.code.bytes,
sym_index as u32, sym_index as u32,
global_value_addr, global_value_addr,
); );
@ -261,6 +265,8 @@ impl<'a> WasmBackend<'a> {
source, source,
}); });
self.called_fns.push(true);
let linker_symbol = SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { let linker_symbol = SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed {
flags: 0, flags: 0,
index: wasm_fn_index, index: wasm_fn_index,
@ -278,7 +284,7 @@ impl<'a> WasmBackend<'a> {
self.maybe_call_host_main(); self.maybe_call_host_main();
let fn_table_size = 1 + self.module.element.max_table_index(); let fn_table_size = 1 + self.module.element.max_table_index();
self.module.table.function_table.limits = Limits::MinMax(fn_table_size, fn_table_size); self.module.table.function_table.limits = Limits::MinMax(fn_table_size, fn_table_size);
(self.module, self.called_preload_fns) (self.module, self.called_fns)
} }
/// If the host has a `main` function then we need to insert a `_start` to call it. /// If the host has a `main` function then we need to insert a `_start` to call it.
@ -319,7 +325,7 @@ impl<'a> WasmBackend<'a> {
self.module.export.append(Export { self.module.export.append(Export {
name: START, name: START,
ty: ExportType::Func, ty: ExportType::Func,
index: self.fn_index_offset + self.module.code.code_builders.len() as u32, index: self.module.code.function_count,
}); });
self.code_builder.i32_const(0); // argc=0 self.code_builder.i32_const(0); // argc=0
@ -329,7 +335,7 @@ impl<'a> WasmBackend<'a> {
self.code_builder.build_fn_header_and_footer(&[], 0, None); self.code_builder.build_fn_header_and_footer(&[], 0, None);
self.reset(); self.reset();
self.called_preload_fns.set(main_fn_index as usize, true); self.called_fns.set(main_fn_index as usize, true);
} }
/// Register the debug names of Symbols in a global lookup table /// Register the debug names of Symbols in a global lookup table
@ -363,11 +369,8 @@ impl<'a> WasmBackend<'a> {
/// Reset function-level data /// Reset function-level data
fn reset(&mut self) { fn reset(&mut self) {
// Push the completed CodeBuilder into the module and swap it for a new empty one self.code_builder.insert_into_module(&mut self.module);
let mut swap_code_builder = CodeBuilder::new(self.env.arena); self.code_builder.clear();
std::mem::swap(&mut swap_code_builder, &mut self.code_builder);
self.module.code.code_builders.push(swap_code_builder);
self.storage.clear(); self.storage.clear();
self.joinpoint_label_map.clear(); self.joinpoint_label_map.clear();
assert_eq!(self.block_depth, 0); assert_eq!(self.block_depth, 0);
@ -1340,10 +1343,9 @@ impl<'a> WasmBackend<'a> {
.find(|(fn_name, _)| *fn_name == name) .find(|(fn_name, _)| *fn_name == name)
.unwrap_or_else(|| panic!("The Roc app tries to call `{}` but I can't find it!", name)); .unwrap_or_else(|| panic!("The Roc app tries to call `{}` but I can't find it!", name));
self.called_preload_fns.set(*fn_index as usize, true); self.called_fns.set(*fn_index as usize, true);
let host_import_count = self.fn_index_offset - self.module.code.preloaded_count; if *fn_index < self.import_fn_count {
if *fn_index < host_import_count {
self.code_builder self.code_builder
.call_import(*fn_index, num_wasm_args, has_return_val); .call_import(*fn_index, num_wasm_args, has_return_val);
} else { } else {

View file

@ -1,53 +1,25 @@
use bumpalo::collections::vec::Vec; use bumpalo::collections::vec::Vec;
use bumpalo::Bump; use bumpalo::Bump;
use core::panic; use core::panic;
use roc_wasm_module::linking::IndexRelocType;
use roc_error_macros::internal_error; use roc_error_macros::internal_error;
use roc_module::symbol::Symbol; use roc_module::symbol::Symbol;
use roc_wasm_module::opcodes::{OpCode, OpCode::*};
use super::opcodes::{OpCode, OpCode::*}; use roc_wasm_module::serialize::SerialBuffer;
use super::serialize::{SerialBuffer, Serialize}; use roc_wasm_module::{
use crate::{ round_up_to_alignment, Align, LocalId, RelocationEntry, ValueType, WasmModule,
round_up_to_alignment, DEBUG_SETTINGS, FRAME_ALIGNMENT_BYTES, STACK_POINTER_GLOBAL_ID, FRAME_ALIGNMENT_BYTES, STACK_POINTER_GLOBAL_ID,
}; };
use crate::DEBUG_SETTINGS;
macro_rules! log_instruction { macro_rules! log_instruction {
($($x: expr),+) => { ($($x: expr),+) => {
if DEBUG_SETTINGS.instructions { println!($($x,)*); } if DEBUG_SETTINGS.instructions { println!($($x,)*); }
}; };
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LocalId(pub u32);
/// Wasm value type. (Rust representation matches Wasm encoding)
#[repr(u8)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum ValueType {
I32 = 0x7f,
I64 = 0x7e,
F32 = 0x7d,
F64 = 0x7c,
}
impl Serialize for ValueType {
fn serialize<T: SerialBuffer>(&self, buffer: &mut T) {
buffer.append_u8(*self as u8);
}
}
impl From<u8> for ValueType {
fn from(x: u8) -> Self {
match x {
0x7f => Self::I32,
0x7e => Self::I64,
0x7d => Self::F32,
0x7c => Self::F64,
_ => internal_error!("Invalid ValueType 0x{:02x}", x),
}
}
}
const BLOCK_NO_RESULT: u8 = 0x40; const BLOCK_NO_RESULT: u8 = 0x40;
/// A control block in our model of the VM /// A control block in our model of the VM
@ -65,52 +37,6 @@ impl std::fmt::Debug for VmBlock<'_> {
} }
} }
/// Wasm memory alignment for load/store instructions.
/// Rust representation matches Wasm encoding.
/// It's an error to specify alignment higher than the "natural" alignment of the instruction
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
pub enum Align {
Bytes1 = 0,
Bytes2 = 1,
Bytes4 = 2,
Bytes8 = 3,
}
impl Align {
/// Calculate the largest possible alignment for a load/store at a given stack frame offset
/// Assumes the stack frame is aligned to at least 8 bytes
pub fn from_stack_offset(max_align: Align, offset: u32) -> Align {
if (max_align == Align::Bytes8) && (offset & 7 == 0) {
return Align::Bytes8;
}
if (max_align >= Align::Bytes4) && (offset & 3 == 0) {
return Align::Bytes4;
}
if (max_align >= Align::Bytes2) && (offset & 1 == 0) {
return Align::Bytes2;
}
Align::Bytes1
}
}
impl From<u32> for Align {
fn from(x: u32) -> Align {
match x {
1 => Align::Bytes1,
2 => Align::Bytes2,
4 => Align::Bytes4,
_ => {
if x.count_ones() == 1 {
Align::Bytes8 // Max value supported by any Wasm instruction
} else {
internal_error!("Cannot align to {} bytes", x);
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Copy)] #[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum VmSymbolState { pub enum VmSymbolState {
/// Value doesn't exist yet /// Value doesn't exist yet
@ -184,12 +110,6 @@ pub struct CodeBuilder<'a> {
import_relocations: Vec<'a, (usize, u32)>, import_relocations: Vec<'a, (usize, u32)>,
} }
impl<'a> Serialize for CodeBuilder<'a> {
fn serialize<T: SerialBuffer>(&self, buffer: &mut T) {
self.serialize_without_relocs(buffer);
}
}
#[allow(clippy::new_without_default)] #[allow(clippy::new_without_default)]
impl<'a> CodeBuilder<'a> { impl<'a> CodeBuilder<'a> {
pub fn new(arena: &'a Bump) -> Self { pub fn new(arena: &'a Bump) -> Self {
@ -212,29 +132,16 @@ impl<'a> CodeBuilder<'a> {
} }
} }
/********************************************************** pub fn clear(&mut self) {
self.code.clear();
self.insertions.clear();
self.insert_bytes.clear();
self.preamble.clear();
self.inner_length.clear();
self.import_relocations.clear();
LINKING self.vm_block_stack.truncate(1);
self.vm_block_stack[0].value_stack.clear();
***********************************************************/
/// Build a dummy function with just a single `unreachable` instruction
pub fn dummy(arena: &'a Bump) -> Self {
let mut builder = Self::new(arena);
builder.unreachable_();
builder.build_fn_header_and_footer(&[], 0, None);
builder
}
pub fn apply_import_relocs(&mut self, live_import_fns: &[usize]) {
for (code_index, fn_index) in self.import_relocations.iter() {
for (new_index, old_index) in live_import_fns.iter().enumerate() {
if *fn_index as usize == *old_index {
self.code
.overwrite_padded_u32(*code_index, new_index as u32);
}
}
}
} }
/********************************************************** /**********************************************************
@ -534,19 +441,52 @@ impl<'a> CodeBuilder<'a> {
} }
/// Serialize all byte vectors in the right order /// Serialize all byte vectors in the right order
/// Also update relocation offsets relative to the base offset (code section body start) /// Insert relocations for imported functions
pub fn serialize_without_relocs<T: SerialBuffer>(&self, buffer: &mut T) { pub fn insert_into_module(&self, module: &mut WasmModule<'a>) {
buffer.append_slice(&self.inner_length); let fn_offset = module.code.bytes.len();
buffer.append_slice(&self.preamble); module.code.function_count += 1;
module.code.function_offsets.push(fn_offset as u32);
// Insertions are chunks of code we generated out-of-order.
// Now insert them at the correct offsets.
let buffer = &mut module.code.bytes;
buffer.extend_from_slice(&self.inner_length);
buffer.extend_from_slice(&self.preamble);
let code_offset = buffer.len();
let mut code_pos = 0; let mut code_pos = 0;
for Insertion { at, start, end } in self.insertions.iter() { for Insertion { at, start, end } in self.insertions.iter() {
buffer.append_slice(&self.code[code_pos..(*at)]); buffer.extend_from_slice(&self.code[code_pos..*at]);
buffer.append_slice(&self.insert_bytes[*start..*end]);
code_pos = *at; code_pos = *at;
buffer.extend_from_slice(&self.insert_bytes[*start..*end]);
} }
buffer.append_slice(&self.code[code_pos..self.code.len()]); buffer.extend_from_slice(&self.code[code_pos..self.code.len()]);
// Create linker relocations for calls to imported functions, whose indices may change during DCE.
let relocs = &mut module.reloc_code.entries;
let mut skip = 0;
for (reloc_code_pos, reloc_fn) in self.import_relocations.iter() {
let mut insertion_bytes = 0;
for (i, insertion) in self.insertions.iter().enumerate().skip(skip) {
if insertion.at >= *reloc_code_pos {
break;
}
insertion_bytes = insertion.end;
skip = i;
}
// Adjust for (1) the offset of this function in the Code section and (2) our own Insertions.
let offset = reloc_code_pos + code_offset + insertion_bytes;
let symbol_index = module
.linking
.find_imported_fn_sym_index(*reloc_fn)
.unwrap();
relocs.push(RelocationEntry::Index {
type_id: IndexRelocType::FunctionIndexLeb,
offset: offset as u32,
symbol_index,
});
}
} }
/********************************************************** /**********************************************************

View file

@ -1,8 +1,8 @@
use roc_builtins::bitcode::{FloatWidth, IntWidth}; use roc_builtins::bitcode::{FloatWidth, IntWidth};
use roc_mono::layout::{Layout, STLayoutInterner, UnionLayout}; use roc_mono::layout::{Layout, STLayoutInterner, UnionLayout};
use crate::wasm_module::ValueType;
use crate::{PTR_SIZE, PTR_TYPE, TARGET_INFO}; use crate::{PTR_SIZE, PTR_TYPE, TARGET_INFO};
use roc_wasm_module::ValueType;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReturnMethod { pub enum ReturnMethod {

View file

@ -1,9 +1,9 @@
//! Provides the WASM backend to generate Roc binaries. //! Provides the WASM backend to generate Roc binaries.
mod backend; mod backend;
mod code_builder;
mod layout; mod layout;
mod low_level; mod low_level;
mod storage; mod storage;
pub mod wasm_module;
// Helpers for interfacing to a Wasm module from outside // Helpers for interfacing to a Wasm module from outside
pub mod wasm32_result; pub mod wasm32_result;
@ -19,10 +19,11 @@ use roc_mono::code_gen_help::CodeGenHelp;
use roc_mono::ir::{Proc, ProcLayout}; use roc_mono::ir::{Proc, ProcLayout};
use roc_mono::layout::{LayoutIds, STLayoutInterner}; use roc_mono::layout::{LayoutIds, STLayoutInterner};
use roc_target::TargetInfo; use roc_target::TargetInfo;
use wasm_module::parse::ParseError; use roc_wasm_module::parse::ParseError;
use roc_wasm_module::{Align, LocalId, ValueType, WasmModule};
use crate::backend::{ProcLookupData, ProcSource, WasmBackend}; use crate::backend::{ProcLookupData, ProcSource, WasmBackend};
use crate::wasm_module::{Align, CodeBuilder, LocalId, ValueType, WasmModule}; use crate::code_builder::CodeBuilder;
const TARGET_INFO: TargetInfo = TargetInfo::default_wasm32(); const TARGET_INFO: TargetInfo = TargetInfo::default_wasm32();
const PTR_SIZE: u32 = { const PTR_SIZE: u32 = {
@ -36,8 +37,6 @@ const PTR_SIZE: u32 = {
}; };
const PTR_TYPE: ValueType = ValueType::I32; const PTR_TYPE: ValueType = ValueType::I32;
pub const STACK_POINTER_GLOBAL_ID: u32 = 0;
pub const FRAME_ALIGNMENT_BYTES: i32 = 16;
pub const MEMORY_NAME: &str = "memory"; pub const MEMORY_NAME: &str = "memory";
pub const BUILTINS_IMPORT_MODULE_NAME: &str = "env"; pub const BUILTINS_IMPORT_MODULE_NAME: &str = "env";
pub const STACK_POINTER_NAME: &str = "__stack_pointer"; pub const STACK_POINTER_NAME: &str = "__stack_pointer";
@ -71,10 +70,9 @@ pub fn build_app_binary<'a>(
host_module: WasmModule<'a>, host_module: WasmModule<'a>,
procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>,
) -> std::vec::Vec<u8> { ) -> std::vec::Vec<u8> {
let (mut wasm_module, called_preload_fns, _) = let (mut wasm_module, called_fns, _) = build_app_module(env, interns, host_module, procedures);
build_app_module(env, interns, host_module, procedures);
wasm_module.eliminate_dead_code(env.arena, called_preload_fns); wasm_module.eliminate_dead_code(env.arena, called_fns);
let mut buffer = std::vec::Vec::with_capacity(wasm_module.size()); let mut buffer = std::vec::Vec::with_capacity(wasm_module.size());
wasm_module.serialize(&mut buffer); wasm_module.serialize(&mut buffer);
@ -99,7 +97,7 @@ pub fn build_app_module<'a>(
// Adjust Wasm function indices to account for functions from the object file // Adjust Wasm function indices to account for functions from the object file
let fn_index_offset: u32 = let fn_index_offset: u32 =
host_module.import.function_count() as u32 + host_module.code.preloaded_count; host_module.import.function_count() as u32 + host_module.code.function_count;
// Pre-pass over the procedure names & layouts // Pre-pass over the procedure names & layouts
// Create a lookup to tell us the final index of each proc in the output file // Create a lookup to tell us the final index of each proc in the output file
@ -186,11 +184,11 @@ pub fn build_app_module<'a>(
} }
} }
let (module, called_preload_fns) = backend.finalize(); let (module, called_fns) = backend.finalize();
let main_function_index = let main_function_index =
maybe_main_fn_index.expect("The app must expose at least one value to the host"); maybe_main_fn_index.expect("The app must expose at least one value to the host");
(module, called_preload_fns, main_function_index) (module, called_fns, main_function_index)
} }
pub struct CopyMemoryConfig { pub struct CopyMemoryConfig {
@ -235,26 +233,6 @@ pub fn copy_memory(code_builder: &mut CodeBuilder, config: CopyMemoryConfig) {
} }
} }
/// Round up to alignment_bytes (which must be a power of 2)
#[macro_export]
macro_rules! round_up_to_alignment {
($unaligned: expr, $alignment_bytes: expr) => {
if $alignment_bytes <= 1 {
$unaligned
} else if $alignment_bytes.count_ones() != 1 {
internal_error!(
"Cannot align to {} bytes. Not a power of 2.",
$alignment_bytes
);
} else {
let mut aligned = $unaligned;
aligned += $alignment_bytes - 1; // if lower bits are non-zero, push it over the next boundary
aligned &= !$alignment_bytes + 1; // mask with a flag that has upper bits 1, lower bits 0
aligned
}
};
}
pub struct WasmDebugSettings { pub struct WasmDebugSettings {
proc_start_end: bool, proc_start_end: bool,
user_procs_ir: bool, user_procs_ir: bool,
@ -263,7 +241,6 @@ pub struct WasmDebugSettings {
instructions: bool, instructions: bool,
storage_map: bool, storage_map: bool,
pub keep_test_binary: bool, pub keep_test_binary: bool,
pub skip_dead_code_elim: bool,
} }
pub const DEBUG_SETTINGS: WasmDebugSettings = WasmDebugSettings { pub const DEBUG_SETTINGS: WasmDebugSettings = WasmDebugSettings {
@ -274,7 +251,6 @@ pub const DEBUG_SETTINGS: WasmDebugSettings = WasmDebugSettings {
instructions: false && cfg!(debug_assertions), instructions: false && cfg!(debug_assertions),
storage_map: false && cfg!(debug_assertions), storage_map: false && cfg!(debug_assertions),
keep_test_binary: false && cfg!(debug_assertions), keep_test_binary: false && cfg!(debug_assertions),
skip_dead_code_elim: false && cfg!(debug_assertions),
}; };
#[cfg(test)] #[cfg(test)]

View file

@ -12,8 +12,8 @@ use roc_mono::low_level::HigherOrder;
use crate::backend::{ProcLookupData, ProcSource, WasmBackend}; use crate::backend::{ProcLookupData, ProcSource, WasmBackend};
use crate::layout::{CallConv, StackMemoryFormat, WasmLayout}; use crate::layout::{CallConv, StackMemoryFormat, WasmLayout};
use crate::storage::{AddressValue, StackMemoryLocation, StoredValue}; use crate::storage::{AddressValue, StackMemoryLocation, StoredValue};
use crate::wasm_module::{Align, LocalId, ValueType};
use crate::{PTR_TYPE, TARGET_INFO}; use crate::{PTR_TYPE, TARGET_INFO};
use roc_wasm_module::{Align, LocalId, ValueType};
/// Number types used for Wasm code gen /// Number types used for Wasm code gen
/// Unlike other enums, this contains no details about layout or storage. /// Unlike other enums, this contains no details about layout or storage.

View file

@ -6,9 +6,10 @@ use roc_error_macros::internal_error;
use roc_module::symbol::Symbol; use roc_module::symbol::Symbol;
use roc_mono::layout::{Layout, STLayoutInterner}; use roc_mono::layout::{Layout, STLayoutInterner};
use crate::code_builder::{CodeBuilder, VmSymbolState};
use crate::layout::{CallConv, ReturnMethod, StackMemoryFormat, WasmLayout}; use crate::layout::{CallConv, ReturnMethod, StackMemoryFormat, WasmLayout};
use crate::wasm_module::{Align, CodeBuilder, LocalId, ValueType, VmSymbolState}; use crate::{copy_memory, CopyMemoryConfig, PTR_TYPE};
use crate::{copy_memory, round_up_to_alignment, CopyMemoryConfig, PTR_TYPE}; use roc_wasm_module::{round_up_to_alignment, Align, LocalId, ValueType};
pub enum StoredVarKind { pub enum StoredVarKind {
Variable, Variable,
@ -592,7 +593,7 @@ impl<'a> Storage<'a> {
| StoredValue::Local { | StoredValue::Local {
value_type, size, .. value_type, size, ..
} => { } => {
use crate::wasm_module::Align::*; use roc_wasm_module::Align::*;
code_builder.get_local(to_ptr); code_builder.get_local(to_ptr);
self.load_symbols(code_builder, &[from_symbol]); self.load_symbols(code_builder, &[from_symbol]);
match (value_type, size) { match (value_type, size) {
@ -666,7 +667,7 @@ impl<'a> Storage<'a> {
| StoredValue::Local { | StoredValue::Local {
value_type, size, .. value_type, size, ..
} => { } => {
use crate::wasm_module::Align::*; use roc_wasm_module::Align::*;
if let AddressValue::NotLoaded(from_ptr) = from_addr { if let AddressValue::NotLoaded(from_ptr) = from_addr {
code_builder.get_local(from_ptr); code_builder.get_local(from_ptr);

View file

@ -5,17 +5,19 @@ The user needs to analyse the Wasm module's memory to decode the result.
*/ */
use bumpalo::{collections::Vec, Bump}; use bumpalo::{collections::Vec, Bump};
use roc_builtins::bitcode::{FloatWidth, IntWidth}; use roc_builtins::bitcode::{FloatWidth, IntWidth};
use roc_intern::Interner; use roc_intern::Interner;
use roc_mono::layout::{Builtin, Layout, UnionLayout}; use roc_mono::layout::{Builtin, Layout, UnionLayout};
use roc_target::TargetInfo;
use crate::wasm32_sized::Wasm32Sized;
use crate::wasm_module::{
linking::SymInfo, linking::WasmObjectSymbol, Align, CodeBuilder, Export, ExportType, LocalId,
Signature, ValueType, WasmModule,
};
use roc_std::{RocDec, RocList, RocOrder, RocResult, RocStr, I128, U128}; use roc_std::{RocDec, RocList, RocOrder, RocResult, RocStr, I128, U128};
use roc_target::TargetInfo;
use roc_wasm_module::{
linking::SymInfo, linking::WasmObjectSymbol, Align, Export, ExportType, LocalId, Signature,
ValueType, WasmModule,
};
use crate::code_builder::CodeBuilder;
use crate::wasm32_sized::Wasm32Sized;
/// Type-driven wrapper generation /// Type-driven wrapper generation
pub trait Wasm32Result { pub trait Wasm32Result {
@ -28,7 +30,7 @@ pub trait Wasm32Result {
insert_wrapper_metadata(arena, module, wrapper_name); insert_wrapper_metadata(arena, module, wrapper_name);
let mut code_builder = CodeBuilder::new(arena); let mut code_builder = CodeBuilder::new(arena);
Self::build_wrapper_body(&mut code_builder, main_function_index); Self::build_wrapper_body(&mut code_builder, main_function_index);
module.code.code_builders.push(code_builder); code_builder.insert_into_module(module);
} }
fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32); fn build_wrapper_body(code_builder: &mut CodeBuilder, main_function_index: u32);
@ -51,7 +53,7 @@ pub fn insert_wrapper_for_layout<'a>(
insert_wrapper_metadata(arena, module, wrapper_name); insert_wrapper_metadata(arena, module, wrapper_name);
let mut code_builder = CodeBuilder::new(arena); let mut code_builder = CodeBuilder::new(arena);
build_wrapper_body_stack_memory(&mut code_builder, main_fn_index, size as usize); build_wrapper_body_stack_memory(&mut code_builder, main_fn_index, size as usize);
module.code.code_builders.push(code_builder); code_builder.insert_into_module(module);
} }
}; };
@ -92,8 +94,7 @@ fn insert_wrapper_metadata<'a>(
) { ) {
let index = (module.import.function_count() as u32) let index = (module.import.function_count() as u32)
+ module.code.dead_import_dummy_count + module.code.dead_import_dummy_count
+ module.code.preloaded_count + module.code.function_count;
+ module.code.code_builders.len() as u32;
module.add_function_signature(Signature { module.add_function_signature(Signature {
param_types: Vec::with_capacity_in(0, arena), param_types: Vec::with_capacity_in(0, arena),

View file

@ -5,9 +5,6 @@ use bumpalo::Bump;
use roc_module::symbol::ModuleId; use roc_module::symbol::ModuleId;
use roc_packaging::cache::RocCacheDir; use roc_packaging::cache::RocCacheDir;
#[cfg(not(windows))]
const ROC_SKIP_SUBS_CACHE: &str = "ROC_SKIP_SUBS_CACHE";
const SKIP_SUBS_CACHE: bool = { const SKIP_SUBS_CACHE: bool = {
match option_env!("ROC_SKIP_SUBS_CACHE") { match option_env!("ROC_SKIP_SUBS_CACHE") {
Some(s) => s.len() == 1 && s.as_bytes()[0] == b'1', Some(s) => s.len() == 1 && s.as_bytes()[0] == b'1',
@ -73,6 +70,7 @@ fn write_types_for_module_dummy(output_path: &Path) {
fn write_types_for_module_real(module_id: ModuleId, filename: &str, output_path: &Path) { fn write_types_for_module_real(module_id: ModuleId, filename: &str, output_path: &Path) {
use roc_can::module::TypeState; use roc_can::module::TypeState;
use roc_load_internal::file::{LoadingProblem, Threading}; use roc_load_internal::file::{LoadingProblem, Threading};
use roc_reporting::cli::report_problems;
let arena = Bump::new(); let arena = Bump::new();
let cwd = std::env::current_dir().unwrap(); let cwd = std::env::current_dir().unwrap();
@ -92,7 +90,7 @@ fn write_types_for_module_real(module_id: ModuleId, filename: &str, output_path:
Threading::AllAvailable, Threading::AllAvailable,
); );
let module = match res_module { let mut module = match res_module {
Ok(v) => v, Ok(v) => v,
Err(LoadingProblem::FormattedReport(report)) => { Err(LoadingProblem::FormattedReport(report)) => {
panic!("{}", report); panic!("{}", report);
@ -102,8 +100,16 @@ fn write_types_for_module_real(module_id: ModuleId, filename: &str, output_path:
} }
}; };
if module.total_problems() > 0 { let problems = report_problems(
panic!("Problems were found! Refusing to build cached subs.\nTry building with {}=1 to see them.", ROC_SKIP_SUBS_CACHE); module.total_problems(),
&module.sources,
&module.interns,
&mut module.can_problems,
&mut module.type_problems,
);
if problems.errors + problems.warnings > 0 {
panic!("Problems were found! Refusing to build cached subs.");
} }
let subs = module.solved.into_inner(); let subs = module.solved.into_inner();

View file

@ -5047,11 +5047,11 @@ fn parse<'a>(arena: &'a Bump, header: ModuleHeader<'a>) -> Result<Msg<'a>, Loadi
let parse_start = Instant::now(); let parse_start = Instant::now();
let source = header.parse_state.original_bytes(); let source = header.parse_state.original_bytes();
let parse_state = header.parse_state; let parse_state = header.parse_state;
let parsed_defs = match module_defs().parse(arena, parse_state, 0) { let parsed_defs = match module_defs().parse(arena, parse_state.clone(), 0) {
Ok((_, success, _state)) => success, Ok((_, success, _state)) => success,
Err((_, fail, state)) => { Err((_, fail)) => {
return Err(LoadingProblem::ParsingFailed( return Err(LoadingProblem::ParsingFailed(
fail.into_file_error(header.module_path, &state), fail.into_file_error(header.module_path, &parse_state),
)); ));
} }
}; };

View file

@ -159,7 +159,7 @@ where
if state.column() >= min_indent { if state.column() >= min_indent {
Ok((NoProgress, (), state)) Ok((NoProgress, (), state))
} else { } else {
Err((NoProgress, indent_problem(state.pos()), state)) Err((NoProgress, indent_problem(state.pos())))
} }
} }
} }
@ -184,7 +184,6 @@ where
FastSpaceState::HasTab(position) => Err(( FastSpaceState::HasTab(position) => Err((
MadeProgress, MadeProgress,
E::space_problem(BadInputError::HasTab, position), E::space_problem(BadInputError::HasTab, position),
state,
)), )),
FastSpaceState::Good { FastSpaceState::Good {
newlines, newlines,
@ -194,7 +193,7 @@ where
if consumed == 0 { if consumed == 0 {
Ok((NoProgress, &[] as &[_], state)) Ok((NoProgress, &[] as &[_], state))
} else if column < min_indent { } else if column < min_indent {
Err((MadeProgress, indent_problem(state.pos()), state)) Err((MadeProgress, indent_problem(state.pos())))
} else { } else {
let comments_and_newlines = Vec::with_capacity_in(newlines, arena); let comments_and_newlines = Vec::with_capacity_in(newlines, arena);
let spaces = eat_spaces(state, comments_and_newlines); let spaces = eat_spaces(state, comments_and_newlines);
@ -218,7 +217,6 @@ where
FastSpaceState::HasTab(position) => Err(( FastSpaceState::HasTab(position) => Err((
MadeProgress, MadeProgress,
E::space_problem(BadInputError::HasTab, position), E::space_problem(BadInputError::HasTab, position),
state,
)), )),
FastSpaceState::Good { FastSpaceState::Good {
newlines, newlines,

View file

@ -14,7 +14,7 @@ use crate::parser::{
word1_indent, word2, EClosure, EExpect, EExpr, EIf, EInParens, EList, ENumber, EPattern, word1_indent, word2, EClosure, EExpect, EExpr, EIf, EInParens, EList, ENumber, EPattern,
ERecord, EString, ETuple, EType, EWhen, Either, ParseResult, Parser, ERecord, EString, ETuple, EType, EWhen, Either, ParseResult, Parser,
}; };
use crate::pattern::{loc_closure_param, loc_has_parser}; use crate::pattern::{closure_param, loc_has_parser};
use crate::state::State; use crate::state::State;
use crate::type_annotation; use crate::type_annotation;
use bumpalo::collections::Vec; use bumpalo::collections::Vec;
@ -30,7 +30,7 @@ fn expr_end<'a>() -> impl Parser<'a, (), EExpr<'a>> {
if state.has_reached_end() { if state.has_reached_end() {
Ok((NoProgress, (), state)) Ok((NoProgress, (), state))
} else { } else {
Err((NoProgress, EExpr::BadExprEnd(state.pos()), state)) Err((NoProgress, EExpr::BadExprEnd(state.pos())))
} }
} }
} }
@ -44,7 +44,7 @@ pub fn test_parse_expr<'a>(
match parser.parse(arena, state, min_indent) { match parser.parse(arena, state, min_indent) {
Ok((_, expression, _)) => Ok(expression), Ok((_, expression, _)) => Ok(expression),
Err((_, fail, _)) => Err(fail), Err((_, fail)) => Err(fail),
} }
} }
@ -102,7 +102,7 @@ fn loc_expr_in_parens_help<'a>() -> impl Parser<'a, Loc<Expr<'a>>, EInParens<'a>
state, state,
)) ))
} else if elements.is_empty() { } else if elements.is_empty() {
Err((NoProgress, EInParens::Empty(state.pos()), state)) Err((NoProgress, EInParens::Empty(state.pos())))
} else { } else {
// TODO: don't discard comments before/after // TODO: don't discard comments before/after
// (stored in the Collection) // (stored in the Collection)
@ -157,7 +157,11 @@ fn loc_expr_in_parens_etc_help<'a>() -> impl Parser<'a, Loc<Expr<'a>>, EExpr<'a>
} }
fn record_field_access_chain<'a>() -> impl Parser<'a, Vec<'a, &'a str>, EExpr<'a>> { fn record_field_access_chain<'a>() -> impl Parser<'a, Vec<'a, &'a str>, EExpr<'a>> {
|arena, state, min_indent| match record_field_access().parse(arena, state, min_indent) { |arena, state: State<'a>, min_indent| match record_field_access().parse(
arena,
state.clone(),
min_indent,
) {
Ok((_, initial, state)) => { Ok((_, initial, state)) => {
let mut accesses = Vec::with_capacity_in(1, arena); let mut accesses = Vec::with_capacity_in(1, arena);
@ -165,18 +169,18 @@ fn record_field_access_chain<'a>() -> impl Parser<'a, Vec<'a, &'a str>, EExpr<'a
let mut loop_state = state; let mut loop_state = state;
loop { loop {
match record_field_access().parse(arena, loop_state, min_indent) { match record_field_access().parse(arena, loop_state.clone(), min_indent) {
Ok((_, next, state)) => { Ok((_, next, state)) => {
accesses.push(next); accesses.push(next);
loop_state = state; loop_state = state;
} }
Err((MadeProgress, fail, state)) => return Err((MadeProgress, fail, state)), Err((MadeProgress, fail)) => return Err((MadeProgress, fail)),
Err((NoProgress, _, state)) => return Ok((MadeProgress, accesses, state)), Err((NoProgress, _)) => return Ok((MadeProgress, accesses, loop_state)),
} }
} }
} }
Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state)), Err((MadeProgress, fail)) => Err((MadeProgress, fail)),
Err((NoProgress, _, state)) => Err((NoProgress, EExpr::Access(state.pos()), state)), Err((NoProgress, _)) => Err((NoProgress, EExpr::Access(state.pos()))),
} }
} }
@ -292,7 +296,7 @@ fn loc_possibly_negative_or_negated_term<'a>(
} }
fn fail_expr_start_e<'a, T: 'a>() -> impl Parser<'a, T, EExpr<'a>> { fn fail_expr_start_e<'a, T: 'a>() -> impl Parser<'a, T, EExpr<'a>> {
|_arena, state: State<'a>, _min_indent: u32| Err((NoProgress, EExpr::Start(state.pos()), state)) |_arena, state: State<'a>, _min_indent: u32| Err((NoProgress, EExpr::Start(state.pos())))
} }
fn unary_negate<'a>() -> impl Parser<'a, (), EExpr<'a>> { fn unary_negate<'a>() -> impl Parser<'a, (), EExpr<'a>> {
@ -313,7 +317,7 @@ fn unary_negate<'a>() -> impl Parser<'a, (), EExpr<'a>> {
Ok((MadeProgress, (), state)) Ok((MadeProgress, (), state))
} else { } else {
// this is not a negated expression // this is not a negated expression
Err((NoProgress, EExpr::UnaryNot(state.pos()), state)) Err((NoProgress, EExpr::UnaryNot(state.pos())))
} }
} }
} }
@ -338,8 +342,8 @@ fn expr_operator_chain<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a
let initial_state = state.clone(); let initial_state = state.clone();
let end = state.pos(); let end = state.pos();
match space0_e(EExpr::IndentEnd).parse(arena, state, min_indent) { match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) {
Err((_, _, state)) => Ok((MadeProgress, expr.value, state)), Err((_, _)) => Ok((MadeProgress, expr.value, state)),
Ok((_, spaces_before_op, state)) => { Ok((_, spaces_before_op, state)) => {
let expr_state = ExprState { let expr_state = ExprState {
operators: Vec::new_in(arena), operators: Vec::new_in(arena),
@ -570,14 +574,14 @@ pub fn parse_single_def<'a>(
let spaces_before_current_start = state.pos(); let spaces_before_current_start = state.pos();
let state = match space0_e(EExpr::IndentStart).parse(arena, state, min_indent) { let state = match space0_e(EExpr::IndentStart).parse(arena, state, min_indent) {
Err((MadeProgress, _, s)) => { Err((MadeProgress, _)) => {
return Err((MadeProgress, EExpr::DefMissingFinalExpr(s.pos()), s)); return Err((MadeProgress, EExpr::DefMissingFinalExpr(initial.pos())));
} }
Ok((_, spaces, state)) => { Ok((_, spaces, state)) => {
spaces_before_current = spaces; spaces_before_current = spaces;
state state
} }
Err((NoProgress, _, state)) => state, Err((NoProgress, _)) => initial.clone(),
}; };
let start = state.pos(); let start = state.pos();
@ -591,9 +595,9 @@ pub fn parse_single_def<'a>(
state.clone(), state.clone(),
min_indent, min_indent,
) { ) {
Err((NoProgress, _, _)) => { Err((NoProgress, _)) => {
match parse_expect.parse(arena, state, min_indent) { match parse_expect.parse(arena, state, min_indent) {
Err((_, _, _)) => { Err((_, _)) => {
// a hacky way to get expression-based error messages. TODO fix this // a hacky way to get expression-based error messages. TODO fix this
Ok((NoProgress, None, initial)) Ok((NoProgress, None, initial))
} }
@ -645,7 +649,7 @@ pub fn parse_single_def<'a>(
} }
} }
} }
Err((MadeProgress, _, _)) => { Err((MadeProgress, _)) => {
// a hacky way to get expression-based error messages. TODO fix this // a hacky way to get expression-based error messages. TODO fix this
Ok((NoProgress, None, initial)) Ok((NoProgress, None, initial))
} }
@ -1014,7 +1018,7 @@ fn parse_defs_end<'a>(
next_state next_state
} }
Ok((progress, None, s)) => return Ok((progress, defs, s)), Ok((progress, None, s)) => return Ok((progress, defs, s)),
Err((progress, err, s)) => return Err((progress, err, s)), Err((progress, err)) => return Err((progress, err)),
}; };
} }
} }
@ -1038,12 +1042,11 @@ fn parse_defs_expr<'a>(
// this is no def, because there is no `=` or `:`; parse as an expr // this is no def, because there is no `=` or `:`; parse as an expr
let parse_final_expr = space0_before_e(loc_expr(), EExpr::IndentEnd); let parse_final_expr = space0_before_e(loc_expr(), EExpr::IndentEnd);
match parse_final_expr.parse(arena, state, min_indent) { match parse_final_expr.parse(arena, state.clone(), min_indent) {
Err((_, fail, state)) => { Err((_, fail)) => {
return Err(( return Err((
MadeProgress, MadeProgress,
EExpr::DefMissingFinalExpr2(arena.alloc(fail), state.pos()), EExpr::DefMissingFinalExpr2(arena.alloc(fail), state.pos()),
state,
)); ));
} }
Ok((_, loc_ret, state)) => { Ok((_, loc_ret, state)) => {
@ -1104,7 +1107,7 @@ fn finish_parsing_alias_or_opaque<'a>(
let (expr, arguments) = expr_state let (expr, arguments) = expr_state
.validate_is_type_def(arena, loc_op, kind) .validate_is_type_def(arena, loc_op, kind)
.map_err(|fail| (MadeProgress, fail, state.clone()))?; .map_err(|fail| (MadeProgress, fail))?;
let mut defs = Defs::default(); let mut defs = Defs::default();
@ -1180,8 +1183,8 @@ fn finish_parsing_alias_or_opaque<'a>(
), ),
); );
match parser.parse(arena, state, min_indent) { match parser.parse(arena, state.clone(), min_indent) {
Err((_, fail, state)) => return Err((MadeProgress, fail, state)), Err((_, fail)) => return Err((MadeProgress, fail)),
Ok((_, mut ann_type, state)) => { Ok((_, mut ann_type, state)) => {
// put the spaces from after the operator in front of the call // put the spaces from after the operator in front of the call
if !spaces_after_operator.is_empty() { if !spaces_after_operator.is_empty() {
@ -1209,7 +1212,7 @@ fn finish_parsing_alias_or_opaque<'a>(
}; };
let fail = EExpr::BadOperator(op, loc_op.region.start()); let fail = EExpr::BadOperator(op, loc_op.region.start());
return Err((MadeProgress, fail, state)); return Err((MadeProgress, fail));
} }
} }
} }
@ -1261,12 +1264,10 @@ mod ability {
indent: IndentLevel, indent: IndentLevel,
) -> impl Parser<'a, (u32, AbilityMember<'a>), EAbility<'a>> { ) -> impl Parser<'a, (u32, AbilityMember<'a>), EAbility<'a>> {
move |arena, state: State<'a>, min_indent: u32| { move |arena, state: State<'a>, min_indent: u32| {
let initial = state.clone();
// Put no restrictions on the indent after the spaces; we'll check it manually. // Put no restrictions on the indent after the spaces; we'll check it manually.
match space0_e(EAbility::DemandName).parse(arena, state, 0) { match space0_e(EAbility::DemandName).parse(arena, state, 0) {
Err((MadeProgress, fail, _)) => Err((NoProgress, fail, initial)), Err((MadeProgress, fail)) => Err((NoProgress, fail)),
Err((NoProgress, fail, _)) => Err((NoProgress, fail, initial)), Err((NoProgress, fail)) => Err((NoProgress, fail)),
Ok((_progress, spaces, state)) => { Ok((_progress, spaces, state)) => {
match indent { match indent {
@ -1275,7 +1276,6 @@ mod ability {
Err(( Err((
MadeProgress, MadeProgress,
EAbility::DemandAlignment(indent_difference, state.pos()), EAbility::DemandAlignment(indent_difference, state.pos()),
initial,
)) ))
} }
IndentLevel::Exact(wanted) if state.column() < wanted => { IndentLevel::Exact(wanted) if state.column() < wanted => {
@ -1286,7 +1286,6 @@ mod ability {
// expression // expression
NoProgress, NoProgress,
EAbility::DemandAlignment(indent_difference, state.pos()), EAbility::DemandAlignment(indent_difference, state.pos()),
initial,
)) ))
} }
IndentLevel::Exact(wanted) if state.column() > wanted => { IndentLevel::Exact(wanted) if state.column() > wanted => {
@ -1304,7 +1303,6 @@ mod ability {
Err(( Err((
progress, progress,
EAbility::DemandAlignment(indent_difference, state.pos()), EAbility::DemandAlignment(indent_difference, state.pos()),
initial,
)) ))
} }
_ => { _ => {
@ -1312,14 +1310,12 @@ mod ability {
let parser = parse_demand_help(); let parser = parse_demand_help();
match parser.parse(arena, state, min_indent) { match parser.parse(arena, state.clone(), min_indent) {
Err((MadeProgress, fail, state)) => { Err((MadeProgress, fail)) => Err((MadeProgress, fail)),
Err((MadeProgress, fail, state)) Err((NoProgress, fail)) => {
}
Err((NoProgress, fail, _)) => {
// We made progress relative to the entire ability definition, // We made progress relative to the entire ability definition,
// so this is an error. // so this is an error.
Err((MadeProgress, fail, initial)) Err((MadeProgress, fail))
} }
Ok((_, mut demand, state)) => { Ok((_, mut demand, state)) => {
@ -1355,12 +1351,11 @@ fn finish_parsing_ability_def_help<'a>(
// Parse the first demand. This will determine the indentation level all the // Parse the first demand. This will determine the indentation level all the
// other demands must observe. // other demands must observe.
let start = state.pos();
let (_, (demand_indent_level, first_demand), mut state) = let (_, (demand_indent_level, first_demand), mut state) =
ability::parse_demand(ability::IndentLevel::PendingMin(min_indent_for_demand)) ability::parse_demand(ability::IndentLevel::PendingMin(min_indent_for_demand))
.parse(arena, state, min_indent_for_demand) .parse(arena, state, min_indent_for_demand)
.map_err(|(progress, err, state)| { .map_err(|(progress, err)| (progress, EExpr::Ability(err, start)))?;
(progress, EExpr::Ability(err, state.pos()), state)
})?;
demands.push(first_demand); demands.push(first_demand);
let demand_indent = ability::IndentLevel::Exact(demand_indent_level); let demand_indent = ability::IndentLevel::Exact(demand_indent_level);
@ -1372,15 +1367,10 @@ fn finish_parsing_ability_def_help<'a>(
state = next_state; state = next_state;
demands.push(demand); demands.push(demand);
} }
Err((MadeProgress, problem, old_state)) => { Err((MadeProgress, problem)) => {
return Err(( return Err((MadeProgress, EExpr::Ability(problem, state.pos())));
MadeProgress,
EExpr::Ability(problem, old_state.pos()),
old_state,
));
} }
Err((NoProgress, _, old_state)) => { Err((NoProgress, _)) => {
state = old_state;
break; break;
} }
} }
@ -1431,10 +1421,11 @@ fn parse_expr_operator<'a>(
let initial_state = state.clone(); let initial_state = state.clone();
let (spaces, state) = match space0_e(EExpr::IndentEnd).parse(arena, state, min_indent) { let (spaces, state) =
Err((_, _, state)) => (&[] as &[_], state), match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) {
Ok((_, spaces, state)) => (spaces, state), Err((_, _)) => (&[] as &[_], state),
}; Ok((_, spaces, state)) => (spaces, state),
};
expr_state.arguments.push(arena.alloc(arg)); expr_state.arguments.push(arena.alloc(arg));
expr_state.spaces_after = spaces; expr_state.spaces_after = spaces;
@ -1448,7 +1439,7 @@ fn parse_expr_operator<'a>(
let call = expr_state let call = expr_state
.validate_assignment_or_backpassing(arena, loc_op, EExpr::ElmStyleFunction) .validate_assignment_or_backpassing(arena, loc_op, EExpr::ElmStyleFunction)
.map_err(|fail| (MadeProgress, fail, state.clone()))?; .map_err(|fail| (MadeProgress, fail))?;
let (value_def, def_region, state) = { let (value_def, def_region, state) = {
match expr_to_pattern_help(arena, &call.value) { match expr_to_pattern_help(arena, &call.value) {
@ -1475,7 +1466,7 @@ fn parse_expr_operator<'a>(
// this `=` likely occurred inline; treat it as an invalid operator // this `=` likely occurred inline; treat it as an invalid operator
let fail = EExpr::BadOperator(arena.alloc("="), loc_op.region.start()); let fail = EExpr::BadOperator(arena.alloc("="), loc_op.region.start());
return Err((MadeProgress, fail, state)); return Err((MadeProgress, fail));
} }
} }
}; };
@ -1493,7 +1484,7 @@ fn parse_expr_operator<'a>(
.validate_assignment_or_backpassing(arena, loc_op, |_, pos| { .validate_assignment_or_backpassing(arena, loc_op, |_, pos| {
EExpr::BadOperator("<-", pos) EExpr::BadOperator("<-", pos)
}) })
.map_err(|fail| (MadeProgress, fail, state.clone()))?; .map_err(|fail| (MadeProgress, fail))?;
let (loc_pattern, loc_body, state) = { let (loc_pattern, loc_body, state) = {
match expr_to_pattern_help(arena, &call.value) { match expr_to_pattern_help(arena, &call.value) {
@ -1514,7 +1505,7 @@ fn parse_expr_operator<'a>(
// this `=` likely occurred inline; treat it as an invalid operator // this `=` likely occurred inline; treat it as an invalid operator
let fail = EExpr::BadOperator("=", loc_op.region.start()); let fail = EExpr::BadOperator("=", loc_op.region.start());
return Err((MadeProgress, fail, state)); return Err((MadeProgress, fail));
} }
} }
}; };
@ -1545,8 +1536,12 @@ fn parse_expr_operator<'a>(
_ => unreachable!(), _ => unreachable!(),
}, },
), ),
_ => match loc_possibly_negative_or_negated_term(options).parse(arena, state, min_indent) { _ => match loc_possibly_negative_or_negated_term(options).parse(
Err((MadeProgress, f, s)) => Err((MadeProgress, f, s)), arena,
state.clone(),
min_indent,
) {
Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok((_, mut new_expr, state)) => { Ok((_, mut new_expr, state)) => {
let new_end = state.pos(); let new_end = state.pos();
@ -1559,8 +1554,8 @@ fn parse_expr_operator<'a>(
.with_spaces_before(spaces_after_operator, new_expr.region); .with_spaces_before(spaces_after_operator, new_expr.region);
} }
match space0_e(EExpr::IndentEnd).parse(arena, state, min_indent) { match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) {
Err((_, _, state)) => { Err((_, _)) => {
let args = std::mem::replace(&mut expr_state.arguments, Vec::new_in(arena)); let args = std::mem::replace(&mut expr_state.arguments, Vec::new_in(arena));
let call = to_call(arena, args, expr_state.expr); let call = to_call(arena, args, expr_state.expr);
@ -1588,8 +1583,8 @@ fn parse_expr_operator<'a>(
} }
} }
} }
Err((NoProgress, expr, e)) => { Err((NoProgress, expr)) => {
todo!("{:?} {:?}", expr, e) todo!("{:?} {:?}", expr, state)
} }
}, },
} }
@ -1609,7 +1604,7 @@ fn parse_expr_end<'a>(
); );
match parser.parse(arena, state.clone(), min_indent) { match parser.parse(arena, state.clone(), min_indent) {
Err((MadeProgress, f, s)) => Err((MadeProgress, f, s)), Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok(( Ok((
_, _,
has @ Loc { has @ Loc {
@ -1638,11 +1633,7 @@ fn parse_expr_end<'a>(
Err(_) => { Err(_) => {
let start = argument.region.start(); let start = argument.region.start();
let err = &*arena.alloc(EPattern::Start(start)); let err = &*arena.alloc(EPattern::Start(start));
return Err(( return Err((MadeProgress, EExpr::Pattern(err, argument.region.start())));
MadeProgress,
EExpr::Pattern(err, argument.region.start()),
state,
));
} }
} }
} }
@ -1679,8 +1670,8 @@ fn parse_expr_end<'a>(
} }
let initial_state = state.clone(); let initial_state = state.clone();
match space0_e(EExpr::IndentEnd).parse(arena, state, min_indent) { match space0_e(EExpr::IndentEnd).parse(arena, state.clone(), min_indent) {
Err((_, _, state)) => { Err((_, _)) => {
expr_state.arguments.push(arena.alloc(arg)); expr_state.arguments.push(arena.alloc(arg));
expr_state.end = new_end; expr_state.end = new_end;
expr_state.spaces_after = &[]; expr_state.spaces_after = &[];
@ -1697,11 +1688,11 @@ fn parse_expr_end<'a>(
} }
} }
} }
Err((NoProgress, _, _)) => { Err((NoProgress, _)) => {
let before_op = state.clone(); let before_op = state.clone();
// try an operator // try an operator
match loc!(operator()).parse(arena, state, min_indent) { match loc!(operator()).parse(arena, state.clone(), min_indent) {
Err((MadeProgress, f, s)) => Err((MadeProgress, f, s)), Err((MadeProgress, f)) => Err((MadeProgress, f)),
Ok((_, loc_op, state)) => { Ok((_, loc_op, state)) => {
expr_state.consume_spaces(arena); expr_state.consume_spaces(arena);
let initial_state = before_op; let initial_state = before_op;
@ -1715,7 +1706,8 @@ fn parse_expr_end<'a>(
initial_state, initial_state,
) )
} }
Err((NoProgress, _, mut state)) => { Err((NoProgress, _)) => {
let mut state = state;
// try multi-backpassing // try multi-backpassing
if options.accept_multi_backpassing && state.bytes().starts_with(b",") { if options.accept_multi_backpassing && state.bytes().starts_with(b",") {
state = state.advance(1); state = state.advance(1);
@ -1743,10 +1735,12 @@ fn parse_expr_end<'a>(
patterns.insert(0, loc_pattern); patterns.insert(0, loc_pattern);
match word2(b'<', b'-', EExpr::BackpassArrow) match word2(b'<', b'-', EExpr::BackpassArrow).parse(
.parse(arena, state, min_indent) arena,
{ state.clone(),
Err((_, fail, state)) => Err((MadeProgress, fail, state)), min_indent,
) {
Err((_, fail)) => Err((MadeProgress, fail)),
Ok((_, _, state)) => { Ok((_, _, state)) => {
let parse_body = space0_before_e( let parse_body = space0_before_e(
increment_min_indent(loc_expr()), increment_min_indent(loc_expr()),
@ -1771,7 +1765,7 @@ fn parse_expr_end<'a>(
} }
} }
} else if options.check_for_arrow && state.bytes().starts_with(b"->") { } else if options.check_for_arrow && state.bytes().starts_with(b"->") {
Err((MadeProgress, EExpr::BadOperator("->", state.pos()), state)) Err((MadeProgress, EExpr::BadOperator("->", state.pos())))
} else { } else {
let expr = parse_expr_final(expr_state, arena); let expr = parse_expr_final(expr_state, arena);
@ -1998,7 +1992,7 @@ fn closure_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EClo
sep_by1_e( sep_by1_e(
word1(b',', EClosure::Comma), word1(b',', EClosure::Comma),
space0_around_ee( space0_around_ee(
specialize(EClosure::Pattern, loc_closure_param()), specialize(EClosure::Pattern, closure_param()),
EClosure::IndentArg, EClosure::IndentArg,
EClosure::IndentArrow, EClosure::IndentArrow,
), ),
@ -2090,11 +2084,7 @@ mod when {
Ok((MadeProgress, (loc_patterns, loc_guard), state)) Ok((MadeProgress, (loc_patterns, loc_guard), state))
} else { } else {
let indent = pattern_indent_level - indent_column; let indent = pattern_indent_level - indent_column;
Err(( Err((MadeProgress, EWhen::PatternAlignment(indent, state.pos())))
MadeProgress,
EWhen::PatternAlignment(indent, state.pos()),
state,
))
} }
}, },
), ),
@ -2111,18 +2101,16 @@ mod when {
); );
while !state.bytes().is_empty() { while !state.bytes().is_empty() {
match branch_parser.parse(arena, state, min_indent) { match branch_parser.parse(arena, state.clone(), min_indent) {
Ok((_, next_output, next_state)) => { Ok((_, next_output, next_state)) => {
state = next_state; state = next_state;
branches.push(arena.alloc(next_output)); branches.push(arena.alloc(next_output));
} }
Err((MadeProgress, problem, old_state)) => { Err((MadeProgress, problem)) => {
return Err((MadeProgress, problem, old_state)); return Err((MadeProgress, problem));
} }
Err((NoProgress, _, old_state)) => { Err((NoProgress, _)) => {
state = old_state;
break; break;
} }
} }
@ -2193,25 +2181,19 @@ mod when {
pattern_indent_level: Option<u32>, pattern_indent_level: Option<u32>,
) -> impl Parser<'a, (u32, Vec<'a, Loc<Pattern<'a>>>), EWhen<'a>> { ) -> impl Parser<'a, (u32, Vec<'a, Loc<Pattern<'a>>>), EWhen<'a>> {
move |arena, state: State<'a>, min_indent: u32| { move |arena, state: State<'a>, min_indent: u32| {
let initial = state.clone();
// put no restrictions on the indent after the spaces; we'll check it manually // put no restrictions on the indent after the spaces; we'll check it manually
match space0_e(EWhen::IndentPattern).parse(arena, state, 0) { match space0_e(EWhen::IndentPattern).parse(arena, state, 0) {
Err((MadeProgress, fail, _)) => Err((NoProgress, fail, initial)), Err((MadeProgress, fail)) => Err((NoProgress, fail)),
Err((NoProgress, fail, _)) => Err((NoProgress, fail, initial)), Err((NoProgress, fail)) => Err((NoProgress, fail)),
Ok((_progress, spaces, state)) => { Ok((_progress, spaces, state)) => {
match pattern_indent_level { match pattern_indent_level {
Some(wanted) if state.column() > wanted => { Some(wanted) if state.column() > wanted => {
// this branch is indented too much // this branch is indented too much
Err((NoProgress, EWhen::IndentPattern(state.pos()), initial)) Err((NoProgress, EWhen::IndentPattern(state.pos())))
} }
Some(wanted) if state.column() < wanted => { Some(wanted) if state.column() < wanted => {
let indent = wanted - state.column(); let indent = wanted - state.column();
Err(( Err((NoProgress, EWhen::PatternAlignment(indent, state.pos())))
NoProgress,
EWhen::PatternAlignment(indent, state.pos()),
initial,
))
} }
_ => { _ => {
let pattern_indent = let pattern_indent =
@ -2223,13 +2205,11 @@ mod when {
let parser = let parser =
sep_by1(word1(b'|', EWhen::Bar), branch_single_alternative()); sep_by1(word1(b'|', EWhen::Bar), branch_single_alternative());
match parser.parse(arena, state, pattern_indent) { match parser.parse(arena, state.clone(), pattern_indent) {
Err((MadeProgress, fail, state)) => { Err((MadeProgress, fail)) => Err((MadeProgress, fail)),
Err((MadeProgress, fail, state)) Err((NoProgress, fail)) => {
}
Err((NoProgress, fail, _)) => {
// roll back space parsing if the pattern made no progress // roll back space parsing if the pattern made no progress
Err((NoProgress, fail, initial)) Err((NoProgress, fail))
} }
Ok((_, mut loc_patterns, state)) => { Ok((_, mut loc_patterns, state)) => {
@ -2303,7 +2283,7 @@ fn expect_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EExpe
EExpect::IndentCondition, EExpect::IndentCondition,
) )
.parse(arena, state, start_column + 1) .parse(arena, state, start_column + 1)
.map_err(|(_, f, s)| (MadeProgress, f, s))?; .map_err(|(_, f)| (MadeProgress, f))?;
let parse_cont = specialize_ref( let parse_cont = specialize_ref(
EExpect::Continuation, EExpect::Continuation,
@ -2340,8 +2320,8 @@ fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<
parser::keyword_e(keyword::IF, EIf::If) parser::keyword_e(keyword::IF, EIf::If)
); );
match optional_if.parse(arena, state, min_indent) { match optional_if.parse(arena, state.clone(), min_indent) {
Err((_, _, state)) => break state, Err((_, _)) => break state,
Ok((_, _, state)) => { Ok((_, _, state)) => {
loop_state = state; loop_state = state;
continue; continue;
@ -2354,7 +2334,7 @@ fn if_expr_help<'a>(options: ExprParseOptions) -> impl Parser<'a, Expr<'a>, EIf<
EIf::IndentElseBranch, EIf::IndentElseBranch,
) )
.parse(arena, state_final_else, min_indent) .parse(arena, state_final_else, min_indent)
.map_err(|(_, f, s)| (MadeProgress, f, s))?; .map_err(|(_, f)| (MadeProgress, f))?;
let expr = Expr::If(branches.into_bump_slice(), arena.alloc(else_branch)); let expr = Expr::If(branches.into_bump_slice(), arena.alloc(else_branch));
@ -2711,12 +2691,12 @@ where
macro_rules! bad_made_progress { macro_rules! bad_made_progress {
($op:expr) => {{ ($op:expr) => {{
Err((MadeProgress, to_error($op, state.pos()), state)) Err((MadeProgress, to_error($op, state.pos())))
}}; }};
} }
match chomped { match chomped {
"" => Err((NoProgress, to_expectation(state.pos()), state)), "" => Err((NoProgress, to_expectation(state.pos()))),
"+" => good!(BinOp::Plus, 1), "+" => good!(BinOp::Plus, 1),
"-" => good!(BinOp::Minus, 1), "-" => good!(BinOp::Minus, 1),
"*" => good!(BinOp::Star, 1), "*" => good!(BinOp::Star, 1),
@ -2727,7 +2707,7 @@ where
"<" => good!(BinOp::LessThan, 1), "<" => good!(BinOp::LessThan, 1),
"." => { "." => {
// a `.` makes no progress, so it does not interfere with `.foo` access(or) // a `.` makes no progress, so it does not interfere with `.foo` access(or)
Err((NoProgress, to_error(".", state.pos()), state)) Err((NoProgress, to_error(".", state.pos())))
} }
"=" => good!(BinOp::Assignment, 1), "=" => good!(BinOp::Assignment, 1),
":=" => good!(BinOp::IsOpaqueType, 2), ":=" => good!(BinOp::IsOpaqueType, 2),
@ -2742,7 +2722,7 @@ where
"//" => good!(BinOp::DoubleSlash, 2), "//" => good!(BinOp::DoubleSlash, 2),
"->" => { "->" => {
// makes no progress, so it does not interfere with `_ if isGood -> ...` // makes no progress, so it does not interfere with `_ if isGood -> ...`
Err((NoProgress, to_error("->", state.pos()), state)) Err((NoProgress, to_error("->", state.pos())))
} }
"<-" => good!(BinOp::Backpassing, 2), "<-" => good!(BinOp::Backpassing, 2),
_ => bad_made_progress!(chomped), _ => bad_made_progress!(chomped),

View file

@ -312,8 +312,8 @@ pub fn package_name<'a>() -> impl Parser<'a, PackageName<'a>, EPackageName<'a>>
.parse(arena, state, min_indent) .parse(arena, state, min_indent)
.and_then(|(progress, text, next_state)| match text { .and_then(|(progress, text, next_state)| match text {
StrLiteral::PlainLine(text) => Ok((progress, PackageName(text), next_state)), StrLiteral::PlainLine(text) => Ok((progress, PackageName(text), next_state)),
StrLiteral::Line(_) => Err((progress, EPackageName::Escapes(pos), next_state)), StrLiteral::Line(_) => Err((progress, EPackageName::Escapes(pos))),
StrLiteral::Block(_) => Err((progress, EPackageName::Multiline(pos), next_state)), StrLiteral::Block(_) => Err((progress, EPackageName::Multiline(pos))),
}) })
} }
} }

View file

@ -88,10 +88,10 @@ impl<'a> Ident<'a> {
/// * A named pattern match, e.g. "foo" in `foo =` or `foo ->` or `\foo ->` /// * A named pattern match, e.g. "foo" in `foo =` or `foo ->` or `\foo ->`
pub fn lowercase_ident<'a>() -> impl Parser<'a, &'a str, ()> { pub fn lowercase_ident<'a>() -> impl Parser<'a, &'a str, ()> {
move |_, state: State<'a>, _min_indent: u32| match chomp_lowercase_part(state.bytes()) { move |_, state: State<'a>, _min_indent: u32| match chomp_lowercase_part(state.bytes()) {
Err(progress) => Err((progress, (), state)), Err(progress) => Err((progress, ())),
Ok(ident) => { Ok(ident) => {
if crate::keyword::KEYWORDS.iter().any(|kw| &ident == kw) { if crate::keyword::KEYWORDS.iter().any(|kw| &ident == kw) {
Err((NoProgress, (), state)) Err((NoProgress, ()))
} else { } else {
let width = ident.len(); let width = ident.len();
Ok((MadeProgress, ident, state.advance(width))) Ok((MadeProgress, ident, state.advance(width)))
@ -113,7 +113,7 @@ pub fn tag_name<'a>() -> impl Parser<'a, &'a str, ()> {
/// * A tag /// * A tag
pub fn uppercase<'a>() -> impl Parser<'a, UppercaseIdent<'a>, ()> { pub fn uppercase<'a>() -> impl Parser<'a, UppercaseIdent<'a>, ()> {
move |_, state: State<'a>, _min_indent: u32| match chomp_uppercase_part(state.bytes()) { move |_, state: State<'a>, _min_indent: u32| match chomp_uppercase_part(state.bytes()) {
Err(progress) => Err((progress, (), state)), Err(progress) => Err((progress, ())),
Ok(ident) => { Ok(ident) => {
let width = ident.len(); let width = ident.len();
Ok((MadeProgress, ident.into(), state.advance(width))) Ok((MadeProgress, ident.into(), state.advance(width)))
@ -128,7 +128,7 @@ pub fn uppercase<'a>() -> impl Parser<'a, UppercaseIdent<'a>, ()> {
/// * A tag /// * A tag
pub fn uppercase_ident<'a>() -> impl Parser<'a, &'a str, ()> { pub fn uppercase_ident<'a>() -> impl Parser<'a, &'a str, ()> {
move |_, state: State<'a>, _min_indent: u32| match chomp_uppercase_part(state.bytes()) { move |_, state: State<'a>, _min_indent: u32| match chomp_uppercase_part(state.bytes()) {
Err(progress) => Err((progress, (), state)), Err(progress) => Err((progress, ())),
Ok(ident) => { Ok(ident) => {
let width = ident.len(); let width = ident.len();
Ok((MadeProgress, ident, state.advance(width))) Ok((MadeProgress, ident, state.advance(width)))
@ -138,10 +138,10 @@ pub fn uppercase_ident<'a>() -> impl Parser<'a, &'a str, ()> {
pub fn unqualified_ident<'a>() -> impl Parser<'a, &'a str, ()> { pub fn unqualified_ident<'a>() -> impl Parser<'a, &'a str, ()> {
move |_, state: State<'a>, _min_indent: u32| match chomp_anycase_part(state.bytes()) { move |_, state: State<'a>, _min_indent: u32| match chomp_anycase_part(state.bytes()) {
Err(progress) => Err((progress, (), state)), Err(progress) => Err((progress, ())),
Ok(ident) => { Ok(ident) => {
if crate::keyword::KEYWORDS.iter().any(|kw| &ident == kw) { if crate::keyword::KEYWORDS.iter().any(|kw| &ident == kw) {
Err((MadeProgress, (), state)) Err((MadeProgress, ()))
} else { } else {
let width = ident.len(); let width = ident.len();
Ok((MadeProgress, ident, state.advance(width))) Ok((MadeProgress, ident, state.advance(width)))
@ -163,27 +163,32 @@ pub fn parse_ident<'a>(
) -> ParseResult<'a, Ident<'a>, EExpr<'a>> { ) -> ParseResult<'a, Ident<'a>, EExpr<'a>> {
let initial = state.clone(); let initial = state.clone();
match parse_ident_help(arena, state) { match chomp_identifier_chain(arena, state.bytes(), state.pos()) {
Ok((progress, ident, state)) => { Ok((width, ident)) => {
let state = advance_state!(state, width as usize)?;
if let Ident::Access { module_name, parts } = ident { if let Ident::Access { module_name, parts } = ident {
if module_name.is_empty() { if module_name.is_empty() {
if let Some(first) = parts.first() { if let Some(first) = parts.first() {
for keyword in crate::keyword::KEYWORDS.iter() { for keyword in crate::keyword::KEYWORDS.iter() {
if first == keyword { if first == keyword {
return Err((NoProgress, EExpr::Start(initial.pos()), initial)); return Err((NoProgress, EExpr::Start(initial.pos())));
} }
} }
} }
} }
} }
Ok((progress, ident, state)) Ok((MadeProgress, ident, state))
} }
Err((NoProgress, _, state)) => Err((NoProgress, EExpr::Start(state.pos()), state)), Err((0, _)) => Err((NoProgress, EExpr::Start(state.pos()))),
Err((MadeProgress, fail, state)) => match fail { Err((width, fail)) => match fail {
BadIdent::Start(pos) => Err((NoProgress, EExpr::Start(pos), state)), BadIdent::Start(pos) => Err((NoProgress, EExpr::Start(pos))),
BadIdent::Space(e, pos) => Err((NoProgress, EExpr::Space(e, pos), state)), BadIdent::Space(e, pos) => Err((NoProgress, EExpr::Space(e, pos))),
_ => malformed_identifier(initial.bytes(), fail, state), _ => malformed_identifier(
initial.bytes(),
fail,
advance_state!(state, width as usize)?,
),
}, },
} }
} }
@ -504,7 +509,7 @@ fn chomp_module_chain(buffer: &[u8]) -> Result<u32, Progress> {
pub fn concrete_type<'a>() -> impl Parser<'a, (&'a str, &'a str), ()> { pub fn concrete_type<'a>() -> impl Parser<'a, (&'a str, &'a str), ()> {
move |_, state: State<'a>, _min_indent: u32| match chomp_concrete_type(state.bytes()) { move |_, state: State<'a>, _min_indent: u32| match chomp_concrete_type(state.bytes()) {
Err(progress) => Err((progress, (), state)), Err(progress) => Err((progress, ())),
Ok((module_name, type_name, width)) => { Ok((module_name, type_name, width)) => {
Ok((MadeProgress, (module_name, type_name), state.advance(width))) Ok((MadeProgress, (module_name, type_name), state.advance(width)))
} }
@ -574,20 +579,3 @@ fn chomp_access_chain<'a>(buffer: &'a [u8], parts: &mut Vec<'a, &'a str>) -> Res
Ok(chomped as u32) Ok(chomped as u32)
} }
} }
fn parse_ident_help<'a>(
arena: &'a Bump,
mut state: State<'a>,
) -> ParseResult<'a, Ident<'a>, BadIdent> {
match chomp_identifier_chain(arena, state.bytes(), state.pos()) {
Ok((width, ident)) => {
state = advance_state!(state, width as usize)?;
Ok((MadeProgress, ident, state))
}
Err((0, fail)) => Err((NoProgress, fail, state)),
Err((width, fail)) => {
state = advance_state!(state, width as usize)?;
Err((MadeProgress, fail, state))
}
}
}

View file

@ -7,9 +7,9 @@ use crate::header::{
use crate::ident::{self, lowercase_ident, unqualified_ident, uppercase, UppercaseIdent}; use crate::ident::{self, lowercase_ident, unqualified_ident, uppercase, UppercaseIdent};
use crate::parser::Progress::{self, *}; use crate::parser::Progress::{self, *};
use crate::parser::{ use crate::parser::{
backtrackable, increment_min_indent, optional, reset_min_indent, specialize, specialize_region, backtrackable, increment_min_indent, optional, reset_min_indent, specialize, word1, EExposes,
word1, EExposes, EGenerates, EGeneratesWith, EHeader, EImports, EPackages, EProvides, EGenerates, EGeneratesWith, EHeader, EImports, EPackages, EProvides, ERequires, ETypedIdent,
ERequires, ETypedIdent, Parser, SourceError, SpaceProblem, SyntaxError, Parser, SourceError, SpaceProblem, SyntaxError,
}; };
use crate::state::State; use crate::state::State;
use crate::string_literal; use crate::string_literal;
@ -21,7 +21,7 @@ fn end_of_file<'a>() -> impl Parser<'a, (), SyntaxError<'a>> {
if state.has_reached_end() { if state.has_reached_end() {
Ok((NoProgress, (), state)) Ok((NoProgress, (), state))
} else { } else {
Err((NoProgress, SyntaxError::NotEndOfFile(state.pos()), state)) Err((NoProgress, SyntaxError::NotEndOfFile(state.pos())))
} }
} }
} }
@ -29,10 +29,7 @@ fn end_of_file<'a>() -> impl Parser<'a, (), SyntaxError<'a>> {
#[inline(always)] #[inline(always)]
pub fn module_defs<'a>() -> impl Parser<'a, Defs<'a>, SyntaxError<'a>> { pub fn module_defs<'a>() -> impl Parser<'a, Defs<'a>, SyntaxError<'a>> {
skip_second!( skip_second!(
specialize_region( specialize(SyntaxError::Expr, crate::expr::toplevel_defs(),),
|e, r| SyntaxError::Expr(e, r.start()),
crate::expr::toplevel_defs(),
),
end_of_file() end_of_file()
) )
} }
@ -42,9 +39,9 @@ pub fn parse_header<'a>(
state: State<'a>, state: State<'a>,
) -> Result<(Module<'a>, State<'a>), SourceError<'a, EHeader<'a>>> { ) -> Result<(Module<'a>, State<'a>), SourceError<'a, EHeader<'a>>> {
let min_indent = 0; let min_indent = 0;
match header().parse(arena, state, min_indent) { match header().parse(arena, state.clone(), min_indent) {
Ok((_, module, state)) => Ok((module, state)), Ok((_, module, state)) => Ok((module, state)),
Err((_, fail, state)) => Err(SourceError::new(fail, &state)), Err((_, fail)) => Err(SourceError::new(fail, &state)),
} }
} }
@ -234,7 +231,7 @@ fn module_name<'a>() -> impl Parser<'a, ModuleName<'a>, ()> {
Ok((MadeProgress, ModuleName::new(name), state)) Ok((MadeProgress, ModuleName::new(name), state))
} }
Err(progress) => Err((progress, (), state)), Err(progress) => Err((progress, ())),
} }
} }

View file

@ -20,7 +20,7 @@ pub fn positive_number_literal<'a>() -> impl Parser<'a, NumLiteral<'a>, ENumber>
} }
_ => { _ => {
// this is not a number at all // this is not a number at all
Err((Progress::NoProgress, ENumber::End, state)) Err((Progress::NoProgress, ENumber::End))
} }
} }
} }
@ -38,7 +38,7 @@ pub fn number_literal<'a>() -> impl Parser<'a, NumLiteral<'a>, ENumber> {
} }
_ => { _ => {
// this is not a number at all // this is not a number at all
Err((Progress::NoProgress, ENumber::End, state)) Err((Progress::NoProgress, ENumber::End))
} }
} }
} }
@ -89,12 +89,12 @@ fn chomp_number_dec<'a>(
if is_negative && chomped == 0 { if is_negative && chomped == 0 {
// we're probably actually looking at unary negation here // we're probably actually looking at unary negation here
return Err((Progress::NoProgress, ENumber::End, state)); return Err((Progress::NoProgress, ENumber::End));
} }
if !bytes.first().copied().unwrap_or_default().is_ascii_digit() { if !bytes.first().copied().unwrap_or_default().is_ascii_digit() {
// we're probably actually looking at unary negation here // we're probably actually looking at unary negation here
return Err((Progress::NoProgress, ENumber::End, state)); return Err((Progress::NoProgress, ENumber::End));
} }
let string = let string =

View file

@ -12,8 +12,7 @@ pub enum Either<First, Second> {
impl<F: Copy, S: Copy> Copy for Either<F, S> {} impl<F: Copy, S: Copy> Copy for Either<F, S> {}
pub type ParseResult<'a, Output, Error> = pub type ParseResult<'a, Output, Error> = Result<(Progress, Output, State<'a>), (Progress, Error)>;
Result<(Progress, Output, State<'a>), (Progress, Error, State<'a>)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Progress { pub enum Progress {
@ -853,7 +852,7 @@ where
let (progress, value, state) = match &res { let (progress, value, state) = match &res {
Ok((progress, result, state)) => (progress, Ok(result), state), Ok((progress, result, state)) => (progress, Ok(result), state),
Err((progress, error, state)) => (progress, Err(error), state), Err((progress, error)) => (progress, Err(error), state),
}; };
println!( println!(
@ -926,7 +925,7 @@ where
let width = keyword.len(); let width = keyword.len();
if !state.bytes().starts_with(keyword.as_bytes()) { if !state.bytes().starts_with(keyword.as_bytes()) {
return Err((NoProgress, if_error(state.pos()), state)); return Err((NoProgress, if_error(state.pos())));
} }
// the next character should not be an identifier character // the next character should not be an identifier character
@ -940,7 +939,7 @@ where
state = state.advance(width); state = state.advance(width);
Ok((MadeProgress, (), state)) Ok((MadeProgress, (), state))
} }
Some(_) => Err((NoProgress, if_error(state.pos()), state)), Some(_) => Err((NoProgress, if_error(state.pos()))),
} }
} }
} }
@ -957,6 +956,8 @@ where
Error: 'a, Error: 'a,
{ {
move |arena, state: State<'a>, min_indent: u32| { move |arena, state: State<'a>, min_indent: u32| {
let original_state = state.clone();
let start_bytes_len = state.bytes().len(); let start_bytes_len = state.bytes().len();
match parser.parse(arena, state, min_indent) { match parser.parse(arena, state, min_indent) {
@ -970,10 +971,10 @@ where
buf.push(first_output); buf.push(first_output);
loop { loop {
match delimiter.parse(arena, state, min_indent) { match delimiter.parse(arena, state.clone(), min_indent) {
Ok((_, (), next_state)) => { Ok((_, (), next_state)) => {
// If the delimiter passed, check the element parser. // If the delimiter passed, check the element parser.
match parser.parse(arena, next_state, min_indent) { match parser.parse(arena, next_state.clone(), min_indent) {
Ok((element_progress, next_output, next_state)) => { Ok((element_progress, next_output, next_state)) => {
// in practice, we want elements to make progress // in practice, we want elements to make progress
debug_assert_eq!(element_progress, MadeProgress); debug_assert_eq!(element_progress, MadeProgress);
@ -981,28 +982,28 @@ where
state = next_state; state = next_state;
buf.push(next_output); buf.push(next_output);
} }
Err((_, fail, state)) => { Err((_, fail)) => {
// If the delimiter parsed, but the following // If the delimiter parsed, but the following
// element did not, that's a fatal error. // element did not, that's a fatal error.
let progress = Progress::from_lengths( let progress = Progress::from_lengths(
start_bytes_len, start_bytes_len,
state.bytes().len(), next_state.bytes().len(),
); );
return Err((progress, fail, state)); return Err((progress, fail));
} }
} }
} }
Err((delim_progress, fail, old_state)) => match delim_progress { Err((delim_progress, fail)) => match delim_progress {
MadeProgress => return Err((MadeProgress, fail, old_state)), MadeProgress => return Err((MadeProgress, fail)),
NoProgress => return Ok((NoProgress, buf, old_state)), NoProgress => return Ok((NoProgress, buf, state)),
}, },
} }
} }
} }
Err((element_progress, fail, new_state)) => match element_progress { Err((element_progress, fail)) => match element_progress {
MadeProgress => Err((MadeProgress, fail, new_state)), MadeProgress => Err((MadeProgress, fail)),
NoProgress => Ok((NoProgress, Vec::new_in(arena), new_state)), NoProgress => Ok((NoProgress, Vec::new_in(arena), original_state)),
}, },
} }
} }
@ -1020,6 +1021,7 @@ where
Error: 'a, Error: 'a,
{ {
move |arena, state: State<'a>, min_indent: u32| { move |arena, state: State<'a>, min_indent: u32| {
let original_state = state.clone();
let start_bytes_len = state.bytes().len(); let start_bytes_len = state.bytes().len();
match parser.parse(arena, state, min_indent) { match parser.parse(arena, state, min_indent) {
@ -1032,10 +1034,10 @@ where
buf.push(first_output); buf.push(first_output);
loop { loop {
match delimiter.parse(arena, state, min_indent) { match delimiter.parse(arena, state.clone(), min_indent) {
Ok((_, (), next_state)) => { Ok((_, (), next_state)) => {
// If the delimiter passed, check the element parser. // If the delimiter passed, check the element parser.
match parser.parse(arena, next_state, min_indent) { match parser.parse(arena, next_state.clone(), min_indent) {
Ok((element_progress, next_output, next_state)) => { Ok((element_progress, next_output, next_state)) => {
// in practice, we want elements to make progress // in practice, we want elements to make progress
debug_assert_eq!(element_progress, MadeProgress); debug_assert_eq!(element_progress, MadeProgress);
@ -1043,27 +1045,27 @@ where
state = next_state; state = next_state;
buf.push(next_output); buf.push(next_output);
} }
Err((_, _fail, old_state)) => { Err((_, _fail)) => {
// If the delimiter parsed, but the following // If the delimiter parsed, but the following
// element did not, that means we saw a trailing comma // element did not, that means we saw a trailing comma
let progress = Progress::from_lengths( let progress = Progress::from_lengths(
start_bytes_len, start_bytes_len,
old_state.bytes().len(), next_state.bytes().len(),
); );
return Ok((progress, buf, old_state)); return Ok((progress, buf, next_state));
} }
} }
} }
Err((delim_progress, fail, old_state)) => match delim_progress { Err((delim_progress, fail)) => match delim_progress {
MadeProgress => return Err((MadeProgress, fail, old_state)), MadeProgress => return Err((MadeProgress, fail)),
NoProgress => return Ok((NoProgress, buf, old_state)), NoProgress => return Ok((NoProgress, buf, state)),
}, },
} }
} }
} }
Err((element_progress, fail, new_state)) => match element_progress { Err((element_progress, fail)) => match element_progress {
MadeProgress => Err((MadeProgress, fail, new_state)), MadeProgress => Err((MadeProgress, fail)),
NoProgress => Ok((NoProgress, Vec::new_in(arena), new_state)), NoProgress => Ok((NoProgress, Vec::new_in(arena), original_state)),
}, },
} }
} }
@ -1092,6 +1094,7 @@ where
buf.push(first_output); buf.push(first_output);
loop { loop {
let old_state = state.clone();
match delimiter.parse(arena, state, min_indent) { match delimiter.parse(arena, state, min_indent) {
Ok((_, (), next_state)) => { Ok((_, (), next_state)) => {
// If the delimiter passed, check the element parser. // If the delimiter passed, check the element parser.
@ -1100,16 +1103,16 @@ where
state = next_state; state = next_state;
buf.push(next_output); buf.push(next_output);
} }
Err((_, fail, state)) => { Err((_, fail)) => {
return Err((MadeProgress, fail, state)); return Err((MadeProgress, fail));
} }
} }
} }
Err((delim_progress, fail, old_state)) => { Err((delim_progress, fail)) => {
match delim_progress { match delim_progress {
MadeProgress => { MadeProgress => {
// fail if the delimiter made progress // fail if the delimiter made progress
return Err((MadeProgress, fail, old_state)); return Err((MadeProgress, fail));
} }
NoProgress => { NoProgress => {
let progress = Progress::from_lengths( let progress = Progress::from_lengths(
@ -1123,7 +1126,7 @@ where
} }
} }
} }
Err((fail_progress, fail, new_state)) => Err((fail_progress, fail, new_state)), Err((fail_progress, fail)) => Err((fail_progress, fail)),
} }
} }
} }
@ -1142,6 +1145,7 @@ where
Error: 'a, Error: 'a,
{ {
move |arena, state: State<'a>, min_indent: u32| { move |arena, state: State<'a>, min_indent: u32| {
let original_state = state.clone();
let start_bytes_len = state.bytes().len(); let start_bytes_len = state.bytes().len();
match parser.parse(arena, state, min_indent) { match parser.parse(arena, state, min_indent) {
@ -1153,27 +1157,28 @@ where
buf.push(first_output); buf.push(first_output);
loop { loop {
let old_state = state.clone();
match delimiter.parse(arena, state, min_indent) { match delimiter.parse(arena, state, min_indent) {
Ok((_, (), next_state)) => { Ok((_, (), next_state)) => {
// If the delimiter passed, check the element parser. // If the delimiter passed, check the element parser.
match parser.parse(arena, next_state, min_indent) { match parser.parse(arena, next_state.clone(), min_indent) {
Ok((_, next_output, next_state)) => { Ok((_, next_output, next_state)) => {
state = next_state; state = next_state;
buf.push(next_output); buf.push(next_output);
} }
Err((MadeProgress, fail, state)) => { Err((MadeProgress, fail)) => {
return Err((MadeProgress, fail, state)); return Err((MadeProgress, fail));
} }
Err((NoProgress, _fail, state)) => { Err((NoProgress, _fail)) => {
return Err((NoProgress, to_element_error(state.pos()), state)); return Err((NoProgress, to_element_error(next_state.pos())));
} }
} }
} }
Err((delim_progress, fail, old_state)) => { Err((delim_progress, fail)) => {
match delim_progress { match delim_progress {
MadeProgress => { MadeProgress => {
// fail if the delimiter made progress // fail if the delimiter made progress
return Err((MadeProgress, fail, old_state)); return Err((MadeProgress, fail));
} }
NoProgress => { NoProgress => {
let progress = Progress::from_lengths( let progress = Progress::from_lengths(
@ -1188,10 +1193,8 @@ where
} }
} }
Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state)), Err((MadeProgress, fail)) => Err((MadeProgress, fail)),
Err((NoProgress, _fail, state)) => { Err((NoProgress, _fail)) => Err((NoProgress, to_element_error(original_state.pos()))),
Err((NoProgress, to_element_error(state.pos()), state))
}
} }
} }
} }
@ -1203,7 +1206,7 @@ pub fn fail_when_progress<T, E>(
state: State<'_>, state: State<'_>,
) -> ParseResult<'_, T, E> { ) -> ParseResult<'_, T, E> {
match progress { match progress {
MadeProgress => Err((MadeProgress, fail, state)), MadeProgress => Err((MadeProgress, fail)),
NoProgress => Ok((NoProgress, value, state)), NoProgress => Ok((NoProgress, value, state)),
} }
} }
@ -1220,7 +1223,7 @@ where
match parser.parse(arena, state, min_indent) { match parser.parse(arena, state, min_indent) {
Ok((progress, out1, state)) => Ok((progress, Some(out1), state)), Ok((progress, out1, state)) => Ok((progress, Some(out1), state)),
Err((_, _, _)) => { Err((_, _)) => {
// NOTE this will backtrack // NOTE this will backtrack
// TODO can we get rid of some of the potential backtracking? // TODO can we get rid of some of the potential backtracking?
Ok((NoProgress, None, original_state)) Ok((NoProgress, None, original_state))
@ -1259,16 +1262,14 @@ macro_rules! loc {
#[macro_export] #[macro_export]
macro_rules! skip_first { macro_rules! skip_first {
($p1:expr, $p2:expr) => { ($p1:expr, $p2:expr) => {
move |arena, state: $crate::state::State<'a>, min_indent: u32| { move |arena, state: $crate::state::State<'a>, min_indent: u32| match $p1
let original_state = state.clone(); .parse(arena, state, min_indent)
{
match $p1.parse(arena, state, min_indent) { Ok((p1, _, state)) => match $p2.parse(arena, state, min_indent) {
Ok((p1, _, state)) => match $p2.parse(arena, state, min_indent) { Ok((p2, out2, state)) => Ok((p1.or(p2), out2, state)),
Ok((p2, out2, state)) => Ok((p1.or(p2), out2, state)), Err((p2, fail)) => Err((p1.or(p2), fail)),
Err((p2, fail, _)) => Err((p1.or(p2), fail, original_state)), },
}, Err((progress, fail)) => Err((progress, fail)),
Err((progress, fail, _)) => Err((progress, fail, original_state)),
}
} }
}; };
} }
@ -1278,16 +1279,14 @@ macro_rules! skip_first {
#[macro_export] #[macro_export]
macro_rules! skip_second { macro_rules! skip_second {
($p1:expr, $p2:expr) => { ($p1:expr, $p2:expr) => {
move |arena, state: $crate::state::State<'a>, min_indent: u32| { move |arena, state: $crate::state::State<'a>, min_indent: u32| match $p1
let original_state = state.clone(); .parse(arena, state, min_indent)
{
match $p1.parse(arena, state, min_indent) { Ok((p1, out1, state)) => match $p2.parse(arena, state, min_indent) {
Ok((p1, out1, state)) => match $p2.parse(arena, state, min_indent) { Ok((p2, _, state)) => Ok((p1.or(p2), out1, state)),
Ok((p2, _, state)) => Ok((p1.or(p2), out1, state)), Err((p2, fail)) => Err((p1.or(p2), fail)),
Err((p2, fail, _)) => Err((p1.or(p2), fail, original_state)), },
}, Err((progress, fail)) => Err((progress, fail)),
Err((progress, fail, _)) => Err((progress, fail, original_state)),
}
} }
}; };
} }
@ -1383,6 +1382,23 @@ macro_rules! succeed {
}; };
} }
pub fn fail_when<'a, T, T2, E, F, P>(f: F, p: P) -> impl Parser<'a, T, E>
where
T: 'a,
T2: 'a,
E: 'a,
F: Fn(Position) -> E,
P: Parser<'a, T2, E>,
{
move |arena: &'a bumpalo::Bump, state: State<'a>, min_indent: u32| {
let original_state = state.clone();
match p.parse(arena, state, min_indent) {
Ok((_, _, _)) => Err((MadeProgress, f(original_state.pos()))),
Err((progress, err)) => Err((progress, err)),
}
}
}
pub fn fail<'a, T, E, F>(f: F) -> impl Parser<'a, T, E> pub fn fail<'a, T, E, F>(f: F) -> impl Parser<'a, T, E>
where where
T: 'a, T: 'a,
@ -1390,25 +1406,21 @@ where
F: Fn(Position) -> E, F: Fn(Position) -> E,
{ {
move |_arena: &'a bumpalo::Bump, state: State<'a>, _min_indent: u32| { move |_arena: &'a bumpalo::Bump, state: State<'a>, _min_indent: u32| {
Err((NoProgress, f(state.pos()), state)) Err((NoProgress, f(state.pos())))
} }
} }
#[macro_export] #[macro_export]
macro_rules! and { macro_rules! and {
($p1:expr, $p2:expr) => { ($p1:expr, $p2:expr) => {
move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| { move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| match $p1
// We have to clone this because if the first parser passes and then .parse(arena, state, min_indent)
// the second one fails, we need to revert back to the original state. {
let original_state = state.clone(); Ok((p1, out1, state)) => match $p2.parse(arena, state, min_indent) {
Ok((p2, out2, state)) => Ok((p1.or(p2), (out1, out2), state)),
match $p1.parse(arena, state, min_indent) { Err((p2, fail)) => Err((p1.or(p2), fail)),
Ok((p1, out1, state)) => match $p2.parse(arena, state, min_indent) { },
Ok((p2, out2, state)) => Ok((p1.or(p2), (out1, out2), state)), Err((progress, fail)) => Err((progress, fail)),
Err((p2, fail, _)) => Err((p1.or(p2), fail, original_state)),
},
Err((progress, fail, state)) => Err((progress, fail, state)),
}
} }
}; };
} }
@ -1430,16 +1442,12 @@ macro_rules! indented_seq {
let p1_indent = start_indent; let p1_indent = start_indent;
let p2_indent = p1_indent + 1; let p2_indent = p1_indent + 1;
// We have to clone this because if the first parser passes and then
// the second one fails, we need to revert back to the original state.
let original_state = state.clone();
match $p1.parse(arena, state, p1_indent) { match $p1.parse(arena, state, p1_indent) {
Ok((p1, (), state)) => match $p2.parse(arena, state, p2_indent) { Ok((p1, (), state)) => match $p2.parse(arena, state, p2_indent) {
Ok((p2, out2, state)) => Ok((p1.or(p2), out2, state)), Ok((p2, out2, state)) => Ok((p1.or(p2), out2, state)),
Err((p2, fail, _)) => Err((p1.or(p2), fail, original_state)), Err((p2, fail)) => Err((p1.or(p2), fail)),
}, },
Err((progress, fail, state)) => Err((progress, fail, state)), Err((progress, fail)) => Err((progress, fail)),
} }
} }
}; };
@ -1456,16 +1464,12 @@ macro_rules! absolute_indented_seq {
let p1_indent = start_indent; let p1_indent = start_indent;
let p2_indent = p1_indent + 1; let p2_indent = p1_indent + 1;
// We have to clone this because if the first parser passes and then
// the second one fails, we need to revert back to the original state.
let original_state = state.clone();
match $p1.parse(arena, state, p1_indent) { match $p1.parse(arena, state, p1_indent) {
Ok((p1, out1, state)) => match $p2.parse(arena, state, p2_indent) { Ok((p1, out1, state)) => match $p2.parse(arena, state, p2_indent) {
Ok((p2, out2, state)) => Ok((p1.or(p2), (out1, out2), state)), Ok((p2, out2, state)) => Ok((p1.or(p2), (out1, out2), state)),
Err((p2, fail, _)) => Err((p1.or(p2), fail, original_state)), Err((p2, fail)) => Err((p1.or(p2), fail)),
}, },
Err((progress, fail, state)) => Err((progress, fail, state)), Err((progress, fail)) => Err((progress, fail)),
} }
} }
}; };
@ -1479,8 +1483,8 @@ macro_rules! one_of {
match $p1.parse(arena, state, min_indent) { match $p1.parse(arena, state, min_indent) {
valid @ Ok(_) => valid, valid @ Ok(_) => valid,
Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state)), Err((MadeProgress, fail)) => Err((MadeProgress, fail)),
Err((NoProgress, _, _)) => $p2.parse(arena, original_state, min_indent), Err((NoProgress, _)) => $p2.parse(arena, original_state, min_indent),
} }
} }
}; };
@ -1496,12 +1500,14 @@ macro_rules! one_of {
#[macro_export] #[macro_export]
macro_rules! maybe { macro_rules! maybe {
($p1:expr) => { ($p1:expr) => {
move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| match $p1 move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| {
.parse(arena, state, min_indent) let original_state = state.clone();
{
Ok((progress, value, state)) => Ok((progress, Some(value), state)), match $p1.parse(arena, state, min_indent) {
Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state)), Ok((progress, value, state)) => Ok((progress, Some(value), state)),
Err((NoProgress, _, state)) => Ok((NoProgress, None, state)), Err((MadeProgress, fail)) => Err((MadeProgress, fail)),
Err((NoProgress, _)) => Ok((NoProgress, None, original_state)),
}
} }
}; };
} }
@ -1510,11 +1516,12 @@ macro_rules! maybe {
macro_rules! one_of_with_error { macro_rules! one_of_with_error {
($toerror:expr; $p1:expr) => { ($toerror:expr; $p1:expr) => {
move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| { move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| {
let original_state = state.clone();
match $p1.parse(arena, state, min_indent) { match $p1.parse(arena, state, min_indent) {
valid @ Ok(_) => valid, valid @ Ok(_) => valid,
Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state )), Err((MadeProgress, fail)) => Err((MadeProgress, fail)),
Err((NoProgress, _, state)) => Err((MadeProgress, $toerror(state.pos()), state)), Err((NoProgress, _)) => Err((MadeProgress, $toerror(original_state.pos()))),
} }
} }
}; };
@ -1571,24 +1578,11 @@ where
P: Parser<'a, T, X>, P: Parser<'a, T, X>,
Y: 'a, Y: 'a,
{ {
move |a, s, min_indent| match parser.parse(a, s, min_indent) { move |a, state: State<'a>, min_indent| {
Ok(t) => Ok(t), let original_state = state.clone();
Err((p, error, s)) => Err((p, map_error(error, s.pos()), s)), match parser.parse(a, state, min_indent) {
}
}
/// Like `specialize`, except the error function receives a Region representing the begin/end of the error
pub fn specialize_region<'a, F, P, T, X, Y>(map_error: F, parser: P) -> impl Parser<'a, T, Y>
where
F: Fn(X, Region) -> Y,
P: Parser<'a, T, X>,
Y: 'a,
{
move |a, s: State<'a>, min_indent: u32| {
let start = s.pos();
match parser.parse(a, s, min_indent) {
Ok(t) => Ok(t), Ok(t) => Ok(t),
Err((p, error, s)) => Err((p, map_error(error, Region::new(start, s.pos())), s)), Err((p, error)) => Err((p, map_error(error, original_state.pos()))),
} }
} }
} }
@ -1600,9 +1594,12 @@ where
Y: 'a, Y: 'a,
X: 'a, X: 'a,
{ {
move |a, s, min_indent| match parser.parse(a, s, min_indent) { move |a, state: State<'a>, min_indent| {
Ok(t) => Ok(t), let original_state = state.clone();
Err((p, error, s)) => Err((p, map_error(a.alloc(error), s.pos()), s)), match parser.parse(a, state, min_indent) {
Ok(t) => Ok(t),
Err((p, error)) => Err((p, map_error(a.alloc(error), original_state.pos()))),
}
} }
} }
@ -1618,7 +1615,7 @@ where
let state = state.advance(1); let state = state.advance(1);
Ok((MadeProgress, (), state)) Ok((MadeProgress, (), state))
} }
_ => Err((NoProgress, to_error(state.pos()), state)), _ => Err((NoProgress, to_error(state.pos()))),
} }
} }
@ -1631,7 +1628,7 @@ where
move |_arena: &'a Bump, state: State<'a>, min_indent: u32| { move |_arena: &'a Bump, state: State<'a>, min_indent: u32| {
if min_indent > state.column() { if min_indent > state.column() {
return Err((NoProgress, to_error(state.pos()), state)); return Err((NoProgress, to_error(state.pos())));
} }
match state.bytes().first() { match state.bytes().first() {
@ -1639,7 +1636,7 @@ where
let state = state.advance(1); let state = state.advance(1);
Ok((MadeProgress, (), state)) Ok((MadeProgress, (), state))
} }
_ => Err((NoProgress, to_error(state.pos()), state)), _ => Err((NoProgress, to_error(state.pos()))),
} }
} }
} }
@ -1659,7 +1656,7 @@ where
let state = state.advance(2); let state = state.advance(2);
Ok((MadeProgress, (), state)) Ok((MadeProgress, (), state))
} else { } else {
Err((NoProgress, to_error(state.pos()), state)) Err((NoProgress, to_error(state.pos())))
} }
} }
} }
@ -1685,7 +1682,7 @@ where
let state = state.advance(3); let state = state.advance(3);
Ok((MadeProgress, (), state)) Ok((MadeProgress, (), state))
} else { } else {
Err((NoProgress, to_error(state.pos()), state)) Err((NoProgress, to_error(state.pos())))
} }
} }
} }
@ -1730,6 +1727,8 @@ macro_rules! zero_or_more {
move |arena, state: State<'a>, min_indent: u32| { move |arena, state: State<'a>, min_indent: u32| {
use bumpalo::collections::Vec; use bumpalo::collections::Vec;
let original_state = state.clone();
let start_bytes_len = state.bytes().len(); let start_bytes_len = state.bytes().len();
match $parser.parse(arena, state, min_indent) { match $parser.parse(arena, state, min_indent) {
@ -1740,16 +1739,17 @@ macro_rules! zero_or_more {
buf.push(first_output); buf.push(first_output);
loop { loop {
let old_state = state.clone();
match $parser.parse(arena, state, min_indent) { match $parser.parse(arena, state, min_indent) {
Ok((_, next_output, next_state)) => { Ok((_, next_output, next_state)) => {
state = next_state; state = next_state;
buf.push(next_output); buf.push(next_output);
} }
Err((fail_progress, fail, old_state)) => { Err((fail_progress, fail)) => {
match fail_progress { match fail_progress {
MadeProgress => { MadeProgress => {
// made progress on an element and then failed; that's an error // made progress on an element and then failed; that's an error
return Err((MadeProgress, fail, old_state)); return Err((MadeProgress, fail));
} }
NoProgress => { NoProgress => {
// the next element failed with no progress // the next element failed with no progress
@ -1762,16 +1762,16 @@ macro_rules! zero_or_more {
} }
} }
} }
Err((fail_progress, fail, new_state)) => { Err((fail_progress, fail)) => {
match fail_progress { match fail_progress {
MadeProgress => { MadeProgress => {
// made progress on an element and then failed; that's an error // made progress on an element and then failed; that's an error
Err((MadeProgress, fail, new_state)) Err((MadeProgress, fail))
} }
NoProgress => { NoProgress => {
// the first element failed (with no progress), but that's OK // the first element failed (with no progress), but that's OK
// because we only need to parse 0 elements // because we only need to parse 0 elements
Ok((NoProgress, Vec::new_in(arena), new_state)) Ok((NoProgress, Vec::new_in(arena), original_state))
} }
} }
} }
@ -1794,15 +1794,16 @@ macro_rules! one_or_more {
buf.push(first_output); buf.push(first_output);
loop { loop {
let old_state = state.clone();
match $parser.parse(arena, state, min_indent) { match $parser.parse(arena, state, min_indent) {
Ok((_, next_output, next_state)) => { Ok((_, next_output, next_state)) => {
state = next_state; state = next_state;
buf.push(next_output); buf.push(next_output);
} }
Err((NoProgress, _, old_state)) => { Err((NoProgress, _)) => {
return Ok((MadeProgress, buf, old_state)); return Ok((MadeProgress, buf, old_state));
} }
Err((MadeProgress, fail, old_state)) => { Err((MadeProgress, fail)) => {
return Err((MadeProgress, fail, old_state)); return Err((MadeProgress, fail, old_state));
} }
} }
@ -1828,19 +1829,22 @@ macro_rules! debug {
#[macro_export] #[macro_export]
macro_rules! either { macro_rules! either {
($p1:expr, $p2:expr) => { ($p1:expr, $p2:expr) => {
move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| match $p1 move |arena: &'a bumpalo::Bump, state: $crate::state::State<'a>, min_indent: u32| {
.parse(arena, state, min_indent) let original_state = state.clone();
{ match $p1.parse(arena, state, min_indent) {
Ok((progress, output, state)) => {
Ok((progress, $crate::parser::Either::First(output), state))
}
Err((NoProgress, _, state)) => match $p2.parse(arena, state, min_indent) {
Ok((progress, output, state)) => { Ok((progress, output, state)) => {
Ok((progress, $crate::parser::Either::Second(output), state)) Ok((progress, $crate::parser::Either::First(output), state))
} }
Err((progress, fail, state)) => Err((progress, fail, state)), Err((NoProgress, _)) => {
}, match $p2.parse(arena, original_state.clone(), min_indent) {
Err((MadeProgress, fail, state)) => Err((MadeProgress, fail, state)), Ok((progress, output, state)) => {
Ok((progress, $crate::parser::Either::Second(output), state))
}
Err((progress, fail)) => Err((progress, fail)),
}
}
Err((MadeProgress, fail)) => Err((MadeProgress, fail)),
}
} }
}; };
} }
@ -1902,12 +1906,10 @@ where
P: Parser<'a, Val, Error>, P: Parser<'a, Val, Error>,
Error: 'a, Error: 'a,
{ {
move |arena: &'a Bump, state: State<'a>, min_indent: u32| { move |arena: &'a Bump, state: State<'a>, min_indent: u32| match parser
let old_state = state.clone(); .parse(arena, state, min_indent)
{
match parser.parse(arena, state, min_indent) { Ok((_, a, s1)) => Ok((NoProgress, a, s1)),
Ok((_, a, s1)) => Ok((NoProgress, a, s1)), Err((_, f)) => Err((NoProgress, f)),
Err((_, f, _)) => Err((NoProgress, f, old_state)),
}
} }
} }

View file

@ -3,8 +3,8 @@ use crate::blankspace::{space0_before_e, space0_e};
use crate::ident::{lowercase_ident, parse_ident, Ident}; use crate::ident::{lowercase_ident, parse_ident, Ident};
use crate::parser::Progress::{self, *}; use crate::parser::Progress::{self, *};
use crate::parser::{ use crate::parser::{
backtrackable, optional, specialize, specialize_ref, then, word1, word2, word3, EPattern, backtrackable, fail_when, optional, specialize, specialize_ref, then, word1, word2, word3,
PInParens, PList, PRecord, ParseResult, Parser, EPattern, PInParens, PList, PRecord, Parser,
}; };
use crate::state::State; use crate::state::State;
use bumpalo::collections::string::String; use bumpalo::collections::string::String;
@ -24,15 +24,7 @@ pub enum PatternType {
WhenBranch, WhenBranch,
} }
pub fn loc_closure_param<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, EPattern<'a>> { pub fn closure_param<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, EPattern<'a>> {
parse_closure_param
}
fn parse_closure_param<'a>(
arena: &'a Bump,
state: State<'a>,
min_indent: u32,
) -> ParseResult<'a, Loc<Pattern<'a>>, EPattern<'a>> {
one_of!( one_of!(
// An ident is the most common param, e.g. \foo -> ... // An ident is the most common param, e.g. \foo -> ...
loc_ident_pattern_help(true), loc_ident_pattern_help(true),
@ -47,7 +39,6 @@ fn parse_closure_param<'a>(
// e.g. \User.UserId userId -> ... // e.g. \User.UserId userId -> ...
specialize(EPattern::PInParens, loc_pattern_in_parens_help()) specialize(EPattern::PInParens, loc_pattern_in_parens_help())
) )
.parse(arena, state, min_indent)
} }
pub fn loc_pattern_help<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, EPattern<'a>> { pub fn loc_pattern_help<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, EPattern<'a>> {
@ -88,16 +79,12 @@ fn loc_tag_pattern_arg<'a>(
min_indent, min_indent,
)?; )?;
let (_, loc_pat, state) = loc_parse_tag_pattern_arg(min_indent, arena, state)?; let (_, loc_pat, state) = loc_parse_tag_pattern_arg().parse(arena, state, min_indent)?;
let Loc { region, value } = loc_pat; let Loc { region, value } = loc_pat;
if stop_on_has_kw && matches!(value, Pattern::Identifier("has")) { if stop_on_has_kw && matches!(value, Pattern::Identifier("has")) {
Err(( Err((NoProgress, EPattern::End(original_state.pos())))
NoProgress,
EPattern::End(original_state.pos()),
original_state,
))
} else { } else {
Ok(( Ok((
MadeProgress, MadeProgress,
@ -119,17 +106,13 @@ pub fn loc_has_parser<'a>() -> impl Parser<'a, Loc<Has<'a>>, EPattern<'a>> {
if matches!(pattern.value, Pattern::Identifier("has")) { if matches!(pattern.value, Pattern::Identifier("has")) {
Ok((progress, Loc::at(pattern.region, Has::Has), state)) Ok((progress, Loc::at(pattern.region, Has::Has), state))
} else { } else {
Err((progress, EPattern::End(state.pos()), state)) Err((progress, EPattern::End(state.pos())))
} }
}, },
) )
} }
fn loc_parse_tag_pattern_arg<'a>( fn loc_parse_tag_pattern_arg<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, EPattern<'a>> {
min_indent: u32,
arena: &'a Bump,
state: State<'a>,
) -> ParseResult<'a, Loc<Pattern<'a>>, EPattern<'a>> {
one_of!( one_of!(
specialize(EPattern::PInParens, loc_pattern_in_parens_help()), specialize(EPattern::PInParens, loc_pattern_in_parens_help()),
loc!(underscore_pattern_help()), loc!(underscore_pattern_help()),
@ -143,7 +126,6 @@ fn loc_parse_tag_pattern_arg<'a>(
loc!(single_quote_pattern_help()), loc!(single_quote_pattern_help()),
loc!(number_pattern_help()) loc!(number_pattern_help())
) )
.parse(arena, state, min_indent)
} }
fn loc_pattern_in_parens_help<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PInParens<'a>> { fn loc_pattern_in_parens_help<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PInParens<'a>> {
@ -168,7 +150,7 @@ fn loc_pattern_in_parens_help<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PInPare
state, state,
)) ))
} else if elements.is_empty() { } else if elements.is_empty() {
Err((NoProgress, PInParens::Empty(state.pos()), state)) Err((NoProgress, PInParens::Empty(state.pos())))
} else { } else {
// TODO: don't discard comments before/after // TODO: don't discard comments before/after
// (stored in the Collection) // (stored in the Collection)
@ -221,8 +203,8 @@ fn single_quote_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>>
} }
fn list_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, PList<'a>> { fn list_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, PList<'a>> {
move |arena, state, min_indent| { map!(
let (_, pats, state) = collection_trailing_sep_e!( collection_trailing_sep_e!(
word1(b'[', PList::Open), word1(b'[', PList::Open),
list_element_pattern(), list_element_pattern(),
word1(b',', PList::End), word1(b',', PList::End),
@ -230,13 +212,9 @@ fn list_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, PList<'a>> {
PList::Open, PList::Open,
PList::IndentEnd, PList::IndentEnd,
Pattern::SpaceBefore Pattern::SpaceBefore
) ),
.parse(arena, state, min_indent)?; Pattern::List
)
let result = Pattern::List(pats);
Ok((MadeProgress, result, state))
}
} }
fn list_element_pattern<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PList<'a>> { fn list_element_pattern<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PList<'a>> {
@ -248,12 +226,7 @@ fn list_element_pattern<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PList<'a>> {
} }
fn three_list_rest_pattern_error<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PList<'a>> { fn three_list_rest_pattern_error<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PList<'a>> {
then( fail_when(PList::Rest, loc!(word3(b'.', b'.', b'.', PList::Rest)))
loc!(word3(b'.', b'.', b'.', PList::Rest)),
|_arena, state, _progress, word| {
Err((MadeProgress, PList::Rest(word.region.start()), state))
},
)
} }
fn list_rest_pattern<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PList<'a>> { fn list_rest_pattern<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PList<'a>> {
@ -330,11 +303,7 @@ fn loc_ident_pattern_help<'a>(
// Plain identifiers (e.g. `foo`) are allowed in patterns, but // Plain identifiers (e.g. `foo`) are allowed in patterns, but
// more complex ones (e.g. `Foo.bar` or `foo.bar.baz`) are not. // more complex ones (e.g. `Foo.bar` or `foo.bar.baz`) are not.
if crate::keyword::KEYWORDS.contains(&parts[0]) { if crate::keyword::KEYWORDS.contains(&parts[0]) {
Err(( Err((NoProgress, EPattern::End(original_state.pos())))
NoProgress,
EPattern::End(original_state.pos()),
original_state,
))
} else if module_name.is_empty() && parts.len() == 1 { } else if module_name.is_empty() && parts.len() == 1 {
Ok(( Ok((
MadeProgress, MadeProgress,
@ -387,34 +356,26 @@ fn loc_ident_pattern_help<'a>(
} }
fn underscore_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> { fn underscore_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, EPattern<'a>> {
move |arena: &'a Bump, state: State<'a>, min_indent: u32| { map!(
let (_, _, next_state) = skip_first!(
word1(b'_', EPattern::Underscore).parse(arena, state, min_indent)?; word1(b'_', EPattern::Underscore),
optional(lowercase_ident_pattern())
let (_, output, final_state) = ),
optional(lowercase_ident_pattern).parse(arena, next_state, min_indent)?; |output| match output {
Some(name) => Pattern::Underscore(name),
match output { None => Pattern::Underscore(""),
Some(name) => Ok((MadeProgress, Pattern::Underscore(name), final_state)),
None => Ok((MadeProgress, Pattern::Underscore(""), final_state)),
} }
} )
} }
fn lowercase_ident_pattern<'a>( fn lowercase_ident_pattern<'a>() -> impl Parser<'a, &'a str, EPattern<'a>> {
arena: &'a Bump, specialize(move |_, pos| EPattern::End(pos), lowercase_ident())
state: State<'a>,
min_indent: u32,
) -> ParseResult<'a, &'a str, EPattern<'a>> {
let pos = state.pos();
specialize(move |_, _| EPattern::End(pos), lowercase_ident()).parse(arena, state, min_indent)
} }
#[inline(always)] #[inline(always)]
fn record_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, PRecord<'a>> { fn record_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, PRecord<'a>> {
move |arena, state, min_indent| { map!(
let (_, fields, state) = collection_trailing_sep_e!( collection_trailing_sep_e!(
word1(b'{', PRecord::Open), word1(b'{', PRecord::Open),
record_pattern_field(), record_pattern_field(),
word1(b',', PRecord::End), word1(b',', PRecord::End),
@ -422,13 +383,9 @@ fn record_pattern_help<'a>() -> impl Parser<'a, Pattern<'a>, PRecord<'a>> {
PRecord::Open, PRecord::Open,
PRecord::IndentEnd, PRecord::IndentEnd,
Pattern::SpaceBefore Pattern::SpaceBefore
) ),
.parse(arena, state, min_indent)?; Pattern::RecordDestructure
)
let result = Pattern::RecordDestructure(fields);
Ok((MadeProgress, result, state))
}
} }
fn record_pattern_field<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PRecord<'a>> { fn record_pattern_field<'a>() -> impl Parser<'a, Loc<Pattern<'a>>, PRecord<'a>> {

View file

@ -58,9 +58,9 @@ impl<'a> State<'a> {
&self, &self,
indent: u32, indent: u32,
e: impl Fn(Position) -> E, e: impl Fn(Position) -> E,
) -> Result<u32, (Progress, E, State<'a>)> { ) -> Result<u32, (Progress, E)> {
if self.column() < indent { if self.column() < indent {
Err((Progress::NoProgress, e(self.pos()), self.clone())) Err((Progress::NoProgress, e(self.pos())))
} else { } else {
Ok(std::cmp::max(indent, self.line_indent())) Ok(std::cmp::max(indent, self.line_indent()))
} }

View file

@ -19,7 +19,7 @@ fn ascii_hex_digits<'a>() -> impl Parser<'a, &'a str, EString<'a>> {
buf.push(byte as char); buf.push(byte as char);
} else if buf.is_empty() { } else if buf.is_empty() {
// We didn't find any hex digits! // We didn't find any hex digits!
return Err((NoProgress, EString::CodePtEnd(state.pos()), state)); return Err((NoProgress, EString::CodePtEnd(state.pos())));
} else { } else {
state.advance_mut(buf.len()); state.advance_mut(buf.len());
@ -27,7 +27,7 @@ fn ascii_hex_digits<'a>() -> impl Parser<'a, &'a str, EString<'a>> {
} }
} }
Err((NoProgress, EString::CodePtEnd(state.pos()), state)) Err((NoProgress, EString::CodePtEnd(state.pos())))
} }
} }
@ -36,7 +36,7 @@ pub fn parse_single_quote<'a>() -> impl Parser<'a, &'a str, EString<'a>> {
if state.consume_mut("\'") { if state.consume_mut("\'") {
// we will be parsing a single-quote-string // we will be parsing a single-quote-string
} else { } else {
return Err((NoProgress, EString::Open(state.pos()), state)); return Err((NoProgress, EString::Open(state.pos())));
} }
// Handle back slaches in byte literal // Handle back slaches in byte literal
@ -64,18 +64,18 @@ pub fn parse_single_quote<'a>() -> impl Parser<'a, &'a str, EString<'a>> {
return Ok((MadeProgress, &*arena.alloc_str(&test.to_string()), state)); return Ok((MadeProgress, &*arena.alloc_str(&test.to_string()), state));
} }
// invalid error, backslah escaping something we do not recognize // invalid error, backslah escaping something we do not recognize
return Err((NoProgress, EString::CodePtEnd(state.pos()), state)); return Err((NoProgress, EString::CodePtEnd(state.pos())));
} }
None => { None => {
// no close quote found // no close quote found
return Err((NoProgress, EString::CodePtEnd(state.pos()), state)); return Err((NoProgress, EString::CodePtEnd(state.pos())));
} }
} }
} }
Some(_) => { Some(_) => {
// do nothing for other characters, handled below // do nothing for other characters, handled below
} }
None => return Err((NoProgress, EString::CodePtEnd(state.pos()), state)), None => return Err((NoProgress, EString::CodePtEnd(state.pos()))),
} }
let mut bytes = state.bytes().iter(); let mut bytes = state.bytes().iter();
@ -90,7 +90,7 @@ pub fn parse_single_quote<'a>() -> impl Parser<'a, &'a str, EString<'a>> {
} }
Some(_) => end_index += 1, Some(_) => end_index += 1,
None => { None => {
return Err((NoProgress, EString::Open(state.pos()), state)); return Err((NoProgress, EString::Open(state.pos())));
} }
} }
} }
@ -99,12 +99,12 @@ pub fn parse_single_quote<'a>() -> impl Parser<'a, &'a str, EString<'a>> {
// no progress was made // no progress was made
// this case is a double single quote, ex: '' // this case is a double single quote, ex: ''
// not supporting empty single quotes // not supporting empty single quotes
return Err((NoProgress, EString::Open(state.pos()), state)); return Err((NoProgress, EString::Open(state.pos())));
} }
if end_index > (std::mem::size_of::<u32>() + 1) { if end_index > (std::mem::size_of::<u32>() + 1) {
// bad case: too big to fit into u32 // bad case: too big to fit into u32
return Err((NoProgress, EString::Open(state.pos()), state)); return Err((NoProgress, EString::Open(state.pos())));
} }
// happy case -> we have some bytes that will fit into a u32 // happy case -> we have some bytes that will fit into a u32
@ -116,13 +116,13 @@ pub fn parse_single_quote<'a>() -> impl Parser<'a, &'a str, EString<'a>> {
Ok(string) => Ok((MadeProgress, string, state)), Ok(string) => Ok((MadeProgress, string, state)),
Err(_) => { Err(_) => {
// invalid UTF-8 // invalid UTF-8
return Err((NoProgress, EString::CodePtEnd(state.pos()), state)); return Err((NoProgress, EString::CodePtEnd(state.pos())));
} }
} }
} }
} }
fn consume_indent(mut state: State, mut indent: u32) -> Result<State, (Progress, EString, State)> { fn consume_indent(mut state: State, mut indent: u32) -> Result<State, (Progress, EString)> {
while indent > 0 { while indent > 0 {
match state.bytes().first() { match state.bytes().first() {
Some(b' ') => { Some(b' ') => {
@ -136,7 +136,6 @@ fn consume_indent(mut state: State, mut indent: u32) -> Result<State, (Progress,
return Err(( return Err((
MadeProgress, MadeProgress,
EString::MultilineInsufficientIndent(state.pos()), EString::MultilineInsufficientIndent(state.pos()),
state,
)); ));
} }
} }
@ -145,10 +144,7 @@ fn consume_indent(mut state: State, mut indent: u32) -> Result<State, (Progress,
Ok(state) Ok(state)
} }
fn utf8<'a>( fn utf8<'a>(state: State<'a>, string_bytes: &'a [u8]) -> Result<&'a str, (Progress, EString<'a>)> {
state: State<'a>,
string_bytes: &'a [u8],
) -> Result<&'a str, (Progress, EString<'a>, State<'a>)> {
std::str::from_utf8(string_bytes).map_err(|_| { std::str::from_utf8(string_bytes).map_err(|_| {
// Note Based on where this `utf8` function is used, the fact that we know the whole string // Note Based on where this `utf8` function is used, the fact that we know the whole string
// in the parser is valid utf8, and barring bugs in the parser itself // in the parser is valid utf8, and barring bugs in the parser itself
@ -156,7 +152,6 @@ fn utf8<'a>(
( (
MadeProgress, MadeProgress,
EString::Space(BadInputError::BadUtf8, state.pos()), EString::Space(BadInputError::BadUtf8, state.pos()),
state,
) )
}) })
} }
@ -186,7 +181,7 @@ pub fn parse<'a>() -> impl Parser<'a, StrLiteral<'a>, EString<'a>> {
// we will be parsing a single-line string // we will be parsing a single-line string
is_multiline = false; is_multiline = false;
} else { } else {
return Err((NoProgress, EString::Open(state.pos()), state)); return Err((NoProgress, EString::Open(state.pos())));
} }
let mut bytes = state.bytes().iter(); let mut bytes = state.bytes().iter();
@ -227,7 +222,6 @@ pub fn parse<'a>() -> impl Parser<'a, StrLiteral<'a>, EString<'a>> {
return Err(( return Err((
MadeProgress, MadeProgress,
EString::Space(BadInputError::BadUtf8, state.pos()), EString::Space(BadInputError::BadUtf8, state.pos()),
state,
)); ));
} }
} }
@ -336,11 +330,7 @@ pub fn parse<'a>() -> impl Parser<'a, StrLiteral<'a>, EString<'a>> {
// all remaining chars. This will mask all other errors, but // all remaining chars. This will mask all other errors, but
// it should make it easiest to debug; the file will be a giant // it should make it easiest to debug; the file will be a giant
// error starting from where the open quote appeared. // error starting from where the open quote appeared.
return Err(( return Err((MadeProgress, EString::EndlessSingle(start_state.pos())));
MadeProgress,
EString::EndlessSingle(start_state.pos()),
start_state,
));
} }
} }
b'\\' => { b'\\' => {
@ -432,7 +422,7 @@ pub fn parse<'a>() -> impl Parser<'a, StrLiteral<'a>, EString<'a>> {
// Invalid escape! A backslash must be followed // Invalid escape! A backslash must be followed
// by either an open paren or else one of the // by either an open paren or else one of the
// escapable characters (\n, \t, \", \\, etc) // escapable characters (\n, \t, \", \\, etc)
return Err((MadeProgress, EString::UnknownEscape(state.pos()), state)); return Err((MadeProgress, EString::UnknownEscape(state.pos())));
} }
} }
} }
@ -450,7 +440,6 @@ pub fn parse<'a>() -> impl Parser<'a, StrLiteral<'a>, EString<'a>> {
} else { } else {
EString::EndlessSingle(start_state.pos()) EString::EndlessSingle(start_state.pos())
}, },
start_state,
)) ))
} }
} }

View file

@ -104,7 +104,7 @@ fn parse_type_alias_after_as<'a>() -> impl Parser<'a, TypeHeader<'a>, EType<'a>>
match res { match res {
Ok(header) => Ok((progress, header, state)), Ok(header) => Ok((progress, header, state)),
Err(err) => Err((progress, EType::TInlineAlias(err, state.pos()), state)), Err(err) => Err((progress, EType::TInlineAlias(err, state.pos()))),
} }
}, },
) )
@ -242,11 +242,13 @@ where
F: Fn(Position) -> E, F: Fn(Position) -> E,
E: 'a, E: 'a,
{ {
move |arena, state: State<'a>, min_indent: u32| match crate::ident::tag_name() move |arena, state: State<'a>, min_indent: u32| match crate::ident::tag_name().parse(
.parse(arena, state, min_indent) arena,
{ state.clone(),
min_indent,
) {
Ok(good) => Ok(good), Ok(good) => Ok(good),
Err((progress, _, state)) => Err((progress, to_problem(state.pos()), state)), Err((progress, _)) => Err((progress, to_problem(state.pos()))),
} }
} }
@ -645,14 +647,15 @@ fn concrete_type<'a>() -> impl Parser<'a, TypeAnnotation<'a>, ETypeApply> {
move |arena: &'a Bump, state: State<'a>, min_indent: u32| { move |arena: &'a Bump, state: State<'a>, min_indent: u32| {
let initial_bytes = state.bytes(); let initial_bytes = state.bytes();
match crate::ident::concrete_type().parse(arena, state, min_indent) { match crate::ident::concrete_type().parse(arena, state.clone(), min_indent) {
Ok((_, (module_name, type_name), state)) => { Ok((_, (module_name, type_name), state)) => {
let answer = TypeAnnotation::Apply(module_name, type_name, &[]); let answer = TypeAnnotation::Apply(module_name, type_name, &[]);
Ok((MadeProgress, answer, state)) Ok((MadeProgress, answer, state))
} }
Err((NoProgress, _, state)) => Err((NoProgress, ETypeApply::End(state.pos()), state)), Err((NoProgress, _)) => Err((NoProgress, ETypeApply::End(state.pos()))),
Err((MadeProgress, _, mut state)) => { Err((MadeProgress, _)) => {
let mut state = state.clone();
// we made some progress, but ultimately failed. // we made some progress, but ultimately failed.
// that means a malformed type name // that means a malformed type name
let chomped = crate::ident::chomp_malformed(state.bytes()); let chomped = crate::ident::chomp_malformed(state.bytes());
@ -671,18 +674,20 @@ fn concrete_type<'a>() -> impl Parser<'a, TypeAnnotation<'a>, ETypeApply> {
fn parse_type_variable<'a>( fn parse_type_variable<'a>(
stop_at_surface_has: bool, stop_at_surface_has: bool,
) -> impl Parser<'a, TypeAnnotation<'a>, EType<'a>> { ) -> impl Parser<'a, TypeAnnotation<'a>, EType<'a>> {
move |arena, state: State<'a>, min_indent: u32| match crate::ident::lowercase_ident() move |arena, state: State<'a>, min_indent: u32| match crate::ident::lowercase_ident().parse(
.parse(arena, state, min_indent) arena,
{ state.clone(),
min_indent,
) {
Ok((_, name, state)) => { Ok((_, name, state)) => {
if name == "has" && stop_at_surface_has { if name == "has" && stop_at_surface_has {
Err((NoProgress, EType::TEnd(state.pos()), state)) Err((NoProgress, EType::TEnd(state.pos())))
} else { } else {
let answer = TypeAnnotation::BoundVariable(name); let answer = TypeAnnotation::BoundVariable(name);
Ok((MadeProgress, answer, state)) Ok((MadeProgress, answer, state))
} }
} }
Err((progress, _, state)) => Err((progress, EType::TBadTypeVariable(state.pos()), state)), Err((progress, _)) => Err((progress, EType::TBadTypeVariable(state.pos()))),
} }
} }

View file

@ -846,7 +846,7 @@ mod test_parse {
Ok((_, _, _state)) => { Ok((_, _, _state)) => {
// dbg!(_state); // dbg!(_state);
} }
Err((_, _fail, _state)) => { Err((_, _fail)) => {
// dbg!(_fail, _state); // dbg!(_fail, _state);
panic!("Failed to parse!"); panic!("Failed to parse!");
} }

View file

@ -621,6 +621,7 @@ fn run_in_place(
state.env state.env
} }
#[derive(Debug)]
enum Work<'a> { enum Work<'a> {
Constraint { Constraint {
env: &'a Env, env: &'a Env,
@ -2232,10 +2233,14 @@ fn either_type_index_to_var(
aliases, aliases,
type_index, type_index,
); );
unsafe {
types.emplace_variable(type_index, var);
}
debug_assert!(
matches!(types[type_index], TypeTag::Variable(v) if v == var)
|| matches!(
types[type_index],
TypeTag::EmptyRecord | TypeTag::EmptyTagUnion
)
);
var var
} }
Err(var_index) => { Err(var_index) => {
@ -2311,14 +2316,17 @@ impl RegisterVariable {
| TypeTag::HostExposedAlias { shared, .. } => { | TypeTag::HostExposedAlias { shared, .. } => {
let AliasShared { symbol, .. } = types[shared]; let AliasShared { symbol, .. } = types[shared];
if let Some(reserved) = Variable::get_reserved(symbol) { if let Some(reserved) = Variable::get_reserved(symbol) {
if rank.is_none() { let direct_var = if rank.is_none() {
// reserved variables are stored with rank NONE // reserved variables are stored with rank NONE
return Direct(reserved); reserved
} else { } else {
// for any other rank, we need to copy; it takes care of adjusting the rank // for any other rank, we need to copy; it takes care of adjusting the rank
let copied = deep_copy_var_in(subs, rank, pools, reserved, arena); deep_copy_var_in(subs, rank, pools, reserved, arena)
return Direct(copied); };
} // Safety: the `destination` will become the source-of-truth for the type index, since it
// was not already transformed before (if it was, we'd be in the Variable branch!)
let _old_typ = unsafe { types.emplace_variable(typ, direct_var) };
return Direct(direct_var);
} }
Deferred Deferred
@ -2334,15 +2342,19 @@ impl RegisterVariable {
pools: &mut Pools, pools: &mut Pools,
arena: &'_ bumpalo::Bump, arena: &'_ bumpalo::Bump,
types: &mut Types, types: &mut Types,
typ: Index<TypeTag>, typ_index: Index<TypeTag>,
stack: &mut bumpalo::collections::Vec<'_, TypeToVar>, stack: &mut bumpalo::collections::Vec<'_, TypeToVar>,
) -> Variable { ) -> Variable {
match Self::from_type(subs, rank, pools, arena, types, typ) { match Self::from_type(subs, rank, pools, arena, types, typ_index) {
Self::Direct(var) => var, Self::Direct(var) => var,
Self::Deferred => { Self::Deferred => {
let var = subs.fresh_unnamed_flex_var(); let var = subs.fresh_unnamed_flex_var();
// Safety: the `destination` will become the source-of-truth for the type index, since it
// was not already transformed before (if it was, it wouldn't be deferred!)
let typ = unsafe { types.emplace_variable(typ_index, var) };
stack.push(TypeToVar::Defer { stack.push(TypeToVar::Defer {
typ, typ,
typ_index,
destination: var, destination: var,
ambient_function: AmbientFunctionPolicy::NoFunction, ambient_function: AmbientFunctionPolicy::NoFunction,
}); });
@ -2405,7 +2417,8 @@ impl AmbientFunctionPolicy {
#[derive(Debug)] #[derive(Debug)]
enum TypeToVar { enum TypeToVar {
Defer { Defer {
typ: Index<TypeTag>, typ: TypeTag,
typ_index: Index<TypeTag>,
destination: Variable, destination: Variable,
ambient_function: AmbientFunctionPolicy, ambient_function: AmbientFunctionPolicy,
}, },
@ -2443,11 +2456,18 @@ fn type_to_variable<'a>(
} }
RegisterVariable::Deferred => { RegisterVariable::Deferred => {
let var = subs.fresh_unnamed_flex_var(); let var = subs.fresh_unnamed_flex_var();
// Safety: the `destination` will become the source-of-truth for the type index, since it
// was not already transformed before (if it was, it wouldn't be deferred!)
let typ = unsafe { types.emplace_variable($typ, var) };
stack.push(TypeToVar::Defer { stack.push(TypeToVar::Defer {
typ: $typ, typ,
typ_index: $typ,
destination: var, destination: var,
ambient_function: $ambient_function_policy, ambient_function: $ambient_function_policy,
}); });
var var
} }
} }
@ -2460,15 +2480,16 @@ fn type_to_variable<'a>(
let result = helper!(typ); let result = helper!(typ);
while let Some(TypeToVar::Defer { while let Some(TypeToVar::Defer {
typ_index,
typ, typ,
destination, destination,
ambient_function, ambient_function,
}) = stack.pop() }) = stack.pop()
{ {
use TypeTag::*; use TypeTag::*;
match types[typ] { match typ {
Variable(_) | EmptyRecord | EmptyTagUnion => { Variable(_) | EmptyRecord | EmptyTagUnion => {
unreachable!("This variant should never be deferred!") unreachable!("This variant should never be deferred!",)
} }
RangedNumber(range) => { RangedNumber(range) => {
let content = Content::RangedNumber(range); let content = Content::RangedNumber(range);
@ -2480,7 +2501,7 @@ fn type_to_variable<'a>(
type_argument_regions: _, type_argument_regions: _,
region: _, region: _,
} => { } => {
let arguments = types.get_type_arguments(typ); let arguments = types.get_type_arguments(typ_index);
let new_arguments = VariableSubsSlice::reserve_into_subs(subs, arguments.len()); let new_arguments = VariableSubsSlice::reserve_into_subs(subs, arguments.len());
for (target_index, var_index) in for (target_index, var_index) in
(new_arguments.indices()).zip(arguments.into_iter()) (new_arguments.indices()).zip(arguments.into_iter())
@ -2499,7 +2520,7 @@ fn type_to_variable<'a>(
name, name,
ambient_function, ambient_function,
} => { } => {
let captures = types.get_type_arguments(typ); let captures = types.get_type_arguments(typ_index);
let union_lambdas = create_union_lambda( let union_lambdas = create_union_lambda(
subs, rank, pools, arena, types, name, captures, &mut stack, subs, rank, pools, arena, types, name, captures, &mut stack,
); );
@ -2546,7 +2567,7 @@ fn type_to_variable<'a>(
} }
// This case is important for the rank of boolean variables // This case is important for the rank of boolean variables
Function(closure_type, ret_type) => { Function(closure_type, ret_type) => {
let arguments = types.get_type_arguments(typ); let arguments = types.get_type_arguments(typ_index);
let new_arguments = VariableSubsSlice::reserve_into_subs(subs, arguments.len()); let new_arguments = VariableSubsSlice::reserve_into_subs(subs, arguments.len());
for (target_index, var_index) in for (target_index, var_index) in
(new_arguments.indices()).zip(arguments.into_iter()) (new_arguments.indices()).zip(arguments.into_iter())
@ -2564,7 +2585,7 @@ fn type_to_variable<'a>(
register_with_known_var(subs, destination, rank, pools, content) register_with_known_var(subs, destination, rank, pools, content)
} }
Record(fields) => { Record(fields) => {
let ext_slice = types.get_type_arguments(typ); let ext_slice = types.get_type_arguments(typ_index);
// An empty fields is inefficient (but would be correct) // An empty fields is inefficient (but would be correct)
// If hit, try to turn the value into an EmptyRecord in canonicalization // If hit, try to turn the value into an EmptyRecord in canonicalization
@ -2611,7 +2632,7 @@ fn type_to_variable<'a>(
} }
TagUnion(tags) => { TagUnion(tags) => {
let ext_slice = types.get_type_arguments(typ); let ext_slice = types.get_type_arguments(typ_index);
// An empty tags is inefficient (but would be correct) // An empty tags is inefficient (but would be correct)
// If hit, try to turn the value into an EmptyTagUnion in canonicalization // If hit, try to turn the value into an EmptyTagUnion in canonicalization
@ -2625,8 +2646,8 @@ fn type_to_variable<'a>(
register_with_known_var(subs, destination, rank, pools, content) register_with_known_var(subs, destination, rank, pools, content)
} }
FunctionOrTagUnion(symbol) => { FunctionOrTagUnion(symbol) => {
let ext_slice = types.get_type_arguments(typ); let ext_slice = types.get_type_arguments(typ_index);
let tag_name = types.get_tag_name(&typ).clone(); let tag_name = types.get_tag_name(&typ_index).clone();
debug_assert!(ext_slice.len() <= 1); debug_assert!(ext_slice.len() <= 1);
let temp_ext_var = match ext_slice.into_iter().next() { let temp_ext_var = match ext_slice.into_iter().next() {
@ -2654,7 +2675,7 @@ fn type_to_variable<'a>(
register_with_known_var(subs, destination, rank, pools, content) register_with_known_var(subs, destination, rank, pools, content)
} }
RecursiveTagUnion(rec_var, tags) => { RecursiveTagUnion(rec_var, tags) => {
let ext_slice = types.get_type_arguments(typ); let ext_slice = types.get_type_arguments(typ_index);
// An empty tags is inefficient (but would be correct) // An empty tags is inefficient (but would be correct)
// If hit, try to turn the value into an EmptyTagUnion in canonicalization // If hit, try to turn the value into an EmptyTagUnion in canonicalization
@ -2692,7 +2713,7 @@ fn type_to_variable<'a>(
infer_ext_in_output_variables, infer_ext_in_output_variables,
} = types[shared]; } = types[shared];
let type_arguments = types.get_type_arguments(typ); let type_arguments = types.get_type_arguments(typ_index);
let alias_variables = { let alias_variables = {
let all_vars_length = type_arguments.len() let all_vars_length = type_arguments.len()
@ -2773,7 +2794,7 @@ fn type_to_variable<'a>(
} }
StructuralAlias { shared, actual } | OpaqueAlias { shared, actual } => { StructuralAlias { shared, actual } | OpaqueAlias { shared, actual } => {
let kind = match types[typ] { let kind = match typ {
StructuralAlias { .. } => AliasKind::Structural, StructuralAlias { .. } => AliasKind::Structural,
OpaqueAlias { .. } => AliasKind::Opaque, OpaqueAlias { .. } => AliasKind::Opaque,
_ => internal_error!(), _ => internal_error!(),
@ -2789,7 +2810,7 @@ fn type_to_variable<'a>(
debug_assert!(roc_types::subs::Variable::get_reserved(symbol).is_none()); debug_assert!(roc_types::subs::Variable::get_reserved(symbol).is_none());
let type_arguments = types.get_type_arguments(typ); let type_arguments = types.get_type_arguments(typ_index);
let alias_variables = { let alias_variables = {
let all_vars_length = type_arguments.len() let all_vars_length = type_arguments.len()
@ -2860,7 +2881,7 @@ fn type_to_variable<'a>(
infer_ext_in_output_variables: _, // TODO infer_ext_in_output_variables: _, // TODO
} = types[shared]; } = types[shared];
let type_arguments = types.get_type_arguments(typ); let type_arguments = types.get_type_arguments(typ_index);
let alias_variables = { let alias_variables = {
let length = type_arguments.len() + lambda_set_variables.len(); let length = type_arguments.len() + lambda_set_variables.len();

View file

@ -382,7 +382,6 @@ mod solve_expr {
let known_specializations = abilities_store.iter_declared_implementations().filter_map( let known_specializations = abilities_store.iter_declared_implementations().filter_map(
|(impl_key, member_impl)| match member_impl { |(impl_key, member_impl)| match member_impl {
MemberImpl::Impl(impl_symbol) => { MemberImpl::Impl(impl_symbol) => {
dbg!(impl_symbol);
let specialization = abilities_store.specialization_info(*impl_symbol).expect( let specialization = abilities_store.specialization_info(*impl_symbol).expect(
"declared implementations should be resolved conclusively after solving", "declared implementations should be resolved conclusively after solving",
); );
@ -8200,6 +8199,31 @@ mod solve_expr {
); );
} }
#[test]
fn inferred_fixed_fixpoints() {
infer_queries!(
indoc!(
r#"
app "test" provides [job] to "./platform"
F : [Bar, FromG G]
G : [G {lst : List F}]
job : { lst : List F } -> G
job = \config -> G config
#^^^{-1}
# ^^^^^^ ^^^^^^^^
"#
),
@r###"
job : { lst : List [Bar, FromG a] } -[[job(0)]]-> [G { lst : List [Bar, FromG a] }] as a
config : { lst : List [Bar, FromG ([G { lst : List [Bar, FromG a] }] as a)] }
G config : [G { lst : List [Bar, FromG a] }] as a
"###
print_only_under_alias: true
);
}
#[test] #[test]
fn fix_recursion_under_alias_issue_4368() { fn fix_recursion_under_alias_issue_4368() {
infer_eq_without_problem( infer_eq_without_problem(
@ -8220,7 +8244,7 @@ mod solve_expr {
"# "#
), ),
"{} -> Task", "{} -> Task",
) );
} }
#[test] #[test]
@ -8260,4 +8284,74 @@ mod solve_expr {
"MDict v -> MDict v | v has Eq", "MDict v -> MDict v | v has Eq",
); );
} }
#[test]
fn unify_types_with_fixed_fixpoints_outside_fixing_region() {
infer_queries!(indoc!(
r#"
app "test" provides [main] to "./platform"
Input := [
FromJob Job
]
Job := [
Job (List Input)
]
job : List Input -> Job
job = \inputs ->
@Job (Job inputs)
helloWorld : Job
helloWorld =
@Job ( Job [ @Input (FromJob greeting) ] )
# ^^^^^^^^^^^^^^^^^^^^^^^^^
greeting : Job
greeting =
job []
main = (\_ -> "Which platform am I running on now?\n") helloWorld
"#
),
@r###"
@Input (FromJob greeting) : [FromJob ([Job (List [FromJob a])] as a)]
"###
print_only_under_alias: true
)
}
#[test]
fn infer_concrete_type_with_inference_var() {
infer_queries!(indoc!(
r#"
app "test" provides [f] to "./platform"
f : _ -> {}
f = \_ -> f {}
#^{-1}
"#
),
@r###"
f : {} -[[f(0)]]-> {}
"###
)
}
#[test]
fn solve_inference_var_in_annotation_requiring_recursion_fix() {
infer_queries!(indoc!(
r#"
app "test" provides [translateStatic] to "./platform"
translateStatic : _ -> _
translateStatic = \Element c ->
#^^^^^^^^^^^^^^^{-1}
Element (List.map c translateStatic)
"#
),
@"translateStatic : [Element (List a)] as a -[[translateStatic(0)]]-> [Element (List b)]* as b"
)
}
} }

View file

@ -14,6 +14,7 @@ path = "src/tests.rs"
roc_builtins = { path = "../builtins" } roc_builtins = { path = "../builtins" }
roc_utils = { path = "../../utils" } roc_utils = { path = "../../utils" }
wasi_libc_sys = { path = "../../wasi-libc-sys" } wasi_libc_sys = { path = "../../wasi-libc-sys" }
tempfile.workspace = true
[dev-dependencies] [dev-dependencies]
roc_gen_llvm = { path = "../gen_llvm" } roc_gen_llvm = { path = "../gen_llvm" }
@ -40,6 +41,7 @@ roc_target = { path = "../roc_target" }
roc_error_macros = { path = "../../error_macros" } roc_error_macros = { path = "../../error_macros" }
roc_std = { path = "../../roc_std" } roc_std = { path = "../../roc_std" }
roc_debug_flags = {path="../debug_flags"} roc_debug_flags = {path="../debug_flags"}
roc_wasm_module = {path="../../wasm_module"}
bumpalo.workspace = true bumpalo.workspace = true
libc.workspace = true libc.workspace = true

View file

@ -100,9 +100,12 @@ fn build_wasm_test_host() {
let mut outfile = PathBuf::from(&out_dir).join(PLATFORM_FILENAME); let mut outfile = PathBuf::from(&out_dir).join(PLATFORM_FILENAME);
outfile.set_extension("wasm"); outfile.set_extension("wasm");
let builtins_host_tempfile =
bitcode::host_wasm_tempfile().expect("failed to write host builtins object to tempfile");
run_zig(&[ run_zig(&[
"wasm-ld", "wasm-ld",
&bitcode::get_builtins_wasm32_obj_path(), builtins_host_tempfile.path().to_str().unwrap(),
platform_path.to_str().unwrap(), platform_path.to_str().unwrap(),
WASI_COMPILER_RT_PATH, WASI_COMPILER_RT_PATH,
WASI_LIBC_PATH, WASI_LIBC_PATH,
@ -111,6 +114,10 @@ fn build_wasm_test_host() {
"--no-entry", "--no-entry",
"--relocatable", "--relocatable",
]); ]);
// Extend the lifetime of the tempfile so it doesn't get dropped
// (and thus deleted) before the Zig process is done using it!
let _ = builtins_host_tempfile;
} }
fn build_wasm_platform(out_dir: &str, source_path: &str) -> PathBuf { fn build_wasm_platform(out_dir: &str, source_path: &str) -> PathBuf {

View file

@ -2513,7 +2513,7 @@ fn function_malformed_pattern() {
#[test] #[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
#[should_panic(expected = "Hit an erroneous type when creating a layout for")] #[ignore = "causes alias analysis panics, should roc_panic"]
fn call_invalid_layout() { fn call_invalid_layout() {
assert_evals_to!( assert_evals_to!(
indoc!( indoc!(

View file

@ -1642,7 +1642,7 @@ fn issue_2777_default_branch_codegen() {
not(target_family = "windows"), not(target_family = "windows"),
any(feature = "gen-llvm", feature = "gen-wasm") any(feature = "gen-llvm", feature = "gen-wasm")
))] ))]
#[should_panic(expected = "Erroneous")] #[should_panic(expected = r#"Roc failed with message: "Tag Foo was part of a type error!""#)]
fn issue_2900_unreachable_pattern() { fn issue_2900_unreachable_pattern() {
assert_evals_to!( assert_evals_to!(
indoc!( indoc!(
@ -1846,7 +1846,7 @@ fn alignment_i128() {
#[test] #[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))] #[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
#[should_panic(expected = r#"Roc failed with message: "Erroneous: Expr::Closure""#)] #[ignore = "causes alias analysis panics, should roc_panic"]
fn error_type_in_tag_union_payload() { fn error_type_in_tag_union_payload() {
assert_evals_to!( assert_evals_to!(
indoc!( indoc!(
@ -2022,3 +2022,64 @@ fn dispatch_tag_union_function_inferred() {
RocStr RocStr
); );
} }
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn issue_4077_fixed_fixpoint() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [main] to "./platform"
Input : [FromProjectSource, FromJob Job]
Job : [Job { inputs : List Input }]
job : { inputs : List Input } -> Job
job = \config -> Job config
main =
when job { inputs: [] } is
_ -> "OKAY"
"#
),
RocStr::from("OKAY"),
RocStr
);
}
#[test]
#[cfg(any(feature = "gen-llvm", feature = "gen-wasm"))]
fn unify_types_with_fixed_fixpoints_outside_fixing_region() {
assert_evals_to!(
indoc!(
r#"
app "test" provides [main] to "./platform"
Input := [
FromJob Job (List Str),
]
Job := [
Job (List Input)
]
job : List Input -> Job
job = \inputs ->
@Job (Job inputs)
helloWorld : Job
helloWorld =
@Job ( Job [ @Input (FromJob greeting []) ] )
greeting : Job
greeting =
job []
main = (\_ -> "OKAY") helloWorld
"#
),
RocStr::from("OKAY"),
RocStr
);
}

View file

@ -193,7 +193,8 @@ pub fn helper(
.expect("failed to build output object"); .expect("failed to build output object");
std::fs::write(&app_o_file, module_out).expect("failed to write object to file"); std::fs::write(&app_o_file, module_out).expect("failed to write object to file");
// std::fs::copy(&app_o_file, "/tmp/app.o").unwrap(); let builtins_host_tempfile =
bitcode::host_unix_tempfile().expect("failed to write host builtins object to tempfile");
let (mut child, dylib_path) = link( let (mut child, dylib_path) = link(
&target, &target,
@ -202,7 +203,7 @@ pub fn helper(
// With the current method all methods are kept and it adds about 100k to all outputs. // With the current method all methods are kept and it adds about 100k to all outputs.
&[ &[
app_o_file.to_str().unwrap(), app_o_file.to_str().unwrap(),
&bitcode::get_builtins_host_obj_path(), builtins_host_tempfile.path().to_str().unwrap(),
], ],
LinkType::Dylib, LinkType::Dylib,
) )
@ -210,6 +211,10 @@ pub fn helper(
child.wait().unwrap(); child.wait().unwrap();
// Extend the lifetime of the tempfile so it doesn't get dropped
// (and thus deleted) before the linking process is done using it!
let _ = builtins_host_tempfile;
// Load the dylib // Load the dylib
let path = dylib_path.as_path().to_str().unwrap(); let path = dylib_path.as_path().to_str().unwrap();

View file

@ -1,7 +1,8 @@
use roc_error_macros::internal_error; use roc_error_macros::internal_error;
use roc_gen_wasm::{round_up_to_alignment, wasm32_sized::Wasm32Sized}; use roc_gen_wasm::wasm32_sized::Wasm32Sized;
use roc_mono::layout::Builtin; use roc_mono::layout::Builtin;
use roc_std::{RocDec, RocList, RocOrder, RocResult, RocStr, I128, U128}; use roc_std::{RocDec, RocList, RocOrder, RocResult, RocStr, I128, U128};
use roc_wasm_module::round_up_to_alignment;
use std::convert::TryInto; use std::convert::TryInto;
pub trait FromWasm32Memory: Wasm32Sized { pub trait FromWasm32Memory: Wasm32Sized {

View file

@ -2,10 +2,10 @@ use super::RefCount;
use crate::helpers::from_wasm32_memory::FromWasm32Memory; use crate::helpers::from_wasm32_memory::FromWasm32Memory;
use roc_collections::all::MutSet; use roc_collections::all::MutSet;
use roc_gen_wasm::wasm32_result::Wasm32Result; use roc_gen_wasm::wasm32_result::Wasm32Result;
use roc_gen_wasm::wasm_module::{Export, ExportType};
use roc_gen_wasm::DEBUG_SETTINGS; use roc_gen_wasm::DEBUG_SETTINGS;
use roc_load::{ExecutionMode, LoadConfig, Threading}; use roc_load::{ExecutionMode, LoadConfig, Threading};
use roc_reporting::report::DEFAULT_PALETTE_HTML; use roc_reporting::report::DEFAULT_PALETTE_HTML;
use roc_wasm_module::{Export, ExportType};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
@ -138,10 +138,11 @@ fn compile_roc_to_wasm_bytes<'a, T: Wasm32Result>(
) )
}); });
let (mut module, called_preload_fns, main_fn_index) = let (mut module, mut called_fns, main_fn_index) =
roc_gen_wasm::build_app_module(&env, &mut interns, host_module, procedures); roc_gen_wasm::build_app_module(&env, &mut interns, host_module, procedures);
T::insert_wrapper(arena, &mut module, TEST_WRAPPER_NAME, main_fn_index); T::insert_wrapper(arena, &mut module, TEST_WRAPPER_NAME, main_fn_index);
called_fns.push(true);
// Export the initialiser function for refcount tests // Export the initialiser function for refcount tests
let init_refcount_idx = module let init_refcount_idx = module
@ -158,7 +159,7 @@ fn compile_roc_to_wasm_bytes<'a, T: Wasm32Result>(
index: init_refcount_idx, index: init_refcount_idx,
}); });
module.eliminate_dead_code(env.arena, called_preload_fns); module.eliminate_dead_code(env.arena, called_fns);
let mut app_module_bytes = std::vec::Vec::with_capacity(module.size()); let mut app_module_bytes = std::vec::Vec::with_capacity(module.size());
module.serialize(&mut app_module_bytes); module.serialize(&mut app_module_bytes);

View file

@ -7,7 +7,6 @@ use std::process::Command;
use roc_builtins::bitcode::IntWidth; use roc_builtins::bitcode::IntWidth;
use roc_collections::{MutMap, MutSet}; use roc_collections::{MutMap, MutSet};
use roc_gen_wasm::wasm_module::WasmModule;
use roc_module::ident::{ForeignSymbol, ModuleName}; use roc_module::ident::{ForeignSymbol, ModuleName};
use roc_module::low_level::LowLevel; use roc_module::low_level::LowLevel;
use roc_module::symbol::{ use roc_module::symbol::{
@ -19,6 +18,7 @@ use roc_mono::ir::{
UpdateModeId, UpdateModeId,
}; };
use roc_mono::layout::{Builtin, CapturesNiche, LambdaName, Layout, STLayoutInterner}; use roc_mono::layout::{Builtin, CapturesNiche, LambdaName, Layout, STLayoutInterner};
use roc_wasm_module::WasmModule;
use wasm3::{Environment, Module}; use wasm3::{Environment, Module};
const LINKING_TEST_HOST_WASM: &str = "build/wasm_linking_test_host.wasm"; const LINKING_TEST_HOST_WASM: &str = "build/wasm_linking_test_host.wasm";
@ -252,7 +252,7 @@ fn test_linking_without_dce() {
] ]
); );
let (final_module, _called_preload_fns, _roc_main_index) = let (final_module, _called_fns, _roc_main_index) =
roc_gen_wasm::build_app_module(&env, &mut interns, host_module, procedures); roc_gen_wasm::build_app_module(&env, &mut interns, host_module, procedures);
let mut buffer = Vec::with_capacity(final_module.size()); let mut buffer = Vec::with_capacity(final_module.size());
@ -309,10 +309,10 @@ fn test_linking_with_dce() {
assert!(&host_module.names.function_names.is_empty()); assert!(&host_module.names.function_names.is_empty());
let (mut final_module, called_preload_fns, _roc_main_index) = let (mut final_module, called_fns, _roc_main_index) =
roc_gen_wasm::build_app_module(&env, &mut interns, host_module, procedures); roc_gen_wasm::build_app_module(&env, &mut interns, host_module, procedures);
final_module.eliminate_dead_code(env.arena, called_preload_fns); final_module.eliminate_dead_code(env.arena, called_fns);
let mut buffer = Vec::with_capacity(final_module.size()); let mut buffer = Vec::with_capacity(final_module.size());
final_module.serialize(&mut buffer); final_module.serialize(&mut buffer);

View file

@ -2004,3 +2004,30 @@ fn match_list() {
"# "#
) )
} }
#[mono_test]
#[ignore = "https://github.com/roc-lang/roc/issues/4561"]
fn recursive_function_and_union_with_inference_hole() {
let _tracing_guards = roc_tracing::setup_tracing!();
indoc!(
r#"
app "test" provides [main] to "./platform"
Html state : [
Element (List (Html state)),
]
translateStatic : Html _ -> Html _
translateStatic = \node ->
when node is
Element children ->
newChildren = List.map children translateStatic
Element newChildren
main = when translateStatic (Element []) is
_ -> ""
"#
)
}

View file

@ -2,7 +2,7 @@
use crate::subs::{ use crate::subs::{
self, AliasVariables, Content, FlatType, GetSubsSlice, Label, Subs, SubsIndex, UnionLabels, self, AliasVariables, Content, FlatType, GetSubsSlice, Label, Subs, SubsIndex, UnionLabels,
UnionTags, UnsortedUnionLabels, Variable, UnsortedUnionLabels, Variable,
}; };
use crate::types::{ use crate::types::{
name_type_var, name_type_var_with_hint, AbilitySet, Polarity, RecordField, Uls, name_type_var, name_type_var_with_hint, AbilitySet, Polarity, RecordField, Uls,
@ -101,43 +101,6 @@ fn find_names_needed(
use crate::subs::Content::*; use crate::subs::Content::*;
use crate::subs::FlatType::*; use crate::subs::FlatType::*;
while let Err((recursive, _chain)) = subs.occurs(variable) {
let rec_var = subs.fresh_unnamed_flex_var();
let content = subs.get_content_without_compacting(recursive);
match content {
Content::Structure(FlatType::TagUnion(tags, ext_var)) => {
let ext_var = *ext_var;
let mut new_tags = MutMap::default();
for (name_index, slice_index) in tags.iter_all() {
let slice = subs[slice_index];
let mut new_vars = Vec::new();
for var_index in slice {
let var = subs[var_index];
new_vars.push(if var == recursive { rec_var } else { var });
}
new_tags.insert(subs[name_index].clone(), new_vars);
}
let mut x: Vec<_> = new_tags.into_iter().collect();
x.sort();
let union_tags = UnionTags::insert_into_subs(subs, x);
let flat_type = FlatType::RecursiveTagUnion(rec_var, union_tags, ext_var);
subs.set_content(recursive, Content::Structure(flat_type));
}
_ => panic!(
"unfixable recursive type in roc_types::pretty_print {:?} {:?} {:?}",
recursive, variable, content
),
}
}
match &subs.get_content_without_compacting(variable).clone() { match &subs.get_content_without_compacting(variable).clone() {
RecursionVar { opt_name: None, .. } | FlexVar(None) => { RecursionVar { opt_name: None, .. } | FlexVar(None) => {
let root = subs.get_root_key_without_compacting(variable); let root = subs.get_root_key_without_compacting(variable);

View file

@ -2148,6 +2148,22 @@ impl Subs {
} }
} }
} }
pub fn dbg(&self, var: Variable) -> impl std::fmt::Debug + '_ {
SubsFmtContent(self.get_content_without_compacting(var), self)
}
/// Is this variable involved in an error?
pub fn is_error_var(&self, var: Variable) -> bool {
match self.get_content_without_compacting(var) {
Content::Error => true,
Content::FlexVar(Some(index)) => {
// Generated names for errors start with `#`
self[*index].as_str().starts_with('#')
}
_ => false,
}
}
} }
#[inline(always)] #[inline(always)]

View file

@ -585,8 +585,9 @@ impl Types {
/// # Safety /// # Safety
/// ///
/// May only be called if `var` is known to represent the type at `index`. /// May only be called if `var` is known to represent the type at `index`.
pub unsafe fn emplace_variable(&mut self, index: Index<TypeTag>, var: Variable) { #[must_use]
self.tags[index.index()] = TypeTag::Variable(var); pub unsafe fn emplace_variable(&mut self, index: Index<TypeTag>, var: Variable) -> TypeTag {
std::mem::replace(&mut self.tags[index.index()], TypeTag::Variable(var))
} }
fn reserve_type_tags(&mut self, length: usize) -> Slice<TypeTag> { fn reserve_type_tags(&mut self, length: usize) -> Slice<TypeTag> {

View file

@ -0,0 +1,334 @@
//! Fix fixpoints of recursive types.
use roc_error_macros::internal_error;
use roc_types::subs::{Content, FlatType, GetSubsSlice, Subs, Variable};
struct Update {
source_of_truth: Variable,
update_var: Variable,
}
/// Fixes fixpoints of recursive types that are isomorphic, but differ at their fixpoints, to be
/// equivalent with respect to fixpoints.
///
/// Fixpoints are adjusted by finding the recursive closures of both recursive types, and emplacing
/// the recursive closure of one type on the other.
///
/// As an example, let's consider
///
/// ```text
/// F : [FromG G]
/// G : [G {lst : List F}]
/// ```
///
/// after expansion, these aliases have type
///
/// ```text
/// F = [FromG [G {lst: List <1>}] as <1>
/// G = [G {lst: List [FromG <2>]}] as <2>
/// ```
///
/// where <1> and <2> are their respective fixpoints.
///
/// Unification will pass through an occurs check, and we'll see that these types are isomorphic
///
/// ```text
/// [G {lst: List <1>}] ~ [G {lst: List [FromG <2>]}] as <2>
/// {lst: List <1>} ~ {lst: List [FromG <2>]}
/// List <1> ~ List [FromG <2>]
/// <1> ~ [FromG <2>]
/// [FromG [G {lst: List <1>}]] as <1> ~ [FromG <2>]
/// [G {lst: List <1>}] ~ <2>
/// [G {lst: List <1>}] ~ [G {lst: List [FromG <2>]}] as <2> <- OCCURS
/// ...cycle
/// ```
///
/// Unfortunately, isomorphism modulo fixpoint is not enough for us - we need isomorphism with
/// respect to fixpoint, because types T, U where T ~= U / fixpoint will have generated layouts
/// Lay_T, Lay_U where Lay_T != Lay_U due to their differing recursion positions.
/// Lay_T != Lay_U is a hard blocker in our compilation pipeline, as we do not transform layouts,
/// or use uniform representations.
///
/// So, in these cases, we clobber the type variables in either closure with the type variables of
/// the other closure. Concretely, in the case above, we will emplace types via the transformation
///
/// ```text
/// [G {lst: List <1>}] <= [G {lst: List [FromG <2>]}] as <2>
/// {lst: List <1>} <= {lst: List [FromG <2>]}
/// List <1> <= List [FromG <2>]
/// <1> <= [FromG <2>]
/// [FromG [G {lst: List <1>}]] as <1> <= [FromG <2>]
/// ```
///
/// Notice that we only need to emplace types in the clousre that consist of concrete head
/// constructors. In particular, we do not include the emplacement
///
/// ```text
/// [G {lst: List <1>}] <= <2>
/// ```
///
/// because this would not be useful - this emplacement is already priced in thanks to
///
/// ```text
/// [G {lst: List <1>}] <= [G {lst: List [FromG <2>]}] as <2>
/// ```
///
/// We know that this transformation is complete because the recursive closure of a recursive type
/// must, by definition, entirely define that recursive type.
///
/// The choice of which side to clobber is arbitrary; in the future, there may be better heuristics
/// to decide it.
#[must_use]
pub fn fix_fixpoint(subs: &mut Subs, left: Variable, right: Variable) -> Vec<Variable> {
let updates = find_chain(subs, left, right);
let mut new = vec![];
for Update {
source_of_truth,
update_var,
} in updates
{
let source_of_truth_desc = subs.get_without_compacting(source_of_truth);
subs.union(source_of_truth, update_var, source_of_truth_desc);
new.push(source_of_truth);
}
new
}
fn find_chain(subs: &Subs, left: Variable, right: Variable) -> impl Iterator<Item = Update> {
let left = subs.get_root_key_without_compacting(left);
let right = subs.get_root_key_without_compacting(right);
let needle = (left, right);
enum ClobberSide {
Left,
Right,
}
use ClobberSide::*;
let (search_left, search_right, clobber_side) = match (
subs.get_content_without_compacting(left),
subs.get_content_without_compacting(right),
) {
(Content::RecursionVar { .. }, Content::RecursionVar { .. }) => internal_error!(
"two recursion variables at the same level can be unified without fixpoint-fixing"
),
// Unwrap one of the recursion variables to their structure, so that we don't end up
// immediately staring at the base case in `help`.
(Content::RecursionVar { structure, .. }, _) => (*structure, right, Right),
(_, Content::RecursionVar { structure, .. }) => (left, *structure, Left),
_ => internal_error!(
"fixpoint-fixing requires a recursion variable and a non-recursion variable"
),
};
let chain = help(subs, needle, search_left, search_right)
.expect("chain must exist if fixpoints are being fixed!");
// Suppose we started with
// (type1, <rec>)
// where <rec> recurses to type2. At this point, the chain should look like
// (type1, <rec>), ..., (type1, type2)
// We'll verify that <rec> appears on the side we'll be choosing to clobber. Then, we don't
// want to explicitly update the recursion var, so we'll just update everything past the first
// item of the chain.
assert_eq!(chain.first().unwrap(), &needle);
let updates_iter = chain
.into_iter()
// Skip the first element to avoid rewritting <rec> => type1 explicitly!
.skip(1)
// Set up the iterator so the right-hand side contains the variable we want to clobber with
// the content of the left-hand side; that is, the left-hand side becomes the
// source-of-truth.
.map(move |(left, right)| {
let (source_of_truth, update_var) = match clobber_side {
Left => (right, left),
Right => (left, right),
};
Update {
source_of_truth,
update_var,
}
});
return updates_iter;
fn help(
subs: &Subs,
needle: (Variable, Variable),
left: Variable,
right: Variable,
) -> Result<Vec<(Variable, Variable)>, ()> {
let left = subs.get_root_key_without_compacting(left);
let right = subs.get_root_key_without_compacting(right);
if (left, right) == needle {
return Ok(vec![needle]);
}
use Content::*;
use FlatType::*;
match (
subs.get_content_without_compacting(left),
subs.get_content_without_compacting(right),
) {
(FlexVar(..), FlexVar(..))
| (RigidVar(..), RigidVar(..))
| (RigidAbleVar(..), RigidAbleVar(..))
| (FlexAbleVar(..), FlexAbleVar(..))
| (Error, Error)
| (RangedNumber(..), RangedNumber(..)) => Err(()),
(RecursionVar { .. }, RecursionVar { .. }) => internal_error!("not expected"),
(RecursionVar { structure, .. }, _) => {
// By construction, the recursion variables will be adjusted to be equal after
// the transformation, so we can immediately look at the inner variable. We only
// need to adjust head constructors.
let chain = help(subs, needle, *structure, right)?;
Ok(chain)
}
(_, RecursionVar { structure, .. }) => {
let chain = help(subs, needle, left, *structure)?;
Ok(chain)
}
(LambdaSet(..), _) | (_, LambdaSet(..)) => {
// NB: I've failed to construct a way for two lambda sets to be recursive and not
// equal. My argument is that, for a lambda set to be recursive, it must be
// owned by one of the closures it passes through. But a lambda set for a closure
// is unique, so equivalent (recursive) lambda sets must be equal.
//
// As such they should never be involved in fixpoint fixing. I may be wrong,
// though.
Err(())
}
(Alias(_, _, left_inner, _), _) => {
// Aliases can be different as long as we adjust their real variables.
help(subs, needle, *left_inner, right)
}
(_, Alias(_, _, right_inner, _)) => {
// Aliases can be different as long as we adjust their real variables.
help(subs, needle, left, *right_inner)
}
(Structure(left_s), Structure(right_s)) => match (left_s, right_s) {
(Apply(left_sym, left_vars), Apply(right_sym, right_vars)) => {
assert_eq!(left_sym, right_sym);
let mut chain = short_circuit(
subs,
needle,
subs.get_subs_slice(*left_vars).iter(),
subs.get_subs_slice(*right_vars).iter(),
)?;
chain.push((left, right));
Ok(chain)
}
(
Func(left_args, _left_clos, left_ret),
Func(right_args, _right_clos, right_ret),
) => {
// lambda sets are ignored; see the comment in the LambdaSet case above.
let check_args = |_| {
short_circuit(
subs,
needle,
subs.get_subs_slice(*left_args).iter(),
subs.get_subs_slice(*right_args).iter(),
)
};
let mut chain =
help(subs, needle, *left_ret, *right_ret).or_else(check_args)?;
chain.push((left, right));
Ok(chain)
}
(Record(left_fields, left_ext), Record(right_fields, right_ext)) => {
let mut left_it = left_fields.sorted_iterator(subs, *left_ext);
let mut right_it = right_fields.sorted_iterator(subs, *right_ext);
let mut chain = loop {
match (left_it.next(), right_it.next()) {
(Some((left_field, left_v)), Some((right_field, right_v))) => {
assert_eq!(left_field, right_field, "fields do not unify");
if let Ok(chain) =
help(subs, needle, left_v.into_inner(), right_v.into_inner())
{
break Ok(chain);
}
}
(None, None) => break Err(()),
_ => internal_error!("fields differ; does not unify"),
}
}?;
chain.push((left, right));
Ok(chain)
}
(
FunctionOrTagUnion(_left_tag_name, left_sym, left_var),
FunctionOrTagUnion(_right_tag_name, right_sym, right_var),
) => {
assert_eq!(
subs.get_subs_slice(*left_sym),
subs.get_subs_slice(*right_sym)
);
let mut chain = help(subs, needle, *left_var, *right_var)?;
chain.push((left, right));
Ok(chain)
}
(TagUnion(left_tags, left_ext), TagUnion(right_tags, right_ext))
| (
RecursiveTagUnion(_, left_tags, left_ext),
RecursiveTagUnion(_, right_tags, right_ext),
)
| (TagUnion(left_tags, left_ext), RecursiveTagUnion(_, right_tags, right_ext))
| (RecursiveTagUnion(_, left_tags, left_ext), TagUnion(right_tags, right_ext)) => {
let (left_it, _) = left_tags.sorted_iterator_and_ext(subs, *left_ext);
let (right_it, _) = right_tags.sorted_iterator_and_ext(subs, *right_ext);
assert_eq!(
left_it.len(),
right_it.len(),
"tag lengths differ; does not unify"
);
for ((left_tag, left_args), (right_tag, right_args)) in left_it.zip(right_it) {
assert_eq!(left_tag, right_tag);
if let Ok(mut chain) =
short_circuit(subs, needle, left_args.iter(), right_args.iter())
{
chain.push((left, right));
return Ok(chain);
}
}
Err(())
}
(EmptyRecord, EmptyRecord)
| (EmptyTagUnion, EmptyTagUnion) => Err(()),
_ => internal_error!(
"structures {:?} and {:?} do not unify; they should never have been involved in fixing!",
roc_types::subs::SubsFmtContent(&Structure(*left_s), subs),
roc_types::subs::SubsFmtContent(&Structure(*right_s), subs)
),
},
_ => internal_error!("types do not unify; they should never have been involved in fixing!"),
}
}
fn short_circuit<'a, T, U>(
subs: &Subs,
needle: (Variable, Variable),
left_iter: T,
right_iter: U,
) -> Result<Vec<(Variable, Variable)>, ()>
where
T: ExactSizeIterator<Item = &'a Variable>,
U: ExactSizeIterator<Item = &'a Variable>,
{
assert_eq!(left_iter.len(), right_iter.len(), "types do not unify");
for (left, right) in left_iter.zip(right_iter) {
if let Ok(chain) = help(subs, needle, *left, *right) {
return Ok(chain);
}
}
Err(())
}
}

View file

@ -4,4 +4,5 @@
// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. // See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check.
#![allow(clippy::large_enum_variant)] #![allow(clippy::large_enum_variant)]
mod fix;
pub mod unify; pub mod unify;

View file

@ -1,8 +1,8 @@
use bitflags::bitflags; use bitflags::bitflags;
use roc_collections::VecMap; use roc_collections::{VecMap, VecSet};
use roc_debug_flags::dbg_do; use roc_debug_flags::{dbg_do, dbg_set};
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
use roc_debug_flags::{ROC_PRINT_MISMATCHES, ROC_PRINT_UNIFICATIONS}; use roc_debug_flags::{ROC_PRINT_MISMATCHES, ROC_PRINT_UNIFICATIONS, ROC_VERIFY_OCCURS_RECURSION};
use roc_error_macros::internal_error; use roc_error_macros::internal_error;
use roc_module::ident::{Lowercase, TagName}; use roc_module::ident::{Lowercase, TagName};
use roc_module::symbol::{ModuleId, Symbol}; use roc_module::symbol::{ModuleId, Symbol};
@ -321,6 +321,8 @@ impl<M: MetaCollector> Outcome<M> {
pub struct Env<'a> { pub struct Env<'a> {
pub subs: &'a mut Subs, pub subs: &'a mut Subs,
compute_outcome_only: bool, compute_outcome_only: bool,
seen_recursion: VecSet<(Variable, Variable)>,
fixed_variables: VecSet<Variable>,
} }
impl<'a> Env<'a> { impl<'a> Env<'a> {
@ -328,6 +330,8 @@ impl<'a> Env<'a> {
Self { Self {
subs, subs,
compute_outcome_only: false, compute_outcome_only: false,
seen_recursion: Default::default(),
fixed_variables: Default::default(),
} }
} }
@ -339,6 +343,48 @@ impl<'a> Env<'a> {
self.compute_outcome_only = false; self.compute_outcome_only = false;
result result
} }
fn add_recursion_pair(&mut self, var1: Variable, var2: Variable) {
let pair = (
self.subs.get_root_key_without_compacting(var1),
self.subs.get_root_key_without_compacting(var2),
);
let already_seen = self.seen_recursion.insert(pair);
debug_assert!(!already_seen);
}
fn remove_recursion_pair(&mut self, var1: Variable, var2: Variable) {
#[cfg(debug_assertions)]
let size_before = self.seen_recursion.len();
self.seen_recursion.retain(|(v1, v2)| {
let is_recursion_pair = self.subs.equivalent_without_compacting(*v1, var1)
&& self.subs.equivalent_without_compacting(*v2, var2);
!is_recursion_pair
});
#[cfg(debug_assertions)]
let size_after = self.seen_recursion.len();
#[cfg(debug_assertions)]
debug_assert!(size_after < size_before, "nothing was removed");
}
fn seen_recursion_pair(&mut self, var1: Variable, var2: Variable) -> bool {
let (var1, var2) = (
self.subs.get_root_key_without_compacting(var1),
self.subs.get_root_key_without_compacting(var2),
);
self.seen_recursion.contains(&(var1, var2))
}
fn was_fixed(&self, var: Variable) -> bool {
self.fixed_variables
.iter()
.any(|fixed_var| self.subs.equivalent_without_compacting(*fixed_var, var))
}
} }
/// Unifies two types. /// Unifies two types.
@ -863,6 +909,12 @@ fn unify_two_aliases<M: MetaCollector>(
} }
} }
fn fix_fixpoint<M: MetaCollector>(env: &mut Env, ctx: &Context) -> Outcome<M> {
let fixed_variables = crate::fix::fix_fixpoint(env.subs, ctx.first, ctx.second);
env.fixed_variables.extend(fixed_variables);
Default::default()
}
// Unifies a structural alias // Unifies a structural alias
#[inline(always)] #[inline(always)]
#[must_use] #[must_use]
@ -883,7 +935,17 @@ fn unify_alias<M: MetaCollector>(
// Alias wins // Alias wins
merge(env, ctx, Alias(symbol, args, real_var, kind)) merge(env, ctx, Alias(symbol, args, real_var, kind))
} }
RecursionVar { structure, .. } => unify_pool(env, pool, real_var, *structure, ctx.mode), RecursionVar { structure, .. } => {
if env.seen_recursion_pair(ctx.first, ctx.second) {
return fix_fixpoint(env, ctx);
}
env.add_recursion_pair(ctx.first, ctx.second);
let outcome = unify_pool(env, pool, real_var, *structure, ctx.mode);
env.remove_recursion_pair(ctx.first, ctx.second);
outcome
}
RigidVar(_) | RigidAbleVar(..) | FlexAbleVar(..) => { RigidVar(_) | RigidAbleVar(..) | FlexAbleVar(..) => {
unify_pool(env, pool, real_var, ctx.second, ctx.mode) unify_pool(env, pool, real_var, ctx.second, ctx.mode)
} }
@ -956,7 +1018,17 @@ fn unify_opaque<M: MetaCollector>(
Alias(_, _, other_real_var, AliasKind::Structural) => { Alias(_, _, other_real_var, AliasKind::Structural) => {
unify_pool(env, pool, ctx.first, *other_real_var, ctx.mode) unify_pool(env, pool, ctx.first, *other_real_var, ctx.mode)
} }
RecursionVar { structure, .. } => unify_pool(env, pool, ctx.first, *structure, ctx.mode), RecursionVar { structure, .. } => {
if env.seen_recursion_pair(ctx.first, ctx.second) {
return fix_fixpoint(env, ctx);
}
env.add_recursion_pair(ctx.first, ctx.second);
let outcome = unify_pool(env, pool, real_var, *structure, ctx.mode);
env.remove_recursion_pair(ctx.first, ctx.second);
outcome
}
Alias(other_symbol, other_args, other_real_var, AliasKind::Opaque) => { Alias(other_symbol, other_args, other_real_var, AliasKind::Opaque) => {
// Opaques types are only equal if the opaque symbols are equal! // Opaques types are only equal if the opaque symbols are equal!
if symbol == *other_symbol { if symbol == *other_symbol {
@ -1030,27 +1102,45 @@ fn unify_structure<M: MetaCollector>(
&other &other
) )
} }
RecursionVar { structure, .. } => match flat_type { RecursionVar { structure, .. } => {
FlatType::TagUnion(_, _) => { if env.seen_recursion_pair(ctx.first, ctx.second) {
// unify the structure with this unrecursive tag union return fix_fixpoint(env, ctx);
unify_pool(env, pool, ctx.first, *structure, ctx.mode)
} }
FlatType::RecursiveTagUnion(rec, _, _) => {
debug_assert!(is_recursion_var(env.subs, *rec)); env.add_recursion_pair(ctx.first, ctx.second);
// unify the structure with this recursive tag union
unify_pool(env, pool, ctx.first, *structure, ctx.mode) let outcome = match flat_type {
} FlatType::TagUnion(_, _) => {
FlatType::FunctionOrTagUnion(_, _, _) => { // unify the structure with this unrecursive tag union
// unify the structure with this unrecursive tag union unify_pool(env, pool, ctx.first, *structure, ctx.mode)
unify_pool(env, pool, ctx.first, *structure, ctx.mode) }
} FlatType::RecursiveTagUnion(rec, _, _) => {
// Only tag unions can be recursive; everything else is an error. debug_assert!(
_ => mismatch!( is_recursion_var(env.subs, *rec),
"trying to unify {:?} with recursive type var {:?}", "{:?}",
&flat_type, roc_types::subs::SubsFmtContent(
structure env.subs.get_content_without_compacting(*rec),
), env.subs
}, )
);
// unify the structure with this recursive tag union
unify_pool(env, pool, ctx.first, *structure, ctx.mode)
}
FlatType::FunctionOrTagUnion(_, _, _) => {
// unify the structure with this unrecursive tag union
unify_pool(env, pool, ctx.first, *structure, ctx.mode)
}
// Only tag unions can be recursive; everything else is an error.
_ => mismatch!(
"trying to unify {:?} with recursive type var {:?}",
&flat_type,
structure
),
};
env.remove_recursion_pair(ctx.first, ctx.second);
outcome
}
Structure(ref other_flat_type) => { Structure(ref other_flat_type) => {
// Unify the two flat types // Unify the two flat types
@ -1121,8 +1211,16 @@ fn unify_lambda_set<M: MetaCollector>(
} }
} }
RecursionVar { structure, .. } => { RecursionVar { structure, .. } => {
if env.seen_recursion_pair(ctx.first, ctx.second) {
return fix_fixpoint(env, ctx);
}
env.add_recursion_pair(ctx.first, ctx.second);
// suppose that the recursion var is a lambda set // suppose that the recursion var is a lambda set
unify_pool(env, pool, ctx.first, *structure, ctx.mode) let outcome = unify_pool(env, pool, ctx.first, *structure, ctx.mode);
env.remove_recursion_pair(ctx.first, ctx.second);
outcome
} }
RigidVar(..) | RigidAbleVar(..) => mismatch!("Lambda sets never unify with rigid"), RigidVar(..) | RigidAbleVar(..) => mismatch!("Lambda sets never unify with rigid"),
FlexAbleVar(..) => mismatch!("Lambda sets should never have abilities attached to them"), FlexAbleVar(..) => mismatch!("Lambda sets should never have abilities attached to them"),
@ -2517,7 +2615,22 @@ fn maybe_mark_union_recursive(env: &mut Env, union_var: Variable) {
}) { }) {
return; return;
} else { } else {
internal_error!("recursive loop does not contain a tag union") // We may have partially solved a recursive type, but still see an occurs, if the type
// has errors inside of it. As such, admit this; however, for well-typed programs, this
// case should never be observed. Set ROC_VERIFY_OCCURS_RECURSION to verify this branch
// is not reached for well-typed programs.
if dbg_set!(ROC_VERIFY_OCCURS_RECURSION)
|| !chain.iter().any(|&var| {
matches!(
subs.get_content_without_compacting(var),
Content::Structure(FlatType::RecursiveTagUnion(..))
)
})
{
internal_error!("recursive loop does not contain a tag union")
}
return;
} }
} }
} }
@ -2701,10 +2814,20 @@ fn unify_shared_tags_merge_new<M: MetaCollector>(
new_ext_var: Variable, new_ext_var: Variable,
recursion_var: Rec, recursion_var: Rec,
) -> Outcome<M> { ) -> Outcome<M> {
if env.was_fixed(ctx.first) && env.was_fixed(ctx.second) {
// Both of the tags we're looking at were just involved in fixpoint-fixing, so their types
// should be aligned. As such, do not attempt to unify them and update the recursion
// pointer again.
debug_assert!(env
.subs
.equivalent_without_compacting(ctx.first, ctx.second));
return Default::default();
}
let flat_type = match recursion_var { let flat_type = match recursion_var {
Rec::None => FlatType::TagUnion(new_tags, new_ext_var), Rec::None => FlatType::TagUnion(new_tags, new_ext_var),
Rec::Left(rec) | Rec::Right(rec) | Rec::Both(rec, _) => { Rec::Left(rec) | Rec::Right(rec) | Rec::Both(rec, _) => {
debug_assert!(is_recursion_var(env.subs, rec)); debug_assert!(is_recursion_var(env.subs, rec), "{:?}", env.subs.dbg(rec));
FlatType::RecursiveTagUnion(rec, new_tags, new_ext_var) FlatType::RecursiveTagUnion(rec, new_tags, new_ext_var)
} }
}; };
@ -2770,8 +2893,16 @@ fn unify_flat_type<M: MetaCollector>(
} }
(RecursiveTagUnion(rec1, tags1, ext1), RecursiveTagUnion(rec2, tags2, ext2)) => { (RecursiveTagUnion(rec1, tags1, ext1), RecursiveTagUnion(rec2, tags2, ext2)) => {
debug_assert!(is_recursion_var(env.subs, *rec1)); debug_assert!(
debug_assert!(is_recursion_var(env.subs, *rec2)); is_recursion_var(env.subs, *rec1),
"{:?}",
env.subs.dbg(*rec1)
);
debug_assert!(
is_recursion_var(env.subs, *rec2),
"{:?}",
env.subs.dbg(*rec2)
);
let rec = Rec::Both(*rec1, *rec2); let rec = Rec::Both(*rec1, *rec2);
let mut outcome = unify_tag_unions(env, pool, ctx, *tags1, *ext1, *tags2, *ext2, rec); let mut outcome = unify_tag_unions(env, pool, ctx, *tags1, *ext1, *tags2, *ext2, rec);
@ -3240,7 +3371,15 @@ fn unify_recursion<M: MetaCollector>(
structure: Variable, structure: Variable,
other: &Content, other: &Content,
) -> Outcome<M> { ) -> Outcome<M> {
match other { if !matches!(other, RecursionVar { .. }) {
if env.seen_recursion_pair(ctx.first, ctx.second) {
return Default::default();
}
env.add_recursion_pair(ctx.first, ctx.second);
}
let outcome = match other {
RecursionVar { RecursionVar {
opt_name: other_opt_name, opt_name: other_opt_name,
structure: _other_structure, structure: _other_structure,
@ -3315,7 +3454,13 @@ fn unify_recursion<M: MetaCollector>(
} }
Error => merge(env, ctx, Error), Error => merge(env, ctx, Error),
};
if !matches!(other, RecursionVar { .. }) {
env.remove_recursion_pair(ctx.first, ctx.second);
} }
outcome
} }
#[must_use] #[must_use]
@ -3369,7 +3514,9 @@ fn is_recursion_var(subs: &Subs, var: Variable) -> bool {
matches!( matches!(
subs.get_content_without_compacting(var), subs.get_content_without_compacting(var),
Content::RecursionVar { .. } Content::RecursionVar { .. }
) ) ||
// Error-like vars should always unify, so pretend they are recursion vars too.
subs.is_error_var(var)
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]

View file

@ -1,4 +1,4 @@
<svg viewBox="0 0 52 53" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 52 53" xmlns="http://www.w3.org/2000/svg">
<style>polygon {fill: #5c0bff;}@media (prefers-color-scheme: dark) {polygon {fill: #7733ff;}} </style> <style>polygon{fill:#7d59dd}@media (prefers-color-scheme: dark){polygon{fill:#9c7bea}}</style>
<polygon points="0,0 23.8834,3.21052 37.2438,19.0101 45.9665,16.6324 50.5,22 45,22 44.0315,26.3689 26.4673,39.3424 27.4527,45.2132 17.655,53 23.6751,22.7086"/> <polygon points="0,0 23.8834,3.21052 37.2438,19.0101 45.9665,16.6324 50.5,22 45,22 44.0315,26.3689 26.4673,39.3424 27.4527,45.2132 17.655,53 23.6751,22.7086"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 335 B

After

Width:  |  Height:  |  Size: 327 B

Before After
Before After

View file

@ -1576,7 +1576,7 @@ mod tests {
} }
#[allow(dead_code)] #[allow(dead_code)]
fn zig_host_app_help(dir: &Path) { fn zig_host_app_help(dir: &Path, target: &Triple) {
let host_zig = indoc!( let host_zig = indoc!(
r#" r#"
const std = @import("std"); const std = @import("std");
@ -1670,8 +1670,7 @@ mod tests {
panic!("zig build-exe failed"); panic!("zig build-exe failed");
} }
let preprocessed_host_filename = let preprocessed_host_filename = dir.join(preprocessed_host_filename(target).unwrap());
dir.join(preprocessed_host_filename(&Triple::host()).unwrap());
preprocess_elf( preprocess_elf(
target_lexicon::Endianness::Little, target_lexicon::Endianness::Little,
@ -1697,10 +1696,12 @@ mod tests {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[test] #[test]
fn zig_host_app() { fn zig_host_app() {
use std::str::FromStr;
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let dir = dir.path(); let dir = dir.path();
zig_host_app_help(dir); zig_host_app_help(dir, &Triple::from_str("x86_64-unknown-linux-musl").unwrap());
let output = std::process::Command::new(&dir.join("final")) let output = std::process::Command::new(&dir.join("final"))
.current_dir(dir) .current_dir(dir)

View file

@ -4,19 +4,17 @@ use roc_error_macros::internal_error;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
use target_lexicon::Triple; use target_lexicon::Triple;
use tempfile::Builder;
// TODO: Eventually do this from scratch and in memory instead of with ld. // TODO: Eventually do this from scratch and in memory instead of with ld.
pub fn create_dylib_macho( pub fn create_dylib_macho(
custom_names: &[String], custom_names: &[String],
triple: &Triple, triple: &Triple,
) -> object::read::Result<Vec<u8>> { ) -> object::read::Result<Vec<u8>> {
let dummy_obj_file = Builder::new() let dummy_obj_file = tempfile::Builder::new()
.prefix("roc_lib") .prefix("roc_lib")
.suffix(".o") .suffix(".o")
.tempfile() .tempfile()
.unwrap_or_else(|e| internal_error!("{}", e)); .unwrap_or_else(|e| internal_error!("{}", e));
let dummy_obj_file = dummy_obj_file.path();
let tmp = tempfile::tempdir().unwrap_or_else(|e| internal_error!("{}", e)); let tmp = tempfile::tempdir().unwrap_or_else(|e| internal_error!("{}", e));
let dummy_lib_file = tmp.path().to_path_buf().with_file_name("libapp.so"); let dummy_lib_file = tmp.path().to_path_buf().with_file_name("libapp.so");
@ -47,7 +45,7 @@ pub fn create_dylib_macho(
} }
std::fs::write( std::fs::write(
dummy_obj_file, dummy_obj_file.path(),
out_object.write().expect("failed to build output object"), out_object.write().expect("failed to build output object"),
) )
.expect("failed to write object to file"); .expect("failed to write object to file");
@ -70,13 +68,17 @@ pub fn create_dylib_macho(
.args([ .args([
ld_flag_soname, ld_flag_soname,
dummy_lib_file.file_name().unwrap().to_str().unwrap(), dummy_lib_file.file_name().unwrap().to_str().unwrap(),
dummy_obj_file.to_str().unwrap(), dummy_obj_file.path().to_str().unwrap(),
"-o", "-o",
dummy_lib_file.to_str().unwrap(), dummy_lib_file.to_str().unwrap(),
]) ])
.output() .output()
.unwrap(); .unwrap();
// Extend the lifetime of the tempfile so it doesn't get dropped
// (and thus deleted) before the linker process is done using it!
let _ = dummy_obj_file;
if !output.status.success() { if !output.status.success() {
match std::str::from_utf8(&output.stderr) { match std::str::from_utf8(&output.stderr) {
Ok(stderr) => panic!( Ok(stderr) => panic!(

View file

@ -321,12 +321,12 @@ fn parse_src<'a>(arena: &'a Bump, line: &'a str) -> ParseOutcome<'a> {
match roc_parse::expr::loc_expr().parse(arena, State::new(src_bytes), 0) { match roc_parse::expr::loc_expr().parse(arena, State::new(src_bytes), 0) {
Ok((_, loc_expr, _)) => ParseOutcome::Expr(loc_expr.value), Ok((_, loc_expr, _)) => ParseOutcome::Expr(loc_expr.value),
// Special case some syntax errors to allow for multi-line inputs // Special case some syntax errors to allow for multi-line inputs
Err((_, EExpr::Closure(EClosure::Body(_, _), _), _)) Err((_, EExpr::Closure(EClosure::Body(_, _), _)))
| Err((_, EExpr::When(EWhen::Pattern(EPattern::Start(_), _), _), _)) | Err((_, EExpr::When(EWhen::Pattern(EPattern::Start(_), _), _)))
| Err((_, EExpr::Start(_), _)) | Err((_, EExpr::Start(_)))
| Err((_, EExpr::IndentStart(_), _)) => ParseOutcome::Incomplete, | Err((_, EExpr::IndentStart(_))) => ParseOutcome::Incomplete,
Err((_, EExpr::DefMissingFinalExpr(_), _)) Err((_, EExpr::DefMissingFinalExpr(_)))
| Err((_, EExpr::DefMissingFinalExpr2(_, _), _)) => { | Err((_, EExpr::DefMissingFinalExpr2(_, _))) => {
// This indicates that we had an attempted def; re-parse it as a single-line def. // This indicates that we had an attempted def; re-parse it as a single-line def.
match parse_single_def( match parse_single_def(
ExprParseOptions { ExprParseOptions {

View file

@ -13,9 +13,10 @@ crate-type = ["cdylib"]
roc_builtins = {path = "../compiler/builtins"} roc_builtins = {path = "../compiler/builtins"}
roc_utils = {path = "../utils"} roc_utils = {path = "../utils"}
wasi_libc_sys = { path = "../wasi-libc-sys" } wasi_libc_sys = { path = "../wasi-libc-sys" }
tempfile.workspace = true
[dependencies] [dependencies]
bumpalo.workspace = true bumpalo.workspace = true
console_error_panic_hook = {version = "0.1.7", optional = true} console_error_panic_hook = {version = "0.1.7", optional = true}
futures = {version = "0.3.24", optional = true} futures = {version = "0.3.24", optional = true}
js-sys = "0.3.60" js-sys = "0.3.60"

View file

@ -23,10 +23,13 @@ fn main() {
pre_linked_binary_path.extend(["pre_linked_binary"]); pre_linked_binary_path.extend(["pre_linked_binary"]);
pre_linked_binary_path.set_extension("o"); pre_linked_binary_path.set_extension("o");
let builtins_host_tempfile =
bitcode::host_wasm_tempfile().expect("failed to write host builtins object to tempfile");
let output = Command::new(&zig_executable()) let output = Command::new(&zig_executable())
.args([ .args([
"wasm-ld", "wasm-ld",
&bitcode::get_builtins_wasm32_obj_path(), builtins_host_tempfile.path().to_str().unwrap(),
platform_obj.to_str().unwrap(), platform_obj.to_str().unwrap(),
WASI_COMPILER_RT_PATH, WASI_COMPILER_RT_PATH,
WASI_LIBC_PATH, WASI_LIBC_PATH,
@ -39,6 +42,10 @@ fn main() {
.output() .output()
.unwrap(); .unwrap();
// Extend the lifetime of the tempfile so it doesn't get dropped
// (and thus deleted) before the Zig process is done using it!
let _ = builtins_host_tempfile;
assert!(output.status.success(), "{:#?}", output); assert!(output.status.success(), "{:#?}", output);
assert!(output.stdout.is_empty(), "{:#?}", output); assert!(output.stdout.is_empty(), "{:#?}", output);
assert!(output.stderr.is_empty(), "{:#?}", output); assert!(output.stderr.is_empty(), "{:#?}", output);

View file

@ -244,7 +244,7 @@ pub async fn entrypoint_from_js(src: String) -> Result<String, String> {
.collect::<MutSet<_>>(), .collect::<MutSet<_>>(),
}; };
let (mut module, called_preload_fns, main_fn_index) = { let (mut module, mut called_fns, main_fn_index) = {
let host_module = roc_gen_wasm::parse_host(env.arena, PRE_LINKED_BINARY).unwrap(); let host_module = roc_gen_wasm::parse_host(env.arena, PRE_LINKED_BINARY).unwrap();
roc_gen_wasm::build_app_module( roc_gen_wasm::build_app_module(
&env, &env,
@ -262,8 +262,9 @@ pub async fn entrypoint_from_js(src: String) -> Result<String, String> {
main_fn_index, main_fn_index,
&main_fn_layout.result, &main_fn_layout.result,
); );
called_fns.push(true);
module.eliminate_dead_code(env.arena, called_preload_fns); module.eliminate_dead_code(env.arena, called_fns);
let mut buffer = Vec::with_capacity_in(module.size(), arena); let mut buffer = Vec::with_capacity_in(module.size(), arena);
module.serialize(&mut buffer); module.serialize(&mut buffer);

123
crates/reporting/src/cli.rs Normal file
View file

@ -0,0 +1,123 @@
use std::path::PathBuf;
use roc_collections::MutMap;
use roc_module::symbol::{Interns, ModuleId};
use roc_region::all::LineInfo;
use roc_solve_problem::TypeError;
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Problems {
pub errors: usize,
pub warnings: usize,
}
impl Problems {
pub fn exit_code(&self) -> i32 {
// 0 means no problems, 1 means errors, 2 means warnings
if self.errors > 0 {
1
} else {
self.warnings.min(1) as i32
}
}
}
pub fn report_problems(
total_problems: usize,
sources: &MutMap<ModuleId, (PathBuf, Box<str>)>,
interns: &Interns,
can_problems: &mut MutMap<ModuleId, Vec<roc_problem::can::Problem>>,
type_problems: &mut MutMap<ModuleId, Vec<TypeError>>,
) -> Problems {
use crate::report::{
can_problem, type_problem, Report, RocDocAllocator, Severity::*, DEFAULT_PALETTE,
};
let palette = DEFAULT_PALETTE;
// This will often over-allocate total memory, but it means we definitely
// never need to re-allocate either the warnings or the errors vec!
let mut warnings = Vec::with_capacity(total_problems);
let mut errors = Vec::with_capacity(total_problems);
for (home, (module_path, src)) in sources.iter() {
let mut src_lines: Vec<&str> = Vec::new();
src_lines.extend(src.split('\n'));
let lines = LineInfo::new(&src_lines.join("\n"));
// Report parsing and canonicalization problems
let alloc = RocDocAllocator::new(&src_lines, *home, interns);
let problems = can_problems.remove(home).unwrap_or_default();
for problem in problems.into_iter() {
let report = can_problem(&alloc, &lines, module_path.clone(), problem);
let severity = report.severity;
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
match severity {
Warning => {
warnings.push(buf);
}
RuntimeError => {
errors.push(buf);
}
}
}
let problems = type_problems.remove(home).unwrap_or_default();
for problem in problems {
if let Some(report) = type_problem(&alloc, &lines, module_path.clone(), problem) {
let severity = report.severity;
let mut buf = String::new();
report.render_color_terminal(&mut buf, &alloc, &palette);
match severity {
Warning => {
warnings.push(buf);
}
RuntimeError => {
errors.push(buf);
}
}
}
}
}
let problems_reported;
// Only print warnings if there are no errors
if errors.is_empty() {
problems_reported = warnings.len();
for warning in warnings.iter() {
println!("\n{}\n", warning);
}
} else {
problems_reported = errors.len();
for error in errors.iter() {
println!("\n{}\n", error);
}
}
// If we printed any problems, print a horizontal rule at the end,
// and then clear any ANSI escape codes (e.g. colors) we've used.
//
// The horizontal rule is nice when running the program right after
// compiling it, as it lets you clearly see where the compiler
// errors/warnings end and the program output begins.
if problems_reported > 0 {
println!("{}\u{001B}[0m\n", Report::horizontal_rule(&palette));
}
Problems {
errors: errors.len(),
warnings: warnings.len(),
}
}

View file

@ -1547,7 +1547,7 @@ fn to_unexpected_arrow_report<'a>(
), ),
alloc.concat([ alloc.concat([
alloc.reflow(r"It makes sense to see arrows around here, "), alloc.reflow(r"It makes sense to see arrows around here, "),
alloc.reflow(r"so I suspect it is something earlier."), alloc.reflow(r"so I suspect it is something earlier. "),
alloc.reflow( alloc.reflow(
r"Maybe this pattern is indented a bit farther from the previous patterns?", r"Maybe this pattern is indented a bit farther from the previous patterns?",
), ),

View file

@ -2411,47 +2411,56 @@ fn count_generated_name_usages<'a>(
usages: &mut VecMap<Lowercase, usize>, usages: &mut VecMap<Lowercase, usize>,
types: impl IntoIterator<Item = &'a ErrorType>, types: impl IntoIterator<Item = &'a ErrorType>,
) { ) {
let mut stack = types.into_iter().collect::<Vec<_>>(); // Stack consists of (type, only_unseen) where if `only_unseen`, then the count should only be
// incremented if the variable has not already been seen. This is to deal with counting phantom
// variables in type aliases, while not double-counting alias type arguments that also appear
// in the real type.
let mut stack = types.into_iter().map(|t| (t, false)).collect::<Vec<_>>();
let mut ext_stack = vec![]; let mut ext_stack = vec![];
use ErrorType::*; use ErrorType::*;
while let Some(tipe) = stack.pop() { while let Some((tipe, only_unseen)) = stack.pop() {
match tipe { match tipe {
FlexVar(name) | FlexAbleVar(name, _) => { FlexVar(name) | FlexAbleVar(name, _) => {
if is_generated_name(name) { if is_generated_name(name) {
let count = usages.get_or_insert(name.clone(), || 0); let count = usages.get_or_insert(name.clone(), || 0);
*count += 1; if !only_unseen || *count == 0 {
*count += 1;
}
} }
} }
RigidVar(name) | RigidAbleVar(name, _) => { RigidVar(name) | RigidAbleVar(name, _) => {
debug_assert!(!is_generated_name(name)); debug_assert!(!is_generated_name(name));
} }
Type(_, tys) => { Type(_, tys) => {
stack.extend(tys); stack.extend(tys.iter().map(|t| (t, only_unseen)));
} }
Record(fields, ext) => { Record(fields, ext) => {
stack.extend(fields.values().map(|f| f.as_inner())); stack.extend(fields.values().map(|f| (f.as_inner(), only_unseen)));
ext_stack.push(ext); ext_stack.push((ext, only_unseen));
} }
TagUnion(tags, ext, _) => { TagUnion(tags, ext, _) => {
stack.extend(tags.values().flatten()); stack.extend(tags.values().flatten().map(|t| (t, only_unseen)));
ext_stack.push(ext); ext_stack.push((ext, only_unseen));
} }
RecursiveTagUnion(rec, tags, ext, _) => { RecursiveTagUnion(rec, tags, ext, _) => {
stack.push(rec); stack.push((rec, only_unseen));
stack.extend(tags.values().flatten()); stack.extend(tags.values().flatten().map(|t| (t, only_unseen)));
ext_stack.push(ext); ext_stack.push((ext, only_unseen));
} }
Function(args, _lset, ret) => { Function(args, _lset, ret) => {
stack.extend(args); stack.extend(args.iter().map(|t| (t, only_unseen)));
stack.push(ret); stack.push((ret, only_unseen));
} }
Alias(_, _args, real, _) => { Alias(_, args, real, _) => {
// Since the arguments should always be captured in the real type, // Then, count up any phantom args that were missed b/c they're not referenced in
// only look at the real type. Otherwise we might think a variable appears twice // the real var. Set `only_unseen` so that we don not double-count vars that do
// when it doesn't. // appear in the real var.
stack.push(real); stack.extend(args.iter().map(|t| (t, true)));
// First, count the occurrences in the real var
stack.push((real, only_unseen));
} }
Infinite | Error => {} Infinite | Error => {}
Range(_) => {} Range(_) => {}
@ -2463,14 +2472,16 @@ fn count_generated_name_usages<'a>(
fn count_generated_name_usages_in_exts<'a>( fn count_generated_name_usages_in_exts<'a>(
usages: &mut VecMap<Lowercase, usize>, usages: &mut VecMap<Lowercase, usize>,
exts: impl IntoIterator<Item = &'a TypeExt>, exts: impl IntoIterator<Item = (&'a TypeExt, bool)>,
) { ) {
for ext in exts { for (ext, only_unseen) in exts {
match ext { match ext {
TypeExt::FlexOpen(name) => { TypeExt::FlexOpen(name) => {
if is_generated_name(name) { if is_generated_name(name) {
let count = usages.get_or_insert(name.clone(), || 0); let count = usages.get_or_insert(name.clone(), || 0);
*count += 1; if !only_unseen || *count == 0 {
*count += 1;
}
} }
} }
TypeExt::RigidOpen(name) => { TypeExt::RigidOpen(name) => {
@ -3093,13 +3104,13 @@ fn diff_tag_union<'b>(
let gen_usages1 = { let gen_usages1 = {
let mut usages = VecMap::default(); let mut usages = VecMap::default();
count_generated_name_usages(&mut usages, fields1.values().flatten()); count_generated_name_usages(&mut usages, fields1.values().flatten());
count_generated_name_usages_in_exts(&mut usages, [&ext1]); count_generated_name_usages_in_exts(&mut usages, [(&ext1, false)]);
usages usages
}; };
let gen_usages2 = { let gen_usages2 = {
let mut usages = VecMap::default(); let mut usages = VecMap::default();
count_generated_name_usages(&mut usages, fields2.values().flatten()); count_generated_name_usages(&mut usages, fields2.values().flatten());
count_generated_name_usages_in_exts(&mut usages, [&ext2]); count_generated_name_usages_in_exts(&mut usages, [(&ext2, false)]);
usages usages
}; };

View file

@ -3,5 +3,6 @@
// See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check. // See github.com/roc-lang/roc/issues/800 for discussion of the large_enum_variant check.
#![allow(clippy::large_enum_variant)] #![allow(clippy::large_enum_variant)]
pub mod cli;
pub mod error; pub mod error;
pub mod report; pub mod report;

View file

@ -5017,12 +5017,13 @@ mod test_reporting {
I am parsing a `when` expression right now, but this arrow is confusing I am parsing a `when` expression right now, but this arrow is confusing
me: me:
5 5 -> 2
6 _ -> 2 6 _ -> 2
^^ ^^
It makes sense to see arrows around here, so I suspect it is something It makes sense to see arrows around here, so I suspect it is something
earlier.Maybe this pattern is indented a bit farther from the previous earlier. Maybe this pattern is indented a bit farther from the
patterns? previous patterns?
Note: Here is an example of a valid `when` expression for reference. Note: Here is an example of a valid `when` expression for reference.
@ -5053,12 +5054,13 @@ mod test_reporting {
I am parsing a `when` expression right now, but this arrow is confusing I am parsing a `when` expression right now, but this arrow is confusing
me: me:
5 5 -> Num.neg
6 2 -> 2 6 2 -> 2
^^ ^^
It makes sense to see arrows around here, so I suspect it is something It makes sense to see arrows around here, so I suspect it is something
earlier.Maybe this pattern is indented a bit farther from the previous earlier. Maybe this pattern is indented a bit farther from the
patterns? previous patterns?
Note: Here is an example of a valid `when` expression for reference. Note: Here is an example of a valid `when` expression for reference.
@ -5301,6 +5303,7 @@ mod test_reporting {
This multiline string is not sufficiently indented: This multiline string is not sufficiently indented:
4 """
5 testing 5 testing
^ ^
@ -8075,6 +8078,7 @@ All branches in an `if` must have the same type!
I was partway through parsing an ability definition, but I got stuck I was partway through parsing an ability definition, but I got stuck
here: here:
4 MEq has
5 eq b c : a, a -> U64 | a has MEq 5 eq b c : a, a -> U64 | a has MEq
^ ^
@ -8647,38 +8651,23 @@ All branches in an `if` must have the same type!
hash = \@Id n -> n hash = \@Id n -> n
"# "#
), ),
@r#" @r###"
TYPE MISMATCH /code/proj/Main.roc TYPE MISMATCH /code/proj/Main.roc
Something is off with the body of the `hash` definition: Something is off with the body of the `hash` definition:
8 hash : Id -> U32 8 hash : Id -> U32
9 hash = \@Id n -> n 9 hash = \@Id n -> n
^ ^
This `n` value is a: This `n` value is a:
U64 U64
But the type annotation on `hash` says it should be: But the type annotation on `hash` says it should be:
U32 U32
"###
TYPE MISMATCH /code/proj/Main.roc
Something is off with this specialization of `hash`:
9 hash = \@Id n -> n
^^^^^^^^^^^
This value is a declared specialization of type:
Id -> U32
But the type annotation on `hash` says it must match:
Id -> U64
"#
); );
test_report!( test_report!(

View file

@ -102,7 +102,7 @@ pub fn first_last_index_of<T: ::std::fmt::Debug + std::cmp::Eq>(
} }
// get the path of the lib folder // get the path of the lib folder
// runtime dependencies like zig files, builtin_host.o are put in the lib folder // runtime dependencies like zig files, Windows dylib builds, are put in the lib folder
pub fn get_lib_path() -> Option<PathBuf> { pub fn get_lib_path() -> Option<PathBuf> {
let exe_relative_str_path_opt = std::env::current_exe().ok(); let exe_relative_str_path_opt = std::env::current_exe().ok();

View file

@ -0,0 +1,12 @@
[package]
name = "roc_wasm_interp"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
roc_wasm_module = { path = "../wasm_module" }
bitvec.workspace = true
bumpalo.workspace = true

View file

@ -0,0 +1,210 @@
use bitvec::vec::BitVec;
use bumpalo::{collections::Vec, Bump};
use roc_wasm_module::ValueType;
use std::iter::repeat;
use crate::Value;
/// Struct-of-Arrays storage for the call stack.
/// Type info is packed to avoid wasting space on padding.
/// However we store 64 bits for every local, even 32-bit values, for easy random access.
#[derive(Debug)]
pub struct CallStack<'a> {
/// return addresses (one entry per frame)
return_addrs: Vec<'a, u32>,
/// frame offsets into the `locals`, `is_float`, and `is_64` vectors (one entry per frame)
frame_offsets: Vec<'a, u32>,
/// binary data for local variables (one entry per local)
locals_data: Vec<'a, u64>,
/// int/float type info (one entry per local)
is_float: BitVec,
/// bitwidth type info (one entry per local)
is_64: BitVec,
}
/*
TODO, maybe?
Store data as `Vec<u8>` and a current frame offset.
To find a local by index, take a slice of `is_64` starting at current frame offset,
and use count_ones to know how many of the locals in-between are 64-bit vs 32.
Big size reduction, since most locals are i32. And we're loading that word from is_64 anyway.
When pushing/popping frames, move the current frame offset using a similar calculation.
Not clear if this would be better! Stack access pattern is pretty cache-friendly anyway.
*/
impl<'a> CallStack<'a> {
pub fn new(arena: &'a Bump) -> Self {
CallStack {
return_addrs: Vec::with_capacity_in(256, arena),
frame_offsets: Vec::with_capacity_in(256, arena),
locals_data: Vec::with_capacity_in(16 * 256, arena),
is_float: BitVec::with_capacity(256),
is_64: BitVec::with_capacity(256),
}
}
/// On entering a Wasm call, save the return address, and make space for locals
pub fn push_frame(&mut self, return_addr: u32, local_groups: &[(u32, ValueType)]) {
self.return_addrs.push(return_addr);
let frame_offset = self.is_64.len();
self.frame_offsets.push(frame_offset as u32);
let mut total = 0;
for (num_locals, ty) in local_groups {
let n = *num_locals as usize;
total += n;
self.is_64
.extend(repeat(matches!(ty, ValueType::I64 | ValueType::F64)).take(n));
self.is_float
.extend(repeat(matches!(ty, ValueType::F32 | ValueType::F64)).take(n));
}
self.locals_data.extend(repeat(0).take(total));
}
/// On returning from a Wasm call, drop its locals and retrieve the return address
pub fn pop_frame(&mut self) -> u32 {
let frame_offset = self.frame_offsets.pop().unwrap() as usize;
self.locals_data.truncate(frame_offset);
self.is_64.truncate(frame_offset);
self.is_64.truncate(frame_offset);
self.return_addrs.pop().unwrap()
}
pub fn get_local(&self, local_index: u32) -> Value {
let frame_offset = self.frame_offsets.last().unwrap();
let index = (*frame_offset + local_index) as usize;
let data64 = self.locals_data[index];
let is_float = self.is_float[index];
let is_64 = self.is_64[index];
if is_64 {
if is_float {
Value::F64(f64::from_ne_bytes(data64.to_ne_bytes()))
} else {
Value::I64(i64::from_ne_bytes(data64.to_ne_bytes()))
}
} else {
let data32 = data64 as u32;
if is_float {
Value::F32(f32::from_ne_bytes(data32.to_ne_bytes()))
} else {
Value::I32(i32::from_ne_bytes(data32.to_ne_bytes()))
}
}
}
pub fn set_local(&mut self, local_index: u32, value: Value) {
let frame_offset = *self.frame_offsets.last().unwrap();
let index = (frame_offset + local_index) as usize;
match value {
Value::I32(x) => {
self.locals_data[index] = u64::from_ne_bytes((x as i64).to_ne_bytes());
debug_assert!(!self.is_64[index]);
debug_assert!(!self.is_float[index]);
}
Value::I64(x) => {
self.locals_data[index] = u64::from_ne_bytes((x).to_ne_bytes());
debug_assert!(!self.is_float[index]);
debug_assert!(self.is_64[index]);
}
Value::F32(x) => {
self.locals_data[index] = x.to_bits() as u64;
debug_assert!(self.is_float[index]);
debug_assert!(!self.is_64[index]);
}
Value::F64(x) => {
self.locals_data[index] = x.to_bits();
debug_assert!(self.is_float[index]);
debug_assert!(self.is_64[index]);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const RETURN_ADDR: u32 = 0x12345;
fn test_get_set(call_stack: &mut CallStack<'_>, index: u32, value: Value) {
call_stack.set_local(index, value);
assert_eq!(call_stack.get_local(index), value);
}
fn setup(call_stack: &mut CallStack<'_>) {
// Push a other few frames before the test frame, just to make the scenario more typical.
call_stack.push_frame(0x11111, &[(1, ValueType::I32)]);
call_stack.push_frame(0x22222, &[(2, ValueType::I32)]);
call_stack.push_frame(0x33333, &[(3, ValueType::I32)]);
// Create a test call frame with local variables of every type
let current_frame_local_decls = [
(8, ValueType::I32),
(4, ValueType::I64),
(2, ValueType::F32),
(1, ValueType::F64),
];
call_stack.push_frame(RETURN_ADDR, &current_frame_local_decls);
}
#[test]
fn test_all() {
let arena = Bump::new();
let mut call_stack = CallStack::new(&arena);
setup(&mut call_stack);
test_get_set(&mut call_stack, 0, Value::I32(123));
test_get_set(&mut call_stack, 8, Value::I64(123456));
test_get_set(&mut call_stack, 12, Value::F32(1.01));
test_get_set(&mut call_stack, 14, Value::F64(-1.1));
test_get_set(&mut call_stack, 0, Value::I32(i32::MIN));
test_get_set(&mut call_stack, 0, Value::I32(i32::MAX));
test_get_set(&mut call_stack, 8, Value::I64(i64::MIN));
test_get_set(&mut call_stack, 8, Value::I64(i64::MAX));
test_get_set(&mut call_stack, 12, Value::F32(f32::MIN));
test_get_set(&mut call_stack, 12, Value::F32(f32::MAX));
test_get_set(&mut call_stack, 14, Value::F64(f64::MIN));
test_get_set(&mut call_stack, 14, Value::F64(f64::MAX));
assert_eq!(call_stack.pop_frame(), RETURN_ADDR);
}
#[test]
#[should_panic]
fn test_type_error_i32() {
let arena = Bump::new();
let mut call_stack = CallStack::new(&arena);
setup(&mut call_stack);
test_get_set(&mut call_stack, 0, Value::F32(1.01));
}
#[test]
#[should_panic]
fn test_type_error_i64() {
let arena = Bump::new();
let mut call_stack = CallStack::new(&arena);
setup(&mut call_stack);
test_get_set(&mut call_stack, 8, Value::F32(1.01));
}
#[test]
#[should_panic]
fn test_type_error_f32() {
let arena = Bump::new();
let mut call_stack = CallStack::new(&arena);
setup(&mut call_stack);
test_get_set(&mut call_stack, 12, Value::I32(123));
}
#[test]
#[should_panic]
fn test_type_error_f64() {
let arena = Bump::new();
let mut call_stack = CallStack::new(&arena);
setup(&mut call_stack);
test_get_set(&mut call_stack, 14, Value::I32(123));
}
}

View file

@ -0,0 +1,577 @@
use bumpalo::{collections::Vec, Bump};
use roc_wasm_module::opcodes::OpCode;
use roc_wasm_module::parse::Parse;
use roc_wasm_module::sections::MemorySection;
use roc_wasm_module::WasmModule;
use crate::call_stack::CallStack;
use crate::value_stack::ValueStack;
use crate::Value;
#[derive(Debug)]
pub struct ExecutionState<'a> {
#[allow(dead_code)]
memory: Vec<'a, u8>,
#[allow(dead_code)]
call_stack: CallStack<'a>,
pub value_stack: ValueStack<'a>,
program_counter: usize,
}
impl<'a> ExecutionState<'a> {
pub fn new(arena: &'a Bump, memory_pages: u32, program_counter: usize) -> Self {
let mem_bytes = memory_pages * MemorySection::PAGE_SIZE;
ExecutionState {
memory: Vec::with_capacity_in(mem_bytes as usize, arena),
call_stack: CallStack::new(arena),
value_stack: ValueStack::new(arena),
program_counter,
}
}
pub fn execute_next_instruction(&mut self, module: &WasmModule<'a>) {
use OpCode::*;
let op_code = OpCode::from(module.code.bytes[self.program_counter]);
self.program_counter += 1;
match op_code {
UNREACHABLE => {
unreachable!("WebAssembly tried to execute an `unreachable` instruction.");
}
NOP => {}
BLOCK => {
todo!("{:?}", op_code);
}
LOOP => {
todo!("{:?}", op_code);
}
IF => {
todo!("{:?}", op_code);
}
ELSE => {
todo!("{:?}", op_code);
}
END => {
todo!("{:?}", op_code);
}
BR => {
todo!("{:?}", op_code);
}
BRIF => {
todo!("{:?}", op_code);
}
BRTABLE => {
todo!("{:?}", op_code);
}
RETURN => {
todo!("{:?}", op_code);
}
CALL => {
todo!("{:?}", op_code);
}
CALLINDIRECT => {
todo!("{:?}", op_code);
}
DROP => {
todo!("{:?}", op_code);
}
SELECT => {
todo!("{:?}", op_code);
}
GETLOCAL => {
todo!("{:?}", op_code);
}
SETLOCAL => {
todo!("{:?}", op_code);
}
TEELOCAL => {
todo!("{:?}", op_code);
}
GETGLOBAL => {
todo!("{:?}", op_code);
}
SETGLOBAL => {
todo!("{:?}", op_code);
}
I32LOAD => {
todo!("{:?}", op_code);
}
I64LOAD => {
todo!("{:?}", op_code);
}
F32LOAD => {
todo!("{:?}", op_code);
}
F64LOAD => {
todo!("{:?}", op_code);
}
I32LOAD8S => {
todo!("{:?}", op_code);
}
I32LOAD8U => {
todo!("{:?}", op_code);
}
I32LOAD16S => {
todo!("{:?}", op_code);
}
I32LOAD16U => {
todo!("{:?}", op_code);
}
I64LOAD8S => {
todo!("{:?}", op_code);
}
I64LOAD8U => {
todo!("{:?}", op_code);
}
I64LOAD16S => {
todo!("{:?}", op_code);
}
I64LOAD16U => {
todo!("{:?}", op_code);
}
I64LOAD32S => {
todo!("{:?}", op_code);
}
I64LOAD32U => {
todo!("{:?}", op_code);
}
I32STORE => {
todo!("{:?}", op_code);
}
I64STORE => {
todo!("{:?}", op_code);
}
F32STORE => {
todo!("{:?}", op_code);
}
F64STORE => {
todo!("{:?}", op_code);
}
I32STORE8 => {
todo!("{:?}", op_code);
}
I32STORE16 => {
todo!("{:?}", op_code);
}
I64STORE8 => {
todo!("{:?}", op_code);
}
I64STORE16 => {
todo!("{:?}", op_code);
}
I64STORE32 => {
todo!("{:?}", op_code);
}
CURRENTMEMORY => {
todo!("{:?}", op_code);
}
GROWMEMORY => {
todo!("{:?}", op_code);
}
I32CONST => {
let value = i32::parse((), &module.code.bytes, &mut self.program_counter).unwrap();
self.value_stack.push(Value::I32(value));
}
I64CONST => {
let value = i64::parse((), &module.code.bytes, &mut self.program_counter).unwrap();
self.value_stack.push(Value::I64(value));
}
F32CONST => {
let mut bytes = [0; 4];
bytes.copy_from_slice(&module.code.bytes[self.program_counter..][..4]);
self.value_stack.push(Value::F32(f32::from_le_bytes(bytes)));
self.program_counter += 4;
}
F64CONST => {
let mut bytes = [0; 8];
bytes.copy_from_slice(&module.code.bytes[self.program_counter..][..8]);
self.value_stack.push(Value::F64(f64::from_le_bytes(bytes)));
self.program_counter += 8;
}
I32EQZ => {
todo!("{:?}", op_code);
}
I32EQ => {
todo!("{:?}", op_code);
}
I32NE => {
todo!("{:?}", op_code);
}
I32LTS => {
todo!("{:?}", op_code);
}
I32LTU => {
todo!("{:?}", op_code);
}
I32GTS => {
todo!("{:?}", op_code);
}
I32GTU => {
todo!("{:?}", op_code);
}
I32LES => {
todo!("{:?}", op_code);
}
I32LEU => {
todo!("{:?}", op_code);
}
I32GES => {
todo!("{:?}", op_code);
}
I32GEU => {
todo!("{:?}", op_code);
}
I64EQZ => {
todo!("{:?}", op_code);
}
I64EQ => {
todo!("{:?}", op_code);
}
I64NE => {
todo!("{:?}", op_code);
}
I64LTS => {
todo!("{:?}", op_code);
}
I64LTU => {
todo!("{:?}", op_code);
}
I64GTS => {
todo!("{:?}", op_code);
}
I64GTU => {
todo!("{:?}", op_code);
}
I64LES => {
todo!("{:?}", op_code);
}
I64LEU => {
todo!("{:?}", op_code);
}
I64GES => {
todo!("{:?}", op_code);
}
I64GEU => {
todo!("{:?}", op_code);
}
F32EQ => {
todo!("{:?}", op_code);
}
F32NE => {
todo!("{:?}", op_code);
}
F32LT => {
todo!("{:?}", op_code);
}
F32GT => {
todo!("{:?}", op_code);
}
F32LE => {
todo!("{:?}", op_code);
}
F32GE => {
todo!("{:?}", op_code);
}
F64EQ => {
todo!("{:?}", op_code);
}
F64NE => {
todo!("{:?}", op_code);
}
F64LT => {
todo!("{:?}", op_code);
}
F64GT => {
todo!("{:?}", op_code);
}
F64LE => {
todo!("{:?}", op_code);
}
F64GE => {
todo!("{:?}", op_code);
}
I32CLZ => {
todo!("{:?}", op_code);
}
I32CTZ => {
todo!("{:?}", op_code);
}
I32POPCNT => {
todo!("{:?}", op_code);
}
I32ADD => {
let x = self.value_stack.pop_i32();
let y = self.value_stack.pop_i32();
self.value_stack.push(Value::I32(y + x));
}
I32SUB => {
let x = self.value_stack.pop_i32();
let y = self.value_stack.pop_i32();
self.value_stack.push(Value::I32(y - x));
}
I32MUL => {
let x = self.value_stack.pop_i32();
let y = self.value_stack.pop_i32();
self.value_stack.push(Value::I32(y * x));
}
I32DIVS => {
todo!("{:?}", op_code);
}
I32DIVU => {
todo!("{:?}", op_code);
}
I32REMS => {
todo!("{:?}", op_code);
}
I32REMU => {
todo!("{:?}", op_code);
}
I32AND => {
todo!("{:?}", op_code);
}
I32OR => {
todo!("{:?}", op_code);
}
I32XOR => {
todo!("{:?}", op_code);
}
I32SHL => {
todo!("{:?}", op_code);
}
I32SHRS => {
todo!("{:?}", op_code);
}
I32SHRU => {
todo!("{:?}", op_code);
}
I32ROTL => {
todo!("{:?}", op_code);
}
I32ROTR => {
todo!("{:?}", op_code);
}
I64CLZ => {
todo!("{:?}", op_code);
}
I64CTZ => {
todo!("{:?}", op_code);
}
I64POPCNT => {
todo!("{:?}", op_code);
}
I64ADD => {
todo!("{:?}", op_code);
}
I64SUB => {
todo!("{:?}", op_code);
}
I64MUL => {
todo!("{:?}", op_code);
}
I64DIVS => {
todo!("{:?}", op_code);
}
I64DIVU => {
todo!("{:?}", op_code);
}
I64REMS => {
todo!("{:?}", op_code);
}
I64REMU => {
todo!("{:?}", op_code);
}
I64AND => {
todo!("{:?}", op_code);
}
I64OR => {
todo!("{:?}", op_code);
}
I64XOR => {
todo!("{:?}", op_code);
}
I64SHL => {
todo!("{:?}", op_code);
}
I64SHRS => {
todo!("{:?}", op_code);
}
I64SHRU => {
todo!("{:?}", op_code);
}
I64ROTL => {
todo!("{:?}", op_code);
}
I64ROTR => {
todo!("{:?}", op_code);
}
F32ABS => {
todo!("{:?}", op_code);
}
F32NEG => {
todo!("{:?}", op_code);
}
F32CEIL => {
todo!("{:?}", op_code);
}
F32FLOOR => {
todo!("{:?}", op_code);
}
F32TRUNC => {
todo!("{:?}", op_code);
}
F32NEAREST => {
todo!("{:?}", op_code);
}
F32SQRT => {
todo!("{:?}", op_code);
}
F32ADD => {
todo!("{:?}", op_code);
}
F32SUB => {
todo!("{:?}", op_code);
}
F32MUL => {
todo!("{:?}", op_code);
}
F32DIV => {
todo!("{:?}", op_code);
}
F32MIN => {
todo!("{:?}", op_code);
}
F32MAX => {
todo!("{:?}", op_code);
}
F32COPYSIGN => {
todo!("{:?}", op_code);
}
F64ABS => {
todo!("{:?}", op_code);
}
F64NEG => {
todo!("{:?}", op_code);
}
F64CEIL => {
todo!("{:?}", op_code);
}
F64FLOOR => {
todo!("{:?}", op_code);
}
F64TRUNC => {
todo!("{:?}", op_code);
}
F64NEAREST => {
todo!("{:?}", op_code);
}
F64SQRT => {
todo!("{:?}", op_code);
}
F64ADD => {
todo!("{:?}", op_code);
}
F64SUB => {
todo!("{:?}", op_code);
}
F64MUL => {
todo!("{:?}", op_code);
}
F64DIV => {
todo!("{:?}", op_code);
}
F64MIN => {
todo!("{:?}", op_code);
}
F64MAX => {
todo!("{:?}", op_code);
}
F64COPYSIGN => {
todo!("{:?}", op_code);
}
I32WRAPI64 => {
todo!("{:?}", op_code);
}
I32TRUNCSF32 => {
todo!("{:?}", op_code);
}
I32TRUNCUF32 => {
todo!("{:?}", op_code);
}
I32TRUNCSF64 => {
todo!("{:?}", op_code);
}
I32TRUNCUF64 => {
todo!("{:?}", op_code);
}
I64EXTENDSI32 => {
todo!("{:?}", op_code);
}
I64EXTENDUI32 => {
todo!("{:?}", op_code);
}
I64TRUNCSF32 => {
todo!("{:?}", op_code);
}
I64TRUNCUF32 => {
todo!("{:?}", op_code);
}
I64TRUNCSF64 => {
todo!("{:?}", op_code);
}
I64TRUNCUF64 => {
todo!("{:?}", op_code);
}
F32CONVERTSI32 => {
todo!("{:?}", op_code);
}
F32CONVERTUI32 => {
todo!("{:?}", op_code);
}
F32CONVERTSI64 => {
todo!("{:?}", op_code);
}
F32CONVERTUI64 => {
todo!("{:?}", op_code);
}
F32DEMOTEF64 => {
todo!("{:?}", op_code);
}
F64CONVERTSI32 => {
todo!("{:?}", op_code);
}
F64CONVERTUI32 => {
todo!("{:?}", op_code);
}
F64CONVERTSI64 => {
todo!("{:?}", op_code);
}
F64CONVERTUI64 => {
todo!("{:?}", op_code);
}
F64PROMOTEF32 => {
todo!("{:?}", op_code);
}
I32REINTERPRETF32 => {
todo!("{:?}", op_code);
}
I64REINTERPRETF64 => {
todo!("{:?}", op_code);
}
F32REINTERPRETI32 => {
todo!("{:?}", op_code);
}
F64REINTERPRETI64 => {
todo!("{:?}", op_code);
}
}
}
}

View file

@ -0,0 +1,16 @@
mod call_stack;
mod execute;
mod value_stack;
// Exposed for testing only. Should eventually become private.
pub use call_stack::CallStack;
pub use execute::ExecutionState;
pub use value_stack::ValueStack;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Value {
I32(i32),
I64(i64),
F32(f32),
F64(f64),
}

View file

@ -0,0 +1,202 @@
use bitvec::vec::BitVec;
use bumpalo::{collections::Vec, Bump};
use roc_wasm_module::ValueType;
use std::{fmt::Debug, mem::size_of};
use crate::Value;
/// Memory-efficient Struct-of-Arrays storage for the value stack.
/// Pack the values and their types as densely as possible,
/// to get better cache usage, at the expense of some extra logic.
pub struct ValueStack<'a> {
bytes: Vec<'a, u8>,
is_float: BitVec,
is_64: BitVec,
}
macro_rules! pop_bytes {
($ty: ty, $bytes: expr) => {{
const SIZE: usize = size_of::<$ty>();
let bytes_idx = $bytes.len() - SIZE;
let mut b = [0; SIZE];
b.copy_from_slice(&$bytes[bytes_idx..][..SIZE]);
$bytes.truncate(bytes_idx);
<$ty>::from_ne_bytes(b)
}};
}
impl<'a> ValueStack<'a> {
pub fn new(arena: &'a Bump) -> Self {
ValueStack {
bytes: Vec::with_capacity_in(1024, arena),
is_float: BitVec::with_capacity(1024),
is_64: BitVec::with_capacity(1024),
}
}
pub fn push(&mut self, value: Value) {
match value {
Value::I32(x) => {
self.bytes.extend_from_slice(&x.to_ne_bytes());
self.is_float.push(false);
self.is_64.push(false);
}
Value::I64(x) => {
self.bytes.extend_from_slice(&x.to_ne_bytes());
self.is_float.push(false);
self.is_64.push(true);
}
Value::F32(x) => {
self.bytes.extend_from_slice(&x.to_ne_bytes());
self.is_float.push(true);
self.is_64.push(false);
}
Value::F64(x) => {
self.bytes.extend_from_slice(&x.to_ne_bytes());
self.is_float.push(true);
self.is_64.push(true);
}
}
}
pub fn pop(&mut self) -> Value {
let is_64 = self.is_64.pop().unwrap();
let is_float = self.is_float.pop().unwrap();
let size = if is_64 { 8 } else { 4 };
let bytes_idx = self.bytes.len() - size;
let value = self.get(is_64, is_float, bytes_idx);
self.bytes.truncate(bytes_idx);
value
}
fn get(&self, is_64: bool, is_float: bool, bytes_idx: usize) -> Value {
if is_64 {
let mut b = [0; 8];
b.copy_from_slice(&self.bytes[bytes_idx..][..8]);
if is_float {
Value::F64(f64::from_ne_bytes(b))
} else {
Value::I64(i64::from_ne_bytes(b))
}
} else {
let mut b = [0; 4];
b.copy_from_slice(&self.bytes[bytes_idx..][..4]);
if is_float {
Value::F32(f32::from_ne_bytes(b))
} else {
Value::I32(i32::from_ne_bytes(b))
}
}
}
pub fn pop_i32(&mut self) -> i32 {
match (self.is_float.pop(), self.is_64.pop()) {
(Some(false), Some(false)) => pop_bytes!(i32, self.bytes),
(Some(is_float), Some(is_64)) => panic!(
"Expected I32 but found {:?}",
type_from_flags(is_float, is_64)
),
_ => panic!("Expected I32 but value stack was empty"),
}
}
pub fn pop_i64(&mut self) -> i64 {
match (self.is_float.pop(), self.is_64.pop()) {
(Some(false), Some(true)) => pop_bytes!(i64, self.bytes),
(Some(is_float), Some(is_64)) => panic!(
"Expected I64 but found {:?}",
type_from_flags(is_float, is_64)
),
_ => panic!("Expected I64 but value stack was empty"),
}
}
pub fn pop_f32(&mut self) -> f32 {
match (self.is_float.pop(), self.is_64.pop()) {
(Some(true), Some(false)) => pop_bytes!(f32, self.bytes),
(Some(is_float), Some(is_64)) => panic!(
"Expected F32 but found {:?}",
type_from_flags(is_float, is_64)
),
_ => panic!("Expected F32 but value stack was empty"),
}
}
pub fn pop_f64(&mut self) -> f64 {
match (self.is_float.pop(), self.is_64.pop()) {
(Some(true), Some(true)) => pop_bytes!(f64, self.bytes),
(Some(is_float), Some(is_64)) => panic!(
"Expected F64 but found {:?}",
type_from_flags(is_float, is_64)
),
_ => panic!("Expected F64 but value stack was empty"),
}
}
}
fn type_from_flags(is_float: bool, is_64: bool) -> ValueType {
match (is_float, is_64) {
(false, false) => ValueType::I32,
(false, true) => ValueType::I64,
(true, false) => ValueType::F32,
(true, true) => ValueType::F64,
}
}
impl Debug for ValueStack<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[")?;
let mut index = 0;
assert_eq!(self.is_64.len(), self.is_float.len());
let iter_64 = self.is_64.iter().by_vals();
let iter_float = self.is_float.iter().by_vals();
for (i, (is_64, is_float)) in iter_64.zip(iter_float).enumerate() {
let value = self.get(is_64, is_float, index);
index += if is_64 { 8 } else { 4 };
value.fmt(f)?;
if i < self.is_64.len() - 1 {
write!(f, ", ")?;
}
}
write!(f, "]")
}
}
#[cfg(test)]
mod tests {
use super::*;
const VALUES: [Value; 4] = [
Value::I32(123),
Value::I64(123456),
Value::F32(1.01),
Value::F64(-1.1),
];
#[test]
fn test_push_pop() {
let arena = Bump::new();
let mut stack = ValueStack::new(&arena);
for val in VALUES {
stack.push(val);
}
for val in VALUES.iter().rev() {
let popped = stack.pop();
assert_eq!(popped, *val);
}
}
#[test]
fn test_debug_fmt() {
let arena = Bump::new();
let mut stack = ValueStack::new(&arena);
for val in VALUES {
stack.push(val);
}
assert_eq!(format!("{:?}", VALUES), format!("{:?}", stack));
}
}

View file

@ -0,0 +1,609 @@
#![cfg(test)]
use bumpalo::Bump;
use roc_wasm_interp::{ExecutionState, Value};
use roc_wasm_module::{opcodes::OpCode, SerialBuffer, WasmModule};
const DEFAULT_MEMORY_PAGES: u32 = 1;
const DEFAULT_PROGRAM_COUNTER: usize = 0;
// #[test]
// fn test_unreachable() {}
// #[test]
// fn test_nop() {}
// #[test]
// fn test_block() {}
// #[test]
// fn test_loop() {}
// #[test]
// fn test_if() {}
// #[test]
// fn test_else() {}
// #[test]
// fn test_end() {}
// #[test]
// fn test_br() {}
// #[test]
// fn test_brif() {}
// #[test]
// fn test_brtable() {}
// #[test]
// fn test_return() {}
// #[test]
// fn test_call() {}
// #[test]
// fn test_callindirect() {}
// #[test]
// fn test_drop() {}
// #[test]
// fn test_select() {}
// #[test]
// fn test_getlocal() {}
// #[test]
// fn test_setlocal() {}
// #[test]
// fn test_teelocal() {}
// #[test]
// fn test_getglobal() {}
// #[test]
// fn test_setglobal() {}
// #[test]
// fn test_i32load() {}
// #[test]
// fn test_i64load() {}
// #[test]
// fn test_f32load() {}
// #[test]
// fn test_f64load() {}
// #[test]
// fn test_i32load8s() {}
// #[test]
// fn test_i32load8u() {}
// #[test]
// fn test_i32load16s() {}
// #[test]
// fn test_i32load16u() {}
// #[test]
// fn test_i64load8s() {}
// #[test]
// fn test_i64load8u() {}
// #[test]
// fn test_i64load16s() {}
// #[test]
// fn test_i64load16u() {}
// #[test]
// fn test_i64load32s() {}
// #[test]
// fn test_i64load32u() {}
// #[test]
// fn test_i32store() {}
// #[test]
// fn test_i64store() {}
// #[test]
// fn test_f32store() {}
// #[test]
// fn test_f64store() {}
// #[test]
// fn test_i32store8() {}
// #[test]
// fn test_i32store16() {}
// #[test]
// fn test_i64store8() {}
// #[test]
// fn test_i64store16() {}
// #[test]
// fn test_i64store32() {}
// #[test]
// fn test_currentmemory() {}
// #[test]
// fn test_growmemory() {}
#[test]
fn test_i32const() {
let arena = Bump::new();
let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER);
let mut module = WasmModule::new(&arena);
module.code.bytes.push(OpCode::I32CONST as u8);
module.code.bytes.encode_i32(12345);
state.execute_next_instruction(&module);
assert_eq!(state.value_stack.pop(), Value::I32(12345))
}
#[test]
fn test_i64const() {
let arena = Bump::new();
let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER);
let mut module = WasmModule::new(&arena);
module.code.bytes.push(OpCode::I64CONST as u8);
module.code.bytes.encode_i64(1234567890);
state.execute_next_instruction(&module);
assert_eq!(state.value_stack.pop(), Value::I64(1234567890))
}
#[test]
fn test_f32const() {
let arena = Bump::new();
let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER);
let mut module = WasmModule::new(&arena);
module.code.bytes.push(OpCode::F32CONST as u8);
module.code.bytes.encode_f32(123.45);
state.execute_next_instruction(&module);
assert_eq!(state.value_stack.pop(), Value::F32(123.45))
}
#[test]
fn test_f64const() {
let arena = Bump::new();
let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER);
let mut module = WasmModule::new(&arena);
module.code.bytes.push(OpCode::F64CONST as u8);
module.code.bytes.encode_f64(12345.67890);
state.execute_next_instruction(&module);
assert_eq!(state.value_stack.pop(), Value::F64(12345.67890))
}
// #[test]
// fn test_i32eqz() {}
// #[test]
// fn test_i32eq() {}
// #[test]
// fn test_i32ne() {}
// #[test]
// fn test_i32lts() {}
// #[test]
// fn test_i32ltu() {}
// #[test]
// fn test_i32gts() {}
// #[test]
// fn test_i32gtu() {}
// #[test]
// fn test_i32les() {}
// #[test]
// fn test_i32leu() {}
// #[test]
// fn test_i32ges() {}
// #[test]
// fn test_i32geu() {}
// #[test]
// fn test_i64eqz() {}
// #[test]
// fn test_i64eq() {}
// #[test]
// fn test_i64ne() {}
// #[test]
// fn test_i64lts() {}
// #[test]
// fn test_i64ltu() {}
// #[test]
// fn test_i64gts() {}
// #[test]
// fn test_i64gtu() {}
// #[test]
// fn test_i64les() {}
// #[test]
// fn test_i64leu() {}
// #[test]
// fn test_i64ges() {}
// #[test]
// fn test_i64geu() {}
// #[test]
// fn test_f32eq() {}
// #[test]
// fn test_f32ne() {}
// #[test]
// fn test_f32lt() {}
// #[test]
// fn test_f32gt() {}
// #[test]
// fn test_f32le() {}
// #[test]
// fn test_f32ge() {}
// #[test]
// fn test_f64eq() {}
// #[test]
// fn test_f64ne() {}
// #[test]
// fn test_f64lt() {}
// #[test]
// fn test_f64gt() {}
// #[test]
// fn test_f64le() {}
// #[test]
// fn test_f64ge() {}
// #[test]
// fn test_i32clz() {}
// #[test]
// fn test_i32ctz() {}
// #[test]
// fn test_i32popcnt() {}
#[test]
fn test_i32add() {
let arena = Bump::new();
let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER);
let mut module = WasmModule::new(&arena);
module.code.bytes.push(OpCode::I32CONST as u8);
module.code.bytes.encode_i32(123);
module.code.bytes.push(OpCode::I32CONST as u8);
module.code.bytes.encode_i32(321);
module.code.bytes.push(OpCode::I32ADD as u8);
state.execute_next_instruction(&module);
state.execute_next_instruction(&module);
state.execute_next_instruction(&module);
assert_eq!(state.value_stack.pop(), Value::I32(444))
}
#[test]
fn test_i32sub() {
let arena = Bump::new();
let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER);
let mut module = WasmModule::new(&arena);
module.code.bytes.push(OpCode::I32CONST as u8);
module.code.bytes.encode_i32(123);
module.code.bytes.push(OpCode::I32CONST as u8);
module.code.bytes.encode_i32(321);
module.code.bytes.push(OpCode::I32SUB as u8);
state.execute_next_instruction(&module);
state.execute_next_instruction(&module);
state.execute_next_instruction(&module);
assert_eq!(state.value_stack.pop(), Value::I32(-198))
}
#[test]
fn test_i32mul() {
let arena = Bump::new();
let mut state = ExecutionState::new(&arena, DEFAULT_MEMORY_PAGES, DEFAULT_PROGRAM_COUNTER);
let mut module = WasmModule::new(&arena);
module.code.bytes.push(OpCode::I32CONST as u8);
module.code.bytes.encode_i32(123);
module.code.bytes.push(OpCode::I32CONST as u8);
module.code.bytes.encode_i32(321);
module.code.bytes.push(OpCode::I32MUL as u8);
state.execute_next_instruction(&module);
state.execute_next_instruction(&module);
state.execute_next_instruction(&module);
assert_eq!(state.value_stack.pop(), Value::I32(39483))
}
// #[test]
// fn test_i32divs() {}
// #[test]
// fn test_i32divu() {}
// #[test]
// fn test_i32rems() {}
// #[test]
// fn test_i32remu() {}
// #[test]
// fn test_i32and() {}
// #[test]
// fn test_i32or() {}
// #[test]
// fn test_i32xor() {}
// #[test]
// fn test_i32shl() {}
// #[test]
// fn test_i32shrs() {}
// #[test]
// fn test_i32shru() {}
// #[test]
// fn test_i32rotl() {}
// #[test]
// fn test_i32rotr() {}
// #[test]
// fn test_i64clz() {}
// #[test]
// fn test_i64ctz() {}
// #[test]
// fn test_i64popcnt() {}
// #[test]
// fn test_i64add() {}
// #[test]
// fn test_i64sub() {}
// #[test]
// fn test_i64mul() {}
// #[test]
// fn test_i64divs() {}
// #[test]
// fn test_i64divu() {}
// #[test]
// fn test_i64rems() {}
// #[test]
// fn test_i64remu() {}
// #[test]
// fn test_i64and() {}
// #[test]
// fn test_i64or() {}
// #[test]
// fn test_i64xor() {}
// #[test]
// fn test_i64shl() {}
// #[test]
// fn test_i64shrs() {}
// #[test]
// fn test_i64shru() {}
// #[test]
// fn test_i64rotl() {}
// #[test]
// fn test_i64rotr() {}
// #[test]
// fn test_f32abs() {}
// #[test]
// fn test_f32neg() {}
// #[test]
// fn test_f32ceil() {}
// #[test]
// fn test_f32floor() {}
// #[test]
// fn test_f32trunc() {}
// #[test]
// fn test_f32nearest() {}
// #[test]
// fn test_f32sqrt() {}
// #[test]
// fn test_f32add() {}
// #[test]
// fn test_f32sub() {}
// #[test]
// fn test_f32mul() {}
// #[test]
// fn test_f32div() {}
// #[test]
// fn test_f32min() {}
// #[test]
// fn test_f32max() {}
// #[test]
// fn test_f32copysign() {}
// #[test]
// fn test_f64abs() {}
// #[test]
// fn test_f64neg() {}
// #[test]
// fn test_f64ceil() {}
// #[test]
// fn test_f64floor() {}
// #[test]
// fn test_f64trunc() {}
// #[test]
// fn test_f64nearest() {}
// #[test]
// fn test_f64sqrt() {}
// #[test]
// fn test_f64add() {}
// #[test]
// fn test_f64sub() {}
// #[test]
// fn test_f64mul() {}
// #[test]
// fn test_f64div() {}
// #[test]
// fn test_f64min() {}
// #[test]
// fn test_f64max() {}
// #[test]
// fn test_f64copysign() {}
// #[test]
// fn test_i32wrapi64() {}
// #[test]
// fn test_i32truncsf32() {}
// #[test]
// fn test_i32truncuf32() {}
// #[test]
// fn test_i32truncsf64() {}
// #[test]
// fn test_i32truncuf64() {}
// #[test]
// fn test_i64extendsi32() {}
// #[test]
// fn test_i64extendui32() {}
// #[test]
// fn test_i64truncsf32() {}
// #[test]
// fn test_i64truncuf32() {}
// #[test]
// fn test_i64truncsf64() {}
// #[test]
// fn test_i64truncuf64() {}
// #[test]
// fn test_f32convertsi32() {}
// #[test]
// fn test_f32convertui32() {}
// #[test]
// fn test_f32convertsi64() {}
// #[test]
// fn test_f32convertui64() {}
// #[test]
// fn test_f32demotef64() {}
// #[test]
// fn test_f64convertsi32() {}
// #[test]
// fn test_f64convertui32() {}
// #[test]
// fn test_f64convertsi64() {}
// #[test]
// fn test_f64convertui64() {}
// #[test]
// fn test_f64promotef32() {}
// #[test]
// fn test_i32reinterpretf32() {}
// #[test]
// fn test_i64reinterpretf64() {}
// #[test]
// fn test_f32reinterpreti32() {}
// #[test]
// fn test_f64reinterpreti64() {}

View file

@ -0,0 +1,13 @@
[package]
name = "roc_wasm_module"
version = "0.0.1"
edition = "2021"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
description = "Parse, manipulate, and serialize WebAssembly modules."
[dependencies]
roc_error_macros = { path = "../error_macros" }
bitvec.workspace = true
bumpalo.workspace = true

View file

@ -1,4 +1,3 @@
pub mod code_builder;
pub mod linking; pub mod linking;
pub mod opcodes; pub mod opcodes;
pub mod parse; pub mod parse;
@ -7,15 +6,14 @@ pub mod serialize;
use std::iter::repeat; use std::iter::repeat;
pub use code_builder::{Align, CodeBuilder, LocalId, ValueType, VmSymbolState};
pub use linking::{OffsetRelocType, RelocationEntry, SymInfo}; pub use linking::{OffsetRelocType, RelocationEntry, SymInfo};
use opcodes::OpCode;
use roc_error_macros::internal_error;
pub use sections::{ConstExpr, Export, ExportType, Global, GlobalType, Signature}; pub use sections::{ConstExpr, Export, ExportType, Global, GlobalType, Signature};
use bitvec::vec::BitVec; use bitvec::vec::BitVec;
use bumpalo::{collections::Vec, Bump}; use bumpalo::{collections::Vec, Bump};
use crate::DEBUG_SETTINGS;
use self::linking::{IndexRelocType, LinkingSection, RelocationSection, WasmObjectSymbol}; use self::linking::{IndexRelocType, LinkingSection, RelocationSection, WasmObjectSymbol};
use self::parse::{Parse, ParseError}; use self::parse::{Parse, ParseError};
use self::sections::{ use self::sections::{
@ -23,7 +21,10 @@ use self::sections::{
ImportDesc, ImportSection, MemorySection, NameSection, OpaqueSection, Section, SectionId, ImportDesc, ImportSection, MemorySection, NameSection, OpaqueSection, Section, SectionId,
TableSection, TypeSection, TableSection, TypeSection,
}; };
use self::serialize::{SerialBuffer, Serialize}; pub use self::serialize::{SerialBuffer, Serialize};
pub const STACK_POINTER_GLOBAL_ID: u32 = 0;
pub const FRAME_ALIGNMENT_BYTES: i32 = 16;
/// A representation of the WebAssembly binary file format /// A representation of the WebAssembly binary file format
/// https://webassembly.github.io/spec/core/binary/modules.html /// https://webassembly.github.io/spec/core/binary/modules.html
@ -49,6 +50,26 @@ pub struct WasmModule<'a> {
impl<'a> WasmModule<'a> { impl<'a> WasmModule<'a> {
pub const WASM_VERSION: u32 = 1; pub const WASM_VERSION: u32 = 1;
pub fn new(arena: &'a Bump) -> Self {
WasmModule {
types: TypeSection::new(arena),
import: ImportSection::new(arena),
function: FunctionSection::new(arena),
table: TableSection::new(),
memory: MemorySection::new(arena, 0),
global: GlobalSection::new(arena),
export: ExportSection::new(arena),
start: OpaqueSection::new(),
element: ElementSection::new(arena),
code: CodeSection::new(arena),
data: DataSection::new(arena),
linking: LinkingSection::new(arena),
reloc_code: RelocationSection::new(arena, "reloc.CODE"),
reloc_data: RelocationSection::new(arena, "reloc.DATA"),
names: NameSection::new(arena),
}
}
/// Create entries in the Type and Function sections for a function signature /// Create entries in the Type and Function sections for a function signature
pub fn add_function_signature(&mut self, signature: Signature<'a>) { pub fn add_function_signature(&mut self, signature: Signature<'a>) {
let index = self.types.insert(signature); let index = self.types.insert(signature);
@ -131,7 +152,7 @@ impl<'a> WasmModule<'a> {
if function.signatures.is_empty() { if function.signatures.is_empty() {
module_errors.push_str("Missing Function section\n"); module_errors.push_str("Missing Function section\n");
} }
if code.preloaded_bytes.is_empty() { if code.bytes.is_empty() {
module_errors.push_str("Missing Code section\n"); module_errors.push_str("Missing Code section\n");
} }
if linking.symbol_table.is_empty() { if linking.symbol_table.is_empty() {
@ -176,17 +197,17 @@ impl<'a> WasmModule<'a> {
}) })
} }
pub fn eliminate_dead_code(&mut self, arena: &'a Bump, called_host_fns: BitVec<usize>) { pub fn eliminate_dead_code(&mut self, arena: &'a Bump, called_fns: BitVec<usize>) {
if DEBUG_SETTINGS.skip_dead_code_elim { if DEBUG_SETTINGS.skip_dead_code_elim {
return; return;
} }
// //
// Mark all live host functions // Mark all live functions
// //
let import_count = self.import.imports.len(); let import_count = self.import.imports.len();
let host_fn_min = import_count as u32 + self.code.dead_import_dummy_count; let fn_index_min = import_count as u32 + self.code.dead_import_dummy_count;
let host_fn_max = host_fn_min + self.code.preloaded_count; let fn_index_max = called_fns.len() as u32;
// All functions exported to JS must be kept alive // All functions exported to JS must be kept alive
let exported_fns = self let exported_fns = self
@ -212,13 +233,13 @@ impl<'a> WasmModule<'a> {
); );
// Trace callees of the live functions, and mark those as live too // Trace callees of the live functions, and mark those as live too
let live_flags = self.trace_live_host_functions( let live_flags = self.trace_live_functions(
arena, arena,
called_host_fns, called_fns,
exported_fns, exported_fns,
indirect_callees_and_signatures, indirect_callees_and_signatures,
host_fn_min, fn_index_min,
host_fn_max, fn_index_max,
); );
// //
@ -266,9 +287,9 @@ impl<'a> WasmModule<'a> {
self.names.function_names[old_index].1 = new_name; self.names.function_names[old_index].1 = new_name;
} }
// Relocate calls from host to JS imports // Relocate calls to JS imports
// This must happen *before* we run dead code elimination on the code section, // This must happen *before* we run dead code elimination on the code section,
// so that byte offsets in the host's linking data will still be valid. // so that byte offsets in the linking data will still be valid.
for (new_index, &old_index) in live_import_fns.iter().enumerate() { for (new_index, &old_index) in live_import_fns.iter().enumerate() {
if new_index == old_index { if new_index == old_index {
continue; continue;
@ -277,49 +298,41 @@ impl<'a> WasmModule<'a> {
.linking .linking
.find_and_reindex_imported_fn(old_index as u32, new_index as u32) .find_and_reindex_imported_fn(old_index as u32, new_index as u32)
.unwrap(); .unwrap();
self.reloc_code.apply_relocs_u32( self.reloc_code
&mut self.code.preloaded_bytes, .apply_relocs_u32(&mut self.code.bytes, sym_index, new_index as u32);
sym_index,
new_index as u32,
);
}
// Relocate calls from Roc app to JS imports
for code_builder in self.code.code_builders.iter_mut() {
code_builder.apply_import_relocs(&live_import_fns);
} }
// //
// Dead code elimination. Replace dead functions with tiny dummies. // Code section: Replace dead functions with tiny dummies.
// Live function indices are unchanged, so no relocations are needed. // Live function indices are unchanged, so no relocations are needed.
// //
let dummy = CodeBuilder::dummy(arena); let mut buffer = Vec::with_capacity_in(self.code.bytes.len(), arena);
let mut dummy_bytes = Vec::with_capacity_in(dummy.size(), arena); self.code.function_count.serialize(&mut buffer);
dummy.serialize(&mut dummy_bytes); for (i, fn_index) in (fn_index_min..fn_index_max).enumerate() {
let mut buffer = Vec::with_capacity_in(self.code.preloaded_bytes.len(), arena);
self.code.preloaded_count.serialize(&mut buffer);
for (i, fn_index) in (host_fn_min..host_fn_max).enumerate() {
if live_flags[fn_index as usize] { if live_flags[fn_index as usize] {
let code_start = self.code.preloaded_offsets[i] as usize; let code_start = self.code.function_offsets[i] as usize;
let code_end = self.code.preloaded_offsets[i + 1] as usize; let code_end = if i < self.code.function_offsets.len() - 1 {
buffer.extend_from_slice(&self.code.preloaded_bytes[code_start..code_end]); self.code.function_offsets[i + 1] as usize
} else {
self.code.bytes.len()
};
buffer.extend_from_slice(&self.code.bytes[code_start..code_end]);
} else { } else {
buffer.extend_from_slice(&dummy_bytes); DUMMY_FUNCTION.serialize(&mut buffer);
} }
} }
self.code.preloaded_bytes = buffer; self.code.bytes = buffer;
} }
fn trace_live_host_functions<I: Iterator<Item = u32>>( fn trace_live_functions<I: Iterator<Item = u32>>(
&self, &self,
arena: &'a Bump, arena: &'a Bump,
called_host_fns: BitVec<usize>, called_fns: BitVec<usize>,
exported_fns: I, exported_fns: I,
indirect_callees_and_signatures: Vec<'a, (u32, u32)>, indirect_callees_and_signatures: Vec<'a, (u32, u32)>,
host_fn_min: u32, fn_index_min: u32,
host_fn_max: u32, fn_index_max: u32,
) -> BitVec<usize> { ) -> BitVec<usize> {
let reloc_len = self.reloc_code.entries.len(); let reloc_len = self.reloc_code.entries.len();
@ -356,10 +369,10 @@ impl<'a> WasmModule<'a> {
); );
// Loop variables for the main loop below // Loop variables for the main loop below
let mut live_flags = BitVec::repeat(false, called_host_fns.len()); let mut live_flags = BitVec::repeat(false, called_fns.len());
let mut next_pass_fns = BitVec::repeat(false, called_host_fns.len()); let mut next_pass_fns = BitVec::repeat(false, called_fns.len());
let mut current_pass_fns = called_host_fns; let mut current_pass_fns = called_fns;
for index in exported_fns.filter(|i| *i < host_fn_max) { for index in exported_fns {
current_pass_fns.set(index as usize, true); current_pass_fns.set(index as usize, true);
} }
@ -371,14 +384,18 @@ impl<'a> WasmModule<'a> {
// For each live function in the current pass // For each live function in the current pass
for fn_index in current_pass_fns.iter_ones() { for fn_index in current_pass_fns.iter_ones() {
// Skip JS imports and Roc functions // Skip JS imports and Roc functions
if fn_index < host_fn_min as usize || fn_index >= host_fn_max as usize { if fn_index < fn_index_min as usize || fn_index >= fn_index_max as usize {
continue; continue;
} }
// Find where the function body is // Find where the function body is
let offset_index = fn_index - host_fn_min as usize; let offset_index = fn_index - fn_index_min as usize;
let code_start = self.code.preloaded_offsets[offset_index]; let code_start = self.code.function_offsets[offset_index];
let code_end = self.code.preloaded_offsets[offset_index + 1]; let code_end = if offset_index < self.code.function_offsets.len() - 1 {
self.code.function_offsets[offset_index + 1]
} else {
self.code.bytes.len() as u32
};
// For each call in the body // For each call in the body
for (offset, symbol) in call_offsets_and_symbols.iter() { for (offset, symbol) in call_offsets_and_symbols.iter() {
@ -422,11 +439,8 @@ impl<'a> WasmModule<'a> {
self.linking self.linking
.find_internal_symbol(sym_name) .find_internal_symbol(sym_name)
.map(|sym_index| { .map(|sym_index| {
self.reloc_code.apply_relocs_u32( self.reloc_code
&mut self.code.preloaded_bytes, .apply_relocs_u32(&mut self.code.bytes, sym_index as u32, value);
sym_index as u32,
value,
);
sym_index as u32 sym_index as u32
}) })
@ -493,11 +507,8 @@ impl<'a> WasmModule<'a> {
.unwrap(); .unwrap();
// Update calls to use the app function instead of the host import // Update calls to use the app function instead of the host import
self.reloc_code.apply_relocs_u32( self.reloc_code
&mut self.code.preloaded_bytes, .apply_relocs_u32(&mut self.code.bytes, host_sym_index, app_fn_index);
host_sym_index,
app_fn_index,
);
if swap_import_index != host_import_index { if swap_import_index != host_import_index {
// get the name using the old host import index because we already swapped it! // get the name using the old host import index because we already swapped it!
@ -511,7 +522,7 @@ impl<'a> WasmModule<'a> {
// Update calls to the swapped JS import // Update calls to the swapped JS import
self.reloc_code.apply_relocs_u32( self.reloc_code.apply_relocs_u32(
&mut self.code.preloaded_bytes, &mut self.code.bytes,
swap_sym_index, swap_sym_index,
host_fn_index as u32, host_fn_index as u32,
); );
@ -593,3 +604,123 @@ impl<'a> WasmModule<'a> {
) )
} }
} }
/*******************************************************************
*
* Common types & utility functions
*
*******************************************************************/
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LocalId(pub u32);
/// Wasm value type. (Rust representation matches Wasm encoding)
#[repr(u8)]
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum ValueType {
I32 = 0x7f,
I64 = 0x7e,
F32 = 0x7d,
F64 = 0x7c,
}
impl Serialize for ValueType {
fn serialize<T: SerialBuffer>(&self, buffer: &mut T) {
buffer.append_u8(*self as u8);
}
}
impl From<u8> for ValueType {
fn from(x: u8) -> Self {
match x {
0x7f => Self::I32,
0x7e => Self::I64,
0x7d => Self::F32,
0x7c => Self::F64,
_ => internal_error!("Invalid ValueType 0x{:02x}", x),
}
}
}
/// Wasm memory alignment for load/store instructions.
/// Rust representation matches Wasm encoding.
/// It's an error to specify alignment higher than the "natural" alignment of the instruction
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
pub enum Align {
Bytes1 = 0,
Bytes2 = 1,
Bytes4 = 2,
Bytes8 = 3,
}
impl Align {
/// Calculate the largest possible alignment for a load/store at a given stack frame offset
/// Assumes the stack frame is aligned to at least 8 bytes
pub fn from_stack_offset(max_align: Align, offset: u32) -> Align {
if (max_align == Align::Bytes8) && (offset & 7 == 0) {
return Align::Bytes8;
}
if (max_align >= Align::Bytes4) && (offset & 3 == 0) {
return Align::Bytes4;
}
if (max_align >= Align::Bytes2) && (offset & 1 == 0) {
return Align::Bytes2;
}
Align::Bytes1
}
}
impl From<u32> for Align {
fn from(x: u32) -> Align {
match x {
1 => Align::Bytes1,
2 => Align::Bytes2,
4 => Align::Bytes4,
_ => {
if x.count_ones() == 1 {
Align::Bytes8 // Max value supported by any Wasm instruction
} else {
internal_error!("Cannot align to {} bytes", x);
}
}
}
}
}
/// Round up to alignment_bytes (which must be a power of 2)
#[macro_export]
macro_rules! round_up_to_alignment {
($unaligned: expr, $alignment_bytes: expr) => {
if $alignment_bytes <= 1 {
$unaligned
} else if $alignment_bytes.count_ones() != 1 {
internal_error!(
"Cannot align to {} bytes. Not a power of 2.",
$alignment_bytes
);
} else {
let mut aligned = $unaligned;
aligned += $alignment_bytes - 1; // if lower bits are non-zero, push it over the next boundary
aligned &= !$alignment_bytes + 1; // mask with a flag that has upper bits 1, lower bits 0
aligned
}
};
}
/// Bytes for a dummy function with just a single `unreachable` instruction.
/// Used in dead code elimination to replace unused functions.
const DUMMY_FUNCTION: [u8; 3] = [
0, // number of local variable declarations
OpCode::UNREACHABLE as u8, // panic if we were wrong to eliminate!
OpCode::END as u8, // end of function (required for validation)
];
// TODO: make this an environment variable
pub struct WasmDebugSettings {
pub skip_dead_code_elim: bool,
}
pub const DEBUG_SETTINGS: WasmDebugSettings = WasmDebugSettings {
skip_dead_code_elim: false && cfg!(debug_assertions),
};

View file

@ -1,10 +1,9 @@
use bumpalo::collections::vec::Vec; use bumpalo::collections::vec::Vec;
use bumpalo::Bump; use bumpalo::Bump;
use super::parse::parse_fixed_size_items; use super::parse::{parse_fixed_size_items, Parse, ParseError, SkipBytes};
use super::sections::SectionId; use super::sections::SectionId;
use super::serialize::{overwrite_padded_i32, overwrite_padded_u32}; use super::serialize::{overwrite_padded_i32, overwrite_padded_u32};
use crate::wasm_module::parse::{Parse, ParseError, SkipBytes};
/******************************************************************* /*******************************************************************
* *
@ -159,7 +158,7 @@ pub struct RelocationSection<'a> {
} }
impl<'a> RelocationSection<'a> { impl<'a> RelocationSection<'a> {
fn new(arena: &'a Bump, name: &'a str) -> Self { pub(crate) fn new(arena: &'a Bump, name: &'a str) -> Self {
RelocationSection { RelocationSection {
name, name,
target_section_index: 0, target_section_index: 0,
@ -583,6 +582,20 @@ impl<'a> LinkingSection<'a> {
}) })
} }
pub fn find_imported_fn_sym_index(&mut self, fn_index: u32) -> Result<u32, String> {
self.symbol_table
.iter_mut()
.position(|sym| match sym {
SymInfo::Function(WasmObjectSymbol::ImplicitlyNamed { flags, index, .. })
| SymInfo::Function(WasmObjectSymbol::ExplicitlyNamed { flags, index, .. }) => {
*flags & WASM_SYM_UNDEFINED != 0 && *index == fn_index
}
_ => false,
})
.map(|sym_index| sym_index as u32)
.ok_or_else(|| format!("Can't find fn #{} in host symbol table", fn_index))
}
pub fn find_and_reindex_imported_fn( pub fn find_and_reindex_imported_fn(
&mut self, &mut self,
old_fn_index: u32, old_fn_index: u32,

View file

@ -183,6 +183,193 @@ pub enum OpCode {
F64REINTERPRETI64 = 0xbf, F64REINTERPRETI64 = 0xbf,
} }
impl From<u8> for OpCode {
fn from(x: u8) -> Self {
use OpCode::*;
match x {
0x00 => UNREACHABLE,
0x01 => NOP,
0x02 => BLOCK,
0x03 => LOOP,
0x04 => IF,
0x05 => ELSE,
0x0b => END,
0x0c => BR,
0x0d => BRIF,
0x0e => BRTABLE,
0x0f => RETURN,
0x10 => CALL,
0x11 => CALLINDIRECT,
0x1a => DROP,
0x1b => SELECT,
0x20 => GETLOCAL,
0x21 => SETLOCAL,
0x22 => TEELOCAL,
0x23 => GETGLOBAL,
0x24 => SETGLOBAL,
0x28 => I32LOAD,
0x29 => I64LOAD,
0x2a => F32LOAD,
0x2b => F64LOAD,
0x2c => I32LOAD8S,
0x2d => I32LOAD8U,
0x2e => I32LOAD16S,
0x2f => I32LOAD16U,
0x30 => I64LOAD8S,
0x31 => I64LOAD8U,
0x32 => I64LOAD16S,
0x33 => I64LOAD16U,
0x34 => I64LOAD32S,
0x35 => I64LOAD32U,
0x36 => I32STORE,
0x37 => I64STORE,
0x38 => F32STORE,
0x39 => F64STORE,
0x3a => I32STORE8,
0x3b => I32STORE16,
0x3c => I64STORE8,
0x3d => I64STORE16,
0x3e => I64STORE32,
0x3f => CURRENTMEMORY,
0x40 => GROWMEMORY,
0x41 => I32CONST,
0x42 => I64CONST,
0x43 => F32CONST,
0x44 => F64CONST,
0x45 => I32EQZ,
0x46 => I32EQ,
0x47 => I32NE,
0x48 => I32LTS,
0x49 => I32LTU,
0x4a => I32GTS,
0x4b => I32GTU,
0x4c => I32LES,
0x4d => I32LEU,
0x4e => I32GES,
0x4f => I32GEU,
0x50 => I64EQZ,
0x51 => I64EQ,
0x52 => I64NE,
0x53 => I64LTS,
0x54 => I64LTU,
0x55 => I64GTS,
0x56 => I64GTU,
0x57 => I64LES,
0x58 => I64LEU,
0x59 => I64GES,
0x5a => I64GEU,
0x5b => F32EQ,
0x5c => F32NE,
0x5d => F32LT,
0x5e => F32GT,
0x5f => F32LE,
0x60 => F32GE,
0x61 => F64EQ,
0x62 => F64NE,
0x63 => F64LT,
0x64 => F64GT,
0x65 => F64LE,
0x66 => F64GE,
0x67 => I32CLZ,
0x68 => I32CTZ,
0x69 => I32POPCNT,
0x6a => I32ADD,
0x6b => I32SUB,
0x6c => I32MUL,
0x6d => I32DIVS,
0x6e => I32DIVU,
0x6f => I32REMS,
0x70 => I32REMU,
0x71 => I32AND,
0x72 => I32OR,
0x73 => I32XOR,
0x74 => I32SHL,
0x75 => I32SHRS,
0x76 => I32SHRU,
0x77 => I32ROTL,
0x78 => I32ROTR,
0x79 => I64CLZ,
0x7a => I64CTZ,
0x7b => I64POPCNT,
0x7c => I64ADD,
0x7d => I64SUB,
0x7e => I64MUL,
0x7f => I64DIVS,
0x80 => I64DIVU,
0x81 => I64REMS,
0x82 => I64REMU,
0x83 => I64AND,
0x84 => I64OR,
0x85 => I64XOR,
0x86 => I64SHL,
0x87 => I64SHRS,
0x88 => I64SHRU,
0x89 => I64ROTL,
0x8a => I64ROTR,
0x8b => F32ABS,
0x8c => F32NEG,
0x8d => F32CEIL,
0x8e => F32FLOOR,
0x8f => F32TRUNC,
0x90 => F32NEAREST,
0x91 => F32SQRT,
0x92 => F32ADD,
0x93 => F32SUB,
0x94 => F32MUL,
0x95 => F32DIV,
0x96 => F32MIN,
0x97 => F32MAX,
0x98 => F32COPYSIGN,
0x99 => F64ABS,
0x9a => F64NEG,
0x9b => F64CEIL,
0x9c => F64FLOOR,
0x9d => F64TRUNC,
0x9e => F64NEAREST,
0x9f => F64SQRT,
0xa0 => F64ADD,
0xa1 => F64SUB,
0xa2 => F64MUL,
0xa3 => F64DIV,
0xa4 => F64MIN,
0xa5 => F64MAX,
0xa6 => F64COPYSIGN,
0xa7 => I32WRAPI64,
0xa8 => I32TRUNCSF32,
0xa9 => I32TRUNCUF32,
0xaa => I32TRUNCSF64,
0xab => I32TRUNCUF64,
0xac => I64EXTENDSI32,
0xad => I64EXTENDUI32,
0xae => I64TRUNCSF32,
0xaf => I64TRUNCUF32,
0xb0 => I64TRUNCSF64,
0xb1 => I64TRUNCUF64,
0xb2 => F32CONVERTSI32,
0xb3 => F32CONVERTUI32,
0xb4 => F32CONVERTSI64,
0xb5 => F32CONVERTUI64,
0xb6 => F32DEMOTEF64,
0xb7 => F64CONVERTSI32,
0xb8 => F64CONVERTUI32,
0xb9 => F64CONVERTSI64,
0xba => F64CONVERTUI64,
0xbb => F64PROMOTEF32,
0xbc => I32REINTERPRETF32,
0xbd => I64REINTERPRETF64,
0xbe => F32REINTERPRETI32,
0xbf => F64REINTERPRETI64,
_ => unreachable!(),
}
}
}
/// The format of the *immediate* operands of an operator /// The format of the *immediate* operands of an operator
/// Immediates appear directly in the byte stream after the opcode, /// Immediates appear directly in the byte stream after the opcode,
/// rather than being popped off the value stack. These are the possible forms. /// rather than being popped off the value stack. These are the possible forms.
@ -264,7 +451,7 @@ impl SkipBytes for OpCode {
let opcode_byte: u8 = bytes[*cursor]; let opcode_byte: u8 = bytes[*cursor];
let opcode: OpCode = unsafe { std::mem::transmute(opcode_byte) }; let opcode: OpCode = OpCode::from(opcode_byte);
// will return Err if transmute was invalid // will return Err if transmute was invalid
let immediates = immediates_for(opcode).map_err(|message| ParseError { let immediates = immediates_for(opcode).map_err(|message| ParseError {
message, message,

View file

@ -1,4 +1,4 @@
use super::serialize::MAX_SIZE_ENCODED_U32; use super::serialize::{MAX_SIZE_ENCODED_U32, MAX_SIZE_ENCODED_U64};
use bumpalo::collections::vec::Vec; use bumpalo::collections::vec::Vec;
use bumpalo::Bump; use bumpalo::Bump;
@ -92,6 +92,43 @@ impl Parse<()> for i32 {
} }
} }
/// Decode a signed 64-bit integer from the provided buffer in LEB-128 format
/// Return the integer itself and the offset after it ends
fn decode_i64(bytes: &[u8]) -> Result<(i64, usize), ()> {
let mut value = 0;
let mut shift = 0;
for (i, byte) in bytes.iter().take(MAX_SIZE_ENCODED_U64).enumerate() {
value |= ((byte & 0x7f) as i64) << shift;
if (byte & 0x80) == 0 {
let is_negative = byte & 0x40 != 0;
if shift < MAX_SIZE_ENCODED_U64 && is_negative {
value |= -1 << shift;
}
return Ok((value, i + 1));
}
shift += 7;
}
Err(())
}
impl Parse<()> for i64 {
fn parse(_ctx: (), bytes: &[u8], cursor: &mut usize) -> Result<Self, ParseError> {
match decode_i64(&bytes[*cursor..]) {
Ok((value, len)) => {
*cursor += len;
Ok(value)
}
Err(()) => Err(ParseError {
offset: *cursor,
message: format!(
"Failed to decode i64 as LEB-128 from bytes: {:2x?}",
&bytes[*cursor..][..MAX_SIZE_ENCODED_U64]
),
}),
}
}
}
impl<'a> Parse<&'a Bump> for &'a str { impl<'a> Parse<&'a Bump> for &'a str {
fn parse(arena: &'a Bump, bytes: &[u8], cursor: &mut usize) -> Result<Self, ParseError> { fn parse(arena: &'a Bump, bytes: &[u8], cursor: &mut usize) -> Result<Self, ParseError> {
let len = u32::parse((), bytes, cursor)?; let len = u32::parse((), bytes, cursor)?;
@ -204,7 +241,7 @@ impl SkipBytes for String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::wasm_module::parse::decode_u32; use crate::parse::decode_u32;
#[test] #[test]
fn test_decode_u32() { fn test_decode_u32() {

View file

@ -4,11 +4,13 @@ use bumpalo::collections::vec::Vec;
use bumpalo::Bump; use bumpalo::Bump;
use roc_error_macros::internal_error; use roc_error_macros::internal_error;
use crate::DUMMY_FUNCTION;
use super::linking::{LinkingSection, SymInfo, WasmObjectSymbol}; use super::linking::{LinkingSection, SymInfo, WasmObjectSymbol};
use super::opcodes::OpCode; use super::opcodes::OpCode;
use super::parse::{Parse, ParseError, SkipBytes}; use super::parse::{Parse, ParseError, SkipBytes};
use super::serialize::{SerialBuffer, Serialize, MAX_SIZE_ENCODED_U32}; use super::serialize::{SerialBuffer, Serialize, MAX_SIZE_ENCODED_U32};
use super::{CodeBuilder, ValueType}; use super::ValueType;
/******************************************************************* /*******************************************************************
* *
@ -218,6 +220,14 @@ pub struct TypeSection<'a> {
} }
impl<'a> TypeSection<'a> { impl<'a> TypeSection<'a> {
pub fn new(arena: &'a Bump) -> Self {
TypeSection {
arena,
bytes: Vec::new_in(arena),
offsets: Vec::new_in(arena),
}
}
/// Find a matching signature or insert a new one. Return the index. /// Find a matching signature or insert a new one. Return the index.
pub fn insert(&mut self, signature: Signature<'a>) -> u32 { pub fn insert(&mut self, signature: Signature<'a>) -> u32 {
let mut sig_bytes = Vec::with_capacity_in(signature.param_types.len() + 4, self.arena); let mut sig_bytes = Vec::with_capacity_in(signature.param_types.len() + 4, self.arena);
@ -423,6 +433,12 @@ pub struct ImportSection<'a> {
impl<'a> ImportSection<'a> { impl<'a> ImportSection<'a> {
const ID: SectionId = SectionId::Import; const ID: SectionId = SectionId::Import;
pub fn new(arena: &'a Bump) -> Self {
ImportSection {
imports: Vec::new_in(arena),
}
}
pub fn size(&self) -> usize { pub fn size(&self) -> usize {
self.imports.iter().map(|imp| imp.size()).sum() self.imports.iter().map(|imp| imp.size()).sum()
} }
@ -486,6 +502,12 @@ pub struct FunctionSection<'a> {
} }
impl<'a> FunctionSection<'a> { impl<'a> FunctionSection<'a> {
pub fn new(arena: &'a Bump) -> Self {
FunctionSection {
signatures: Vec::new_in(arena),
}
}
pub fn add_sig(&mut self, sig_id: u32) { pub fn add_sig(&mut self, sig_id: u32) {
self.signatures.push(sig_id); self.signatures.push(sig_id);
} }
@ -525,8 +547,8 @@ impl<'a> Serialize for FunctionSection<'a> {
* *
* Table section * Table section
* *
* Defines tables used for indirect references to host memory. * Defines tables used for indirect references to external code or data.
* The table *contents* are elsewhere, in the ElementSection. * The table *contents* are in the ElementSection.
* *
*******************************************************************/ *******************************************************************/
@ -588,6 +610,16 @@ pub struct TableSection {
impl TableSection { impl TableSection {
const ID: SectionId = SectionId::Table; const ID: SectionId = SectionId::Table;
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
TableSection {
function_table: TableType {
ref_type: RefType::Func,
limits: Limits::Min(0),
},
}
}
pub fn size(&self) -> usize { pub fn size(&self) -> usize {
let section_id_bytes = 1; let section_id_bytes = 1;
let section_length_bytes = 1; let section_length_bytes = 1;
@ -878,6 +910,13 @@ pub struct GlobalSection<'a> {
} }
impl<'a> GlobalSection<'a> { impl<'a> GlobalSection<'a> {
pub fn new(arena: &'a Bump) -> Self {
GlobalSection {
count: 0,
bytes: Vec::new_in(arena),
}
}
pub fn parse_u32_at_index(&self, index: u32) -> Result<u32, ParseError> { pub fn parse_u32_at_index(&self, index: u32) -> Result<u32, ParseError> {
let mut cursor = 0; let mut cursor = 0;
for _ in 0..index { for _ in 0..index {
@ -959,6 +998,12 @@ pub struct ExportSection<'a> {
impl<'a> ExportSection<'a> { impl<'a> ExportSection<'a> {
const ID: SectionId = SectionId::Export; const ID: SectionId = SectionId::Export;
pub fn new(arena: &'a Bump) -> Self {
ExportSection {
exports: Vec::new_in(arena),
}
}
pub fn append(&mut self, export: Export<'a>) { pub fn append(&mut self, export: Export<'a>) {
self.exports.push(export); self.exports.push(export);
} }
@ -1084,6 +1129,12 @@ pub struct ElementSection<'a> {
impl<'a> ElementSection<'a> { impl<'a> ElementSection<'a> {
const ID: SectionId = SectionId::Element; const ID: SectionId = SectionId::Element;
pub fn new(arena: &'a Bump) -> Self {
ElementSection {
segments: Vec::new_in(arena),
}
}
/// Get a table index for a function (equivalent to a function pointer) /// Get a table index for a function (equivalent to a function pointer)
/// The function will be inserted into the table if it's not already there. /// The function will be inserted into the table if it's not already there.
/// This index is what the call_indirect instruction expects. /// This index is what the call_indirect instruction expects.
@ -1168,20 +1219,26 @@ impl<'a> Serialize for ElementSection<'a> {
#[derive(Debug)] #[derive(Debug)]
pub struct CodeSection<'a> { pub struct CodeSection<'a> {
pub preloaded_count: u32, pub function_count: u32,
pub preloaded_bytes: Vec<'a, u8>, pub bytes: Vec<'a, u8>,
/// The start of each preloaded function /// The start of each preloaded function
pub preloaded_offsets: Vec<'a, u32>, pub function_offsets: Vec<'a, u32>,
/// Dead imports are replaced with dummy functions in CodeSection /// Dead imports are replaced with dummy functions in CodeSection
pub dead_import_dummy_count: u32, pub dead_import_dummy_count: u32,
pub code_builders: Vec<'a, CodeBuilder<'a>>,
} }
impl<'a> CodeSection<'a> { impl<'a> CodeSection<'a> {
pub fn size(&self) -> usize { pub fn new(arena: &'a Bump) -> Self {
let builders_size: usize = self.code_builders.iter().map(|cb| cb.size()).sum(); CodeSection {
function_count: 0,
bytes: Vec::new_in(arena),
function_offsets: Vec::new_in(arena),
dead_import_dummy_count: 0,
}
}
MAX_SIZE_SECTION_HEADER + self.preloaded_bytes.len() + builders_size pub fn size(&self) -> usize {
MAX_SIZE_SECTION_HEADER + self.bytes.len()
} }
pub fn parse( pub fn parse(
@ -1196,39 +1253,34 @@ impl<'a> CodeSection<'a> {
}); });
} }
*cursor += 1; *cursor += 1;
let section_size = u32::parse((), module_bytes, cursor)?; let section_size = u32::parse((), module_bytes, cursor)? as usize;
let section_body_start = *cursor; let section_body_start = *cursor;
let count = u32::parse((), module_bytes, cursor)?; let function_count = u32::parse((), module_bytes, cursor)?;
let function_bodies_start = *cursor; let next_section_start = section_body_start + section_size;
let next_section_start = section_body_start + section_size as usize;
// preloaded_bytes starts at the function count, since that's considered the zero offset in the linker data. // `bytes` must include the function count for linker offsets to be correct.
// But when we finally write to file, we'll exclude the function count and write our own, including app fns. let mut bytes = Vec::with_capacity_in(section_size + section_size / 2, arena);
let mut preloaded_bytes = bytes.extend_from_slice(&module_bytes[section_body_start..*cursor]);
Vec::with_capacity_in(next_section_start - function_bodies_start, arena);
preloaded_bytes.extend_from_slice(&module_bytes[section_body_start..*cursor]);
let mut preloaded_offsets = Vec::with_capacity_in(count as usize, arena); let mut function_offsets = Vec::with_capacity_in(function_count as usize, arena);
// While copying the code bytes, also note where each function starts & ends // While copying the code bytes, also note where each function starts & ends
// Later we will use this for dead code elimination // Later we will use this for dead code elimination
while *cursor < next_section_start { while *cursor < next_section_start {
let fn_start = *cursor; let fn_start = *cursor;
preloaded_offsets.push((fn_start - section_body_start) as u32); function_offsets.push((fn_start - section_body_start) as u32);
let fn_length = u32::parse((), module_bytes, cursor)? as usize; let fn_length = u32::parse((), module_bytes, cursor)? as usize;
*cursor += fn_length; *cursor += fn_length;
preloaded_bytes.extend_from_slice(&module_bytes[fn_start..*cursor]); bytes.extend_from_slice(&module_bytes[fn_start..*cursor]);
} }
preloaded_offsets.push((next_section_start - section_body_start) as u32);
debug_assert_eq!(preloaded_offsets.len(), 1 + count as usize); debug_assert_eq!(function_offsets.len(), function_count as usize);
Ok(CodeSection { Ok(CodeSection {
preloaded_count: count, function_count,
preloaded_bytes, bytes,
preloaded_offsets, function_offsets,
dead_import_dummy_count: 0, dead_import_dummy_count: 0,
code_builders: Vec::with_capacity_in(0, arena),
}) })
} }
} }
@ -1236,26 +1288,17 @@ impl<'a> CodeSection<'a> {
impl<'a> Serialize for CodeSection<'a> { impl<'a> Serialize for CodeSection<'a> {
fn serialize<T: SerialBuffer>(&self, buffer: &mut T) { fn serialize<T: SerialBuffer>(&self, buffer: &mut T) {
let header_indices = write_section_header(buffer, SectionId::Code); let header_indices = write_section_header(buffer, SectionId::Code);
buffer.encode_u32( buffer.encode_u32(self.dead_import_dummy_count + self.function_count);
self.dead_import_dummy_count + self.preloaded_count + self.code_builders.len() as u32,
);
// Insert dummy functions, requested by our linking logic. // Insert dummy functions, requested by our linking logic.
// This helps to minimise the number of functions we need to move around during linking. // This helps to minimise the number of functions we need to move around during linking.
let arena = self.code_builders[0].arena;
let dummy = CodeBuilder::dummy(arena);
for _ in 0..self.dead_import_dummy_count { for _ in 0..self.dead_import_dummy_count {
dummy.serialize(buffer); DUMMY_FUNCTION.serialize(buffer);
} }
// host + builtin functions // real functions
let first_fn_start = self.preloaded_offsets[0] as usize; let first_fn_start = self.function_offsets[0] as usize;
buffer.append_slice(&self.preloaded_bytes[first_fn_start..]); buffer.append_slice(&self.bytes[first_fn_start..]);
// Roc functions
for code_builder in self.code_builders.iter() {
code_builder.serialize(buffer);
}
update_section_size(buffer, header_indices); update_section_size(buffer, header_indices);
} }
@ -1344,6 +1387,14 @@ pub struct DataSection<'a> {
impl<'a> DataSection<'a> { impl<'a> DataSection<'a> {
const ID: SectionId = SectionId::Data; const ID: SectionId = SectionId::Data;
pub fn new(arena: &'a Bump) -> Self {
DataSection {
end_addr: 0,
count: 0,
bytes: Vec::new_in(arena),
}
}
pub fn size(&self) -> usize { pub fn size(&self) -> usize {
MAX_SIZE_SECTION_HEADER + self.bytes.len() MAX_SIZE_SECTION_HEADER + self.bytes.len()
} }
@ -1409,6 +1460,10 @@ pub struct OpaqueSection<'a> {
} }
impl<'a> OpaqueSection<'a> { impl<'a> OpaqueSection<'a> {
pub fn new() -> Self {
OpaqueSection { bytes: &[] }
}
pub fn size(&self) -> usize { pub fn size(&self) -> usize {
self.bytes.len() self.bytes.len()
} }
@ -1479,7 +1534,7 @@ impl<'a> NameSection<'a> {
self.function_names.push((index, name)); self.function_names.push((index, name));
} }
pub fn empty(arena: &'a Bump) -> Self { pub fn new(arena: &'a Bump) -> Self {
NameSection { NameSection {
function_names: bumpalo::vec![in arena], function_names: bumpalo::vec![in arena],
} }
@ -1517,12 +1572,12 @@ impl<'a> Parse<&'a Bump> for NameSection<'a> {
// If we're already past the end of the preloaded file then there is no Name section // If we're already past the end of the preloaded file then there is no Name section
if *cursor >= module_bytes.len() { if *cursor >= module_bytes.len() {
return Ok(Self::empty(arena)); return Ok(Self::new(arena));
} }
// Custom section ID // Custom section ID
if module_bytes[*cursor] != Self::ID as u8 { if module_bytes[*cursor] != Self::ID as u8 {
return Ok(Self::empty(arena)); return Ok(Self::new(arena));
} }
*cursor += 1; *cursor += 1;
@ -1535,7 +1590,7 @@ impl<'a> Parse<&'a Bump> for NameSection<'a> {
// This is a different Custom section. This host has no debug info. // This is a different Custom section. This host has no debug info.
// Not a parse error, just an empty section. // Not a parse error, just an empty section.
*cursor = cursor_start; *cursor = cursor_start;
return Ok(Self::empty(arena)); return Ok(Self::new(arena));
} }
// Find function names subsection // Find function names subsection

View file

@ -6,8 +6,9 @@ use std::fmt::Debug;
/// In practice, this saves space, since small numbers used more often than large numbers. /// In practice, this saves space, since small numbers used more often than large numbers.
/// Of course there is a price for this - an encoded U32 can be up to 5 bytes wide. /// Of course there is a price for this - an encoded U32 can be up to 5 bytes wide.
pub const MAX_SIZE_ENCODED_U32: usize = 5; pub const MAX_SIZE_ENCODED_U32: usize = 5;
pub const MAX_SIZE_ENCODED_U64: usize = 10;
pub(super) trait Serialize { pub trait Serialize {
fn serialize<T: SerialBuffer>(&self, buffer: &mut T); fn serialize<T: SerialBuffer>(&self, buffer: &mut T);
} }

1
design/editor/design.md Normal file
View file

@ -0,0 +1 @@
Should the editor organize all UI into a tree for easy altering/communication with plugins?

View file

@ -238,7 +238,11 @@ e.g. you have a test `calculate_sum_test` that only uses the function `add`, whe
- In the file explorer, auto-close subtree after unused for certain time. - In the file explorer, auto-close subtree after unused for certain time.
- Ability to right click error message > "create github issue". - Ability to right click error message > "create github issue".
- Record (locally) all steps performed by editor + logs and make them searchable for user. The user should be able to scrub through these steps. - Record (locally) all steps performed by editor + logs and make them searchable for user. The user should be able to scrub through these steps.
- clustering of search results (rg like) based on statistics of result; amount of spaces, avg word length, amount of numbers...
- Search engine to answer questions about your code base, logs, all intermediary values. Examples: search for "xy" in a string, search for xy in a variable name, show all intermediary values of index variable...
- When a stacktrace is shown in the logs, highlight all lines of code in the editor that are in the stacktrace.
- Double-click to select all child expressions of parent: [example](https://twitter.com/disconcision/status/1587156531203678208?s=20&t=GySrwPVnMB6rDKFqRuUcBw).
- We should encourage users to ask editor something in the command window; e.g. "hide all types", "List docs", "switch light mode"... If the questions can not be matched to existing actions, the user should be given the option to share the question with editor developers so the action can be created or mapped to an existing action.
#### Autocomplete #### Autocomplete
@ -291,6 +295,7 @@ e.g. you have a test `calculate_sum_test` that only uses the function `add`, whe
- Users with large private code bases could (re)train a publicly available error recovery model to experience benefits without having to share their code. - Users with large private code bases could (re)train a publicly available error recovery model to experience benefits without having to share their code.
- It could be useful to a user who is creating a function to show them the most similar function (type signature, name, comment) in a public+their private database. Say I was using a web framework and I just created a function that has a multipart form as argument, it would be great to have an example instantly available. - It could be useful to a user who is creating a function to show them the most similar function (type signature, name, comment) in a public+their private database. Say I was using a web framework and I just created a function that has a multipart form as argument, it would be great to have an example instantly available.
- A simpler start for this idea without user data gathering: how the user a code snippet that is most similar to what they are currently writing. Snippets can be aggregated from examples, tests, docstrings at zero cost to the package/platform authors. - A simpler start for this idea without user data gathering: how the user a code snippet that is most similar to what they are currently writing. Snippets can be aggregated from examples, tests, docstrings at zero cost to the package/platform authors.
- Train ML model to rank snippets by likely usefulness to user.
- See [codata](https://www.codota.com/code/java/classes/okhttp3.OkHttpClient) for inspiration on a snippet/example finder. - See [codata](https://www.codota.com/code/java/classes/okhttp3.OkHttpClient) for inspiration on a snippet/example finder.
- Fuzzy natural language based setting adjustment in search bar or with voice input: increase font size, enable autosave, switch to light theme... - Fuzzy natural language based setting adjustment in search bar or with voice input: increase font size, enable autosave, switch to light theme...
- Detect deviation of best practices, example case: alert developer when they are defining a color inline (rgb(30,30,30)) while all colors have been previously imported from a single file. See also [Codota](https://www.codota.com). - Detect deviation of best practices, example case: alert developer when they are defining a color inline (rgb(30,30,30)) while all colors have been previously imported from a single file. See also [Codota](https://www.codota.com).
@ -301,6 +306,7 @@ e.g. you have a test `calculate_sum_test` that only uses the function `add`, whe
- Chatbot that can answer questions about the code base. - Chatbot that can answer questions about the code base.
- Smart navigation assistant to help you navigate with fuzzy text: take me to the false-interpreter's platform. - Smart navigation assistant to help you navigate with fuzzy text: take me to the false-interpreter's platform.
- select some code (x='a'\ny='b') > Open transform command window > Type: concat chars > AI adds line: `concatenated = Char.concat 'a' 'b'` - select some code (x='a'\ny='b') > Open transform command window > Type: concat chars > AI adds line: `concatenated = Char.concat 'a' 'b'`
- Use terminal output to predict suggestions for files to navigate to.
## Testing ## Testing
@ -339,24 +345,6 @@ e.g. you have a test `calculate_sum_test` that only uses the function `add`, whe
- Inclusion of step-by-step tutrials in Roc libraries, platforms or business specific code. - Inclusion of step-by-step tutrials in Roc libraries, platforms or business specific code.
- Having to set up your own website for a tutorial can be a lot of work, making it easy to make quality tutorials would make for a more delightful experience. - Having to set up your own website for a tutorial can be a lot of work, making it easy to make quality tutorials would make for a more delightful experience.
## General Plugin Ideas
### Ideas
- Plugin to translate linux commands like curl to Roc code
- Plugin to view diff between two texts
- Plugin to present codebase to new developer or walk co-worker through a problem. Records sequence of filenames and line numbers.
- A Logbook plugin. I've found that writing down steps and thoughts when you're implementing or debugging something can be really useful for later.
If we make an integrated terminal, we can automatically add executed commands to this logbook. This plugin could have a publish button so you can produce useful "blogs" for others with minimal effort.
### Inspiration
- [Boop](https://github.com/IvanMathy/Boop) scriptable scratchpad for developers. Contains collection of useful conversions: json formatting, url encoding, encode to base64...
- [processing](https://processing.org) Interactive editor, dragging left or right with mouse to change values. Instant results.
- [flowistry](https://github.com/willcrichton/flowistry) easily track all named values in a certain expression throughout your program.
- Match blocks of assembly with lines of code: [link](https://twitter.com/dgryski/status/1547952259828330498).
- [livelits](https://hazel.org/build/livelits/) inline interactive plugin blocks
## High performance ## High performance
### Inspiration ### Inspiration
@ -379,12 +367,6 @@ If we make an integrated terminal, we can automatically add executed commands to
- [Duolingo](https://www.duolingo.com) app to learn languages - [Duolingo](https://www.duolingo.com) app to learn languages
- [Khan academy](https://www.khanacademy.org/) free quality education for everyone - [Khan academy](https://www.khanacademy.org/) free quality education for everyone
## Security
- Remove permissions if plugin has not been used for a long time.
- Log plugin actions that require a permission.
- Show plugin that is currently using a permission, e.g. roc-core is reading from folder /gitrepos/hello in status bar and with a scrollable log.
## Accessibility ## Accessibility
- Visual Imapirments - Visual Imapirments

View file

@ -0,0 +1,3 @@
- [Action state design document](https://docs.google.com/document/d/16qY4NGVOHu8mvInVD-ddTajZYSsFvFBvQON_hmyHGfo/edit?usp=drivesdk)
- [Roc theme changer plugin concept study](https://gist.github.com/lukewilliamboswell/1b921641e7d68457ba88d81747c1bd44)
- Plugin can register view (=UI) to editor; editor can use that view to render all occurrences of (a value of) that type.

View file

@ -0,0 +1,5 @@
- [Hazel Livelits](https://hazel.org/papers/livelits-paper.pdf) interactive plugins, see GIF's [here](https://twitter.com/disconcision/status/1408155781120376833).
- [Boop](https://github.com/IvanMathy/Boop) scriptable scratchpad for developers. Contains collection of useful conversions: json formatting, url encoding, encode to base64...
- [processing](https://processing.org) Interactive editor, dragging left or right with mouse to change values. Instant results.
- [flowistry](https://github.com/willcrichton/flowistry) easily track all named values in a certain expression throughout your program.
- Match blocks of assembly with lines of code: [link](https://twitter.com/dgryski/status/1547952259828330498).

View file

@ -0,0 +1,19 @@
# Bundled with package (=library)
- Regex description to actual regex.
- Color picker.
- Visualising parser execution: [example](https://github.com/fasterthanlime/pegviz).
# Standalone
- Translating short pieces of code from another programming language to Roc. [Relevant research](https://www.youtube.com/watch?v=xTzFJIknh7E). Someone who only knows the R language could get started with Roc with less friction if they could quickly define a list R style (`lst <- c(1,2,3)`) and get it translated to Roc.
- Translate linux commands like curl to Roc code.
- Diff viewer
- Plugin to present codebase to new developer or walk co-worker through a problem. Records sequence of filenames and line numbers.
- Logbook; I've found that writing down steps and thoughts when you're implementing or debugging something can be really useful for later. If we make an integrated terminal, we can automatically add executed commands to this logbook. This plugin could have a publish button so you can produce useful "blogs" for others with minimal effort.
- Upload selected code/file to gist.
- Friendly/smart text filtering; be able to filter values by selecting a part. For example; logs contain `roc_expect_buffer_109680`, we select `109680`, numbers at identical positions are extracted.
- Diagram rendering from text, similar to [mermaid](https://mermaid-js.github.io/mermaid/#/).
- Pomodoro timer to remind you to take breaks regularly.

View file

@ -0,0 +1,3 @@
- Remove permissions if plugin has not been used for a long time.
- Log plugin actions that require a permission.
- Show plugin that is currently using a permission, e.g. roc-core is reading from folder /gitrepos/hello in status bar and with a scrollable log.

View file

@ -0,0 +1,33 @@
# Inspiration
## Plugin window organization
- Notebooks:
+ Jupyter Notebook
- ![notebook image](https://github.com/quantopian/pyfolio/blob/master/pyfolio/examples/sector_mappings_example.ipynb)
- The notebooks scrollable nature makes embedding plugins in the UI easy and clean.
+ Elixir Livebook
- [example video](https://youtu.be/lyiqw3O8d_A?t=246)
+ Note: notebooks fit very well if you are trying to tell a story; for a tutorial, analyzing data, interactive textbook, prototyping… . I have not seen them used for large systems or codebases.
- Glamorous toolkit:
+ ![Gtoolkit overview](https://raw.githubusercontent.com/feenkcom/gtoolkit/main/doc/gtr-overview.png)
+ multi-language notebook, code editor, software analysis platform, visualization engine, knowledge management system
- Design software:
+ blender
- ![blender screenshot](https://cdn.80.lv/api/upload/content/a0/60f703b067544.jpg)
+ photoshop
- ![photoshop screenshot](https://www.startpage.com/av/proxy-image?piurl=https%3A%2F%2Fi.ytimg.com%2Fvi%2FiMo20Gqj-sM%2Fmaxresdefault.jpg&sp=1668869642T68632e7db288b83ef9c5bf202804c88f48718ba3fd052003e161e78c4e1adb79)
+ gimp
- ![gimp screenshot](https://www.lifewire.com/thmb/R-Gy2C1RdRH_KbiaS04rPvehA7I=/1366x0/filters:no_upscale():max_bytes(150000):strip_icc():format(webp)/gimp-photo-open-6d392895064245ec8e3b1ecd09f0651a.jpg)
- Music production software:
+ Ableton Live
- ![ableton screenshot](https://i.pcmag.com/imagery/reviews/026WYE4XN99dDtWflTxyi5l-33.fit_lim.size_1152x.jpg)
- Hazel livelit:
+ Plugins are rendered inline.
+ [link](https://hazel.org/build/livelits/), test with `let foo<tab>$color`
# Prototyping
- [Visually organizing plugin windows](https://docs.google.com/presentation/d/1Oc8w5lZw2dg2L5evt42d3FsmT2deYo4JNoqxNsh4q9M/edit?usp=drivesdk)

View file

@ -124,8 +124,7 @@ toHelp = \parser ->
succeed : a -> Parser a succeed : a -> Parser a
succeed = \val -> @Parser (Succeed val) succeed = \val -> @Parser (Succeed val)
# TODO: check overflows when this annotation is included toHelpHelper : Parser *, List Config -> Help
# toHelpHelper : Parser *, List Config -> Help
toHelpHelper = \@Parser parser, configs -> toHelpHelper = \@Parser parser, configs ->
when parser is when parser is
Succeed _ -> Config configs Succeed _ -> Config configs

View file

@ -13,20 +13,14 @@ rm -rf build/
cp -r public/ build/ cp -r public/ build/
# download fonts just-in-time so we don't have to bloat the repo with them. # download fonts just-in-time so we don't have to bloat the repo with them.
mkdir build/fonts DESIGN_ASSETS_COMMIT="4d949642ebc56ca455cf270b288382788bce5873"
pushd build/fonts DESIGN_ASSETS_TARFILE="roc-lang-design-assets-4d94964.tar.gz"
DESIGN_ASSETS_DIR="roc-lang-design-assets-4d94964"
wget -O PermanentMarker.woff2 https://fonts.gstatic.com/s/permanentmarker/v16/Fh4uPib9Iyv2ucM6pGQMWimMp004La2Cf5b6jlg.woff2 wget -O $DESIGN_ASSETS_TARFILE https://github.com/roc-lang/design-assets/tarball/$DESIGN_ASSETS_COMMIT
wget -O MerriweatherExt.woff2 https://fonts.gstatic.com/s/merriweather/v30/u-440qyriQwlOrhSvowK_l5-ciZMdeX3rsHo.woff2 tar -xzf $DESIGN_ASSETS_TARFILE
wget -O Merriweather.woff2 https://fonts.gstatic.com/s/merriweather/v30/u-440qyriQwlOrhSvowK_l5-fCZMdeX3rg.woff2 mv $DESIGN_ASSETS_DIR/fonts build/
wget -O MerriweatherSansExt.woff https://fonts.gstatic.com/s/merriweathersans/v22/2-cO9IRs1JiJN1FRAMjTN5zd9vgsFF_5asQTb6hZ2JKZou4Vh-sBzRRXnKOrnx4.woff rm -rf $DESIGN_ASSETS_TARFILE $DESIGN_ASSETS_DIR
wget -O MerriweatherSans.woff https://fonts.gstatic.com/s/merriweathersans/v22/2-cO9IRs1JiJN1FRAMjTN5zd9vgsFF_5asQTb6hZ2JKZou4ViesBzRRXnKOr.woff
wget -O LatoExt.woff2 https://fonts.gstatic.com/s/lato/v23/S6uyw4BMUTPHjxAwXiWtFCfQ7A.woff2
wget -O Lato.woff2 https://fonts.gstatic.com/s/lato/v23/S6uyw4BMUTPHjx4wXiWtFCc.woff2
wget -O SourceCodeProExt.woff https://fonts.gstatic.com/s/sourcecodepro/v22/HI_diYsKILxRpg3hIP6sJ7fM7PqPMcMnZFqUwX28DMyQtMdrSlcZZJmOpwVS.woff
wget -O SourceCodePro.woff https://fonts.gstatic.com/s/sourcecodepro/v22/HI_diYsKILxRpg3hIP6sJ7fM7PqPMcMnZFqUwX28DMyQtMlrSlcZZJmOpw.woff
popd
# grab the source code and copy it to Netlify's server; if it's not there, fail the build. # grab the source code and copy it to Netlify's server; if it's not there, fail the build.
pushd build pushd build

View file

@ -17,7 +17,16 @@
# unsafe-eval is needed for wasm compilation in the repl to work on Safari and Chrome; # unsafe-eval is needed for wasm compilation in the repl to work on Safari and Chrome;
# otherwise they block it. # otherwise they block it.
# TODO figure out how to tell Netlify to apply that policy only to the repl, not to everything. # TODO figure out how to tell Netlify to apply that policy only to the repl, not to everything.
Content-Security-Policy = "default-src 'self'; img-src *; script-src 'self' 'unsafe-eval';" #
# This style-src hash is to permit the <style> in the favicon so it looks good in both light and
# dark mode. Favicons can only do this using inline <style> tags, so this exception is needed!
#
# When changing the favicon's <style>, generate a new hash and put it here using:
#
# $ echo -n "polygon{fill:#7d59dd}@media (prefers-color-scheme: dark){polygon{fill:#9c7bea}}" | openssl dgst -sha256 -binary | openssl enc -base64
#
# Details on how this works: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src
Content-Security-Policy = "default-src 'self'; img-src *; script-src 'self' 'unsafe-eval'; style-src 'self' 'sha256-07CmErfGgav8i4u4UOrGznokE/Q4Cax2Kmj7zGcW4l8=';"
X-Content-Type-Options = "nosniff" X-Content-Type-Options = "nosniff"
# Firefox prefetch requires some cache-control to be set # Firefox prefetch requires some cache-control to be set
# See https://bugzilla.mozilla.org/show_bug.cgi?id=1527334 # See https://bugzilla.mozilla.org/show_bug.cgi?id=1527334

View file

@ -1,4 +0,0 @@
<svg viewBox="0 0 52 53" xmlns="http://www.w3.org/2000/svg">
<style>polygon {fill: #7d59dd;}@media (prefers-color-scheme: dark) {polygon {fill: #9c7bea;}}</style>
<polygon points="0,0 23.8834,3.21052 37.2438,19.0101 45.9665,16.6324 50.5,22 45,22 44.0315,26.3689 26.4673,39.3424 27.4527,45.2132 17.655,53 23.6751,22.7086"/>
</svg>

Before

Width:  |  Height:  |  Size: 334 B

View file

@ -367,7 +367,8 @@ h4 {
font-family: 'Permanent Marker'; font-family: 'Permanent Marker';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("fonts/PermanentMarker.woff2") format('woff2'); src: url('/fonts/permanent-marker-v16-latin/permanent-marker-v16-latin-regular.woff2') format('woff2'),
url('/fonts/permanent-marker-v16-latin/permanent-marker-v16-latin-regular.woff') format('woff');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@ -376,7 +377,8 @@ h4 {
font-family: 'Merriweather'; font-family: 'Merriweather';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("fonts/MerriweatherExt.woff2") format('woff2'); src: url('/fonts/merriweather-v30-latin-ext_latin/merriweather-v30-latin-ext_latin-regular.woff2') format('woff2'),
url('/fonts/merriweather-v30-latin-ext_latin/merriweather-v30-latin-ext_latin-regular.woff') format('woff');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
@ -385,7 +387,8 @@ h4 {
font-family: 'Merriweather'; font-family: 'Merriweather';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("fonts/Merriweather.woff2") format('woff2'); src: url('/fonts/merriweather-v30-latin/merriweather-v30-latin-regular.woff2') format('woff2'),
url('/fonts/merriweather-v30-latin/merriweather-v30-latin-regular.woff') format('woff');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@ -394,7 +397,8 @@ h4 {
font-family: 'Merriweather Sans'; font-family: 'Merriweather Sans';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("fonts/MerriweatherSansExt.woff") format('woff'); src: url('/fonts/merriweather-sans-v22-latin-ext_latin/merriweather-sans-v22-latin-ext_latin-regular.woff2') format('woff2'),
url('/fonts/merriweather-sans-v22-latin-ext_latin/merriweather-sans-v22-latin-ext_latin-regular.woff') format('woff');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
/* latin */ /* latin */
@ -402,7 +406,8 @@ h4 {
font-family: 'Merriweather Sans'; font-family: 'Merriweather Sans';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("fonts/MerriweatherSans.woff") format('woff'); src: url('/fonts/merriweather-sans-v22-latin/merriweather-sans-v22-latin-regular.woff2') format('woff2'),
url('/fonts/merriweather-sans-v22-latin/merriweather-sans-v22-latin-regular.woff') format('woff');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@ -411,7 +416,8 @@ h4 {
font-family: 'Lato'; font-family: 'Lato';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: src("fonts/LatoExt.woff2") format("woff2"); src: url('/fonts/lato-v23-latin-ext_latin/lato-v23-latin-ext_latin-regular.woff2') format('woff2'),
url('/fonts/lato-v23-latin-ext_latin/lato-v23-latin-ext_latin-regular.woff') format('woff');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
/* latin */ /* latin */
@ -419,7 +425,8 @@ h4 {
font-family: 'Lato'; font-family: 'Lato';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("fonts/Lato.woff2") format('woff2'); src: url('/fonts/lato-v23-latin/lato-v23-latin-regular.woff2') format('woff2'),
url('/fonts/lato-v23-latin/lato-v23-latin-regular.woff') format('woff');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
@ -428,7 +435,8 @@ h4 {
font-family: 'Source Code Pro'; font-family: 'Source Code Pro';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("fonts/SourceCodeProExt.woff") format('woff'); src: url('/fonts/source-code-pro-v22-latin-ext_latin/source-code-pro-v22-latin-ext_latin-regular.woff2') format('woff2'),
url('/fonts/source-code-pro-v22-latin-ext_latin/source-code-pro-v22-latin-ext_latin-regular.woff') format('woff');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
/* latin */ /* latin */
@ -436,7 +444,8 @@ h4 {
font-family: 'Source Code Pro'; font-family: 'Source Code Pro';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url("fonts/SourceCodePro.woff") format('woff'); src: url('/fonts/source-code-pro-v22-latin/source-code-pro-v22-latin-regular.woff2') format('woff2'),
url('/fonts/source-code-pro-v22-latin/source-code-pro-v22-latin-regular.woff') format('woff');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }

View file

@ -1718,13 +1718,5 @@ For example, <code>[Foo Str][Bar Bool][Baz (List Str)]</code> is equivalent to <
</table> </table>
</main> </main>
<footer>Made by people who like to make nice things. © 2022</footer> <footer>Made by people who like to make nice things. © 2022</footer>
<script>
// When you double-click in a repl box, select its content (to make it easier to copy/paste).
document.querySelectorAll(".repl-prompt").forEach((elem) => {
elem.addEventListener("dblclick", (event) => {
getSelection().selectAllChildren(event.target.closest(".repl-prompt"));
});
});
</script>
</body> </body>
</html> </html>