diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index 26777843..c286b6ec 100644 --- a/TEST-NEEDS.adoc +++ b/TEST-NEEDS.adoc @@ -21,9 +21,9 @@ compliance, 16 tests) performance, correctness, 13 tests) * Benchmarks: `+src/ephapax-parser/benches/parse_bench.rs+`, `+src/ephapax-vram-cache/benches/cache_bench.rs+` -* Total: *600 tests* (`+cargo test --workspace --all-targets+`); +* Total: *605 tests* (`+cargo test --workspace --all-targets+`); pass/fail enforced by `+rust-ci.yml+` -* Documented all-target tests: 600 +* Documented all-target tests: 605 === CRG Testing Taxonomy — Status @@ -83,7 +83,7 @@ eliminator-fork invariant + its gated capstone, per PROOF-NEEDS.adoc §4) ==== Build & Execution * [x] cargo build — compiles -* [x] cargo test –workspace –all-targets — 600 pass, 0 fail +* [x] cargo test –workspace –all-targets — 605 pass, 0 fail * [x] idris2/ephapax-parse-tests.ipkg — compiles AND runs green (2026-07-07, idris2 0.8.0 + zig-built libephapax_tokbuf.so); CI-gated compile-only by abi-verify.yml `+idris2-parse-front+` (runtime run stays diff --git a/src/ephapax-cli/src/main.rs b/src/ephapax-cli/src/main.rs index 14a1983f..2cb55fa4 100644 --- a/src/ephapax-cli/src/main.rs +++ b/src/ephapax-cli/src/main.rs @@ -332,7 +332,7 @@ fn run_file( } if name == "main" => Some((params.len(), ret_ty.clone())), _ => None, }); - let module = program.into_merged(); + let module = program.into_merged().map_err(|e| report_load_error(&e))?; let Some((n_params, ret_ty)) = main else { if verbose { println!("{} Module loaded (no main function)", "✓".green()); @@ -516,7 +516,7 @@ fn compile_file( } finish_compile( - program.into_merged(), + program.into_merged().map_err(|e| report_load_error(&e))?, &filename, path, output, diff --git a/src/ephapax-cli/tests/generics.rs b/src/ephapax-cli/tests/generics.rs new file mode 100644 index 00000000..e0cbfbcb --- /dev/null +++ b/src/ephapax-cli/tests/generics.rs @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: MPL-2.0 +// Owner: Jonathan D.A. Jewell +// +// generics.rs, v2 plan rung E7 gate (generic functions). +// +// A generic function is checked one time, with each type parameter as an +// opaque linear type. The program then gets one monomorphic copy of the +// function for each set of type arguments it is used with, and the +// generic definition is removed: codegen and the interpreter see +// monomorphic functions only. + +use std::path::Path; +use std::process::{Command, Output}; + +use ephapax_syntax::Decl; + +fn ephapax(dir: &Path, args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_ephapax")) + .current_dir(dir) + .args(args) + .output() + .expect("spawn ephapax") +} + +fn write(dir: &Path, name: &str, text: &str) { + std::fs::write(dir.join(name), text).expect("write program"); +} + +/// `id` used at three types, with two inputs for each: every copy +/// returns its input, as wasm and in the interpreter. +#[test] +fn a_generic_function_runs_at_each_type_it_is_used_with() { + let tmp = tempfile::tempdir().expect("tempdir"); + for (ty, input, want) in [ + ("I32", "3", "3"), + ("I32", "41", "41"), + ("I64", "5000000000", "5000000000i64"), + ("I64", "6000000000", "6000000000i64"), + ("F64", "2.5", "2.5"), + ("F64", "0.25", "0.25"), + ] { + write( + tmp.path(), + "g.eph", + &format!("module g\n\nfn id(x: a): a = x\n\nfn main(): {ty} = id({input})\n"), + ); + for args in [&["run", "g.eph"][..], &["run", "--interp", "g.eph"]] { + let out = ephapax(tmp.path(), args); + assert_eq!( + String::from_utf8_lossy(&out.stdout).trim(), + want, + "id({input}) at {ty} under {args:?}:\n{}", + String::from_utf8_lossy(&out.stderr) + ); + } + } + + // An upper-case type parameter, also in an annotation of the body. + write( + tmp.path(), + "upper.eph", + "module upper\n\nfn apply(x: T): T =\n let f = fn(y: T) -> y\n f(x)\n\n\ + fn main(): I32 = apply(3)\n", + ); + for args in [&["run", "upper.eph"][..], &["run", "--interp", "upper.eph"]] { + let out = ephapax(tmp.path(), args); + assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "3", "{args:?}"); + } + + // A generic function of another module, and one that calls another + // generic function. + write( + tmp.path(), + "lib.eph", + "module lib\n\npub fn id(x: a): a = x\n\npub fn twice(x: a): a = id(id(x))\n", + ); + write( + tmp.path(), + "app.eph", + "module app\n\nimport lib\n\nfn main(): F64 = twice(1.5)\n", + ); + for args in [&["run", "app.eph"][..], &["run", "--interp", "app.eph"]] { + let out = ephapax(tmp.path(), args); + assert_eq!( + String::from_utf8_lossy(&out.stdout).trim(), + "1.5", + "{args:?}" + ); + } +} + +/// The merged program has one monomorphic function for each set of type +/// arguments, and no generic function. +#[test] +fn a_program_has_one_copy_for_each_set_of_type_arguments() { + let tmp = tempfile::tempdir().expect("tempdir"); + write( + tmp.path(), + "two.eph", + "module two\n\nfn id(x: a): a = x\n\nfn main(): I64 =\n let n = id(7)\n id(6000000000)\n", + ); + let merged = ephapax_driver::load_program(&tmp.path().join("two.eph"), None) + .expect("loads") + .into_merged() + .expect("merges"); + let mut functions = Vec::new(); + for decl in &merged.decls { + if let Decl::Fn { + name, type_params, .. + } = decl + { + assert!(type_params.is_empty(), "`{name}` is still generic"); + functions.push(name.as_str()); + } + } + assert_eq!(functions, ["main", "id", "id"]); +} + +/// What the checker refuses in a generic program, each with a named +/// `TypeError`, on `check` and on `compile`. +#[test] +fn generic_programs_the_checker_refuses() { + let tmp = tempfile::tempdir().expect("tempdir"); + // (program after the module header, text of the error) + for (program, error) in [ + // A value of a type-parameter type can be linear in a use of the + // function: no second use, and no discard. + ( + "fn dup(x: a): (a, a) = (x, x)\n\nfn main(): I32 = fst(dup(3))\n", + "Linear variable `x` used more than once", + ), + ( + "fn k(x: a): I32 = 0\n\nfn main(): I32 = k(3)\n", + "Linear variable `x` not consumed", + ), + // Each call needs its own copy of `f`: copies without end. + ( + "fn f(x: a): I32 = f((x, 1))\n\nfn main(): I32 = f(3)\n", + "Polymorphic recursion", + ), + // Nothing in the call fixes `a`. + ( + "fn zero(): I32 = 0\n\nfn main(): I32 = zero()\n", + "Cannot infer a type argument of the generic function `zero`", + ), + // No call gives the entry point its type arguments. + ( + "fn main(): I32 = 0\n", + "`main` is the entry point of a program and cannot have type parameters", + ), + ] { + write(tmp.path(), "bad.eph", &format!("module bad\n\n{program}")); + for args in [&["check", "bad.eph"][..], &["compile", "bad.eph"]] { + let out = ephapax(tmp.path(), args); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + !out.status.success() && stderr.contains("Type error") && stderr.contains(error), + "{args:?} on:\n{program}\nexit {:?}, stderr:\n{stderr}", + out.status.code() + ); + } + } +} + +/// A limit of the typed lowering that only a copy at a wide type shows +/// (here a lambda with an F64 parameter) is a load error on `check` too. +#[test] +fn check_sees_the_limits_of_the_monomorphic_copies() { + let tmp = tempfile::tempdir().expect("tempdir"); + let program = |ty: &str, input: &str| { + format!( + "module lam\n\nfn apply(x: a): a =\n let f = fn(y: a) -> y\n f(x)\n\n\ + fn main(): {ty} = apply({input})\n" + ) + }; + write(tmp.path(), "lam.eph", &program("I32", "3")); + assert!(ephapax(tmp.path(), &["check", "lam.eph"]).status.success()); + + write(tmp.path(), "lam.eph", &program("F64", "2.5")); + for args in [&["check", "lam.eph"][..], &["compile", "lam.eph"]] { + let out = ephapax(tmp.path(), args); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + !out.status.success() && stderr.contains("Codegen error") && stderr.contains("lambda"), + "{args:?}: exit {:?}, stderr:\n{stderr}", + out.status.code() + ); + } +} diff --git a/src/ephapax-cli/tests/typed_codegen_matrix.rs b/src/ephapax-cli/tests/typed_codegen_matrix.rs index fcc0231d..c82dfd4c 100644 --- a/src/ephapax-cli/tests/typed_codegen_matrix.rs +++ b/src/ephapax-cli/tests/typed_codegen_matrix.rs @@ -275,6 +275,7 @@ fn load(src: &str) -> ephapax_syntax::Module { ephapax_driver::load_source(src, "matrix", None, stdlib.as_deref()) .unwrap_or_else(|e| panic!("the matrix module must load: {e}")) .into_merged() + .expect("the matrix module merges") } fn linker(engine: &Engine) -> Linker<()> { diff --git a/src/ephapax-desugar/src/lib.rs b/src/ephapax-desugar/src/lib.rs index 05deaf09..f4700277 100644 --- a/src/ephapax-desugar/src/lib.rs +++ b/src/ephapax-desugar/src/lib.rs @@ -258,6 +258,10 @@ pub struct Desugarer { /// Named types currently being expanded by `desugar_named_type`, /// innermost last. Re-entering one is a recursive type. expanding: RefCell>, + /// The type parameters of the function being desugared. In its + /// signature and body such a name is a type variable, also when it + /// starts with an upper-case letter (`fn id(x: T): T`). + fn_type_params: Vec, } impl Desugarer { @@ -267,6 +271,7 @@ impl Desugarer { registry: DataRegistry::new(), module_qualifiers: HashSet::new(), expanding: RefCell::new(Vec::new()), + fn_type_params: Vec::new(), } } @@ -276,6 +281,7 @@ impl Desugarer { registry, module_qualifiers: HashSet::new(), expanding: RefCell::new(Vec::new()), + fn_type_params: Vec::new(), } } @@ -320,20 +326,23 @@ impl Desugarer { SurfaceDecl::Fn { name, visibility, + type_params, params, ret_ty, body, } => { + self.fn_type_params.clone_from(type_params); let core_params: Vec<_> = params .iter() .map(|(n, t)| Ok((n.clone(), self.desugar_ty(t)?))) .collect::>()?; let core_ret = self.desugar_ty(ret_ty)?; let core_body = self.desugar_expr(body)?; + self.fn_type_params.clear(); core_decls.push(Decl::Fn { name: name.clone(), visibility: lower_visibility(*visibility), - type_params: vec![], + type_params: type_params.clone(), params: core_params, ret_ty: core_ret, body: core_body, @@ -684,6 +693,18 @@ impl Desugarer { /// codegen). Two distinct extern types `Window` and `IpcChannel` /// produce two distinct `Ty::Var` rigids that never unify. fn desugar_named_type(&self, name: &SmolStr, args: &[SurfaceTy]) -> Result { + // A type parameter of the enclosing function shadows every other + // meaning of the name. + if self.fn_type_params.contains(name) { + if !args.is_empty() { + return Err(DesugarError::TypeArityMismatch { + name: name.to_string(), + expected: 0, + got: args.len(), + }); + } + return Ok(Ty::Var(name.clone())); + } // Built-in type aliases that aren't (yet) keywords in the lexer. // `Unit` is the type-position spelling of the literal `()`; // bridge.eph and other ML-adjacent corpora write it freely. @@ -1795,6 +1816,7 @@ mod tests { SurfaceDecl::Fn { name: "unwrap_or".into(), visibility: SurfaceVisibility::Private, + type_params: vec![], params: vec![ ( "opt".into(), @@ -1879,6 +1901,7 @@ mod tests { SurfaceDecl::Fn { name: "test".into(), visibility: SurfaceVisibility::Private, + type_params: vec![], params: vec![( "opt".into(), SurfaceTy::Named { @@ -1944,6 +1967,7 @@ mod tests { SurfaceDecl::Fn { name: "test".into(), visibility: SurfaceVisibility::Private, + type_params: vec![], params: vec![( "opt".into(), SurfaceTy::Named { diff --git a/src/ephapax-driver/src/lib.rs b/src/ephapax-driver/src/lib.rs index ce9c768e..fa9d00e1 100644 --- a/src/ephapax-driver/src/lib.rs +++ b/src/ephapax-driver/src/lib.rs @@ -55,14 +55,35 @@ impl Program { .expect("a program has at least its root module") } - /// Merge every imported module into the root, for codegen and the - /// interpreter. Wasm produces a single binary; each imported module's - /// `Fn` declarations become part of the same wasm module. When two - /// modules declare a function of the same name, one of them gets a - /// module-qualified name (`deep::helper`) and its callers follow it; - /// the root keeps its names. An extern item that an earlier module - /// already declared is dropped (first-seen wins, the root first). - pub fn into_merged(self) -> Module { + /// The whole program as one monomorphic module, for codegen and the + /// interpreter: [`Self::merge`], then one copy of each generic + /// function for each set of type arguments it is used with (v2 plan + /// E7). The error is a type error of the merged program; a program + /// that passed [`check_program`] has none. + pub fn into_merged(self) -> Result { + let root = self.root(); + let (module, path, source_text) = ( + root.core.name.to_string(), + root.file_path.clone(), + root.source.clone(), + ); + ephapax_typing::monomorphise(&self.merge()).map_err(|error| DriverError::Type { + module, + path, + source_text, + error: Box::new(error), + }) + } + + /// Merge every imported module into the root. Wasm produces a single + /// binary; each imported module's `Fn` declarations become part of + /// the same wasm module. When two modules declare a function of the + /// same name, one of them gets a module-qualified name + /// (`deep::helper`) and its callers follow it; the root keeps its + /// names. An extern item that an earlier module already declared is + /// dropped (first-seen wins, the root first). A generic function is + /// still generic: [`Self::into_merged`] is the form for a back end. + pub fn merge(self) -> Module { // Dedup keys: `fn:` for a function, `extern::` for // an extern item (each extern fn is one wasm import, so the same // item from two modules must be emitted once). Extern blocks are @@ -295,6 +316,33 @@ pub fn check_program(program: &Program) -> Result<(), DriverError> { } })?; } + // A generic function is compiled as its monomorphic copies, so the + // copies get the same check: a copy at a wide type can meet a limit + // that the generic body does not show. + let generic = + |decl: &Decl| matches!(decl, Decl::Fn { type_params, .. } if !type_params.is_empty()); + if program + .modules + .iter() + .any(|m| m.core.decls.iter().any(generic)) + { + let root = program.root(); + let merged = program.clone().into_merged()?; + let types = module_expr_types(&merged, &mut ModuleRegistry::new()).map_err(|error| { + DriverError::Type { + module: root.core.name.to_string(), + path: root.file_path.clone(), + source_text: root.source.clone(), + error: Box::new(error), + } + })?; + ephapax_wasm::validate_supported_typed(&merged, &types).map_err(|error| { + DriverError::Unsupported { + path: root.file_path.clone(), + error, + } + })?; + } Ok(()) } @@ -354,7 +402,7 @@ mod tests { fn main(): I32 = only_app() + lib.only_lib() + shared()\n", ); let program = load_program(&dir.path().join("app.eph"), None).expect("loads"); - let merged = program.into_merged(); + let merged = program.into_merged().expect("merges"); // `shared` once (the root's), and each module's own item. assert_eq!(extern_fn_names(&merged), ["only_app", "only_lib", "shared"]); ephapax_wasm::compile_module(&merged).expect("the merged module compiles"); @@ -400,7 +448,7 @@ mod tests { write("deep.eph", &deep(deep_fn)); write("app.eph", &app("fn helper(n: I32): I32 = n + 7")); - let merged = load().expect("loads").into_merged(); + let merged = load().expect("loads").into_merged().expect("merges"); let names: Vec<&str> = merged .decls .iter() diff --git a/src/ephapax-driver/src/qualify.rs b/src/ephapax-driver/src/qualify.rs index 3bf2b419..677458f8 100644 --- a/src/ephapax-driver/src/qualify.rs +++ b/src/ephapax-driver/src/qualify.rs @@ -143,83 +143,58 @@ fn rename(name: &mut Var, scope: &Renames) { /// local `let helper = ...` is renamed together with its uses, so it /// shadows the renamed function as it shadowed the function before. fn rename_expr(expr: &mut Expr, scope: &Renames) { - // No `_` arm: a new expression form must say where its names are. - let children: Vec<&mut Expr> = match &mut expr.kind { - ExprKind::Lit(_) | ExprKind::StringNew { .. } => vec![], - ExprKind::Var(name) => { - rename(name, scope); - vec![] - } - ExprKind::StringConcat { left, right } - | ExprKind::Pair { left, right } - | ExprKind::BinOp { left, right, .. } => vec![left, right], - ExprKind::App { func, arg } => vec![func, arg], - ExprKind::ListIndex { list, index } => vec![list, index], - ExprKind::StringLen(e) - | ExprKind::Fst(e) - | ExprKind::Snd(e) - | ExprKind::Deref(e) - | ExprKind::Drop(e) - | ExprKind::Copy(e) => vec![e], - ExprKind::Inl { value: e, .. } - | ExprKind::Inr { value: e, .. } - | ExprKind::Region { body: e, .. } - | ExprKind::Borrow { inner: e, .. } - | ExprKind::UnaryOp { operand: e, .. } - | ExprKind::TupleIndex { tuple: e, .. } => vec![e], - ExprKind::Let { - name, value, body, .. - } - | ExprKind::LetLin { - name, value, body, .. - } => { - rename(name, scope); - vec![value, body] - } - ExprKind::Lambda { param, body, .. } => { - rename(param, scope); - vec![body] - } + // The names of this node: a use or a binder. No `_` arm: a new + // expression form must say if it has a name. + match &mut expr.kind { + ExprKind::Var(name) + | ExprKind::Let { name, .. } + | ExprKind::LetLin { name, .. } + | ExprKind::Lambda { param: name, .. } => rename(name, scope), ExprKind::Case { - scrutinee, left_var, - left_body, right_var, - right_body, + .. } => { rename(left_var, scope); rename(right_var, scope); - vec![scrutinee, left_body, right_body] } - ExprKind::Match { scrutinee, arms } => { - let mut children: Vec<&mut Expr> = vec![scrutinee]; + ExprKind::Match { arms, .. } => { for arm in arms { rename_pattern(&mut arm.pattern, scope); - children.extend(arm.guard.as_deref_mut()); - children.push(&mut arm.body); } - children } - ExprKind::If { - cond, - then_branch, - else_branch, - } => vec![cond, then_branch, else_branch], - ExprKind::Block(exprs) - | ExprKind::ListLit(exprs) - | ExprKind::TupleLit(exprs) - | ExprKind::FFI { args: exprs, .. } - | ExprKind::Perform { args: exprs, .. } => exprs.iter_mut().collect(), - ExprKind::Handle { body, clauses } => { - let mut children: Vec<&mut Expr> = vec![body]; + ExprKind::Handle { clauses, .. } => { for clause in clauses { clause.params.iter_mut().for_each(|p| rename(p, scope)); - children.push(&mut clause.body); } - children } - }; - for child in children { + ExprKind::Lit(_) + | ExprKind::StringNew { .. } + | ExprKind::StringConcat { .. } + | ExprKind::StringLen(_) + | ExprKind::App { .. } + | ExprKind::Pair { .. } + | ExprKind::Fst(_) + | ExprKind::Snd(_) + | ExprKind::Inl { .. } + | ExprKind::Inr { .. } + | ExprKind::If { .. } + | ExprKind::Region { .. } + | ExprKind::Borrow { .. } + | ExprKind::Deref(_) + | ExprKind::Drop(_) + | ExprKind::Copy(_) + | ExprKind::Block(_) + | ExprKind::FFI { .. } + | ExprKind::Perform { .. } + | ExprKind::BinOp { .. } + | ExprKind::UnaryOp { .. } + | ExprKind::ListLit(_) + | ExprKind::ListIndex { .. } + | ExprKind::TupleLit(_) + | ExprKind::TupleIndex { .. } => {} + } + for child in expr.children_mut() { rename_expr(child, scope); } } diff --git a/src/ephapax-parser/src/surface.rs b/src/ephapax-parser/src/surface.rs index f50ff931..53b81fc1 100644 --- a/src/ephapax-parser/src/surface.rs +++ b/src/ephapax-parser/src/surface.rs @@ -315,6 +315,7 @@ fn parse_fn_decl(pair: pest::iterators::Pair) -> Result = None; + let mut type_params = Vec::new(); let mut params = Vec::new(); let mut ret_ty = None; let mut body = None; @@ -327,6 +328,13 @@ fn parse_fn_decl(pair: pest::iterators::Pair) -> Result { name = Some(parse_identifier(item)); } + Rule::fn_type_params => { + type_params.extend( + item.into_inner() + .filter(|p| p.as_rule() == Rule::identifier) + .map(parse_identifier), + ); + } Rule::param_list => { for param in item.into_inner() { if param.as_rule() == Rule::param { @@ -360,6 +368,7 @@ fn parse_fn_decl(pair: pest::iterators::Pair) -> Result { - compile_module(&program.clone().into_merged()).map_err(|e| e.0) - } + (Ok(program), Ok(())) => program + .clone() + .into_merged() + .map_err(|e| format!("cannot compile: {e}")) + .and_then(|module| compile_module(&module).map_err(|e| e.0)), (Err(e), _) | (_, Err(e)) => Err(format!("cannot compile: {e}")), }; self.recomputes += 1; diff --git a/src/ephapax-repl/src/lib.rs b/src/ephapax-repl/src/lib.rs index 06a7cbc9..d1580f23 100644 --- a/src/ephapax-repl/src/lib.rs +++ b/src/ephapax-repl/src/lib.rs @@ -359,7 +359,9 @@ impl Repl { fn load_declarations(&mut self, source: &str) -> Result<(), DriverError> { let stdlib = ephapax_driver::stdlib_dir(None); let program = ephapax_driver::parse_source(source, "repl", None, stdlib.as_deref())?; - self.interpreter.load_module(&program.into_merged()); + // Not monomorphised (that needs a type check): the interpreter + // runs a generic function as it is. + self.interpreter.load_module(&program.merge()); Ok(()) } @@ -367,7 +369,10 @@ impl Repl { &mut self, program: Result, ) -> Result { - let module = program.map_err(ReplError::Load)?.into_merged(); + // The driver checked the program. It is loaded without + // monomorphisation, as a prompt declaration is: a generic function + // of the file stays callable from the prompt by its own name. + let module = program.map_err(ReplError::Load)?.merge(); self.interpreter.load_module(&module); Ok(module.decls.len()) } @@ -495,6 +500,20 @@ mod tests { assert!(matches!(result, Value::I32(42))); } + /// A generic function of a loaded file is callable from the prompt, + /// also when the file itself does not use it. + #[test] + fn loaded_generic_function_is_callable_from_the_prompt() { + let mut repl = Repl::new().expect("REPL should initialize"); + repl.load_module("fn id(x: a): a = x\n", "lib") + .expect("the module loads"); + let result = repl + .interpreter + .call("id", vec![Value::I32(7)]) + .expect("id runs"); + assert!(matches!(result, Value::I32(7))); + } + #[test] fn test_eval_if() { let mut repl = Repl::new().expect("REPL should initialize"); diff --git a/src/ephapax-surface/src/lib.rs b/src/ephapax-surface/src/lib.rs index f71e0d18..2f9aa8d6 100644 --- a/src/ephapax-surface/src/lib.rs +++ b/src/ephapax-surface/src/lib.rs @@ -414,6 +414,9 @@ pub enum SurfaceDecl { name: Var, #[serde(default, skip_serializing_if = "SurfaceVisibility::is_private")] visibility: SurfaceVisibility, + /// `fn id(...)`: the type parameters, in order. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + type_params: Vec, params: Vec<(Var, SurfaceTy)>, ret_ty: SurfaceTy, body: SurfaceExpr, diff --git a/src/ephapax-syntax/src/lib.rs b/src/ephapax-syntax/src/lib.rs index 02664260..2e5a4e39 100644 --- a/src/ephapax-syntax/src/lib.rs +++ b/src/ephapax-syntax/src/lib.rs @@ -659,6 +659,71 @@ impl Expr { pub fn dummy(kind: ExprKind) -> Self { Self::new(kind, Span::dummy()) } + + /// Give this node a new id. A copy of a tree that will get its own + /// entries in a table keyed by [`ExprId`] needs this on every node. + pub fn renew_id(&mut self) { + self.id = ExprId::fresh(); + } + + /// The direct sub-expressions, for a pass that rewrites a tree. + pub fn children_mut(&mut self) -> Vec<&mut Expr> { + // No `_` arm: a new expression form must list its children. + let children: Vec<&mut Expr> = match &mut self.kind { + ExprKind::Lit(_) | ExprKind::Var(_) | ExprKind::StringNew { .. } => vec![], + ExprKind::StringConcat { left, right } + | ExprKind::Pair { left, right } + | ExprKind::BinOp { left, right, .. } => vec![left, right], + ExprKind::App { func, arg } => vec![func, arg], + ExprKind::ListIndex { list, index } => vec![list, index], + ExprKind::StringLen(e) + | ExprKind::Fst(e) + | ExprKind::Snd(e) + | ExprKind::Deref(e) + | ExprKind::Drop(e) + | ExprKind::Copy(e) => vec![e], + ExprKind::Inl { value: e, .. } + | ExprKind::Inr { value: e, .. } + | ExprKind::Lambda { body: e, .. } + | ExprKind::Region { body: e, .. } + | ExprKind::Borrow { inner: e, .. } + | ExprKind::UnaryOp { operand: e, .. } + | ExprKind::TupleIndex { tuple: e, .. } => vec![e], + ExprKind::Let { value, body, .. } | ExprKind::LetLin { value, body, .. } => { + vec![value, body] + } + ExprKind::Case { + scrutinee, + left_body, + right_body, + .. + } => vec![scrutinee, left_body, right_body], + ExprKind::Match { scrutinee, arms } => { + let mut children: Vec<&mut Expr> = vec![scrutinee]; + for arm in arms { + children.extend(arm.guard.as_deref_mut()); + children.push(&mut arm.body); + } + children + } + ExprKind::If { + cond, + then_branch, + else_branch, + } => vec![cond, then_branch, else_branch], + ExprKind::Block(exprs) + | ExprKind::ListLit(exprs) + | ExprKind::TupleLit(exprs) + | ExprKind::FFI { args: exprs, .. } + | ExprKind::Perform { args: exprs, .. } => exprs.iter_mut().collect(), + ExprKind::Handle { body, clauses } => { + let mut children: Vec<&mut Expr> = vec![body]; + children.extend(clauses.iter_mut().map(|c| &mut c.body)); + children + } + }; + children + } } /// A single arm in an `ExprKind::Match` expression (added ephapax#61). diff --git a/src/ephapax-typing/src/lib.rs b/src/ephapax-typing/src/lib.rs index 80b58672..4b6dc6f8 100644 --- a/src/ephapax-typing/src/lib.rs +++ b/src/ephapax-typing/src/lib.rs @@ -18,6 +18,9 @@ //! See [`discipline`] module for the complete rules reference. pub mod discipline; +mod mono; + +pub use mono::monomorphise; use ephapax_syntax::{ BaseTy, BinOp, Decl, Expr, ExprId, ExprKind, ExternItem, Literal, MatchArm, Module, Pattern, @@ -124,6 +127,25 @@ pub enum TypeError { #[error("Parameter `{0}` is declared more than once in the same function")] DuplicateParameter(Var), + /// A call of a generic function leaves one of its type parameters + /// with no type (nothing in the call fixes it). + #[error("Cannot infer a type argument of the generic function `{0}` at this call")] + CannotInferTypeArgument(Var), + + /// A generic function reaches itself with other type arguments than + /// its own type parameters. Each generic function becomes one copy + /// for each set of type arguments, and this needs copies without end. + #[error( + "Polymorphic recursion: the generic function `{0}` reaches itself with \ + type arguments that are not its own type parameters" + )] + PolymorphicRecursion(Var), + + /// `main` is the entry point: no call gives it type arguments, so no + /// monomorphic copy of it would exist. + #[error("`main` is the entry point of a program and cannot have type parameters")] + GenericMain, + /// A projection (`t.i` or `xs[i]`) would consume the container and /// return one component, discarding unselected linear components. #[error( @@ -864,10 +886,33 @@ fn is_useful( /// instructions and layouts (v2 plan rung E5). pub type ExprTypes = HashMap; +/// For each use of a generic function (the [`ExprId`] of the variable +/// node), its type arguments in the order of the type parameters. Inside +/// a generic function they can name its own type parameters. +pub type TypeArgs = HashMap>; + +/// A use of a generic function in the function body being checked. +#[derive(Clone)] +struct Instantiation { + id: ExprId, + span: Span, + callee: Var, + /// Fresh unification variables until the body is checked. + args: Vec, +} + +#[derive(Clone)] pub struct TypeChecker { ctx: Context, /// Type of each checked expression; see [`TypeChecker::expr_types`]. expr_types: ExprTypes, + /// Uses of generic functions in the body being checked. + instantiations: Vec, + /// The resolved type arguments of every use; see [`TypeArgs`]. + type_args: TypeArgs, + /// (generic caller, generic callee, type arguments, span of the call): + /// the input of the polymorphic-recursion check. + generic_calls: Vec<(Var, Var, Vec, Span)>, /// Next fresh unification variable ID. next_unif: u32, wildcard_obligations: Vec<(Span, Ty)>, @@ -885,6 +930,9 @@ impl TypeChecker { Self { ctx: Context::new(), expr_types: ExprTypes::new(), + instantiations: Vec::new(), + type_args: TypeArgs::new(), + generic_calls: Vec::new(), next_unif: 0, wildcard_obligations: Vec::new(), unif_solutions: HashMap::new(), @@ -912,16 +960,16 @@ impl TypeChecker { } /// Instantiate a ForAll type by replacing bound variables with fresh - /// unification variables. Non-ForAll types pass through unchanged. - fn instantiate(&mut self, ty: Ty) -> Ty { - match ty { - Ty::ForAll { var, body } => { - let fresh = self.fresh_unif(); - let instantiated = body.subst_var(&var, &fresh); - self.instantiate(instantiated) - } - other => other, + /// unification variables, which are also returned, outermost first. + /// Non-ForAll types pass through unchanged. + fn instantiate(&mut self, mut ty: Ty) -> (Ty, Vec) { + let mut args = Vec::new(); + while let Ty::ForAll { var, body } = ty { + let fresh = self.fresh_unif(); + ty = body.subst_var(&var, &fresh); + args.push(fresh); } + (ty, args) } /// Resolve a type by chasing unification variable solutions. @@ -1092,7 +1140,7 @@ impl TypeChecker { let s = expr.span; match &expr.kind { ExprKind::Lit(lit) => self.check_lit(s, lit), - ExprKind::Var(name) => self.check_var(s, name), + ExprKind::Var(name) => self.check_var(s, expr.id, name), ExprKind::StringNew { region, .. } => self.check_string_new(s, region), ExprKind::StringConcat { left, right } => self.check_string_concat(s, left, right), ExprKind::Let { @@ -1164,7 +1212,7 @@ impl TypeChecker { }) } - fn check_var(&mut self, s: Span, name: &Var) -> Result { + fn check_var(&mut self, s: Span, id: ExprId, name: &Var) -> Result { let ty = self .ctx .lookup(name) @@ -1175,7 +1223,16 @@ impl TypeChecker { // Instantiate ForAll types with fresh unification variables. // E.g. `identity : forall T. T -> T` becomes `?0 -> ?0`. - Ok(self.instantiate(ty)) + let (ty, args) = self.instantiate(ty); + if !args.is_empty() { + self.instantiations.push(Instantiation { + id, + span: s, + callee: name.clone(), + args, + }); + } + Ok(ty) } fn check_string_new(&self, s: Span, region: &RegionName) -> Result { @@ -2550,6 +2607,15 @@ pub fn module_expr_types( module: &Module, registry: &mut ModuleRegistry, ) -> Result { + module_types_and_args(module, registry).map(|(types, _)| types) +} + +/// [`module_expr_types`], and the type arguments of every use of a +/// generic function ([`TypeArgs`]). +pub fn module_types_and_args( + module: &Module, + registry: &mut ModuleRegistry, +) -> Result<(ExprTypes, TypeArgs), SpannedTypeError> { let mut tc = TypeChecker::new(); // Resolve imports: bring imported names into scope @@ -2584,7 +2650,7 @@ pub fn module_expr_types( // Register this module's declarations for dependents registry.register(module); - Ok(tc.expr_types()) + Ok((tc.expr_types(), tc.type_args)) } /// Internal module checking logic shared by single-module and registry paths. @@ -2673,69 +2739,189 @@ fn type_check_module_inner(tc: &mut TypeChecker, module: &Module) -> Result<(), // Second pass: type check each function body for decl in &module.decls { - match decl { - Decl::Fn { - name: _, - type_params: _, - visibility: _, - params, - ret_ty, - body, - } => { - let saved_ctx = tc.ctx.clone(); - let saved_unif = tc.next_unif; - - // A repeated parameter name would shadow the earlier - // parameter for the whole body, so a linear earlier - // parameter could never be consumed or checked. - let mut seen_params: HashSet<&Var> = HashSet::new(); - for (param_name, _) in params { - if !seen_params.insert(param_name) { - return Err(SpannedTypeError { - error: TypeError::DuplicateParameter(param_name.clone()), - span: body.span, - }); - } - } + let Decl::Fn { + name, + type_params, + params, + ret_ty, + body, + .. + } = decl + else { + // A constant is handled in module registration. An extern + // signature is registered in the first pass, and an extern + // type is opaque. Data constructors go through + // `DataCtorRegistry`. None of them has a body to check here. + continue; + }; + check_fn(tc, name, type_params, params, ret_ty, body)?; + if type_params.is_empty() { + continue; + } + if name == "main" { + return Err(SpannedTypeError { + error: TypeError::GenericMain, + span: body.span, + }); + } + // A type parameter can stand for a linear type. The check above + // took each type parameter as an opaque type, so the body uses + // such a value only in ways that every type allows. Check the + // body one more time with each type parameter as a linear type: + // now only the linear discipline can fail (a value used two times, + // or not consumed). Until grades exist (E13), an unconstrained + // type parameter is linear (v2 plan E7). + let linear = type_params + .iter() + .map(|p| (p.clone(), Ty::String("_".into()))) + .collect(); + let mut copy = decl.clone(); + mono::substitute_fn(&mut copy, &linear); + if let Decl::Fn { + params, + ret_ty, + body, + .. + } = © + { + // On a scratch checker: this check records nothing. + check_fn(&mut tc.clone(), name, &[], params, ret_ty, body)?; + } + } + check_polymorphic_recursion(tc, module) +} - for (param_name, param_ty) in params { - tc.ctx - .extend(param_name.clone(), param_ty.clone(), BindingForm::Param); - } +/// Check the body of one function against its signature. +fn check_fn( + tc: &mut TypeChecker, + name: &Var, + type_params: &[SmolStr], + params: &[(Var, Ty)], + ret_ty: &Ty, + body: &Expr, +) -> Result<(), SpannedTypeError> { + let saved_ctx = tc.ctx.clone(); + let saved_unif = tc.next_unif; + + // A repeated parameter name would shadow the earlier + // parameter for the whole body, so a linear earlier + // parameter could never be consumed or checked. + let mut seen_params: HashSet<&Var> = HashSet::new(); + for (param_name, _) in params { + if !seen_params.insert(param_name) { + return Err(SpannedTypeError { + error: TypeError::DuplicateParameter(param_name.clone()), + span: body.span, + }); + } + } - let body_ty = tc.check(body)?; + for (param_name, param_ty) in params { + tc.ctx + .extend(param_name.clone(), param_ty.clone(), BindingForm::Param); + } - tc.unify(body.span, ret_ty, &body_ty)?; + let body_ty = tc.check(body)?; - for (param_name, _param_ty) in params { - if let Some(entry) = tc.ctx.vars.get(param_name) { - if entry.demands_consumption() && !entry.used { - return Err(SpannedTypeError { - error: TypeError::LinearVariableNotConsumed(param_name.clone()), - span: body.span, - }); - } - } - } + tc.unify(body.span, ret_ty, &body_ty)?; - tc.ctx = saved_ctx; - tc.next_unif = saved_unif; - tc.unif_solutions.clear(); - tc.wildcard_obligations.clear(); + for (param_name, _param_ty) in params { + if let Some(entry) = tc.ctx.vars.get(param_name) { + if entry.demands_consumption() && !entry.used { + return Err(SpannedTypeError { + error: TypeError::LinearVariableNotConsumed(param_name.clone()), + span: body.span, + }); } - Decl::Type { .. } => {} - Decl::Const { .. } => {} // Constants are handled in module registration - // Extern fn signatures are registered earlier in the - // signature-collection pass; the body-checking pass has - // nothing more to do. Extern types are opaque. - Decl::Extern { .. } => {} - // Data ctor signatures flow through `DataCtorRegistry` - // (populated pre-pass); pattern-checking dispatches via the - // registry, so there is no per-decl body to walk here. - Decl::Data { .. } => {} } } + // The body is checked, so the type arguments of each use of a generic + // function in it are known. Inside a generic function they can name + // its type parameters. + for use_ in std::mem::take(&mut tc.instantiations) { + let args: Vec = use_.args.iter().map(|t| tc.resolve(t)).collect(); + if args.iter().any(has_unif) { + return Err(SpannedTypeError { + error: TypeError::CannotInferTypeArgument(use_.callee), + span: use_.span, + }); + } + if !type_params.is_empty() { + tc.generic_calls + .push((name.clone(), use_.callee, args.clone(), use_.span)); + } + tc.type_args.insert(use_.id, args); + } + + tc.ctx = saved_ctx; + tc.next_unif = saved_unif; + tc.unif_solutions.clear(); + tc.wildcard_obligations.clear(); + Ok(()) +} + +/// True when `ty` has a unification variable with no solution. +fn has_unif(ty: &Ty) -> bool { + match ty { + Ty::Unif(_) => true, + Ty::Base(_) | Ty::String(_) | Ty::Var(_) => false, + Ty::Ref { inner, .. } + | Ty::Region { inner, .. } + | Ty::Borrow { inner, .. } + | Ty::ForAll { body: inner, .. } + | Ty::List(inner) => has_unif(inner), + Ty::Fun { param, ret } | Ty::Effectful { param, ret, .. } => { + has_unif(param) || has_unif(ret) + } + Ty::Prod { left, right } | Ty::Sum { left, right } => has_unif(left) || has_unif(right), + Ty::Tuple(elems) => elems.iter().any(has_unif), + } +} + +/// Reject a generic function that reaches itself, through calls between +/// generic functions of this module, with a type argument that is not one +/// of the type parameters of the caller. Monomorphisation makes one copy +/// of a generic function for each set of type arguments; with this rule +/// every such cycle uses a finite set of types, so the copies end. +/// (A cycle cannot leave the module: imports have no cycle.) +fn check_polymorphic_recursion(tc: &TypeChecker, module: &Module) -> Result<(), SpannedTypeError> { + let generic: HashMap<&Var, &Vec> = module + .decls + .iter() + .filter_map(|decl| match decl { + Decl::Fn { + name, type_params, .. + } if !type_params.is_empty() => Some((name, type_params)), + _ => None, + }) + .collect(); + let calls: Vec<_> = tc + .generic_calls + .iter() + .filter(|(_, callee, ..)| generic.contains_key(callee)) + .collect(); + let reaches = |from: &Var, to: &Var| { + let (mut stack, mut seen) = (vec![from], HashSet::new()); + while let Some(f) = stack.pop() { + if f == to { + return true; + } + if seen.insert(f) { + stack.extend(calls.iter().filter(|c| &c.0 == f).map(|c| &c.1)); + } + } + false + }; + for (caller, callee, args, span) in &calls { + let own = |arg: &Ty| matches!(arg, Ty::Var(v) if generic[caller].contains(v)); + if !args.iter().all(own) && reaches(callee, caller) { + return Err(SpannedTypeError { + error: TypeError::PolymorphicRecursion(caller.clone()), + span: *span, + }); + } + } Ok(()) } @@ -3541,45 +3727,72 @@ mod tests { #[test] fn test_generic_two_params() { - // fn const_fn(x: A, y: B) : A = x - // fn main() : I32 = const_fn(42, true) - let module = Module { + // fn pair(x: A, y: B) : (A, B) = (x, y) + // fn main() : I32 = fst(pair(42, true)) + let generic = |name: &str, ret_ty: Ty, body: Expr| Decl::Fn { + name: name.into(), + visibility: Visibility::Private, + type_params: vec!["A".into(), "B".into()], + params: vec![ + ("x".into(), Ty::Var("A".into())), + ("y".into(), Ty::Var("B".into())), + ], + ret_ty, + body, + }; + let var = |name: &str| dummy_expr(ExprKind::Var(name.into())); + // callee(42)(true) — curried application + let call = |callee: &str| { + dummy_expr(ExprKind::App { + func: Box::new(dummy_expr(ExprKind::App { + func: Box::new(var(callee)), + arg: Box::new(dummy_expr(ExprKind::Lit(Literal::I32(42)))), + })), + arg: Box::new(dummy_expr(ExprKind::Lit(Literal::Bool(true)))), + }) + }; + let main = |body: Expr| Decl::Fn { + name: "main".into(), + visibility: Visibility::Private, + type_params: vec![], + params: vec![], + ret_ty: Ty::Base(BaseTy::I32), + body, + }; + let module = |decls| Module { name: "test".into(), imports: vec![], - decls: vec![ - Decl::Fn { - name: "const_fn".into(), - visibility: Visibility::Private, - type_params: vec!["A".into(), "B".into()], - params: vec![ - ("x".into(), Ty::Var("A".into())), - ("y".into(), Ty::Var("B".into())), - ], - ret_ty: Ty::Var("A".into()), - // const_fn is curried: fn(x: A) -> fn(y: B) -> x - // But our module checker expects multi-param. - // Actually module checker expects the body to be checked - // with all params in scope, so just return x. - body: dummy_expr(ExprKind::Var("x".into())), - }, - Decl::Fn { - name: "main".into(), - visibility: Visibility::Private, - type_params: vec![], - params: vec![], - ret_ty: Ty::Base(BaseTy::I32), - // const_fn(42)(true) — curried application - body: dummy_expr(ExprKind::App { - func: Box::new(dummy_expr(ExprKind::App { - func: Box::new(dummy_expr(ExprKind::Var("const_fn".into()))), - arg: Box::new(dummy_expr(ExprKind::Lit(Literal::I32(42)))), - })), - arg: Box::new(dummy_expr(ExprKind::Lit(Literal::Bool(true)))), - }), - }, - ], + decls, }; - assert!(type_check_module(&module).is_ok()); + + let pair_ty = Ty::Prod { + left: Box::new(Ty::Var("A".into())), + right: Box::new(Ty::Var("B".into())), + }; + let pair_body = dummy_expr(ExprKind::Pair { + left: Box::new(var("x")), + right: Box::new(var("y")), + }); + let ok = module(vec![ + generic("pair", pair_ty, pair_body), + main(dummy_expr(ExprKind::Fst(Box::new(call("pair"))))), + ]); + assert!(type_check_module(&ok).is_ok()); + + // fn const_fn(x: A, y: B) : A = x + // `y` can have a linear type in a use of `const_fn`, so the body + // must consume it (v2 plan E7: a type parameter is linear). + let discards = module(vec![ + generic("const_fn", Ty::Var("A".into()), var("x")), + main(call("const_fn")), + ]); + assert!(matches!( + type_check_module(&discards), + Err(SpannedTypeError { + error: TypeError::LinearVariableNotConsumed(name), + .. + }) if name == "y" + )); } #[test] @@ -3671,7 +3884,7 @@ mod tests { ret: Box::new(Ty::Var("T".into())), }), }; - let instantiated = tc.instantiate(forall_ty); + let (instantiated, _) = tc.instantiate(forall_ty); // Should be Fun { param: Unif(0), ret: Unif(0) } match &instantiated { diff --git a/src/ephapax-typing/src/mono.rs b/src/ephapax-typing/src/mono.rs new file mode 100644 index 00000000..f8f1010f --- /dev/null +++ b/src/ephapax-typing/src/mono.rs @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: MPL-2.0 +// Owner: Jonathan D.A. Jewell +// +//! Monomorphisation of generic functions (v2 plan rung E7, step S2b). +//! +//! The checker checks a generic function one time, with its type +//! parameters as opaque types, and records the type arguments of every +//! use ([`TypeArgs`]). [`monomorphise`] then makes one copy of the +//! function for each set of type arguments and removes the generic +//! definition. Code generation and the interpreter see monomorphic +//! functions only. + +use std::collections::{HashMap, HashSet}; + +use ephapax_syntax::{Decl, Expr, ExprKind, Module, Ty, Var}; +use smol_str::SmolStr; + +use crate::{module_types_and_args, subst_tys, ModuleRegistry, SpannedTypeError, TypeArgs}; + +/// Type variable → type. +type Subst = HashMap; + +/// Replace type variables in the signature of the function `decl` and in +/// the type annotations of its body. +pub(crate) fn substitute_fn(decl: &mut Decl, subst: &Subst) { + let Decl::Fn { + params, + ret_ty, + body, + .. + } = decl + else { + return; + }; + for (_, ty) in params.iter_mut() { + *ty = subst_tys(ty, subst); + } + *ret_ty = subst_tys(ret_ty, subst); + substitute_expr(body, subst); +} + +fn substitute_expr(expr: &mut Expr, subst: &Subst) { + substitute_node(expr, subst); + for child in expr.children_mut() { + substitute_expr(child, subst); + } +} + +/// The type annotations of one node. +fn substitute_node(expr: &mut Expr, subst: &Subst) { + match &mut expr.kind { + ExprKind::Let { ty: Some(ty), .. } + | ExprKind::LetLin { ty: Some(ty), .. } + | ExprKind::Lambda { param_ty: ty, .. } + | ExprKind::Inl { ty, .. } + | ExprKind::Inr { ty, .. } => *ty = subst_tys(ty, subst), + _ => {} + } +} + +/// `module` with one monomorphic copy of each generic function for each +/// set of type arguments it is used with, and no generic function. +/// +/// `module` must be a whole program (the imports already merged in): it +/// is checked on its own. A program with no generic function is returned +/// as it is, without a check. +pub fn monomorphise(module: &Module) -> Result { + let generic: HashMap<&Var, &Decl> = module + .decls + .iter() + .filter_map(|decl| match decl { + Decl::Fn { + name, type_params, .. + } if !type_params.is_empty() => Some((name, decl)), + _ => None, + }) + .collect(); + if generic.is_empty() { + return Ok(module.clone()); + } + let (_, type_args) = module_types_and_args(module, &mut ModuleRegistry::new())?; + let mut mono = Mono { + generic, + type_args, + made: HashSet::new(), + instances: Vec::new(), + }; + + let mut decls = Vec::new(); + for decl in &module.decls { + match decl { + Decl::Fn { type_params, .. } if !type_params.is_empty() => {} + Decl::Fn { .. } => { + let mut decl = decl.clone(); + if let Decl::Fn { body, .. } = &mut decl { + mono.rewrite(body, &Subst::new()); + } + decls.push(decl); + } + other => decls.push(other.clone()), + } + } + decls.extend(mono.instances); + Ok(Module { + name: module.name.clone(), + imports: module.imports.clone(), + decls, + }) +} + +struct Mono<'a> { + generic: HashMap<&'a Var, &'a Decl>, + type_args: TypeArgs, + /// Names of the copies made (or in work) so far. + made: HashSet, + instances: Vec, +} + +impl Mono<'_> { + /// Point each use of a generic function in `expr` at the copy for its + /// type arguments. `subst` is not empty when `expr` is the body of + /// such a copy: it gives the type parameters of the copied function. + fn rewrite(&mut self, expr: &mut Expr, subst: &Subst) { + // The table has the id that the node had when it was checked. + let args = self.type_args.get(&expr.id).cloned(); + if let (ExprKind::Var(name), Some(args)) = (&mut expr.kind, args) { + let args = args.iter().map(|t| subst_tys(t, subst)).collect(); + *name = self.instance(name, args); + } + if !subst.is_empty() { + substitute_node(expr, subst); + // The copy gets its own entries in the next type table. + expr.renew_id(); + } + for child in expr.children_mut() { + self.rewrite(child, subst); + } + } + + /// The name of the copy of the generic function `name` for `args`; + /// the copy is made at the first request. + fn instance(&mut self, name: &Var, args: Vec) -> Var { + let Some(&generic) = self.generic.get(name) else { + return name.clone(); + }; + let rendered: Vec = args.iter().map(ty_name).collect(); + let copy_name: Var = format!("{name}<{}>", rendered.join(", ")).into(); + // Recorded before the body is rewritten, so a recursive call + // finds the copy in work. + if !self.made.insert(copy_name.clone()) { + return copy_name; + } + let mut copy = generic.clone(); + if let Decl::Fn { + name, + type_params, + params, + ret_ty, + body, + .. + } = &mut copy + { + let subst: Subst = std::mem::take(type_params).into_iter().zip(args).collect(); + *name = copy_name.clone(); + for (_, ty) in params.iter_mut() { + *ty = subst_tys(ty, &subst); + } + *ret_ty = subst_tys(ret_ty, &subst); + self.rewrite(body, &subst); + } + self.instances.push(copy); + copy_name + } +} + +/// A short name of a type, for the name of a copy. Two types have the +/// same name only when they are equal. +fn ty_name(ty: &Ty) -> String { + let list = |tys: &[Ty]| tys.iter().map(ty_name).collect::>().join(", "); + match ty { + Ty::Base(base) => format!("{base:?}"), + Ty::String(region) => format!("String@{region}"), + Ty::Var(name) => name.to_string(), + Ty::Prod { left, right } => format!("({} * {})", ty_name(left), ty_name(right)), + Ty::Sum { left, right } => format!("({} + {})", ty_name(left), ty_name(right)), + Ty::Tuple(elems) => format!("({})", list(elems)), + Ty::List(elem) => format!("[{}]", ty_name(elem)), + other => format!("{other:?}"), + } +} diff --git a/tests/probes/expectations.tsv b/tests/probes/expectations.tsv index d0c83b5c..caa1dc4f 100644 --- a/tests/probes/expectations.tsv +++ b/tests/probes/expectations.tsv @@ -50,10 +50,10 @@ perform wasm-run no-module no-module E1 g5 check error:TypeError error:* E1: raw variadic __ffi rejected with a named error (kind name set by E1) g5 compile error:TypeError error:* E1: named raw-FFI error, not an invalid module g5 wasm-run no-module no-module E1 -gen_angle check error:TypeError ok E7 generics: id instantiated at I32 -gen_angle compile error:TypeError ok E7 -gen_angle run error:TypeError ok:3 E7 (since E3, run loads through ephapax-driver and rejects what check rejects; before E3 the interpreter path printed 3) -gen_angle wasm-run no-module ok:3 E7 +gen_angle check ok ok E7 generics: id instantiated at I32 +gen_angle compile ok ok E7 +gen_angle run ok:3 ok:3 E7 +gen_angle wasm-run ok:3 ok:3 E7 (baseline recorded from `ephapax run`, same engine; no wasmtime CLI in that session) qa/app2 check error:DesugarError ok E7: qualified M.Ctor(x) across modules qa/app2 compile error:DesugarError ok E7 qa/app2 run error:DesugarError ok:9 E7 + E3 (run goes through ephapax-driver::load_program)