diff --git a/Cargo.lock b/Cargo.lock index 09595750c4..a41b95749d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5084,8 +5084,12 @@ dependencies = [ "pretty_assertions", "regex", "roc_builtins", + "roc_collections", "roc_derive", "roc_load", + "roc_module", + "roc_mono", + "roc_packaging", "roc_parse", "roc_problem", "roc_reporting", diff --git a/crates/compiler/mono/src/ir.rs b/crates/compiler/mono/src/ir.rs index dc807292db..e453f75319 100644 --- a/crates/compiler/mono/src/ir.rs +++ b/crates/compiler/mono/src/ir.rs @@ -5049,9 +5049,12 @@ pub fn with_hole<'a>( ); } CopyExisting(index) => { - let record_needs_specialization = - procs.ability_member_aliases.get(structure).is_some(); - let specialized_structure_sym = if record_needs_specialization { + let structure_needs_specialization = + procs.ability_member_aliases.get(structure).is_some() + || procs.is_module_thunk(structure) + || procs.is_imported_module_thunk(structure); + + let specialized_structure_sym = if structure_needs_specialization { // We need to specialize the record now; create a new one for it. // TODO: reuse this symbol for all updates env.unique_symbol() @@ -5068,10 +5071,7 @@ pub fn with_hole<'a>( stmt = Stmt::Let(*symbol, access_expr, *field_layout, arena.alloc(stmt)); - // If the records needs specialization or it's a thunk, we need to - // create the specialized definition or force the thunk, respectively. - // Both cases are handled below. - if record_needs_specialization || procs.is_module_thunk(structure) { + if structure_needs_specialization { stmt = specialize_symbol( env, procs, diff --git a/crates/compiler/solve/tests/solve_expr.rs b/crates/compiler/solve/tests/solve_expr.rs index 1aeb12b22d..c86499166a 100644 --- a/crates/compiler/solve/tests/solve_expr.rs +++ b/crates/compiler/solve/tests/solve_expr.rs @@ -31,7 +31,7 @@ mod solve_expr { .. }, src, - ) = run_load_and_infer(src, false)?; + ) = run_load_and_infer(src, [], false)?; let mut can_problems = can_problems.remove(&home).unwrap_or_default(); let type_problems = type_problems.remove(&home).unwrap_or_default(); @@ -103,7 +103,7 @@ mod solve_expr { interns, abilities_store, .. - } = run_load_and_infer(src, false).unwrap().0; + } = run_load_and_infer(src, [], false).unwrap().0; let can_problems = can_problems.remove(&home).unwrap_or_default(); let type_problems = type_problems.remove(&home).unwrap_or_default(); diff --git a/crates/compiler/test_solve_helpers/src/lib.rs b/crates/compiler/test_solve_helpers/src/lib.rs index 61e0c0477d..74a818d9c1 100644 --- a/crates/compiler/test_solve_helpers/src/lib.rs +++ b/crates/compiler/test_solve_helpers/src/lib.rs @@ -44,8 +44,9 @@ fn promote_expr_to_module(src: &str) -> String { buffer } -pub fn run_load_and_infer( +pub fn run_load_and_infer<'a>( src: &str, + dependencies: impl IntoIterator, no_promote: bool, ) -> Result<(LoadedModule, String), std::io::Error> { use tempfile::tempdir; @@ -65,6 +66,11 @@ pub fn run_load_and_infer( let loaded = { let dir = tempdir()?; + + for (file, source) in dependencies { + std::fs::write(dir.path().join(format!("{file}.roc")), source)?; + } + let filename = PathBuf::from("Test.roc"); let file_path = dir.path().join(filename); let result = roc_load::load_and_typecheck_str( @@ -338,7 +344,11 @@ impl InferredProgram { } } -pub fn infer_queries(src: &str, options: InferOptions) -> Result> { +pub fn infer_queries<'a>( + src: &str, + dependencies: impl IntoIterator, + options: InferOptions, +) -> Result> { let ( LoadedModule { module_id: home, @@ -351,7 +361,7 @@ pub fn infer_queries(src: &str, options: InferOptions) -> Result Result QueryCtx<'a> { }) } } - -pub fn infer_queries_help(src: &str, expected: impl FnOnce(&str), options: InferOptions) { - let InferredProgram { - program, - inferred_queries, - } = infer_queries(src, options).unwrap(); - - let mut output_parts = Vec::with_capacity(inferred_queries.len() + 2); - - if options.print_can_decls { - use roc_can::debug::{pretty_print_declarations, PPCtx}; - let ctx = PPCtx { - home: program.home, - interns: &program.interns, - print_lambda_names: true, - }; - let pretty_decls = pretty_print_declarations(&ctx, &program.declarations); - output_parts.push(pretty_decls); - output_parts.push("\n".to_owned()); - } - - for InferredQuery { elaboration, .. } in inferred_queries { - let output_part = match elaboration { - Elaboration::Specialization { - specialized_name, - typ, - } => format!("{specialized_name} : {typ}"), - Elaboration::Source { source, typ } => format!("{source} : {typ}"), - Elaboration::Instantiation { .. } => panic!("Use uitest instead"), - }; - output_parts.push(output_part); - } - - let pretty_output = output_parts.join("\n"); - - expected(&pretty_output); -} diff --git a/crates/compiler/uitest/Cargo.toml b/crates/compiler/uitest/Cargo.toml index 52386d0f0b..11527de2e2 100644 --- a/crates/compiler/uitest/Cargo.toml +++ b/crates/compiler/uitest/Cargo.toml @@ -14,8 +14,12 @@ harness = false [dev-dependencies] roc_builtins = { path = "../builtins" } +roc_collections = { path = "../collections" } roc_derive = { path = "../derive", features = ["debug-derived-symbols"] } roc_load = { path = "../load" } +roc_packaging = { path = "../../packaging" } +roc_module = { path = "../module", features = ["debug-symbols"] } +roc_mono = { path = "../mono" } roc_parse = { path = "../parse" } roc_problem = { path = "../problem" } roc_reporting = { path = "../../reporting" } diff --git a/crates/compiler/uitest/src/mono.rs b/crates/compiler/uitest/src/mono.rs new file mode 100644 index 0000000000..8fb7751255 --- /dev/null +++ b/crates/compiler/uitest/src/mono.rs @@ -0,0 +1,135 @@ +use std::io; + +use bumpalo::Bump; +use roc_collections::MutMap; +use roc_load::{ExecutionMode, LoadConfig, LoadMonomorphizedError, Threading}; +use roc_module::symbol::{Interns, Symbol}; +use roc_mono::{ + ir::{Proc, ProcLayout}, + layout::STLayoutInterner, +}; +use tempfile::tempdir; + +#[derive(Default)] +pub struct MonoOptions { + pub no_check: bool, +} + +pub fn write_compiled_ir<'a>( + writer: &mut impl io::Write, + test_module: &str, + dependencies: impl IntoIterator, + options: MonoOptions, +) -> io::Result<()> { + use roc_packaging::cache::RocCacheDir; + use std::path::PathBuf; + + let exec_mode = ExecutionMode::Executable; + + let arena = &Bump::new(); + + let dir = tempdir()?; + + for (file, source) in dependencies { + std::fs::write(dir.path().join(format!("{file}.roc")), source)?; + } + + let filename = PathBuf::from("Test.roc"); + let file_path = dir.path().join(filename); + + let load_config = LoadConfig { + target_info: roc_target::TargetInfo::default_x86_64(), + threading: Threading::Single, + render: roc_reporting::report::RenderTarget::Generic, + palette: roc_reporting::report::DEFAULT_PALETTE, + exec_mode, + }; + let loaded = roc_load::load_and_monomorphize_from_str( + arena, + file_path, + test_module, + dir.path().to_path_buf(), + RocCacheDir::Disallowed, + load_config, + ); + + let loaded = match loaded { + Ok(x) => x, + Err(LoadMonomorphizedError::LoadingProblem(roc_load::LoadingProblem::FormattedReport( + report, + ))) => { + println!("{}", report); + panic!(); + } + Err(e) => panic!("{:?}", e), + }; + + use roc_load::MonomorphizedModule; + let MonomorphizedModule { + procedures, + exposed_to_host, + mut layout_interner, + interns, + .. + } = loaded; + + let main_fn_symbol = exposed_to_host.top_level_values.keys().copied().next(); + + if !options.no_check { + check_procedures(arena, &interns, &mut layout_interner, &procedures); + } + + write_procedures(writer, layout_interner, procedures, main_fn_symbol) +} + +fn check_procedures<'a>( + arena: &'a Bump, + interns: &Interns, + interner: &mut STLayoutInterner<'a>, + procedures: &MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, +) { + use roc_mono::debug::{check_procs, format_problems}; + let problems = check_procs(arena, interner, procedures); + if problems.is_empty() { + return; + } + let formatted = format_problems(interns, interner, problems); + panic!("IR problems found:\n{formatted}"); +} + +fn write_procedures<'a>( + writer: &mut impl io::Write, + interner: STLayoutInterner<'a>, + procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, + opt_main_fn_symbol: Option, +) -> io::Result<()> { + let mut procs_strings = procedures + .values() + .map(|proc| proc.to_pretty(&interner, 200, false)) + .collect::>(); + + let opt_main_fn = opt_main_fn_symbol.map(|main_fn_symbol| { + let index = procedures + .keys() + .position(|(s, _)| *s == main_fn_symbol) + .unwrap(); + procs_strings.swap_remove(index) + }); + + procs_strings.sort(); + + if let Some(main_fn) = opt_main_fn { + procs_strings.push(main_fn); + } + + let mut procs = procs_strings.iter().peekable(); + while let Some(proc) = procs.next() { + if procs.peek().is_some() { + writeln!(writer, "{}", proc)?; + } else { + write!(writer, "{}", proc)?; + } + } + + Ok(()) +} diff --git a/crates/compiler/uitest/src/uitest.rs b/crates/compiler/uitest/src/uitest.rs index 7e75e435f1..01c75487b3 100644 --- a/crates/compiler/uitest/src/uitest.rs +++ b/crates/compiler/uitest/src/uitest.rs @@ -8,11 +8,15 @@ use std::{ use lazy_static::lazy_static; use libtest_mimic::{run, Arguments, Failed, Trial}; +use mono::MonoOptions; use regex::Regex; +use roc_collections::VecMap; use test_solve_helpers::{ infer_queries, Elaboration, InferOptions, InferredProgram, InferredQuery, MUTLILINE_MARKER, }; +mod mono; + fn main() -> Result<(), Box> { let args = Arguments::from_args(); @@ -36,9 +40,17 @@ lazy_static! { static ref RE_OPT_INFER: Regex = Regex::new(r#"# \+opt infer:(?P.*)"#).unwrap(); - /// # +opt print: - static ref RE_OPT_PRINT: Regex = - Regex::new(r#"# \+opt print:(?P.*)"#).unwrap(); + /// # +opt mono: + static ref RE_OPT_MONO: Regex = + Regex::new(r#"# \+opt mono:(?P.*)"#).unwrap(); + + /// # +emit: + static ref RE_EMIT: Regex = + Regex::new(r#"# \+emit:(?P.*)"#).unwrap(); + + /// ## module + static ref RE_MODULE: Regex = + Regex::new(r#"## module (?P.*)"#).unwrap(); } fn collect_uitest_files() -> io::Result> { @@ -79,11 +91,19 @@ fn run_test(path: PathBuf) -> Result<(), Failed> { let data = std::fs::read_to_string(&path)?; let TestCase { infer_options, - print_options, - source, - } = TestCase::parse(data)?; + emit_options, + mono_options, + program, + } = TestCase::parse(&data)?; - let inferred_program = infer_queries(&source, infer_options)?; + let inferred_program = infer_queries( + program.test_module, + program + .other_modules + .iter() + .map(|(md, src)| (&**md, &**src)), + infer_options, + )?; { let mut fd = fs::OpenOptions::new() @@ -91,7 +111,13 @@ fn run_test(path: PathBuf) -> Result<(), Failed> { .truncate(true) .open(&path)?; - assemble_query_output(&mut fd, &source, inferred_program, print_options)?; + assemble_query_output( + &mut fd, + program, + inferred_program, + mono_options, + emit_options, + )?; } check_for_changes(&path)?; @@ -101,32 +127,93 @@ fn run_test(path: PathBuf) -> Result<(), Failed> { const EMIT_HEADER: &str = "# -emit:"; -struct TestCase { +struct Modules<'a> { + before_any: &'a str, + other_modules: VecMap<&'a str, &'a str>, + test_module: &'a str, +} + +struct TestCase<'a> { infer_options: InferOptions, - print_options: PrintOptions, - source: String, + mono_options: MonoOptions, + emit_options: EmitOptions, + program: Modules<'a>, } #[derive(Default)] -struct PrintOptions { +struct EmitOptions { can_decls: bool, + mono: bool, } -impl TestCase { - fn parse(mut data: String) -> Result { +impl<'a> TestCase<'a> { + fn parse(mut data: &'a str) -> Result { // Drop anything following `# -emit:` header lines; that's the output. if let Some(drop_at) = data.find(EMIT_HEADER) { - data.truncate(drop_at); - data.truncate(data.trim_end().len()); + data = data[..drop_at].trim_end(); } + let infer_options = Self::parse_infer_options(data)?; + let mono_options = Self::parse_mono_options(data)?; + let emit_options = Self::parse_emit_options(data)?; + + let program = Self::parse_modules(data); + Ok(TestCase { - infer_options: Self::parse_infer_options(&data)?, - print_options: Self::parse_print_options(&data)?, - source: data, + infer_options, + mono_options, + emit_options, + program, }) } + fn parse_modules(data: &'a str) -> Modules<'a> { + let mut module_starts = RE_MODULE.captures_iter(data).peekable(); + + let first_module_start = match module_starts.peek() { + None => { + // This is just a single module with no name; it is the test module. + return Modules { + before_any: Default::default(), + other_modules: Default::default(), + test_module: data, + }; + } + Some(p) => p.get(0).unwrap().start(), + }; + + let before_any = data[..first_module_start].trim(); + + let mut test_module = None; + let mut other_modules = VecMap::new(); + + while let Some(module_start) = module_starts.next() { + let module_name = module_start.name("name").unwrap().as_str(); + let module_start = module_start.get(0).unwrap().end(); + let module = match module_starts.peek() { + None => &data[module_start..], + Some(next_module_start) => { + let module_end = next_module_start.get(0).unwrap().start(); + &data[module_start..module_end] + } + } + .trim(); + + if module_name == "Test" { + test_module = Some(module); + } else { + other_modules.insert(module_name, module); + } + } + + let test_module = test_module.expect("no Test module found"); + Modules { + before_any, + other_modules, + test_module, + } + } + fn parse_infer_options(data: &str) -> Result { let mut infer_opts = InferOptions { no_promote: true, @@ -146,19 +233,35 @@ impl TestCase { Ok(infer_opts) } - fn parse_print_options(data: &str) -> Result { - let mut print_opts = PrintOptions::default(); + fn parse_mono_options(data: &str) -> Result { + let mut mono_opts = MonoOptions::default(); - let found_infer_opts = RE_OPT_PRINT.captures_iter(data); + let found_infer_opts = RE_OPT_MONO.captures_iter(data); for infer_opt in found_infer_opts { let opt = infer_opt.name("opt").unwrap().as_str(); match opt.trim() { - "can_decls" => print_opts.can_decls = true, - other => return Err(format!("unknown print option: {other:?}").into()), + "no_check" => mono_opts.no_check = true, + other => return Err(format!("unknown mono option: {other:?}").into()), } } - Ok(print_opts) + Ok(mono_opts) + } + + fn parse_emit_options(data: &str) -> Result { + let mut emit_opts = EmitOptions::default(); + + let found_infer_opts = RE_EMIT.captures_iter(data); + for infer_opt in found_infer_opts { + let opt = infer_opt.name("opt").unwrap().as_str(); + match opt.trim() { + "can_decls" => emit_opts.can_decls = true, + "mono" => emit_opts.mono = true, + other => return Err(format!("unknown emit option: {other:?}").into()), + } + } + + Ok(emit_opts) } } @@ -184,25 +287,53 @@ fn check_for_changes(path: &Path) -> Result<(), Failed> { /// Assemble the output for a test, with queries elaborated in-line. fn assemble_query_output( writer: &mut impl io::Write, - source: &str, + program: Modules<'_>, inferred_program: InferredProgram, - print_options: PrintOptions, + mono_options: MonoOptions, + emit_options: EmitOptions, ) -> io::Result<()> { + let Modules { + before_any, + other_modules, + test_module, + } = program; + + if !before_any.is_empty() { + writeln!(writer, "{before_any}\n")?; + } + + for (module, source) in other_modules.iter() { + writeln!(writer, "## module {module}")?; + writeln!(writer, "{}\n", source)?; + } + + if !other_modules.is_empty() { + writeln!(writer, "## module Test")?; + } + // Reverse the queries so that we can pop them off the end as we pass through the lines. let (queries, program) = inferred_program.decompose(); let mut sorted_queries = queries.into_sorted(); sorted_queries.reverse(); let mut reflow = Reflow::new_unindented(writer); - write_source_with_answers(&mut reflow, source, sorted_queries, 0)?; + write_source_with_answers(&mut reflow, test_module, sorted_queries, 0)?; - // Finish up with any remaining print options we were asked to provide. - let PrintOptions { can_decls } = print_options; + // Finish up with any remaining emit options we were asked to provide. + let EmitOptions { can_decls, mono } = emit_options; if can_decls { writeln!(writer, "\n{EMIT_HEADER}can_decls")?; program.write_can_decls(writer)?; } + if mono { + writeln!(writer, "\n{EMIT_HEADER}mono")?; + // Unfortunately, with the current setup we must now recompile into the IR. + // TODO: extend the data returned by a monomorphized module to include + // that of a solved module. + mono::write_compiled_ir(writer, test_module, other_modules, mono_options)?; + } + Ok(()) } diff --git a/crates/compiler/uitest/tests/lambda_set/disjoint_nested_lambdas_result_in_disjoint_parents_issue_4712.txt b/crates/compiler/uitest/tests/lambda_set/disjoint_nested_lambdas_result_in_disjoint_parents_issue_4712.txt index 1165451017..55a9402db2 100644 --- a/crates/compiler/uitest/tests/lambda_set/disjoint_nested_lambdas_result_in_disjoint_parents_issue_4712.txt +++ b/crates/compiler/uitest/tests/lambda_set/disjoint_nested_lambdas_result_in_disjoint_parents_issue_4712.txt @@ -1,5 +1,5 @@ -# +opt print:can_decls # +opt infer:print_only_under_alias +# +emit:can_decls app "test" provides [main] to "./platform" Parser a : {} -> a diff --git a/crates/compiler/uitest/tests/specialize/record_update_between_modules.txt b/crates/compiler/uitest/tests/specialize/record_update_between_modules.txt new file mode 100644 index 0000000000..f12c711389 --- /dev/null +++ b/crates/compiler/uitest/tests/specialize/record_update_between_modules.txt @@ -0,0 +1,32 @@ +# +emit:mono +# +opt mono:no_check + +## module Dep +interface Dep exposes [defaultRequest] imports [] + +defaultRequest = { + url: "", + body: "", +} + +## module Test +app "test" imports [Dep.{ defaultRequest }] provides [main] to "./platform" + +main = + { defaultRequest & url: "http://www.example.com" } + +# -emit:mono +procedure Dep.0 (): + let Dep.2 : Str = ""; + let Dep.3 : Str = ""; + let Dep.1 : {Str, Str} = Struct {Dep.2, Dep.3}; + ret Dep.1; + +procedure Test.0 (): + let Test.3 : Str = "http://www.example.com"; + let Test.4 : {Str, Str} = CallByName Dep.0; + let Test.2 : Str = StructAtIndex 0 Test.4; + inc Test.2; + dec Test.4; + let Test.1 : {Str, Str} = Struct {Test.2, Test.3}; + ret Test.1;