diff --git a/TEST-NEEDS.adoc b/TEST-NEEDS.adoc index 12964ee2..b5fe471d 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: *607 tests* (`+cargo test --workspace --all-targets+`); +* Total: *611 tests* (`+cargo test --workspace --all-targets+`); pass/fail enforced by `+rust-ci.yml+` -* Documented all-target tests: 607 +* Documented all-target tests: 611 === 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 — 607 pass, 0 fail +* [x] cargo test –workspace –all-targets — 611 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/ephapax-linear/src/affine.rs b/ephapax-linear/src/affine.rs index 0830d74c..d80e81e9 100644 --- a/ephapax-linear/src/affine.rs +++ b/ephapax-linear/src/affine.rs @@ -95,7 +95,7 @@ impl AffineChecker { match &expr.kind { ExprKind::Lit(_) => {} - ExprKind::Var(name) => { + ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => { self.consume_var(name); } diff --git a/ephapax-linear/src/linear.rs b/ephapax-linear/src/linear.rs index 28d154df..331adb6f 100644 --- a/ephapax-linear/src/linear.rs +++ b/ephapax-linear/src/linear.rs @@ -107,7 +107,7 @@ impl LinearChecker { ExprKind::Lit(_) => {} // --- Variable reference: consume the binding --- - ExprKind::Var(name) => { + ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => { self.consume_var(name); } diff --git a/src/ephapax-analysis/src/escape.rs b/src/ephapax-analysis/src/escape.rs index d5456e85..0dd0ce01 100644 --- a/src/ephapax-analysis/src/escape.rs +++ b/src/ephapax-analysis/src/escape.rs @@ -53,7 +53,9 @@ impl EscapeAnalysis { fn analyze_expr(expr: &Expr, escaping: &mut HashSet, in_escaping_context: bool) { match &expr.kind { - ExprKind::Var(name) => { + // A generic function at explicit type arguments is a use of the + // function, as a variable is. + ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => { if in_escaping_context { escaping.insert(name.clone()); } diff --git a/src/ephapax-analysis/src/free_vars.rs b/src/ephapax-analysis/src/free_vars.rs index 7f83dc30..2fd12630 100644 --- a/src/ephapax-analysis/src/free_vars.rs +++ b/src/ephapax-analysis/src/free_vars.rs @@ -57,7 +57,9 @@ impl FreeVarAnalysis { fn collect(expr: &Expr, free: &mut HashSet, bound: &mut HashSet) { match &expr.kind { - ExprKind::Var(name) => { + // A generic function at explicit type arguments is a use of the + // function, as a variable is. + ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => { if !bound.contains(name) { free.insert(name.clone()); } diff --git a/src/ephapax-analysis/src/liveness.rs b/src/ephapax-analysis/src/liveness.rs index 93577238..a97632cf 100644 --- a/src/ephapax-analysis/src/liveness.rs +++ b/src/ephapax-analysis/src/liveness.rs @@ -62,7 +62,9 @@ impl LivenessAnalysis { fn compute(expr: &Expr, live: &mut HashSet) { match &expr.kind { - ExprKind::Var(name) => { + // A generic function at explicit type arguments is a use of the + // function, as a variable is. + ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => { live.insert(name.clone()); } diff --git a/src/ephapax-cli/tests/generics.rs b/src/ephapax-cli/tests/generics.rs index e5bbca34..6f358f7d 100644 --- a/src/ephapax-cli/tests/generics.rs +++ b/src/ephapax-cli/tests/generics.rs @@ -89,6 +89,94 @@ fn a_generic_function_runs_at_each_type_it_is_used_with() { } } +/// A call can give its type arguments: `id[I32](3)`. The checker takes +/// them from the brackets, so a type parameter that no argument fixes +/// has a value. +#[test] +fn a_call_can_give_its_type_arguments() { + let tmp = tempfile::tempdir().expect("tempdir"); + let id = "fn id(x: a): a = x\n\n"; + // (declarations, type and body of `main`, output) + for (decls, ty, main, want) in [ + (id, "I32", "id[I32](3)", "3"), + (id, "I32", "id[I32](41)", "41"), + (id, "F64", "id[F64](2.5)", "2.5"), + // Two type parameters. + ( + "fn pair(p: (a, b)): (a, b) = p\n\n", + "I64", + "snd(pair[I32, I64]((1, 6000000000)))", + "6000000000i64", + ), + // A type parameter of the caller as a type argument, in lower + // case and in upper case. In `g`, `a` is a variable too. + ( + "fn id(x: a): a = x\n\nfn g(a: a): a = id[a](a)\n\n\ + fn wrap(x: T): T = id[T](x)\n\n", + "I32", + "g(8) * 10 + wrap[I32](5)", + "85", + ), + // A region type as a type argument. + ( + id, + "I32", + "region r {\n let! s = String.new@r(\"hello\")\n String.len(id[String@r](s))\n }", + "5", + ), + // Brackets with one name that is not a type parameter are an + // index, as before: the call is a call of the list element. + ( + "", + "I32", + "let fs = [fn(x: I32) -> x + 1, fn(x: I32) -> x + 2] in\n let i = 1 in\n fs[i](3)", + "5", + ), + ] { + write( + tmp.path(), + "app.eph", + &format!("module app\n\n{decls}fn main(): {ty} =\n {main}\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(), + want, + "{main} under {args:?}:\n{}", + String::from_utf8_lossy(&out.stderr) + ); + } + } + + // No argument fixes `a`: the brackets do. Wasm only: the interpreter + // cannot call a function with no parameter by its name. + write( + tmp.path(), + "zero.eph", + "module zero\n\nfn zero(): I32 = 7\n\nfn main(): I32 = zero[I64]()\n", + ); + let out = ephapax(tmp.path(), &["run", "zero.eph"]); + assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "7"); + + // A type argument of a function of another module, with and without + // the module qualifier. + write( + tmp.path(), + "lib.eph", + "module lib\n\npub fn id(x: a): a = x\n", + ); + write( + tmp.path(), + "use.eph", + "module use\n\nimport lib\n\nfn main(): I32 = lib.id[I32](4) + id[I32](5)\n", + ); + for args in [&["run", "use.eph"][..], &["run", "--interp", "use.eph"]] { + let out = ephapax(tmp.path(), args); + assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "9", "{args:?}"); + } +} + /// The merged program has one monomorphic function for each set of type /// arguments, and no generic function. #[test] @@ -143,6 +231,20 @@ fn generic_programs_the_checker_refuses() { "fn zero(): I32 = 0\n\nfn main(): I32 = zero()\n", "Cannot infer a type argument of the generic function `zero`", ), + // Explicit type arguments: the wrong type, a function that is not + // generic, the wrong number. + ( + "fn id(x: a): a = x\n\nfn main(): I32 = id[I64](3)\n", + "Type mismatch: expected Base(I64), found Base(I32)", + ), + ( + "fn inc(x: I32): I32 = x + 1\n\nfn main(): I32 = inc[I32](3)\n", + "`inc` is not a generic function and cannot take type arguments", + ), + ( + "fn id(x: a): a = x\n\nfn main(): I32 = id[I32, I64](3)\n", + "The generic function `id` has 1 type parameter(s), but the call gives 2 type argument(s)", + ), // No call gives the entry point its type arguments. ( "fn main(): I32 = 0\n", diff --git a/src/ephapax-cli/tests/run_wasm.rs b/src/ephapax-cli/tests/run_wasm.rs index 0c5704a7..00bfc8f7 100644 --- a/src/ephapax-cli/tests/run_wasm.rs +++ b/src/ephapax-cli/tests/run_wasm.rs @@ -264,6 +264,260 @@ fn a_qualified_constructor_takes_its_arguments() { } } +/// A module names a data type and its constructors when it declares +/// them, or when it imports the module that declares them and the type is +/// `pub` (owner decision 2026-11-10: the rule of functions). Each module +/// used to see each data type of each module loaded before it. +#[test] +fn a_module_names_a_data_type_only_through_a_pub_import() { + let tmp = tempfile::tempdir().expect("tempdir"); + let write = + |name: &str, text: &str| std::fs::write(tmp.path().join(name), text).expect("write module"); + // `Inner` and `Secret` are private. `Outer` is public and has a + // private type in its field. + write( + "lib.eph", + "module lib\n\ndata Inner = I(I32)\n\npub data Outer = O(Inner)\n\n\ + pub data Box = MkBox(I32)\n\ndata Secret = Hidden(I32)\n\n\ + pub fn mk(n: I32): Outer = O(I(n))\n\n\ + pub fn get(o: Outer): I32 = match o of | O(i) => match i of | I(n) => n end end\n\n\ + pub fn unbox(b: Box): I32 = match b of | MkBox(v) => v end\n\n\ + pub fn secret(n: I32): Secret = Hidden(n)\n", + ); + write( + "deep.eph", + "module deep\n\npub data Far = MkFar(I32)\n\npub fn far(n: I32): Far = MkFar(n)\n", + ); + write( + "mid.eph", + "module mid\n\nimport deep\n\npub fn through(n: I32): Far = far(n)\n", + ); + + // (imports and declarations, `main` body, output) + for (head, main, want) in [ + ( + "import lib\n", + "unbox(MkBox(9)) + unbox(lib.MkBox(1))", + "10", + ), + // An import list names the type; the constructors come with it. + ("import lib(Box, unbox)\n", "unbox(MkBox(9))", "9"), + ( + "import lib(Box)\n", + "match MkBox(4) of | MkBox(v) => v end", + "4", + ), + // `Outer` has the `Inner` of `lib` in its field, also in a module + // that has its own `Inner`. + ( + "import lib\n\ndata Inner = Mine(I64)\n\nfn pass(o: Outer): Outer = o\n", + "get(pass(mk(5)))", + "5", + ), + // Not a recursive type: this `Inner` is not the `Inner` in `Outer`. + ( + "import lib\n\ndata Inner = Wrap(Outer)\n", + "match Wrap(mk(6)) of | Wrap(o) => get(o) end", + "6", + ), + // A declaration of the module shadows an imported one. + ( + "import lib\n\ndata Box = MkBox(I32, I32)\n", + "match MkBox(3, 4) of | MkBox(a, b) => a + b end", + "7", + ), + ] { + write( + "app.eph", + &format!("module app\n\n{head}\nfn main(): I32 = {main}\n"), + ); + for args in [&["run", "app.eph"][..], &["run", "--interp", "app.eph"]] { + assert_eq!(stdout_of(tmp.path(), args), want, "{main} under {args:?}"); + } + } + + // (imports and declarations, `main` body, text of the load error) + for (head, main, error) in [ + // A private data type: its constructor and its name. + ( + "import lib\n", + "match secret(3) of | Hidden(n) => n end", + "unknown constructor `Hidden`", + ), + ( + "import lib\n", + "lib.Hidden(3)", + "unknown constructor `Hidden`", + ), + ( + "import lib\n\nfn keep(s: Secret): Secret = s\n", + "0", + "unknown type `Secret`", + ), + // The import list does not name the type. + ( + "import lib(unbox)\n", + "unbox(MkBox(9))", + "unknown constructor `MkBox`", + ), + // The list names only the type: no function comes with it. + ("import lib(Box)\n", "unbox(MkBox(9))", "unbox"), + // A data type of a module that this module does not import. + ( + "import mid\n", + "match through(3) of | MkFar(n) => n end", + "unknown constructor `MkFar`", + ), + ] { + write( + "bad.eph", + &format!("module bad\n\n{head}\nfn main(): I32 = {main}\n"), + ); + let out = ephapax(tmp.path(), &["compile", "bad.eph"]); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + !out.status.success() && stderr.contains(error), + "{head}{main}: exit {:?}, stderr:\n{stderr}", + out.status.code() + ); + } +} + +/// v2 plan rung E7: an arm with a false guard gives the value to the next +/// arm, as wasm and in the interpreter. The desugarer used to take a +/// guarded constructor arm without a look at its guard. +#[test] +fn a_false_guard_gives_the_value_to_the_next_arm() { + let tmp = tempfile::tempdir().expect("tempdir"); + let write = |name: &str, text: &str| { + std::fs::write(tmp.path().join(name), text).expect("write program") + }; + let opt = "data Opt = None | Some(I32)\n\n"; + // (declarations, `main` body, output) + for (decls, main, want) in [ + // The guard selects between two arms of one constructor. + ( + format!( + "{opt}fn pick(o: Opt): I32 =\n \ + match o of | Some(x) if x > 5 => 1 | Some(x) => 2 | None => 0 end\n" + ), + "pick(Some(9)) * 100 + pick(Some(3)) * 10 + pick(None)", + "120", + ), + // A guard that is always false: the wildcard arm takes the value. + ( + format!("{opt}fn pick(o: Opt): I32 = match o of | Some(x) if false => 1 | _ => 7 end\n"), + "pick(Some(9)) * 10 + pick(None)", + "77", + ), + // The wildcard arm reads the outer `x`, not the `x` of the arm + // before it. + ( + format!( + "{opt}fn pick(o: Opt): I32 =\n let x = 40 in\n \ + match o of | Some(x) if x > 5 => x | _ => x end\n" + ), + "pick(Some(9)) * 100 + pick(Some(3))", + "940", + ), + // Three constructors, two guards on one of them, a wildcard field. + ( + "data Shape = Dot | Seg(I32) | Rect(I32, I32)\n\n\ + fn f(s: Shape): I32 =\n match s of\n \ + | Rect(w, h) if w > h => 1\n | Rect(w, h) if w == h => 2\n | Rect(_, _) => 3\n \ + | Seg(n) if n > 0 => 4\n | Dot => 5\n | _ => 6\n end\n" + .to_string(), + "f(Rect(3, 2)) * 100000 + f(Rect(2, 2)) * 10000 + f(Rect(1, 2)) * 1000 \ + + f(Seg(1)) * 100 + f(Seg(0)) * 10 + f(Dot)", + "123465", + ), + // A literal in a constructor pattern can refuse the value too. + ( + format!( + "{opt}fn f(o: Opt): I32 =\n match o of\n \ + | Some(0) if true => 1\n | Some(7) => 2\n | Some(n) if n > 100 => 3\n \ + | Some(_) => 4\n | None => 5\n end\n" + ), + "f(Some(0)) * 10000 + f(Some(7)) * 1000 + f(Some(200)) * 100 + f(Some(8)) * 10 + f(None)", + "12345", + ), + // A data type with one constructor. + ( + "data Box = MkBox(I32)\n\n\ + fn f(b: Box): I32 = match b of | MkBox(v) if v > 0 => 1 | MkBox(v) => 0 end\n" + .to_string(), + "f(MkBox(4)) * 10 + f(MkBox(0))", + "10", + ), + // No constructor: literal, variable and pair patterns. + ( + "fn f(n: I32): I32 =\n \ + match n of | 0 => 10 | x if x > 5 => 20 | 3 if false => 30 | _ => 40 end\n\n\ + fn g(p: (I32, I32)): I32 = match p of | (a, b) if a > b => a | (a, b) => b end\n" + .to_string(), + "f(0) * 100000 + f(9) * 10000 + f(3) * 1000 + g((3, 9)) * 10 + g((7, 2))", + "1240097", + ), + ] { + write( + "guard.eph", + &format!("module guard\n\n{decls}\nfn main(): I32 = {main}\n"), + ); + for args in [&["run", "guard.eph"][..], &["run", "--interp", "guard.eph"]] { + assert_eq!(stdout_of(tmp.path(), args), want, "{main} under {args:?}"); + } + } + + // A linear payload: the arm after a false guard binds it again, and + // each arm consumes it one time. + for (k, want) in [("7", "105"), ("3", "5")] { + write( + "linear.eph", + &format!( + "module linear\n\ndata Entry = Empty | Named(String@r, I32)\n\n\ + fn main(): I32 =\n region r {{\n let! s = String.new@r(\"hello\")\n \ + let e = Named(s, {k})\n match e of\n \ + | Named(n, k) if k > 5 => String.len(n) + 100\n \ + | Named(n, k) => String.len(n)\n | Empty => 0\n end\n }}\n" + ), + ); + for args in [ + &["run", "linear.eph"][..], + &["run", "--interp", "linear.eph"], + ] { + assert_eq!(stdout_of(tmp.path(), args), want, "k = {k} under {args:?}"); + } + } + + // (match, text of the load error) + for (matched, error) in [ + // A guarded arm does not cover its constructor. + ( + "match o of | Some(x) if x > 5 => 1 | None => 0 end", + "non-exhaustive match: missing constructor `Some`", + ), + ( + "match W(o) of | W(Some(x)) if x > 5 => 1 | _ => 0 end", + "cannot have the constructor pattern `Some` inside another pattern", + ), + ] { + write( + "bad.eph", + &format!( + "module bad\n\n{opt}data Wrap = W(Opt)\n\n\ + fn f(o: Opt): I32 = {matched}\n\nfn main(): I32 = f(None)\n" + ), + ); + let out = ephapax(tmp.path(), &["compile", "bad.eph"]); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + !out.status.success() && stderr.contains(error), + "{matched}: exit {:?}, stderr:\n{stderr}", + out.status.code() + ); + } +} + /// A pair pattern binds each component at its own type: a destructuring /// `let`, and a constructor with several fields (a right-nested pair). /// Patterns used to read every component as an i32. diff --git a/src/ephapax-desugar/src/lib.rs b/src/ephapax-desugar/src/lib.rs index 74a4a718..849f9331 100644 --- a/src/ephapax-desugar/src/lib.rs +++ b/src/ephapax-desugar/src/lib.rs @@ -120,6 +120,16 @@ pub enum DesugarError { #[error("empty match expression")] EmptyMatch, + #[error("type arguments need the name of a generic function before them, as in `id[I32](3)`")] + TypeArgumentsOnExpression, + + /// A `match` with a guard goes to the core `match`, which has no + /// form for a constructor inside another pattern. + #[error( + "a match with a guard cannot have the constructor pattern `{ctor}` inside another pattern" + )] + GuardWithNestedConstructor { ctor: String }, + #[error("qualified access `{qualifier}.{member}`: `{qualifier}` is not an imported module (record field access is not yet supported)")] QualifiedAccessUnknownModule { qualifier: String, member: String }, @@ -143,26 +153,54 @@ pub enum DesugarError { struct ConstructorInfo { /// Index of this constructor in the data declaration (0-based). index: usize, - /// Total number of constructors in the data type. - _total: usize, /// Payload types (empty for nullary constructors). fields: Vec, - /// The data type this constructor belongs to. + /// The data type this constructor belongs to, by its name in `home`. data_name: SmolStr, - /// Type parameters of the data type. - _data_params: Vec, + /// Key of the module that declares the data type. + home: SmolStr, +} + +/// A data type as its module declares it. +#[derive(Debug, Clone)] +struct DataInfo { + params: Vec, + ctors: Vec, + /// Key of the module that declares the type. The names in the + /// fields of `ctors` are names in the scope of that module. + home: SmolStr, + /// `pub data`: a module that imports `home` can name the type. + public: bool, +} + +/// The data types and the constructors that one module can name: its +/// own, and the public ones of the modules that it imports. +#[derive(Debug, Clone, Default)] +struct Scope { + /// Constructor name → info + constructors: HashMap, + /// Data type name → declaration + types: HashMap, +} + +/// The key of a module in the registry. `Foo.Bar` and `Foo/Bar` name the +/// same module, so an import can spell it either way. +fn module_key(name: &str) -> SmolStr { + name.replace('.', "/").into() } /// Registry of all data declarations encountered during desugaring. /// /// Maps constructor names to their encoding information. This is built /// from `DataDecl` nodes and used to desugar `Construct` and `Match`. +/// Each module has its own [`Scope`], as it has for functions: a module +/// names a data type of another module only when it imports that module +/// and the type is `pub`. #[derive(Debug, Clone, Default)] pub struct DataRegistry { - /// Constructor name → info - constructors: HashMap, - /// Data type name → (params, constructors) - types: HashMap, Vec)>, + /// Module key → what the module can name. The key is the empty + /// string for declarations registered with no module. + scopes: HashMap, /// Names declared via `extern "abi" { type Foo }` blocks. /// /// Extern types are nominal and opaque: the desugar pass resolves @@ -176,8 +214,9 @@ pub struct DataRegistry { /// form so `desugar_named_type` can recursively expand them on /// demand. The stored value is whatever the parser produced for the /// alias body (record/sum decls already pre-lowered to `Prod`/`Sum`/ - /// `Tuple` by `parse_type_decl`). - type_aliases: HashMap, + /// `Tuple` by `parse_type_decl`), and the key of the module that + /// declares the alias: the body expands in the scope of that module. + type_aliases: HashMap, } impl DataRegistry { @@ -186,51 +225,88 @@ impl DataRegistry { Self::default() } - /// Register a data declaration. - pub fn register(&mut self, decl: &DataDecl) { - let total = decl.constructors.len(); + /// Register a data declaration of the module `home`. + pub fn register(&mut self, home: &str, decl: &DataDecl) { + let scope = self.scopes.entry(home.into()).or_default(); for (index, ctor) in decl.constructors.iter().enumerate() { - self.constructors.insert( + scope.constructors.insert( ctor.name.clone(), ConstructorInfo { index, - _total: total, fields: ctor.fields.clone(), data_name: decl.name.clone(), - _data_params: decl.params.clone(), + home: home.into(), }, ); } - self.types.insert( + scope.types.insert( decl.name.clone(), - (decl.params.clone(), decl.constructors.clone()), + DataInfo { + params: decl.params.clone(), + ctors: decl.constructors.clone(), + home: home.into(), + public: decl.visibility == SurfaceVisibility::Public, + }, ); } + /// Bring the public data types that the module `from` declares, with + /// their constructors, into the scope of the module `into`. A list of + /// `names` that is not empty limits the import to the types in it. + /// Returns the names of the imported types. + fn import(&mut self, into: &str, from: &str, names: &[SmolStr]) -> HashSet { + let Some(source) = self.scopes.get(from) else { + return HashSet::new(); + }; + let mut imported = Scope::default(); + for (name, data) in &source.types { + if !data.public || data.home != from || !(names.is_empty() || names.contains(name)) { + continue; + } + for ctor in &data.ctors { + // A later data type of `from` can have a constructor of + // the same name: the constructor is then not of `name`. + let info = source.constructors.get(&ctor.name); + if let Some(info) = info.filter(|i| i.home == from && i.data_name == *name) { + imported + .constructors + .insert(ctor.name.clone(), info.clone()); + } + } + imported.types.insert(name.clone(), data.clone()); + } + let types = imported.types.keys().cloned().collect(); + let scope = self.scopes.entry(into.into()).or_default(); + scope.constructors.extend(imported.constructors); + scope.types.extend(imported.types); + types + } + /// Register an opaque extern type name (from /// `extern "abi" { type Foo }`). pub fn register_extern_type(&mut self, name: &SmolStr) { self.extern_types.insert(name.clone()); } - /// Register a `type Foo = T` alias in surface form. Lookups in - /// `desugar_named_type` find the entry and recursively desugar it. - pub fn register_type_alias(&mut self, name: SmolStr, ty: SurfaceTy) { - self.type_aliases.insert(name, ty); + /// Register a `type Foo = T` alias of the module `home` in surface + /// form. Lookups in `desugar_named_type` find the entry and + /// recursively desugar it. + pub fn register_type_alias(&mut self, home: &str, name: SmolStr, ty: SurfaceTy) { + self.type_aliases.insert(name, (ty, home.into())); } - fn lookup_type_alias(&self, name: &str) -> Option<&SurfaceTy> { + fn lookup_type_alias(&self, name: &str) -> Option<&(SurfaceTy, SmolStr)> { self.type_aliases.get(name) } - /// Look up a constructor by name. - fn get_ctor(&self, name: &str) -> Option<&ConstructorInfo> { - self.constructors.get(name) + /// Look up a constructor by its name in the module `scope`. + fn get_ctor(&self, scope: &str, name: &str) -> Option<&ConstructorInfo> { + self.scopes.get(scope)?.constructors.get(name) } - /// Get the constructors for a data type (in order). - fn get_type_ctors(&self, name: &str) -> Option<&(Vec, Vec)> { - self.types.get(name) + /// Look up a data type by its name in the module `scope`. + fn get_type_ctors(&self, scope: &str, name: &str) -> Option<&DataInfo> { + self.scopes.get(scope)?.types.get(name) } /// Is this name a registered extern (opaque) type? @@ -256,8 +332,13 @@ pub struct Desugarer { /// [`Desugarer::desugar_module`]. module_qualifiers: HashSet, /// Named types currently being expanded by `desugar_named_type`, - /// innermost last. Re-entering one is a recursive type. - expanding: RefCell>, + /// innermost last, each with the key of the module that declares it. + /// Re-entering one is a recursive type. + expanding: RefCell>, + /// Key of the module whose scope gives a name its meaning: the module + /// being desugared, or, while the body of a named type expands, the + /// module that declares that type. + scope: 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`). @@ -271,6 +352,7 @@ impl Desugarer { registry: DataRegistry::new(), module_qualifiers: HashSet::new(), expanding: RefCell::new(Vec::new()), + scope: RefCell::default(), fn_type_params: Vec::new(), } } @@ -281,10 +363,39 @@ impl Desugarer { registry, module_qualifiers: HashSet::new(), expanding: RefCell::new(Vec::new()), + scope: RefCell::default(), fn_type_params: Vec::new(), } } + /// The constructor `name` in the current scope. + fn ctor(&self, name: &str) -> Result<&ConstructorInfo, DesugarError> { + self.registry + .get_ctor(&self.scope.borrow(), name) + .ok_or_else(|| DesugarError::UnknownConstructor { + ctor: name.to_string(), + }) + } + + /// The constructors of the data type of the constructor `info`. + fn ctors_of(&self, info: &ConstructorInfo) -> Result, DesugarError> { + self.registry + .get_type_ctors(&info.home, &info.data_name) + .map(|data| data.ctors.clone()) + .ok_or_else(|| DesugarError::UnknownType { + name: info.data_name.to_string(), + }) + } + + /// Run `f` with the names of the module `home` in scope: the names + /// in the body of a type are names of the module that declares it. + fn in_scope(&self, home: &SmolStr, f: impl FnOnce(&Self) -> T) -> T { + let outer = self.scope.replace(home.clone()); + let result = f(self); + self.scope.replace(outer); + result + } + /// Desugar a complete surface module to a core module. /// /// First pass: collect all data declarations into the registry. @@ -299,11 +410,34 @@ impl Desugarer { .map(|i| last_segment(i.module.as_str())) .collect(); + // The public data types of each imported module, in the order of + // the imports: a later one shadows an earlier one, and a + // declaration of this module shadows both. + let key = module_key(&module.name); + // The imports of the core module: the type checker reads each + // name in an import list as a function, so the list keeps only + // the names that are not data types. An import that names only + // data types imports no function. + let mut imports = Vec::with_capacity(module.imports.len()); + for import in &module.imports { + let types = self + .registry + .import(&key, &module_key(&import.module), &import.names); + let mut import = import.clone(); + let listed = !import.names.is_empty(); + import.names.retain(|name| !types.contains(name)); + // An empty list is each public name of the module. + if !(listed && import.names.is_empty()) { + imports.push(import); + } + } + self.scope.replace(key.clone()); + // First pass: register all data types and extern types for decl in &module.decls { match decl { SurfaceDecl::Data(data) => { - self.registry.register(data); + self.registry.register(&key, data); } SurfaceDecl::Extern { items, .. } => { for item in items { @@ -313,7 +447,8 @@ impl Desugarer { } } SurfaceDecl::Type { name, ty, .. } => { - self.registry.register_type_alias(name.clone(), ty.clone()); + self.registry + .register_type_alias(&key, name.clone(), ty.clone()); } _ => {} } @@ -401,7 +536,7 @@ impl Desugarer { Ok(Module { name: module.name.clone(), - imports: module.imports.clone(), + imports, decls: core_decls, }) } @@ -573,6 +708,28 @@ impl Desugarer { list: Box::new(self.desugar_expr(list)?), index: Box::new(self.desugar_expr(index)?), }, + SurfaceExprKind::TypeApp { func, type_args } => { + let func = self.desugar_expr(func)?; + match (&type_args[..], func.kind) { + // `xs[i](3)`: a lower-case name is a type only as a + // type parameter of this function. Any other name + // is a variable, and the brackets are an index. + ([SurfaceTy::Var(v)], list) if !self.fn_type_params.contains(v) => { + ExprKind::ListIndex { + list: Box::new(Expr::new(list, func.span)), + index: Box::new(Expr::new(ExprKind::Var(v.clone()), span)), + } + } + (_, ExprKind::Var(name)) => ExprKind::TypeApp { + name, + type_args: type_args + .iter() + .map(|ty| self.desugar_ty(ty)) + .collect::>()?, + }, + _ => return Err(DesugarError::TypeArgumentsOnExpression), + } + } SurfaceExprKind::TupleLit(elements) => { let desugared: Vec<_> = elements .iter() @@ -624,10 +781,7 @@ impl Desugarer { return None; } args.reverse(); - let nullary = self - .registry - .get_ctor(field.as_str()) - .is_some_and(|info| info.fields.is_empty()); + let nullary = self.ctor(field).is_ok_and(|info| info.fields.is_empty()); if nullary && matches!( args[..], @@ -785,7 +939,7 @@ impl Desugarer { // `type Foo = T` aliases — recursively desugar the stored surface // form. Aliases are monomorphic here (parameterised aliases would // carry their args via the body). - if let Some(aliased) = self.registry.lookup_type_alias(name.as_str()).cloned() { + if let Some((aliased, home)) = self.registry.lookup_type_alias(name.as_str()).cloned() { if !args.is_empty() { return Err(DesugarError::TypeArityMismatch { name: name.to_string(), @@ -793,14 +947,20 @@ impl Desugarer { got: args.len(), }); } - return self.with_expansion(name, args, |this| this.desugar_ty(&aliased)); + return self.with_expansion(name, &home, |this| this.desugar_ty(&aliased)); } - let (params, ctors) = self.registry.get_type_ctors(name.as_str()).ok_or_else(|| { - DesugarError::UnknownType { + let DataInfo { + params, + ctors, + home, + .. + } = self + .registry + .get_type_ctors(&self.scope.borrow(), name.as_str()) + .ok_or_else(|| DesugarError::UnknownType { name: name.to_string(), - } - })?; + })?; if params.len() != args.len() { return Err(DesugarError::TypeArityMismatch { @@ -818,28 +978,28 @@ impl Desugarer { let subst: HashMap<&SmolStr, &Ty> = params.iter().zip(core_args.iter()).collect(); // Build right-nested sum from constructor field types - let ctors = ctors.clone(); - self.with_expansion(name, args, |this| this.build_sum_type(&ctors, &subst)) + self.with_expansion(name, home, |this| this.build_sum_type(ctors, &subst)) } - /// Run `f` with the named type application `name(args)` pushed on the - /// expansion stack. Fails with [`DesugarError::RecursiveType`] if the - /// same declaration is already being expanded, including non-regular - /// recursion with changing arguments. Arguments are elaborated before - /// entering the declaration, so finite nested applications remain valid. - /// Errors propagate after removing this expansion from the stack. + /// Run `f` with the named type `name` of the module `home` pushed on + /// the expansion stack, and with the names of `home` in scope. Fails + /// with [`DesugarError::RecursiveType`] if the same declaration is + /// already being expanded, including non-regular recursion with + /// changing arguments. Arguments are elaborated before entering the + /// declaration, so finite nested applications remain valid. Errors + /// propagate after removing this expansion from the stack. fn with_expansion( &self, name: &SmolStr, - _args: &[SurfaceTy], + home: &SmolStr, f: impl FnOnce(&Self) -> Result, ) -> Result { - let key = name.clone(); + let key = (home.clone(), name.clone()); { let stack = self.expanding.borrow(); if stack.contains(&key) { - let mut cycle: Vec = stack.iter().map(|k| k.to_string()).collect(); - cycle.push(key.to_string()); + let mut cycle: Vec = stack.iter().map(|(_, n)| n.to_string()).collect(); + cycle.push(name.to_string()); return Err(DesugarError::RecursiveType { name: name.to_string(), cycle: cycle.join(" -> "), @@ -847,7 +1007,7 @@ impl Desugarer { } } self.expanding.borrow_mut().push(key); - let result = f(self); + let result = self.in_scope(home, f); self.expanding.borrow_mut().pop(); result } @@ -942,13 +1102,7 @@ impl Desugarer { args: &[SurfaceExpr], span: Span, ) -> Result { - let info = self - .registry - .get_ctor(ctor.as_str()) - .ok_or_else(|| DesugarError::UnknownConstructor { - ctor: ctor.to_string(), - })? - .clone(); + let info = self.ctor(ctor)?.clone(); if info.fields.len() != args.len() { return Err(DesugarError::ConstructorArityMismatch { @@ -963,16 +1117,13 @@ impl Desugarer { // Get the full sum type for the "other side" annotations let empty_subst: HashMap<&SmolStr, &Ty> = HashMap::new(); - let (_, ctor_defs) = self - .registry - .get_type_ctors(info.data_name.as_str()) - .ok_or_else(|| DesugarError::UnknownType { - name: info.data_name.to_string(), - })?; - let ctors = ctor_defs.clone(); + let ctors = self.ctors_of(&info)?; - // Wrap in inl/inr chain based on index - self.wrap_injection(payload, info.index, &ctors, &empty_subst, span) + // Wrap in inl/inr chain based on index. The types of the other + // constructors are names of the module that declares the type. + self.in_scope(&info.home, |this| { + this.wrap_injection(payload, info.index, &ctors, &empty_subst, span) + }) } /// Build the payload expression for a constructor application. @@ -1085,6 +1236,18 @@ impl Desugarer { return self.bind_single_pattern(&core_scrutinee, &arms[0].pattern, body, span); } + // A guard, and no constructor pattern at the top of an arm: the + // core `match` tries the arms in order and honours each guard. + let guarded = arms.iter().any(|a| a.guard.is_some()); + if guarded + && !arms + .iter() + .any(|a| matches!(a.pattern, Pattern::Constructor { .. })) + { + let patterns = arms.iter().map(|a| a.pattern.clone()).collect(); + return self.guarded_match(core_scrutinee, arms.iter(), patterns, span); + } + // Match on a literal-typed scrutinee (`match n of | 0 => a | 1 => // b | _ => c end`). All arms are Literal / Wildcard / Var, no // constructor — desugar to nested `if scrutinee == lit then arm @@ -1130,63 +1293,54 @@ impl Desugarer { return Ok(acc); } - // Find the data type from constructor patterns - let data_name = self.find_data_type_from_arms(arms)?; - - let (_, ctor_defs) = self - .registry - .get_type_ctors(data_name.as_str()) - .ok_or_else(|| DesugarError::UnknownType { - name: data_name.to_string(), - })?; - let ctors = ctor_defs.clone(); - - // Build an ordered map: constructor index → (pattern bindings, body) - let mut arm_map: HashMap = HashMap::new(); - let mut wildcard_arm: Option<&MatchArm> = None; - + // The data type, from the first constructor pattern. + let first_ctor = arms + .iter() + .find_map(|arm| match &arm.pattern { + Pattern::Constructor { ctor, .. } => Some(ctor), + _ => None, + }) + .ok_or(DesugarError::EmptyMatch)?; + let ctors = self.ctors_of(self.ctor(first_ctor)?)?; + + // The constructor index of each arm; `None` for an arm that takes + // every constructor. A literal or a tuple pattern at the top of + // a match on a data type is taken as a wildcard. + let mut indexed: Vec<(Option, &MatchArm)> = Vec::with_capacity(arms.len()); for arm in arms { - match &arm.pattern { - Pattern::Constructor { ctor, .. } => { - let info = self.registry.get_ctor(ctor.as_str()).ok_or_else(|| { - DesugarError::UnknownConstructor { - ctor: ctor.to_string(), - } - })?; - arm_map.insert(info.index, arm); - } - Pattern::Wildcard | Pattern::Var(_) => { - wildcard_arm = Some(arm); - } - _ => { - // Literal or tuple patterns at top level of match on ADT — - // treat as wildcard for now - wildcard_arm = Some(arm); - } - } + let index = match &arm.pattern { + Pattern::Constructor { ctor, .. } => Some(self.ctor(ctor)?.index), + _ => None, + }; + indexed.push((index, arm)); } - // Generate the nested case tree - self.build_case_tree(&core_scrutinee, &ctors, &arm_map, wildcard_arm, 0, span) - } - - /// Find which data type is being matched by examining constructor patterns. - fn find_data_type_from_arms(&self, arms: &[MatchArm]) -> Result { - for arm in arms { - if let Pattern::Constructor { ctor, .. } = &arm.pattern { - if let Some(info) = self.registry.get_ctor(ctor.as_str()) { - return Ok(info.data_name.clone()); - } else { - return Err(DesugarError::UnknownConstructor { - ctor: ctor.to_string(), - }); + // The arms that can take constructor `i`, in the order to try. + // With a guard in the match: each arm for `i`, in source order. + // With no guard, one arm: the last arm for `i`, or else the last + // arm for every constructor. + let chains: Vec> = (0..ctors.len()) + .map(|i| { + if guarded { + return indexed + .iter() + .filter(|(index, _)| index.is_none_or(|n| n == i)) + .map(|(_, arm)| *arm) + .collect(); } - } - } - // No constructor patterns found — all wildcards/vars - // This is valid but we can't determine the type. Return a placeholder. - // In practice, the type checker will resolve this. - Err(DesugarError::EmptyMatch) + let last = |want: Option| { + indexed + .iter() + .rev() + .find(|(index, _)| *index == want) + .map(|(_, arm)| *arm) + }; + last(Some(i)).or_else(|| last(None)).into_iter().collect() + }) + .collect(); + + // Generate the nested case tree + self.build_case_tree(&core_scrutinee, &ctors, &chains, 0, span) } /// Build the nested case tree for a match expression. @@ -1203,41 +1357,32 @@ impl Desugarer { &self, scrutinee: &Expr, ctors: &[ConstructorDef], - arm_map: &HashMap, - wildcard: Option<&MatchArm>, + chains: &[Vec<&MatchArm>], base_index: usize, span: Span, ) -> Result { + let chain = &chains[base_index]; match ctors.len() { 0 => unreachable!("empty constructor list"), - // Last (or only) constructor — no case needed, just bind - 1 => { - let arm = arm_map - .get(&base_index) - .or(wildcard.as_ref()) - .ok_or_else(|| DesugarError::NonExhaustive { - ctor: ctors[0].name.to_string(), - })?; - - self.build_arm_body(scrutinee, &ctors[0], arm, span) - } + // Last (or only) constructor: no case needed, the scrutinee + // is the payload (no inl/inr wrapper). + 1 => self.build_arm_chain(scrutinee, &ctors[0], chain, span), // Two or more constructors — generate case _ => { - let left_arm = arm_map - .get(&base_index) - .or(wildcard.as_ref()) - .ok_or_else(|| DesugarError::NonExhaustive { - ctor: ctors[0].name.to_string(), - })?; - - // Variable name for the left (inl) payload - let left_var = self.fresh_var_for_arm(left_arm, &ctors[0]); - let left_body = self.build_arm_body_inner( + // Variable name for the left (inl) payload. More than one + // arm can read it, so it does not take the name of a + // variable of one arm: that name would hide an outer + // variable in the other arms. + let left_var = match chain[..] { + [arm] => self.fresh_var_for_arm(arm, &ctors[0]), + _ => format!("__{}", ctors[0].name.to_lowercase()).into(), + }; + let left_body = self.build_arm_chain( &Expr::new(ExprKind::Var(left_var.clone()), span), &ctors[0], - left_arm, + chain, span, )?; @@ -1247,8 +1392,7 @@ impl Desugarer { let right_body = self.build_case_tree( &rest_scrutinee, &ctors[1..], - arm_map, - wildcard, + chains, base_index + 1, span, )?; @@ -1267,16 +1411,95 @@ impl Desugarer { } } - /// Build the body for a match arm (single constructor, no more case needed). - fn build_arm_body( + /// The code for one constructor: `chain` has the arms to try on its + /// payload, in order. A chain where each arm has a guard does not + /// cover the constructor. For a longer chain, the type checker finds + /// if the patterns cover the payload. + fn build_arm_chain( &self, - scrutinee: &Expr, + payload: &Expr, ctor: &ConstructorDef, - arm: &MatchArm, + chain: &[&MatchArm], + span: Span, + ) -> Result { + match chain { + [arm] if arm.guard.is_none() => self.build_arm_body_inner(payload, ctor, arm, span), + _ if chain.iter().any(|arm| arm.guard.is_none()) => { + // A pattern on the constructor is a pattern on its + // payload: `()`, the one field, or a right-nested pair. + let patterns = chain + .iter() + .map(|arm| match &arm.pattern { + Pattern::Constructor { args, .. } => args + .iter() + .rev() + .cloned() + .reduce(|rest, p| Pattern::Pair(Box::new(p), Box::new(rest))) + .unwrap_or(Pattern::Unit), + Pattern::Var(v) => Pattern::Var(v.clone()), + _ => Pattern::Wildcard, + }) + .collect(); + self.guarded_match(payload.clone(), chain.iter().copied(), patterns, span) + } + _ => Err(DesugarError::NonExhaustive { + ctor: ctor.name.to_string(), + }), + } + } + + /// A core `match` with one arm for each of `arms`, on `patterns` + /// (one for each arm). The core `match` tries the arms in order, and + /// an arm with a false guard gives the value to the next arm. + fn guarded_match<'a>( + &self, + scrutinee: Expr, + arms: impl Iterator, + patterns: Vec, span: Span, ) -> Result { - // For the last constructor, scrutinee IS the payload (no inl/inr wrapper) - self.build_arm_body_inner(scrutinee, ctor, arm, span) + let arms = arms + .zip(&patterns) + .map(|(arm, pattern)| { + Ok(ephapax_syntax::MatchArm { + pattern: Self::core_pattern(pattern)?, + guard: match &arm.guard { + Some(guard) => Some(Box::new(self.desugar_expr(guard)?)), + None => None, + }, + body: self.desugar_expr(&arm.body)?, + }) + }) + .collect::>()?; + Ok(Expr::new( + ExprKind::Match { + scrutinee: Box::new(scrutinee), + arms, + }, + span, + )) + } + + /// The core form of a pattern that has no constructor pattern in it. + fn core_pattern(pattern: &Pattern) -> Result { + use ephapax_syntax::Pattern as CorePattern; + let all = |ps: &[Pattern]| ps.iter().map(Self::core_pattern).collect::>(); + Ok(match pattern { + Pattern::Wildcard => CorePattern::Wildcard, + Pattern::Var(v) => CorePattern::Var(v.clone()), + Pattern::Unit => CorePattern::Unit, + Pattern::Literal(lit) => CorePattern::Literal(lit.clone()), + Pattern::Pair(l, r) => CorePattern::Pair( + Box::new(Self::core_pattern(l)?), + Box::new(Self::core_pattern(r)?), + ), + Pattern::Tuple(elements) => CorePattern::Tuple(all(elements)?), + Pattern::Constructor { ctor, .. } => { + return Err(DesugarError::GuardWithNestedConstructor { + ctor: ctor.to_string(), + }) + } + }) } /// Build the arm body, binding pattern variables to the payload. @@ -1410,7 +1633,7 @@ impl Desugarer { ExprKind::Match { scrutinee: Box::new(value.clone()), arms: vec![ephapax_syntax::MatchArm { - pattern: Self::core_pair_pattern(pattern), + pattern: Self::core_pattern(pattern)?, guard: None, body, }], @@ -1453,22 +1676,6 @@ impl Desugarer { } } - /// Convert a pattern accepted by [`Self::is_irrefutable_pair_pattern`] - /// to the core pattern type. - fn core_pair_pattern(pattern: &Pattern) -> ephapax_syntax::Pattern { - use ephapax_syntax::Pattern as CorePattern; - match pattern { - Pattern::Var(v) => CorePattern::Var(v.clone()), - Pattern::Unit => CorePattern::Unit, - Pattern::Pair(l, r) => CorePattern::Pair( - Box::new(Self::core_pair_pattern(l)), - Box::new(Self::core_pair_pattern(r)), - ), - // Only reachable for `Wildcard` (the guard excludes the rest). - _ => CorePattern::Wildcard, - } - } - /// Generate a fresh variable name for a match arm's payload binding. fn fresh_var_for_arm(&self, arm: &MatchArm, ctor: &ConstructorDef) -> SmolStr { match &arm.pattern { @@ -1520,6 +1727,7 @@ mod tests { fn option_decl() -> DataDecl { DataDecl { name: "Option".into(), + visibility: Default::default(), params: vec!["a".into()], constructors: vec![ ConstructorDef { @@ -1538,6 +1746,7 @@ mod tests { fn color_decl() -> DataDecl { DataDecl { name: "Color".into(), + visibility: Default::default(), params: vec![], constructors: vec![ ConstructorDef { @@ -1562,7 +1771,7 @@ mod tests { #[test] fn desugar_option_i32() { let mut ds = Desugarer::new(); - ds.registry.register(&option_decl()); + ds.registry.register("", &option_decl()); let ty = SurfaceTy::Named { name: "Option".into(), @@ -1583,7 +1792,7 @@ mod tests { #[test] fn desugar_color_type() { let mut ds = Desugarer::new(); - ds.registry.register(&color_decl()); + ds.registry.register("", &color_decl()); let ty = SurfaceTy::Named { name: "Color".into(), @@ -1607,7 +1816,7 @@ mod tests { #[test] fn desugar_type_arity_mismatch() { let mut ds = Desugarer::new(); - ds.registry.register(&option_decl()); + ds.registry.register("", &option_decl()); let ty = SurfaceTy::Named { name: "Option".into(), @@ -1622,7 +1831,7 @@ mod tests { #[test] fn desugar_none_constructor() { let mut ds = Desugarer::new(); - ds.registry.register(&option_decl()); + ds.registry.register("", &option_decl()); let expr = se(SurfaceExprKind::Construct { ctor: "None".into(), @@ -1637,7 +1846,7 @@ mod tests { #[test] fn desugar_some_constructor() { let mut ds = Desugarer::new(); - ds.registry.register(&option_decl()); + ds.registry.register("", &option_decl()); let expr = se(SurfaceExprKind::Construct { ctor: "Some".into(), @@ -1652,7 +1861,7 @@ mod tests { #[test] fn desugar_color_green() { let mut ds = Desugarer::new(); - ds.registry.register(&color_decl()); + ds.registry.register("", &color_decl()); let expr = se(SurfaceExprKind::Construct { ctor: "Green".into(), @@ -1671,7 +1880,7 @@ mod tests { #[test] fn desugar_color_blue() { let mut ds = Desugarer::new(); - ds.registry.register(&color_decl()); + ds.registry.register("", &color_decl()); let expr = se(SurfaceExprKind::Construct { ctor: "Blue".into(), @@ -1704,7 +1913,7 @@ mod tests { #[test] fn desugar_constructor_arity_mismatch() { let mut ds = Desugarer::new(); - ds.registry.register(&option_decl()); + ds.registry.register("", &option_decl()); let expr = se(SurfaceExprKind::Construct { ctor: "None".into(), @@ -1718,7 +1927,7 @@ mod tests { #[test] fn desugar_match_option() { let mut ds = Desugarer::new(); - ds.registry.register(&option_decl()); + ds.registry.register("", &option_decl()); let expr = se(SurfaceExprKind::Match { scrutinee: Box::new(se(SurfaceExprKind::Var("x".into()))), @@ -1750,7 +1959,7 @@ mod tests { #[test] fn desugar_match_color() { let mut ds = Desugarer::new(); - ds.registry.register(&color_decl()); + ds.registry.register("", &color_decl()); let expr = se(SurfaceExprKind::Match { scrutinee: Box::new(se(SurfaceExprKind::Var("c".into()))), @@ -1797,7 +2006,7 @@ mod tests { #[test] fn desugar_match_with_wildcard() { let mut ds = Desugarer::new(); - ds.registry.register(&color_decl()); + ds.registry.register("", &color_decl()); let expr = se(SurfaceExprKind::Match { scrutinee: Box::new(se(SurfaceExprKind::Var("c".into()))), @@ -1826,7 +2035,7 @@ mod tests { #[test] fn desugar_non_exhaustive_match() { let mut ds = Desugarer::new(); - ds.registry.register(&color_decl()); + ds.registry.register("", &color_decl()); let expr = se(SurfaceExprKind::Match { scrutinee: Box::new(se(SurfaceExprKind::Var("c".into()))), @@ -1930,6 +2139,7 @@ mod tests { decls: vec![ SurfaceDecl::Data(DataDecl { name: "Option".into(), + visibility: Default::default(), params: vec!["a".into()], constructors: vec![ ConstructorDef { @@ -1996,6 +2206,7 @@ mod tests { decls: vec![ SurfaceDecl::Data(DataDecl { name: "Option".into(), + visibility: Default::default(), params: vec!["a".into()], constructors: vec![ ConstructorDef { @@ -2210,16 +2421,17 @@ mod expansion_regressions { #[test] fn nested_finite_application_and_nonregular_recursion() { let mut registry = DataRegistry::new(); - registry.types.insert( - "Id".into(), - ( - vec!["a".into()], - vec![ConstructorDef { - name: "IdValue".into(), - fields: vec![SurfaceTy::Var("a".into())], - }], - ), - ); + let data = |name: &str, ctor: &str, field: SurfaceTy| DataDecl { + name: name.into(), + visibility: Default::default(), + params: vec!["a".into()], + constructors: vec![ConstructorDef { + name: ctor.into(), + fields: vec![field], + }], + span: Span::dummy(), + }; + registry.register("", &data("Id", "IdValue", SurfaceTy::Var("a".into()))); let inner = SurfaceTy::Named { name: "Id".into(), args: vec![SurfaceTy::Base(BaseTy::I32)], @@ -2234,19 +2446,11 @@ mod expansion_regressions { .unwrap(), Ty::Base(BaseTy::I32) ); - registry.types.insert( - "Grow".into(), - ( - vec!["a".into()], - vec![ConstructorDef { - name: "More".into(), - fields: vec![SurfaceTy::Named { - name: "Grow".into(), - args: vec![SurfaceTy::List(Box::new(SurfaceTy::Var("a".into())))], - }], - }], - ), - ); + let grow = SurfaceTy::Named { + name: "Grow".into(), + args: vec![SurfaceTy::List(Box::new(SurfaceTy::Var("a".into())))], + }; + registry.register("", &data("Grow", "More", grow)); let recursive = SurfaceTy::Named { name: "Grow".into(), args: vec![SurfaceTy::Base(BaseTy::I32)], @@ -2261,6 +2465,7 @@ mod expansion_regressions { let mut registry = DataRegistry::new(); for i in 0..65 { registry.register_type_alias( + "", format!("A{i}").into(), if i == 64 { SurfaceTy::Base(BaseTy::I32) @@ -2274,7 +2479,7 @@ mod expansion_regressions { desugarer.desugar_ty(&named("A0")).unwrap(), Ty::Base(BaseTy::I32) ); - registry.register_type_alias("A64".into(), named("A0")); + registry.register_type_alias("", "A64".into(), named("A0")); assert!(matches!( Desugarer::with_registry(registry).desugar_ty(&named("A0")), Err(DesugarError::RecursiveType { .. }) diff --git a/src/ephapax-driver/src/lib.rs b/src/ephapax-driver/src/lib.rs index 6db7ae4b..9ff6e901 100644 --- a/src/ephapax-driver/src/lib.rs +++ b/src/ephapax-driver/src/lib.rs @@ -223,8 +223,9 @@ pub fn parse_source( stdlib_dir, )?; - // Chain a single DataRegistry across all modules so imported `data` - // and `extern type` items are visible. + // One DataRegistry for all modules: it gives each module its own + // scope, with the `pub data` types of the modules that it imports. + // An `extern type` is visible in each module. let mut desugarer = Desugarer::with_registry(DataRegistry::new()); let mut modules = Vec::with_capacity(resolved.len()); for m in resolved { diff --git a/src/ephapax-driver/src/qualify.rs b/src/ephapax-driver/src/qualify.rs index 677458f8..50423e90 100644 --- a/src/ephapax-driver/src/qualify.rs +++ b/src/ephapax-driver/src/qualify.rs @@ -147,6 +147,7 @@ fn rename_expr(expr: &mut Expr, scope: &Renames) { // expression form must say if it has a name. match &mut expr.kind { ExprKind::Var(name) + | ExprKind::TypeApp { name, .. } | ExprKind::Let { name, .. } | ExprKind::LetLin { name, .. } | ExprKind::Lambda { param: name, .. } => rename(name, scope), diff --git a/src/ephapax-interp/src/lib.rs b/src/ephapax-interp/src/lib.rs index 3dde9ca4..4af42de2 100644 --- a/src/ephapax-interp/src/lib.rs +++ b/src/ephapax-interp/src/lib.rs @@ -407,7 +407,9 @@ impl Interpreter { pub fn eval(&mut self, expr: &Expr) -> Result { match &expr.kind { ExprKind::Lit(lit) => self.eval_lit(lit), - ExprKind::Var(name) => self.eval_var(name), + // The interpreter runs a generic function as it is: the type + // arguments select no code. + ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => self.eval_var(name), ExprKind::StringNew { region, value } => self.eval_string_new(region, value), ExprKind::StringConcat { left, right } => self.eval_string_concat(left, right), ExprKind::StringLen(inner) => self.eval_string_len(inner), diff --git a/src/ephapax-ir/src/lib.rs b/src/ephapax-ir/src/lib.rs index f3c0a69e..2cfa6383 100644 --- a/src/ephapax-ir/src/lib.rs +++ b/src/ephapax-ir/src/lib.rs @@ -422,6 +422,14 @@ pub fn expr_to_sexpr(expr: &Expr) -> SExpr { SExpr::Atom("var".into()), SExpr::Atom(escape_atom(name)), ]), + ExprKind::TypeApp { name, type_args } => { + let mut items = vec![ + SExpr::Atom("type-app".into()), + SExpr::Atom(escape_atom(name)), + ]; + items.extend(type_args.iter().map(ty_to_sexpr)); + SExpr::List(items) + } ExprKind::StringNew { region, value } => SExpr::List(vec![ SExpr::Atom("string-new".into()), SExpr::Atom(escape_atom(region)), @@ -654,6 +662,15 @@ pub fn decode_expr(expr: &SExpr) -> Result { ExprKind::Lit(decode_lit(&list[1])?) } "var" => ExprKind::Var(SmolStr::new(atom_string(&list[1])?)), + "type-app" => { + let [_, name, type_args @ ..] = &list[..] else { + return Err(SExprError::Invalid("(type-app f ty ...)".into())); + }; + ExprKind::TypeApp { + name: SmolStr::new(atom_string(name)?), + type_args: type_args.iter().map(decode_ty).collect::>()?, + } + } "string-new" => { if list.len() != 3 { return Err(SExprError::Invalid("(string-new r \"s\")".into())); @@ -1126,4 +1143,19 @@ mod tests { let parsed = module_from_sexpr(&sexpr).expect("parse"); assert_eq!(module, parsed); } + + /// A generic function at explicit type arguments keeps its name and + /// its type arguments; a `type-app` with no name is refused. + #[test] + fn roundtrip_type_app() { + let kind = ExprKind::TypeApp { + name: SmolStr::new("id"), + type_args: vec![Ty::Base(BaseTy::I32), Ty::Var(SmolStr::new("a"))], + }; + let sexpr = expr_to_sexpr(&Expr::dummy(kind.clone())); + assert_eq!(decode_expr(&sexpr).expect("decode").kind, kind); + + let no_name = SExpr::List(vec![SExpr::Atom("type-app".into())]); + assert!(decode_expr(&no_name).is_err()); + } } diff --git a/src/ephapax-lsp/src/main.rs b/src/ephapax-lsp/src/main.rs index 22e8f3be..cacbe01f 100644 --- a/src/ephapax-lsp/src/main.rs +++ b/src/ephapax-lsp/src/main.rs @@ -933,7 +933,10 @@ fn find_let_binding_span(expr: &Expr, target: &str) -> Option { }) }) } - ExprKind::Var(_) | ExprKind::Lit(_) | ExprKind::StringNew { .. } => None, + ExprKind::Var(_) + | ExprKind::TypeApp { .. } + | ExprKind::Lit(_) + | ExprKind::StringNew { .. } => None, } } diff --git a/src/ephapax-parser/src/ephapax.pest b/src/ephapax-parser/src/ephapax.pest index a5a2f4e6..171f5d8c 100644 --- a/src/ephapax-parser/src/ephapax.pest +++ b/src/ephapax-parser/src/ephapax.pest @@ -341,12 +341,18 @@ postfix_expr = { atom_expr ~ postfix_op* } postfix_op = { call_op + | type_app_op | index_op | member_op } call_op = { "(" ~ call_args? ~ ")" } call_args = { expression ~ ("," ~ expression)* } +// Explicit type arguments of a generic function: `id[I32](3)`. Only +// before a call, and before `index_op`: `xs[i](3)` has the two readings +// when `i` is one name. The desugarer reads that case as an index, unless +// the name is a type parameter of the function it is in. +type_app_op = { "[" ~ ty_list ~ "]" ~ &"(" } index_op = { "[" ~ expression ~ "]" } member_op = { "." ~ (integer | identifier | constructor_name) } diff --git a/src/ephapax-parser/src/lib.rs b/src/ephapax-parser/src/lib.rs index 332ca19a..23fcb6e6 100644 --- a/src/ephapax-parser/src/lib.rs +++ b/src/ephapax-parser/src/lib.rs @@ -1679,6 +1679,32 @@ fn parse_postfix_expr(pair: pest::iterators::Pair) -> Result { + let type_args: Vec = op_inner + .into_inner() + .flat_map(|list| list.into_inner()) + .filter(|p| p.as_rule() == Rule::ty) + .map(parse_type) + .collect::>()?; + // snip: this parser does not know the type parameters + // in scope, so one name is an index, as it was before + // `type_app_op`. The surface parser path knows them. + let kind = match (&type_args[..], result.kind) { + ([Ty::Var(v)], list) => ExprKind::ListIndex { + list: Box::new(Expr::new(list, span)), + index: Box::new(Expr::new(ExprKind::Var(v.clone()), span)), + }, + (_, ExprKind::Var(name)) => ExprKind::TypeApp { name, type_args }, + _ => { + return Err(ParseError::Syntax { + message: "type arguments need the name of a generic function" + .into(), + span, + }) + } + }; + result = Expr::new(kind, span); + } Rule::index_op => { let index = parse_expression( op_inner diff --git a/src/ephapax-parser/src/surface.rs b/src/ephapax-parser/src/surface.rs index 53b81fc1..89bd8994 100644 --- a/src/ephapax-parser/src/surface.rs +++ b/src/ephapax-parser/src/surface.rs @@ -243,15 +243,15 @@ fn parse_data_decl(pair: pest::iterators::Pair) -> Result = None; let mut params = Vec::new(); let mut constructors = Vec::new(); for item in inner { if name.is_none() && item.as_rule() == Rule::visibility { + visibility = SurfaceVisibility::Public; continue; } if name.is_none() && item.as_rule() == Rule::constructor_name { @@ -275,6 +275,7 @@ fn parse_data_decl(pair: pest::iterators::Pair) -> Result) -> Result { + let type_args = op_inner + .into_inner() + .flat_map(|list| list.into_inner()) + .filter(|p| p.as_rule() == Rule::ty) + .map(parse_type) + .collect::>()?; + result = SurfaceExpr::new( + SurfaceExprKind::TypeApp { + func: Box::new(result), + type_args, + }, + span, + ); + } Rule::index_op => { let idx_pair = op_inner .into_inner() diff --git a/src/ephapax-surface/src/lib.rs b/src/ephapax-surface/src/lib.rs index 2f9aa8d6..8a4dcd67 100644 --- a/src/ephapax-surface/src/lib.rs +++ b/src/ephapax-surface/src/lib.rs @@ -211,6 +211,10 @@ pub struct ConstructorDef { pub struct DataDecl { /// The type name (e.g. "Option", "Result", "Color") pub name: SmolStr, + /// `pub data`: a module that imports this one can name the type and + /// its constructors. + #[serde(default, skip_serializing_if = "SurfaceVisibility::is_private")] + pub visibility: SurfaceVisibility, /// Type parameters (e.g. ["a"] for Option, ["a", "e"] for Result) pub params: Vec, /// The constructors @@ -363,6 +367,13 @@ pub enum SurfaceExprKind { index: usize, }, + /// Explicit type arguments of a generic function: `id[I32]` in + /// `id[I32](3)`. + TypeApp { + func: Box, + type_args: Vec, + }, + // === Surface-only nodes === /// Named constructor application: `Some(x)`, `None`, `Ok(value)` /// @@ -580,6 +591,7 @@ mod tests { fn data_decl_option() { let decl = DataDecl { name: "Option".into(), + visibility: Default::default(), params: vec!["a".into()], constructors: vec![ ConstructorDef { @@ -602,6 +614,7 @@ mod tests { // data Color = Red | Green | Blue let decl = DataDecl { name: "Color".into(), + visibility: Default::default(), params: vec![], constructors: vec![ ConstructorDef { diff --git a/src/ephapax-syntax/src/lib.rs b/src/ephapax-syntax/src/lib.rs index 2e5a4e39..b9bdf805 100644 --- a/src/ephapax-syntax/src/lib.rs +++ b/src/ephapax-syntax/src/lib.rs @@ -483,6 +483,12 @@ pub enum ExprKind { /// Variable reference Var(Var), + /// A generic function with explicit type arguments: `id[I32]` (v2 + /// plan rung E7). The checker takes the type arguments from here + /// and not from the call; monomorphisation replaces the node with + /// the name of the copy. + TypeApp { name: Var, type_args: Vec }, + // ===== String operations ===== /// String allocation: String.new@r("...") StringNew { region: RegionName, value: String }, @@ -670,7 +676,10 @@ impl Expr { 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::Lit(_) + | ExprKind::Var(_) + | ExprKind::TypeApp { .. } + | ExprKind::StringNew { .. } => vec![], ExprKind::StringConcat { left, right } | ExprKind::Pair { left, right } | ExprKind::BinOp { left, right, .. } => vec![left, right], diff --git a/src/ephapax-typing/src/lib.rs b/src/ephapax-typing/src/lib.rs index 479de7c2..cc3dbe08 100644 --- a/src/ephapax-typing/src/lib.rs +++ b/src/ephapax-typing/src/lib.rs @@ -141,6 +141,19 @@ pub enum TypeError { )] PolymorphicRecursion(Var), + /// Type arguments on a name that is not a generic function. + #[error("`{0}` is not a generic function and cannot take type arguments")] + NotGeneric(Var), + + /// The number of explicit type arguments is not the number of type + /// parameters. + #[error("The generic function `{callee}` has {expected} type parameter(s), but the call gives {got} type argument(s)")] + TypeArgumentCount { + callee: Var, + expected: usize, + got: usize, + }, + /// `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")] @@ -1141,6 +1154,9 @@ impl TypeChecker { match &expr.kind { ExprKind::Lit(lit) => self.check_lit(s, lit), ExprKind::Var(name) => self.check_var(s, expr.id, name), + ExprKind::TypeApp { name, type_args } => { + self.check_type_app(s, expr.id, name, type_args) + } ExprKind::StringNew { region, .. } => self.check_string_new(s, region), ExprKind::StringConcat { left, right } => self.check_string_concat(s, left, right), ExprKind::Let { @@ -1235,6 +1251,37 @@ impl TypeChecker { Ok(ty) } + /// `name[type_args]`: the generic function `name` at the type + /// arguments that the program gives. + fn check_type_app( + &mut self, + s: Span, + id: ExprId, + name: &Var, + type_args: &[Ty], + ) -> Result { + let ty = self.check_var(s, id, name)?; + // `check_var` recorded the use if `name` is a generic function. + let params = match self.instantiations.last() { + Some(use_) if use_.id == id => use_.args.clone(), + _ => return Err(self.at(s, TypeError::NotGeneric(name.clone()))), + }; + if params.len() != type_args.len() { + return Err(self.at( + s, + TypeError::TypeArgumentCount { + callee: name.clone(), + expected: params.len(), + got: type_args.len(), + }, + )); + } + for (param, arg) in params.iter().zip(type_args) { + self.unify(s, arg, param)?; + } + Ok(ty) + } + fn check_string_new(&self, s: Span, region: &RegionName) -> Result { // The wildcard region name `_` is implicitly active everywhere — // it stands for the global / data-section pool that holds bare diff --git a/src/ephapax-typing/src/mono.rs b/src/ephapax-typing/src/mono.rs index f8f1010f..50722d39 100644 --- a/src/ephapax-typing/src/mono.rs +++ b/src/ephapax-typing/src/mono.rs @@ -54,6 +54,11 @@ fn substitute_node(expr: &mut Expr, subst: &Subst) { | ExprKind::Lambda { param_ty: ty, .. } | ExprKind::Inl { ty, .. } | ExprKind::Inr { ty, .. } => *ty = subst_tys(ty, subst), + ExprKind::TypeApp { type_args, .. } => { + for ty in type_args { + *ty = subst_tys(ty, subst); + } + } _ => {} } } @@ -123,9 +128,12 @@ impl Mono<'_> { 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) { + if let (ExprKind::Var(name) | ExprKind::TypeApp { name, .. }, Some(args)) = + (&expr.kind, args) + { let args = args.iter().map(|t| subst_tys(t, subst)).collect(); - *name = self.instance(name, args); + // The copy has no type parameter: explicit type arguments end here. + expr.kind = ExprKind::Var(self.instance(name, args)); } if !subst.is_empty() { substitute_node(expr, subst); diff --git a/src/ephapax-wasm/src/lib.rs b/src/ephapax-wasm/src/lib.rs index 075c60b5..ff394a4c 100644 --- a/src/ephapax-wasm/src/lib.rs +++ b/src/ephapax-wasm/src/lib.rs @@ -267,7 +267,10 @@ fn first_unsupported_in(expr: &Expr, types: Option<&ExprTypes>) -> Option = match &expr.kind { - ExprKind::Lit(_) | ExprKind::Var(_) | ExprKind::StringNew { .. } => vec![], + ExprKind::Lit(_) + | ExprKind::Var(_) + | ExprKind::TypeApp { .. } + | ExprKind::StringNew { .. } => vec![], ExprKind::StringConcat { left, right } | ExprKind::Pair { left, right } | ExprKind::BinOp { left, right, .. } => vec![left, right], @@ -2230,7 +2233,10 @@ impl Codegen { match &expr.kind { ExprKind::Lit(lit) => self.compile_lit(func, lit), - ExprKind::Var(name) => self.compile_var(func, name), + // Monomorphisation replaces a `TypeApp` with the name of the + // copy. In a module that was not monomorphised it is a use of + // the name, as a variable is. + ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => self.compile_var(func, name), ExprKind::StringNew { region: _, value } => self.compile_string_new(func, value), ExprKind::StringConcat { left, right } => self.compile_string_concat(func, left, right), ExprKind::StringLen(inner) => self.compile_string_len(func, inner), @@ -2487,7 +2493,7 @@ impl Codegen { seen: &mut HashSet, ) { match &expr.kind { - ExprKind::Var(name) => { + ExprKind::Var(name) | ExprKind::TypeApp { name, .. } => { if !bound.contains(name.as_str()) && !seen.contains(name.as_str()) { free.push(name.to_string()); seen.insert(name.to_string());