Merge pull request #5284 from roc-lang/i5131

Handle record updates of imported module thunks
This commit is contained in:
Ayaz 2023-04-12 17:08:01 -05:00 committed by GitHub
commit 526cd25611
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 359 additions and 83 deletions

View file

@ -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,

View file

@ -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();

View file

@ -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<Item = (&'a str, &'a str)>,
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<InferredProgram, Box<dyn Error>> {
pub fn infer_queries<'a>(
src: &str,
dependencies: impl IntoIterator<Item = (&'a str, &'a str)>,
options: InferOptions,
) -> Result<InferredProgram, Box<dyn Error>> {
let (
LoadedModule {
module_id: home,
@ -351,7 +361,7 @@ pub fn infer_queries(src: &str, options: InferOptions) -> Result<InferredProgram
..
},
src,
) = run_load_and_infer(src, options.no_promote)?;
) = run_load_and_infer(src, dependencies, options.no_promote)?;
let declarations = declarations_by_id.remove(&home).unwrap();
let subs = solved.inner_mut();
@ -373,9 +383,6 @@ pub fn infer_queries(src: &str, options: InferOptions) -> Result<InferredProgram
let line_info = LineInfo::new(&src);
let queries = parse_queries(&src, &line_info);
if queries.is_empty() {
return Err("No queries provided!".into());
}
let mut inferred_queries = Vec::with_capacity(queries.len());
let exposed_by_module = ExposedByModule::default();
@ -561,40 +568,3 @@ impl<'a> 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);
}

View file

@ -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" }

View file

@ -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<Item = (&'a str, &'a str)>,
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<Symbol>,
) -> io::Result<()> {
let mut procs_strings = procedures
.values()
.map(|proc| proc.to_pretty(&interner, 200, false))
.collect::<Vec<_>>();
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(())
}

View file

@ -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<dyn Error>> {
let args = Arguments::from_args();
@ -36,9 +40,17 @@ lazy_static! {
static ref RE_OPT_INFER: Regex =
Regex::new(r#"# \+opt infer:(?P<opt>.*)"#).unwrap();
/// # +opt print:<opt>
static ref RE_OPT_PRINT: Regex =
Regex::new(r#"# \+opt print:(?P<opt>.*)"#).unwrap();
/// # +opt mono:<opt>
static ref RE_OPT_MONO: Regex =
Regex::new(r#"# \+opt mono:(?P<opt>.*)"#).unwrap();
/// # +emit:<opt>
static ref RE_EMIT: Regex =
Regex::new(r#"# \+emit:(?P<opt>.*)"#).unwrap();
/// ## module <name>
static ref RE_MODULE: Regex =
Regex::new(r#"## module (?P<name>.*)"#).unwrap();
}
fn collect_uitest_files() -> io::Result<Vec<PathBuf>> {
@ -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<Self, Failed> {
impl<'a> TestCase<'a> {
fn parse(mut data: &'a str) -> Result<Self, Failed> {
// 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<InferOptions, Failed> {
let mut infer_opts = InferOptions {
no_promote: true,
@ -146,19 +233,35 @@ impl TestCase {
Ok(infer_opts)
}
fn parse_print_options(data: &str) -> Result<PrintOptions, Failed> {
let mut print_opts = PrintOptions::default();
fn parse_mono_options(data: &str) -> Result<MonoOptions, Failed> {
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<EmitOptions, Failed> {
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(())
}

View file

@ -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

View file

@ -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;