From 31596843d74858c478ecd37bab2fedc44473873b Mon Sep 17 00:00:00 2001 From: LukasK Date: Tue, 24 Mar 2026 11:25:30 +0000 Subject: [PATCH 01/43] docs: add Pi types design document and update syntax reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add docs/bs/pi_types.md covering the design for dependent function types (Pi) and lambdas at the meta level: motivation, syntax (fn(x: A) -> B for types, |x: A| body for lambdas), typing rules, core IR refactoring (App/Head → PrimApp + Global + FunApp), evaluator closures, and staging interaction. Update docs/SYNTAX.md with function type syntax, lambda syntax, and revised EBNF grammar. Co-Authored-By: Claude Opus 4.6 --- docs/SYNTAX.md | 50 ++++++++- docs/bs/README.md | 1 + docs/bs/pi_types.md | 264 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 313 insertions(+), 2 deletions(-) create mode 100644 docs/bs/pi_types.md diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md index 6094085..c36fb21 100644 --- a/docs/SYNTAX.md +++ b/docs/SYNTAX.md @@ -22,7 +22,7 @@ x + y // This is also a comment | Keyword | Description | |-----------|-------------| -| `fn` | Function definition | +| `fn` | Function definition or function type | | `code` | Object-level marker | | `let` | Variable binding | | `match` | Pattern matching | @@ -45,6 +45,41 @@ x + y // This is also a comment Identifiers matching `u[0-9]+` are reserved for primitive types. +## Function Types + +Function types use the `fn` keyword with parenthesized parameters: + +``` +fn(u64) -> u64 // non-dependent function type +fn(x: u64) -> u64 // dependent: return type may mention x +fn(A: Type, x: A) -> A // polymorphic: type parameter used in value positions +fn(fn(u64) -> u64) -> u64 // higher-order: function taking a function +``` + +Function types are right-associative: `fn(A) -> fn(B) -> C` means `fn(A) -> (fn(B) -> C)`. + +Multi-parameter function types desugar to nested single-parameter types: + +``` +fn(A: Type, x: A) -> A ≡ fn(A: Type) -> fn(x: A) -> A +``` + +Function types are meta-level only — they inhabit `Type`, not `VmType`. + +## Lambda Expressions + +Lambdas use Rust's closure syntax with mandatory type annotations: + +``` +|x: u64| x + 1 // single parameter +|x: u64, y: u64| x + y // multi-parameter (desugars to nested lambdas) +|f: fn(u64) -> u64, x: u64| f(x) // higher-order +``` + +Type annotations on lambda parameters are required. This makes lambdas inferable — the typechecker can synthesise the full function type from the annotations and the body. + +Lambdas are meta-level only — they cannot appear in object-level (`code fn`) bodies. + ## Operators Lowest to highest, left-associative unless noted: @@ -58,6 +93,8 @@ Lowest to highest, left-associative unless noted: | 5 | `*` `/` | | 6 | `!` (unary) | +Note: `|` as bitwise OR is distinguished from `|` as lambda delimiter by position: a leading `|` in atom position starts a lambda; `|` after an expression is bitwise OR. + Note: The comparison operators are provisional. See [bs/comparison_operators.md](bs/comparison_operators.md) for discussion. ## Grammar (EBNF-like) @@ -87,7 +124,9 @@ expr ::= literal | expr "(" expr ("," expr)* ")" -- application | expr binary_op expr | unary_op expr - | "#(" expr ")" -- quotation + | fn_type -- function type + | lambda -- lambda expression + | "#(" expr ")" -- quotation | "#{" stmt* expr "}" -- block quotation | "$(" expr ")" -- splice | "${" stmt* expr "}" -- block splice @@ -95,6 +134,13 @@ expr ::= literal | "match" expr "{" match_arm* "}" | block +fn_type ::= "fn" "(" fn_params ")" "->" expr +fn_params ::= (fn_param ("," fn_param)*)? +fn_param ::= identifier ":" expr -- dependent: fn(x: A) -> B + | expr -- non-dependent: fn(A) -> B + +lambda ::= "|" param ("," param)* "|" expr + binary_op ::= "+" | "-" | "*" | "/" | "==" | "!=" | "<" | ">" | "<=" | ">=" | "&" | "|" unary_op ::= "!" diff --git a/docs/bs/README.md b/docs/bs/README.md index 4a27f13..2a67da1 100644 --- a/docs/bs/README.md +++ b/docs/bs/README.md @@ -12,6 +12,7 @@ The folder name, `bs`, stands for brainstorming. Obviously. - [functional_goto.md](functional_goto.md) — Control flow via SSA-style basic blocks with goto - [comparison_operators.md](comparison_operators.md) — Boolean vs propositional comparisons - [tuples_and_inference.md](tuples_and_inference.md) — Tuple syntax and type inference +- [pi_types.md](pi_types.md) — Dependent function types (Pi) and lambdas at the meta level ## Compiler Internals diff --git a/docs/bs/pi_types.md b/docs/bs/pi_types.md new file mode 100644 index 0000000..a6de125 --- /dev/null +++ b/docs/bs/pi_types.md @@ -0,0 +1,264 @@ +# Pi Types: Dependent Function Types at the Meta Level + +This document records the design decisions for adding dependent function types (Pi types) and lambda abstractions to Splic at the meta level. + +## Motivation + +The current prototype has no first-class functions. All functions are top-level named definitions; there is no way to pass a function as an argument or return one from another function. This blocks key use cases: + +**Higher-order meta functions.** The `repeat` combinator from `prototype_next.md` requires passing a code-generating function: + +```splic +fn repeat(f: fn([[u64]]) -> [[u64]], n: u64, x: [[u64]]) -> [[u64]] { + match n { + 0 => x, + n => repeat(f, n - 1, f(x)), + } +} + +code fn square_twice(x: u64) -> u64 { + $(repeat(|y: [[u64]]| #($(y) * $(y)), 2, #(x))) +} +``` + +**Polymorphic functions.** Dependent function types let parameters appear in subsequent types: + +```splic +fn id(A: Type, x: A) -> A { x } +``` + +Here `A` is a type passed at compile time, and the return type depends on it. + +**Type-level computation.** With Pi types, function types are first-class terms in the meta universe, enabling functions that compute types. + +## Syntax + +### Function types + +Dependent function types use the `fn` keyword — the same keyword used for definitions: + +``` +fn(x: A) -> B // dependent: B may mention x +fn(A) -> B // non-dependent (sugar for fn(_: A) -> B) +``` + +Right-associative: `fn(A) -> fn(B) -> C` means `fn(A) -> (fn(B) -> C)`. + +Multi-parameter function types desugar to nested Pi: + +``` +fn(x: A, y: B) -> C ≡ fn(x: A) -> fn(y: B) -> C +``` + +**Rationale.** Using `fn` for types mirrors its use for definitions — in Splic, `fn` introduces anything function-shaped. The parenthesized parameter syntax `fn(x: A)` is visually distinct from a definition `fn name(x: A)` (the presence of a name between `fn` and `(` distinguishes them). The `(x: A) -> B` Agda/Lean convention was considered but `fn(x: A) -> B` is more Rust-flavored. + +### Lambdas + +Lambda expressions use Rust's closure syntax: + +``` +|x: A| body // type annotation required +|x: A, y: B| body // multi-parameter (desugars to nested lambdas) +``` + +Type annotations on lambda parameters are **mandatory**. This makes lambdas inferable — the typechecker can construct the full Pi type from the annotation and the inferred body type, without needing an expected type pushed down from context. This is a deliberate simplification for the prototype; unannotated `|x| body` syntax may be added later when check-mode lambdas are needed. + +**Rationale.** The `|...|` syntax is familiar to Rust users. It reuses the existing `|` token. Disambiguation with bitwise OR is positional: `|` at the start of an atom is a lambda; `|` after an expression is bitwise OR. + +### Scope + +Pi types and lambdas are **meta-level only**. Object-level functions remain top-level `code fn` definitions. A lambda cannot appear in object-level code, and `fn(A) -> B` cannot appear as an object-level type. This matches the 2LTT philosophy: the meta level is a rich functional language; the object level is a simple low-level language. + +## Typing Rules + +Pi types inhabit the meta universe (`Type`). The formation, introduction, and elimination rules: + +### Formation (Pi) + +``` +Γ ⊢ A : Type Γ, x : A ⊢ B : Type +────────────────────────────────────── + Γ ⊢ fn(x: A) -> B : Type +``` + +Both `A` and `B` must be types. The parameter `x` is in scope in `B` (dependent case). For non-dependent arrows, `x` does not appear free in `B`. + +### Introduction (Lambda) + +Lambdas are **inferable** because type annotations on parameters are mandatory: + +``` +Γ ⊢ A : Type Γ, x : A ⊢ body ⇒ B +───────────────────────────────────────── + Γ ⊢ |x: A| body ⇒ fn(x: A) -> B +``` + +The parameter type `A` comes from the annotation; the body type `B` is inferred in the extended context. The synthesised type is the Pi type `fn(x: A) -> B`. + +### Elimination (Application) + +Application is inferable when the function is inferable: + +``` +Γ ⊢ f ⇒ fn(x: A) -> B Γ ⊢ arg ⇐ A +───────────────────────────────────────── + Γ ⊢ f(arg) ⇒ B[arg/x] +``` + +The return type `B[arg/x]` is the body type with the argument substituted for the parameter. For non-dependent functions this is just `B`. + +Multi-argument calls desugar to curried application: `f(a, b)` = `f(a)(b)`. + +## Core IR Design + +### New Term variants + +```rust +Pi { param_name: &'a str, param_ty: &'a Term<'a>, body_ty: &'a Term<'a> } +Lam { param_name: &'a str, param_ty: &'a Term<'a>, body: &'a Term<'a> } +FunApp { func: &'a Term<'a>, arg: &'a Term<'a> } +Global(Name<'a>) +PrimApp { prim: Prim, args: &'a [&'a Term<'a>] } +``` + +### Refactoring App/Head + +The current `App { head: Head, args }` where `Head` is `Global(Name) | Prim(Prim)` is replaced by: + +- **`Global(Name)`** — a term representing a reference to a top-level function. Now a first-class term rather than just an application head. +- **`FunApp { func, arg }`** — single-argument curried application. Used for both global and local function calls. Multi-arg calls `foo(a, b)` elaborate to `FunApp(FunApp(Global("foo"), a), b)`. +- **`PrimApp { prim, args }`** — primitive operation application. Kept separate because prims carry resolved `IntType` and are always fully applied. Eventually prims will become regular typed symbols, but the typechecker isn't ready for that yet. + +**`FunSig` is preserved** as a convenience structure in the globals table. It stores the flat parameter list and return type for efficient lookup. A `FunSig::to_pi_type(arena)` method constructs the corresponding nested Pi type when needed (e.g., for `type_of(Global(name))`). + +### Substitution + +Dependent return types require substitution: `B[arg/x]`. Since the core IR uses De Bruijn levels, substitution replaces `Var(lvl)` with the argument term. Levels do not shift, making the implementation straightforward: + +```rust +fn subst<'a>(arena: &'a Bump, term: &'a Term<'a>, lvl: Lvl, replacement: &'a Term<'a>) -> &'a Term<'a> +``` + +### Alpha-equivalence + +The current `PartialEq` on `Term` compares structurally, including `param_name` fields. Two Pi types that differ only in parameter names (`fn(x: A) -> B` vs `fn(y: A) -> B`) should be equal. A dedicated `alpha_eq` function ignores names and compares only structure (De Bruijn levels handle binding correctly). + +## Evaluator Design + +### Closures + +A new `MetaVal` variant captures the environment at lambda creation: + +```rust +VClosure { + param_name: &str, + body: &Term, + env: Vec, + obj_next: Lvl, +} +``` + +This follows the substitution-based approach already in use. Application extends the captured env with the argument value and evaluates the body. + +### Global function references + +When `eval_meta` encounters `Global(name)`, it constructs a closure from the global's body and parameters. When applied via `FunApp`, this closure behaves identically to a lambda — the argument extends the env and the body is evaluated. + +For multi-parameter globals, partial application produces a closure that awaits the remaining arguments. This falls out naturally from curried `FunApp` chains. + +### Pi types in evaluation + +`Pi` terms are type-level and never appear in evaluation position (the typechecker ensures this). They are unreachable in `eval_meta`. + +## Staging Interaction + +### Closures cannot be quoted + +Meta-level closures (`VClosure`) cannot appear in object-level code. The type system prevents this: + +- Pi types inhabit `Type` (meta universe), not `VmType` (object universe) +- Therefore `[[fn(A) -> B]]` is ill-formed — you cannot lift a function type +- Lambdas have Pi types, so they cannot have lifted types, so they cannot be quoted + +This is the correct behavior: closures are compile-time values that are fully evaluated during staging. + +### Code-generating lambdas + +The main staging use case is lambdas that *produce* code: + +```splic +fn repeat(f: fn([[u64]]) -> [[u64]], n: u64, x: [[u64]]) -> [[u64]] { + match n { + 0 => x, + n => repeat(f, n - 1, f(x)), + } +} + +code fn square_twice(x: u64) -> u64 { + $(repeat(|y: [[u64]]| #($(y) * $(y)), 2, #(x))) +} +``` + +Here `f` has type `fn([[u64]]) -> [[u64]]` — it takes object code and returns object code. The lambda `|y| #($(y) * $(y))` is a meta-level function that generates object-level multiplication code. After staging, all meta computation (including the lambda and the `repeat` recursion) is erased: + +```splic +code fn square_twice(x: u64) -> u64 { + (x * x) * (x * x) +} +``` + +### Object-level FunApp/Global + +`FunApp` and `Global` can appear in object-level terms for object-level function calls. The unstager passes them through structurally (copying to the output arena), just as it does for the current `App { head: Global }`. + +## Examples + +### Polymorphic identity + +```splic +fn id(A: Type, x: A) -> A { x } +fn use_id() -> u64 { id(u64, 42) } +``` + +### Const combinator + +```splic +fn const_(A: Type, B: Type) -> fn(A) -> fn(B) -> A { + |a: A| |b: B| a +} +``` + +### Function composition + +```splic +fn compose(A: Type, B: Type, C: Type, f: fn(B) -> C, g: fn(A) -> B) -> fn(A) -> C { + |x: A| f(g(x)) +} +``` + +### Higher-order staging + +```splic +fn map_code(f: fn([[u64]]) -> [[u64]], x: [[u64]]) -> [[u64]] { + f(x) +} + +code fn double(x: u64) -> u64 { + $(map_code(|y: [[u64]]| #($(y) + $(y)), #(x))) +} +// Stages to: code fn double(x: u64) -> u64 { x + x } +``` + +## Future Work + +- **Prims as typed symbols**: Currently prims are special-cased with `PrimApp`. Eventually they should have types (polymorphic in width/phase) and be typechecked uniformly. +- **Object-level closures**: The closure-free approach from Kovács 2024 avoids runtime closures while still supporting higher-order object code. +- **Implicit arguments**: `fn {A: Type}(x: A) -> A` with unification to infer `A` at call sites. +- **Spine-based evaluation**: Replace substitution-based closures with lazy spines before adding full dependent elimination. + +## References + +- Kovács 2022: Staged Compilation with Two-Level Type Theory (ICFP) +- Kovács 2024: Closure-Free Functional Programming in a Two-Level Type Theory (ICFP) +- [prototype_eval.md](prototype_eval.md): Evaluator design and progression plan +- [prototype_next.md](prototype_next.md): Roadmap (Phase 2: Meta-level Functions, Phase 3: Dependent Types) From f8478d9d77270bc67d31f68c3b51e03866db8666 Mon Sep 17 00:00:00 2001 From: LukasK Date: Tue, 24 Mar 2026 11:25:52 +0000 Subject: [PATCH 02/43] test: add snapshot test inputs for Pi types and lambdas 15 test programs exercising dependent function types and lambdas: - Positive: pi_basic, pi_lambda_arg, pi_polymorphic_id, pi_const, pi_compose, pi_polycompose, pi_nested, pi_dependent_ret, pi_repeat, pi_staging_hof - Error: pi_apply_non_fn, pi_lambda_type_mismatch, pi_lambda_in_object, pi_arity_mismatch, pi_lambda_missing_annotation Snapshot outputs will be generated once the implementation lands. Co-Authored-By: Claude Opus 4.6 --- .../tests/snap/full/pi_apply_non_fn/0_input.splic | 5 +++++ .../tests/snap/full/pi_arity_mismatch/0_input.splic | 4 ++++ compiler/tests/snap/full/pi_basic/0_input.splic | 12 ++++++++++++ compiler/tests/snap/full/pi_compose/0_input.splic | 13 +++++++++++++ compiler/tests/snap/full/pi_const/0_input.splic | 10 ++++++++++ .../tests/snap/full/pi_dependent_ret/0_input.splic | 10 ++++++++++ .../tests/snap/full/pi_lambda_arg/0_input.splic | 10 ++++++++++ .../snap/full/pi_lambda_in_object/0_input.splic | 4 ++++ .../full/pi_lambda_missing_annotation/0_input.splic | 8 ++++++++ .../snap/full/pi_lambda_type_mismatch/0_input.splic | 8 ++++++++ compiler/tests/snap/full/pi_nested/0_input.splic | 12 ++++++++++++ .../tests/snap/full/pi_polycompose/0_input.splic | 13 +++++++++++++ .../tests/snap/full/pi_polymorphic_id/0_input.splic | 8 ++++++++ compiler/tests/snap/full/pi_repeat/0_input.splic | 11 +++++++++++ .../tests/snap/full/pi_staging_hof/0_input.splic | 8 ++++++++ 15 files changed, 136 insertions(+) create mode 100644 compiler/tests/snap/full/pi_apply_non_fn/0_input.splic create mode 100644 compiler/tests/snap/full/pi_arity_mismatch/0_input.splic create mode 100644 compiler/tests/snap/full/pi_basic/0_input.splic create mode 100644 compiler/tests/snap/full/pi_compose/0_input.splic create mode 100644 compiler/tests/snap/full/pi_const/0_input.splic create mode 100644 compiler/tests/snap/full/pi_dependent_ret/0_input.splic create mode 100644 compiler/tests/snap/full/pi_lambda_arg/0_input.splic create mode 100644 compiler/tests/snap/full/pi_lambda_in_object/0_input.splic create mode 100644 compiler/tests/snap/full/pi_lambda_missing_annotation/0_input.splic create mode 100644 compiler/tests/snap/full/pi_lambda_type_mismatch/0_input.splic create mode 100644 compiler/tests/snap/full/pi_nested/0_input.splic create mode 100644 compiler/tests/snap/full/pi_polycompose/0_input.splic create mode 100644 compiler/tests/snap/full/pi_polymorphic_id/0_input.splic create mode 100644 compiler/tests/snap/full/pi_repeat/0_input.splic create mode 100644 compiler/tests/snap/full/pi_staging_hof/0_input.splic diff --git a/compiler/tests/snap/full/pi_apply_non_fn/0_input.splic b/compiler/tests/snap/full/pi_apply_non_fn/0_input.splic new file mode 100644 index 0000000..a45916a --- /dev/null +++ b/compiler/tests/snap/full/pi_apply_non_fn/0_input.splic @@ -0,0 +1,5 @@ +// ERROR: applying a non-function value +fn test() -> u64 { + let x: u64 = 42; + x(1) +} diff --git a/compiler/tests/snap/full/pi_arity_mismatch/0_input.splic b/compiler/tests/snap/full/pi_arity_mismatch/0_input.splic new file mode 100644 index 0000000..fc7647d --- /dev/null +++ b/compiler/tests/snap/full/pi_arity_mismatch/0_input.splic @@ -0,0 +1,4 @@ +// ERROR: too many arguments to a function-typed variable +fn apply(f: fn(u64) -> u64, x: u64) -> u64 { + f(x, x) +} diff --git a/compiler/tests/snap/full/pi_basic/0_input.splic b/compiler/tests/snap/full/pi_basic/0_input.splic new file mode 100644 index 0000000..1b3aae6 --- /dev/null +++ b/compiler/tests/snap/full/pi_basic/0_input.splic @@ -0,0 +1,12 @@ +// Higher-order function: pass a function as an argument +fn apply(f: fn(u64) -> u64, x: u64) -> u64 { + f(x) +} + +fn inc(x: u64) -> u64 { x + 1 } + +fn test() -> u64 { + apply(inc, 42) +} + +code fn result() -> u64 { $(test()) } diff --git a/compiler/tests/snap/full/pi_compose/0_input.splic b/compiler/tests/snap/full/pi_compose/0_input.splic new file mode 100644 index 0000000..48d7c45 --- /dev/null +++ b/compiler/tests/snap/full/pi_compose/0_input.splic @@ -0,0 +1,13 @@ +// Function composition (monomorphic) +fn compose(f: fn(u64) -> u64, g: fn(u64) -> u64) -> fn(u64) -> u64 { + |x: u64| f(g(x)) +} + +fn double(x: u64) -> u64 { x + x } +fn inc(x: u64) -> u64 { x + 1 } + +fn test() -> u64 { + compose(double, inc)(5) +} + +code fn result() -> u64 { $(test()) } diff --git a/compiler/tests/snap/full/pi_const/0_input.splic b/compiler/tests/snap/full/pi_const/0_input.splic new file mode 100644 index 0000000..2b58331 --- /dev/null +++ b/compiler/tests/snap/full/pi_const/0_input.splic @@ -0,0 +1,10 @@ +// Const combinator: returns a function that ignores its argument +fn const_(A: Type, B: Type) -> fn(A) -> fn(B) -> A { + |a: A| |b: B| a +} + +fn test() -> u64 { + const_(u64, u8)(42)(7) +} + +code fn result() -> u64 { $(test()) } diff --git a/compiler/tests/snap/full/pi_dependent_ret/0_input.splic b/compiler/tests/snap/full/pi_dependent_ret/0_input.splic new file mode 100644 index 0000000..b9b4069 --- /dev/null +++ b/compiler/tests/snap/full/pi_dependent_ret/0_input.splic @@ -0,0 +1,10 @@ +// Return type depends on a type argument +fn default(A: Type) -> A { + 0 +} + +fn test_u64() -> u64 { default(u64) } +fn test_u8() -> u8 { default(u8) } + +code fn result_u64() -> u64 { $(test_u64()) } +code fn result_u8() -> u8 { $(test_u8()) } diff --git a/compiler/tests/snap/full/pi_lambda_arg/0_input.splic b/compiler/tests/snap/full/pi_lambda_arg/0_input.splic new file mode 100644 index 0000000..7a47a5c --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_arg/0_input.splic @@ -0,0 +1,10 @@ +// Pass a lambda as an argument to a higher-order function +fn apply(f: fn(u64) -> u64, x: u64) -> u64 { + f(x) +} + +fn test() -> u64 { + apply(|x: u64| x + 1, 42) +} + +code fn result() -> u64 { $(test()) } diff --git a/compiler/tests/snap/full/pi_lambda_in_object/0_input.splic b/compiler/tests/snap/full/pi_lambda_in_object/0_input.splic new file mode 100644 index 0000000..69981b4 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_in_object/0_input.splic @@ -0,0 +1,4 @@ +// ERROR: lambda in object-level function body (meta-level only) +code fn test(x: u64) -> u64 { + |y: u64| y +} diff --git a/compiler/tests/snap/full/pi_lambda_missing_annotation/0_input.splic b/compiler/tests/snap/full/pi_lambda_missing_annotation/0_input.splic new file mode 100644 index 0000000..4178535 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_missing_annotation/0_input.splic @@ -0,0 +1,8 @@ +// ERROR: lambda without type annotation should fail +fn apply(f: fn(u64) -> u64, x: u64) -> u64 { + f(x) +} + +fn test() -> u64 { + apply(|x| x + 1, 42) +} diff --git a/compiler/tests/snap/full/pi_lambda_type_mismatch/0_input.splic b/compiler/tests/snap/full/pi_lambda_type_mismatch/0_input.splic new file mode 100644 index 0000000..21aff91 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_type_mismatch/0_input.splic @@ -0,0 +1,8 @@ +// ERROR: lambda parameter type doesn't match expected function type +fn apply(f: fn(u64) -> u64, x: u64) -> u64 { + f(x) +} + +fn test() -> u64 { + apply(|x: u32| x, 42) +} diff --git a/compiler/tests/snap/full/pi_nested/0_input.splic b/compiler/tests/snap/full/pi_nested/0_input.splic new file mode 100644 index 0000000..b9e1877 --- /dev/null +++ b/compiler/tests/snap/full/pi_nested/0_input.splic @@ -0,0 +1,12 @@ +// Nested function types: function that takes a function and applies it twice +fn apply_twice(f: fn(u64) -> u64, x: u64) -> u64 { + f(f(x)) +} + +fn inc(x: u64) -> u64 { x + 1 } + +fn test() -> u64 { + apply_twice(inc, 0) +} + +code fn result() -> u64 { $(test()) } diff --git a/compiler/tests/snap/full/pi_polycompose/0_input.splic b/compiler/tests/snap/full/pi_polycompose/0_input.splic new file mode 100644 index 0000000..05e24e3 --- /dev/null +++ b/compiler/tests/snap/full/pi_polycompose/0_input.splic @@ -0,0 +1,13 @@ +// Polymorphic function composition +fn compose(A: Type, B: Type, C: Type, f: fn(B) -> C, g: fn(A) -> B) -> fn(A) -> C { + |x: A| f(g(x)) +} + +fn double(x: u64) -> u64 { x + x } +fn to_u8(x: u64) -> u8 { x } + +fn test() -> u8 { + compose(u64, u64, u8, to_u8, double)(5) +} + +code fn result() -> u8 { $(test()) } diff --git a/compiler/tests/snap/full/pi_polymorphic_id/0_input.splic b/compiler/tests/snap/full/pi_polymorphic_id/0_input.splic new file mode 100644 index 0000000..352f9e9 --- /dev/null +++ b/compiler/tests/snap/full/pi_polymorphic_id/0_input.splic @@ -0,0 +1,8 @@ +// Polymorphic identity function using dependent types +fn id(A: Type, x: A) -> A { x } + +fn test_u64() -> u64 { id(u64, 42) } +fn test_u8() -> u8 { id(u8, 7) } + +code fn result_u64() -> u64 { $(test_u64()) } +code fn result_u8() -> u8 { $(test_u8()) } diff --git a/compiler/tests/snap/full/pi_repeat/0_input.splic b/compiler/tests/snap/full/pi_repeat/0_input.splic new file mode 100644 index 0000000..a31c2af --- /dev/null +++ b/compiler/tests/snap/full/pi_repeat/0_input.splic @@ -0,0 +1,11 @@ +// The motivating example: pass a code-generating lambda, unroll at compile time +fn repeat(f: fn([[u64]]) -> [[u64]], n: u64, x: [[u64]]) -> [[u64]] { + match n { + 0 => x, + n => repeat(f, n - 1, f(x)), + } +} + +code fn square_twice(x: u64) -> u64 { + $(repeat(|y: [[u64]]| #($(y) * $(y)), 2, #(x))) +} diff --git a/compiler/tests/snap/full/pi_staging_hof/0_input.splic b/compiler/tests/snap/full/pi_staging_hof/0_input.splic new file mode 100644 index 0000000..06b7c9e --- /dev/null +++ b/compiler/tests/snap/full/pi_staging_hof/0_input.splic @@ -0,0 +1,8 @@ +// Higher-order staging: meta function that transforms code via a lambda +fn map_code(f: fn([[u64]]) -> [[u64]], x: [[u64]]) -> [[u64]] { + f(x) +} + +code fn double(x: u64) -> u64 { + $(map_code(|y: [[u64]]| #($(y) + $(y)), #(x))) +} From 4f46858afa64d0020b6f5d18fd5c1e4e71355048 Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 07:34:47 +0000 Subject: [PATCH 03/43] feat: pi types initial impl --- cli/src/main.rs | 6 +- compiler/src/checker/mod.rs | 534 ++++++++++-------- compiler/src/checker/test/apply.rs | 18 +- compiler/src/checker/test/context.rs | 40 +- compiler/src/checker/test/mod.rs | 2 +- compiler/src/core/mod.rs | 102 ++-- compiler/src/core/pretty.rs | 120 ++-- compiler/src/eval/mod.rs | 401 ++++++------- compiler/src/parser/ast.rs | 10 + compiler/src/parser/mod.rs | 45 ++ .../snap/full/pi_arity_mismatch/0_input.splic | 2 +- .../tests/snap/full/pi_basic/0_input.splic | 2 +- .../tests/snap/full/pi_compose/0_input.splic | 2 +- .../tests/snap/full/pi_const/0_input.splic | 2 +- .../snap/full/pi_dependent_ret/0_input.splic | 11 +- .../snap/full/pi_lambda_arg/0_input.splic | 2 +- .../0_input.splic | 2 +- .../pi_lambda_type_mismatch/0_input.splic | 2 +- .../tests/snap/full/pi_nested/0_input.splic | 2 +- .../snap/full/pi_polycompose/0_input.splic | 2 +- .../tests/snap/full/pi_repeat/0_input.splic | 2 +- .../snap/full/pi_staging_hof/0_input.splic | 2 +- .../snap/full/splice_meta_int/3_check.txt | 2 +- compiler/tests/snap/full/staging/3_check.txt | 2 +- .../stage_error/add_overflow_u32/3_check.txt | 2 +- .../stage_error/add_overflow_u8/3_check.txt | 2 +- .../stage_error/mul_overflow_u8/3_check.txt | 2 +- .../stage_error/sub_underflow_u8/3_check.txt | 2 +- 28 files changed, 750 insertions(+), 573 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index acf4dfb..109db0f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -49,11 +49,9 @@ fn stage(file: &PathBuf) -> Result<()> { checker::elaborate_program(&core_arena, &program).context("failed to elaborate program")?; drop(src_arena); - // Unstage into out_arena; src_arena and core_arena are no longer needed. - let out_arena = bumpalo::Bump::new(); + // Unstage: the core_arena must remain alive since closures reference core terms. let staged = - eval::unstage_program(&out_arena, &core_program).context("failed to stage program")?; - drop(core_arena); + eval::unstage_program(&core_arena, &core_program).context("failed to stage program")?; // Print the staged result, then out_arena is dropped at end of scope. println!("{staged}"); diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index dd36320..bfb0e20 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use anyhow::{Context as _, Result, anyhow, bail, ensure}; -use crate::core::{self, IntType, IntWidth, Lvl, Prim}; +use crate::core::{self, FunApp, IntType, IntWidth, Lam, Lvl, Pi, Prim, alpha_eq, subst}; use crate::parser::ast::{self, Phase}; /// Elaboration context. @@ -127,37 +127,68 @@ impl<'core, 'globals> Ctx<'core, 'globals> { self.alloc(core::Term::Lift(core::Term::int_ty(*w, Phase::Object))) } - // Application: return type comes from the head. - core::Term::App(app) => match &app.head { - core::Head::Global(name) => { - self.globals - .get(name) - .expect("App/Global with unknown name (typechecker invariant)") - .ret_ty + // Global reference: type is the Pi type of the signature. + core::Term::Global(name) => { + let sig = self + .globals + .get(name) + .expect("Global with unknown name (typechecker invariant)"); + sig.to_pi_type(self.arena) + } + + // PrimApp: return type comes from the prim. + core::Term::PrimApp(app) => match app.prim { + Prim::Add(it) + | Prim::Sub(it) + | Prim::Mul(it) + | Prim::Div(it) + | Prim::BitAnd(it) + | Prim::BitOr(it) + | Prim::BitNot(it) => core::Term::int_ty(it.width, it.phase), + Prim::Eq(it) + | Prim::Ne(it) + | Prim::Lt(it) + | Prim::Gt(it) + | Prim::Le(it) + | Prim::Ge(it) => core::Term::u1_ty(it.phase), + Prim::Embed(w) => { + self.alloc(core::Term::Lift(core::Term::int_ty(w, Phase::Object))) + } + Prim::IntTy(_) | Prim::U(_) => { + unreachable!("type-level prim in PrimApp (typechecker invariant)") } - core::Head::Prim(p) => match *p { - Prim::Add(it) - | Prim::Sub(it) - | Prim::Mul(it) - | Prim::Div(it) - | Prim::BitAnd(it) - | Prim::BitOr(it) - | Prim::BitNot(it) => core::Term::int_ty(it.width, it.phase), - Prim::Eq(it) - | Prim::Ne(it) - | Prim::Lt(it) - | Prim::Gt(it) - | Prim::Le(it) - | Prim::Ge(it) => core::Term::u1_ty(it.phase), - Prim::Embed(w) => { - self.alloc(core::Term::Lift(core::Term::int_ty(w, Phase::Object))) - } - Prim::IntTy(_) | Prim::U(_) => { - unreachable!("type-level prim in App head (typechecker invariant)") - } - }, }, + // Pi type inhabits Type + core::Term::Pi(_) => &core::Term::TYPE, + + // Lam: synthesise Pi from param_ty and body type + core::Term::Lam(lam) => { + self.push_local(lam.param_name, lam.param_ty); + let body_ty = self.type_of(lam.body); + self.pop_local(); + self.alloc(core::Term::Pi(Pi { + param_name: lam.param_name, + param_ty: lam.param_ty, + body_ty, + })) + } + + // FunApp: get func type (must be Pi), return body_ty with substitution. + // The Pi binder level equals the number of FunApp layers already applied to the + // root callee — global signatures are elaborated in an empty context so the i-th + // binder is at level i. + core::Term::FunApp(app) => { + let func_ty = self.type_of(app.func); + match func_ty { + core::Term::Pi(pi) => { + let binder_lvl = Lvl(funapp_depth(app.func)); + subst(self.arena, pi.body_ty, binder_lvl, app.arg) + } + _ => unreachable!("FunApp func must have Pi type (typechecker invariant)"), + } + } + // #(t) : [[type_of(t)]] core::Term::Quote(inner) => { let inner_ty = self.type_of(inner); @@ -169,16 +200,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { let inner_ty = self.type_of(inner); match inner_ty { core::Term::Lift(object_ty) => object_ty, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::App(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => { - unreachable!("Splice inner must have Lift type (typechecker invariant)") - } + _ => unreachable!("Splice inner must have Lift type (typechecker invariant)"), } } @@ -237,6 +259,7 @@ fn elaborate_sig<'src, 'core>( arena.alloc_slice_try_fill_iter(func.params.iter().map(|p| -> Result<_> { let param_name: &'core str = arena.alloc_str(p.name.as_str()); let param_ty = infer(&mut ctx, func.phase, p.ty)?; + ctx.push_local(param_name, param_ty); Ok((param_name, param_ty)) }))?; @@ -329,26 +352,43 @@ pub fn elaborate_program<'core>( /// - `U(Meta)` (Type) inhabits `U(Meta)` (type-in-type for the meta universe) /// - `U(Object)` (`VmType`) inhabits `U(Meta)` (the meta universe classifies object types) /// - `Lift(_)` inhabits `U(Meta)` -const fn type_universe(ty: &core::Term<'_>) -> Option { +/// - `Pi` inhabits `U(Meta)` (function types are meta-level) +/// - `Var(lvl)` — look up the variable's type in `locals`; if it is `U(p)`, it is a type in `p` +fn type_universe<'core>( + ty: &core::Term<'_>, + locals: &[(&'core str, &'core core::Term<'core>)], +) -> Option { match ty { core::Term::Prim(Prim::IntTy(IntType { phase, .. })) => Some(*phase), - core::Term::Prim(Prim::U(_)) | core::Term::Lift(_) => Some(Phase::Meta), - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::App { .. } - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let { .. } - | core::Term::Match { .. } => None, + core::Term::Prim(Prim::U(_)) | core::Term::Lift(_) | core::Term::Pi(_) => Some(Phase::Meta), + // A type variable: its universe is determined by what universe its type inhabits. + // E.g. if `A : Type` (= U(Meta)), then A is a meta-level type. + core::Term::Var(lvl) => match locals.get(lvl.0)?.1 { + core::Term::Prim(Prim::U(phase)) => Some(*phase), + _ => None, + }, + _ => None, } } -/// Structural equality of core types (no normalisation needed for this prototype). +/// Type equality: alpha-equality (ignores param names in Pi/Lam). fn types_equal(a: &core::Term<'_>, b: &core::Term<'_>) -> bool { - // Uses pointer equality as a fast path — terms allocated from the same arena - // slot are guaranteed identical without recursion. - std::ptr::eq(a, b) || a == b + alpha_eq(a, b) +} + +/// Count the number of `FunApp` layers between the outermost callee and the given term. +/// +/// Used to determine which Pi binder level to target during dependent-return-type +/// substitution: global function signatures are elaborated in an empty context, so the +/// binder introduced by the i-th Pi in the chain sits at De Bruijn level i. +const fn funapp_depth(term: &core::Term<'_>) -> usize { + let mut depth = 0; + let mut cur = term; + while let core::Term::FunApp(app) = cur { + depth += 1; + cur = app.func; + } + depth } /// Synthesise and return the elaborated core term; recover its type via `ctx.type_of`. @@ -374,11 +414,16 @@ pub fn infer<'src, 'core>( } return Ok(term); } - // Otherwise look in locals. - let (lvl, _) = ctx - .lookup_local(name_str) - .ok_or_else(|| anyhow!("unbound variable `{name_str}`"))?; - Ok(ctx.alloc(core::Term::Var(lvl))) + // Check locals. + if let Some((lvl, _)) = ctx.lookup_local(name_str) { + return Ok(ctx.alloc(core::Term::Var(lvl))); + } + // Check globals — bare reference without call, produces Global term. + let core_name = core::Name::new(ctx.arena.alloc_str(name_str)); + if ctx.globals.contains_key(&core_name) { + return Ok(ctx.alloc(core::Term::Global(core_name))); + } + Err(anyhow!("unbound variable `{name_str}`")) } // ------------------------------------------------------------------ Lit @@ -387,53 +432,59 @@ pub fn infer<'src, 'core>( "cannot infer type of a literal; add a type annotation" )), - // ------------------------------------------------------------------ App { Global } - // Look up the callee in globals, check each argument, return the return type. + // ------------------------------------------------------------------ App { Global or local } + // Function calls: look up callee, elaborate as curried FunApp chain. ast::Term::App { func: ast::FunName::Name(name), args, } => { - let sig = ctx - .globals - .get(name) - .ok_or_else(|| anyhow!("unknown function `{name}`"))?; - - // The call phase must match the current elaboration phase. - let call_phase = sig.phase; - ensure!( - call_phase == phase, - "function `{name}` is a {call_phase}-phase function, but called in {phase}-phase context" - ); - let params = sig.params; + // Elaborate the callee + let callee = infer(ctx, phase, &ast::Term::Var(*name))?; + let mut callee_ty = ctx.type_of(callee); + + // For globals, verify phase matches. + if let core::Term::Global(gname) = callee { + let sig = ctx + .globals + .get(gname) + .expect("Global must be in globals table"); + ensure!( + sig.phase == phase, + "function `{name}` is a {}-phase function, but called in {phase}-phase context", + sig.phase + ); + } - ensure!( - args.len() == params.len(), - "function `{name}` expects {} argument(s), got {}", - params.len(), - args.len() - ); + // Build curried FunApp chain, checking each arg against the Pi param type. + let mut result: &'core core::Term<'core> = callee; + for (i, arg) in args.iter().enumerate() { + let pi = match callee_ty { + core::Term::Pi(pi) => pi, + _ => bail!( + "too many arguments: function `{name}` expects {i} argument(s), got {}", + args.len() + ), + }; + + let core_arg = check(ctx, phase, arg, pi.param_ty) + .with_context(|| format!("in argument {i} of call to `{name}`"))?; + + // The return type may depend on the argument (dependent types). + // Global function signatures are elaborated in an empty context, + // so the i-th Pi binder corresponds to De Bruijn level i. + callee_ty = subst(ctx.arena, pi.body_ty, Lvl(i), core_arg); + + result = ctx.alloc(core::Term::FunApp(FunApp { + func: result, + arg: core_arg, + })); + } - // Check each argument against its declared parameter type. - let core_args: &'core [&'core core::Term<'core>] = ctx - .arena - .alloc_slice_try_fill_iter(args.iter().zip(params.iter()).map( - |(arg, (pname, pty))| -> Result<_> { - let core_arg = check(ctx, call_phase, arg, pty) - .with_context(|| format!("in call to '{name}' argument '{pname}'"))?; - Ok(core_arg) - }, - ))?; - - Ok(ctx.alloc(core::Term::new_app( - core::Head::Global(core::Name::new(ctx.arena.alloc_str(name.as_str()))), - core_args, - ))) + Ok(result) } // ------------------------------------------------------------------ App { Prim (BinOp/UnOp) } - // Arithmetic/bitwise ops are check-only (width comes from expected type). - // Comparison ops are inferable: they always return u1, and the operand type - // is inferred from the first argument (the second is checked to match). + // Comparison ops are inferable: they always return u1. ast::Term::App { func: ast::FunName::BinOp(op), args, @@ -452,25 +503,13 @@ pub fn infer<'src, 'core>( bail!("binary operation expects exactly 2 arguments") }; - // Infer the operand type from the first argument. let core_arg0 = infer(ctx, phase, lhs)?; let operand_ty = ctx.type_of(core_arg0); - // Check the second argument against the same operand type. let core_arg1 = check(ctx, phase, rhs, operand_ty)?; - // Verify both operands are integers and build the prim carrying the operand type. let op_int_ty = match operand_ty { core::Term::Prim(Prim::IntTy(it)) => *it, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::App(_) - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => { - ensure!(false, "comparison operands must be integers"); - unreachable!() + _ => { + bail!("comparison operands must be integers"); } }; let prim = match op { @@ -488,7 +527,7 @@ pub fn infer<'src, 'core>( | BinOp::BitOr => unreachable!(), }; let core_args = ctx.alloc_slice([core_arg0, core_arg1]); - Ok(ctx.alloc(core::Term::new_app(core::Head::Prim(prim), core_args))) + Ok(ctx.alloc(core::Term::new_prim_app(prim, core_args))) } ast::Term::App { func: ast::FunName::BinOp(_) | ast::FunName::UnOp(_), @@ -497,17 +536,95 @@ pub fn infer<'src, 'core>( "cannot infer type of a primitive operation; add a type annotation" )), + // ------------------------------------------------------------------ Pi + // Function type expression: elaborate each param type, push locals, elaborate body type. + ast::Term::Pi { params, ret_ty } => { + ensure!( + phase == Phase::Meta, + "function types are only valid in meta-phase context" + ); + let depth_before = ctx.depth(); + + // Elaborate param types and push locals. + let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); + for p in *params { + let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); + let param_ty = infer(ctx, Phase::Meta, p.ty)?; + ensure!( + type_universe(param_ty, &ctx.locals).is_some(), + "parameter type must be a type" + ); + elaborated_params.push((param_name, param_ty)); + ctx.push_local(param_name, param_ty); + } + + let core_ret_ty = infer(ctx, Phase::Meta, ret_ty)?; + ensure!( + type_universe(core_ret_ty, &ctx.locals).is_some(), + "return type must be a type" + ); + + // Build nested Pi from inside out. + let mut result: &'core core::Term<'core> = core_ret_ty; + for &(param_name, param_ty) in elaborated_params.iter().rev() { + ctx.pop_local(); + result = ctx.alloc(core::Term::Pi(Pi { + param_name, + param_ty, + body_ty: result, + })); + } + + assert_eq!(ctx.depth(), depth_before, "Pi elaboration leaked locals"); + Ok(result) + } + + // ------------------------------------------------------------------ Lam + // Lambda with mandatory type annotations — inferable. + ast::Term::Lam { params, body } => { + ensure!( + phase == Phase::Meta, + "lambdas are only valid in meta-phase context" + ); + ensure!( + !params.is_empty(), + "lambda must have at least one parameter" + ); + + let depth_before = ctx.depth(); + let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); + + for p in *params { + let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); + let param_ty = infer(ctx, Phase::Meta, p.ty)?; + elaborated_params.push((param_name, param_ty)); + ctx.push_local(param_name, param_ty); + } + + let core_body = infer(ctx, phase, body)?; + + // Build nested Lam from inside out. + let mut result: &'core core::Term<'core> = core_body; + for &(param_name, param_ty) in elaborated_params.iter().rev() { + ctx.pop_local(); + result = ctx.alloc(core::Term::Lam(Lam { + param_name, + param_ty, + body: result, + })); + } + + assert_eq!(ctx.depth(), depth_before, "Lam elaboration leaked locals"); + Ok(result) + } + // ------------------------------------------------------------------ Lift - // `[[T]]` — elaborate T at the object phase, type is Type (meta universe). ast::Term::Lift(inner) => { - // Lift is only legal in meta phase. ensure!( phase == Phase::Meta, "`[[...]]` is only valid in a meta-phase context" ); - // The inner expression must be an object type. let core_inner = infer(ctx, Phase::Object, inner)?; - // Verify the inner term is indeed a type (inhabits VmType). ensure!( types_equal(ctx.type_of(core_inner), &core::Term::VM_TYPE), "argument of `[[...]]` must be an object type" @@ -516,9 +633,7 @@ pub fn infer<'src, 'core>( } // ------------------------------------------------------------------ Quote - // `#(t)` — infer iff the inner term is inferable (phase shifts meta→object). ast::Term::Quote(inner) => { - // Quote is only legal in meta phase. ensure!( phase == Phase::Meta, "`#(...)` is only valid in a meta-phase context" @@ -528,11 +643,7 @@ pub fn infer<'src, 'core>( } // ------------------------------------------------------------------ Splice - // `$(t)` — infer iff `t` infers as `[[T]]`; result type is `T` (phase shifts object→meta). - // If `t` infers as a meta integer `IntTy(w, Meta)`, insert an implicit `Embed(w)` - // to produce `[[IntTy(w, Object)]]` before splicing. ast::Term::Splice(inner) => { - // Splice is only legal in object phase. ensure!( phase == Phase::Object, "`$(...)` is only valid in an object-phase context" @@ -541,44 +652,31 @@ pub fn infer<'src, 'core>( let inner_ty = ctx.type_of(core_inner); match inner_ty { core::Term::Lift(_) => Ok(ctx.alloc(core::Term::Splice(core_inner))), - // A meta-level integer is implicitly embedded: insert Embed(w) so that - // the splice argument has type `[[IntTy(w, Object)]]`. core::Term::Prim(Prim::IntTy(IntType { width, phase: Phase::Meta, })) => { - let embedded = ctx.alloc(core::Term::new_app( - core::Head::Prim(Prim::Embed(*width)), + let embedded = ctx.alloc(core::Term::new_prim_app( + Prim::Embed(*width), ctx.alloc_slice([core_inner]), )); Ok(ctx.alloc(core::Term::Splice(embedded))) } - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::App(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => Err(anyhow!( + _ => Err(anyhow!( "argument of `$(...)` must have a lifted type `[[T]]` or be a meta-level integer" )), } } // ------------------------------------------------------------------ Block (Let*) - // Elaborate each `let` binding in sequence, then the trailing expression. ast::Term::Block { stmts, expr } => { let depth_before = ctx.depth(); let result = infer_block(ctx, phase, stmts, expr); - // Each let-binding is responsible for pushing and popping its own local - // (via `elaborate_let`), so the depth must be restored exactly. assert_eq!(ctx.depth(), depth_before, "infer_block leaked locals"); result } // ------------------------------------------------------------------ Match - // Without an expected type, match is not inferable — require an annotation. ast::Term::Match { .. } => Err(anyhow!( "cannot infer type of match expression; add a type annotation or use in a \ checked position" @@ -587,14 +685,7 @@ pub fn infer<'src, 'core>( } /// Check exhaustiveness of `arms` given the scrutinee type `scrut_ty`. -/// -/// Returns `Err` if coverage cannot be established. fn check_exhaustiveness(scrut_ty: &core::Term<'_>, arms: &[ast::MatchArm<'_>]) -> Result<()> { - // For u0/u1/u8 scrutinees we track which literal values have been covered - // using a Vec of length 1/2/256 respectively. If all entries become - // true the match is exhaustive even without a wildcard. For any other type - // (u16/u32/u64) we only accept a wildcard or bind-all arm as evidence of - // exhaustiveness, since enumerating every value is impractical. let mut covered_lits: Option> = match scrut_ty { core::Term::Prim(Prim::IntTy(ty)) => match ty.width { IntWidth::U0 => Some(vec![false; 1]), @@ -602,15 +693,7 @@ fn check_exhaustiveness(scrut_ty: &core::Term<'_>, arms: &[ast::MatchArm<'_>]) - IntWidth::U8 => Some(vec![false; 256]), IntWidth::U16 | IntWidth::U32 | IntWidth::U64 => None, }, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::App { .. } - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let { .. } - | core::Term::Match { .. } => None, + _ => None, }; let mut has_catch_all = false; @@ -639,7 +722,6 @@ fn check_exhaustiveness(scrut_ty: &core::Term<'_>, arms: &[ast::MatchArm<'_>]) - } /// Elaborate a match pattern into a core pattern. -/// Any bound name can be recovered via `core::Pat::bound_name()`. fn elaborate_pat<'core>(ctx: &Ctx<'core, '_>, pat: &ast::Pat<'_>) -> core::Pat<'core> { match pat { ast::Pat::Lit(n) => core::Pat::Lit(*n), @@ -655,15 +737,7 @@ fn elaborate_pat<'core>(ctx: &Ctx<'core, '_>, pat: &ast::Pat<'_>) -> core::Pat<' } } -/// Elaborate a single `let` binding: resolve the binding type, elaborate the -/// initialiser, push the local into the context, call `cont`, then pop and -/// assemble `core::Term::Let`. -/// -/// `cont` receives the extended context and returns any result `T`. A -/// `body_of` accessor is used to extract the body term (needed to build the -/// `Let` node) from `T`, and a `wrap` function replaces the body in `T` with -/// the finished `Let` node — letting the caller thread arbitrary extra data -/// (e.g. the inferred type) through without any dummy pairs. +/// Elaborate a single `let` binding. fn elaborate_let<'src, 'core, T, F, G, W>( ctx: &mut Ctx<'core, '_>, phase: Phase, @@ -677,7 +751,6 @@ where G: FnOnce(&T) -> &'core core::Term<'core>, W: FnOnce(&'core core::Term<'core>, T) -> T, { - // Determine the binding type: use annotation if present, otherwise infer. let (core_expr, bind_ty) = if let Some(ann) = stmt.ty { let ty = infer(ctx, phase, ann)?; let core_e = check(ctx, phase, stmt.expr, ty) @@ -752,9 +825,7 @@ pub fn check<'src, 'core>( expected: &'core core::Term<'core>, ) -> Result<&'core core::Term<'core>> { // Verify `expected` inhabits the correct universe for the current phase. - // Every `expected` originates from `elaborate_ty` or from `infer`, both of which - // only produce `IntTy`, `U`, or `Lift` — so `None` here is an internal compiler bug. - let ty_phase = type_universe(expected) + let ty_phase = type_universe(expected, &ctx.locals) .expect("expected type passed to `check` is not a well-formed type expression"); ensure!( ty_phase == phase, @@ -763,7 +834,6 @@ pub fn check<'src, 'core>( ); match term { // ------------------------------------------------------------------ Lit - // Literals check against any integer type. ast::Term::Lit(n) => match expected { core::Term::Prim(Prim::IntTy(it)) => { let width = it.width; @@ -773,23 +843,11 @@ pub fn check<'src, 'core>( ); Ok(ctx.alloc(core::Term::Lit(*n, *it))) } - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::App { .. } - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let { .. } - | core::Term::Match { .. } => { - Err(anyhow!("literal `{n}` cannot have a non-integer type")) - } + _ => Err(anyhow!("literal `{n}` cannot have a non-integer type")), }, // ------------------------------------------------------------------ App { Prim (BinOp) } // Width is resolved from the expected type. - // Comparison ops (Eq/Ne/Lt/Gt/Le/Ge) are handled in infer mode and fall through - // to infer+unify below, since they always return u1 (inferable). ast::Term::App { func: ast::FunName::BinOp(op), args, @@ -805,15 +863,7 @@ pub fn check<'src, 'core>( { let int_ty = match expected { core::Term::Prim(Prim::IntTy(it)) => *it, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::App { .. } - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let { .. } - | core::Term::Match { .. } => { + _ => { bail!("primitive operation requires an integer type") } }; @@ -839,7 +889,7 @@ pub fn check<'src, 'core>( let core_arg1 = check(ctx, phase, rhs, expected)?; let core_args = ctx.alloc_slice([core_arg0, core_arg1]); - Ok(ctx.alloc(core::Term::new_app(core::Head::Prim(prim), core_args))) + Ok(ctx.alloc(core::Term::new_prim_app(prim, core_args))) } // ------------------------------------------------------------------ App { UnOp } @@ -849,15 +899,7 @@ pub fn check<'src, 'core>( } => { let int_ty = match expected { core::Term::Prim(Prim::IntTy(it)) => *it, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::App(_) - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => { + _ => { bail!("primitive operation requires an integer type") } }; @@ -871,44 +913,24 @@ pub fn check<'src, 'core>( }; let core_arg = check(ctx, phase, arg, expected)?; let core_args = std::slice::from_ref(ctx.arena.alloc(core_arg)); - Ok(ctx.alloc(core::Term::new_app(core::Head::Prim(prim), core_args))) + Ok(ctx.alloc(core::Term::new_prim_app(prim, core_args))) } // ------------------------------------------------------------------ Quote (check mode) - // `#(t)` checked against `[[T]]` — check `t` against `T` at object phase. ast::Term::Quote(inner) => match expected { core::Term::Lift(obj_ty) => { let core_inner = check(ctx, Phase::Object, inner, obj_ty)?; Ok(ctx.alloc(core::Term::Quote(core_inner))) } - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::App(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => { - Err(anyhow!("quote `#(...)` must have a lifted type `[[T]]`")) - } + _ => Err(anyhow!("quote `#(...)` must have a lifted type `[[T]]`")), }, // ------------------------------------------------------------------ Splice (check mode) - // `$(e)` checked against `T` (object) — check `e` against `[[T]]` at meta phase. - // Mirror image of Quote: Quote unwraps `[[T]]` to check inner at object phase; - // Splice wraps `T` in `[[...]]` to check inner at meta phase. - // - // For object integer types `T = IntTy(w, Object)`, also accept `e : IntTy(w, Meta)` - // with an implicit `Embed(w)` insertion — the same coercion as the infer path. ast::Term::Splice(inner) => { ensure!( phase == Phase::Object, "`$(...)` is only valid in an object-phase context" ); - // For object integer expected types, first try the standard [[T]] path; if - // that fails, try the meta-integer embed path (inner has type IntTy(w, Meta)). - // Trying [[T]] first means a variable `x : [[u64]]` is always handled - // correctly and the embed path only activates when [[T]] genuinely fails. if let core::Term::Prim(Prim::IntTy(IntType { width, phase: Phase::Object, @@ -920,8 +942,8 @@ pub fn check<'src, 'core>( } let meta_int_ty = ctx.alloc(core::Term::Prim(Prim::IntTy(IntType::meta(*width)))); let core_inner = check(ctx, Phase::Meta, inner, meta_int_ty)?; - let embedded = ctx.alloc(core::Term::new_app( - core::Head::Prim(Prim::Embed(*width)), + let embedded = ctx.alloc(core::Term::new_prim_app( + Prim::Embed(*width), ctx.arena.alloc_slice_fill_iter([core_inner]), )); return Ok(ctx.alloc(core::Term::Splice(embedded))); @@ -931,8 +953,62 @@ pub fn check<'src, 'core>( Ok(ctx.alloc(core::Term::Splice(core_inner))) } + // ------------------------------------------------------------------ Lam (check mode) + // Check lambda against an expected Pi type. + ast::Term::Lam { params, body } => { + ensure!( + phase == Phase::Meta, + "lambdas are only valid in meta-phase context" + ); + ensure!( + !params.is_empty(), + "lambda must have at least one parameter" + ); + + let depth_before = ctx.depth(); + + // Peel off one Pi per lambda param, checking annotation matches. + let mut current_expected = expected; + let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); + + for p in *params { + let pi = match current_expected { + core::Term::Pi(pi) => pi, + _ => bail!("lambda has more parameters than the expected function type"), + }; + + let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); + let annotated_ty = infer(ctx, Phase::Meta, p.ty)?; + + ensure!( + types_equal(annotated_ty, pi.param_ty), + "lambda parameter type mismatch: annotation gives a different type \ + than the expected function type" + ); + + elaborated_params.push((param_name, pi.param_ty)); + ctx.push_local(param_name, pi.param_ty); + current_expected = pi.body_ty; + } + + let core_body = check(ctx, phase, body, current_expected)?; + + // Build nested Lam from inside out. + let mut result: &'core core::Term<'core> = core_body; + for &(param_name, param_ty) in elaborated_params.iter().rev() { + ctx.pop_local(); + result = ctx.alloc(core::Term::Lam(Lam { + param_name, + param_ty, + body: result, + })); + } + + assert_eq!(ctx.depth(), depth_before, "Lam check leaked locals"); + Ok(result) + } + // ------------------------------------------------------------------ Match (check mode) - // Check each arm body against the expected type; the scrutinee is always inferred. ast::Term::Match { scrutinee, arms } => { let core_scrutinee = infer(ctx, phase, scrutinee)?; let scrut_ty = ctx.type_of(core_scrutinee); @@ -943,8 +1019,6 @@ pub fn check<'src, 'core>( ctx.arena .alloc_slice_try_fill_iter(arms.iter().map(|arm| -> Result<_> { let core_pat = elaborate_pat(ctx, &arm.pat); - // If the pattern binds a name, push it into locals for the arm body. - // We use a placeholder type (scrutinee type) — sufficient for the prototype. if let Some(bname) = core_pat.bound_name() { ctx.push_local(bname, scrut_ty); } @@ -966,19 +1040,15 @@ pub fn check<'src, 'core>( } // ------------------------------------------------------------------ Block (check mode) - // Thread the expected type down through let-bindings to the final expression. ast::Term::Block { stmts, expr } => { let depth_before = ctx.depth(); let result = check_block(ctx, phase, stmts, expr, expected); - // Each let-binding is responsible for pushing and popping its own local - // (via `elaborate_let`), so the depth must be restored exactly. assert_eq!(ctx.depth(), depth_before, "check_block leaked locals"); result } // ------------------------------------------------------------------ fallthrough: infer then unify - // For all other forms, infer the type and check it matches expected. - ast::Term::Var(_) | ast::Term::App { .. } | ast::Term::Lift(_) => { + ast::Term::Var(_) | ast::Term::App { .. } | ast::Term::Lift(_) | ast::Term::Pi { .. } => { let core_term = infer(ctx, phase, term)?; ensure!( types_equal(ctx.type_of(core_term), expected), diff --git a/compiler/src/checker/test/apply.rs b/compiler/src/checker/test/apply.rs index 80f7da2..f5a7c08 100644 --- a/compiler/src/checker/test/apply.rs +++ b/compiler/src/checker/test/apply.rs @@ -139,11 +139,11 @@ fn check_binop_add_against_u32_succeeds() { let result = check(&mut ctx, Phase::Object, term, expected).expect("should check"); assert!(matches!( result, - core::Term::App(core::App { - head: Head::Prim(Prim::Add(IntType { + core::Term::PrimApp(core::PrimApp { + prim: Prim::Add(IntType { width: IntWidth::U32, .. - })), + }), .. }) )); @@ -172,11 +172,11 @@ fn infer_comparison_op_returns_u1() { let ty = ctx.type_of(core_term); assert!(matches!( core_term, - core::Term::App(core::App { - head: Head::Prim(Prim::Eq(IntType { + core::Term::PrimApp(core::PrimApp { + prim: Prim::Eq(IntType { width: IntWidth::U64, .. - })), + }), .. }) )); @@ -282,11 +282,11 @@ fn check_eq_op_produces_u1() { // The prim carries the operand type (u64), not u1. assert!(matches!( result, - core::Term::App(core::App { - head: Head::Prim(Prim::Eq(IntType { + core::Term::PrimApp(core::PrimApp { + prim: Prim::Eq(IntType { width: IntWidth::U64, .. - })), + }), .. }) )); diff --git a/compiler/src/checker/test/context.rs b/compiler/src/checker/test/context.rs index bb2954c..fbc1505 100644 --- a/compiler/src/checker/test/context.rs +++ b/compiler/src/checker/test/context.rs @@ -168,15 +168,9 @@ fn arithmetic_requires_expected_type() { fn global_call_is_inferable() { let arena = bumpalo::Bump::new(); let arg = arena.alloc(core::Term::Lit(1, IntType::U64_META)); - let args = &*arena.alloc_slice_fill_iter([&*arg]); - let app = arena.alloc(core::Term::new_app(Head::Global(Name::new("foo")), args)); - assert!(matches!( - app, - core::Term::App(core::App { - head: Head::Global(Name("foo")), - .. - }) - )); + let global = arena.alloc(core::Term::Global(Name::new("foo"))); + let app = arena.alloc(core::Term::FunApp(core::FunApp { func: global, arg })); + assert!(matches!(app, core::Term::FunApp(_))); } #[test] @@ -203,10 +197,7 @@ fn lift_type_structure() { #[test] fn quote_inference_mirrors_inner() { let arena = bumpalo::Bump::new(); - let inner = arena.alloc(core::Term::new_app( - Head::Global(Name::new("foo")), - arena.alloc_slice_fill_iter([] as [&core::Term; 0]), - )); + let inner = arena.alloc(core::Term::Global(Name::new("foo"))); let quoted = arena.alloc(core::Term::Quote(inner)); assert!(matches!(quoted, core::Term::Quote(_))); } @@ -276,16 +267,10 @@ fn match_with_binding_pattern() { fn function_call_to_global() { let arena = bumpalo::Bump::new(); let arg = arena.alloc(core::Term::Lit(42, IntType::U64_META)); - let args = &*arena.alloc_slice_fill_iter([&*arg]); - let app = arena.alloc(core::Term::new_app(Head::Global(Name::new("foo")), args)); + let global = arena.alloc(core::Term::Global(Name::new("foo"))); + let app = arena.alloc(core::Term::FunApp(core::FunApp { func: global, arg })); - assert!(matches!( - app, - core::Term::App(core::App { - head: Head::Global(Name("foo")), - .. - }) - )); + assert!(matches!(app, core::Term::FunApp(_))); } #[test] @@ -294,18 +279,15 @@ fn builtin_operation_call() { let arg1 = arena.alloc(core::Term::Lit(1, IntType::U64_OBJ)); let arg2 = arena.alloc(core::Term::Lit(2, IntType::U64_OBJ)); let args = &*arena.alloc_slice_fill_iter([&*arg1, &*arg2]); - let app = arena.alloc(core::Term::new_app( - Head::Prim(Prim::Add(IntType::U64_OBJ)), - args, - )); + let app = arena.alloc(core::Term::new_prim_app(Prim::Add(IntType::U64_OBJ), args)); assert!(matches!( app, - core::Term::App(core::App { - head: Head::Prim(Prim::Add(IntType { + core::Term::PrimApp(core::PrimApp { + prim: Prim::Add(IntType { width: IntWidth::U64, .. - })), + }), .. }) )); diff --git a/compiler/src/checker/test/mod.rs b/compiler/src/checker/test/mod.rs index 92d2afc..4949961 100644 --- a/compiler/src/checker/test/mod.rs +++ b/compiler/src/checker/test/mod.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use super::*; -use crate::core::{self, FunSig, Head, IntType, IntWidth, Name, Pat, Prim}; +use crate::core::{self, FunSig, IntType, IntWidth, Name, Pat, Prim}; use crate::parser::ast::{self, BinOp, FunName, MatchArm, Phase}; mod helpers; diff --git a/compiler/src/core/mod.rs b/compiler/src/core/mod.rs index e846c83..2e24c19 100644 --- a/compiler/src/core/mod.rs +++ b/compiler/src/core/mod.rs @@ -1,8 +1,12 @@ pub mod pretty; mod prim; +mod subst; +pub mod alpha_eq; pub use crate::common::{Name, Phase}; +pub use alpha_eq::alpha_eq; pub use prim::{IntType, IntWidth, Prim}; +pub use subst::subst; /// De Bruijn level (counts from the outermost binder) #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -19,20 +23,6 @@ impl Lvl { } } -/// Head of an application: either a top-level function or a primitive op -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Head<'a> { - Global(Name<'a>), // resolved top-level function name - Prim(Prim), // built-in operation with resolved width -} - -impl Head<'_> { - /// Returns `true` if this head is a binary infix primitive operator. - pub const fn is_binop(&self) -> bool { - matches!(self, Self::Prim(p) if p.is_binop()) - } -} - /// Match pattern in the core IR #[derive(Debug, Clone, PartialEq, Eq)] pub enum Pat<'a> { @@ -66,6 +56,22 @@ pub struct FunSig<'a> { pub phase: Phase, } +impl<'a> FunSig<'a> { + /// Construct a nested Pi type from this signature: + /// `fn(x: A, y: B) -> C` becomes `Pi(x, A, Pi(y, B, C))`. + pub fn to_pi_type(&self, arena: &'a bumpalo::Bump) -> &'a Term<'a> { + let mut result = self.ret_ty; + for &(name, ty) in self.params.iter().rev() { + result = arena.alloc(Term::Pi(Pi { + param_name: name, + param_ty: ty, + body_ty: result, + })); + } + result + } +} + /// Elaborated top-level function definition #[derive(Debug)] pub struct Function<'a> { @@ -80,32 +86,56 @@ pub struct Program<'a> { pub functions: &'a [Function<'a>], } -/// Application of a global function or primitive operation to arguments. +/// Primitive operation application (always fully applied, carries resolved `IntType`) #[derive(Debug, PartialEq, Eq)] -pub struct App<'a> { - pub head: Head<'a>, +pub struct PrimApp<'a> { + pub prim: Prim, pub args: &'a [&'a Term<'a>], } -impl App<'_> { +impl PrimApp<'_> { /// Returns the number of arguments. pub const fn arity(&self) -> usize { self.args.len() } - /// Returns `true` if this application is a binary infix primitive operator. - /// - /// Asserts that the argument count is exactly 2, which is an invariant - /// enforced by the elaborator for all binop applications. + /// Returns `true` if this is a binary infix primitive operator. pub fn is_binop(&self) -> bool { - let result = self.head.is_binop(); + let result = self.prim.is_binop(); if result { - assert_eq!(self.arity(), 2, "binop App must have exactly 2 arguments"); + assert_eq!( + self.arity(), + 2, + "binop PrimApp must have exactly 2 arguments" + ); } result } } +/// Dependent function type: fn(x: A) -> B +#[derive(Debug, PartialEq, Eq)] +pub struct Pi<'a> { + pub param_name: &'a str, + pub param_ty: &'a Term<'a>, + pub body_ty: &'a Term<'a>, +} + +/// Lambda abstraction: |x: A| body +#[derive(Debug, PartialEq, Eq)] +pub struct Lam<'a> { + pub param_name: &'a str, + pub param_ty: &'a Term<'a>, + pub body: &'a Term<'a>, +} + +/// Function application (single-arg, curried): f(x) +#[derive(Debug, PartialEq, Eq)] +pub struct FunApp<'a> { + pub func: &'a Term<'a>, + pub arg: &'a Term<'a>, +} + /// Let binding with explicit type annotation and a body. #[derive(Debug, PartialEq, Eq)] pub struct Let<'a> { @@ -127,12 +157,20 @@ pub struct Match<'a> { pub enum Term<'a> { /// Local variable, identified by De Bruijn level Var(Lvl), - /// Built-in type or operation + /// Built-in type or operation (not applied) Prim(Prim), /// Numeric literal with its integer type Lit(u64, IntType), - /// Application of a global function or primitive operation to arguments - App(App<'a>), + /// Global function reference + Global(Name<'a>), + /// Primitive operation application (always fully applied, carries resolved `IntType`) + PrimApp(PrimApp<'a>), + /// Dependent function type: fn(x: A) -> B + Pi(Pi<'a>), + /// Lambda abstraction: |x: A| body + Lam(Lam<'a>), + /// Function application (single-arg, curried): f(x) + FunApp(FunApp<'a>), /// Lift: [[T]] — meta type representing object-level code of type T Lift(&'a Self), /// Quotation: #(t) — produce object-level code from a meta expression @@ -202,8 +240,8 @@ impl Term<'static> { } impl<'a> Term<'a> { - pub const fn new_app(head: Head<'a>, args: &'a [&'a Self]) -> Self { - Self::App(App { head, args }) + pub const fn new_prim_app(prim: Prim, args: &'a [&'a Self]) -> Self { + Self::PrimApp(PrimApp { prim, args }) } pub const fn new_let(name: &'a str, ty: &'a Self, expr: &'a Self, body: &'a Self) -> Self { @@ -220,9 +258,9 @@ impl<'a> Term<'a> { } } -impl<'a> From> for Term<'a> { - fn from(app: App<'a>) -> Self { - Self::App(app) +impl<'a> From> for Term<'a> { + fn from(app: PrimApp<'a>) -> Self { + Self::PrimApp(app) } } diff --git a/compiler/src/core/pretty.rs b/compiler/src/core/pretty.rs index 4007591..6110280 100644 --- a/compiler/src/core/pretty.rs +++ b/compiler/src/core/pretty.rs @@ -2,7 +2,7 @@ use std::fmt; use crate::parser::ast::Phase; -use super::{App, Arm, Function, Head, Pat, Program, Term}; +use super::{Arm, Function, Pat, PrimApp, Program, Term}; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -26,13 +26,7 @@ impl<'a> Term<'a> { // Let and Match manage their own indentation internally. Term::Let(_) | Term::Match(_) => self.fmt_term_inline(env, indent, f), // Everything else gets a leading indent. - Term::Var(_) - | Term::Prim(_) - | Term::Lit(..) - | Term::App(_) - | Term::Lift(_) - | Term::Quote(_) - | Term::Splice(_) => { + _ => { write_indent(f, indent)?; self.fmt_term_inline(env, indent, f) } @@ -63,8 +57,55 @@ impl<'a> Term<'a> { // ── Primitive type / universe ───────────────────────────────────────── Term::Prim(p) => write!(f, "{p}"), - // ── Application ────────────────────────────────────────────────────── - Term::App(app) => app.fmt_app(env, indent, f), + // ── Global reference ────────────────────────────────────────────────── + Term::Global(name) => write!(f, "{name}"), + + // ── Primitive application ───────────────────────────────────────────── + Term::PrimApp(app) => app.fmt_prim_app(env, indent, f), + + // ── Pi type ─────────────────────────────────────────────────────────── + Term::Pi(pi) => { + if pi.param_name == "_" { + write!(f, "fn(")?; + pi.param_ty.fmt_expr(env, indent, f)?; + write!(f, ") -> ")?; + pi.body_ty.fmt_expr(env, indent, f) + } else { + write!(f, "fn({}@{}: ", pi.param_name, env.len())?; + pi.param_ty.fmt_expr(env, indent, f)?; + write!(f, ") -> ")?; + env.push(pi.param_name); + pi.body_ty.fmt_expr(env, indent, f)?; + env.pop(); + Ok(()) + } + } + + // ── Lambda ──────────────────────────────────────────────────────────── + Term::Lam(lam) => { + write!(f, "|{}@{}: ", lam.param_name, env.len())?; + lam.param_ty.fmt_expr(env, indent, f)?; + write!(f, "| ")?; + env.push(lam.param_name); + lam.body.fmt_expr(env, indent, f)?; + env.pop(); + Ok(()) + } + + // ── Function application ────────────────────────────────────────────── + // For curried chains FunApp(FunApp(f, a), b), collect args and print f(a, b). + Term::FunApp(_) => { + let (head, args) = self.collect_fun_app_args(); + head.fmt_expr(env, indent, f)?; + write!(f, "(")?; + for (i, arg) in args.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + arg.fmt_expr(env, indent, f)?; + } + write!(f, ")") + } // ── Lift / Quote / Splice ───────────────────────────────────────────── Term::Lift(inner) => { @@ -131,54 +172,39 @@ impl<'a> Term<'a> { write_indent(f, indent)?; write!(f, "}}") } - Term::Var(_) - | Term::Prim(_) - | Term::Lit(..) - | Term::App(_) - | Term::Lift(_) - | Term::Quote(_) - | Term::Splice(_) => self.fmt_term_inline(env, indent, f), + _ => self.fmt_term_inline(env, indent, f), + } + } + + /// Collect a chain of curried `FunApp` into (head, [arg1, arg2, ...]). + fn collect_fun_app_args(&self) -> (&Self, Vec<&Self>) { + let mut args = Vec::new(); + let mut current = self; + while let Term::FunApp(app) = current { + args.push(app.arg); + current = app.func; } + args.reverse(); + (current, args) } } -impl<'a> App<'a> { - /// Print an application. - /// - /// All primitives use `@name(arg, arg, ...)` function-call syntax. No infix - /// operators are emitted in the core pretty-printer. - fn fmt_app( +impl<'a> PrimApp<'a> { + /// Print a primitive application using `@name(arg, arg, ...)` syntax. + fn fmt_prim_app( &self, env: &mut Vec<&'a str>, indent: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { - match &self.head { - // ── Global function call ────────────────────────────────────────────── - Head::Global(name) => { - write!(f, "{name}(")?; - for (i, arg) in self.args.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - arg.fmt_expr(env, indent, f)?; - } - write!(f, ")") - } - - // ── Primitive operation ─────────────────────────────────────────────── - // All builtins use `@name(args...)` function-call syntax. - Head::Prim(prim) => { - write!(f, "{prim}(")?; - for (i, arg) in self.args.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - arg.fmt_expr(env, indent, f)?; - } - write!(f, ")") + write!(f, "{}(", self.prim)?; + for (i, arg) in self.args.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; } + arg.fmt_expr(env, indent, f)?; } + write!(f, ")") } } diff --git a/compiler/src/eval/mod.rs b/compiler/src/eval/mod.rs index 19e2f34..8bf37b1 100644 --- a/compiler/src/eval/mod.rs +++ b/compiler/src/eval/mod.rs @@ -4,26 +4,28 @@ use anyhow::{Result, anyhow, ensure}; use bumpalo::Bump; use crate::core::{ - App, Arm, FunSig, Function, Head, IntType, IntWidth, Lvl, Name, Pat, Prim, Program, Term, + Arm, FunApp, FunSig, Function, IntType, IntWidth, Lam, Lvl, Name, Pat, Prim, Program, Term, }; use crate::parser::ast::Phase; // ── Value types ─────────────────────────────────────────────────────────────── /// A value produced by meta-level evaluation. -/// -/// In this substitution-based prototype there are only two kinds of meta -/// values: integer literals and quoted object-level code. Meta-level -/// lambdas / closures are not yet supported (the current surface language -/// has no meta-level lambda syntax; only top-level `fn` definitions). #[derive(Clone, Debug)] enum MetaVal<'out> { /// A concrete integer value computed at meta (compile) time. - VLit(u64), - /// Quoted object-level code: the result of evaluating `#(t)` or of - /// wrapping a literal via `Embed`. The inner term is a splice-free - /// object term produced by `unstage_obj`. - VCode(&'out Term<'out>), + Lit(u64), + /// Quoted object-level code. + Code(&'out Term<'out>), + /// A type term passed as a type argument (dependent types: types are values). + /// The type term itself is not inspected during evaluation. + Ty, + /// A closure: a lambda body captured with its environment. + Closure { + body: &'out Term<'out>, + env: Vec>, + obj_next: Lvl, + }, } // ── Environment ─────────────────────────────────────────────────────────────── @@ -33,18 +35,11 @@ enum MetaVal<'out> { enum Binding<'out> { /// A meta-level variable bound to a concrete `MetaVal`. Meta(MetaVal<'out>), - /// An object-level variable. Object variables are opaque during - /// meta-level evaluation and remain as `Var(lvl)` in the output. - /// The Lvl inside signifies the level in the generated output - /// rather than in the original program where bindings for the - /// object level and meta level may be interwoven. + /// An object-level variable. Obj(Lvl), } /// Evaluation environment: a stack of bindings indexed by De Bruijn level. -/// -/// Level 0 is the outermost binding (first function parameter); new bindings -/// are pushed onto the end and accessed by their index. #[derive(Debug)] struct Env<'out> { bindings: Vec>, @@ -66,8 +61,7 @@ impl<'out> Env<'out> { .expect("De Bruijn level in env bounds") } - /// Push an object-level binding. Assigns the next consecutive object-level - /// De Bruijn level and advances `obj_next`. + /// Push an object-level binding. fn push_obj(&mut self) { let lvl = self.obj_next; self.obj_next = lvl.succ(); @@ -79,7 +73,7 @@ impl<'out> Env<'out> { self.bindings.push(Binding::Meta(val)); } - /// Pop the last binding (used to restore the environment after a let / arm). + /// Pop the last binding. fn pop(&mut self) { match self.bindings.pop().expect("pop on empty environment") { Binding::Obj(_) => { @@ -98,30 +92,21 @@ impl<'out> Env<'out> { // ── Globals table ───────────────────────────────────────────────────────────── /// Everything the evaluator needs to know about a top-level function. -struct GlobalDef<'core> { - sig: &'core FunSig<'core>, - body: &'core Term<'core>, +struct GlobalDef<'a> { + sig: &'a FunSig<'a>, + body: &'a Term<'a>, } -type Globals<'core> = HashMap, GlobalDef<'core>>; +type Globals<'a> = HashMap, GlobalDef<'a>>; // ── Meta-level evaluator ────────────────────────────────────────────────────── /// Evaluate a meta-level `term` to a `MetaVal`. -/// -/// `env` maps De Bruijn levels to their current values. `globals` provides -/// the definitions of all top-level functions. `arena` is used when -/// allocating object terms inside `VCode` values (via `unstage_obj`). -/// -/// Invariants enforced by the typechecker (violations panic via `unreachable!`): -/// - `Splice` nodes never appear at meta level. -/// - `Lift` and type-level `Prim` nodes never appear in term positions. -/// - Object variables (`Binding::Obj`) are never referenced at meta level. -fn eval_meta<'out, 'core>( +fn eval_meta<'out>( arena: &'out Bump, - globals: &Globals<'core>, + globals: &Globals<'out>, env: &mut Env<'out>, - term: &'core Term<'core>, + term: &'out Term<'out>, ) -> Result> { match term { // ── Variable ───────────────────────────────────────────────────────── @@ -134,20 +119,51 @@ fn eval_meta<'out, 'core>( }, // ── Literal ────────────────────────────────────────────────────────── - Term::Lit(n, _) => Ok(MetaVal::VLit(*n)), + Term::Lit(n, _) => Ok(MetaVal::Lit(*n)), - // ── Application ────────────────────────────────────────────────────── - Term::App(app) => eval_meta_app(arena, globals, env, app), + // ── Global reference ───────────────────────────────────────────────── + Term::Global(name) => { + let def = globals + .get(name) + .unwrap_or_else(|| panic!("unknown global `{name}` during staging")); + if def.sig.params.is_empty() { + // Zero-param global: evaluate the body immediately in a fresh env. + // (Zero-param Pi types don't exist, so zero-param globals are always + // called, never passed as values.) + let mut callee_env = Env::new(env.obj_next); + eval_meta(arena, globals, &mut callee_env, def.body) + } else { + // Multi-param global: produce a closure, capturing the caller's + // obj_next so object-level let bindings inside quotes don't clash + // with output levels already in use at the call site. + Ok(global_to_closure(arena, def, env.obj_next)) + } + } + + // ── Lambda ─────────────────────────────────────────────────────────── + Term::Lam(lam) => Ok(MetaVal::Closure { + body: lam.body, + env: env.bindings.clone(), + obj_next: env.obj_next, + }), + + // ── Function application ───────────────────────────────────────────── + Term::FunApp(app) => { + let func_val = eval_meta(arena, globals, env, app.func)?; + let arg_val = eval_meta(arena, globals, env, app.arg)?; + apply_closure(arena, globals, func_val, arg_val) + } + + // ── PrimApp ────────────────────────────────────────────────────────── + Term::PrimApp(app) => eval_meta_prim(arena, globals, env, app.prim, app.args), - // ── Quote: #(t) ─────────────────────────────────────────────────────── - // Unstage the enclosed object term (eliminating any splices inside it) - // and wrap the result as object code. + // ── Quote: #(t) ────────────────────────────────────────────────────── Term::Quote(inner) => { let obj_term = unstage_obj(arena, globals, env, inner)?; - Ok(MetaVal::VCode(obj_term)) + Ok(MetaVal::Code(obj_term)) } - // ── Let binding ─────────────────────────────────────────────────────── + // ── Let binding ────────────────────────────────────────────────────── Term::Let(let_) => { let val = eval_meta(arena, globals, env, let_.expr)?; env.push_meta(val); @@ -156,87 +172,106 @@ fn eval_meta<'out, 'core>( result } - // ── Match ───────────────────────────────────────────────────────────── + // ── Match ──────────────────────────────────────────────────────────── Term::Match(match_) => { let scrut_val = eval_meta(arena, globals, env, match_.scrutinee)?; let n = match scrut_val { - MetaVal::VLit(n) => n, - MetaVal::VCode(_) => unreachable!( - "cannot match on object code at meta level (typechecker invariant)" + MetaVal::Lit(n) => n, + MetaVal::Code(_) | MetaVal::Ty | MetaVal::Closure { .. } => unreachable!( + "cannot match on non-integer at meta level (typechecker invariant)" ), }; eval_meta_match(arena, globals, env, n, match_.arms) } - // ── Unreachable in well-typed meta terms ────────────────────────────── + // ── Unreachable in well-typed meta terms ───────────────────────────── Term::Splice(_) => unreachable!("Splice in meta context (typechecker invariant)"), - Term::Lift(_) | Term::Prim(_) => { - unreachable!("type-level term in evaluation position (typechecker invariant)") + // Type-level terms evaluate to themselves when passed as type arguments + // in a dependently-typed function call (e.g. `id(u64, x)` passes `u64 : Type`). + Term::Lift(_) | Term::Prim(_) | Term::Pi(_) => Ok(MetaVal::Ty), + } +} + +/// Convert a global function definition into a closure value. +/// +/// For a multi-parameter function, we build nested closures. E.g., `fn f(x, y) = body` +/// becomes a closure whose body is a lambda `|y| body`. +fn global_to_closure<'out>( + arena: &'out Bump, + def: &GlobalDef<'out>, + obj_next: Lvl, +) -> MetaVal<'out> { + let params = def.sig.params; + if params.is_empty() { + MetaVal::Closure { + body: def.body, + env: Vec::new(), + obj_next, + } + } else { + // Build nested lambdas for params[1..], then wrap in a closure for params[0]. + let mut body: &Term = def.body; + for &(name, ty) in params.iter().rev().skip(1) { + body = arena.alloc(Term::Lam(Lam { + param_name: name, + param_ty: ty, + body, + })); + } + MetaVal::Closure { + body, + env: Vec::new(), + obj_next, } } } -/// Evaluate a function application at meta level. -fn eval_meta_app<'out, 'core>( +/// Apply a closure value to an argument value. +fn apply_closure<'out>( arena: &'out Bump, - globals: &Globals<'core>, - env: &mut Env<'out>, - app: &'core App<'core>, + globals: &Globals<'out>, + func_val: MetaVal<'out>, + arg_val: MetaVal<'out>, ) -> Result> { - match &app.head { - // ── Global function call ────────────────────────────────────────────── - Head::Global(name) => { - let def = globals - .get(name) - .unwrap_or_else(|| panic!("unknown global `{name}` during staging")); - - assert_eq!( - def.sig.phase, - Phase::Meta, - "object-phase function `{name}` called in meta context during staging" - ); - - // Evaluate each argument in the *caller's* environment. - let mut arg_vals: Vec> = Vec::with_capacity(app.args.len()); - for arg in app.args { - arg_vals.push(eval_meta(arena, globals, env, arg)?); - } - - // Build a fresh environment for the callee: one binding per parameter. - let mut callee_env = Env::new(env.obj_next); - for val in arg_vals { - callee_env.push_meta(val); - } - - eval_meta(arena, globals, &mut callee_env, def.body) + match func_val { + MetaVal::Closure { + body, + env, + obj_next, + .. + } => { + let mut callee_env = Env { + bindings: env, + obj_next, + }; + callee_env.push_meta(arg_val); + + // Restore env for the caller (bindings are consumed by the callee). + eval_meta(arena, globals, &mut callee_env, body) + } + MetaVal::Lit(_) | MetaVal::Code(_) | MetaVal::Ty => { + unreachable!("applying a non-function value (typechecker invariant)") } - - // ── Primitive operations ────────────────────────────────────────────── - Head::Prim(prim) => eval_meta_prim(arena, globals, env, *prim, app.args), } } /// Evaluate a primitive operation at meta level. -fn eval_meta_prim<'out, 'core>( +fn eval_meta_prim<'out>( arena: &'out Bump, - globals: &Globals<'core>, + globals: &Globals<'out>, env: &mut Env<'out>, prim: Prim, - args: &'core [&'core Term<'core>], + args: &'out [&'out Term<'out>], ) -> Result> { - // Evaluate args[i] and extract its integer value. - // Panics if the value is `VCode` — the typechecker guarantees integer operands. - let eval_lit = |arena: &'out Bump, - globals: &Globals<'core>, - env: &mut Env<'out>, - arg: &'core Term<'core>| { - eval_meta(arena, globals, env, arg).map(|v| match v { - MetaVal::VLit(n) => n, - MetaVal::VCode(_) => unreachable!( - "expected integer meta value for primitive operand, got code (typechecker invariant)" - ), - }) - }; + let eval_lit = + |arena: &'out Bump, globals: &Globals<'out>, env: &mut Env<'out>, arg: &'out Term<'out>| { + eval_meta(arena, globals, env, arg).map(|v| match v { + MetaVal::Lit(n) => n, + MetaVal::Code(_) | MetaVal::Ty | MetaVal::Closure { .. } => unreachable!( + "expected integer meta value for primitive operand (typechecker invariant)" + ), + }) + }; #[expect(clippy::indexing_slicing)] match prim { @@ -255,7 +290,7 @@ fn eval_meta_prim<'out, 'core>( width.max_value() ) })?; - Ok(MetaVal::VLit(result)) + Ok(MetaVal::Lit(result)) } Prim::Sub(IntType { width, .. }) => { let a = eval_lit(arena, globals, env, args[0])?; @@ -266,7 +301,7 @@ fn eval_meta_prim<'out, 'core>( {a} - {b} underflows {width}" ) })?; - Ok(MetaVal::VLit(result)) + Ok(MetaVal::Lit(result)) } Prim::Mul(IntType { width, .. }) => { let a = eval_lit(arena, globals, env, args[0])?; @@ -282,72 +317,69 @@ fn eval_meta_prim<'out, 'core>( width.max_value() ) })?; - Ok(MetaVal::VLit(result)) + Ok(MetaVal::Lit(result)) } Prim::Div(_) => { let a = eval_lit(arena, globals, env, args[0])?; let b = eval_lit(arena, globals, env, args[1])?; ensure!(b != 0, "division by zero during staging"); - Ok(MetaVal::VLit(a / b)) + Ok(MetaVal::Lit(a / b)) } // ── Bitwise ─────────────────────────────────────────────────────────── Prim::BitAnd(_) => { let a = eval_lit(arena, globals, env, args[0])?; let b = eval_lit(arena, globals, env, args[1])?; - Ok(MetaVal::VLit(a & b)) + Ok(MetaVal::Lit(a & b)) } Prim::BitOr(_) => { let a = eval_lit(arena, globals, env, args[0])?; let b = eval_lit(arena, globals, env, args[1])?; - Ok(MetaVal::VLit(a | b)) + Ok(MetaVal::Lit(a | b)) } Prim::BitNot(IntType { width, .. }) => { let a = eval_lit(arena, globals, env, args[0])?; - Ok(MetaVal::VLit(mask_to_width(width, !a))) + Ok(MetaVal::Lit(mask_to_width(width, !a))) } // ── Comparison ──────────────────────────────────────────────────────── Prim::Eq(_) => { let a = eval_lit(arena, globals, env, args[0])?; let b = eval_lit(arena, globals, env, args[1])?; - Ok(MetaVal::VLit(u64::from(a == b))) + Ok(MetaVal::Lit(u64::from(a == b))) } Prim::Ne(_) => { let a = eval_lit(arena, globals, env, args[0])?; let b = eval_lit(arena, globals, env, args[1])?; - Ok(MetaVal::VLit(u64::from(a != b))) + Ok(MetaVal::Lit(u64::from(a != b))) } Prim::Lt(_) => { let a = eval_lit(arena, globals, env, args[0])?; let b = eval_lit(arena, globals, env, args[1])?; - Ok(MetaVal::VLit(u64::from(a < b))) + Ok(MetaVal::Lit(u64::from(a < b))) } Prim::Gt(_) => { let a = eval_lit(arena, globals, env, args[0])?; let b = eval_lit(arena, globals, env, args[1])?; - Ok(MetaVal::VLit(u64::from(a > b))) + Ok(MetaVal::Lit(u64::from(a > b))) } Prim::Le(_) => { let a = eval_lit(arena, globals, env, args[0])?; let b = eval_lit(arena, globals, env, args[1])?; - Ok(MetaVal::VLit(u64::from(a <= b))) + Ok(MetaVal::Lit(u64::from(a <= b))) } Prim::Ge(_) => { let a = eval_lit(arena, globals, env, args[0])?; let b = eval_lit(arena, globals, env, args[1])?; - Ok(MetaVal::VLit(u64::from(a >= b))) + Ok(MetaVal::Lit(u64::from(a >= b))) } // ── Embed: meta integer → object code ───────────────────────────────── - // `Embed(w)` applied to a meta integer `n` produces object-level code - // consisting of the literal `n`. This is how a compile-time integer - // constant is embedded into the generated object program. Prim::Embed(width) => { let n = eval_lit(arena, globals, env, args[0])?; let phase = Phase::Object; let lit_term = arena.alloc(Term::Lit(n, IntType { width, phase })); - Ok(MetaVal::VCode(lit_term)) + Ok(MetaVal::Code(lit_term)) } // ── Type-level prims are unreachable ────────────────────────────────── @@ -370,15 +402,12 @@ const fn mask_to_width(width: IntWidth, val: u64) -> u64 { } /// Evaluate a meta-level `match` expression. -/// -/// `n` is the already-evaluated scrutinee value. -/// Arms are checked in order; the first matching arm wins. -fn eval_meta_match<'out, 'core>( +fn eval_meta_match<'out>( arena: &'out Bump, - globals: &Globals<'core>, + globals: &Globals<'out>, env: &mut Env<'out>, n: u64, - arms: &'core [Arm<'core>], + arms: &'out [Arm<'out>], ) -> Result> { for arm in arms { match &arm.pat { @@ -388,18 +417,13 @@ fn eval_meta_match<'out, 'core>( } } Pat::Bind(_) | Pat::Wildcard => { - // Catch-all: bind the scrutinee value and evaluate the body. - env.push_meta(MetaVal::VLit(n)); + env.push_meta(MetaVal::Lit(n)); let result = eval_meta(arena, globals, env, arm.body); env.pop(); return result; } } } - // The typechecker enforces exhaustiveness, so this should not be reachable - // for well-typed programs. It can happen if the meta computation produces - // a value outside the covered range (e.g. a u64 with no wildcard arm), which - // is a user-visible runtime staging error rather than an internal bug. Err(anyhow!( "non-exhaustive match during staging (scrutinee = {n})" )) @@ -408,36 +432,30 @@ fn eval_meta_match<'out, 'core>( // ── Object-level unstager ───────────────────────────────────────────────────── /// Unstage an object-level `term`, eliminating all `Splice` nodes. -/// -/// Object variables (`Var`), operations (`App`), `Let`, and `Match` are left -/// structurally intact; only `Splice` nodes are reduced by running the -/// enclosed meta computation. -/// -/// `env` is shared with the meta evaluator so that meta variables that are in -/// scope at a splice point (e.g. from an enclosing `Quote`) remain accessible. -fn unstage_obj<'out, 'core>( +fn unstage_obj<'out>( arena: &'out Bump, - globals: &Globals<'core>, + globals: &Globals<'out>, env: &mut Env<'out>, - term: &'core Term<'core>, + term: &'out Term<'out>, ) -> Result<&'out Term<'out>> { match term { // ── Variable ───────────────────────────────────────────────────────── Term::Var(lvl) => match env.get(*lvl) { - // A plain object variable (e.g. a `code fn` parameter) passes - // through as-is — it will be a free variable in the output. Binding::Obj(out_lvl) => Ok(arena.alloc(Term::Var(*out_lvl))), - // A meta variable of type `[[T]]` is referenced inside a quoted - // object term. Its value is object code. `VCode` is always - // fully staged (produced by `unstage_obj` at quote time), so we - // return it directly without recursing — that would be unsound - // because the levels inside the VCode term are relative to the - // environment at the *quoting site*, not the current env. - // This implements the ∼⟨t⟩ ≡ t definitional equality. - Binding::Meta(MetaVal::VCode(obj)) => Ok(obj), - Binding::Meta(MetaVal::VLit(_)) => unreachable!( + Binding::Meta(MetaVal::Code(obj)) => Ok(obj), + Binding::Meta(MetaVal::Lit(_)) => unreachable!( "integer meta variable at level {} referenced in object context \ - (typechecker invariant: only [[T]]-typed meta vars can appear in object terms)", + (typechecker invariant)", + lvl.0 + ), + Binding::Meta(MetaVal::Closure { .. }) => unreachable!( + "closure meta variable at level {} referenced in object context \ + (typechecker invariant)", + lvl.0 + ), + Binding::Meta(MetaVal::Ty) => unreachable!( + "type meta variable at level {} referenced in object context \ + (typechecker invariant)", lvl.0 ), }, @@ -448,41 +466,46 @@ fn unstage_obj<'out, 'core>( // ── Primitive ──────────────────────────────────────────────────────── Term::Prim(p) => Ok(arena.alloc(Term::Prim(*p))), - // ── Application ────────────────────────────────────────────────────── - Term::App(app) => { - let staged_head = match &app.head { - Head::Global(name) => Head::Global(Name::new(arena.alloc_str(name.as_str()))), - Head::Prim(p) => Head::Prim(*p), - }; + // ── Global reference (in object terms, e.g. object-level function call) ── + Term::Global(name) => { + Ok(arena.alloc(Term::Global(Name::new(arena.alloc_str(name.as_str()))))) + } + + // ── PrimApp ────────────────────────────────────────────────────────── + Term::PrimApp(app) => { let staged_args: &'out [&'out Term<'out>] = arena.alloc_slice_try_fill_iter( app.args .iter() .map(|arg| unstage_obj(arena, globals, env, arg)), )?; - Ok(arena.alloc(Term::new_app(staged_head, staged_args))) + Ok(arena.alloc(Term::new_prim_app(app.prim, staged_args))) + } + + // ── FunApp (in object terms) ───────────────────────────────────────── + Term::FunApp(app) => { + let staged_func = unstage_obj(arena, globals, env, app.func)?; + let staged_arg = unstage_obj(arena, globals, env, app.arg)?; + Ok(arena.alloc(Term::FunApp(FunApp { + func: staged_func, + arg: staged_arg, + }))) } - // ── Splice: $(t) — the key staging step ─────────────────────────────── - // Evaluate the meta term `t` to a `VCode(obj)`. `VCode` values are - // always fully staged (produced by `unstage_obj` at quote time), so - // we return the inner term directly. + // ── Splice: $(t) — the key staging step ────────────────────────────── Term::Splice(inner) => { let meta_val = eval_meta(arena, globals, env, inner)?; match meta_val { - MetaVal::VCode(obj_term) => Ok(obj_term), - MetaVal::VLit(_) => unreachable!( - "splice evaluated to an integer literal (typechecker invariant: \ - splice argument must have type [[T]])" - ), + MetaVal::Code(obj_term) => Ok(obj_term), + MetaVal::Lit(_) | MetaVal::Ty | MetaVal::Closure { .. } => { + unreachable!("splice evaluated to non-code value (typechecker invariant)") + } } } - // ── Let binding ─────────────────────────────────────────────────────── + // ── Let binding ────────────────────────────────────────────────────── Term::Let(let_) => { let staged_ty = unstage_obj(arena, globals, env, let_.ty)?; let staged_expr = unstage_obj(arena, globals, env, let_.expr)?; - // Push an object binding so that subsequent Var references by - // De Bruijn level resolve to the correct slot. env.push_obj(); let staged_body = unstage_obj(arena, globals, env, let_.body); env.pop(); @@ -494,7 +517,7 @@ fn unstage_obj<'out, 'core>( ))) } - // ── Match ───────────────────────────────────────────────────────────── + // ── Match ──────────────────────────────────────────────────────────── Term::Match(match_) => { let staged_scrutinee = unstage_obj(arena, globals, env, match_.scrutinee)?; let staged_arms: &'out [Arm<'out>] = @@ -520,32 +543,22 @@ fn unstage_obj<'out, 'core>( Ok(arena.alloc(Term::new_match(staged_scrutinee, staged_arms))) } - // ── Unreachable in well-typed object terms ──────────────────────────── + // ── Unreachable in well-typed object terms ─────────────────────────── Term::Quote(_) => unreachable!("Quote in object context (typechecker invariant)"), - Term::Lift(_) => unreachable!("Lift in object context (typechecker invariant)"), + Term::Lift(_) | Term::Pi(_) | Term::Lam(_) => { + unreachable!("meta-only term in object context (typechecker invariant)") + } } } // ── Public entry point ──────────────────────────────────────────────────────── -/// Unstage an elaborated program, eliminating all meta-level functions and -/// splices to produce a splice-free object-level program. -/// -/// The output `Program` contains only `Phase::Object` functions. All -/// `Phase::Meta` functions are erased (they served only as compile-time -/// helpers). Every `Splice` node in object-function bodies is replaced by -/// the object code it produces when the enclosing meta computation runs. -/// -/// # Errors -/// -/// Returns an error for genuine runtime staging errors: division by zero, -/// or a non-exhaustive match on a value not covered by any arm. -pub fn unstage_program<'out, 'core>( +/// Unstage an elaborated program. +pub fn unstage_program<'out>( arena: &'out Bump, - program: &'core Program<'core>, + program: &'out Program<'out>, ) -> Result> { - // Build the globals table from all functions in the program. - let globals: Globals<'core> = program + let globals: Globals<'out> = program .functions .iter() .map(|f| { @@ -559,15 +572,11 @@ pub fn unstage_program<'out, 'core>( }) .collect(); - // Unstage each object-level function; discard meta-level functions. let staged_fns: Vec> = program .functions .iter() .filter(|f| f.sig.phase == Phase::Object) .map(|f| -> Result<_> { - // Build an initial environment: one Obj binding per parameter, - // processing each parameter's type term before pushing the binding - // so that the env is correct for dependent types. let mut env = Env::new(Lvl::new(0)); let staged_params = arena.alloc_slice_try_fill_iter(f.sig.params.iter().map( diff --git a/compiler/src/parser/ast.rs b/compiler/src/parser/ast.rs index a261d72..12cfd4a 100644 --- a/compiler/src/parser/ast.rs +++ b/compiler/src/parser/ast.rs @@ -65,6 +65,16 @@ pub enum Term<'a> { func: FunName<'a>, args: &'a [&'a Self], }, + /// Function type: `fn(name: ty, ...) -> ret_ty` + Pi { + params: &'a [Param<'a>], + ret_ty: &'a Self, + }, + /// Lambda: `|params| body` + Lam { + params: &'a [Param<'a>], + body: &'a Self, + }, Quote(&'a Self), Splice(&'a Self), Lift(&'a Self), diff --git a/compiler/src/parser/mod.rs b/compiler/src/parser/mod.rs index c3b9873..1d367fe 100644 --- a/compiler/src/parser/mod.rs +++ b/compiler/src/parser/mod.rs @@ -294,6 +294,7 @@ where #[expect(clippy::wildcard_enum_match_arm)] fn match_binop(&mut self) -> Option { match self.peek()? { + // `|` after an expression is bitwise OR (never lambda — lambdas are atoms) Token::Bar => Some(BinOp::BitOr), Token::Ampersand => Some(BinOp::BitAnd), Token::EqEq => Some(BinOp::Eq), @@ -346,6 +347,46 @@ where Ok(Term::Match { scrutinee, arms }) } + /// Parse a function type: `fn(params) -> ret_ty` + /// + /// Called after consuming the `fn` token. Each param is `name: type`. + fn parse_fn_type(&mut self) -> Result> { + self.take(Token::LParen) + .context("expected '(' in function type")?; + let params = self.parse_params()?; + self.take(Token::RParen) + .context("expected ')' in function type")?; + self.take(Token::Arrow) + .context("expected '->' in function type")?; + let ret_ty = self + .parse_expr() + .context("expected return type in function type")?; + Ok(Term::Pi { params, ret_ty }) + } + + /// Parse a lambda expression: `|params| body` + /// + /// Called after consuming the `|` token. Each param is `name: type`. + fn parse_lambda(&mut self) -> Result> { + let params_vec = self.parse_separated_list(Token::Bar, |parser| { + let name = parser + .take_ident() + .context("expected parameter name in lambda")?; + parser + .take(Token::Colon) + .context("expected ':' in lambda parameter (type annotations are required)")?; + let ty = parser.parse_expr().context("expected parameter type")?; + let ty = parser.arena.alloc(ty); + Ok(Param { name, ty }) + })?; + self.take(Token::Bar) + .context("expected '|' after lambda parameters")?; + + let body = self.parse_expr().context("expected lambda body")?; + let params = self.arena.alloc_slice_fill_iter(params_vec); + Ok(Term::Lam { params, body }) + } + #[expect(clippy::wildcard_enum_match_arm)] fn parse_atom_owned(&mut self) -> Result> { let token = self.next().context("expected expression")??; @@ -358,6 +399,10 @@ where Ok(Term::Var(name)) } } + // `fn` not followed by ident → function type expression + Token::Fn => self.parse_fn_type(), + // `|` in atom position → lambda (not bitwise OR, which is infix) + Token::Bar => self.parse_lambda(), Token::LParen => self.parse_paren_expr(), Token::HashLParen => self.parse_quoted_expr(), Token::HashLBrace => self.parse_quoted_block(), diff --git a/compiler/tests/snap/full/pi_arity_mismatch/0_input.splic b/compiler/tests/snap/full/pi_arity_mismatch/0_input.splic index fc7647d..63db83c 100644 --- a/compiler/tests/snap/full/pi_arity_mismatch/0_input.splic +++ b/compiler/tests/snap/full/pi_arity_mismatch/0_input.splic @@ -1,4 +1,4 @@ // ERROR: too many arguments to a function-typed variable -fn apply(f: fn(u64) -> u64, x: u64) -> u64 { +fn apply(f: fn(_: u64) -> u64, x: u64) -> u64 { f(x, x) } diff --git a/compiler/tests/snap/full/pi_basic/0_input.splic b/compiler/tests/snap/full/pi_basic/0_input.splic index 1b3aae6..19cb53f 100644 --- a/compiler/tests/snap/full/pi_basic/0_input.splic +++ b/compiler/tests/snap/full/pi_basic/0_input.splic @@ -1,5 +1,5 @@ // Higher-order function: pass a function as an argument -fn apply(f: fn(u64) -> u64, x: u64) -> u64 { +fn apply(f: fn(_: u64) -> u64, x: u64) -> u64 { f(x) } diff --git a/compiler/tests/snap/full/pi_compose/0_input.splic b/compiler/tests/snap/full/pi_compose/0_input.splic index 48d7c45..55d74a9 100644 --- a/compiler/tests/snap/full/pi_compose/0_input.splic +++ b/compiler/tests/snap/full/pi_compose/0_input.splic @@ -1,5 +1,5 @@ // Function composition (monomorphic) -fn compose(f: fn(u64) -> u64, g: fn(u64) -> u64) -> fn(u64) -> u64 { +fn compose(f: fn(_: u64) -> u64, g: fn(_: u64) -> u64) -> fn(_: u64) -> u64 { |x: u64| f(g(x)) } diff --git a/compiler/tests/snap/full/pi_const/0_input.splic b/compiler/tests/snap/full/pi_const/0_input.splic index 2b58331..837b0d4 100644 --- a/compiler/tests/snap/full/pi_const/0_input.splic +++ b/compiler/tests/snap/full/pi_const/0_input.splic @@ -1,5 +1,5 @@ // Const combinator: returns a function that ignores its argument -fn const_(A: Type, B: Type) -> fn(A) -> fn(B) -> A { +fn const_(A: Type, B: Type) -> fn(_: A) -> fn(_: B) -> A { |a: A| |b: B| a } diff --git a/compiler/tests/snap/full/pi_dependent_ret/0_input.splic b/compiler/tests/snap/full/pi_dependent_ret/0_input.splic index b9b4069..bde60a5 100644 --- a/compiler/tests/snap/full/pi_dependent_ret/0_input.splic +++ b/compiler/tests/snap/full/pi_dependent_ret/0_input.splic @@ -1,10 +1,9 @@ -// Return type depends on a type argument -fn default(A: Type) -> A { - 0 -} +// Return type depends on the first type argument. +// const_(A, B, a, b) ignores b and returns a : A. +fn const_(A: Type, B: Type, a: A, b: B) -> A { a } -fn test_u64() -> u64 { default(u64) } -fn test_u8() -> u8 { default(u8) } +fn test_u64() -> u64 { const_(u64, u8, 10, 7) } +fn test_u8() -> u8 { const_(u8, u64, 7, 10) } code fn result_u64() -> u64 { $(test_u64()) } code fn result_u8() -> u8 { $(test_u8()) } diff --git a/compiler/tests/snap/full/pi_lambda_arg/0_input.splic b/compiler/tests/snap/full/pi_lambda_arg/0_input.splic index 7a47a5c..8b92c4c 100644 --- a/compiler/tests/snap/full/pi_lambda_arg/0_input.splic +++ b/compiler/tests/snap/full/pi_lambda_arg/0_input.splic @@ -1,5 +1,5 @@ // Pass a lambda as an argument to a higher-order function -fn apply(f: fn(u64) -> u64, x: u64) -> u64 { +fn apply(f: fn(_: u64) -> u64, x: u64) -> u64 { f(x) } diff --git a/compiler/tests/snap/full/pi_lambda_missing_annotation/0_input.splic b/compiler/tests/snap/full/pi_lambda_missing_annotation/0_input.splic index 4178535..70b6935 100644 --- a/compiler/tests/snap/full/pi_lambda_missing_annotation/0_input.splic +++ b/compiler/tests/snap/full/pi_lambda_missing_annotation/0_input.splic @@ -1,5 +1,5 @@ // ERROR: lambda without type annotation should fail -fn apply(f: fn(u64) -> u64, x: u64) -> u64 { +fn apply(f: fn(_: u64) -> u64, x: u64) -> u64 { f(x) } diff --git a/compiler/tests/snap/full/pi_lambda_type_mismatch/0_input.splic b/compiler/tests/snap/full/pi_lambda_type_mismatch/0_input.splic index 21aff91..59927da 100644 --- a/compiler/tests/snap/full/pi_lambda_type_mismatch/0_input.splic +++ b/compiler/tests/snap/full/pi_lambda_type_mismatch/0_input.splic @@ -1,5 +1,5 @@ // ERROR: lambda parameter type doesn't match expected function type -fn apply(f: fn(u64) -> u64, x: u64) -> u64 { +fn apply(f: fn(_: u64) -> u64, x: u64) -> u64 { f(x) } diff --git a/compiler/tests/snap/full/pi_nested/0_input.splic b/compiler/tests/snap/full/pi_nested/0_input.splic index b9e1877..f1b2a04 100644 --- a/compiler/tests/snap/full/pi_nested/0_input.splic +++ b/compiler/tests/snap/full/pi_nested/0_input.splic @@ -1,5 +1,5 @@ // Nested function types: function that takes a function and applies it twice -fn apply_twice(f: fn(u64) -> u64, x: u64) -> u64 { +fn apply_twice(f: fn(_: u64) -> u64, x: u64) -> u64 { f(f(x)) } diff --git a/compiler/tests/snap/full/pi_polycompose/0_input.splic b/compiler/tests/snap/full/pi_polycompose/0_input.splic index 05e24e3..61fe7df 100644 --- a/compiler/tests/snap/full/pi_polycompose/0_input.splic +++ b/compiler/tests/snap/full/pi_polycompose/0_input.splic @@ -1,5 +1,5 @@ // Polymorphic function composition -fn compose(A: Type, B: Type, C: Type, f: fn(B) -> C, g: fn(A) -> B) -> fn(A) -> C { +fn compose(A: Type, B: Type, C: Type, f: fn(_: B) -> C, g: fn(_: A) -> B) -> fn(_: A) -> C { |x: A| f(g(x)) } diff --git a/compiler/tests/snap/full/pi_repeat/0_input.splic b/compiler/tests/snap/full/pi_repeat/0_input.splic index a31c2af..6b5d2d6 100644 --- a/compiler/tests/snap/full/pi_repeat/0_input.splic +++ b/compiler/tests/snap/full/pi_repeat/0_input.splic @@ -1,5 +1,5 @@ // The motivating example: pass a code-generating lambda, unroll at compile time -fn repeat(f: fn([[u64]]) -> [[u64]], n: u64, x: [[u64]]) -> [[u64]] { +fn repeat(f: fn(_: [[u64]]) -> [[u64]], n: u64, x: [[u64]]) -> [[u64]] { match n { 0 => x, n => repeat(f, n - 1, f(x)), diff --git a/compiler/tests/snap/full/pi_staging_hof/0_input.splic b/compiler/tests/snap/full/pi_staging_hof/0_input.splic index 06b7c9e..001f69a 100644 --- a/compiler/tests/snap/full/pi_staging_hof/0_input.splic +++ b/compiler/tests/snap/full/pi_staging_hof/0_input.splic @@ -1,5 +1,5 @@ // Higher-order staging: meta function that transforms code via a lambda -fn map_code(f: fn([[u64]]) -> [[u64]], x: [[u64]]) -> [[u64]] { +fn map_code(f: fn(_: [[u64]]) -> [[u64]], x: [[u64]]) -> [[u64]] { f(x) } diff --git a/compiler/tests/snap/full/splice_meta_int/3_check.txt b/compiler/tests/snap/full/splice_meta_int/3_check.txt index 68b9fc6..2b3221f 100644 --- a/compiler/tests/snap/full/splice_meta_int/3_check.txt +++ b/compiler/tests/snap/full/splice_meta_int/3_check.txt @@ -3,6 +3,6 @@ fn val() -> u32 { } code fn use_val() -> u32 { - $(@embed_u32(val())) + $(@embed_u32(val)) } diff --git a/compiler/tests/snap/full/staging/3_check.txt b/compiler/tests/snap/full/staging/3_check.txt index 8513c7a..8427053 100644 --- a/compiler/tests/snap/full/staging/3_check.txt +++ b/compiler/tests/snap/full/staging/3_check.txt @@ -3,6 +3,6 @@ fn k() -> [[u64]] { } code fn zero() -> u64 { - $(k()) + $(k) } diff --git a/compiler/tests/snap/stage_error/add_overflow_u32/3_check.txt b/compiler/tests/snap/stage_error/add_overflow_u32/3_check.txt index 962a00f..5cd8e62 100644 --- a/compiler/tests/snap/stage_error/add_overflow_u32/3_check.txt +++ b/compiler/tests/snap/stage_error/add_overflow_u32/3_check.txt @@ -3,6 +3,6 @@ fn f() -> u32 { } code fn g() -> u32 { - $(@embed_u32(f())) + $(@embed_u32(f)) } diff --git a/compiler/tests/snap/stage_error/add_overflow_u8/3_check.txt b/compiler/tests/snap/stage_error/add_overflow_u8/3_check.txt index f0e414d..80551d5 100644 --- a/compiler/tests/snap/stage_error/add_overflow_u8/3_check.txt +++ b/compiler/tests/snap/stage_error/add_overflow_u8/3_check.txt @@ -3,6 +3,6 @@ fn f() -> u8 { } code fn g() -> u8 { - $(@embed_u8(f())) + $(@embed_u8(f)) } diff --git a/compiler/tests/snap/stage_error/mul_overflow_u8/3_check.txt b/compiler/tests/snap/stage_error/mul_overflow_u8/3_check.txt index ec0f18b..280e10c 100644 --- a/compiler/tests/snap/stage_error/mul_overflow_u8/3_check.txt +++ b/compiler/tests/snap/stage_error/mul_overflow_u8/3_check.txt @@ -3,6 +3,6 @@ fn f() -> u8 { } code fn g() -> u8 { - $(@embed_u8(f())) + $(@embed_u8(f)) } diff --git a/compiler/tests/snap/stage_error/sub_underflow_u8/3_check.txt b/compiler/tests/snap/stage_error/sub_underflow_u8/3_check.txt index 20bf244..4169cf8 100644 --- a/compiler/tests/snap/stage_error/sub_underflow_u8/3_check.txt +++ b/compiler/tests/snap/stage_error/sub_underflow_u8/3_check.txt @@ -3,6 +3,6 @@ fn f() -> u8 { } code fn g() -> u8 { - $(@embed_u8(f())) + $(@embed_u8(f)) } From c1a090cf31f02788b3d37648c3a93a7f33bdace6 Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 07:35:46 +0000 Subject: [PATCH 04/43] test: sep function tests --- .../tests/snap/full/pi_apply_non_fn/1_lex.txt | 19 +++ .../snap/full/pi_apply_non_fn/2_parse.txt | 35 +++++ .../snap/full/pi_apply_non_fn/3_check.txt | 2 + .../snap/full/pi_arity_mismatch/1_lex.txt | 28 ++++ .../snap/full/pi_arity_mismatch/2_parse.txt | 49 +++++++ .../snap/full/pi_arity_mismatch/3_check.txt | 2 + compiler/tests/snap/full/pi_basic/1_lex.txt | 68 +++++++++ compiler/tests/snap/full/pi_basic/2_parse.txt | 114 +++++++++++++++ compiler/tests/snap/full/pi_basic/3_check.txt | 16 +++ compiler/tests/snap/full/pi_basic/6_stage.txt | 4 + compiler/tests/snap/full/pi_compose/1_lex.txt | 107 ++++++++++++++ .../tests/snap/full/pi_compose/2_parse.txt | 2 + compiler/tests/snap/full/pi_const/1_lex.txt | 74 ++++++++++ compiler/tests/snap/full/pi_const/2_parse.txt | 2 + .../snap/full/pi_dependent_ret/1_lex.txt | 88 ++++++++++++ .../snap/full/pi_dependent_ret/2_parse.txt | 133 ++++++++++++++++++ .../snap/full/pi_dependent_ret/3_check.txt | 20 +++ .../snap/full/pi_dependent_ret/6_stage.txt | 8 ++ .../tests/snap/full/pi_lambda_arg/1_lex.txt | 61 ++++++++ .../tests/snap/full/pi_lambda_arg/2_parse.txt | 2 + .../snap/full/pi_lambda_in_object/1_lex.txt | 18 +++ .../snap/full/pi_lambda_in_object/2_parse.txt | 2 + .../pi_lambda_missing_annotation/1_lex.txt | 45 ++++++ .../pi_lambda_missing_annotation/2_parse.txt | 2 + .../full/pi_lambda_type_mismatch/1_lex.txt | 45 ++++++ .../full/pi_lambda_type_mismatch/2_parse.txt | 2 + compiler/tests/snap/full/pi_nested/1_lex.txt | 71 ++++++++++ .../tests/snap/full/pi_nested/2_parse.txt | 119 ++++++++++++++++ .../tests/snap/full/pi_nested/3_check.txt | 16 +++ .../tests/snap/full/pi_nested/6_stage.txt | 4 + .../tests/snap/full/pi_polycompose/1_lex.txt | 123 ++++++++++++++++ .../snap/full/pi_polycompose/2_parse.txt | 2 + .../snap/full/pi_polymorphic_id/1_lex.txt | 72 ++++++++++ .../snap/full/pi_polymorphic_id/2_parse.txt | 109 ++++++++++++++ .../snap/full/pi_polymorphic_id/3_check.txt | 20 +++ .../snap/full/pi_polymorphic_id/6_stage.txt | 8 ++ compiler/tests/snap/full/pi_repeat/1_lex.txt | 97 +++++++++++++ .../tests/snap/full/pi_repeat/2_parse.txt | 2 + .../tests/snap/full/pi_staging_hof/1_lex.txt | 71 ++++++++++ .../snap/full/pi_staging_hof/2_parse.txt | 2 + 40 files changed, 1664 insertions(+) create mode 100644 compiler/tests/snap/full/pi_apply_non_fn/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_apply_non_fn/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_apply_non_fn/3_check.txt create mode 100644 compiler/tests/snap/full/pi_arity_mismatch/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_arity_mismatch/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_arity_mismatch/3_check.txt create mode 100644 compiler/tests/snap/full/pi_basic/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_basic/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_basic/3_check.txt create mode 100644 compiler/tests/snap/full/pi_basic/6_stage.txt create mode 100644 compiler/tests/snap/full/pi_compose/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_compose/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_const/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_const/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_dependent_ret/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_dependent_ret/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_dependent_ret/3_check.txt create mode 100644 compiler/tests/snap/full/pi_dependent_ret/6_stage.txt create mode 100644 compiler/tests/snap/full/pi_lambda_arg/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_lambda_arg/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_lambda_in_object/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_lambda_in_object/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_lambda_missing_annotation/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_lambda_missing_annotation/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_lambda_type_mismatch/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_nested/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_nested/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_nested/3_check.txt create mode 100644 compiler/tests/snap/full/pi_nested/6_stage.txt create mode 100644 compiler/tests/snap/full/pi_polycompose/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_polycompose/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_polymorphic_id/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_polymorphic_id/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_polymorphic_id/3_check.txt create mode 100644 compiler/tests/snap/full/pi_polymorphic_id/6_stage.txt create mode 100644 compiler/tests/snap/full/pi_repeat/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_repeat/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_staging_hof/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_staging_hof/2_parse.txt diff --git a/compiler/tests/snap/full/pi_apply_non_fn/1_lex.txt b/compiler/tests/snap/full/pi_apply_non_fn/1_lex.txt new file mode 100644 index 0000000..c417ba6 --- /dev/null +++ b/compiler/tests/snap/full/pi_apply_non_fn/1_lex.txt @@ -0,0 +1,19 @@ +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u64") +LBrace +Let +Ident("x") +Colon +Ident("u64") +Eq +Num(42) +Semi +Ident("x") +LParen +Num(1) +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_apply_non_fn/2_parse.txt b/compiler/tests/snap/full/pi_apply_non_fn/2_parse.txt new file mode 100644 index 0000000..239bf1e --- /dev/null +++ b/compiler/tests/snap/full/pi_apply_non_fn/2_parse.txt @@ -0,0 +1,35 @@ +Program { + functions: [ + Function { + phase: Meta, + name: "test", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [ + Let { + name: "x", + ty: Some( + Var( + "u64", + ), + ), + expr: Lit( + 42, + ), + }, + ], + expr: App { + func: "x", + args: [ + Lit( + 1, + ), + ], + }, + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt b/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt new file mode 100644 index 0000000..d774b96 --- /dev/null +++ b/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt @@ -0,0 +1,2 @@ +ERROR +in function `test`: too many arguments: function `x` expects 0 argument(s), got 1 diff --git a/compiler/tests/snap/full/pi_arity_mismatch/1_lex.txt b/compiler/tests/snap/full/pi_arity_mismatch/1_lex.txt new file mode 100644 index 0000000..d009de6 --- /dev/null +++ b/compiler/tests/snap/full/pi_arity_mismatch/1_lex.txt @@ -0,0 +1,28 @@ +Fn +Ident("apply") +LParen +Ident("f") +Colon +Fn +LParen +Ident("_") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +Comma +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("f") +LParen +Ident("x") +Comma +Ident("x") +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_arity_mismatch/2_parse.txt b/compiler/tests/snap/full/pi_arity_mismatch/2_parse.txt new file mode 100644 index 0000000..a5b9275 --- /dev/null +++ b/compiler/tests/snap/full/pi_arity_mismatch/2_parse.txt @@ -0,0 +1,49 @@ +Program { + functions: [ + Function { + phase: Meta, + name: "apply", + params: [ + Param { + name: "f", + ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + }, + }, + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: "f", + args: [ + Var( + "x", + ), + Var( + "x", + ), + ], + }, + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt b/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt new file mode 100644 index 0000000..22ef48e --- /dev/null +++ b/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt @@ -0,0 +1,2 @@ +ERROR +in function `apply`: too many arguments: function `f` expects 1 argument(s), got 2 diff --git a/compiler/tests/snap/full/pi_basic/1_lex.txt b/compiler/tests/snap/full/pi_basic/1_lex.txt new file mode 100644 index 0000000..07abe97 --- /dev/null +++ b/compiler/tests/snap/full/pi_basic/1_lex.txt @@ -0,0 +1,68 @@ +Fn +Ident("apply") +LParen +Ident("f") +Colon +Fn +LParen +Ident("_") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +Comma +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("f") +LParen +Ident("x") +RParen +RBrace +Fn +Ident("inc") +LParen +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("x") +Plus +Num(1) +RBrace +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("apply") +LParen +Ident("inc") +Comma +Num(42) +RParen +RBrace +Code +Fn +Ident("result") +LParen +RParen +Arrow +Ident("u64") +LBrace +DollarLParen +Ident("test") +LParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_basic/2_parse.txt b/compiler/tests/snap/full/pi_basic/2_parse.txt new file mode 100644 index 0000000..9933050 --- /dev/null +++ b/compiler/tests/snap/full/pi_basic/2_parse.txt @@ -0,0 +1,114 @@ +Program { + functions: [ + Function { + phase: Meta, + name: "apply", + params: [ + Param { + name: "f", + ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + }, + }, + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: "f", + args: [ + Var( + "x", + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "inc", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: Add, + args: [ + Var( + "x", + ), + Lit( + 1, + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "test", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: "apply", + args: [ + Var( + "inc", + ), + Lit( + 42, + ), + ], + }, + }, + }, + Function { + phase: Object, + name: "result", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: "test", + args: [], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_basic/3_check.txt b/compiler/tests/snap/full/pi_basic/3_check.txt new file mode 100644 index 0000000..4863cfd --- /dev/null +++ b/compiler/tests/snap/full/pi_basic/3_check.txt @@ -0,0 +1,16 @@ +fn apply(f@0: fn(u64) -> u64, x@1: u64) -> u64 { + f@0(x@1) +} + +fn inc(x@0: u64) -> u64 { + @add_u64(x@0, 1_u64) +} + +fn test() -> u64 { + apply(inc, 42_u64) +} + +code fn result() -> u64 { + $(@embed_u64(test)) +} + diff --git a/compiler/tests/snap/full/pi_basic/6_stage.txt b/compiler/tests/snap/full/pi_basic/6_stage.txt new file mode 100644 index 0000000..0ae8d94 --- /dev/null +++ b/compiler/tests/snap/full/pi_basic/6_stage.txt @@ -0,0 +1,4 @@ +code fn result() -> u64 { + 43_u64 +} + diff --git a/compiler/tests/snap/full/pi_compose/1_lex.txt b/compiler/tests/snap/full/pi_compose/1_lex.txt new file mode 100644 index 0000000..72121d6 --- /dev/null +++ b/compiler/tests/snap/full/pi_compose/1_lex.txt @@ -0,0 +1,107 @@ +Fn +Ident("compose") +LParen +Ident("f") +Colon +Fn +LParen +Ident("_") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +Comma +Ident("g") +Colon +Fn +LParen +Ident("_") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +RParen +Arrow +Fn +LParen +Ident("_") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Bar +Ident("x") +Colon +Ident("u64") +Bar +Ident("f") +LParen +Ident("g") +LParen +Ident("x") +RParen +RParen +RBrace +Fn +Ident("double") +LParen +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("x") +Plus +Ident("x") +RBrace +Fn +Ident("inc") +LParen +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("x") +Plus +Num(1) +RBrace +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("compose") +LParen +Ident("double") +Comma +Ident("inc") +RParen +LParen +Num(5) +RParen +RBrace +Code +Fn +Ident("result") +LParen +RParen +Arrow +Ident("u64") +LBrace +DollarLParen +Ident("test") +LParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_compose/2_parse.txt b/compiler/tests/snap/full/pi_compose/2_parse.txt new file mode 100644 index 0000000..19dcab7 --- /dev/null +++ b/compiler/tests/snap/full/pi_compose/2_parse.txt @@ -0,0 +1,2 @@ +ERROR +in function `compose`: expected function body: parsing expression in block: expected '|' after lambda parameters: expected Bar, got RBrace diff --git a/compiler/tests/snap/full/pi_const/1_lex.txt b/compiler/tests/snap/full/pi_const/1_lex.txt new file mode 100644 index 0000000..26dee3f --- /dev/null +++ b/compiler/tests/snap/full/pi_const/1_lex.txt @@ -0,0 +1,74 @@ +Fn +Ident("const_") +LParen +Ident("A") +Colon +Ident("Type") +Comma +Ident("B") +Colon +Ident("Type") +RParen +Arrow +Fn +LParen +Ident("_") +Colon +Ident("A") +RParen +Arrow +Fn +LParen +Ident("_") +Colon +Ident("B") +RParen +Arrow +Ident("A") +LBrace +Bar +Ident("a") +Colon +Ident("A") +Bar +Bar +Ident("b") +Colon +Ident("B") +Bar +Ident("a") +RBrace +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("const_") +LParen +Ident("u64") +Comma +Ident("u8") +RParen +LParen +Num(42) +RParen +LParen +Num(7) +RParen +RBrace +Code +Fn +Ident("result") +LParen +RParen +Arrow +Ident("u64") +LBrace +DollarLParen +Ident("test") +LParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_const/2_parse.txt b/compiler/tests/snap/full/pi_const/2_parse.txt new file mode 100644 index 0000000..faf4c41 --- /dev/null +++ b/compiler/tests/snap/full/pi_const/2_parse.txt @@ -0,0 +1,2 @@ +ERROR +in function `const_`: expected function body: parsing expression in block: expected parameter type: parsing right-hand side of binary expression: expected '|' after lambda parameters: expected Bar, got RBrace diff --git a/compiler/tests/snap/full/pi_dependent_ret/1_lex.txt b/compiler/tests/snap/full/pi_dependent_ret/1_lex.txt new file mode 100644 index 0000000..1dcecd2 --- /dev/null +++ b/compiler/tests/snap/full/pi_dependent_ret/1_lex.txt @@ -0,0 +1,88 @@ +Fn +Ident("const_") +LParen +Ident("A") +Colon +Ident("Type") +Comma +Ident("B") +Colon +Ident("Type") +Comma +Ident("a") +Colon +Ident("A") +Comma +Ident("b") +Colon +Ident("B") +RParen +Arrow +Ident("A") +LBrace +Ident("a") +RBrace +Fn +Ident("test_u64") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("const_") +LParen +Ident("u64") +Comma +Ident("u8") +Comma +Num(10) +Comma +Num(7) +RParen +RBrace +Fn +Ident("test_u8") +LParen +RParen +Arrow +Ident("u8") +LBrace +Ident("const_") +LParen +Ident("u8") +Comma +Ident("u64") +Comma +Num(7) +Comma +Num(10) +RParen +RBrace +Code +Fn +Ident("result_u64") +LParen +RParen +Arrow +Ident("u64") +LBrace +DollarLParen +Ident("test_u64") +LParen +RParen +RParen +RBrace +Code +Fn +Ident("result_u8") +LParen +RParen +Arrow +Ident("u8") +LBrace +DollarLParen +Ident("test_u8") +LParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_dependent_ret/2_parse.txt b/compiler/tests/snap/full/pi_dependent_ret/2_parse.txt new file mode 100644 index 0000000..e8ca932 --- /dev/null +++ b/compiler/tests/snap/full/pi_dependent_ret/2_parse.txt @@ -0,0 +1,133 @@ +Program { + functions: [ + Function { + phase: Meta, + name: "const_", + params: [ + Param { + name: "A", + ty: Var( + "Type", + ), + }, + Param { + name: "B", + ty: Var( + "Type", + ), + }, + Param { + name: "a", + ty: Var( + "A", + ), + }, + Param { + name: "b", + ty: Var( + "B", + ), + }, + ], + ret_ty: Var( + "A", + ), + body: Block { + stmts: [], + expr: Var( + "a", + ), + }, + }, + Function { + phase: Meta, + name: "test_u64", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: "const_", + args: [ + Var( + "u64", + ), + Var( + "u8", + ), + Lit( + 10, + ), + Lit( + 7, + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "test_u8", + params: [], + ret_ty: Var( + "u8", + ), + body: Block { + stmts: [], + expr: App { + func: "const_", + args: [ + Var( + "u8", + ), + Var( + "u64", + ), + Lit( + 7, + ), + Lit( + 10, + ), + ], + }, + }, + }, + Function { + phase: Object, + name: "result_u64", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: "test_u64", + args: [], + }, + ), + }, + }, + Function { + phase: Object, + name: "result_u8", + params: [], + ret_ty: Var( + "u8", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: "test_u8", + args: [], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_dependent_ret/3_check.txt b/compiler/tests/snap/full/pi_dependent_ret/3_check.txt new file mode 100644 index 0000000..1b5782f --- /dev/null +++ b/compiler/tests/snap/full/pi_dependent_ret/3_check.txt @@ -0,0 +1,20 @@ +fn const_(A@0: Type, B@1: Type, a@2: A@0, b@3: B@1) -> A@0 { + a@2 +} + +fn test_u64() -> u64 { + const_(u64, u8, 10_u64, 7_u8) +} + +fn test_u8() -> u8 { + const_(u8, u64, 7_u8, 10_u64) +} + +code fn result_u64() -> u64 { + $(@embed_u64(test_u64)) +} + +code fn result_u8() -> u8 { + $(@embed_u8(test_u8)) +} + diff --git a/compiler/tests/snap/full/pi_dependent_ret/6_stage.txt b/compiler/tests/snap/full/pi_dependent_ret/6_stage.txt new file mode 100644 index 0000000..2d777e3 --- /dev/null +++ b/compiler/tests/snap/full/pi_dependent_ret/6_stage.txt @@ -0,0 +1,8 @@ +code fn result_u64() -> u64 { + 10_u64 +} + +code fn result_u8() -> u8 { + 7_u8 +} + diff --git a/compiler/tests/snap/full/pi_lambda_arg/1_lex.txt b/compiler/tests/snap/full/pi_lambda_arg/1_lex.txt new file mode 100644 index 0000000..175ebe7 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_arg/1_lex.txt @@ -0,0 +1,61 @@ +Fn +Ident("apply") +LParen +Ident("f") +Colon +Fn +LParen +Ident("_") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +Comma +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("f") +LParen +Ident("x") +RParen +RBrace +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("apply") +LParen +Bar +Ident("x") +Colon +Ident("u64") +Bar +Ident("x") +Plus +Num(1) +Comma +Num(42) +RParen +RBrace +Code +Fn +Ident("result") +LParen +RParen +Arrow +Ident("u64") +LBrace +DollarLParen +Ident("test") +LParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_lambda_arg/2_parse.txt b/compiler/tests/snap/full/pi_lambda_arg/2_parse.txt new file mode 100644 index 0000000..bb309ff --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_arg/2_parse.txt @@ -0,0 +1,2 @@ +ERROR +in function `test`: expected function body: parsing expression in block: parsing function argument: expected parameter name in lambda: expected identifier, got Num(42) diff --git a/compiler/tests/snap/full/pi_lambda_in_object/1_lex.txt b/compiler/tests/snap/full/pi_lambda_in_object/1_lex.txt new file mode 100644 index 0000000..6f1a5b5 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_in_object/1_lex.txt @@ -0,0 +1,18 @@ +Code +Fn +Ident("test") +LParen +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Bar +Ident("y") +Colon +Ident("u64") +Bar +Ident("y") +RBrace diff --git a/compiler/tests/snap/full/pi_lambda_in_object/2_parse.txt b/compiler/tests/snap/full/pi_lambda_in_object/2_parse.txt new file mode 100644 index 0000000..9251dfa --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_in_object/2_parse.txt @@ -0,0 +1,2 @@ +ERROR +in function `test`: expected function body: parsing expression in block: expected '|' after lambda parameters: expected Bar, got RBrace diff --git a/compiler/tests/snap/full/pi_lambda_missing_annotation/1_lex.txt b/compiler/tests/snap/full/pi_lambda_missing_annotation/1_lex.txt new file mode 100644 index 0000000..ced7ad1 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_missing_annotation/1_lex.txt @@ -0,0 +1,45 @@ +Fn +Ident("apply") +LParen +Ident("f") +Colon +Fn +LParen +Ident("_") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +Comma +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("f") +LParen +Ident("x") +RParen +RBrace +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("apply") +LParen +Bar +Ident("x") +Bar +Ident("x") +Plus +Num(1) +Comma +Num(42) +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_lambda_missing_annotation/2_parse.txt b/compiler/tests/snap/full/pi_lambda_missing_annotation/2_parse.txt new file mode 100644 index 0000000..5dad2f0 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_missing_annotation/2_parse.txt @@ -0,0 +1,2 @@ +ERROR +in function `test`: expected function body: parsing expression in block: parsing function argument: expected ':' in lambda parameter (type annotations are required): expected Colon, got Bar diff --git a/compiler/tests/snap/full/pi_lambda_type_mismatch/1_lex.txt b/compiler/tests/snap/full/pi_lambda_type_mismatch/1_lex.txt new file mode 100644 index 0000000..b5fda56 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_type_mismatch/1_lex.txt @@ -0,0 +1,45 @@ +Fn +Ident("apply") +LParen +Ident("f") +Colon +Fn +LParen +Ident("_") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +Comma +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("f") +LParen +Ident("x") +RParen +RBrace +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("apply") +LParen +Bar +Ident("x") +Colon +Ident("u32") +Bar +Ident("x") +Comma +Num(42) +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt b/compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt new file mode 100644 index 0000000..bb309ff --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt @@ -0,0 +1,2 @@ +ERROR +in function `test`: expected function body: parsing expression in block: parsing function argument: expected parameter name in lambda: expected identifier, got Num(42) diff --git a/compiler/tests/snap/full/pi_nested/1_lex.txt b/compiler/tests/snap/full/pi_nested/1_lex.txt new file mode 100644 index 0000000..5d36ff6 --- /dev/null +++ b/compiler/tests/snap/full/pi_nested/1_lex.txt @@ -0,0 +1,71 @@ +Fn +Ident("apply_twice") +LParen +Ident("f") +Colon +Fn +LParen +Ident("_") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +Comma +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("f") +LParen +Ident("f") +LParen +Ident("x") +RParen +RParen +RBrace +Fn +Ident("inc") +LParen +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("x") +Plus +Num(1) +RBrace +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("apply_twice") +LParen +Ident("inc") +Comma +Num(0) +RParen +RBrace +Code +Fn +Ident("result") +LParen +RParen +Arrow +Ident("u64") +LBrace +DollarLParen +Ident("test") +LParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_nested/2_parse.txt b/compiler/tests/snap/full/pi_nested/2_parse.txt new file mode 100644 index 0000000..a140edc --- /dev/null +++ b/compiler/tests/snap/full/pi_nested/2_parse.txt @@ -0,0 +1,119 @@ +Program { + functions: [ + Function { + phase: Meta, + name: "apply_twice", + params: [ + Param { + name: "f", + ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + }, + }, + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: "f", + args: [ + App { + func: "f", + args: [ + Var( + "x", + ), + ], + }, + ], + }, + }, + }, + Function { + phase: Meta, + name: "inc", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: Add, + args: [ + Var( + "x", + ), + Lit( + 1, + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "test", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: "apply_twice", + args: [ + Var( + "inc", + ), + Lit( + 0, + ), + ], + }, + }, + }, + Function { + phase: Object, + name: "result", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: "test", + args: [], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_nested/3_check.txt b/compiler/tests/snap/full/pi_nested/3_check.txt new file mode 100644 index 0000000..67dba9c --- /dev/null +++ b/compiler/tests/snap/full/pi_nested/3_check.txt @@ -0,0 +1,16 @@ +fn apply_twice(f@0: fn(u64) -> u64, x@1: u64) -> u64 { + f@0(f@0(x@1)) +} + +fn inc(x@0: u64) -> u64 { + @add_u64(x@0, 1_u64) +} + +fn test() -> u64 { + apply_twice(inc, 0_u64) +} + +code fn result() -> u64 { + $(@embed_u64(test)) +} + diff --git a/compiler/tests/snap/full/pi_nested/6_stage.txt b/compiler/tests/snap/full/pi_nested/6_stage.txt new file mode 100644 index 0000000..f4f672a --- /dev/null +++ b/compiler/tests/snap/full/pi_nested/6_stage.txt @@ -0,0 +1,4 @@ +code fn result() -> u64 { + 2_u64 +} + diff --git a/compiler/tests/snap/full/pi_polycompose/1_lex.txt b/compiler/tests/snap/full/pi_polycompose/1_lex.txt new file mode 100644 index 0000000..02a37a6 --- /dev/null +++ b/compiler/tests/snap/full/pi_polycompose/1_lex.txt @@ -0,0 +1,123 @@ +Fn +Ident("compose") +LParen +Ident("A") +Colon +Ident("Type") +Comma +Ident("B") +Colon +Ident("Type") +Comma +Ident("C") +Colon +Ident("Type") +Comma +Ident("f") +Colon +Fn +LParen +Ident("_") +Colon +Ident("B") +RParen +Arrow +Ident("C") +Comma +Ident("g") +Colon +Fn +LParen +Ident("_") +Colon +Ident("A") +RParen +Arrow +Ident("B") +RParen +Arrow +Fn +LParen +Ident("_") +Colon +Ident("A") +RParen +Arrow +Ident("C") +LBrace +Bar +Ident("x") +Colon +Ident("A") +Bar +Ident("f") +LParen +Ident("g") +LParen +Ident("x") +RParen +RParen +RBrace +Fn +Ident("double") +LParen +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("x") +Plus +Ident("x") +RBrace +Fn +Ident("to_u8") +LParen +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u8") +LBrace +Ident("x") +RBrace +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u8") +LBrace +Ident("compose") +LParen +Ident("u64") +Comma +Ident("u64") +Comma +Ident("u8") +Comma +Ident("to_u8") +Comma +Ident("double") +RParen +LParen +Num(5) +RParen +RBrace +Code +Fn +Ident("result") +LParen +RParen +Arrow +Ident("u8") +LBrace +DollarLParen +Ident("test") +LParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_polycompose/2_parse.txt b/compiler/tests/snap/full/pi_polycompose/2_parse.txt new file mode 100644 index 0000000..19dcab7 --- /dev/null +++ b/compiler/tests/snap/full/pi_polycompose/2_parse.txt @@ -0,0 +1,2 @@ +ERROR +in function `compose`: expected function body: parsing expression in block: expected '|' after lambda parameters: expected Bar, got RBrace diff --git a/compiler/tests/snap/full/pi_polymorphic_id/1_lex.txt b/compiler/tests/snap/full/pi_polymorphic_id/1_lex.txt new file mode 100644 index 0000000..f19a724 --- /dev/null +++ b/compiler/tests/snap/full/pi_polymorphic_id/1_lex.txt @@ -0,0 +1,72 @@ +Fn +Ident("id") +LParen +Ident("A") +Colon +Ident("Type") +Comma +Ident("x") +Colon +Ident("A") +RParen +Arrow +Ident("A") +LBrace +Ident("x") +RBrace +Fn +Ident("test_u64") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("id") +LParen +Ident("u64") +Comma +Num(42) +RParen +RBrace +Fn +Ident("test_u8") +LParen +RParen +Arrow +Ident("u8") +LBrace +Ident("id") +LParen +Ident("u8") +Comma +Num(7) +RParen +RBrace +Code +Fn +Ident("result_u64") +LParen +RParen +Arrow +Ident("u64") +LBrace +DollarLParen +Ident("test_u64") +LParen +RParen +RParen +RBrace +Code +Fn +Ident("result_u8") +LParen +RParen +Arrow +Ident("u8") +LBrace +DollarLParen +Ident("test_u8") +LParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_polymorphic_id/2_parse.txt b/compiler/tests/snap/full/pi_polymorphic_id/2_parse.txt new file mode 100644 index 0000000..eb8762b --- /dev/null +++ b/compiler/tests/snap/full/pi_polymorphic_id/2_parse.txt @@ -0,0 +1,109 @@ +Program { + functions: [ + Function { + phase: Meta, + name: "id", + params: [ + Param { + name: "A", + ty: Var( + "Type", + ), + }, + Param { + name: "x", + ty: Var( + "A", + ), + }, + ], + ret_ty: Var( + "A", + ), + body: Block { + stmts: [], + expr: Var( + "x", + ), + }, + }, + Function { + phase: Meta, + name: "test_u64", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: "id", + args: [ + Var( + "u64", + ), + Lit( + 42, + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "test_u8", + params: [], + ret_ty: Var( + "u8", + ), + body: Block { + stmts: [], + expr: App { + func: "id", + args: [ + Var( + "u8", + ), + Lit( + 7, + ), + ], + }, + }, + }, + Function { + phase: Object, + name: "result_u64", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: "test_u64", + args: [], + }, + ), + }, + }, + Function { + phase: Object, + name: "result_u8", + params: [], + ret_ty: Var( + "u8", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: "test_u8", + args: [], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_polymorphic_id/3_check.txt b/compiler/tests/snap/full/pi_polymorphic_id/3_check.txt new file mode 100644 index 0000000..f7f7d2a --- /dev/null +++ b/compiler/tests/snap/full/pi_polymorphic_id/3_check.txt @@ -0,0 +1,20 @@ +fn id(A@0: Type, x@1: A@0) -> A@0 { + x@1 +} + +fn test_u64() -> u64 { + id(u64, 42_u64) +} + +fn test_u8() -> u8 { + id(u8, 7_u8) +} + +code fn result_u64() -> u64 { + $(@embed_u64(test_u64)) +} + +code fn result_u8() -> u8 { + $(@embed_u8(test_u8)) +} + diff --git a/compiler/tests/snap/full/pi_polymorphic_id/6_stage.txt b/compiler/tests/snap/full/pi_polymorphic_id/6_stage.txt new file mode 100644 index 0000000..83bfdca --- /dev/null +++ b/compiler/tests/snap/full/pi_polymorphic_id/6_stage.txt @@ -0,0 +1,8 @@ +code fn result_u64() -> u64 { + 42_u64 +} + +code fn result_u8() -> u8 { + 7_u8 +} + diff --git a/compiler/tests/snap/full/pi_repeat/1_lex.txt b/compiler/tests/snap/full/pi_repeat/1_lex.txt new file mode 100644 index 0000000..af4e85a --- /dev/null +++ b/compiler/tests/snap/full/pi_repeat/1_lex.txt @@ -0,0 +1,97 @@ +Fn +Ident("repeat") +LParen +Ident("f") +Colon +Fn +LParen +Ident("_") +Colon +DoubleLBracket +Ident("u64") +DoubleRBracket +RParen +Arrow +DoubleLBracket +Ident("u64") +DoubleRBracket +Comma +Ident("n") +Colon +Ident("u64") +Comma +Ident("x") +Colon +DoubleLBracket +Ident("u64") +DoubleRBracket +RParen +Arrow +DoubleLBracket +Ident("u64") +DoubleRBracket +LBrace +Match +Ident("n") +LBrace +Num(0) +DArrow +Ident("x") +Comma +Ident("n") +DArrow +Ident("repeat") +LParen +Ident("f") +Comma +Ident("n") +Minus +Num(1) +Comma +Ident("f") +LParen +Ident("x") +RParen +RParen +Comma +RBrace +RBrace +Code +Fn +Ident("square_twice") +LParen +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +DollarLParen +Ident("repeat") +LParen +Bar +Ident("y") +Colon +DoubleLBracket +Ident("u64") +DoubleRBracket +Bar +HashLParen +DollarLParen +Ident("y") +RParen +Star +DollarLParen +Ident("y") +RParen +RParen +Comma +Num(2) +Comma +HashLParen +Ident("x") +RParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_repeat/2_parse.txt b/compiler/tests/snap/full/pi_repeat/2_parse.txt new file mode 100644 index 0000000..c4cc2e4 --- /dev/null +++ b/compiler/tests/snap/full/pi_repeat/2_parse.txt @@ -0,0 +1,2 @@ +ERROR +in function `square_twice`: expected function body: parsing expression in block: parsing spliced expression: parsing function argument: expected parameter name in lambda: expected identifier, got Num(2) diff --git a/compiler/tests/snap/full/pi_staging_hof/1_lex.txt b/compiler/tests/snap/full/pi_staging_hof/1_lex.txt new file mode 100644 index 0000000..4257536 --- /dev/null +++ b/compiler/tests/snap/full/pi_staging_hof/1_lex.txt @@ -0,0 +1,71 @@ +Fn +Ident("map_code") +LParen +Ident("f") +Colon +Fn +LParen +Ident("_") +Colon +DoubleLBracket +Ident("u64") +DoubleRBracket +RParen +Arrow +DoubleLBracket +Ident("u64") +DoubleRBracket +Comma +Ident("x") +Colon +DoubleLBracket +Ident("u64") +DoubleRBracket +RParen +Arrow +DoubleLBracket +Ident("u64") +DoubleRBracket +LBrace +Ident("f") +LParen +Ident("x") +RParen +RBrace +Code +Fn +Ident("double") +LParen +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +DollarLParen +Ident("map_code") +LParen +Bar +Ident("y") +Colon +DoubleLBracket +Ident("u64") +DoubleRBracket +Bar +HashLParen +DollarLParen +Ident("y") +RParen +Plus +DollarLParen +Ident("y") +RParen +RParen +Comma +HashLParen +Ident("x") +RParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_staging_hof/2_parse.txt b/compiler/tests/snap/full/pi_staging_hof/2_parse.txt new file mode 100644 index 0000000..40b569e --- /dev/null +++ b/compiler/tests/snap/full/pi_staging_hof/2_parse.txt @@ -0,0 +1,2 @@ +ERROR +in function `double`: expected function body: parsing expression in block: parsing spliced expression: parsing function argument: expected parameter name in lambda: expected identifier, got HashLParen From ced72d6b7fb8b0a4db10f7be73e2f59dc63bda7a Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 07:36:37 +0000 Subject: [PATCH 05/43] fix: add forgotten files --- compiler/src/core/alpha_eq.rs | 49 ++++++++++++++++++++ compiler/src/core/subst.rs | 84 +++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 compiler/src/core/alpha_eq.rs create mode 100644 compiler/src/core/subst.rs diff --git a/compiler/src/core/alpha_eq.rs b/compiler/src/core/alpha_eq.rs new file mode 100644 index 0000000..118aaeb --- /dev/null +++ b/compiler/src/core/alpha_eq.rs @@ -0,0 +1,49 @@ +use super::Term; + +/// Alpha-equality: structural equality ignoring `param_name` fields in Pi/Lam. +pub fn alpha_eq(a: &Term<'_>, b: &Term<'_>) -> bool { + // Fast path: pointer equality + if std::ptr::eq(a, b) { + return true; + } + match (a, b) { + (Term::Var(l1), Term::Var(l2)) => l1 == l2, + (Term::Prim(p1), Term::Prim(p2)) => p1 == p2, + (Term::Lit(n1, t1), Term::Lit(n2, t2)) => n1 == n2 && t1 == t2, + (Term::Global(n1), Term::Global(n2)) => n1 == n2, + (Term::PrimApp(a1), Term::PrimApp(a2)) => { + a1.prim == a2.prim + && a1.args.len() == a2.args.len() + && a1 + .args + .iter() + .zip(a2.args.iter()) + .all(|(x, y)| alpha_eq(x, y)) + } + (Term::Pi(p1), Term::Pi(p2)) => { + alpha_eq(p1.param_ty, p2.param_ty) && alpha_eq(p1.body_ty, p2.body_ty) + } + (Term::Lam(l1), Term::Lam(l2)) => { + alpha_eq(l1.param_ty, l2.param_ty) && alpha_eq(l1.body, l2.body) + } + (Term::FunApp(a1), Term::FunApp(a2)) => { + alpha_eq(a1.func, a2.func) && alpha_eq(a1.arg, a2.arg) + } + (Term::Lift(i1), Term::Lift(i2)) + | (Term::Quote(i1), Term::Quote(i2)) + | (Term::Splice(i1), Term::Splice(i2)) => alpha_eq(i1, i2), + (Term::Let(l1), Term::Let(l2)) => { + alpha_eq(l1.ty, l2.ty) && alpha_eq(l1.expr, l2.expr) && alpha_eq(l1.body, l2.body) + } + (Term::Match(m1), Term::Match(m2)) => { + alpha_eq(m1.scrutinee, m2.scrutinee) + && m1.arms.len() == m2.arms.len() + && m1 + .arms + .iter() + .zip(m2.arms.iter()) + .all(|(a, b)| a.pat == b.pat && alpha_eq(a.body, b.body)) + } + _ => false, + } +} diff --git a/compiler/src/core/subst.rs b/compiler/src/core/subst.rs new file mode 100644 index 0000000..94bdf25 --- /dev/null +++ b/compiler/src/core/subst.rs @@ -0,0 +1,84 @@ +use super::{Arm, FunApp, Lam, Lvl, Pi, Term}; + +/// Substitute `replacement` for `Var(target)` in `term`. +/// +/// Used for dependent return types: when applying `f : Pi(x, A, B)` to `arg`, +/// the result type is `subst(arena, B, target_lvl, arg)`. +pub fn subst<'a>( + arena: &'a bumpalo::Bump, + term: &'a Term<'a>, + target: Lvl, + replacement: &'a Term<'a>, +) -> &'a Term<'a> { + match term { + Term::Var(lvl) if *lvl == target => replacement, + Term::Var(_) | Term::Prim(_) | Term::Lit(..) | Term::Global(_) => term, + + Term::PrimApp(app) => { + let new_args = arena.alloc_slice_fill_iter( + app.args + .iter() + .map(|arg| subst(arena, arg, target, replacement)), + ); + arena.alloc(Term::new_prim_app(app.prim, new_args)) + } + + Term::Pi(pi) => { + let new_param_ty = subst(arena, pi.param_ty, target, replacement); + let new_body_ty = subst(arena, pi.body_ty, target, replacement); + arena.alloc(Term::Pi(Pi { + param_name: pi.param_name, + param_ty: new_param_ty, + body_ty: new_body_ty, + })) + } + + Term::Lam(lam) => { + let new_param_ty = subst(arena, lam.param_ty, target, replacement); + let new_body = subst(arena, lam.body, target, replacement); + arena.alloc(Term::Lam(Lam { + param_name: lam.param_name, + param_ty: new_param_ty, + body: new_body, + })) + } + + Term::FunApp(app) => { + let new_func = subst(arena, app.func, target, replacement); + let new_arg = subst(arena, app.arg, target, replacement); + arena.alloc(Term::FunApp(FunApp { + func: new_func, + arg: new_arg, + })) + } + + Term::Lift(inner) => { + let new_inner = subst(arena, inner, target, replacement); + arena.alloc(Term::Lift(new_inner)) + } + Term::Quote(inner) => { + let new_inner = subst(arena, inner, target, replacement); + arena.alloc(Term::Quote(new_inner)) + } + Term::Splice(inner) => { + let new_inner = subst(arena, inner, target, replacement); + arena.alloc(Term::Splice(new_inner)) + } + + Term::Let(let_) => { + let new_ty = subst(arena, let_.ty, target, replacement); + let new_expr = subst(arena, let_.expr, target, replacement); + let new_body = subst(arena, let_.body, target, replacement); + arena.alloc(Term::new_let(let_.name, new_ty, new_expr, new_body)) + } + + Term::Match(match_) => { + let new_scrutinee = subst(arena, match_.scrutinee, target, replacement); + let new_arms = arena.alloc_slice_fill_iter(match_.arms.iter().map(|arm| Arm { + pat: arm.pat.clone(), + body: subst(arena, arm.body, target, replacement), + })); + arena.alloc(Term::new_match(new_scrutinee, new_arms)) + } + } +} From f112f2cf93d12ce59c5886133cf9bef47a7114c8 Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 09:38:16 +0000 Subject: [PATCH 06/43] chore: clippy --- compiler/src/checker/mod.rs | 186 ++++++++++++++++++++++++++++++++---- compiler/src/core/pretty.rs | 24 ++++- 2 files changed, 191 insertions(+), 19 deletions(-) diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index bfb0e20..c76e3e2 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -110,7 +110,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { // Primitive types inhabit the relevant universe. core::Term::Prim(Prim::IntTy(it)) => core::Term::universe(it.phase), // Type, VmType, and [[T]] all inhabit Type (meta universe). - core::Term::Prim(Prim::U(_)) | core::Term::Lift(_) => &core::Term::TYPE, + core::Term::Prim(Prim::U(_)) | core::Term::Lift(_) | core::Term::Pi(_) => &core::Term::TYPE, // Comparison ops return u1 at the operand phase. core::Term::Prim( @@ -159,9 +159,6 @@ impl<'core, 'globals> Ctx<'core, 'globals> { } }, - // Pi type inhabits Type - core::Term::Pi(_) => &core::Term::TYPE, - // Lam: synthesise Pi from param_ty and body type core::Term::Lam(lam) => { self.push_local(lam.param_name, lam.param_ty); @@ -185,7 +182,20 @@ impl<'core, 'globals> Ctx<'core, 'globals> { let binder_lvl = Lvl(funapp_depth(app.func)); subst(self.arena, pi.body_ty, binder_lvl, app.arg) } - _ => unreachable!("FunApp func must have Pi type (typechecker invariant)"), + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Lift(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => { + unreachable!("FunApp func must have Pi type (typechecker invariant)") + } } } @@ -200,7 +210,20 @@ impl<'core, 'globals> Ctx<'core, 'globals> { let inner_ty = self.type_of(inner); match inner_ty { core::Term::Lift(object_ty) => object_ty, - _ => unreachable!("Splice inner must have Lift type (typechecker invariant)"), + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Pi(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => { + unreachable!("Splice inner must have Lift type (typechecker invariant)") + } } } @@ -365,9 +388,30 @@ fn type_universe<'core>( // E.g. if `A : Type` (= U(Meta)), then A is a meta-level type. core::Term::Var(lvl) => match locals.get(lvl.0)?.1 { core::Term::Prim(Prim::U(phase)) => Some(*phase), - _ => None, + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Pi(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Lift(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => None, }, - _ => None, + core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => None, } } @@ -460,7 +504,18 @@ pub fn infer<'src, 'core>( for (i, arg) in args.iter().enumerate() { let pi = match callee_ty { core::Term::Pi(pi) => pi, - _ => bail!( + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Lift(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => bail!( "too many arguments: function `{name}` expects {i} argument(s), got {}", args.len() ), @@ -508,7 +563,19 @@ pub fn infer<'src, 'core>( let core_arg1 = check(ctx, phase, rhs, operand_ty)?; let op_int_ty = match operand_ty { core::Term::Prim(Prim::IntTy(it)) => *it, - _ => { + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Pi(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Lift(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => { bail!("comparison operands must be integers"); } }; @@ -662,7 +729,18 @@ pub fn infer<'src, 'core>( )); Ok(ctx.alloc(core::Term::Splice(embedded))) } - _ => Err(anyhow!( + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Pi(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => Err(anyhow!( "argument of `$(...)` must have a lifted type `[[T]]` or be a meta-level integer" )), } @@ -693,7 +771,19 @@ fn check_exhaustiveness(scrut_ty: &core::Term<'_>, arms: &[ast::MatchArm<'_>]) - IntWidth::U8 => Some(vec![false; 256]), IntWidth::U16 | IntWidth::U32 | IntWidth::U64 => None, }, - _ => None, + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Pi(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Lift(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => None, }; let mut has_catch_all = false; @@ -843,7 +933,19 @@ pub fn check<'src, 'core>( ); Ok(ctx.alloc(core::Term::Lit(*n, *it))) } - _ => Err(anyhow!("literal `{n}` cannot have a non-integer type")), + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Pi(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Lift(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => Err(anyhow!("literal `{n}` cannot have a non-integer type")), }, // ------------------------------------------------------------------ App { Prim (BinOp) } @@ -863,7 +965,19 @@ pub fn check<'src, 'core>( { let int_ty = match expected { core::Term::Prim(Prim::IntTy(it)) => *it, - _ => { + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Pi(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Lift(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => { bail!("primitive operation requires an integer type") } }; @@ -899,7 +1013,19 @@ pub fn check<'src, 'core>( } => { let int_ty = match expected { core::Term::Prim(Prim::IntTy(it)) => *it, - _ => { + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Pi(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Lift(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => { bail!("primitive operation requires an integer type") } }; @@ -922,7 +1048,20 @@ pub fn check<'src, 'core>( let core_inner = check(ctx, Phase::Object, inner, obj_ty)?; Ok(ctx.alloc(core::Term::Quote(core_inner))) } - _ => Err(anyhow!("quote `#(...)` must have a lifted type `[[T]]`")), + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Pi(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => { + Err(anyhow!("quote `#(...)` must have a lifted type `[[T]]`")) + } }, // ------------------------------------------------------------------ Splice (check mode) @@ -974,7 +1113,20 @@ pub fn check<'src, 'core>( for p in *params { let pi = match current_expected { core::Term::Pi(pi) => pi, - _ => bail!("lambda has more parameters than the expected function type"), + core::Term::Var(_) + | core::Term::Prim(_) + | core::Term::Lit(..) + | core::Term::Global(_) + | core::Term::PrimApp(_) + | core::Term::Lam(_) + | core::Term::FunApp(_) + | core::Term::Lift(_) + | core::Term::Quote(_) + | core::Term::Splice(_) + | core::Term::Let(_) + | core::Term::Match(_) => { + bail!("lambda has more parameters than the expected function type") + } }; let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); diff --git a/compiler/src/core/pretty.rs b/compiler/src/core/pretty.rs index 6110280..7661eff 100644 --- a/compiler/src/core/pretty.rs +++ b/compiler/src/core/pretty.rs @@ -26,7 +26,17 @@ impl<'a> Term<'a> { // Let and Match manage their own indentation internally. Term::Let(_) | Term::Match(_) => self.fmt_term_inline(env, indent, f), // Everything else gets a leading indent. - _ => { + Term::Var(_) + | Term::Prim(_) + | Term::Lit(..) + | Term::Global(_) + | Term::PrimApp(_) + | Term::Pi(_) + | Term::Lam(_) + | Term::FunApp(_) + | Term::Lift(_) + | Term::Quote(_) + | Term::Splice(_) => { write_indent(f, indent)?; self.fmt_term_inline(env, indent, f) } @@ -172,7 +182,17 @@ impl<'a> Term<'a> { write_indent(f, indent)?; write!(f, "}}") } - _ => self.fmt_term_inline(env, indent, f), + Term::Var(_) + | Term::Prim(_) + | Term::Lit(..) + | Term::Global(_) + | Term::PrimApp(_) + | Term::Pi(_) + | Term::Lam(_) + | Term::FunApp(_) + | Term::Lift(_) + | Term::Quote(_) + | Term::Splice(_) => self.fmt_term_inline(env, indent, f), } } From 77fc7fa3b5e6ae948966722f5e182fb5a6466415 Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 10:03:52 +0000 Subject: [PATCH 07/43] fix: owrkaround type parsing in lambdas --- compiler/src/parser/mod.rs | 2 +- .../tests/snap/full/pi_compose/2_parse.txt | 2 +- compiler/tests/snap/full/pi_const/2_parse.txt | 2 +- .../tests/snap/full/pi_lambda_arg/2_parse.txt | 105 ++++++++++- .../snap/full/pi_lambda_in_object/2_parse.txt | 37 +++- .../full/pi_lambda_type_mismatch/2_parse.txt | 80 ++++++++- .../snap/full/pi_polycompose/2_parse.txt | 2 +- .../tests/snap/full/pi_repeat/2_parse.txt | 163 +++++++++++++++++- .../snap/full/pi_staging_hof/2_parse.txt | 115 +++++++++++- 9 files changed, 494 insertions(+), 14 deletions(-) diff --git a/compiler/src/parser/mod.rs b/compiler/src/parser/mod.rs index 1d367fe..e225179 100644 --- a/compiler/src/parser/mod.rs +++ b/compiler/src/parser/mod.rs @@ -375,7 +375,7 @@ where parser .take(Token::Colon) .context("expected ':' in lambda parameter (type annotations are required)")?; - let ty = parser.parse_expr().context("expected parameter type")?; + let ty = parser.parse_atom_owned().context("expected parameter type")?; let ty = parser.arena.alloc(ty); Ok(Param { name, ty }) })?; diff --git a/compiler/tests/snap/full/pi_compose/2_parse.txt b/compiler/tests/snap/full/pi_compose/2_parse.txt index 19dcab7..dbd59a1 100644 --- a/compiler/tests/snap/full/pi_compose/2_parse.txt +++ b/compiler/tests/snap/full/pi_compose/2_parse.txt @@ -1,2 +1,2 @@ ERROR -in function `compose`: expected function body: parsing expression in block: expected '|' after lambda parameters: expected Bar, got RBrace +in function `test`: expected function body: expected '}': expected RBrace, got LParen diff --git a/compiler/tests/snap/full/pi_const/2_parse.txt b/compiler/tests/snap/full/pi_const/2_parse.txt index faf4c41..dbd59a1 100644 --- a/compiler/tests/snap/full/pi_const/2_parse.txt +++ b/compiler/tests/snap/full/pi_const/2_parse.txt @@ -1,2 +1,2 @@ ERROR -in function `const_`: expected function body: parsing expression in block: expected parameter type: parsing right-hand side of binary expression: expected '|' after lambda parameters: expected Bar, got RBrace +in function `test`: expected function body: expected '}': expected RBrace, got LParen diff --git a/compiler/tests/snap/full/pi_lambda_arg/2_parse.txt b/compiler/tests/snap/full/pi_lambda_arg/2_parse.txt index bb309ff..44a8e94 100644 --- a/compiler/tests/snap/full/pi_lambda_arg/2_parse.txt +++ b/compiler/tests/snap/full/pi_lambda_arg/2_parse.txt @@ -1,2 +1,103 @@ -ERROR -in function `test`: expected function body: parsing expression in block: parsing function argument: expected parameter name in lambda: expected identifier, got Num(42) +Program { + functions: [ + Function { + phase: Meta, + name: "apply", + params: [ + Param { + name: "f", + ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + }, + }, + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: "f", + args: [ + Var( + "x", + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "test", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: "apply", + args: [ + Lam { + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + body: App { + func: Add, + args: [ + Var( + "x", + ), + Lit( + 1, + ), + ], + }, + }, + Lit( + 42, + ), + ], + }, + }, + }, + Function { + phase: Object, + name: "result", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: "test", + args: [], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_lambda_in_object/2_parse.txt b/compiler/tests/snap/full/pi_lambda_in_object/2_parse.txt index 9251dfa..9932c1f 100644 --- a/compiler/tests/snap/full/pi_lambda_in_object/2_parse.txt +++ b/compiler/tests/snap/full/pi_lambda_in_object/2_parse.txt @@ -1,2 +1,35 @@ -ERROR -in function `test`: expected function body: parsing expression in block: expected '|' after lambda parameters: expected Bar, got RBrace +Program { + functions: [ + Function { + phase: Object, + name: "test", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Lam { + params: [ + Param { + name: "y", + ty: Var( + "u64", + ), + }, + ], + body: Var( + "y", + ), + }, + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt b/compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt index bb309ff..547f25e 100644 --- a/compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt +++ b/compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt @@ -1,2 +1,78 @@ -ERROR -in function `test`: expected function body: parsing expression in block: parsing function argument: expected parameter name in lambda: expected identifier, got Num(42) +Program { + functions: [ + Function { + phase: Meta, + name: "apply", + params: [ + Param { + name: "f", + ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + }, + }, + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: "f", + args: [ + Var( + "x", + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "test", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: "apply", + args: [ + Lam { + params: [ + Param { + name: "x", + ty: Var( + "u32", + ), + }, + ], + body: Var( + "x", + ), + }, + Lit( + 42, + ), + ], + }, + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_polycompose/2_parse.txt b/compiler/tests/snap/full/pi_polycompose/2_parse.txt index 19dcab7..dbd59a1 100644 --- a/compiler/tests/snap/full/pi_polycompose/2_parse.txt +++ b/compiler/tests/snap/full/pi_polycompose/2_parse.txt @@ -1,2 +1,2 @@ ERROR -in function `compose`: expected function body: parsing expression in block: expected '|' after lambda parameters: expected Bar, got RBrace +in function `test`: expected function body: expected '}': expected RBrace, got LParen diff --git a/compiler/tests/snap/full/pi_repeat/2_parse.txt b/compiler/tests/snap/full/pi_repeat/2_parse.txt index c4cc2e4..0417ab7 100644 --- a/compiler/tests/snap/full/pi_repeat/2_parse.txt +++ b/compiler/tests/snap/full/pi_repeat/2_parse.txt @@ -1,2 +1,161 @@ -ERROR -in function `square_twice`: expected function body: parsing expression in block: parsing spliced expression: parsing function argument: expected parameter name in lambda: expected identifier, got Num(2) +Program { + functions: [ + Function { + phase: Meta, + name: "repeat", + params: [ + Param { + name: "f", + ty: Pi { + params: [ + Param { + name: "_", + ty: Lift( + Var( + "u64", + ), + ), + }, + ], + ret_ty: Lift( + Var( + "u64", + ), + ), + }, + }, + Param { + name: "n", + ty: Var( + "u64", + ), + }, + Param { + name: "x", + ty: Lift( + Var( + "u64", + ), + ), + }, + ], + ret_ty: Lift( + Var( + "u64", + ), + ), + body: Block { + stmts: [], + expr: Match { + scrutinee: Var( + "n", + ), + arms: [ + MatchArm { + pat: Lit( + 0, + ), + body: Var( + "x", + ), + }, + MatchArm { + pat: Name( + "n", + ), + body: App { + func: "repeat", + args: [ + Var( + "f", + ), + App { + func: Sub, + args: [ + Var( + "n", + ), + Lit( + 1, + ), + ], + }, + App { + func: "f", + args: [ + Var( + "x", + ), + ], + }, + ], + }, + }, + ], + }, + }, + }, + Function { + phase: Object, + name: "square_twice", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: "repeat", + args: [ + Lam { + params: [ + Param { + name: "y", + ty: Lift( + Var( + "u64", + ), + ), + }, + ], + body: Quote( + App { + func: Mul, + args: [ + Splice( + Var( + "y", + ), + ), + Splice( + Var( + "y", + ), + ), + ], + }, + ), + }, + Lit( + 2, + ), + Quote( + Var( + "x", + ), + ), + ], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_staging_hof/2_parse.txt b/compiler/tests/snap/full/pi_staging_hof/2_parse.txt index 40b569e..c1b9610 100644 --- a/compiler/tests/snap/full/pi_staging_hof/2_parse.txt +++ b/compiler/tests/snap/full/pi_staging_hof/2_parse.txt @@ -1,2 +1,113 @@ -ERROR -in function `double`: expected function body: parsing expression in block: parsing spliced expression: parsing function argument: expected parameter name in lambda: expected identifier, got HashLParen +Program { + functions: [ + Function { + phase: Meta, + name: "map_code", + params: [ + Param { + name: "f", + ty: Pi { + params: [ + Param { + name: "_", + ty: Lift( + Var( + "u64", + ), + ), + }, + ], + ret_ty: Lift( + Var( + "u64", + ), + ), + }, + }, + Param { + name: "x", + ty: Lift( + Var( + "u64", + ), + ), + }, + ], + ret_ty: Lift( + Var( + "u64", + ), + ), + body: Block { + stmts: [], + expr: App { + func: "f", + args: [ + Var( + "x", + ), + ], + }, + }, + }, + Function { + phase: Object, + name: "double", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: "map_code", + args: [ + Lam { + params: [ + Param { + name: "y", + ty: Lift( + Var( + "u64", + ), + ), + }, + ], + body: Quote( + App { + func: Add, + args: [ + Splice( + Var( + "y", + ), + ), + Splice( + Var( + "y", + ), + ), + ], + }, + ), + }, + Quote( + Var( + "x", + ), + ), + ], + }, + ), + }, + }, + ], +} From 0b62409211d1b80bfc62be38d863de86cf181d0f Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 10:27:53 +0000 Subject: [PATCH 08/43] fix: repair parser and clippy issues for Pi types and lambdas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix lambda param type parsing: use parse_atom_owned() instead of parse_expr() so the closing | is not consumed as BitOr - Support postfix call syntax expr(args) in the expression parser - Unify FunName::Name(Name) into FunName::Term(&Term) so named calls are a special case of expression calls - Fix wildcard_enum_match_arm clippy lints in checker and pretty printer - Fix match_same_arms for Pi/Lift/U all inhabiting TYPE - Fix MetaVal variant name mismatches in eval (VLit/VCode/VTy/VClosure → Lit/Code/Ty/Closure) - Update and add snapshots for pi_compose, pi_const, pi_lambda_arg, pi_repeat, pi_staging_hof, and others Co-Authored-By: Claude Sonnet 4.6 --- compiler/src/checker/mod.rs | 10 +- compiler/src/checker/test/apply.rs | 10 +- compiler/src/checker/test/matching.rs | 18 +- compiler/src/checker/test/meta.rs | 4 +- compiler/src/checker/test/signatures.rs | 4 +- compiler/src/parser/ast.rs | 6 +- compiler/src/parser/mod.rs | 17 +- compiler/src/parser/test/expr/app.snap.txt | 4 +- .../src/parser/test/expr/complex.snap.txt | 4 +- compiler/src/parser/test/mod.rs | 6 +- .../snap/full/pi_apply_non_fn/2_parse.txt | 4 +- .../snap/full/pi_apply_non_fn/3_check.txt | 2 +- .../snap/full/pi_arity_mismatch/2_parse.txt | 4 +- .../snap/full/pi_arity_mismatch/3_check.txt | 2 +- compiler/tests/snap/full/pi_basic/2_parse.txt | 12 +- .../tests/snap/full/pi_compose/2_parse.txt | 195 +++++++++++++++- .../tests/snap/full/pi_compose/3_check.txt | 20 ++ .../tests/snap/full/pi_compose/6_stage.txt | 4 + compiler/tests/snap/full/pi_const/2_parse.txt | 130 ++++++++++- compiler/tests/snap/full/pi_const/3_check.txt | 12 + compiler/tests/snap/full/pi_const/6_stage.txt | 4 + .../snap/full/pi_dependent_ret/2_parse.txt | 16 +- .../tests/snap/full/pi_lambda_arg/2_parse.txt | 12 +- .../tests/snap/full/pi_lambda_arg/3_check.txt | 12 + .../tests/snap/full/pi_lambda_arg/6_stage.txt | 4 + .../snap/full/pi_lambda_in_object/3_check.txt | 2 + .../full/pi_lambda_type_mismatch/2_parse.txt | 8 +- .../full/pi_lambda_type_mismatch/3_check.txt | 2 + .../tests/snap/full/pi_nested/2_parse.txt | 16 +- .../snap/full/pi_polycompose/2_parse.txt | 214 +++++++++++++++++- .../snap/full/pi_polycompose/3_check.txt | 2 + .../snap/full/pi_polymorphic_id/2_parse.txt | 16 +- .../tests/snap/full/pi_repeat/2_parse.txt | 12 +- .../tests/snap/full/pi_repeat/3_check.txt | 11 + .../tests/snap/full/pi_repeat/6_stage.txt | 4 + .../snap/full/pi_staging_hof/2_parse.txt | 8 +- .../snap/full/pi_staging_hof/3_check.txt | 8 + .../snap/full/pi_staging_hof/6_stage.txt | 4 + compiler/tests/snap/full/power/2_parse.txt | 12 +- .../tests/snap/full/power_acc/2_parse.txt | 44 +++- .../tests/snap/full/power_simple/2_parse.txt | 12 +- .../snap/full/splice_meta_int/2_parse.txt | 4 +- compiler/tests/snap/full/staging/2_parse.txt | 4 +- compiler/tests/snap/full/sum_n/2_parse.txt | 8 +- .../stage_error/add_overflow_u32/2_parse.txt | 4 +- .../stage_error/add_overflow_u8/2_parse.txt | 4 +- .../stage_error/mul_overflow_u8/2_parse.txt | 4 +- .../stage_error/sub_underflow_u8/2_parse.txt | 4 +- 48 files changed, 831 insertions(+), 92 deletions(-) create mode 100644 compiler/tests/snap/full/pi_compose/3_check.txt create mode 100644 compiler/tests/snap/full/pi_compose/6_stage.txt create mode 100644 compiler/tests/snap/full/pi_const/3_check.txt create mode 100644 compiler/tests/snap/full/pi_const/6_stage.txt create mode 100644 compiler/tests/snap/full/pi_lambda_arg/3_check.txt create mode 100644 compiler/tests/snap/full/pi_lambda_arg/6_stage.txt create mode 100644 compiler/tests/snap/full/pi_lambda_in_object/3_check.txt create mode 100644 compiler/tests/snap/full/pi_lambda_type_mismatch/3_check.txt create mode 100644 compiler/tests/snap/full/pi_polycompose/3_check.txt create mode 100644 compiler/tests/snap/full/pi_repeat/3_check.txt create mode 100644 compiler/tests/snap/full/pi_repeat/6_stage.txt create mode 100644 compiler/tests/snap/full/pi_staging_hof/3_check.txt create mode 100644 compiler/tests/snap/full/pi_staging_hof/6_stage.txt diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index c76e3e2..320be98 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -479,11 +479,11 @@ pub fn infer<'src, 'core>( // ------------------------------------------------------------------ App { Global or local } // Function calls: look up callee, elaborate as curried FunApp chain. ast::Term::App { - func: ast::FunName::Name(name), + func: ast::FunName::Term(func_term), args, } => { // Elaborate the callee - let callee = infer(ctx, phase, &ast::Term::Var(*name))?; + let callee = infer(ctx, phase, func_term)?; let mut callee_ty = ctx.type_of(callee); // For globals, verify phase matches. @@ -494,7 +494,7 @@ pub fn infer<'src, 'core>( .expect("Global must be in globals table"); ensure!( sig.phase == phase, - "function `{name}` is a {}-phase function, but called in {phase}-phase context", + "function `{gname}` is a {}-phase function, but called in {phase}-phase context", sig.phase ); } @@ -516,13 +516,13 @@ pub fn infer<'src, 'core>( | core::Term::Splice(_) | core::Term::Let(_) | core::Term::Match(_) => bail!( - "too many arguments: function `{name}` expects {i} argument(s), got {}", + "too many arguments: callee expects {i} argument(s), got {}", args.len() ), }; let core_arg = check(ctx, phase, arg, pi.param_ty) - .with_context(|| format!("in argument {i} of call to `{name}`"))?; + .with_context(|| format!("in argument {i} of function call"))?; // The return type may depend on the argument (dependent types). // Global function signatures are elaborated in an empty context, diff --git a/compiler/src/checker/test/apply.rs b/compiler/src/checker/test/apply.rs index f5a7c08..c717fb0 100644 --- a/compiler/src/checker/test/apply.rs +++ b/compiler/src/checker/test/apply.rs @@ -12,7 +12,7 @@ fn infer_global_call_no_args_returns_ret_ty() { let mut ctx = test_ctx_with_globals(&core_arena, &globals); let term = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("f")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("f")))), args: &[], }); let result = infer(&mut ctx, Phase::Meta, term).expect("should infer"); @@ -34,7 +34,7 @@ fn infer_global_call_unknown_name_fails() { let mut ctx = test_ctx(&core_arena); let term = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("unknown")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("unknown")))), args: &[], }); assert!(infer(&mut ctx, Phase::Meta, term).is_err()); @@ -52,7 +52,7 @@ fn infer_global_call_wrong_arity_fails() { let mut ctx = test_ctx_with_globals(&core_arena, &globals); let term = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("f")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("f")))), args, }); assert!(infer(&mut ctx, Phase::Meta, term).is_err()); @@ -79,7 +79,7 @@ fn infer_global_call_phase_mismatch_fails() { // Call `f()` from meta phase — should be rejected. let term = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("f")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("f")))), args: &[], }); assert!(infer(&mut ctx, Phase::Meta, term).is_err()); @@ -98,7 +98,7 @@ fn infer_global_call_with_arg_checks_arg_type() { let arg = src_arena.alloc(ast::Term::Lit(42)); let args = src_arena.alloc_slice_fill_iter([arg as &ast::Term]); let term = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("f")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("f")))), args, }); let result = infer(&mut ctx, Phase::Meta, term).expect("should infer"); diff --git a/compiler/src/checker/test/matching.rs b/compiler/src/checker/test/matching.rs index fafa5db..d33e5ee 100644 --- a/compiler/src/checker/test/matching.rs +++ b/compiler/src/checker/test/matching.rs @@ -24,11 +24,11 @@ fn check_match_all_arms_same_type_succeeds() { let scrutinee = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let arm0_body = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("k32")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("k32")))), args: &[], }); let arm1_body = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("k32")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("k32")))), args: &[], }); let arms = src_arena.alloc_slice_fill_iter([ @@ -67,11 +67,11 @@ fn check_match_u1_fully_covered_succeeds() { let scrutinee = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let arm0_body = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("k1")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("k1")))), args: &[], }); let arm1_body = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("k1")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("k1")))), args: &[], }); // Both values of u1 are covered — exhaustive without a wildcard. @@ -111,7 +111,7 @@ fn infer_match_u1_partially_covered_fails() { let scrutinee = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let arm0_body = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("k1")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("k1")))), args: &[], }); // Only 0 covered, 1 is missing — not exhaustive. @@ -146,11 +146,11 @@ fn infer_match_no_catch_all_fails() { let scrutinee = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let arm0_body = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("k32")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("k32")))), args: &[], }); let arm1_body = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("k32")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("k32")))), args: &[], }); // Only literal arms, no wildcard/bind — not exhaustive. @@ -200,11 +200,11 @@ fn infer_match_arms_type_mismatch_fails() { let scrutinee = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let arm0_body = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("k32")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("k32")))), args: &[], }); let arm1_body = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("k64")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("k64")))), args: &[], }); let arms = src_arena.alloc_slice_fill_iter([ diff --git a/compiler/src/checker/test/meta.rs b/compiler/src/checker/test/meta.rs index 8347af2..1deb510 100644 --- a/compiler/src/checker/test/meta.rs +++ b/compiler/src/checker/test/meta.rs @@ -75,7 +75,7 @@ fn infer_quote_of_global_call_returns_lifted_type() { // Surface: `#(f())` let inner = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("f")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("f")))), args: &[], }); let term = src_arena.alloc(ast::Term::Quote(inner)); @@ -107,7 +107,7 @@ fn infer_quote_at_object_phase_fails() { let mut ctx = test_ctx_with_globals(&core_arena, &globals); let inner = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("f")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("f")))), args: &[], }); let term = src_arena.alloc(ast::Term::Quote(inner)); diff --git a/compiler/src/checker/test/signatures.rs b/compiler/src/checker/test/signatures.rs index 015d651..c40e815 100644 --- a/compiler/src/checker/test/signatures.rs +++ b/compiler/src/checker/test/signatures.rs @@ -256,7 +256,7 @@ fn elaborate_program_code_fn_with_splice() { // pow0's body: $(k()) let k_call = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("k")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("k")))), args: &[], }); let pow0_body = src_arena.alloc(ast::Term::Splice(k_call)); @@ -296,7 +296,7 @@ fn elaborate_program_forward_reference_succeeds() { // fn a() -> u32 { b() } let a_body = src_arena.alloc(ast::Term::App { - func: FunName::Name(ast::Name::new("b")), + func: FunName::Term(src_arena.alloc(ast::Term::Var(ast::Name::new("b")))), args: &[], }); // fn b() -> u32 { 42 } diff --git a/compiler/src/parser/ast.rs b/compiler/src/parser/ast.rs index 12cfd4a..8a11b56 100644 --- a/compiler/src/parser/ast.rs +++ b/compiler/src/parser/ast.rs @@ -1,9 +1,9 @@ pub use crate::common::{Assoc, BinOp, Name, Phase, UnOp}; /// Function or operator reference -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy)] pub enum FunName<'a> { - Name(Name<'a>), + Term(&'a Term<'a>), BinOp(BinOp), UnOp(UnOp), } @@ -11,7 +11,7 @@ pub enum FunName<'a> { impl std::fmt::Debug for FunName<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Name(n) => n.fmt(f), + Self::Term(t) => t.fmt(f), Self::BinOp(o) => o.fmt(f), Self::UnOp(o) => o.fmt(f), } diff --git a/compiler/src/parser/mod.rs b/compiler/src/parser/mod.rs index e225179..ef22c4a 100644 --- a/compiler/src/parser/mod.rs +++ b/compiler/src/parser/mod.rs @@ -256,6 +256,21 @@ where }; loop { + if self.peek() == Some(Token::LParen) { + self.next(); + let args = self.parse_separated_list(Token::RParen, |parser| { + parser.parse_expr().context("parsing function argument") + })?; + self.take(Token::RParen) + .context("expected ')' after function arguments")?; + let args = self.arena.alloc_slice_fill_iter(args); + lhs = Term::App { + func: FunName::Term(self.arena.alloc(lhs)), + args, + }; + continue; + } + let Some(op) = self.match_binop() else { break; }; @@ -320,7 +335,7 @@ where .context("expected ')' after function arguments")?; let args = self.arena.alloc_slice_fill_iter(args); Ok(Term::App { - func: FunName::Name(name), + func: FunName::Term(self.arena.alloc(Term::Var(name))), args, }) } diff --git a/compiler/src/parser/test/expr/app.snap.txt b/compiler/src/parser/test/expr/app.snap.txt index 7e23324..bb9cdd9 100644 --- a/compiler/src/parser/test/expr/app.snap.txt +++ b/compiler/src/parser/test/expr/app.snap.txt @@ -1,5 +1,7 @@ App { - func: "f", + func: Var( + "f", + ), args: [ Var( "x", diff --git a/compiler/src/parser/test/expr/complex.snap.txt b/compiler/src/parser/test/expr/complex.snap.txt index 3246487..ee65230 100644 --- a/compiler/src/parser/test/expr/complex.snap.txt +++ b/compiler/src/parser/test/expr/complex.snap.txt @@ -21,7 +21,9 @@ App { func: Not, args: [ App { - func: "foo", + func: Var( + "foo", + ), args: [ Var( "z", diff --git a/compiler/src/parser/test/mod.rs b/compiler/src/parser/test/mod.rs index aef7ce6..ff90fad 100644 --- a/compiler/src/parser/test/mod.rs +++ b/compiler/src/parser/test/mod.rs @@ -72,7 +72,7 @@ fn parse_expr_prec() { match expr { Term::App { func, args } => { assert_eq!(args.len(), 2); - assert_eq!(func, &FunName::BinOp(BinOp::Add)); + assert!(matches!(func, FunName::BinOp(BinOp::Add))); } _ => panic!("expected App"), } @@ -87,7 +87,7 @@ fn parse_expr_prec2() { match expr { Term::App { func, args } => { assert_eq!(args.len(), 2); - assert_eq!(func, &FunName::BinOp(BinOp::Add)); + assert!(matches!(func, FunName::BinOp(BinOp::Add))); } _ => panic!("expected App"), } @@ -102,7 +102,7 @@ fn parse_expr_paren() { match expr { Term::App { func, args } => { assert_eq!(args.len(), 2); - assert_eq!(func, &FunName::BinOp(BinOp::Mul)); + assert!(matches!(func, FunName::BinOp(BinOp::Mul))); } _ => panic!("expected App"), } diff --git a/compiler/tests/snap/full/pi_apply_non_fn/2_parse.txt b/compiler/tests/snap/full/pi_apply_non_fn/2_parse.txt index 239bf1e..fcdc77a 100644 --- a/compiler/tests/snap/full/pi_apply_non_fn/2_parse.txt +++ b/compiler/tests/snap/full/pi_apply_non_fn/2_parse.txt @@ -22,7 +22,9 @@ Program { }, ], expr: App { - func: "x", + func: Var( + "x", + ), args: [ Lit( 1, diff --git a/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt b/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt index d774b96..07717ec 100644 --- a/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt +++ b/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt @@ -1,2 +1,2 @@ ERROR -in function `test`: too many arguments: function `x` expects 0 argument(s), got 1 +in function `test`: too many arguments: callee expects 0 argument(s), got 1 diff --git a/compiler/tests/snap/full/pi_arity_mismatch/2_parse.txt b/compiler/tests/snap/full/pi_arity_mismatch/2_parse.txt index a5b9275..878f1a8 100644 --- a/compiler/tests/snap/full/pi_arity_mismatch/2_parse.txt +++ b/compiler/tests/snap/full/pi_arity_mismatch/2_parse.txt @@ -33,7 +33,9 @@ Program { body: Block { stmts: [], expr: App { - func: "f", + func: Var( + "f", + ), args: [ Var( "x", diff --git a/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt b/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt index 22ef48e..3927c5d 100644 --- a/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt +++ b/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt @@ -1,2 +1,2 @@ ERROR -in function `apply`: too many arguments: function `f` expects 1 argument(s), got 2 +in function `apply`: too many arguments: callee expects 1 argument(s), got 2 diff --git a/compiler/tests/snap/full/pi_basic/2_parse.txt b/compiler/tests/snap/full/pi_basic/2_parse.txt index 9933050..de3e8f5 100644 --- a/compiler/tests/snap/full/pi_basic/2_parse.txt +++ b/compiler/tests/snap/full/pi_basic/2_parse.txt @@ -33,7 +33,9 @@ Program { body: Block { stmts: [], expr: App { - func: "f", + func: Var( + "f", + ), args: [ Var( "x", @@ -81,7 +83,9 @@ Program { body: Block { stmts: [], expr: App { - func: "apply", + func: Var( + "apply", + ), args: [ Var( "inc", @@ -104,7 +108,9 @@ Program { stmts: [], expr: Splice( App { - func: "test", + func: Var( + "test", + ), args: [], }, ), diff --git a/compiler/tests/snap/full/pi_compose/2_parse.txt b/compiler/tests/snap/full/pi_compose/2_parse.txt index dbd59a1..596643d 100644 --- a/compiler/tests/snap/full/pi_compose/2_parse.txt +++ b/compiler/tests/snap/full/pi_compose/2_parse.txt @@ -1,2 +1,193 @@ -ERROR -in function `test`: expected function body: expected '}': expected RBrace, got LParen +Program { + functions: [ + Function { + phase: Meta, + name: "compose", + params: [ + Param { + name: "f", + ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + }, + }, + Param { + name: "g", + ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + }, + }, + ], + ret_ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + }, + body: Block { + stmts: [], + expr: Lam { + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + body: App { + func: Var( + "f", + ), + args: [ + App { + func: Var( + "g", + ), + args: [ + Var( + "x", + ), + ], + }, + ], + }, + }, + }, + }, + Function { + phase: Meta, + name: "double", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: Add, + args: [ + Var( + "x", + ), + Var( + "x", + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "inc", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: Add, + args: [ + Var( + "x", + ), + Lit( + 1, + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "test", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: App { + func: Var( + "compose", + ), + args: [ + Var( + "double", + ), + Var( + "inc", + ), + ], + }, + args: [ + Lit( + 5, + ), + ], + }, + }, + }, + Function { + phase: Object, + name: "result", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: Var( + "test", + ), + args: [], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_compose/3_check.txt b/compiler/tests/snap/full/pi_compose/3_check.txt new file mode 100644 index 0000000..9ec24b7 --- /dev/null +++ b/compiler/tests/snap/full/pi_compose/3_check.txt @@ -0,0 +1,20 @@ +fn compose(f@0: fn(u64) -> u64, g@1: fn(u64) -> u64) -> fn(u64) -> u64 { + |x@2: u64| f@0(g@1(x@2)) +} + +fn double(x@0: u64) -> u64 { + @add_u64(x@0, x@0) +} + +fn inc(x@0: u64) -> u64 { + @add_u64(x@0, 1_u64) +} + +fn test() -> u64 { + compose(double, inc, 5_u64) +} + +code fn result() -> u64 { + $(@embed_u64(test)) +} + diff --git a/compiler/tests/snap/full/pi_compose/6_stage.txt b/compiler/tests/snap/full/pi_compose/6_stage.txt new file mode 100644 index 0000000..5b9ed78 --- /dev/null +++ b/compiler/tests/snap/full/pi_compose/6_stage.txt @@ -0,0 +1,4 @@ +code fn result() -> u64 { + 12_u64 +} + diff --git a/compiler/tests/snap/full/pi_const/2_parse.txt b/compiler/tests/snap/full/pi_const/2_parse.txt index dbd59a1..82b89ee 100644 --- a/compiler/tests/snap/full/pi_const/2_parse.txt +++ b/compiler/tests/snap/full/pi_const/2_parse.txt @@ -1,2 +1,128 @@ -ERROR -in function `test`: expected function body: expected '}': expected RBrace, got LParen +Program { + functions: [ + Function { + phase: Meta, + name: "const_", + params: [ + Param { + name: "A", + ty: Var( + "Type", + ), + }, + Param { + name: "B", + ty: Var( + "Type", + ), + }, + ], + ret_ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "A", + ), + }, + ], + ret_ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "B", + ), + }, + ], + ret_ty: Var( + "A", + ), + }, + }, + body: Block { + stmts: [], + expr: Lam { + params: [ + Param { + name: "a", + ty: Var( + "A", + ), + }, + ], + body: Lam { + params: [ + Param { + name: "b", + ty: Var( + "B", + ), + }, + ], + body: Var( + "a", + ), + }, + }, + }, + }, + Function { + phase: Meta, + name: "test", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: App { + func: App { + func: Var( + "const_", + ), + args: [ + Var( + "u64", + ), + Var( + "u8", + ), + ], + }, + args: [ + Lit( + 42, + ), + ], + }, + args: [ + Lit( + 7, + ), + ], + }, + }, + }, + Function { + phase: Object, + name: "result", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: Var( + "test", + ), + args: [], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_const/3_check.txt b/compiler/tests/snap/full/pi_const/3_check.txt new file mode 100644 index 0000000..a944b05 --- /dev/null +++ b/compiler/tests/snap/full/pi_const/3_check.txt @@ -0,0 +1,12 @@ +fn const_(A@0: Type, B@1: Type) -> fn(A@0) -> fn(B@1) -> A@0 { + |a@2: A@0| |b@3: B@1| a@2 +} + +fn test() -> u64 { + const_(u64, u8, 42_u64, 7_u8) +} + +code fn result() -> u64 { + $(@embed_u64(test)) +} + diff --git a/compiler/tests/snap/full/pi_const/6_stage.txt b/compiler/tests/snap/full/pi_const/6_stage.txt new file mode 100644 index 0000000..8e18575 --- /dev/null +++ b/compiler/tests/snap/full/pi_const/6_stage.txt @@ -0,0 +1,4 @@ +code fn result() -> u64 { + 42_u64 +} + diff --git a/compiler/tests/snap/full/pi_dependent_ret/2_parse.txt b/compiler/tests/snap/full/pi_dependent_ret/2_parse.txt index e8ca932..bb3e28d 100644 --- a/compiler/tests/snap/full/pi_dependent_ret/2_parse.txt +++ b/compiler/tests/snap/full/pi_dependent_ret/2_parse.txt @@ -49,7 +49,9 @@ Program { body: Block { stmts: [], expr: App { - func: "const_", + func: Var( + "const_", + ), args: [ Var( "u64", @@ -77,7 +79,9 @@ Program { body: Block { stmts: [], expr: App { - func: "const_", + func: Var( + "const_", + ), args: [ Var( "u8", @@ -106,7 +110,9 @@ Program { stmts: [], expr: Splice( App { - func: "test_u64", + func: Var( + "test_u64", + ), args: [], }, ), @@ -123,7 +129,9 @@ Program { stmts: [], expr: Splice( App { - func: "test_u8", + func: Var( + "test_u8", + ), args: [], }, ), diff --git a/compiler/tests/snap/full/pi_lambda_arg/2_parse.txt b/compiler/tests/snap/full/pi_lambda_arg/2_parse.txt index 44a8e94..06128c6 100644 --- a/compiler/tests/snap/full/pi_lambda_arg/2_parse.txt +++ b/compiler/tests/snap/full/pi_lambda_arg/2_parse.txt @@ -33,7 +33,9 @@ Program { body: Block { stmts: [], expr: App { - func: "f", + func: Var( + "f", + ), args: [ Var( "x", @@ -52,7 +54,9 @@ Program { body: Block { stmts: [], expr: App { - func: "apply", + func: Var( + "apply", + ), args: [ Lam { params: [ @@ -93,7 +97,9 @@ Program { stmts: [], expr: Splice( App { - func: "test", + func: Var( + "test", + ), args: [], }, ), diff --git a/compiler/tests/snap/full/pi_lambda_arg/3_check.txt b/compiler/tests/snap/full/pi_lambda_arg/3_check.txt new file mode 100644 index 0000000..fe03503 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_arg/3_check.txt @@ -0,0 +1,12 @@ +fn apply(f@0: fn(u64) -> u64, x@1: u64) -> u64 { + f@0(x@1) +} + +fn test() -> u64 { + apply(|x@0: u64| @add_u64(x@0, 1_u64), 42_u64) +} + +code fn result() -> u64 { + $(@embed_u64(test)) +} + diff --git a/compiler/tests/snap/full/pi_lambda_arg/6_stage.txt b/compiler/tests/snap/full/pi_lambda_arg/6_stage.txt new file mode 100644 index 0000000..0ae8d94 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_arg/6_stage.txt @@ -0,0 +1,4 @@ +code fn result() -> u64 { + 43_u64 +} + diff --git a/compiler/tests/snap/full/pi_lambda_in_object/3_check.txt b/compiler/tests/snap/full/pi_lambda_in_object/3_check.txt new file mode 100644 index 0000000..7e5df17 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_in_object/3_check.txt @@ -0,0 +1,2 @@ +ERROR +in function `test`: lambdas are only valid in meta-phase context diff --git a/compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt b/compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt index 547f25e..9650eec 100644 --- a/compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt +++ b/compiler/tests/snap/full/pi_lambda_type_mismatch/2_parse.txt @@ -33,7 +33,9 @@ Program { body: Block { stmts: [], expr: App { - func: "f", + func: Var( + "f", + ), args: [ Var( "x", @@ -52,7 +54,9 @@ Program { body: Block { stmts: [], expr: App { - func: "apply", + func: Var( + "apply", + ), args: [ Lam { params: [ diff --git a/compiler/tests/snap/full/pi_lambda_type_mismatch/3_check.txt b/compiler/tests/snap/full/pi_lambda_type_mismatch/3_check.txt new file mode 100644 index 0000000..2109895 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_type_mismatch/3_check.txt @@ -0,0 +1,2 @@ +ERROR +in function `test`: in argument 0 of function call: lambda parameter type mismatch: annotation gives a different type than the expected function type diff --git a/compiler/tests/snap/full/pi_nested/2_parse.txt b/compiler/tests/snap/full/pi_nested/2_parse.txt index a140edc..89cf92c 100644 --- a/compiler/tests/snap/full/pi_nested/2_parse.txt +++ b/compiler/tests/snap/full/pi_nested/2_parse.txt @@ -33,10 +33,14 @@ Program { body: Block { stmts: [], expr: App { - func: "f", + func: Var( + "f", + ), args: [ App { - func: "f", + func: Var( + "f", + ), args: [ Var( "x", @@ -86,7 +90,9 @@ Program { body: Block { stmts: [], expr: App { - func: "apply_twice", + func: Var( + "apply_twice", + ), args: [ Var( "inc", @@ -109,7 +115,9 @@ Program { stmts: [], expr: Splice( App { - func: "test", + func: Var( + "test", + ), args: [], }, ), diff --git a/compiler/tests/snap/full/pi_polycompose/2_parse.txt b/compiler/tests/snap/full/pi_polycompose/2_parse.txt index dbd59a1..54fa46a 100644 --- a/compiler/tests/snap/full/pi_polycompose/2_parse.txt +++ b/compiler/tests/snap/full/pi_polycompose/2_parse.txt @@ -1,2 +1,212 @@ -ERROR -in function `test`: expected function body: expected '}': expected RBrace, got LParen +Program { + functions: [ + Function { + phase: Meta, + name: "compose", + params: [ + Param { + name: "A", + ty: Var( + "Type", + ), + }, + Param { + name: "B", + ty: Var( + "Type", + ), + }, + Param { + name: "C", + ty: Var( + "Type", + ), + }, + Param { + name: "f", + ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "B", + ), + }, + ], + ret_ty: Var( + "C", + ), + }, + }, + Param { + name: "g", + ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "A", + ), + }, + ], + ret_ty: Var( + "B", + ), + }, + }, + ], + ret_ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "A", + ), + }, + ], + ret_ty: Var( + "C", + ), + }, + body: Block { + stmts: [], + expr: Lam { + params: [ + Param { + name: "x", + ty: Var( + "A", + ), + }, + ], + body: App { + func: Var( + "f", + ), + args: [ + App { + func: Var( + "g", + ), + args: [ + Var( + "x", + ), + ], + }, + ], + }, + }, + }, + }, + Function { + phase: Meta, + name: "double", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: Add, + args: [ + Var( + "x", + ), + Var( + "x", + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "to_u8", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u8", + ), + body: Block { + stmts: [], + expr: Var( + "x", + ), + }, + }, + Function { + phase: Meta, + name: "test", + params: [], + ret_ty: Var( + "u8", + ), + body: Block { + stmts: [], + expr: App { + func: App { + func: Var( + "compose", + ), + args: [ + Var( + "u64", + ), + Var( + "u64", + ), + Var( + "u8", + ), + Var( + "to_u8", + ), + Var( + "double", + ), + ], + }, + args: [ + Lit( + 5, + ), + ], + }, + }, + }, + Function { + phase: Object, + name: "result", + params: [], + ret_ty: Var( + "u8", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: Var( + "test", + ), + args: [], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_polycompose/3_check.txt b/compiler/tests/snap/full/pi_polycompose/3_check.txt new file mode 100644 index 0000000..dc2c23d --- /dev/null +++ b/compiler/tests/snap/full/pi_polycompose/3_check.txt @@ -0,0 +1,2 @@ +ERROR +in function `to_u8`: type mismatch diff --git a/compiler/tests/snap/full/pi_polymorphic_id/2_parse.txt b/compiler/tests/snap/full/pi_polymorphic_id/2_parse.txt index eb8762b..08afa73 100644 --- a/compiler/tests/snap/full/pi_polymorphic_id/2_parse.txt +++ b/compiler/tests/snap/full/pi_polymorphic_id/2_parse.txt @@ -37,7 +37,9 @@ Program { body: Block { stmts: [], expr: App { - func: "id", + func: Var( + "id", + ), args: [ Var( "u64", @@ -59,7 +61,9 @@ Program { body: Block { stmts: [], expr: App { - func: "id", + func: Var( + "id", + ), args: [ Var( "u8", @@ -82,7 +86,9 @@ Program { stmts: [], expr: Splice( App { - func: "test_u64", + func: Var( + "test_u64", + ), args: [], }, ), @@ -99,7 +105,9 @@ Program { stmts: [], expr: Splice( App { - func: "test_u8", + func: Var( + "test_u8", + ), args: [], }, ), diff --git a/compiler/tests/snap/full/pi_repeat/2_parse.txt b/compiler/tests/snap/full/pi_repeat/2_parse.txt index 0417ab7..5d47607 100644 --- a/compiler/tests/snap/full/pi_repeat/2_parse.txt +++ b/compiler/tests/snap/full/pi_repeat/2_parse.txt @@ -64,7 +64,9 @@ Program { "n", ), body: App { - func: "repeat", + func: Var( + "repeat", + ), args: [ Var( "f", @@ -81,7 +83,9 @@ Program { ], }, App { - func: "f", + func: Var( + "f", + ), args: [ Var( "x", @@ -113,7 +117,9 @@ Program { stmts: [], expr: Splice( App { - func: "repeat", + func: Var( + "repeat", + ), args: [ Lam { params: [ diff --git a/compiler/tests/snap/full/pi_repeat/3_check.txt b/compiler/tests/snap/full/pi_repeat/3_check.txt new file mode 100644 index 0000000..044af27 --- /dev/null +++ b/compiler/tests/snap/full/pi_repeat/3_check.txt @@ -0,0 +1,11 @@ +fn repeat(f@0: fn([[u64]]) -> [[u64]], n@1: u64, x@2: [[u64]]) -> [[u64]] { + match n@1 { + 0 => x@2, + n@3 => repeat(f@0, @sub_u64(n@3, 1_u64), f@0(x@2)), + } +} + +code fn square_twice(x@0: u64) -> u64 { + $(repeat(|y@1: [[u64]]| #(@mul_u64($(y@1), $(y@1))), 2_u64, #(x@0))) +} + diff --git a/compiler/tests/snap/full/pi_repeat/6_stage.txt b/compiler/tests/snap/full/pi_repeat/6_stage.txt new file mode 100644 index 0000000..93dcff6 --- /dev/null +++ b/compiler/tests/snap/full/pi_repeat/6_stage.txt @@ -0,0 +1,4 @@ +code fn square_twice(x@0: u64) -> u64 { + @mul_u64(@mul_u64(x@0, x@0), @mul_u64(x@0, x@0)) +} + diff --git a/compiler/tests/snap/full/pi_staging_hof/2_parse.txt b/compiler/tests/snap/full/pi_staging_hof/2_parse.txt index c1b9610..301f150 100644 --- a/compiler/tests/snap/full/pi_staging_hof/2_parse.txt +++ b/compiler/tests/snap/full/pi_staging_hof/2_parse.txt @@ -41,7 +41,9 @@ Program { body: Block { stmts: [], expr: App { - func: "f", + func: Var( + "f", + ), args: [ Var( "x", @@ -68,7 +70,9 @@ Program { stmts: [], expr: Splice( App { - func: "map_code", + func: Var( + "map_code", + ), args: [ Lam { params: [ diff --git a/compiler/tests/snap/full/pi_staging_hof/3_check.txt b/compiler/tests/snap/full/pi_staging_hof/3_check.txt new file mode 100644 index 0000000..5a8038b --- /dev/null +++ b/compiler/tests/snap/full/pi_staging_hof/3_check.txt @@ -0,0 +1,8 @@ +fn map_code(f@0: fn([[u64]]) -> [[u64]], x@1: [[u64]]) -> [[u64]] { + f@0(x@1) +} + +code fn double(x@0: u64) -> u64 { + $(map_code(|y@1: [[u64]]| #(@add_u64($(y@1), $(y@1))), #(x@0))) +} + diff --git a/compiler/tests/snap/full/pi_staging_hof/6_stage.txt b/compiler/tests/snap/full/pi_staging_hof/6_stage.txt new file mode 100644 index 0000000..3a65226 --- /dev/null +++ b/compiler/tests/snap/full/pi_staging_hof/6_stage.txt @@ -0,0 +1,4 @@ +code fn double(x@0: u64) -> u64 { + @add_u64(x@0, x@0) +} + diff --git a/compiler/tests/snap/full/power/2_parse.txt b/compiler/tests/snap/full/power/2_parse.txt index 489213b..f1c5410 100644 --- a/compiler/tests/snap/full/power/2_parse.txt +++ b/compiler/tests/snap/full/power/2_parse.txt @@ -72,7 +72,9 @@ Program { ty: None, expr: Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Var( "x", @@ -196,7 +198,9 @@ Program { stmts: [], expr: Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Quote( Var( @@ -229,7 +233,9 @@ Program { stmts: [], expr: Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Quote( Var( diff --git a/compiler/tests/snap/full/power_acc/2_parse.txt b/compiler/tests/snap/full/power_acc/2_parse.txt index 0b48d10..1e1178c 100644 --- a/compiler/tests/snap/full/power_acc/2_parse.txt +++ b/compiler/tests/snap/full/power_acc/2_parse.txt @@ -109,7 +109,9 @@ Program { 0, ), body: App { - func: "power_acc", + func: Var( + "power_acc", + ), args: [ Quote( App { @@ -150,7 +152,9 @@ Program { stmts: [], expr: Splice( App { - func: "power_acc", + func: Var( + "power_acc", + ), args: [ Quote( App { @@ -311,7 +315,9 @@ Program { 0, ), body: App { - func: "power_acc_1", + func: Var( + "power_acc_1", + ), args: [ Quote( App { @@ -345,7 +351,9 @@ Program { 1, ), body: App { - func: "power_acc", + func: Var( + "power_acc", + ), args: [ Quote( App { @@ -417,7 +425,9 @@ Program { body: Block { stmts: [], expr: App { - func: "power_acc_1", + func: Var( + "power_acc_1", + ), args: [ Var( "x", @@ -447,7 +457,9 @@ Program { stmts: [], expr: Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Quote( Var( @@ -480,7 +492,9 @@ Program { stmts: [], expr: Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Quote( Var( @@ -513,7 +527,9 @@ Program { stmts: [], expr: Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Quote( Block { @@ -569,7 +585,9 @@ Program { stmts: [], expr: Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Quote( Var( @@ -602,7 +620,9 @@ Program { stmts: [], expr: Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Quote( Var( @@ -635,7 +655,9 @@ Program { stmts: [], expr: Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Quote( Var( diff --git a/compiler/tests/snap/full/power_simple/2_parse.txt b/compiler/tests/snap/full/power_simple/2_parse.txt index 5704138..ae2e728 100644 --- a/compiler/tests/snap/full/power_simple/2_parse.txt +++ b/compiler/tests/snap/full/power_simple/2_parse.txt @@ -61,7 +61,9 @@ Program { args: [ Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Var( "x", @@ -112,7 +114,9 @@ Program { stmts: [], expr: Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Quote( Var( @@ -145,7 +149,9 @@ Program { stmts: [], expr: Splice( App { - func: "power", + func: Var( + "power", + ), args: [ Quote( Var( diff --git a/compiler/tests/snap/full/splice_meta_int/2_parse.txt b/compiler/tests/snap/full/splice_meta_int/2_parse.txt index c9af4d8..8f5f797 100644 --- a/compiler/tests/snap/full/splice_meta_int/2_parse.txt +++ b/compiler/tests/snap/full/splice_meta_int/2_parse.txt @@ -25,7 +25,9 @@ Program { stmts: [], expr: Splice( App { - func: "val", + func: Var( + "val", + ), args: [], }, ), diff --git a/compiler/tests/snap/full/staging/2_parse.txt b/compiler/tests/snap/full/staging/2_parse.txt index cdb2845..70fe89e 100644 --- a/compiler/tests/snap/full/staging/2_parse.txt +++ b/compiler/tests/snap/full/staging/2_parse.txt @@ -29,7 +29,9 @@ Program { stmts: [], expr: Splice( App { - func: "k", + func: Var( + "k", + ), args: [], }, ), diff --git a/compiler/tests/snap/full/sum_n/2_parse.txt b/compiler/tests/snap/full/sum_n/2_parse.txt index 0fa4992..5d10b31 100644 --- a/compiler/tests/snap/full/sum_n/2_parse.txt +++ b/compiler/tests/snap/full/sum_n/2_parse.txt @@ -37,7 +37,9 @@ Program { func: Add, args: [ App { - func: "sum_n", + func: Var( + "sum_n", + ), args: [ App { func: Sub, @@ -73,7 +75,9 @@ Program { stmts: [], expr: Splice( App { - func: "sum_n", + func: Var( + "sum_n", + ), args: [ Lit( 5, diff --git a/compiler/tests/snap/stage_error/add_overflow_u32/2_parse.txt b/compiler/tests/snap/stage_error/add_overflow_u32/2_parse.txt index eac0cb9..d3df84e 100644 --- a/compiler/tests/snap/stage_error/add_overflow_u32/2_parse.txt +++ b/compiler/tests/snap/stage_error/add_overflow_u32/2_parse.txt @@ -33,7 +33,9 @@ Program { stmts: [], expr: Splice( App { - func: "f", + func: Var( + "f", + ), args: [], }, ), diff --git a/compiler/tests/snap/stage_error/add_overflow_u8/2_parse.txt b/compiler/tests/snap/stage_error/add_overflow_u8/2_parse.txt index 5b4df65..803f16a 100644 --- a/compiler/tests/snap/stage_error/add_overflow_u8/2_parse.txt +++ b/compiler/tests/snap/stage_error/add_overflow_u8/2_parse.txt @@ -33,7 +33,9 @@ Program { stmts: [], expr: Splice( App { - func: "f", + func: Var( + "f", + ), args: [], }, ), diff --git a/compiler/tests/snap/stage_error/mul_overflow_u8/2_parse.txt b/compiler/tests/snap/stage_error/mul_overflow_u8/2_parse.txt index 3e48c02..4d4f5d0 100644 --- a/compiler/tests/snap/stage_error/mul_overflow_u8/2_parse.txt +++ b/compiler/tests/snap/stage_error/mul_overflow_u8/2_parse.txt @@ -33,7 +33,9 @@ Program { stmts: [], expr: Splice( App { - func: "f", + func: Var( + "f", + ), args: [], }, ), diff --git a/compiler/tests/snap/stage_error/sub_underflow_u8/2_parse.txt b/compiler/tests/snap/stage_error/sub_underflow_u8/2_parse.txt index cd548f1..e62be59 100644 --- a/compiler/tests/snap/stage_error/sub_underflow_u8/2_parse.txt +++ b/compiler/tests/snap/stage_error/sub_underflow_u8/2_parse.txt @@ -33,7 +33,9 @@ Program { stmts: [], expr: Splice( App { - func: "f", + func: Var( + "f", + ), args: [], }, ), From 19cc4f2c2198f5c7a1fafb9d7f7a594443b73dbf Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 11:18:54 +0000 Subject: [PATCH 09/43] chore: fmt --- compiler/src/checker/mod.rs | 4 +++- compiler/src/eval/mod.rs | 2 +- compiler/src/parser/mod.rs | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index 320be98..6e6cc4f 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -110,7 +110,9 @@ impl<'core, 'globals> Ctx<'core, 'globals> { // Primitive types inhabit the relevant universe. core::Term::Prim(Prim::IntTy(it)) => core::Term::universe(it.phase), // Type, VmType, and [[T]] all inhabit Type (meta universe). - core::Term::Prim(Prim::U(_)) | core::Term::Lift(_) | core::Term::Pi(_) => &core::Term::TYPE, + core::Term::Prim(Prim::U(_)) | core::Term::Lift(_) | core::Term::Pi(_) => { + &core::Term::TYPE + } // Comparison ops return u1 at the operand phase. core::Term::Prim( diff --git a/compiler/src/eval/mod.rs b/compiler/src/eval/mod.rs index 8bf37b1..e9f84ce 100644 --- a/compiler/src/eval/mod.rs +++ b/compiler/src/eval/mod.rs @@ -245,7 +245,7 @@ fn apply_closure<'out>( obj_next, }; callee_env.push_meta(arg_val); - + // Restore env for the caller (bindings are consumed by the callee). eval_meta(arena, globals, &mut callee_env, body) } diff --git a/compiler/src/parser/mod.rs b/compiler/src/parser/mod.rs index ef22c4a..42329c1 100644 --- a/compiler/src/parser/mod.rs +++ b/compiler/src/parser/mod.rs @@ -390,7 +390,9 @@ where parser .take(Token::Colon) .context("expected ':' in lambda parameter (type annotations are required)")?; - let ty = parser.parse_atom_owned().context("expected parameter type")?; + let ty = parser + .parse_atom_owned() + .context("expected parameter type")?; let ty = parser.arena.alloc(ty); Ok(Param { name, ty }) })?; From 38d992e792847d52345eb99f3cc4d38e3f2de64b Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 11:53:56 +0000 Subject: [PATCH 10/43] fix: restore two-lifetime design for unstage_program Introduce a local `eval_bump` arena for temporary allocations during staging (synthetic Lam wrappers for global closures). `Term` is covariant, so `'core` data coerces to the shorter `'eval` lifetime. - `MetaVal<'out, 'eval>` / `Binding` / `Env`: two lifetimes; `Closure.body` is `&'eval Term<'eval>`, covering both input terms and eval-arena allocs - All internal eval functions gain `eval_arena: &'eval Bump` - `unstage_program<'out, 'core>`: two lifetimes restored; `core_program` may be dropped once the function returns - `cli/main.rs`: restore separate `core_arena` / `out_arena` Co-Authored-By: Claude Sonnet 4.6 --- cli/src/main.rs | 6 +- compiler/src/eval/mod.rs | 244 +++++++++++++++++++++------------------ 2 files changed, 136 insertions(+), 114 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 109db0f..1f33bb1 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -49,9 +49,11 @@ fn stage(file: &PathBuf) -> Result<()> { checker::elaborate_program(&core_arena, &program).context("failed to elaborate program")?; drop(src_arena); - // Unstage: the core_arena must remain alive since closures reference core terms. + // Unstage into out_arena; core_arena is no longer needed after this. + let out_arena = bumpalo::Bump::new(); let staged = - eval::unstage_program(&core_arena, &core_program).context("failed to stage program")?; + eval::unstage_program(&out_arena, &core_program).context("failed to stage program")?; + drop(core_arena); // Print the staged result, then out_arena is dropped at end of scope. println!("{staged}"); diff --git a/compiler/src/eval/mod.rs b/compiler/src/eval/mod.rs index e9f84ce..8992d38 100644 --- a/compiler/src/eval/mod.rs +++ b/compiler/src/eval/mod.rs @@ -11,8 +11,14 @@ use crate::parser::ast::Phase; // ── Value types ─────────────────────────────────────────────────────────────── /// A value produced by meta-level evaluation. +/// +/// Two lifetime parameters: +/// - `'out`: lifetime of the output arena (for `Code` values that appear in the result). +/// - `'eval`: lifetime of the evaluation phase — covers both the input program data (`'core`) +/// and any temporary terms allocated in the local eval arena. Since `Term` is covariant +/// in its lifetime, `'core` data can be coerced to `'eval` at call sites. #[derive(Clone, Debug)] -enum MetaVal<'out> { +enum MetaVal<'out, 'eval> { /// A concrete integer value computed at meta (compile) time. Lit(u64), /// Quoted object-level code. @@ -22,8 +28,8 @@ enum MetaVal<'out> { Ty, /// A closure: a lambda body captured with its environment. Closure { - body: &'out Term<'out>, - env: Vec>, + body: &'eval Term<'eval>, + env: Vec>, obj_next: Lvl, }, } @@ -32,21 +38,21 @@ enum MetaVal<'out> { /// A binding stored in the evaluation environment, indexed by De Bruijn level. #[derive(Clone, Debug)] -enum Binding<'out> { +enum Binding<'out, 'eval> { /// A meta-level variable bound to a concrete `MetaVal`. - Meta(MetaVal<'out>), + Meta(MetaVal<'out, 'eval>), /// An object-level variable. Obj(Lvl), } /// Evaluation environment: a stack of bindings indexed by De Bruijn level. #[derive(Debug)] -struct Env<'out> { - bindings: Vec>, +struct Env<'out, 'eval> { + bindings: Vec>, obj_next: Lvl, } -impl<'out> Env<'out> { +impl<'out, 'eval> Env<'out, 'eval> { const fn new(obj_next: Lvl) -> Self { Env { bindings: Vec::new(), @@ -55,7 +61,7 @@ impl<'out> Env<'out> { } /// Look up the binding at level `lvl`. - fn get(&self, lvl: Lvl) -> &Binding<'out> { + fn get(&self, lvl: Lvl) -> &Binding<'out, 'eval> { self.bindings .get(lvl.0) .expect("De Bruijn level in env bounds") @@ -69,7 +75,7 @@ impl<'out> Env<'out> { } /// Push a meta-level binding bound to the given value. - fn push_meta(&mut self, val: MetaVal<'out>) { + fn push_meta(&mut self, val: MetaVal<'out, 'eval>) { self.bindings.push(Binding::Meta(val)); } @@ -102,12 +108,13 @@ type Globals<'a> = HashMap, GlobalDef<'a>>; // ── Meta-level evaluator ────────────────────────────────────────────────────── /// Evaluate a meta-level `term` to a `MetaVal`. -fn eval_meta<'out>( +fn eval_meta<'out, 'eval>( arena: &'out Bump, - globals: &Globals<'out>, - env: &mut Env<'out>, - term: &'out Term<'out>, -) -> Result> { + eval_arena: &'eval Bump, + globals: &Globals<'eval>, + env: &mut Env<'out, 'eval>, + term: &'eval Term<'eval>, +) -> Result> { match term { // ── Variable ───────────────────────────────────────────────────────── Term::Var(lvl) => match env.get(*lvl) { @@ -128,15 +135,11 @@ fn eval_meta<'out>( .unwrap_or_else(|| panic!("unknown global `{name}` during staging")); if def.sig.params.is_empty() { // Zero-param global: evaluate the body immediately in a fresh env. - // (Zero-param Pi types don't exist, so zero-param globals are always - // called, never passed as values.) let mut callee_env = Env::new(env.obj_next); - eval_meta(arena, globals, &mut callee_env, def.body) + eval_meta(arena, eval_arena, globals, &mut callee_env, def.body) } else { - // Multi-param global: produce a closure, capturing the caller's - // obj_next so object-level let bindings inside quotes don't clash - // with output levels already in use at the call site. - Ok(global_to_closure(arena, def, env.obj_next)) + // Multi-param global: produce a closure. + Ok(global_to_closure(eval_arena, def, env.obj_next)) } } @@ -149,39 +152,39 @@ fn eval_meta<'out>( // ── Function application ───────────────────────────────────────────── Term::FunApp(app) => { - let func_val = eval_meta(arena, globals, env, app.func)?; - let arg_val = eval_meta(arena, globals, env, app.arg)?; - apply_closure(arena, globals, func_val, arg_val) + let func_val = eval_meta(arena, eval_arena, globals, env, app.func)?; + let arg_val = eval_meta(arena, eval_arena, globals, env, app.arg)?; + apply_closure(arena, eval_arena, globals, func_val, arg_val) } // ── PrimApp ────────────────────────────────────────────────────────── - Term::PrimApp(app) => eval_meta_prim(arena, globals, env, app.prim, app.args), + Term::PrimApp(app) => eval_meta_prim(arena, eval_arena, globals, env, app.prim, app.args), // ── Quote: #(t) ────────────────────────────────────────────────────── Term::Quote(inner) => { - let obj_term = unstage_obj(arena, globals, env, inner)?; + let obj_term = unstage_obj(arena, eval_arena, globals, env, inner)?; Ok(MetaVal::Code(obj_term)) } // ── Let binding ────────────────────────────────────────────────────── Term::Let(let_) => { - let val = eval_meta(arena, globals, env, let_.expr)?; + let val = eval_meta(arena, eval_arena, globals, env, let_.expr)?; env.push_meta(val); - let result = eval_meta(arena, globals, env, let_.body); + let result = eval_meta(arena, eval_arena, globals, env, let_.body); env.pop(); result } // ── Match ──────────────────────────────────────────────────────────── Term::Match(match_) => { - let scrut_val = eval_meta(arena, globals, env, match_.scrutinee)?; + let scrut_val = eval_meta(arena, eval_arena, globals, env, match_.scrutinee)?; let n = match scrut_val { MetaVal::Lit(n) => n, MetaVal::Code(_) | MetaVal::Ty | MetaVal::Closure { .. } => unreachable!( "cannot match on non-integer at meta level (typechecker invariant)" ), }; - eval_meta_match(arena, globals, env, n, match_.arms) + eval_meta_match(arena, eval_arena, globals, env, n, match_.arms) } // ── Unreachable in well-typed meta terms ───────────────────────────── @@ -194,13 +197,15 @@ fn eval_meta<'out>( /// Convert a global function definition into a closure value. /// -/// For a multi-parameter function, we build nested closures. E.g., `fn f(x, y) = body` -/// becomes a closure whose body is a lambda `|y| body`. -fn global_to_closure<'out>( - arena: &'out Bump, - def: &GlobalDef<'out>, +/// For a multi-parameter function, we build nested closures. E.g., `fn f(x, y) = body` +/// becomes a closure whose body is a lambda `|y| body`. The synthetic `Lam` wrapper nodes +/// are allocated in `eval_arena`, which is local to `unstage_program` and lives for the +/// duration of staging — long enough to outlive any closure values. +fn global_to_closure<'out, 'eval>( + eval_arena: &'eval Bump, + def: &GlobalDef<'eval>, obj_next: Lvl, -) -> MetaVal<'out> { +) -> MetaVal<'out, 'eval> { let params = def.sig.params; if params.is_empty() { MetaVal::Closure { @@ -210,9 +215,9 @@ fn global_to_closure<'out>( } } else { // Build nested lambdas for params[1..], then wrap in a closure for params[0]. - let mut body: &Term = def.body; + let mut body: &'eval Term<'eval> = def.body; for &(name, ty) in params.iter().rev().skip(1) { - body = arena.alloc(Term::Lam(Lam { + body = eval_arena.alloc(Term::Lam(Lam { param_name: name, param_ty: ty, body, @@ -227,12 +232,13 @@ fn global_to_closure<'out>( } /// Apply a closure value to an argument value. -fn apply_closure<'out>( +fn apply_closure<'out, 'eval>( arena: &'out Bump, - globals: &Globals<'out>, - func_val: MetaVal<'out>, - arg_val: MetaVal<'out>, -) -> Result> { + eval_arena: &'eval Bump, + globals: &Globals<'eval>, + func_val: MetaVal<'out, 'eval>, + arg_val: MetaVal<'out, 'eval>, +) -> Result> { match func_val { MetaVal::Closure { body, @@ -246,8 +252,7 @@ fn apply_closure<'out>( }; callee_env.push_meta(arg_val); - // Restore env for the caller (bindings are consumed by the callee). - eval_meta(arena, globals, &mut callee_env, body) + eval_meta(arena, eval_arena, globals, &mut callee_env, body) } MetaVal::Lit(_) | MetaVal::Code(_) | MetaVal::Ty => { unreachable!("applying a non-function value (typechecker invariant)") @@ -256,29 +261,33 @@ fn apply_closure<'out>( } /// Evaluate a primitive operation at meta level. -fn eval_meta_prim<'out>( +fn eval_meta_prim<'out, 'eval>( arena: &'out Bump, - globals: &Globals<'out>, - env: &mut Env<'out>, + eval_arena: &'eval Bump, + globals: &Globals<'eval>, + env: &mut Env<'out, 'eval>, prim: Prim, - args: &'out [&'out Term<'out>], -) -> Result> { - let eval_lit = - |arena: &'out Bump, globals: &Globals<'out>, env: &mut Env<'out>, arg: &'out Term<'out>| { - eval_meta(arena, globals, env, arg).map(|v| match v { - MetaVal::Lit(n) => n, - MetaVal::Code(_) | MetaVal::Ty | MetaVal::Closure { .. } => unreachable!( - "expected integer meta value for primitive operand (typechecker invariant)" - ), - }) - }; + args: &'eval [&'eval Term<'eval>], +) -> Result> { + let eval_lit = |arena: &'out Bump, + eval_arena: &'eval Bump, + globals: &Globals<'eval>, + env: &mut Env<'out, 'eval>, + arg: &'eval Term<'eval>| { + eval_meta(arena, eval_arena, globals, env, arg).map(|v| match v { + MetaVal::Lit(n) => n, + MetaVal::Code(_) | MetaVal::Ty | MetaVal::Closure { .. } => unreachable!( + "expected integer meta value for primitive operand (typechecker invariant)" + ), + }) + }; #[expect(clippy::indexing_slicing)] match prim { // ── Arithmetic ──────────────────────────────────────────────────────── Prim::Add(IntType { width, .. }) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; let result = a .checked_add(b) .filter(|&r| r <= width.max_value()) @@ -293,8 +302,8 @@ fn eval_meta_prim<'out>( Ok(MetaVal::Lit(result)) } Prim::Sub(IntType { width, .. }) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; let result = a.checked_sub(b).ok_or_else(|| { anyhow!( "arithmetic overflow during staging: \ @@ -304,8 +313,8 @@ fn eval_meta_prim<'out>( Ok(MetaVal::Lit(result)) } Prim::Mul(IntType { width, .. }) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; let result = a .checked_mul(b) .filter(|&r| r <= width.max_value()) @@ -320,63 +329,63 @@ fn eval_meta_prim<'out>( Ok(MetaVal::Lit(result)) } Prim::Div(_) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; ensure!(b != 0, "division by zero during staging"); Ok(MetaVal::Lit(a / b)) } // ── Bitwise ─────────────────────────────────────────────────────────── Prim::BitAnd(_) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; Ok(MetaVal::Lit(a & b)) } Prim::BitOr(_) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; Ok(MetaVal::Lit(a | b)) } Prim::BitNot(IntType { width, .. }) => { - let a = eval_lit(arena, globals, env, args[0])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; Ok(MetaVal::Lit(mask_to_width(width, !a))) } // ── Comparison ──────────────────────────────────────────────────────── Prim::Eq(_) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; Ok(MetaVal::Lit(u64::from(a == b))) } Prim::Ne(_) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; Ok(MetaVal::Lit(u64::from(a != b))) } Prim::Lt(_) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; Ok(MetaVal::Lit(u64::from(a < b))) } Prim::Gt(_) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; Ok(MetaVal::Lit(u64::from(a > b))) } Prim::Le(_) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; Ok(MetaVal::Lit(u64::from(a <= b))) } Prim::Ge(_) => { - let a = eval_lit(arena, globals, env, args[0])?; - let b = eval_lit(arena, globals, env, args[1])?; + let a = eval_lit(arena, eval_arena, globals, env, args[0])?; + let b = eval_lit(arena, eval_arena, globals, env, args[1])?; Ok(MetaVal::Lit(u64::from(a >= b))) } // ── Embed: meta integer → object code ───────────────────────────────── Prim::Embed(width) => { - let n = eval_lit(arena, globals, env, args[0])?; + let n = eval_lit(arena, eval_arena, globals, env, args[0])?; let phase = Phase::Object; let lit_term = arena.alloc(Term::Lit(n, IntType { width, phase })); Ok(MetaVal::Code(lit_term)) @@ -402,23 +411,24 @@ const fn mask_to_width(width: IntWidth, val: u64) -> u64 { } /// Evaluate a meta-level `match` expression. -fn eval_meta_match<'out>( +fn eval_meta_match<'out, 'eval>( arena: &'out Bump, - globals: &Globals<'out>, - env: &mut Env<'out>, + eval_arena: &'eval Bump, + globals: &Globals<'eval>, + env: &mut Env<'out, 'eval>, n: u64, - arms: &'out [Arm<'out>], -) -> Result> { + arms: &'eval [Arm<'eval>], +) -> Result> { for arm in arms { match &arm.pat { Pat::Lit(m) => { if n == *m { - return eval_meta(arena, globals, env, arm.body); + return eval_meta(arena, eval_arena, globals, env, arm.body); } } Pat::Bind(_) | Pat::Wildcard => { env.push_meta(MetaVal::Lit(n)); - let result = eval_meta(arena, globals, env, arm.body); + let result = eval_meta(arena, eval_arena, globals, env, arm.body); env.pop(); return result; } @@ -432,11 +442,12 @@ fn eval_meta_match<'out>( // ── Object-level unstager ───────────────────────────────────────────────────── /// Unstage an object-level `term`, eliminating all `Splice` nodes. -fn unstage_obj<'out>( +fn unstage_obj<'out, 'eval>( arena: &'out Bump, - globals: &Globals<'out>, - env: &mut Env<'out>, - term: &'out Term<'out>, + eval_arena: &'eval Bump, + globals: &Globals<'eval>, + env: &mut Env<'out, 'eval>, + term: &'eval Term<'eval>, ) -> Result<&'out Term<'out>> { match term { // ── Variable ───────────────────────────────────────────────────────── @@ -476,15 +487,15 @@ fn unstage_obj<'out>( let staged_args: &'out [&'out Term<'out>] = arena.alloc_slice_try_fill_iter( app.args .iter() - .map(|arg| unstage_obj(arena, globals, env, arg)), + .map(|arg| unstage_obj(arena, eval_arena, globals, env, arg)), )?; Ok(arena.alloc(Term::new_prim_app(app.prim, staged_args))) } // ── FunApp (in object terms) ───────────────────────────────────────── Term::FunApp(app) => { - let staged_func = unstage_obj(arena, globals, env, app.func)?; - let staged_arg = unstage_obj(arena, globals, env, app.arg)?; + let staged_func = unstage_obj(arena, eval_arena, globals, env, app.func)?; + let staged_arg = unstage_obj(arena, eval_arena, globals, env, app.arg)?; Ok(arena.alloc(Term::FunApp(FunApp { func: staged_func, arg: staged_arg, @@ -493,7 +504,7 @@ fn unstage_obj<'out>( // ── Splice: $(t) — the key staging step ────────────────────────────── Term::Splice(inner) => { - let meta_val = eval_meta(arena, globals, env, inner)?; + let meta_val = eval_meta(arena, eval_arena, globals, env, inner)?; match meta_val { MetaVal::Code(obj_term) => Ok(obj_term), MetaVal::Lit(_) | MetaVal::Ty | MetaVal::Closure { .. } => { @@ -504,10 +515,10 @@ fn unstage_obj<'out>( // ── Let binding ────────────────────────────────────────────────────── Term::Let(let_) => { - let staged_ty = unstage_obj(arena, globals, env, let_.ty)?; - let staged_expr = unstage_obj(arena, globals, env, let_.expr)?; + let staged_ty = unstage_obj(arena, eval_arena, globals, env, let_.ty)?; + let staged_expr = unstage_obj(arena, eval_arena, globals, env, let_.expr)?; env.push_obj(); - let staged_body = unstage_obj(arena, globals, env, let_.body); + let staged_body = unstage_obj(arena, eval_arena, globals, env, let_.body); env.pop(); Ok(arena.alloc(Term::new_let( arena.alloc_str(let_.name), @@ -519,7 +530,7 @@ fn unstage_obj<'out>( // ── Match ──────────────────────────────────────────────────────────── Term::Match(match_) => { - let staged_scrutinee = unstage_obj(arena, globals, env, match_.scrutinee)?; + let staged_scrutinee = unstage_obj(arena, eval_arena, globals, env, match_.scrutinee)?; let staged_arms: &'out [Arm<'out>] = arena.alloc_slice_try_fill_iter(match_.arms.iter().map(|arm| -> Result<_> { let staged_pat = match &arm.pat { @@ -531,7 +542,7 @@ fn unstage_obj<'out>( if has_binding { env.push_obj(); } - let staged_body = unstage_obj(arena, globals, env, arm.body); + let staged_body = unstage_obj(arena, eval_arena, globals, env, arm.body); if has_binding { env.pop(); } @@ -554,11 +565,20 @@ fn unstage_obj<'out>( // ── Public entry point ──────────────────────────────────────────────────────── /// Unstage an elaborated program. -pub fn unstage_program<'out>( +/// +/// - `arena`: output arena; the returned `Program<'out>` is allocated here. +/// - `program`: input core program; may be dropped once this function returns. +pub fn unstage_program<'out, 'core>( arena: &'out Bump, - program: &'out Program<'out>, + program: &'core Program<'core>, ) -> Result> { - let globals: Globals<'out> = program + // A temporary arena for intermediate values (synthetic Lam wrappers for closures, etc.) + // that exist only during evaluation and must not appear in the output. Its lifetime + // `'eval` is shorter than `'core`, so `'core` data is coercible to `'eval` via the + // covariance of `Term`. + let eval_bump = Bump::new(); + + let globals: Globals<'_> = program .functions .iter() .map(|f| { @@ -581,15 +601,15 @@ pub fn unstage_program<'out>( let staged_params = arena.alloc_slice_try_fill_iter(f.sig.params.iter().map( |(n, ty)| -> Result<(&'out str, &'out Term<'out>)> { - let staged_ty = unstage_obj(arena, &globals, &mut env, ty)?; + let staged_ty = unstage_obj(arena, &eval_bump, &globals, &mut env, ty)?; env.push_obj(); Ok((arena.alloc_str(n), staged_ty)) }, ))?; - let staged_ret_ty = unstage_obj(arena, &globals, &mut env, f.sig.ret_ty)?; + let staged_ret_ty = unstage_obj(arena, &eval_bump, &globals, &mut env, f.sig.ret_ty)?; - let staged_body = unstage_obj(arena, &globals, &mut env, f.body)?; + let staged_body = unstage_obj(arena, &eval_bump, &globals, &mut env, f.body)?; Ok(Function { name: Name::new(arena.alloc_str(f.name.as_str())), From 4bf047ff6defaefb455fd1d1aa2f66c4f5b91cb2 Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 13:56:49 +0000 Subject: [PATCH 11/43] test: normalization test (currently failing) --- .../tests/snap/full/let_type/0_input.splic | 7 +++ compiler/tests/snap/full/let_type/1_lex.txt | 37 ++++++++++++ compiler/tests/snap/full/let_type/2_parse.txt | 60 +++++++++++++++++++ compiler/tests/snap/full/let_type/3_check.txt | 2 + 4 files changed, 106 insertions(+) create mode 100644 compiler/tests/snap/full/let_type/0_input.splic create mode 100644 compiler/tests/snap/full/let_type/1_lex.txt create mode 100644 compiler/tests/snap/full/let_type/2_parse.txt create mode 100644 compiler/tests/snap/full/let_type/3_check.txt diff --git a/compiler/tests/snap/full/let_type/0_input.splic b/compiler/tests/snap/full/let_type/0_input.splic new file mode 100644 index 0000000..d424ea0 --- /dev/null +++ b/compiler/tests/snap/full/let_type/0_input.splic @@ -0,0 +1,7 @@ +fn let_type() -> u32 { + let ty: Type = u32; + let x: ty = 1337; + x +} + +code fn test() -> u32 { $(let_type()) } diff --git a/compiler/tests/snap/full/let_type/1_lex.txt b/compiler/tests/snap/full/let_type/1_lex.txt new file mode 100644 index 0000000..173385d --- /dev/null +++ b/compiler/tests/snap/full/let_type/1_lex.txt @@ -0,0 +1,37 @@ +Fn +Ident("let_type") +LParen +RParen +Arrow +Ident("u32") +LBrace +Let +Ident("ty") +Colon +Ident("Type") +Eq +Ident("u32") +Semi +Let +Ident("x") +Colon +Ident("ty") +Eq +Num(1337) +Semi +Ident("x") +RBrace +Code +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u32") +LBrace +DollarLParen +Ident("let_type") +LParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/let_type/2_parse.txt b/compiler/tests/snap/full/let_type/2_parse.txt new file mode 100644 index 0000000..bfdefb7 --- /dev/null +++ b/compiler/tests/snap/full/let_type/2_parse.txt @@ -0,0 +1,60 @@ +Program { + functions: [ + Function { + phase: Meta, + name: "let_type", + params: [], + ret_ty: Var( + "u32", + ), + body: Block { + stmts: [ + Let { + name: "ty", + ty: Some( + Var( + "Type", + ), + ), + expr: Var( + "u32", + ), + }, + Let { + name: "x", + ty: Some( + Var( + "ty", + ), + ), + expr: Lit( + 1337, + ), + }, + ], + expr: Var( + "x", + ), + }, + }, + Function { + phase: Object, + name: "test", + params: [], + ret_ty: Var( + "u32", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: Var( + "let_type", + ), + args: [], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/let_type/3_check.txt b/compiler/tests/snap/full/let_type/3_check.txt new file mode 100644 index 0000000..0ea29b3 --- /dev/null +++ b/compiler/tests/snap/full/let_type/3_check.txt @@ -0,0 +1,2 @@ +ERROR +in function `let_type`: in let binding `x`: literal `1337` cannot have a non-integer type From fcc5162183944f11fc598e9733ca8b436bdfbaad Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 14:23:18 +0000 Subject: [PATCH 12/43] refactor: unify PrimApp and FunApp into a single App node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the two separate application nodes with a single `App { func, args }` where `func` is any `Term` — `Term::Prim(p)` for primitives, `Term::Global(name)` for top-level calls, etc. Empty args now distinguishes `f()` from a bare `f` reference. No implicit currying in the core IR; the typechecker checks arity explicitly. Co-Authored-By: Claude Sonnet 4.6 --- compiler/src/checker/mod.rs | 178 ++++++++---------- compiler/src/checker/test/apply.rs | 18 +- compiler/src/checker/test/context.rs | 19 +- compiler/src/core/alpha_eq.rs | 7 +- compiler/src/core/mod.rs | 63 +++---- compiler/src/core/pretty.rs | 68 ++----- compiler/src/core/subst.rs | 16 +- compiler/src/eval/mod.rs | 40 ++-- compiler/tests/snap/full/pi_basic/3_check.txt | 2 +- .../tests/snap/full/pi_compose/3_check.txt | 4 +- compiler/tests/snap/full/pi_const/3_check.txt | 4 +- .../snap/full/pi_dependent_ret/3_check.txt | 4 +- .../tests/snap/full/pi_lambda_arg/3_check.txt | 2 +- .../tests/snap/full/pi_nested/3_check.txt | 2 +- .../snap/full/pi_polymorphic_id/3_check.txt | 4 +- .../snap/full/splice_meta_int/3_check.txt | 2 +- compiler/tests/snap/full/staging/3_check.txt | 2 +- .../stage_error/add_overflow_u32/3_check.txt | 2 +- .../stage_error/add_overflow_u8/3_check.txt | 2 +- .../stage_error/mul_overflow_u8/3_check.txt | 2 +- .../stage_error/sub_underflow_u8/3_check.txt | 2 +- 21 files changed, 178 insertions(+), 265 deletions(-) diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index 6e6cc4f..8efea4c 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use anyhow::{Context as _, Result, anyhow, bail, ensure}; -use crate::core::{self, FunApp, IntType, IntWidth, Lam, Lvl, Pi, Prim, alpha_eq, subst}; +use crate::core::{self, IntType, IntWidth, Lam, Lvl, Pi, Prim, alpha_eq, subst}; use crate::parser::ast::{self, Phase}; /// Elaboration context. @@ -138,26 +138,49 @@ impl<'core, 'globals> Ctx<'core, 'globals> { sig.to_pi_type(self.arena) } - // PrimApp: return type comes from the prim. - core::Term::PrimApp(app) => match app.prim { - Prim::Add(it) - | Prim::Sub(it) - | Prim::Mul(it) - | Prim::Div(it) - | Prim::BitAnd(it) - | Prim::BitOr(it) - | Prim::BitNot(it) => core::Term::int_ty(it.width, it.phase), - Prim::Eq(it) - | Prim::Ne(it) - | Prim::Lt(it) - | Prim::Gt(it) - | Prim::Le(it) - | Prim::Ge(it) => core::Term::u1_ty(it.phase), - Prim::Embed(w) => { - self.alloc(core::Term::Lift(core::Term::int_ty(w, Phase::Object))) - } - Prim::IntTy(_) | Prim::U(_) => { - unreachable!("type-level prim in PrimApp (typechecker invariant)") + // App: dispatch on func. + // - Prim callee: return type is determined by the primitive. + // - Other callee: peel Pi types, substituting each arg. + core::Term::App(app) => match app.func { + core::Term::Prim(prim) => match prim { + Prim::Add(it) + | Prim::Sub(it) + | Prim::Mul(it) + | Prim::Div(it) + | Prim::BitAnd(it) + | Prim::BitOr(it) + | Prim::BitNot(it) => core::Term::int_ty(it.width, it.phase), + Prim::Eq(it) + | Prim::Ne(it) + | Prim::Lt(it) + | Prim::Gt(it) + | Prim::Le(it) + | Prim::Ge(it) => core::Term::u1_ty(it.phase), + Prim::Embed(w) => { + self.alloc(core::Term::Lift(core::Term::int_ty(*w, Phase::Object))) + } + Prim::IntTy(_) | Prim::U(_) => { + unreachable!("type-level prim in App (typechecker invariant)") + } + }, + _ => { + // Global function signatures are elaborated in an empty context, + // so the i-th Pi binder is at De Bruijn level (base_depth + i) + // where base_depth counts args already applied by an outer App. + let base_depth = app_base_depth(app.func); + let mut current_ty = self.type_of(app.func); + for (i, arg) in app.args.iter().enumerate() { + match current_ty { + core::Term::Pi(pi) => { + current_ty = + subst(self.arena, pi.body_ty, Lvl(base_depth + i), arg); + } + _ => unreachable!( + "App func must have Pi type for each arg (typechecker invariant)" + ), + } + } + current_ty } }, @@ -173,34 +196,6 @@ impl<'core, 'globals> Ctx<'core, 'globals> { })) } - // FunApp: get func type (must be Pi), return body_ty with substitution. - // The Pi binder level equals the number of FunApp layers already applied to the - // root callee — global signatures are elaborated in an empty context so the i-th - // binder is at level i. - core::Term::FunApp(app) => { - let func_ty = self.type_of(app.func); - match func_ty { - core::Term::Pi(pi) => { - let binder_lvl = Lvl(funapp_depth(app.func)); - subst(self.arena, pi.body_ty, binder_lvl, app.arg) - } - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::PrimApp(_) - | core::Term::Lam(_) - | core::Term::FunApp(_) - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => { - unreachable!("FunApp func must have Pi type (typechecker invariant)") - } - } - } - // #(t) : [[type_of(t)]] core::Term::Quote(inner) => { let inner_ty = self.type_of(inner); @@ -216,10 +211,9 @@ impl<'core, 'globals> Ctx<'core, 'globals> { | core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Pi(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Quote(_) | core::Term::Splice(_) | core::Term::Let(_) @@ -394,10 +388,9 @@ fn type_universe<'core>( | core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Pi(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Lift(_) | core::Term::Quote(_) | core::Term::Splice(_) @@ -407,9 +400,8 @@ fn type_universe<'core>( core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Quote(_) | core::Term::Splice(_) | core::Term::Let(_) @@ -422,19 +414,16 @@ fn types_equal(a: &core::Term<'_>, b: &core::Term<'_>) -> bool { alpha_eq(a, b) } -/// Count the number of `FunApp` layers between the outermost callee and the given term. +/// Count the total number of arguments already applied by nested `App` nodes. /// /// Used to determine which Pi binder level to target during dependent-return-type /// substitution: global function signatures are elaborated in an empty context, so the /// binder introduced by the i-th Pi in the chain sits at De Bruijn level i. -const fn funapp_depth(term: &core::Term<'_>) -> usize { - let mut depth = 0; - let mut cur = term; - while let core::Term::FunApp(app) = cur { - depth += 1; - cur = app.func; +fn app_base_depth(term: &core::Term<'_>) -> usize { + match term { + core::Term::App(app) => app_base_depth(app.func) + app.args.len(), + _ => 0, } - depth } /// Synthesise and return the elaborated core term; recover its type via `ctx.type_of`. @@ -501,8 +490,8 @@ pub fn infer<'src, 'core>( ); } - // Build curried FunApp chain, checking each arg against the Pi param type. - let mut result: &'core core::Term<'core> = callee; + // Check each argument against the next Pi parameter type and collect. + let mut core_args: Vec<&'core core::Term<'core>> = Vec::with_capacity(args.len()); for (i, arg) in args.iter().enumerate() { let pi = match callee_ty { core::Term::Pi(pi) => pi, @@ -510,9 +499,8 @@ pub fn infer<'src, 'core>( | core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Lift(_) | core::Term::Quote(_) | core::Term::Splice(_) @@ -530,14 +518,11 @@ pub fn infer<'src, 'core>( // Global function signatures are elaborated in an empty context, // so the i-th Pi binder corresponds to De Bruijn level i. callee_ty = subst(ctx.arena, pi.body_ty, Lvl(i), core_arg); - - result = ctx.alloc(core::Term::FunApp(FunApp { - func: result, - arg: core_arg, - })); + core_args.push(core_arg); } - Ok(result) + let args_slice = ctx.alloc_slice(core_args); + Ok(ctx.alloc(core::Term::new_app(callee, args_slice))) } // ------------------------------------------------------------------ App { Prim (BinOp/UnOp) } @@ -569,10 +554,9 @@ pub fn infer<'src, 'core>( | core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Pi(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Lift(_) | core::Term::Quote(_) | core::Term::Splice(_) @@ -596,7 +580,10 @@ pub fn infer<'src, 'core>( | BinOp::BitOr => unreachable!(), }; let core_args = ctx.alloc_slice([core_arg0, core_arg1]); - Ok(ctx.alloc(core::Term::new_prim_app(prim, core_args))) + Ok(ctx.alloc(core::Term::new_app( + ctx.alloc(core::Term::Prim(prim)), + core_args, + ))) } ast::Term::App { func: ast::FunName::BinOp(_) | ast::FunName::UnOp(_), @@ -725,8 +712,8 @@ pub fn infer<'src, 'core>( width, phase: Phase::Meta, })) => { - let embedded = ctx.alloc(core::Term::new_prim_app( - Prim::Embed(*width), + let embedded = ctx.alloc(core::Term::new_app( + ctx.alloc(core::Term::Prim(Prim::Embed(*width))), ctx.alloc_slice([core_inner]), )); Ok(ctx.alloc(core::Term::Splice(embedded))) @@ -735,10 +722,9 @@ pub fn infer<'src, 'core>( | core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Pi(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Quote(_) | core::Term::Splice(_) | core::Term::Let(_) @@ -777,10 +763,9 @@ fn check_exhaustiveness(scrut_ty: &core::Term<'_>, arms: &[ast::MatchArm<'_>]) - | core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Pi(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Lift(_) | core::Term::Quote(_) | core::Term::Splice(_) @@ -939,10 +924,9 @@ pub fn check<'src, 'core>( | core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Pi(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Lift(_) | core::Term::Quote(_) | core::Term::Splice(_) @@ -971,10 +955,9 @@ pub fn check<'src, 'core>( | core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Pi(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Lift(_) | core::Term::Quote(_) | core::Term::Splice(_) @@ -1005,7 +988,10 @@ pub fn check<'src, 'core>( let core_arg1 = check(ctx, phase, rhs, expected)?; let core_args = ctx.alloc_slice([core_arg0, core_arg1]); - Ok(ctx.alloc(core::Term::new_prim_app(prim, core_args))) + Ok(ctx.alloc(core::Term::new_app( + ctx.alloc(core::Term::Prim(prim)), + core_args, + ))) } // ------------------------------------------------------------------ App { UnOp } @@ -1019,10 +1005,9 @@ pub fn check<'src, 'core>( | core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Pi(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Lift(_) | core::Term::Quote(_) | core::Term::Splice(_) @@ -1041,7 +1026,10 @@ pub fn check<'src, 'core>( }; let core_arg = check(ctx, phase, arg, expected)?; let core_args = std::slice::from_ref(ctx.arena.alloc(core_arg)); - Ok(ctx.alloc(core::Term::new_prim_app(prim, core_args))) + Ok(ctx.alloc(core::Term::new_app( + ctx.alloc(core::Term::Prim(prim)), + core_args, + ))) } // ------------------------------------------------------------------ Quote (check mode) @@ -1054,10 +1042,9 @@ pub fn check<'src, 'core>( | core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Pi(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Quote(_) | core::Term::Splice(_) | core::Term::Let(_) @@ -1083,8 +1070,8 @@ pub fn check<'src, 'core>( } let meta_int_ty = ctx.alloc(core::Term::Prim(Prim::IntTy(IntType::meta(*width)))); let core_inner = check(ctx, Phase::Meta, inner, meta_int_ty)?; - let embedded = ctx.alloc(core::Term::new_prim_app( - Prim::Embed(*width), + let embedded = ctx.alloc(core::Term::new_app( + ctx.alloc(core::Term::Prim(Prim::Embed(*width))), ctx.arena.alloc_slice_fill_iter([core_inner]), )); return Ok(ctx.alloc(core::Term::Splice(embedded))); @@ -1119,9 +1106,8 @@ pub fn check<'src, 'core>( | core::Term::Prim(_) | core::Term::Lit(..) | core::Term::Global(_) - | core::Term::PrimApp(_) + | core::Term::App(_) | core::Term::Lam(_) - | core::Term::FunApp(_) | core::Term::Lift(_) | core::Term::Quote(_) | core::Term::Splice(_) diff --git a/compiler/src/checker/test/apply.rs b/compiler/src/checker/test/apply.rs index c717fb0..80692c0 100644 --- a/compiler/src/checker/test/apply.rs +++ b/compiler/src/checker/test/apply.rs @@ -139,11 +139,11 @@ fn check_binop_add_against_u32_succeeds() { let result = check(&mut ctx, Phase::Object, term, expected).expect("should check"); assert!(matches!( result, - core::Term::PrimApp(core::PrimApp { - prim: Prim::Add(IntType { + core::Term::App(core::App { + func: core::Term::Prim(Prim::Add(IntType { width: IntWidth::U32, .. - }), + })), .. }) )); @@ -172,11 +172,11 @@ fn infer_comparison_op_returns_u1() { let ty = ctx.type_of(core_term); assert!(matches!( core_term, - core::Term::PrimApp(core::PrimApp { - prim: Prim::Eq(IntType { + core::Term::App(core::App { + func: core::Term::Prim(Prim::Eq(IntType { width: IntWidth::U64, .. - }), + })), .. }) )); @@ -282,11 +282,11 @@ fn check_eq_op_produces_u1() { // The prim carries the operand type (u64), not u1. assert!(matches!( result, - core::Term::PrimApp(core::PrimApp { - prim: Prim::Eq(IntType { + core::Term::App(core::App { + func: core::Term::Prim(Prim::Eq(IntType { width: IntWidth::U64, .. - }), + })), .. }) )); diff --git a/compiler/src/checker/test/context.rs b/compiler/src/checker/test/context.rs index fbc1505..49acf6a 100644 --- a/compiler/src/checker/test/context.rs +++ b/compiler/src/checker/test/context.rs @@ -169,8 +169,9 @@ fn global_call_is_inferable() { let arena = bumpalo::Bump::new(); let arg = arena.alloc(core::Term::Lit(1, IntType::U64_META)); let global = arena.alloc(core::Term::Global(Name::new("foo"))); - let app = arena.alloc(core::Term::FunApp(core::FunApp { func: global, arg })); - assert!(matches!(app, core::Term::FunApp(_))); + let args = &*arena.alloc_slice_fill_iter([arg as &core::Term]); + let app = arena.alloc(core::Term::new_app(global, args)); + assert!(matches!(app, core::Term::App(_))); } #[test] @@ -268,9 +269,10 @@ fn function_call_to_global() { let arena = bumpalo::Bump::new(); let arg = arena.alloc(core::Term::Lit(42, IntType::U64_META)); let global = arena.alloc(core::Term::Global(Name::new("foo"))); - let app = arena.alloc(core::Term::FunApp(core::FunApp { func: global, arg })); + let args = &*arena.alloc_slice_fill_iter([arg as &core::Term]); + let app = arena.alloc(core::Term::new_app(global, args)); - assert!(matches!(app, core::Term::FunApp(_))); + assert!(matches!(app, core::Term::App(_))); } #[test] @@ -279,15 +281,16 @@ fn builtin_operation_call() { let arg1 = arena.alloc(core::Term::Lit(1, IntType::U64_OBJ)); let arg2 = arena.alloc(core::Term::Lit(2, IntType::U64_OBJ)); let args = &*arena.alloc_slice_fill_iter([&*arg1, &*arg2]); - let app = arena.alloc(core::Term::new_prim_app(Prim::Add(IntType::U64_OBJ), args)); + let prim = arena.alloc(core::Term::Prim(Prim::Add(IntType::U64_OBJ))); + let app = arena.alloc(core::Term::new_app(prim, args)); assert!(matches!( app, - core::Term::PrimApp(core::PrimApp { - prim: Prim::Add(IntType { + core::Term::App(core::App { + func: core::Term::Prim(Prim::Add(IntType { width: IntWidth::U64, .. - }), + })), .. }) )); diff --git a/compiler/src/core/alpha_eq.rs b/compiler/src/core/alpha_eq.rs index 118aaeb..f918985 100644 --- a/compiler/src/core/alpha_eq.rs +++ b/compiler/src/core/alpha_eq.rs @@ -11,8 +11,8 @@ pub fn alpha_eq(a: &Term<'_>, b: &Term<'_>) -> bool { (Term::Prim(p1), Term::Prim(p2)) => p1 == p2, (Term::Lit(n1, t1), Term::Lit(n2, t2)) => n1 == n2 && t1 == t2, (Term::Global(n1), Term::Global(n2)) => n1 == n2, - (Term::PrimApp(a1), Term::PrimApp(a2)) => { - a1.prim == a2.prim + (Term::App(a1), Term::App(a2)) => { + alpha_eq(a1.func, a2.func) && a1.args.len() == a2.args.len() && a1 .args @@ -26,9 +26,6 @@ pub fn alpha_eq(a: &Term<'_>, b: &Term<'_>) -> bool { (Term::Lam(l1), Term::Lam(l2)) => { alpha_eq(l1.param_ty, l2.param_ty) && alpha_eq(l1.body, l2.body) } - (Term::FunApp(a1), Term::FunApp(a2)) => { - alpha_eq(a1.func, a2.func) && alpha_eq(a1.arg, a2.arg) - } (Term::Lift(i1), Term::Lift(i2)) | (Term::Quote(i1), Term::Quote(i2)) | (Term::Splice(i1), Term::Splice(i2)) => alpha_eq(i1, i2), diff --git a/compiler/src/core/mod.rs b/compiler/src/core/mod.rs index 2e24c19..dd3faf1 100644 --- a/compiler/src/core/mod.rs +++ b/compiler/src/core/mod.rs @@ -86,33 +86,21 @@ pub struct Program<'a> { pub functions: &'a [Function<'a>], } -/// Primitive operation application (always fully applied, carries resolved `IntType`) +/// Function or primitive application: `func(args...)` +/// +/// `func` may be any term yielding a function type — most commonly: +/// - `Term::Global(name)` for top-level function calls +/// - `Term::Prim(p)` for built-in primitive operations +/// - any expression for higher-order calls +/// +/// An empty `args` slice represents a zero-argument call and is distinct from +/// a bare reference to `func`. #[derive(Debug, PartialEq, Eq)] -pub struct PrimApp<'a> { - pub prim: Prim, +pub struct App<'a> { + pub func: &'a Term<'a>, pub args: &'a [&'a Term<'a>], } -impl PrimApp<'_> { - /// Returns the number of arguments. - pub const fn arity(&self) -> usize { - self.args.len() - } - - /// Returns `true` if this is a binary infix primitive operator. - pub fn is_binop(&self) -> bool { - let result = self.prim.is_binop(); - if result { - assert_eq!( - self.arity(), - 2, - "binop PrimApp must have exactly 2 arguments" - ); - } - result - } -} - /// Dependent function type: fn(x: A) -> B #[derive(Debug, PartialEq, Eq)] pub struct Pi<'a> { @@ -129,13 +117,6 @@ pub struct Lam<'a> { pub body: &'a Term<'a>, } -/// Function application (single-arg, curried): f(x) -#[derive(Debug, PartialEq, Eq)] -pub struct FunApp<'a> { - pub func: &'a Term<'a>, - pub arg: &'a Term<'a>, -} - /// Let binding with explicit type annotation and a body. #[derive(Debug, PartialEq, Eq)] pub struct Let<'a> { @@ -163,14 +144,12 @@ pub enum Term<'a> { Lit(u64, IntType), /// Global function reference Global(Name<'a>), - /// Primitive operation application (always fully applied, carries resolved `IntType`) - PrimApp(PrimApp<'a>), + /// Function or primitive application: func(args...) + App(App<'a>), /// Dependent function type: fn(x: A) -> B Pi(Pi<'a>), /// Lambda abstraction: |x: A| body Lam(Lam<'a>), - /// Function application (single-arg, curried): f(x) - FunApp(FunApp<'a>), /// Lift: [[T]] — meta type representing object-level code of type T Lift(&'a Self), /// Quotation: #(t) — produce object-level code from a meta expression @@ -240,8 +219,8 @@ impl Term<'static> { } impl<'a> Term<'a> { - pub const fn new_prim_app(prim: Prim, args: &'a [&'a Self]) -> Self { - Self::PrimApp(PrimApp { prim, args }) + pub const fn new_app(func: &'a Self, args: &'a [&'a Self]) -> Self { + Self::App(App { func, args }) } pub const fn new_let(name: &'a str, ty: &'a Self, expr: &'a Self, body: &'a Self) -> Self { @@ -258,18 +237,18 @@ impl<'a> Term<'a> { } } -impl<'a> From> for Term<'a> { - fn from(app: PrimApp<'a>) -> Self { - Self::PrimApp(app) - } -} - impl<'a> From> for Term<'a> { fn from(let_: Let<'a>) -> Self { Self::Let(let_) } } +impl<'a> From> for Term<'a> { + fn from(app: App<'a>) -> Self { + Self::App(app) + } +} + impl<'a> From> for Term<'a> { fn from(match_: Match<'a>) -> Self { Self::Match(match_) diff --git a/compiler/src/core/pretty.rs b/compiler/src/core/pretty.rs index 7661eff..3a7ab71 100644 --- a/compiler/src/core/pretty.rs +++ b/compiler/src/core/pretty.rs @@ -2,7 +2,7 @@ use std::fmt; use crate::parser::ast::Phase; -use super::{Arm, Function, Pat, PrimApp, Program, Term}; +use super::{Arm, Function, Pat, Program, Term}; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -30,10 +30,9 @@ impl<'a> Term<'a> { | Term::Prim(_) | Term::Lit(..) | Term::Global(_) - | Term::PrimApp(_) + | Term::App(_) | Term::Pi(_) | Term::Lam(_) - | Term::FunApp(_) | Term::Lift(_) | Term::Quote(_) | Term::Splice(_) => { @@ -70,8 +69,18 @@ impl<'a> Term<'a> { // ── Global reference ────────────────────────────────────────────────── Term::Global(name) => write!(f, "{name}"), - // ── Primitive application ───────────────────────────────────────────── - Term::PrimApp(app) => app.fmt_prim_app(env, indent, f), + // ── Application ─────────────────────────────────────────────────────── + Term::App(app) => { + app.func.fmt_expr(env, indent, f)?; + write!(f, "(")?; + for (i, arg) in app.args.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + arg.fmt_expr(env, indent, f)?; + } + write!(f, ")") + } // ── Pi type ─────────────────────────────────────────────────────────── Term::Pi(pi) => { @@ -102,21 +111,6 @@ impl<'a> Term<'a> { Ok(()) } - // ── Function application ────────────────────────────────────────────── - // For curried chains FunApp(FunApp(f, a), b), collect args and print f(a, b). - Term::FunApp(_) => { - let (head, args) = self.collect_fun_app_args(); - head.fmt_expr(env, indent, f)?; - write!(f, "(")?; - for (i, arg) in args.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - arg.fmt_expr(env, indent, f)?; - } - write!(f, ")") - } - // ── Lift / Quote / Splice ───────────────────────────────────────────── Term::Lift(inner) => { write!(f, "[[")?; @@ -186,46 +180,14 @@ impl<'a> Term<'a> { | Term::Prim(_) | Term::Lit(..) | Term::Global(_) - | Term::PrimApp(_) + | Term::App(_) | Term::Pi(_) | Term::Lam(_) - | Term::FunApp(_) | Term::Lift(_) | Term::Quote(_) | Term::Splice(_) => self.fmt_term_inline(env, indent, f), } } - - /// Collect a chain of curried `FunApp` into (head, [arg1, arg2, ...]). - fn collect_fun_app_args(&self) -> (&Self, Vec<&Self>) { - let mut args = Vec::new(); - let mut current = self; - while let Term::FunApp(app) = current { - args.push(app.arg); - current = app.func; - } - args.reverse(); - (current, args) - } -} - -impl<'a> PrimApp<'a> { - /// Print a primitive application using `@name(arg, arg, ...)` syntax. - fn fmt_prim_app( - &self, - env: &mut Vec<&'a str>, - indent: usize, - f: &mut fmt::Formatter<'_>, - ) -> fmt::Result { - write!(f, "{}(", self.prim)?; - for (i, arg) in self.args.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - arg.fmt_expr(env, indent, f)?; - } - write!(f, ")") - } } impl<'a> Arm<'a> { diff --git a/compiler/src/core/subst.rs b/compiler/src/core/subst.rs index 94bdf25..edf6c09 100644 --- a/compiler/src/core/subst.rs +++ b/compiler/src/core/subst.rs @@ -1,4 +1,4 @@ -use super::{Arm, FunApp, Lam, Lvl, Pi, Term}; +use super::{Arm, Lam, Lvl, Pi, Term}; /// Substitute `replacement` for `Var(target)` in `term`. /// @@ -14,13 +14,14 @@ pub fn subst<'a>( Term::Var(lvl) if *lvl == target => replacement, Term::Var(_) | Term::Prim(_) | Term::Lit(..) | Term::Global(_) => term, - Term::PrimApp(app) => { + Term::App(app) => { + let new_func = subst(arena, app.func, target, replacement); let new_args = arena.alloc_slice_fill_iter( app.args .iter() .map(|arg| subst(arena, arg, target, replacement)), ); - arena.alloc(Term::new_prim_app(app.prim, new_args)) + arena.alloc(Term::new_app(new_func, new_args)) } Term::Pi(pi) => { @@ -43,15 +44,6 @@ pub fn subst<'a>( })) } - Term::FunApp(app) => { - let new_func = subst(arena, app.func, target, replacement); - let new_arg = subst(arena, app.arg, target, replacement); - arena.alloc(Term::FunApp(FunApp { - func: new_func, - arg: new_arg, - })) - } - Term::Lift(inner) => { let new_inner = subst(arena, inner, target, replacement); arena.alloc(Term::Lift(new_inner)) diff --git a/compiler/src/eval/mod.rs b/compiler/src/eval/mod.rs index 8992d38..95d971a 100644 --- a/compiler/src/eval/mod.rs +++ b/compiler/src/eval/mod.rs @@ -4,7 +4,7 @@ use anyhow::{Result, anyhow, ensure}; use bumpalo::Bump; use crate::core::{ - Arm, FunApp, FunSig, Function, IntType, IntWidth, Lam, Lvl, Name, Pat, Prim, Program, Term, + Arm, FunSig, Function, IntType, IntWidth, Lam, Lvl, Name, Pat, Prim, Program, Term, }; use crate::parser::ast::Phase; @@ -150,15 +150,18 @@ fn eval_meta<'out, 'eval>( obj_next: env.obj_next, }), - // ── Function application ───────────────────────────────────────────── - Term::FunApp(app) => { - let func_val = eval_meta(arena, eval_arena, globals, env, app.func)?; - let arg_val = eval_meta(arena, eval_arena, globals, env, app.arg)?; - apply_closure(arena, eval_arena, globals, func_val, arg_val) - } - - // ── PrimApp ────────────────────────────────────────────────────────── - Term::PrimApp(app) => eval_meta_prim(arena, eval_arena, globals, env, app.prim, app.args), + // ── Application ────────────────────────────────────────────────────── + Term::App(app) => match app.func { + Term::Prim(prim) => eval_meta_prim(arena, eval_arena, globals, env, *prim, app.args), + _ => { + let mut val = eval_meta(arena, eval_arena, globals, env, app.func)?; + for arg in app.args { + let arg_val = eval_meta(arena, eval_arena, globals, env, arg)?; + val = apply_closure(arena, eval_arena, globals, val, arg_val)?; + } + Ok(val) + } + }, // ── Quote: #(t) ────────────────────────────────────────────────────── Term::Quote(inner) => { @@ -482,24 +485,15 @@ fn unstage_obj<'out, 'eval>( Ok(arena.alloc(Term::Global(Name::new(arena.alloc_str(name.as_str()))))) } - // ── PrimApp ────────────────────────────────────────────────────────── - Term::PrimApp(app) => { + // ── App ─────────────────────────────────────────────────────────────── + Term::App(app) => { + let staged_func = unstage_obj(arena, eval_arena, globals, env, app.func)?; let staged_args: &'out [&'out Term<'out>] = arena.alloc_slice_try_fill_iter( app.args .iter() .map(|arg| unstage_obj(arena, eval_arena, globals, env, arg)), )?; - Ok(arena.alloc(Term::new_prim_app(app.prim, staged_args))) - } - - // ── FunApp (in object terms) ───────────────────────────────────────── - Term::FunApp(app) => { - let staged_func = unstage_obj(arena, eval_arena, globals, env, app.func)?; - let staged_arg = unstage_obj(arena, eval_arena, globals, env, app.arg)?; - Ok(arena.alloc(Term::FunApp(FunApp { - func: staged_func, - arg: staged_arg, - }))) + Ok(arena.alloc(Term::new_app(staged_func, staged_args))) } // ── Splice: $(t) — the key staging step ────────────────────────────── diff --git a/compiler/tests/snap/full/pi_basic/3_check.txt b/compiler/tests/snap/full/pi_basic/3_check.txt index 4863cfd..30f6eb0 100644 --- a/compiler/tests/snap/full/pi_basic/3_check.txt +++ b/compiler/tests/snap/full/pi_basic/3_check.txt @@ -11,6 +11,6 @@ fn test() -> u64 { } code fn result() -> u64 { - $(@embed_u64(test)) + $(@embed_u64(test())) } diff --git a/compiler/tests/snap/full/pi_compose/3_check.txt b/compiler/tests/snap/full/pi_compose/3_check.txt index 9ec24b7..598f5a2 100644 --- a/compiler/tests/snap/full/pi_compose/3_check.txt +++ b/compiler/tests/snap/full/pi_compose/3_check.txt @@ -11,10 +11,10 @@ fn inc(x@0: u64) -> u64 { } fn test() -> u64 { - compose(double, inc, 5_u64) + compose(double, inc)(5_u64) } code fn result() -> u64 { - $(@embed_u64(test)) + $(@embed_u64(test())) } diff --git a/compiler/tests/snap/full/pi_const/3_check.txt b/compiler/tests/snap/full/pi_const/3_check.txt index a944b05..2805819 100644 --- a/compiler/tests/snap/full/pi_const/3_check.txt +++ b/compiler/tests/snap/full/pi_const/3_check.txt @@ -3,10 +3,10 @@ fn const_(A@0: Type, B@1: Type) -> fn(A@0) -> fn(B@1) -> A@0 { } fn test() -> u64 { - const_(u64, u8, 42_u64, 7_u8) + const_(u64, u8)(42_u64)(7_u8) } code fn result() -> u64 { - $(@embed_u64(test)) + $(@embed_u64(test())) } diff --git a/compiler/tests/snap/full/pi_dependent_ret/3_check.txt b/compiler/tests/snap/full/pi_dependent_ret/3_check.txt index 1b5782f..e9417a9 100644 --- a/compiler/tests/snap/full/pi_dependent_ret/3_check.txt +++ b/compiler/tests/snap/full/pi_dependent_ret/3_check.txt @@ -11,10 +11,10 @@ fn test_u8() -> u8 { } code fn result_u64() -> u64 { - $(@embed_u64(test_u64)) + $(@embed_u64(test_u64())) } code fn result_u8() -> u8 { - $(@embed_u8(test_u8)) + $(@embed_u8(test_u8())) } diff --git a/compiler/tests/snap/full/pi_lambda_arg/3_check.txt b/compiler/tests/snap/full/pi_lambda_arg/3_check.txt index fe03503..768088f 100644 --- a/compiler/tests/snap/full/pi_lambda_arg/3_check.txt +++ b/compiler/tests/snap/full/pi_lambda_arg/3_check.txt @@ -7,6 +7,6 @@ fn test() -> u64 { } code fn result() -> u64 { - $(@embed_u64(test)) + $(@embed_u64(test())) } diff --git a/compiler/tests/snap/full/pi_nested/3_check.txt b/compiler/tests/snap/full/pi_nested/3_check.txt index 67dba9c..8a1e99e 100644 --- a/compiler/tests/snap/full/pi_nested/3_check.txt +++ b/compiler/tests/snap/full/pi_nested/3_check.txt @@ -11,6 +11,6 @@ fn test() -> u64 { } code fn result() -> u64 { - $(@embed_u64(test)) + $(@embed_u64(test())) } diff --git a/compiler/tests/snap/full/pi_polymorphic_id/3_check.txt b/compiler/tests/snap/full/pi_polymorphic_id/3_check.txt index f7f7d2a..957a747 100644 --- a/compiler/tests/snap/full/pi_polymorphic_id/3_check.txt +++ b/compiler/tests/snap/full/pi_polymorphic_id/3_check.txt @@ -11,10 +11,10 @@ fn test_u8() -> u8 { } code fn result_u64() -> u64 { - $(@embed_u64(test_u64)) + $(@embed_u64(test_u64())) } code fn result_u8() -> u8 { - $(@embed_u8(test_u8)) + $(@embed_u8(test_u8())) } diff --git a/compiler/tests/snap/full/splice_meta_int/3_check.txt b/compiler/tests/snap/full/splice_meta_int/3_check.txt index 2b3221f..68b9fc6 100644 --- a/compiler/tests/snap/full/splice_meta_int/3_check.txt +++ b/compiler/tests/snap/full/splice_meta_int/3_check.txt @@ -3,6 +3,6 @@ fn val() -> u32 { } code fn use_val() -> u32 { - $(@embed_u32(val)) + $(@embed_u32(val())) } diff --git a/compiler/tests/snap/full/staging/3_check.txt b/compiler/tests/snap/full/staging/3_check.txt index 8427053..8513c7a 100644 --- a/compiler/tests/snap/full/staging/3_check.txt +++ b/compiler/tests/snap/full/staging/3_check.txt @@ -3,6 +3,6 @@ fn k() -> [[u64]] { } code fn zero() -> u64 { - $(k) + $(k()) } diff --git a/compiler/tests/snap/stage_error/add_overflow_u32/3_check.txt b/compiler/tests/snap/stage_error/add_overflow_u32/3_check.txt index 5cd8e62..962a00f 100644 --- a/compiler/tests/snap/stage_error/add_overflow_u32/3_check.txt +++ b/compiler/tests/snap/stage_error/add_overflow_u32/3_check.txt @@ -3,6 +3,6 @@ fn f() -> u32 { } code fn g() -> u32 { - $(@embed_u32(f)) + $(@embed_u32(f())) } diff --git a/compiler/tests/snap/stage_error/add_overflow_u8/3_check.txt b/compiler/tests/snap/stage_error/add_overflow_u8/3_check.txt index 80551d5..f0e414d 100644 --- a/compiler/tests/snap/stage_error/add_overflow_u8/3_check.txt +++ b/compiler/tests/snap/stage_error/add_overflow_u8/3_check.txt @@ -3,6 +3,6 @@ fn f() -> u8 { } code fn g() -> u8 { - $(@embed_u8(f)) + $(@embed_u8(f())) } diff --git a/compiler/tests/snap/stage_error/mul_overflow_u8/3_check.txt b/compiler/tests/snap/stage_error/mul_overflow_u8/3_check.txt index 280e10c..ec0f18b 100644 --- a/compiler/tests/snap/stage_error/mul_overflow_u8/3_check.txt +++ b/compiler/tests/snap/stage_error/mul_overflow_u8/3_check.txt @@ -3,6 +3,6 @@ fn f() -> u8 { } code fn g() -> u8 { - $(@embed_u8(f)) + $(@embed_u8(f())) } diff --git a/compiler/tests/snap/stage_error/sub_underflow_u8/3_check.txt b/compiler/tests/snap/stage_error/sub_underflow_u8/3_check.txt index 4169cf8..20bf244 100644 --- a/compiler/tests/snap/stage_error/sub_underflow_u8/3_check.txt +++ b/compiler/tests/snap/stage_error/sub_underflow_u8/3_check.txt @@ -3,6 +3,6 @@ fn f() -> u8 { } code fn g() -> u8 { - $(@embed_u8(f)) + $(@embed_u8(f())) } From bd1f992678ed56b72234bf1eab00a657c6f1b5e3 Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 14:25:03 +0000 Subject: [PATCH 13/43] chore: downgrade wildcard_enum_match_arm lint to allow The lint is too aggressive when wildcards are used for non-exhaustive dispatch (e.g. inner match on App callee kind). Co-Authored-By: Claude Sonnet 4.6 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 15977fc..57725ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ missing_const_for_fn = "deny" trivially_copy_pass_by_ref = "deny" cast_possible_truncation = "deny" explicit_iter_loop = "deny" -wildcard_enum_match_arm = "deny" +wildcard_enum_match_arm = "allow" indexing_slicing = "deny" self_named_module_files = "deny" precedence_bits = "deny" From 55a689207756e4370e7d29bf9f5e3baa342439c2 Mon Sep 17 00:00:00 2001 From: LukasK Date: Wed, 25 Mar 2026 18:35:39 +0000 Subject: [PATCH 14/43] docs: fix fn type syntax examples to require parameter name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anonymous parameters like fn(u64) are not valid — the parser requires at least a wildcard: fn(_: u64). Update all examples accordingly. Co-Authored-By: Claude Sonnet 4.6 --- docs/SYNTAX.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md index c36fb21..391a899 100644 --- a/docs/SYNTAX.md +++ b/docs/SYNTAX.md @@ -50,13 +50,13 @@ Identifiers matching `u[0-9]+` are reserved for primitive types. Function types use the `fn` keyword with parenthesized parameters: ``` -fn(u64) -> u64 // non-dependent function type +fn(_: u64) -> u64 // non-dependent function type (wildcard name required) fn(x: u64) -> u64 // dependent: return type may mention x fn(A: Type, x: A) -> A // polymorphic: type parameter used in value positions -fn(fn(u64) -> u64) -> u64 // higher-order: function taking a function +fn(_: fn(_: u64) -> u64) -> u64 // higher-order: function taking a function ``` -Function types are right-associative: `fn(A) -> fn(B) -> C` means `fn(A) -> (fn(B) -> C)`. +Function types are right-associative: `fn(_: A) -> fn(_: B) -> C` means `fn(_: A) -> (fn(_: B) -> C)`. Multi-parameter function types desugar to nested single-parameter types: From b82c2264aa5cd4040afba1ed35f14aaf51a27538 Mon Sep 17 00:00:00 2001 From: LukasK Date: Thu, 26 Mar 2026 12:56:37 +0000 Subject: [PATCH 15/43] fix: address PR #21 review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pretty: print argument names in function types (fn(_: u64) -> u64) - pretty: suppress @lvl suffix on wildcard binders - subst: remove redundant doc comment lines - test: fix pi_polycompose input (use capped match instead of implicit cast) - test: add pi_arity_mismatch_too_few (one arg to two-arg function) - test: add pi_nested_polymorphic (apply_twice polymorphic in value type) - docs: update SYNTAX.md — wildcard name required, no binder-free form - docs: update bs/pi_types.md — no binder-free form, variables required, multi-param Pi not desugared (arity preserved) Co-Authored-By: Claude Sonnet 4.6 --- compiler/src/core/pretty.rs | 17 +-- compiler/src/core/subst.rs | 3 - .../pi_arity_mismatch_too_few/0_input.splic | 8 ++ .../full/pi_arity_mismatch_too_few/1_lex.txt | 30 ++++ .../pi_arity_mismatch_too_few/2_parse.txt | 60 ++++++++ .../pi_arity_mismatch_too_few/3_check.txt | 2 + compiler/tests/snap/full/pi_basic/3_check.txt | 2 +- .../tests/snap/full/pi_compose/3_check.txt | 2 +- compiler/tests/snap/full/pi_const/3_check.txt | 2 +- .../tests/snap/full/pi_lambda_arg/3_check.txt | 2 +- .../tests/snap/full/pi_nested/3_check.txt | 2 +- .../full/pi_nested_polymorphic/0_input.splic | 12 ++ .../snap/full/pi_nested_polymorphic/1_lex.txt | 77 ++++++++++ .../full/pi_nested_polymorphic/2_parse.txt | 136 ++++++++++++++++++ .../full/pi_nested_polymorphic/3_check.txt | 2 + .../snap/full/pi_polycompose/0_input.splic | 2 +- .../tests/snap/full/pi_polycompose/1_lex.txt | 18 +++ .../snap/full/pi_polycompose/2_parse.txt | 42 +++++- .../snap/full/pi_polycompose/3_check.txt | 27 +++- .../snap/full/pi_polycompose/6_stage.txt | 4 + .../tests/snap/full/pi_repeat/3_check.txt | 2 +- .../snap/full/pi_staging_hof/3_check.txt | 2 +- docs/SYNTAX.md | 5 +- docs/bs/pi_types.md | 10 +- 24 files changed, 437 insertions(+), 32 deletions(-) create mode 100644 compiler/tests/snap/full/pi_arity_mismatch_too_few/0_input.splic create mode 100644 compiler/tests/snap/full/pi_arity_mismatch_too_few/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_arity_mismatch_too_few/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_arity_mismatch_too_few/3_check.txt create mode 100644 compiler/tests/snap/full/pi_nested_polymorphic/0_input.splic create mode 100644 compiler/tests/snap/full/pi_nested_polymorphic/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_nested_polymorphic/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_nested_polymorphic/3_check.txt create mode 100644 compiler/tests/snap/full/pi_polycompose/6_stage.txt diff --git a/compiler/src/core/pretty.rs b/compiler/src/core/pretty.rs index 3a7ab71..d81b7e3 100644 --- a/compiler/src/core/pretty.rs +++ b/compiler/src/core/pretty.rs @@ -85,19 +85,20 @@ impl<'a> Term<'a> { // ── Pi type ─────────────────────────────────────────────────────────── Term::Pi(pi) => { if pi.param_name == "_" { - write!(f, "fn(")?; - pi.param_ty.fmt_expr(env, indent, f)?; - write!(f, ") -> ")?; - pi.body_ty.fmt_expr(env, indent, f) + write!(f, "fn(_: ")?; } else { write!(f, "fn({}@{}: ", pi.param_name, env.len())?; - pi.param_ty.fmt_expr(env, indent, f)?; - write!(f, ") -> ")?; + } + pi.param_ty.fmt_expr(env, indent, f)?; + write!(f, ") -> ")?; + if pi.param_name != "_" { env.push(pi.param_name); - pi.body_ty.fmt_expr(env, indent, f)?; + } + pi.body_ty.fmt_expr(env, indent, f)?; + if pi.param_name != "_" { env.pop(); - Ok(()) } + Ok(()) } // ── Lambda ──────────────────────────────────────────────────────────── diff --git a/compiler/src/core/subst.rs b/compiler/src/core/subst.rs index edf6c09..ef5f7a8 100644 --- a/compiler/src/core/subst.rs +++ b/compiler/src/core/subst.rs @@ -1,9 +1,6 @@ use super::{Arm, Lam, Lvl, Pi, Term}; /// Substitute `replacement` for `Var(target)` in `term`. -/// -/// Used for dependent return types: when applying `f : Pi(x, A, B)` to `arg`, -/// the result type is `subst(arena, B, target_lvl, arg)`. pub fn subst<'a>( arena: &'a bumpalo::Bump, term: &'a Term<'a>, diff --git a/compiler/tests/snap/full/pi_arity_mismatch_too_few/0_input.splic b/compiler/tests/snap/full/pi_arity_mismatch_too_few/0_input.splic new file mode 100644 index 0000000..3b84764 --- /dev/null +++ b/compiler/tests/snap/full/pi_arity_mismatch_too_few/0_input.splic @@ -0,0 +1,8 @@ +// ERROR: too few arguments to a two-arg function +fn add(x: u64, y: u64) -> u64 { + x + y +} + +fn test() -> u64 { + add(1) +} diff --git a/compiler/tests/snap/full/pi_arity_mismatch_too_few/1_lex.txt b/compiler/tests/snap/full/pi_arity_mismatch_too_few/1_lex.txt new file mode 100644 index 0000000..7cf4c46 --- /dev/null +++ b/compiler/tests/snap/full/pi_arity_mismatch_too_few/1_lex.txt @@ -0,0 +1,30 @@ +Fn +Ident("add") +LParen +Ident("x") +Colon +Ident("u64") +Comma +Ident("y") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("x") +Plus +Ident("y") +RBrace +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("add") +LParen +Num(1) +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_arity_mismatch_too_few/2_parse.txt b/compiler/tests/snap/full/pi_arity_mismatch_too_few/2_parse.txt new file mode 100644 index 0000000..8fac109 --- /dev/null +++ b/compiler/tests/snap/full/pi_arity_mismatch_too_few/2_parse.txt @@ -0,0 +1,60 @@ +Program { + functions: [ + Function { + phase: Meta, + name: "add", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + Param { + name: "y", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: Add, + args: [ + Var( + "x", + ), + Var( + "y", + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "test", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: Var( + "add", + ), + args: [ + Lit( + 1, + ), + ], + }, + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_arity_mismatch_too_few/3_check.txt b/compiler/tests/snap/full/pi_arity_mismatch_too_few/3_check.txt new file mode 100644 index 0000000..0a81d87 --- /dev/null +++ b/compiler/tests/snap/full/pi_arity_mismatch_too_few/3_check.txt @@ -0,0 +1,2 @@ +ERROR +in function `test`: type mismatch diff --git a/compiler/tests/snap/full/pi_basic/3_check.txt b/compiler/tests/snap/full/pi_basic/3_check.txt index 30f6eb0..aaab64c 100644 --- a/compiler/tests/snap/full/pi_basic/3_check.txt +++ b/compiler/tests/snap/full/pi_basic/3_check.txt @@ -1,4 +1,4 @@ -fn apply(f@0: fn(u64) -> u64, x@1: u64) -> u64 { +fn apply(f@0: fn(_: u64) -> u64, x@1: u64) -> u64 { f@0(x@1) } diff --git a/compiler/tests/snap/full/pi_compose/3_check.txt b/compiler/tests/snap/full/pi_compose/3_check.txt index 598f5a2..8935a86 100644 --- a/compiler/tests/snap/full/pi_compose/3_check.txt +++ b/compiler/tests/snap/full/pi_compose/3_check.txt @@ -1,4 +1,4 @@ -fn compose(f@0: fn(u64) -> u64, g@1: fn(u64) -> u64) -> fn(u64) -> u64 { +fn compose(f@0: fn(_: u64) -> u64, g@1: fn(_: u64) -> u64) -> fn(_: u64) -> u64 { |x@2: u64| f@0(g@1(x@2)) } diff --git a/compiler/tests/snap/full/pi_const/3_check.txt b/compiler/tests/snap/full/pi_const/3_check.txt index 2805819..5c4d60c 100644 --- a/compiler/tests/snap/full/pi_const/3_check.txt +++ b/compiler/tests/snap/full/pi_const/3_check.txt @@ -1,4 +1,4 @@ -fn const_(A@0: Type, B@1: Type) -> fn(A@0) -> fn(B@1) -> A@0 { +fn const_(A@0: Type, B@1: Type) -> fn(_: A@0) -> fn(_: B@1) -> A@0 { |a@2: A@0| |b@3: B@1| a@2 } diff --git a/compiler/tests/snap/full/pi_lambda_arg/3_check.txt b/compiler/tests/snap/full/pi_lambda_arg/3_check.txt index 768088f..f9a981a 100644 --- a/compiler/tests/snap/full/pi_lambda_arg/3_check.txt +++ b/compiler/tests/snap/full/pi_lambda_arg/3_check.txt @@ -1,4 +1,4 @@ -fn apply(f@0: fn(u64) -> u64, x@1: u64) -> u64 { +fn apply(f@0: fn(_: u64) -> u64, x@1: u64) -> u64 { f@0(x@1) } diff --git a/compiler/tests/snap/full/pi_nested/3_check.txt b/compiler/tests/snap/full/pi_nested/3_check.txt index 8a1e99e..2f5001f 100644 --- a/compiler/tests/snap/full/pi_nested/3_check.txt +++ b/compiler/tests/snap/full/pi_nested/3_check.txt @@ -1,4 +1,4 @@ -fn apply_twice(f@0: fn(u64) -> u64, x@1: u64) -> u64 { +fn apply_twice(f@0: fn(_: u64) -> u64, x@1: u64) -> u64 { f@0(f@0(x@1)) } diff --git a/compiler/tests/snap/full/pi_nested_polymorphic/0_input.splic b/compiler/tests/snap/full/pi_nested_polymorphic/0_input.splic new file mode 100644 index 0000000..3ba6fff --- /dev/null +++ b/compiler/tests/snap/full/pi_nested_polymorphic/0_input.splic @@ -0,0 +1,12 @@ +// Polymorphic apply_twice: polymorphic in both the value type and the function +fn apply_twice(A: Type, f: fn(_: A) -> A, x: A) -> A { + f(f(x)) +} + +fn inc(x: u64) -> u64 { x + 1 } + +fn test() -> u64 { + apply_twice(u64, inc, 0) +} + +code fn result() -> u64 { $(test()) } diff --git a/compiler/tests/snap/full/pi_nested_polymorphic/1_lex.txt b/compiler/tests/snap/full/pi_nested_polymorphic/1_lex.txt new file mode 100644 index 0000000..0236883 --- /dev/null +++ b/compiler/tests/snap/full/pi_nested_polymorphic/1_lex.txt @@ -0,0 +1,77 @@ +Fn +Ident("apply_twice") +LParen +Ident("A") +Colon +Ident("Type") +Comma +Ident("f") +Colon +Fn +LParen +Ident("_") +Colon +Ident("A") +RParen +Arrow +Ident("A") +Comma +Ident("x") +Colon +Ident("A") +RParen +Arrow +Ident("A") +LBrace +Ident("f") +LParen +Ident("f") +LParen +Ident("x") +RParen +RParen +RBrace +Fn +Ident("inc") +LParen +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Ident("u64") +LBrace +Ident("x") +Plus +Num(1) +RBrace +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("apply_twice") +LParen +Ident("u64") +Comma +Ident("inc") +Comma +Num(0) +RParen +RBrace +Code +Fn +Ident("result") +LParen +RParen +Arrow +Ident("u64") +LBrace +DollarLParen +Ident("test") +LParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_nested_polymorphic/2_parse.txt b/compiler/tests/snap/full/pi_nested_polymorphic/2_parse.txt new file mode 100644 index 0000000..15eccd2 --- /dev/null +++ b/compiler/tests/snap/full/pi_nested_polymorphic/2_parse.txt @@ -0,0 +1,136 @@ +Program { + functions: [ + Function { + phase: Meta, + name: "apply_twice", + params: [ + Param { + name: "A", + ty: Var( + "Type", + ), + }, + Param { + name: "f", + ty: Pi { + params: [ + Param { + name: "_", + ty: Var( + "A", + ), + }, + ], + ret_ty: Var( + "A", + ), + }, + }, + Param { + name: "x", + ty: Var( + "A", + ), + }, + ], + ret_ty: Var( + "A", + ), + body: Block { + stmts: [], + expr: App { + func: Var( + "f", + ), + args: [ + App { + func: Var( + "f", + ), + args: [ + Var( + "x", + ), + ], + }, + ], + }, + }, + }, + Function { + phase: Meta, + name: "inc", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: Add, + args: [ + Var( + "x", + ), + Lit( + 1, + ), + ], + }, + }, + }, + Function { + phase: Meta, + name: "test", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: Var( + "apply_twice", + ), + args: [ + Var( + "u64", + ), + Var( + "inc", + ), + Lit( + 0, + ), + ], + }, + }, + }, + Function { + phase: Object, + name: "result", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: Var( + "test", + ), + args: [], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_nested_polymorphic/3_check.txt b/compiler/tests/snap/full/pi_nested_polymorphic/3_check.txt new file mode 100644 index 0000000..6d94ea5 --- /dev/null +++ b/compiler/tests/snap/full/pi_nested_polymorphic/3_check.txt @@ -0,0 +1,2 @@ +ERROR +in function `apply_twice`: in argument 0 of function call: type mismatch diff --git a/compiler/tests/snap/full/pi_polycompose/0_input.splic b/compiler/tests/snap/full/pi_polycompose/0_input.splic index 61fe7df..0964a51 100644 --- a/compiler/tests/snap/full/pi_polycompose/0_input.splic +++ b/compiler/tests/snap/full/pi_polycompose/0_input.splic @@ -4,7 +4,7 @@ fn compose(A: Type, B: Type, C: Type, f: fn(_: B) -> C, g: fn(_: A) -> B) -> fn( } fn double(x: u64) -> u64 { x + x } -fn to_u8(x: u64) -> u8 { x } +fn to_u8(x: u64) -> u8 { match x { 0 => 0, 1 => 1, 2 => 2, _ => 3 } } fn test() -> u8 { compose(u64, u64, u8, to_u8, double)(5) diff --git a/compiler/tests/snap/full/pi_polycompose/1_lex.txt b/compiler/tests/snap/full/pi_polycompose/1_lex.txt index 02a37a6..87e85d9 100644 --- a/compiler/tests/snap/full/pi_polycompose/1_lex.txt +++ b/compiler/tests/snap/full/pi_polycompose/1_lex.txt @@ -82,7 +82,25 @@ RParen Arrow Ident("u8") LBrace +Match Ident("x") +LBrace +Num(0) +DArrow +Num(0) +Comma +Num(1) +DArrow +Num(1) +Comma +Num(2) +DArrow +Num(2) +Comma +Ident("_") +DArrow +Num(3) +RBrace RBrace Fn Ident("test") diff --git a/compiler/tests/snap/full/pi_polycompose/2_parse.txt b/compiler/tests/snap/full/pi_polycompose/2_parse.txt index 54fa46a..1dcc577 100644 --- a/compiler/tests/snap/full/pi_polycompose/2_parse.txt +++ b/compiler/tests/snap/full/pi_polycompose/2_parse.txt @@ -144,9 +144,45 @@ Program { ), body: Block { stmts: [], - expr: Var( - "x", - ), + expr: Match { + scrutinee: Var( + "x", + ), + arms: [ + MatchArm { + pat: Lit( + 0, + ), + body: Lit( + 0, + ), + }, + MatchArm { + pat: Lit( + 1, + ), + body: Lit( + 1, + ), + }, + MatchArm { + pat: Lit( + 2, + ), + body: Lit( + 2, + ), + }, + MatchArm { + pat: Name( + "_", + ), + body: Lit( + 3, + ), + }, + ], + }, }, }, Function { diff --git a/compiler/tests/snap/full/pi_polycompose/3_check.txt b/compiler/tests/snap/full/pi_polycompose/3_check.txt index dc2c23d..0ee33d6 100644 --- a/compiler/tests/snap/full/pi_polycompose/3_check.txt +++ b/compiler/tests/snap/full/pi_polycompose/3_check.txt @@ -1,2 +1,25 @@ -ERROR -in function `to_u8`: type mismatch +fn compose(A@0: Type, B@1: Type, C@2: Type, f@3: fn(_: B@1) -> C@2, g@4: fn(_: A@0) -> B@1) -> fn(_: A@0) -> C@2 { + |x@5: A@0| f@3(g@4(x@5)) +} + +fn double(x@0: u64) -> u64 { + @add_u64(x@0, x@0) +} + +fn to_u8(x@0: u64) -> u8 { + match x@0 { + 0 => 0_u8, + 1 => 1_u8, + 2 => 2_u8, + _ => 3_u8, + } +} + +fn test() -> u8 { + compose(u64, u64, u8, to_u8, double)(5_u64) +} + +code fn result() -> u8 { + $(@embed_u8(test())) +} + diff --git a/compiler/tests/snap/full/pi_polycompose/6_stage.txt b/compiler/tests/snap/full/pi_polycompose/6_stage.txt new file mode 100644 index 0000000..84580a6 --- /dev/null +++ b/compiler/tests/snap/full/pi_polycompose/6_stage.txt @@ -0,0 +1,4 @@ +code fn result() -> u8 { + 3_u8 +} + diff --git a/compiler/tests/snap/full/pi_repeat/3_check.txt b/compiler/tests/snap/full/pi_repeat/3_check.txt index 044af27..6e926ae 100644 --- a/compiler/tests/snap/full/pi_repeat/3_check.txt +++ b/compiler/tests/snap/full/pi_repeat/3_check.txt @@ -1,4 +1,4 @@ -fn repeat(f@0: fn([[u64]]) -> [[u64]], n@1: u64, x@2: [[u64]]) -> [[u64]] { +fn repeat(f@0: fn(_: [[u64]]) -> [[u64]], n@1: u64, x@2: [[u64]]) -> [[u64]] { match n@1 { 0 => x@2, n@3 => repeat(f@0, @sub_u64(n@3, 1_u64), f@0(x@2)), diff --git a/compiler/tests/snap/full/pi_staging_hof/3_check.txt b/compiler/tests/snap/full/pi_staging_hof/3_check.txt index 5a8038b..a389070 100644 --- a/compiler/tests/snap/full/pi_staging_hof/3_check.txt +++ b/compiler/tests/snap/full/pi_staging_hof/3_check.txt @@ -1,4 +1,4 @@ -fn map_code(f@0: fn([[u64]]) -> [[u64]], x@1: [[u64]]) -> [[u64]] { +fn map_code(f@0: fn(_: [[u64]]) -> [[u64]], x@1: [[u64]]) -> [[u64]] { f@0(x@1) } diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md index 391a899..e85dc51 100644 --- a/docs/SYNTAX.md +++ b/docs/SYNTAX.md @@ -73,7 +73,7 @@ Lambdas use Rust's closure syntax with mandatory type annotations: ``` |x: u64| x + 1 // single parameter |x: u64, y: u64| x + y // multi-parameter (desugars to nested lambdas) -|f: fn(u64) -> u64, x: u64| f(x) // higher-order +|f: fn(_: u64) -> u64, x: u64| f(x) // higher-order ``` Type annotations on lambda parameters are required. This makes lambdas inferable — the typechecker can synthesise the full function type from the annotations and the body. @@ -136,8 +136,7 @@ expr ::= literal fn_type ::= "fn" "(" fn_params ")" "->" expr fn_params ::= (fn_param ("," fn_param)*)? -fn_param ::= identifier ":" expr -- dependent: fn(x: A) -> B - | expr -- non-dependent: fn(A) -> B +fn_param ::= identifier ":" expr -- name required; use "_" for non-dependent lambda ::= "|" param ("," param)* "|" expr diff --git a/docs/bs/pi_types.md b/docs/bs/pi_types.md index a6de125..96fd2d1 100644 --- a/docs/bs/pi_types.md +++ b/docs/bs/pi_types.md @@ -39,15 +39,15 @@ Dependent function types use the `fn` keyword — the same keyword used for defi ``` fn(x: A) -> B // dependent: B may mention x -fn(A) -> B // non-dependent (sugar for fn(_: A) -> B) +fn(_: A) -> B // non-dependent: wildcard name required ``` -Right-associative: `fn(A) -> fn(B) -> C` means `fn(A) -> (fn(B) -> C)`. +Right-associative: `fn(_: A) -> fn(_: B) -> C` means `fn(_: A) -> (fn(_: B) -> C)`. -Multi-parameter function types desugar to nested Pi: +Multi-parameter function types are **not** desugared to nested Pi — the arity is preserved to enable proper arity checking at call sites: ``` -fn(x: A, y: B) -> C ≡ fn(x: A) -> fn(y: B) -> C +fn(x: A, y: B) -> C -- two-argument function, not sugar for nested Pi ``` **Rationale.** Using `fn` for types mirrors its use for definitions — in Splic, `fn` introduces anything function-shaped. The parenthesized parameter syntax `fn(x: A)` is visually distinct from a definition `fn name(x: A)` (the presence of a name between `fn` and `(` distinguishes them). The `(x: A) -> B` Agda/Lean convention was considered but `fn(x: A) -> B` is more Rust-flavored. @@ -67,7 +67,7 @@ Type annotations on lambda parameters are **mandatory**. This makes lambdas infe ### Scope -Pi types and lambdas are **meta-level only**. Object-level functions remain top-level `code fn` definitions. A lambda cannot appear in object-level code, and `fn(A) -> B` cannot appear as an object-level type. This matches the 2LTT philosophy: the meta level is a rich functional language; the object level is a simple low-level language. +Pi types and lambdas are **meta-level only**. Object-level functions remain top-level `code fn` definitions. A lambda cannot appear in object-level code, and `fn(_: A) -> B` cannot appear as an object-level type. This matches the 2LTT philosophy: the meta level is a rich functional language; the object level is a simple low-level language. ## Typing Rules From 5fe996fe3fe1bebffc76554268d79a401eed2bc7 Mon Sep 17 00:00:00 2001 From: LukasK Date: Thu, 26 Mar 2026 17:11:57 +0000 Subject: [PATCH 16/43] refactor: make Pi and Lam hold a variadic param list (uncurried) Replace single-param Pi/Lam with a slice of (name, type) pairs, enabling proper arity checking at the call site and allowing empty parameter lists (thunks: `fn() -> T`). - core: Pi and Lam now carry `params: &[(&str, &Term)]` - checker: upfront arity check in App; dependent subst of earlier args into later param types; Lam type_of builds variadic Pi - eval: zero-param Lam produces a Closure; zero-arg App calls force_thunk (returns already-evaluated values as-is) - subst, alpha_eq, pretty: updated for variadic params - test: add pi_lambda_empty_params showing thunk round-trip Co-Authored-By: Claude Sonnet 4.6 --- compiler/src/checker/mod.rs | 186 +++++++----------- compiler/src/core/alpha_eq.rs | 8 +- compiler/src/core/mod.rs | 26 +-- compiler/src/core/pretty.rs | 36 ++-- compiler/src/core/subst.rs | 20 +- compiler/src/eval/mod.rs | 80 +++++--- .../snap/full/pi_apply_non_fn/3_check.txt | 2 +- .../snap/full/pi_arity_mismatch/3_check.txt | 2 +- .../pi_arity_mismatch_too_few/3_check.txt | 2 +- .../full/pi_lambda_empty_params/0_input.splic | 10 + .../full/pi_lambda_empty_params/1_lex.txt | 46 +++++ .../full/pi_lambda_empty_params/2_parse.txt | 74 +++++++ .../full/pi_lambda_empty_params/3_check.txt | 12 ++ .../full/pi_lambda_empty_params/6_stage.txt | 4 + 14 files changed, 315 insertions(+), 193 deletions(-) create mode 100644 compiler/tests/snap/full/pi_lambda_empty_params/0_input.splic create mode 100644 compiler/tests/snap/full/pi_lambda_empty_params/1_lex.txt create mode 100644 compiler/tests/snap/full/pi_lambda_empty_params/2_parse.txt create mode 100644 compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt create mode 100644 compiler/tests/snap/full/pi_lambda_empty_params/6_stage.txt diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index 8efea4c..d40d0de 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -166,34 +166,36 @@ impl<'core, 'globals> Ctx<'core, 'globals> { _ => { // Global function signatures are elaborated in an empty context, // so the i-th Pi binder is at De Bruijn level (base_depth + i) - // where base_depth counts args already applied by an outer App. + // where base_depth counts args already applied by outer Apps. let base_depth = app_base_depth(app.func); - let mut current_ty = self.type_of(app.func); - for (i, arg) in app.args.iter().enumerate() { - match current_ty { - core::Term::Pi(pi) => { - current_ty = - subst(self.arena, pi.body_ty, Lvl(base_depth + i), arg); + let func_ty = self.type_of(app.func); + match func_ty { + core::Term::Pi(pi) => { + // Substitute each arg for its corresponding Pi param. + let mut result = pi.body_ty; + for (i, arg) in app.args.iter().enumerate() { + result = subst(self.arena, result, Lvl(base_depth + i), arg); } - _ => unreachable!( - "App func must have Pi type for each arg (typechecker invariant)" - ), + result } + _ => unreachable!( + "App func must have Pi type (typechecker invariant)" + ), } - current_ty } }, - // Lam: synthesise Pi from param_ty and body type + // Lam: synthesise Pi from params and body type. core::Term::Lam(lam) => { - self.push_local(lam.param_name, lam.param_ty); + for &(name, ty) in lam.params { + self.push_local(name, ty); + } let body_ty = self.type_of(lam.body); - self.pop_local(); - self.alloc(core::Term::Pi(Pi { - param_name: lam.param_name, - param_ty: lam.param_ty, - body_ty, - })) + for _ in lam.params { + self.pop_local(); + } + let params = self.alloc_slice(lam.params.iter().copied()); + self.alloc(core::Term::Pi(Pi { params, body_ty })) } // #(t) : [[type_of(t)]] @@ -475,7 +477,7 @@ pub fn infer<'src, 'core>( } => { // Elaborate the callee let callee = infer(ctx, phase, func_term)?; - let mut callee_ty = ctx.type_of(callee); + let callee_ty = ctx.type_of(callee); // For globals, verify phase matches. if let core::Term::Global(gname) = callee { @@ -490,34 +492,31 @@ pub fn infer<'src, 'core>( ); } - // Check each argument against the next Pi parameter type and collect. - let mut core_args: Vec<&'core core::Term<'core>> = Vec::with_capacity(args.len()); - for (i, arg) in args.iter().enumerate() { - let pi = match callee_ty { - core::Term::Pi(pi) => pi, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Lam(_) - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => bail!( - "too many arguments: callee expects {i} argument(s), got {}", - args.len() - ), - }; + // Callee type must be Pi; arity must match. + let pi = match callee_ty { + core::Term::Pi(pi) => pi, + _ => bail!("callee is not a function type"), + }; + ensure!( + args.len() == pi.params.len(), + "wrong number of arguments: callee expects {}, got {}", + pi.params.len(), + args.len() + ); - let core_arg = check(ctx, phase, arg, pi.param_ty) + // Check each arg against its Pi param type. + // Global sigs are elaborated in an empty context, so param i is at De Bruijn level i. + // For dependent types, substitute earlier args into later param types. + let base = app_base_depth(callee); + let mut core_args: Vec<&'core core::Term<'core>> = Vec::with_capacity(args.len()); + for (i, (arg, &(_, mut param_ty))) in + args.iter().zip(pi.params.iter()).enumerate() + { + for (j, &earlier_arg) in core_args.iter().enumerate() { + param_ty = subst(ctx.arena, param_ty, Lvl(base + j), earlier_arg); + } + let core_arg = check(ctx, phase, arg, param_ty) .with_context(|| format!("in argument {i} of function call"))?; - - // The return type may depend on the argument (dependent types). - // Global function signatures are elaborated in an empty context, - // so the i-th Pi binder corresponds to De Bruijn level i. - callee_ty = subst(ctx.arena, pi.body_ty, Lvl(i), core_arg); core_args.push(core_arg); } @@ -601,7 +600,6 @@ pub fn infer<'src, 'core>( ); let depth_before = ctx.depth(); - // Elaborate param types and push locals. let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); for p in *params { let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); @@ -620,32 +618,21 @@ pub fn infer<'src, 'core>( "return type must be a type" ); - // Build nested Pi from inside out. - let mut result: &'core core::Term<'core> = core_ret_ty; - for &(param_name, param_ty) in elaborated_params.iter().rev() { + for _ in &elaborated_params { ctx.pop_local(); - result = ctx.alloc(core::Term::Pi(Pi { - param_name, - param_ty, - body_ty: result, - })); } - assert_eq!(ctx.depth(), depth_before, "Pi elaboration leaked locals"); - Ok(result) + let params_slice = ctx.alloc_slice(elaborated_params); + Ok(ctx.alloc(core::Term::Pi(Pi { params: params_slice, body_ty: core_ret_ty }))) } - // ------------------------------------------------------------------ Lam + // ------------------------------------------------------------------ Lam (infer mode) // Lambda with mandatory type annotations — inferable. ast::Term::Lam { params, body } => { ensure!( phase == Phase::Meta, "lambdas are only valid in meta-phase context" ); - ensure!( - !params.is_empty(), - "lambda must have at least one parameter" - ); let depth_before = ctx.depth(); let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); @@ -659,19 +646,12 @@ pub fn infer<'src, 'core>( let core_body = infer(ctx, phase, body)?; - // Build nested Lam from inside out. - let mut result: &'core core::Term<'core> = core_body; - for &(param_name, param_ty) in elaborated_params.iter().rev() { + for _ in &elaborated_params { ctx.pop_local(); - result = ctx.alloc(core::Term::Lam(Lam { - param_name, - param_ty, - body: result, - })); } - assert_eq!(ctx.depth(), depth_before, "Lam elaboration leaked locals"); - Ok(result) + let params_slice = ctx.alloc_slice(elaborated_params); + Ok(ctx.alloc(core::Term::Lam(Lam { params: params_slice, body: core_body }))) } // ------------------------------------------------------------------ Lift @@ -1088,64 +1068,42 @@ pub fn check<'src, 'core>( phase == Phase::Meta, "lambdas are only valid in meta-phase context" ); - ensure!( - !params.is_empty(), - "lambda must have at least one parameter" - ); let depth_before = ctx.depth(); - // Peel off one Pi per lambda param, checking annotation matches. - let mut current_expected = expected; - let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); - - for p in *params { - let pi = match current_expected { - core::Term::Pi(pi) => pi, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Lam(_) - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => { - bail!("lambda has more parameters than the expected function type") - } - }; + // Expected type must be a Pi with matching arity. + let pi = match expected { + core::Term::Pi(pi) => pi, + _ => bail!("expected a function type for this lambda"), + }; + ensure!( + params.len() == pi.params.len(), + "lambda has {} parameter(s) but expected type has {}", + params.len(), + pi.params.len() + ); + let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); + for (p, &(_, pi_param_ty)) in params.iter().zip(pi.params.iter()) { let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); let annotated_ty = infer(ctx, Phase::Meta, p.ty)?; - ensure!( - types_equal(annotated_ty, pi.param_ty), + types_equal(annotated_ty, pi_param_ty), "lambda parameter type mismatch: annotation gives a different type \ than the expected function type" ); - - elaborated_params.push((param_name, pi.param_ty)); - ctx.push_local(param_name, pi.param_ty); - current_expected = pi.body_ty; + elaborated_params.push((param_name, pi_param_ty)); + ctx.push_local(param_name, pi_param_ty); } - let core_body = check(ctx, phase, body, current_expected)?; + let core_body = check(ctx, phase, body, pi.body_ty)?; - // Build nested Lam from inside out. - let mut result: &'core core::Term<'core> = core_body; - for &(param_name, param_ty) in elaborated_params.iter().rev() { + for _ in &elaborated_params { ctx.pop_local(); - result = ctx.alloc(core::Term::Lam(Lam { - param_name, - param_ty, - body: result, - })); } - assert_eq!(ctx.depth(), depth_before, "Lam check leaked locals"); - Ok(result) + let params_slice = ctx.alloc_slice(elaborated_params); + Ok(ctx.alloc(core::Term::Lam(Lam { params: params_slice, body: core_body }))) } // ------------------------------------------------------------------ Match (check mode) diff --git a/compiler/src/core/alpha_eq.rs b/compiler/src/core/alpha_eq.rs index f918985..082afcf 100644 --- a/compiler/src/core/alpha_eq.rs +++ b/compiler/src/core/alpha_eq.rs @@ -21,10 +21,14 @@ pub fn alpha_eq(a: &Term<'_>, b: &Term<'_>) -> bool { .all(|(x, y)| alpha_eq(x, y)) } (Term::Pi(p1), Term::Pi(p2)) => { - alpha_eq(p1.param_ty, p2.param_ty) && alpha_eq(p1.body_ty, p2.body_ty) + p1.params.len() == p2.params.len() + && p1.params.iter().zip(p2.params.iter()).all(|((_, t1), (_, t2))| alpha_eq(t1, t2)) + && alpha_eq(p1.body_ty, p2.body_ty) } (Term::Lam(l1), Term::Lam(l2)) => { - alpha_eq(l1.param_ty, l2.param_ty) && alpha_eq(l1.body, l2.body) + l1.params.len() == l2.params.len() + && l1.params.iter().zip(l2.params.iter()).all(|((_, t1), (_, t2))| alpha_eq(t1, t2)) + && alpha_eq(l1.body, l2.body) } (Term::Lift(i1), Term::Lift(i2)) | (Term::Quote(i1), Term::Quote(i2)) diff --git a/compiler/src/core/mod.rs b/compiler/src/core/mod.rs index dd3faf1..bf28dbd 100644 --- a/compiler/src/core/mod.rs +++ b/compiler/src/core/mod.rs @@ -57,18 +57,12 @@ pub struct FunSig<'a> { } impl<'a> FunSig<'a> { - /// Construct a nested Pi type from this signature: - /// `fn(x: A, y: B) -> C` becomes `Pi(x, A, Pi(y, B, C))`. + /// Construct a Pi type from this signature. pub fn to_pi_type(&self, arena: &'a bumpalo::Bump) -> &'a Term<'a> { - let mut result = self.ret_ty; - for &(name, ty) in self.params.iter().rev() { - result = arena.alloc(Term::Pi(Pi { - param_name: name, - param_ty: ty, - body_ty: result, - })); - } - result + arena.alloc(Term::Pi(Pi { + params: self.params, + body_ty: self.ret_ty, + })) } } @@ -101,19 +95,17 @@ pub struct App<'a> { pub args: &'a [&'a Term<'a>], } -/// Dependent function type: fn(x: A) -> B +/// Dependent function type: fn(params...) -> body_ty #[derive(Debug, PartialEq, Eq)] pub struct Pi<'a> { - pub param_name: &'a str, - pub param_ty: &'a Term<'a>, + pub params: &'a [(&'a str, &'a Term<'a>)], // (name, type) pairs pub body_ty: &'a Term<'a>, } -/// Lambda abstraction: |x: A| body +/// Lambda abstraction: |params...| body #[derive(Debug, PartialEq, Eq)] pub struct Lam<'a> { - pub param_name: &'a str, - pub param_ty: &'a Term<'a>, + pub params: &'a [(&'a str, &'a Term<'a>)], // (name, type) pairs pub body: &'a Term<'a>, } diff --git a/compiler/src/core/pretty.rs b/compiler/src/core/pretty.rs index d81b7e3..852fc51 100644 --- a/compiler/src/core/pretty.rs +++ b/compiler/src/core/pretty.rs @@ -84,31 +84,37 @@ impl<'a> Term<'a> { // ── Pi type ─────────────────────────────────────────────────────────── Term::Pi(pi) => { - if pi.param_name == "_" { - write!(f, "fn(_: ")?; - } else { - write!(f, "fn({}@{}: ", pi.param_name, env.len())?; + let env_before = env.len(); + write!(f, "fn(")?; + for (i, &(name, ty)) in pi.params.iter().enumerate() { + if i > 0 { write!(f, ", ")?; } + if name == "_" { + write!(f, "_: ")?; + } else { + write!(f, "{}@{}: ", name, env.len())?; + } + ty.fmt_expr(env, indent, f)?; + env.push(name); } - pi.param_ty.fmt_expr(env, indent, f)?; write!(f, ") -> ")?; - if pi.param_name != "_" { - env.push(pi.param_name); - } pi.body_ty.fmt_expr(env, indent, f)?; - if pi.param_name != "_" { - env.pop(); - } + env.truncate(env_before); Ok(()) } // ── Lambda ──────────────────────────────────────────────────────────── Term::Lam(lam) => { - write!(f, "|{}@{}: ", lam.param_name, env.len())?; - lam.param_ty.fmt_expr(env, indent, f)?; + let env_before = env.len(); + write!(f, "|")?; + for (i, &(name, ty)) in lam.params.iter().enumerate() { + if i > 0 { write!(f, ", ")?; } + write!(f, "{}@{}: ", name, env.len())?; + ty.fmt_expr(env, indent, f)?; + env.push(name); + } write!(f, "| ")?; - env.push(lam.param_name); lam.body.fmt_expr(env, indent, f)?; - env.pop(); + env.truncate(env_before); Ok(()) } diff --git a/compiler/src/core/subst.rs b/compiler/src/core/subst.rs index ef5f7a8..6cac250 100644 --- a/compiler/src/core/subst.rs +++ b/compiler/src/core/subst.rs @@ -22,23 +22,19 @@ pub fn subst<'a>( } Term::Pi(pi) => { - let new_param_ty = subst(arena, pi.param_ty, target, replacement); + let new_params = arena.alloc_slice_fill_iter( + pi.params.iter().map(|&(name, ty)| (name, subst(arena, ty, target, replacement))), + ); let new_body_ty = subst(arena, pi.body_ty, target, replacement); - arena.alloc(Term::Pi(Pi { - param_name: pi.param_name, - param_ty: new_param_ty, - body_ty: new_body_ty, - })) + arena.alloc(Term::Pi(Pi { params: new_params, body_ty: new_body_ty })) } Term::Lam(lam) => { - let new_param_ty = subst(arena, lam.param_ty, target, replacement); + let new_params = arena.alloc_slice_fill_iter( + lam.params.iter().map(|&(name, ty)| (name, subst(arena, ty, target, replacement))), + ); let new_body = subst(arena, lam.body, target, replacement); - arena.alloc(Term::Lam(Lam { - param_name: lam.param_name, - param_ty: new_param_ty, - body: new_body, - })) + arena.alloc(Term::Lam(Lam { params: new_params, body: new_body })) } Term::Lift(inner) => { diff --git a/compiler/src/eval/mod.rs b/compiler/src/eval/mod.rs index 95d971a..73333ba 100644 --- a/compiler/src/eval/mod.rs +++ b/compiler/src/eval/mod.rs @@ -144,20 +144,37 @@ fn eval_meta<'out, 'eval>( } // ── Lambda ─────────────────────────────────────────────────────────── - Term::Lam(lam) => Ok(MetaVal::Closure { - body: lam.body, - env: env.bindings.clone(), - obj_next: env.obj_next, - }), + Term::Lam(lam) => { + // For a zero-param lambda (thunk), wrap in a Closure whose body IS the + // lambda body; force_thunk evaluates it when applied to zero args. + // For a multi-param lambda, wrap params[1..] in a synthetic Lam so that + // apply_closure can peel one param at a time. + let body = match lam.params { + [] | [_] => lam.body, + [_, rest @ ..] => { + eval_arena.alloc(Term::Lam(Lam { params: rest, body: lam.body })) + } + }; + Ok(MetaVal::Closure { + body, + env: env.bindings.clone(), + obj_next: env.obj_next, + }) + } // ── Application ────────────────────────────────────────────────────── Term::App(app) => match app.func { Term::Prim(prim) => eval_meta_prim(arena, eval_arena, globals, env, *prim, app.args), _ => { let mut val = eval_meta(arena, eval_arena, globals, env, app.func)?; - for arg in app.args { - let arg_val = eval_meta(arena, eval_arena, globals, env, arg)?; - val = apply_closure(arena, eval_arena, globals, val, arg_val)?; + if app.args.is_empty() { + // Zero-arg call: force the thunk closure. + val = force_thunk(arena, eval_arena, globals, val)?; + } else { + for arg in app.args { + let arg_val = eval_meta(arena, eval_arena, globals, env, arg)?; + val = apply_closure(arena, eval_arena, globals, val, arg_val)?; + } } Ok(val) } @@ -209,29 +226,14 @@ fn global_to_closure<'out, 'eval>( def: &GlobalDef<'eval>, obj_next: Lvl, ) -> MetaVal<'out, 'eval> { - let params = def.sig.params; - if params.is_empty() { - MetaVal::Closure { - body: def.body, - env: Vec::new(), - obj_next, - } - } else { - // Build nested lambdas for params[1..], then wrap in a closure for params[0]. - let mut body: &'eval Term<'eval> = def.body; - for &(name, ty) in params.iter().rev().skip(1) { - body = eval_arena.alloc(Term::Lam(Lam { - param_name: name, - param_ty: ty, - body, - })); + // Called only when params is non-empty (zero-param globals are evaluated immediately). + let body = match def.sig.params { + [_] | [] => def.body, + [_, rest @ ..] => { + eval_arena.alloc(Term::Lam(Lam { params: rest, body: def.body })) } - MetaVal::Closure { - body, - env: Vec::new(), - obj_next, - } - } + }; + MetaVal::Closure { body, env: Vec::new(), obj_next } } /// Apply a closure value to an argument value. @@ -263,6 +265,24 @@ fn apply_closure<'out, 'eval>( } } +/// Force a thunk closure: evaluate its body in the captured environment without pushing any arg. +fn force_thunk<'out, 'eval>( + arena: &'out Bump, + eval_arena: &'eval Bump, + globals: &Globals<'eval>, + val: MetaVal<'out, 'eval>, +) -> Result> { + match val { + MetaVal::Closure { body, env, obj_next, .. } => { + let mut callee_env = Env { bindings: env, obj_next }; + eval_meta(arena, eval_arena, globals, &mut callee_env, body) + } + // Already-evaluated value (e.g. a zero-param global reduced to Lit/Code). + // A zero-arg call is a no-op in this case. + other => Ok(other), + } +} + /// Evaluate a primitive operation at meta level. fn eval_meta_prim<'out, 'eval>( arena: &'out Bump, diff --git a/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt b/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt index 07717ec..ad5e030 100644 --- a/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt +++ b/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt @@ -1,2 +1,2 @@ ERROR -in function `test`: too many arguments: callee expects 0 argument(s), got 1 +in function `test`: callee is not a function type diff --git a/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt b/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt index 3927c5d..4051012 100644 --- a/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt +++ b/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt @@ -1,2 +1,2 @@ ERROR -in function `apply`: too many arguments: callee expects 1 argument(s), got 2 +in function `apply`: wrong number of arguments: callee expects 1, got 2 diff --git a/compiler/tests/snap/full/pi_arity_mismatch_too_few/3_check.txt b/compiler/tests/snap/full/pi_arity_mismatch_too_few/3_check.txt index 0a81d87..c24ac68 100644 --- a/compiler/tests/snap/full/pi_arity_mismatch_too_few/3_check.txt +++ b/compiler/tests/snap/full/pi_arity_mismatch_too_few/3_check.txt @@ -1,2 +1,2 @@ ERROR -in function `test`: type mismatch +in function `test`: wrong number of arguments: callee expects 2, got 1 diff --git a/compiler/tests/snap/full/pi_lambda_empty_params/0_input.splic b/compiler/tests/snap/full/pi_lambda_empty_params/0_input.splic new file mode 100644 index 0000000..3c9092e --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_empty_params/0_input.splic @@ -0,0 +1,10 @@ +// A lambda with an empty parameter list creates a thunk +fn make_thunk(x: u64) -> fn() -> u64 { + || x +} + +fn test() -> u64 { + make_thunk(42)() +} + +code fn result() -> u64 { $(test()) } diff --git a/compiler/tests/snap/full/pi_lambda_empty_params/1_lex.txt b/compiler/tests/snap/full/pi_lambda_empty_params/1_lex.txt new file mode 100644 index 0000000..2a1f00a --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_empty_params/1_lex.txt @@ -0,0 +1,46 @@ +Fn +Ident("make_thunk") +LParen +Ident("x") +Colon +Ident("u64") +RParen +Arrow +Fn +LParen +RParen +Arrow +Ident("u64") +LBrace +Bar +Bar +Ident("x") +RBrace +Fn +Ident("test") +LParen +RParen +Arrow +Ident("u64") +LBrace +Ident("make_thunk") +LParen +Num(42) +RParen +LParen +RParen +RBrace +Code +Fn +Ident("result") +LParen +RParen +Arrow +Ident("u64") +LBrace +DollarLParen +Ident("test") +LParen +RParen +RParen +RBrace diff --git a/compiler/tests/snap/full/pi_lambda_empty_params/2_parse.txt b/compiler/tests/snap/full/pi_lambda_empty_params/2_parse.txt new file mode 100644 index 0000000..01110b7 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_empty_params/2_parse.txt @@ -0,0 +1,74 @@ +Program { + functions: [ + Function { + phase: Meta, + name: "make_thunk", + params: [ + Param { + name: "x", + ty: Var( + "u64", + ), + }, + ], + ret_ty: Pi { + params: [], + ret_ty: Var( + "u64", + ), + }, + body: Block { + stmts: [], + expr: Lam { + params: [], + body: Var( + "x", + ), + }, + }, + }, + Function { + phase: Meta, + name: "test", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: App { + func: App { + func: Var( + "make_thunk", + ), + args: [ + Lit( + 42, + ), + ], + }, + args: [], + }, + }, + }, + Function { + phase: Object, + name: "result", + params: [], + ret_ty: Var( + "u64", + ), + body: Block { + stmts: [], + expr: Splice( + App { + func: Var( + "test", + ), + args: [], + }, + ), + }, + }, + ], +} diff --git a/compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt b/compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt new file mode 100644 index 0000000..c9b7ada --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt @@ -0,0 +1,12 @@ +fn make_thunk(x@0: u64) -> fn() -> u64 { + || x@0 +} + +fn test() -> u64 { + make_thunk(42_u64)() +} + +code fn result() -> u64 { + $(@embed_u64(test())) +} + diff --git a/compiler/tests/snap/full/pi_lambda_empty_params/6_stage.txt b/compiler/tests/snap/full/pi_lambda_empty_params/6_stage.txt new file mode 100644 index 0000000..8e18575 --- /dev/null +++ b/compiler/tests/snap/full/pi_lambda_empty_params/6_stage.txt @@ -0,0 +1,4 @@ +code fn result() -> u64 { + 42_u64 +} + From e02aacd1bd0640f9f1c6c1a2dea3eafe0629a91f Mon Sep 17 00:00:00 2001 From: LukasK Date: Thu, 26 Mar 2026 17:46:55 +0000 Subject: [PATCH 17/43] refactor: add phase to Pi, use &Term in globals table, remove FunSig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pi now carries a `phase: Phase` field, making meta-level and object-level function types distinct in the core IR. This allows the globals table to store `&Term` (always a Pi) instead of `FunSig`, unifying type lookup for globals and locals — both are now `&Term` lookups. - core: add `phase` to `Pi`; remove `FunSig`; `Function` stores `ty: &Term` (always a Pi) with a `pi()` convenience method - checker: globals table is now `HashMap`; `type_of(Global)` returns the stored Pi directly with no conversion; phase check for calls reads `pi.phase` instead of `sig.phase`; elaborate_sig returns `&Term` - eval: `GlobalDef` stores `ty: &Term`; staging constructs a Pi term for the output Function - subst, alpha_eq, pretty: propagate/compare/display `pi.phase` - tests: update helpers and inline FunSig literals to Pi term construction Co-Authored-By: Claude Sonnet 4.6 --- compiler/src/checker/mod.rs | 93 +++++++++++-------------- compiler/src/checker/test/apply.rs | 14 ++-- compiler/src/checker/test/helpers.rs | 31 ++++----- compiler/src/checker/test/matching.rs | 66 +++++------------- compiler/src/checker/test/meta.rs | 12 +--- compiler/src/checker/test/mod.rs | 2 +- compiler/src/checker/test/signatures.rs | 30 ++++---- compiler/src/core/alpha_eq.rs | 3 +- compiler/src/core/mod.rs | 42 +++++------ compiler/src/core/pretty.rs | 10 +-- compiler/src/core/subst.rs | 2 +- compiler/src/eval/mod.rs | 44 ++++++------ 12 files changed, 145 insertions(+), 204 deletions(-) diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index d40d0de..c5b0aa3 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -19,15 +19,16 @@ pub struct Ctx<'core, 'globals> { /// Local variables: (source name, core type) /// Indexed by De Bruijn level (0 = outermost in current scope, len-1 = most recent) locals: Vec<(&'core str, &'core core::Term<'core>)>, - /// Global function signatures: name -> signature. + /// Global function types: name -> Pi term. + /// Storing `&Term` (always a Pi) unifies type lookup for globals and locals. /// Borrowed independently of the arena so the map can live on the stack. - globals: &'globals HashMap, core::FunSig<'core>>, + globals: &'globals HashMap, &'core core::Term<'core>>, } impl<'core, 'globals> Ctx<'core, 'globals> { pub const fn new( arena: &'core bumpalo::Bump, - globals: &'globals HashMap, core::FunSig<'core>>, + globals: &'globals HashMap, &'core core::Term<'core>>, ) -> Self { Ctx { arena, @@ -129,14 +130,12 @@ impl<'core, 'globals> Ctx<'core, 'globals> { self.alloc(core::Term::Lift(core::Term::int_ty(*w, Phase::Object))) } - // Global reference: type is the Pi type of the signature. - core::Term::Global(name) => { - let sig = self - .globals - .get(name) - .expect("Global with unknown name (typechecker invariant)"); - sig.to_pi_type(self.arena) - } + // Global reference: look up its Pi type directly from the globals table. + core::Term::Global(name) => self + .globals + .get(name) + .copied() + .expect("Global with unknown name (typechecker invariant)"), // App: dispatch on func. // - Prim callee: return type is determined by the primitive. @@ -195,7 +194,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { self.pop_local(); } let params = self.alloc_slice(lam.params.iter().copied()); - self.alloc(core::Term::Pi(Pi { params, body_ty })) + self.alloc(core::Term::Pi(Pi { params, body_ty, phase: Phase::Meta })) } // #(t) : [[type_of(t)]] @@ -269,10 +268,11 @@ fn builtin_prim_ty(name: &str, phase: Phase) -> Option<&'static core::Term<'stat }) } +/// Elaborate one function's signature into a `Term::Pi` (the globals table entry). fn elaborate_sig<'src, 'core>( arena: &'core bumpalo::Bump, func: &ast::Function<'src>, -) -> Result> { +) -> Result<&'core core::Term<'core>> { let empty_globals = HashMap::new(); let mut ctx = Ctx::new(arena, &empty_globals); @@ -284,25 +284,20 @@ fn elaborate_sig<'src, 'core>( Ok((param_name, param_ty)) }))?; - let ret_ty = infer(&mut ctx, func.phase, func.ret_ty)?; + let body_ty = infer(&mut ctx, func.phase, func.ret_ty)?; - Ok(core::FunSig { - params, - ret_ty, - phase: func.phase, - }) + Ok(arena.alloc(core::Term::Pi(Pi { params, body_ty, phase: func.phase }))) } /// Pass 1: collect all top-level function signatures into a globals table. /// -/// Type annotations on parameters and return types are elaborated here so that -/// pass 2 (body elaboration) has fully-typed signatures available for all -/// functions, including forward references. +/// Each entry is a `Term::Pi` carrying the function's phase, param types, and return type. +/// This allows pass 2 to look up a global's type the same way it looks up a local's type. pub(crate) fn collect_signatures<'src, 'core>( arena: &'core bumpalo::Bump, program: &ast::Program<'src>, -) -> Result, core::FunSig<'core>>> { - let mut globals: HashMap, core::FunSig<'core>> = HashMap::new(); +) -> Result, &'core core::Term<'core>>> { + let mut globals: HashMap, &'core core::Term<'core>> = HashMap::new(); for func in program.functions { let name = core::Name::new(arena.alloc_str(func.name.as_str())); @@ -312,9 +307,9 @@ pub(crate) fn collect_signatures<'src, 'core>( "duplicate function name `{name}`" ); - let sig = elaborate_sig(arena, func).with_context(|| format!("in function `{name}`"))?; + let ty = elaborate_sig(arena, func).with_context(|| format!("in function `{name}`"))?; - globals.insert(name, sig); + globals.insert(name, ty); } Ok(globals) @@ -324,34 +319,30 @@ pub(crate) fn collect_signatures<'src, 'core>( fn elaborate_bodies<'src, 'core>( arena: &'core bumpalo::Bump, program: &ast::Program<'src>, - globals: &HashMap, core::FunSig<'core>>, + globals: &HashMap, &'core core::Term<'core>>, ) -> Result> { let functions: &'core [core::Function<'core>] = arena.alloc_slice_try_fill_iter(program.functions.iter().map(|func| -> Result<_> { let name = core::Name::new(arena.alloc_str(func.name.as_str())); - let ast_sig = globals.get(&name).expect("signature missing from pass 1"); + let ty = *globals.get(&name).expect("signature missing from pass 1"); + let pi = match ty { + core::Term::Pi(pi) => pi, + _ => unreachable!("globals table must contain Pi types"), + }; // Build a fresh context borrowing the stack-owned globals map. let mut ctx = Ctx::new(arena, globals); // Push parameters as locals so the body can reference them. - for (pname, pty) in ast_sig.params { + for (pname, pty) in pi.params { ctx.push_local(pname, pty); } // Elaborate the body, checking it against the declared return type. - let body = check(&mut ctx, ast_sig.phase, func.body, ast_sig.ret_ty) + let body = check(&mut ctx, pi.phase, func.body, pi.body_ty) .with_context(|| format!("in function `{name}`"))?; - // Re-borrow sig from globals (ctx was consumed in the check above). - // We need the sig fields for the Function; collect them before moving ctx. - let sig = core::FunSig { - params: ast_sig.params, - ret_ty: ast_sig.ret_ty, - phase: ast_sig.phase, - }; - - Ok(core::Function { name, sig, body }) + Ok(core::Function { name, ty, body }) }))?; Ok(core::Program { functions }) @@ -479,24 +470,20 @@ pub fn infer<'src, 'core>( let callee = infer(ctx, phase, func_term)?; let callee_ty = ctx.type_of(callee); - // For globals, verify phase matches. - if let core::Term::Global(gname) = callee { - let sig = ctx - .globals - .get(gname) - .expect("Global must be in globals table"); - ensure!( - sig.phase == phase, - "function `{gname}` is a {}-phase function, but called in {phase}-phase context", - sig.phase - ); - } - // Callee type must be Pi; arity must match. let pi = match callee_ty { core::Term::Pi(pi) => pi, _ => bail!("callee is not a function type"), }; + + // For globals, verify phase matches (phase is now carried on the Pi itself). + if let core::Term::Global(gname) = callee { + ensure!( + pi.phase == phase, + "function `{gname}` is a {}-phase function, but called in {phase}-phase context", + pi.phase + ); + } ensure!( args.len() == pi.params.len(), "wrong number of arguments: callee expects {}, got {}", @@ -623,7 +610,7 @@ pub fn infer<'src, 'core>( } assert_eq!(ctx.depth(), depth_before, "Pi elaboration leaked locals"); let params_slice = ctx.alloc_slice(elaborated_params); - Ok(ctx.alloc(core::Term::Pi(Pi { params: params_slice, body_ty: core_ret_ty }))) + Ok(ctx.alloc(core::Term::Pi(Pi { params: params_slice, body_ty: core_ret_ty, phase: Phase::Meta }))) } // ------------------------------------------------------------------ Lam (infer mode) diff --git a/compiler/src/checker/test/apply.rs b/compiler/src/checker/test/apply.rs index 80692c0..50dcadb 100644 --- a/compiler/src/checker/test/apply.rs +++ b/compiler/src/checker/test/apply.rs @@ -8,7 +8,7 @@ fn infer_global_call_no_args_returns_ret_ty() { let src_arena = bumpalo::Bump::new(); let core_arena = bumpalo::Bump::new(); let mut globals = HashMap::new(); - globals.insert(Name::new("f"), sig_no_params_returns_u64()); + globals.insert(Name::new("f"), sig_no_params_returns_u64(&core_arena)); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let term = src_arena.alloc(ast::Term::App { @@ -48,7 +48,7 @@ fn infer_global_call_wrong_arity_fails() { let extra_arg = src_arena.alloc(ast::Term::Lit(99)); let args = src_arena.alloc_slice_fill_iter([extra_arg as &ast::Term]); let mut globals = HashMap::new(); - globals.insert(Name::new("f"), sig_no_params_returns_u64()); + globals.insert(Name::new("f"), sig_no_params_returns_u64(&core_arena)); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let term = src_arena.alloc(ast::Term::App { @@ -67,14 +67,8 @@ fn infer_global_call_phase_mismatch_fails() { // `code fn f() -> u64` — object-phase function let u64_obj = core_arena.alloc(core::Term::Prim(Prim::IntTy(IntType::U64_OBJ))); let mut globals = HashMap::new(); - globals.insert( - Name::new("f"), - FunSig { - params: &[], - ret_ty: u64_obj, - phase: Phase::Object, - }, - ); + let f_ty: &core::Term = core_arena.alloc(core::Term::Pi(Pi { params: &[], body_ty: u64_obj, phase: Phase::Object })); + globals.insert(Name::new("f"), f_ty); let mut ctx = test_ctx_with_globals(&core_arena, &globals); // Call `f()` from meta phase — should be rejected. diff --git a/compiler/src/checker/test/helpers.rs b/compiler/src/checker/test/helpers.rs index 4cd9ac0..0bff9e8 100644 --- a/compiler/src/checker/test/helpers.rs +++ b/compiler/src/checker/test/helpers.rs @@ -4,7 +4,7 @@ use super::*; /// Helper to create a test context with empty globals pub fn test_ctx(arena: &bumpalo::Bump) -> Ctx<'_, '_> { - static EMPTY: std::sync::OnceLock, core::FunSig<'static>>> = + static EMPTY: std::sync::OnceLock, &'static core::Term<'static>>> = std::sync::OnceLock::new(); let globals = EMPTY.get_or_init(HashMap::new); Ctx::new(arena, globals) @@ -15,29 +15,26 @@ pub fn test_ctx(arena: &bumpalo::Bump) -> Ctx<'_, '_> { /// The caller must ensure `globals` outlives the returned `Ctx`. pub fn test_ctx_with_globals<'core, 'globals>( arena: &'core bumpalo::Bump, - globals: &'globals HashMap, core::FunSig<'core>>, + globals: &'globals HashMap, &'core core::Term<'core>>, ) -> Ctx<'core, 'globals> { Ctx::new(arena, globals) } -/// Helper: build a simple `FunSig` for a function `fn f() -> u64` (no params, meta phase). -pub fn sig_no_params_returns_u64() -> FunSig<'static> { - let ret_ty = &core::Term::U64_META; - FunSig { +/// Helper: build a Pi term for a function `fn f() -> u64` (no params, meta phase). +pub fn sig_no_params_returns_u64(arena: &bumpalo::Bump) -> &core::Term<'_> { + arena.alloc(core::Term::Pi(Pi { params: &[], - ret_ty, + body_ty: &core::Term::U64_META, phase: Phase::Meta, - } + })) } -/// Helper: build a `FunSig` for `fn f(x: u32) -> u64`. -pub fn sig_one_param_returns_u64(core_arena: &bumpalo::Bump) -> FunSig<'_> { - let u32_ty = &core::Term::U32_META; - let u64_ty = &core::Term::U64_META; - let param = core_arena.alloc(("x", u32_ty as &core::Term)); - FunSig { - params: std::slice::from_ref(param), - ret_ty: u64_ty, +/// Helper: build a Pi term for `fn f(x: u32) -> u64`. +pub fn sig_one_param_returns_u64<'a>(arena: &'a bumpalo::Bump) -> &'a core::Term<'a> { + let params = arena.alloc_slice_fill_iter([("x", &core::Term::U32_META as &core::Term)]); + arena.alloc(core::Term::Pi(Pi { + params, + body_ty: &core::Term::U64_META, phase: Phase::Meta, - } + })) } diff --git a/compiler/src/checker/test/matching.rs b/compiler/src/checker/test/matching.rs index d33e5ee..879b913 100644 --- a/compiler/src/checker/test/matching.rs +++ b/compiler/src/checker/test/matching.rs @@ -10,14 +10,9 @@ fn check_match_all_arms_same_type_succeeds() { let u32_ty_core = &core::Term::U32_META; let mut globals = HashMap::new(); - globals.insert( - Name::new("k32"), - FunSig { - params: &[], - ret_ty: u32_ty_core, - phase: Phase::Meta, - }, - ); + globals.insert(Name::new("k32"), core_arena.alloc(core::Term::Pi(Pi { + params: &[], body_ty: u32_ty_core, phase: Phase::Meta, + })) as &_); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; ctx.push_local("x", u32_ty); @@ -54,14 +49,9 @@ fn check_match_u1_fully_covered_succeeds() { let u1_ty_core = &core::Term::U1_META; let mut globals = HashMap::new(); - globals.insert( - Name::new("k1"), - FunSig { - params: &[], - ret_ty: u1_ty_core, - phase: Phase::Meta, - }, - ); + globals.insert(Name::new("k1"), core_arena.alloc(core::Term::Pi(Pi { + params: &[], body_ty: u1_ty_core, phase: Phase::Meta, + })) as &_); let mut ctx = test_ctx_with_globals(&core_arena, &globals); ctx.push_local("x", u1_ty_core); @@ -98,14 +88,9 @@ fn infer_match_u1_partially_covered_fails() { let u1_ty_core = &core::Term::U1_META; let mut globals = HashMap::new(); - globals.insert( - Name::new("k1"), - FunSig { - params: &[], - ret_ty: u1_ty_core, - phase: Phase::Meta, - }, - ); + globals.insert(Name::new("k1"), core_arena.alloc(core::Term::Pi(Pi { + params: &[], body_ty: u1_ty_core, phase: Phase::Meta, + })) as &_); let mut ctx = test_ctx_with_globals(&core_arena, &globals); ctx.push_local("x", u1_ty_core); @@ -132,14 +117,9 @@ fn infer_match_no_catch_all_fails() { let u32_ty_core = &core::Term::U32_META; let mut globals = HashMap::new(); - globals.insert( - Name::new("k32"), - FunSig { - params: &[], - ret_ty: u32_ty_core, - phase: Phase::Meta, - }, - ); + globals.insert(Name::new("k32"), core_arena.alloc(core::Term::Pi(Pi { + params: &[], body_ty: u32_ty_core, phase: Phase::Meta, + })) as &_); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; ctx.push_local("x", u32_ty); @@ -178,22 +158,12 @@ fn infer_match_arms_type_mismatch_fails() { let u32_ty_core = &core::Term::U32_META; let u64_ty_core = &core::Term::U64_META; let mut globals = HashMap::new(); - globals.insert( - Name::new("k32"), - FunSig { - params: &[], - ret_ty: u32_ty_core, - phase: Phase::Meta, - }, - ); - globals.insert( - Name::new("k64"), - FunSig { - params: &[], - ret_ty: u64_ty_core, - phase: Phase::Meta, - }, - ); + globals.insert(Name::new("k32"), core_arena.alloc(core::Term::Pi(Pi { + params: &[], body_ty: u32_ty_core, phase: Phase::Meta, + })) as &_); + globals.insert(Name::new("k64"), core_arena.alloc(core::Term::Pi(Pi { + params: &[], body_ty: u64_ty_core, phase: Phase::Meta, + })) as &_); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; ctx.push_local("x", u32_ty); diff --git a/compiler/src/checker/test/meta.rs b/compiler/src/checker/test/meta.rs index 1deb510..4018ab7 100644 --- a/compiler/src/checker/test/meta.rs +++ b/compiler/src/checker/test/meta.rs @@ -65,11 +65,7 @@ fn infer_quote_of_global_call_returns_lifted_type() { let mut globals = HashMap::new(); globals.insert( Name::new("f"), - FunSig { - params: &[], - ret_ty: u64_ty_core, - phase: Phase::Object, - }, + core_arena.alloc(core::Term::Pi(Pi { params: &[], body_ty: u64_ty_core, phase: Phase::Object })) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); @@ -98,11 +94,7 @@ fn infer_quote_at_object_phase_fails() { let mut globals = HashMap::new(); globals.insert( Name::new("f"), - FunSig { - params: &[], - ret_ty: u64_ty_core, - phase: Phase::Object, - }, + core_arena.alloc(core::Term::Pi(Pi { params: &[], body_ty: u64_ty_core, phase: Phase::Object })) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); diff --git a/compiler/src/checker/test/mod.rs b/compiler/src/checker/test/mod.rs index 4949961..21dee3e 100644 --- a/compiler/src/checker/test/mod.rs +++ b/compiler/src/checker/test/mod.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use super::*; -use crate::core::{self, FunSig, IntType, IntWidth, Name, Pat, Prim}; +use crate::core::{self, IntType, IntWidth, Name, Pat, Pi, Prim}; use crate::parser::ast::{self, BinOp, FunName, MatchArm, Phase}; mod helpers; diff --git a/compiler/src/checker/test/signatures.rs b/compiler/src/checker/test/signatures.rs index c40e815..ae5169a 100644 --- a/compiler/src/checker/test/signatures.rs +++ b/compiler/src/checker/test/signatures.rs @@ -50,42 +50,40 @@ fn collect_signatures_two_functions() { assert_eq!(globals.len(), 2); - let id_sig = globals - .get(&Name::new("id")) - .expect("id should be in globals"); - assert_eq!(id_sig.phase, Phase::Meta); - assert_eq!(id_sig.params.len(), 1); - assert_eq!(id_sig.params[0].0, "x"); + let id_ty = globals.get(&Name::new("id")).expect("id should be in globals"); + let core::Term::Pi(id_pi) = id_ty else { panic!("expected Pi") }; + assert_eq!(id_pi.phase, Phase::Meta); + assert_eq!(id_pi.params.len(), 1); + assert_eq!(id_pi.params[0].0, "x"); assert!(matches!( - id_sig.params[0].1, + id_pi.params[0].1, core::Term::Prim(Prim::IntTy(IntType { width: IntWidth::U32, .. })) )); assert!(matches!( - id_sig.ret_ty, + id_pi.body_ty, core::Term::Prim(Prim::IntTy(IntType { width: IntWidth::U32, .. })) )); - let add_sig = globals - .get(&Name::new("add_one")) - .expect("add_one should be in globals"); - assert_eq!(add_sig.phase, Phase::Object); - assert_eq!(add_sig.params.len(), 1); - assert_eq!(add_sig.params[0].0, "y"); + let add_ty = globals.get(&Name::new("add_one")).expect("add_one should be in globals"); + let core::Term::Pi(add_pi) = add_ty else { panic!("expected Pi") }; + assert_eq!(add_pi.phase, Phase::Object); + assert_eq!(add_pi.params.len(), 1); + assert_eq!(add_pi.params[0].0, "y"); assert!(matches!( - add_sig.params[0].1, + add_pi.params[0].1, core::Term::Prim(Prim::IntTy(IntType { width: IntWidth::U64, .. })) )); assert!(matches!( - add_sig.ret_ty, + add_pi.body_ty, core::Term::Prim(Prim::IntTy(IntType { width: IntWidth::U64, .. diff --git a/compiler/src/core/alpha_eq.rs b/compiler/src/core/alpha_eq.rs index 082afcf..7cff37a 100644 --- a/compiler/src/core/alpha_eq.rs +++ b/compiler/src/core/alpha_eq.rs @@ -21,7 +21,8 @@ pub fn alpha_eq(a: &Term<'_>, b: &Term<'_>) -> bool { .all(|(x, y)| alpha_eq(x, y)) } (Term::Pi(p1), Term::Pi(p2)) => { - p1.params.len() == p2.params.len() + p1.phase == p2.phase + && p1.params.len() == p2.params.len() && p1.params.iter().zip(p2.params.iter()).all(|((_, t1), (_, t2))| alpha_eq(t1, t2)) && alpha_eq(p1.body_ty, p2.body_ty) } diff --git a/compiler/src/core/mod.rs b/compiler/src/core/mod.rs index bf28dbd..8450440 100644 --- a/compiler/src/core/mod.rs +++ b/compiler/src/core/mod.rs @@ -48,32 +48,27 @@ pub struct Arm<'a> { pub body: &'a Term<'a>, } -/// Top-level function signature (stored in the globals table during elaboration) -#[derive(Debug)] -pub struct FunSig<'a> { - pub params: &'a [(&'a str, &'a Term<'a>)], // (name, type) pairs - pub ret_ty: &'a Term<'a>, - pub phase: Phase, -} - -impl<'a> FunSig<'a> { - /// Construct a Pi type from this signature. - pub fn to_pi_type(&self, arena: &'a bumpalo::Bump) -> &'a Term<'a> { - arena.alloc(Term::Pi(Pi { - params: self.params, - body_ty: self.ret_ty, - })) - } -} - -/// Elaborated top-level function definition +/// Elaborated top-level function definition. +/// +/// `ty` is always a `Term::Pi`; use `Function::pi()` for convenient access. #[derive(Debug)] pub struct Function<'a> { pub name: Name<'a>, - pub sig: FunSig<'a>, + /// Function type (always `Term::Pi`). The Pi carries the phase, params, and return type. + pub ty: &'a Term<'a>, pub body: &'a Term<'a>, } +impl<'a> Function<'a> { + /// Unwrap `self.ty` as a `Pi`. Panics if `ty` is not a `Pi` (typechecker invariant). + pub fn pi(&self) -> &Pi<'a> { + match self.ty { + Term::Pi(pi) => pi, + _ => unreachable!("Function::ty must be a Pi (typechecker invariant)"), + } + } +} + /// Elaborated program: a sequence of top-level function definitions #[derive(Debug)] pub struct Program<'a> { @@ -95,11 +90,16 @@ pub struct App<'a> { pub args: &'a [&'a Term<'a>], } -/// Dependent function type: fn(params...) -> body_ty +/// Dependent function type: `fn(params...) -> body_ty` +/// +/// `phase` distinguishes meta-level (`fn`) from object-level (`code fn`) functions. +/// This allows the globals table to store `&Term` directly, unifying type lookup +/// for globals and locals. #[derive(Debug, PartialEq, Eq)] pub struct Pi<'a> { pub params: &'a [(&'a str, &'a Term<'a>)], // (name, type) pairs pub body_ty: &'a Term<'a>, + pub phase: Phase, } /// Lambda abstraction: |params...| body diff --git a/compiler/src/core/pretty.rs b/compiler/src/core/pretty.rs index 852fc51..8d5a57d 100644 --- a/compiler/src/core/pretty.rs +++ b/compiler/src/core/pretty.rs @@ -239,11 +239,13 @@ impl fmt::Display for Program<'_> { impl fmt::Display for Function<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let pi = self.pi(); + // Build the name environment for the body: one entry per parameter. - let mut env: Vec<&str> = Vec::with_capacity(self.sig.params.len()); + let mut env: Vec<&str> = Vec::with_capacity(pi.params.len()); // Phase prefix. - match self.sig.phase { + match pi.phase { Phase::Object => write!(f, "code ")?, Phase::Meta => {} } @@ -251,7 +253,7 @@ impl fmt::Display for Function<'_> { // Parameters: types are printed with the env as built so far (dependent // function types: earlier params are in scope for later param types). - for (i, (name, ty)) in self.sig.params.iter().enumerate() { + for (i, (name, ty)) in pi.params.iter().enumerate() { if i > 0 { write!(f, ", ")?; } @@ -261,7 +263,7 @@ impl fmt::Display for Function<'_> { } write!(f, ") -> ")?; - self.sig.ret_ty.fmt_expr(&mut env, 1, f)?; + pi.body_ty.fmt_expr(&mut env, 1, f)?; writeln!(f, " {{")?; // Body in statement position at indent depth 1. diff --git a/compiler/src/core/subst.rs b/compiler/src/core/subst.rs index 6cac250..38ca520 100644 --- a/compiler/src/core/subst.rs +++ b/compiler/src/core/subst.rs @@ -26,7 +26,7 @@ pub fn subst<'a>( pi.params.iter().map(|&(name, ty)| (name, subst(arena, ty, target, replacement))), ); let new_body_ty = subst(arena, pi.body_ty, target, replacement); - arena.alloc(Term::Pi(Pi { params: new_params, body_ty: new_body_ty })) + arena.alloc(Term::Pi(Pi { params: new_params, body_ty: new_body_ty, phase: pi.phase })) } Term::Lam(lam) => { diff --git a/compiler/src/eval/mod.rs b/compiler/src/eval/mod.rs index 73333ba..1603dc5 100644 --- a/compiler/src/eval/mod.rs +++ b/compiler/src/eval/mod.rs @@ -4,7 +4,7 @@ use anyhow::{Result, anyhow, ensure}; use bumpalo::Bump; use crate::core::{ - Arm, FunSig, Function, IntType, IntWidth, Lam, Lvl, Name, Pat, Prim, Program, Term, + Arm, Function, IntType, IntWidth, Lam, Lvl, Name, Pat, Pi, Prim, Program, Term, }; use crate::parser::ast::Phase; @@ -99,7 +99,7 @@ impl<'out, 'eval> Env<'out, 'eval> { /// Everything the evaluator needs to know about a top-level function. struct GlobalDef<'a> { - sig: &'a FunSig<'a>, + ty: &'a Term<'a>, // always Term::Pi body: &'a Term<'a>, } @@ -133,7 +133,10 @@ fn eval_meta<'out, 'eval>( let def = globals .get(name) .unwrap_or_else(|| panic!("unknown global `{name}` during staging")); - if def.sig.params.is_empty() { + let Term::Pi(pi) = def.ty else { + unreachable!("global `{name}` must have a Pi type (typechecker invariant)") + }; + if pi.params.is_empty() { // Zero-param global: evaluate the body immediately in a fresh env. let mut callee_env = Env::new(env.obj_next); eval_meta(arena, eval_arena, globals, &mut callee_env, def.body) @@ -227,7 +230,10 @@ fn global_to_closure<'out, 'eval>( obj_next: Lvl, ) -> MetaVal<'out, 'eval> { // Called only when params is non-empty (zero-param globals are evaluated immediately). - let body = match def.sig.params { + let Term::Pi(pi) = def.ty else { + unreachable!("global must have a Pi type (typechecker invariant)") + }; + let body = match pi.params { [_] | [] => def.body, [_, rest @ ..] => { eval_arena.alloc(Term::Lam(Lam { params: rest, body: def.body })) @@ -595,25 +601,18 @@ pub fn unstage_program<'out, 'core>( let globals: Globals<'_> = program .functions .iter() - .map(|f| { - ( - f.name, - GlobalDef { - sig: &f.sig, - body: f.body, - }, - ) - }) + .map(|f| (f.name, GlobalDef { ty: f.ty, body: f.body })) .collect(); let staged_fns: Vec> = program .functions .iter() - .filter(|f| f.sig.phase == Phase::Object) + .filter(|f| f.pi().phase == Phase::Object) .map(|f| -> Result<_> { + let pi = f.pi(); let mut env = Env::new(Lvl::new(0)); - let staged_params = arena.alloc_slice_try_fill_iter(f.sig.params.iter().map( + let staged_params = arena.alloc_slice_try_fill_iter(pi.params.iter().map( |(n, ty)| -> Result<(&'out str, &'out Term<'out>)> { let staged_ty = unstage_obj(arena, &eval_bump, &globals, &mut env, ty)?; env.push_obj(); @@ -621,17 +620,18 @@ pub fn unstage_program<'out, 'core>( }, ))?; - let staged_ret_ty = unstage_obj(arena, &eval_bump, &globals, &mut env, f.sig.ret_ty)?; - + let staged_ret_ty = unstage_obj(arena, &eval_bump, &globals, &mut env, pi.body_ty)?; let staged_body = unstage_obj(arena, &eval_bump, &globals, &mut env, f.body)?; + let staged_ty = arena.alloc(Term::Pi(Pi { + params: staged_params, + body_ty: staged_ret_ty, + phase: Phase::Object, + })); + Ok(Function { name: Name::new(arena.alloc_str(f.name.as_str())), - sig: FunSig { - params: staged_params, - ret_ty: staged_ret_ty, - phase: f.sig.phase, - }, + ty: staged_ty, body: staged_body, }) }) From 26f8ca965c00b6c997a836f15cd418f41c5e23a6 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 12:01:16 +0000 Subject: [PATCH 18/43] refactor: NbE + De Bruijn indices (replace subst with eval/apply/quote) Switch Term::Var from Lvl (absolute) to Ix (De Bruijn index, 0 = innermost). Add core/value.rs with a semantic value domain (Value, VLam, VPi, Closure) and the three NbE functions eval/inst/quote. Delete the broken subst.rs. The checker now maintains an NbE environment (env + types as Values, lvl) and uses eval + Pi-closure application instead of syntactic substitution when checking dependent function call arguments. This fixes the variable-capture bug where substituting a replacement containing binders could produce stale indices. The staging evaluator is updated for Ix-based variable lookup and gains index-shift logic for Code values: a Code { term, depth } now records the output depth at creation time, and shift_free_ix adjusts free Ix values when a code value is spliced into a deeper context. Without this fix the accumulator variable in power_acc/power_acc_1 was wrongly resolved to the innermost binding. Also adds stage snapshots for let_type and pi_nested_polymorphic (the latter now type-checks correctly: apply_twice(u64, inc, 0) = 2). Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + compiler/src/checker/mod.rs | 812 +++++++++++------- compiler/src/checker/test/apply.rs | 6 +- compiler/src/checker/test/context.rs | 42 +- compiler/src/checker/test/helpers.rs | 2 +- compiler/src/checker/test/matching.rs | 66 +- compiler/src/checker/test/meta.rs | 12 +- compiler/src/checker/test/mod.rs | 2 +- compiler/src/checker/test/signatures.rs | 16 +- compiler/src/checker/test/var.rs | 21 +- compiler/src/core/alpha_eq.rs | 12 +- compiler/src/core/mod.rs | 34 +- compiler/src/core/pretty.rs | 20 +- compiler/src/core/subst.rs | 69 -- compiler/src/core/value.rs | 375 ++++++++ compiler/src/eval/mod.rs | 208 ++++- compiler/tests/snap/full/let_type/3_check.txt | 12 +- compiler/tests/snap/full/let_type/6_stage.txt | 4 + compiler/tests/snap/full/pi_const/3_check.txt | 14 +- .../full/pi_lambda_empty_params/3_check.txt | 14 +- .../full/pi_nested_polymorphic/3_check.txt | 18 +- .../full/pi_nested_polymorphic/6_stage.txt | 4 + 22 files changed, 1229 insertions(+), 535 deletions(-) delete mode 100644 compiler/src/core/subst.rs create mode 100644 compiler/src/core/value.rs create mode 100644 compiler/tests/snap/full/let_type/6_stage.txt create mode 100644 compiler/tests/snap/full/pi_nested_polymorphic/6_stage.txt diff --git a/.gitignore b/.gitignore index 7d79255..623ef91 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __fuzz__/**/crashes/ scratch/ posts/ .claude/settings.local.json +.claude/worktrees/ diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index c5b0aa3..9458f1a 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use anyhow::{Context as _, Result, anyhow, bail, ensure}; -use crate::core::{self, IntType, IntWidth, Lam, Lvl, Pi, Prim, alpha_eq, subst}; +use crate::core::{self, IntType, IntWidth, Ix, Lam, Lvl, Pi, Prim, alpha_eq, lvl_to_ix, value}; use crate::parser::ast::{self, Phase}; /// Elaboration context. @@ -16,9 +16,16 @@ use crate::parser::ast::{self, Phase}; pub struct Ctx<'core, 'globals> { /// Arena for allocating core terms arena: &'core bumpalo::Bump, - /// Local variables: (source name, core type) - /// Indexed by De Bruijn level (0 = outermost in current scope, len-1 = most recent) - locals: Vec<(&'core str, &'core core::Term<'core>)>, + /// Local variable names (oldest first), for error messages. + names: Vec<&'core str>, + /// Evaluation environment (oldest first): values of locals. + /// `env[env.len() - 1 - ix]` = value of `Var(Ix(ix))`. + env: value::Env<'core>, + /// Types of locals as semantic values (oldest first). + /// `types[types.len() - 1 - ix]` = type of `Var(Ix(ix))`. + types: Vec>, + /// Current De Bruijn level (= `env.len()` = `types.len()`). + lvl: Lvl, /// Global function types: name -> Pi term. /// Storing `&Term` (always a Pi) unifies type lookup for globals and locals. /// Borrowed independently of the arena so the map can live on the stack. @@ -32,7 +39,10 @@ impl<'core, 'globals> Ctx<'core, 'globals> { ) -> Self { Ctx { arena, - locals: Vec::new(), + names: Vec::new(), + env: Vec::new(), + types: Vec::new(), + lvl: Lvl::new(0), globals, } } @@ -50,31 +60,66 @@ impl<'core, 'globals> Ctx<'core, 'globals> { self.arena.alloc_slice_fill_iter(items) } - /// Push a local variable onto the context - fn push_local(&mut self, name: &'core str, ty: &'core core::Term<'core>) { - self.locals.push((name, ty)); + /// Push a local variable onto the context, given its type as a term. + /// Evaluates the type term in the current environment. + pub fn push_local(&mut self, name: &'core str, ty: &'core core::Term<'core>) { + let ty_val = value::eval(self.arena, self.globals, &self.env, ty); + self.env.push(value::Value::Rigid(self.lvl)); + self.types.push(ty_val); + self.lvl = self.lvl.succ(); + self.names.push(name); + } + + /// Push a local variable onto the context, given its type as a Value. + /// The variable itself is a fresh rigid (neutral) variable — use for lambda/pi params. + fn push_local_val(&mut self, name: &'core str, ty_val: value::Value<'core>) { + self.env.push(value::Value::Rigid(self.lvl)); + self.types.push(ty_val); + self.lvl = self.lvl.succ(); + self.names.push(name); + } + + /// Push a let binding: the variable has a known value in the environment. + /// Use for `let x = e` bindings so that dependent references to `x` evaluate correctly. + fn push_let_binding( + &mut self, + name: &'core str, + ty_val: value::Value<'core>, + expr_val: value::Value<'core>, + ) { + self.env.push(expr_val); + self.types.push(ty_val); + self.lvl = self.lvl.succ(); + self.names.push(name); } /// Pop the last local variable - fn pop_local(&mut self) { - self.locals.pop(); + pub fn pop_local(&mut self) { + self.names.pop(); + self.env.pop(); + self.types.pop(); + self.lvl = Lvl(self.lvl.0 - 1); } - /// Look up a variable by name, returning its (level, type). + /// Look up a variable by name, returning its (index, type as Value). /// Searches from the most recently pushed variable inward to handle shadowing. - /// Level is the index from the start of the vec (outermost = 0, most recent = len-1). - fn lookup_local(&self, name: &str) -> Option<(Lvl, &'core core::Term<'core>)> { - for (i, (local_name, ty)) in self.locals.iter().enumerate().rev() { - if *local_name == name { - return Some((Lvl(i), ty)); + pub fn lookup_local(&self, name: &str) -> Option<(Ix, &value::Value<'core>)> { + for (i, &local_name) in self.names.iter().enumerate().rev() { + if local_name == name { + let ix = lvl_to_ix(self.lvl, Lvl(i)); + let ty = self + .types + .get(i) + .expect("types and names are always the same length"); + return Some((ix, ty)); } } None } /// Get the current depth of the locals stack - const fn depth(&self) -> usize { - self.locals.len() + pub const fn depth(&self) -> usize { + self.lvl.0 } /// Helper to create a lifted type [[T]] @@ -82,11 +127,21 @@ impl<'core, 'globals> Ctx<'core, 'globals> { self.arena.alloc(core::Term::Lift(inner)) } - /// Recover the type of an already-elaborated core term without re-elaborating. + /// Evaluate a term in the current environment. + fn eval(&self, term: &'core core::Term<'core>) -> value::Value<'core> { + value::eval(self.arena, self.globals, &self.env, term) + } + + /// Quote a value back to a term at the current depth. + fn quote_val(&self, val: &value::Value<'core>) -> &'core core::Term<'core> { + value::quote(self.arena, self.lvl, val) + } + + /// Recover the type of an already-elaborated core term as a semantic Value. /// /// Precondition: `term` was produced by `infer` or `check` in a context /// compatible with `self`. Panics on typechecker invariant violations. - pub fn type_of(&mut self, term: &'core core::Term<'core>) -> &'core core::Term<'core> { + pub fn val_type_of(&self, term: &'core core::Term<'core>) -> value::Value<'core> { match term { // Literal or arithmetic/bitwise op: type is (or returns) the integer type. core::Term::Lit(_, it) @@ -98,21 +153,26 @@ impl<'core, 'globals> Ctx<'core, 'globals> { | Prim::BitAnd(it) | Prim::BitOr(it) | Prim::BitNot(it), - ) => core::Term::int_ty(it.width, it.phase), - - // Variable: look up by De Bruijn level. - core::Term::Var(lvl) => { - self.locals - .get(lvl.0) - .expect("Var level out of range (typechecker invariant)") - .1 + ) => value::Value::Prim(Prim::IntTy(*it)), + + // Variable: look up type by De Bruijn index. + core::Term::Var(ix) => { + let i = self + .types + .len() + .checked_sub(1 + ix.0) + .expect("Var index out of range (typechecker invariant)"); + self.types + .get(i) + .expect("Var index out of range (typechecker invariant)") + .clone() } // Primitive types inhabit the relevant universe. - core::Term::Prim(Prim::IntTy(it)) => core::Term::universe(it.phase), + core::Term::Prim(Prim::IntTy(it)) => value::Value::U(it.phase), // Type, VmType, and [[T]] all inhabit Type (meta universe). core::Term::Prim(Prim::U(_)) | core::Term::Lift(_) | core::Term::Pi(_) => { - &core::Term::TYPE + value::Value::U(Phase::Meta) } // Comparison ops return u1 at the operand phase. @@ -123,23 +183,31 @@ impl<'core, 'globals> Ctx<'core, 'globals> { | Prim::Gt(it) | Prim::Le(it) | Prim::Ge(it), - ) => core::Term::u1_ty(it.phase), + ) => value::Value::Prim(Prim::IntTy(IntType { + width: IntWidth::U1, + phase: it.phase, + })), // Embed: IntTy(w, Meta) -> [[IntTy(w, Object)]] core::Term::Prim(Prim::Embed(w)) => { - self.alloc(core::Term::Lift(core::Term::int_ty(*w, Phase::Object))) + let obj_int_ty = value::Value::Prim(Prim::IntTy(IntType { + width: *w, + phase: Phase::Object, + })); + value::Value::Lift(self.arena.alloc(obj_int_ty)) } - // Global reference: look up its Pi type directly from the globals table. - core::Term::Global(name) => self - .globals - .get(name) - .copied() - .expect("Global with unknown name (typechecker invariant)"), + // Global reference: look up its Pi type and evaluate. + core::Term::Global(name) => { + let pi_term = self + .globals + .get(name) + .copied() + .expect("Global with unknown name (typechecker invariant)"); + self.eval(pi_term) + } - // App: dispatch on func. - // - Prim callee: return type is determined by the primitive. - // - Other callee: peel Pi types, substituting each arg. + // App: compute return type via NbE. core::Term::App(app) => match app.func { core::Term::Prim(prim) => match prim { Prim::Add(it) @@ -148,88 +216,126 @@ impl<'core, 'globals> Ctx<'core, 'globals> { | Prim::Div(it) | Prim::BitAnd(it) | Prim::BitOr(it) - | Prim::BitNot(it) => core::Term::int_ty(it.width, it.phase), + | Prim::BitNot(it) => value::Value::Prim(Prim::IntTy(*it)), Prim::Eq(it) | Prim::Ne(it) | Prim::Lt(it) | Prim::Gt(it) | Prim::Le(it) - | Prim::Ge(it) => core::Term::u1_ty(it.phase), + | Prim::Ge(it) => value::Value::Prim(Prim::IntTy(IntType { + width: IntWidth::U1, + phase: it.phase, + })), Prim::Embed(w) => { - self.alloc(core::Term::Lift(core::Term::int_ty(*w, Phase::Object))) + let obj_int_ty = value::Value::Prim(Prim::IntTy(IntType { + width: *w, + phase: Phase::Object, + })); + value::Value::Lift(self.arena.alloc(obj_int_ty)) } Prim::IntTy(_) | Prim::U(_) => { unreachable!("type-level prim in App (typechecker invariant)") } }, _ => { - // Global function signatures are elaborated in an empty context, - // so the i-th Pi binder is at De Bruijn level (base_depth + i) - // where base_depth counts args already applied by outer Apps. - let base_depth = app_base_depth(app.func); - let func_ty = self.type_of(app.func); - match func_ty { - core::Term::Pi(pi) => { - // Substitute each arg for its corresponding Pi param. - let mut result = pi.body_ty; - for (i, arg) in app.args.iter().enumerate() { - result = subst(self.arena, result, Lvl(base_depth + i), arg); + // Compute return type by peeling Pi closures via NbE. + let mut pi_val = self.val_type_of(app.func); + for arg in app.args { + match pi_val { + value::Value::Pi(vpi) => { + let arg_val = self.eval(arg); + pi_val = + value::inst(self.arena, self.globals, &vpi.closure, arg_val); } - result + _ => unreachable!("App func must have Pi type (typechecker invariant)"), } - _ => unreachable!( - "App func must have Pi type (typechecker invariant)" - ), } + pi_val } }, // Lam: synthesise Pi from params and body type. core::Term::Lam(lam) => { - for &(name, ty) in lam.params { - self.push_local(name, ty); - } - let body_ty = self.type_of(lam.body); - for _ in lam.params { - self.pop_local(); + // Compute the Pi type for this Lam. + // Build a Pi value matching the Lam's structure. + // Since we need the full Pi type, compute it directly from lam params. + let mut env2 = self.env.clone(); + let mut types2 = self.types.clone(); + let mut lvl2 = self.lvl; + let mut names2 = self.names.clone(); + let mut elaborated_param_types: Vec> = Vec::new(); + for &(pname, pty) in lam.params { + let ty_val = value::eval(self.arena, self.globals, &env2, pty); + elaborated_param_types.push(ty_val.clone()); + env2.push(value::Value::Rigid(lvl2)); + types2.push(ty_val); + lvl2 = lvl2.succ(); + names2.push(pname); } - let params = self.alloc_slice(lam.params.iter().copied()); - self.alloc(core::Term::Pi(Pi { params, body_ty, phase: Phase::Meta })) + let body_ty_term = { + // Compute type of body in extended env + let fake_ctx = Ctx { + arena: self.arena, + names: names2, + env: env2, + types: types2, + lvl: lvl2, + globals: self.globals, + }; + fake_ctx.val_type_of(lam.body) + }; + // Quote body type and build Pi term, then eval back to value + let body_ty_quoted = value::quote(self.arena, lvl2, &body_ty_term); + // Build a Pi term with the same params + let params_slice = self.alloc_slice(lam.params.iter().copied()); + let pi_term = self.alloc(core::Term::Pi(Pi { + params: params_slice, + body_ty: body_ty_quoted, + phase: Phase::Meta, + })); + self.eval(pi_term) } // #(t) : [[type_of(t)]] core::Term::Quote(inner) => { - let inner_ty = self.type_of(inner); - self.alloc(core::Term::Lift(inner_ty)) + let inner_ty = self.val_type_of(inner); + value::Value::Lift(self.arena.alloc(inner_ty)) } // $(t) where t : [[T]] — strips the Lift. core::Term::Splice(inner) => { - let inner_ty = self.type_of(inner); + let inner_ty = self.val_type_of(inner); match inner_ty { - core::Term::Lift(object_ty) => object_ty, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Pi(_) - | core::Term::Lam(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => { - unreachable!("Splice inner must have Lift type (typechecker invariant)") - } + value::Value::Lift(object_ty) => (*object_ty).clone(), + _ => unreachable!("Splice inner must have Lift type (typechecker invariant)"), } } // let x : T = e in body — type is type_of(body) with x in scope. - core::Term::Let(core::Let { name, ty, body, .. }) => { - self.push_local(name, ty); - let result = self.type_of(body); - self.pop_local(); - result + core::Term::Let(core::Let { + name, + ty, + expr, + body, + }) => { + let ty_val = self.eval(ty); + let expr_val = self.eval(expr); + let mut env2 = self.env.clone(); + env2.push(expr_val); + let mut types2 = self.types.clone(); + types2.push(ty_val); + let mut names2 = self.names.clone(); + names2.push(name); + let lvl2 = self.lvl.succ(); + let fake_ctx = Ctx { + arena: self.arena, + names: names2, + env: env2, + types: types2, + lvl: lvl2, + globals: self.globals, + }; + fake_ctx.val_type_of(body) } // match: all arms share the same type; recover from the first. @@ -238,18 +344,32 @@ impl<'core, 'globals> Ctx<'core, 'globals> { .first() .expect("Match with no arms (typechecker invariant)"); match arm.pat { - core::Pat::Lit(_) | core::Pat::Wildcard => self.type_of(arm.body), + core::Pat::Lit(_) | core::Pat::Wildcard => self.val_type_of(arm.body), core::Pat::Bind(name) => { - let scrut_ty = self.type_of(scrutinee); - self.push_local(name, scrut_ty); - let result = self.type_of(arm.body); - self.pop_local(); - result + let scrut_ty = self.val_type_of(scrutinee); + // Extend context with scrutinee binding + let scrut_ty_term = self.quote_val(&scrut_ty); + let mut fake_ctx = Ctx { + arena: self.arena, + names: self.names.clone(), + env: self.env.clone(), + types: self.types.clone(), + lvl: self.lvl, + globals: self.globals, + }; + fake_ctx.push_local(name, scrut_ty_term); + fake_ctx.val_type_of(arm.body) } } } } } + + /// Recover the type of an already-elaborated core term as a `&Term` (quoted). + pub fn type_of(&self, term: &'core core::Term<'core>) -> &'core core::Term<'core> { + let val = self.val_type_of(term); + self.quote_val(&val) + } } /// Resolve a built-in type name to a static core term, using `phase` for integer types. @@ -286,7 +406,11 @@ fn elaborate_sig<'src, 'core>( let body_ty = infer(&mut ctx, func.phase, func.ret_ty)?; - Ok(arena.alloc(core::Term::Pi(Pi { params, body_ty, phase: func.phase }))) + Ok(arena.alloc(core::Term::Pi(Pi { + params, + body_ty, + phase: func.phase, + }))) } /// Pass 1: collect all top-level function signatures into a globals table. @@ -339,7 +463,8 @@ fn elaborate_bodies<'src, 'core>( } // Elaborate the body, checking it against the declared return type. - let body = check(&mut ctx, pi.phase, func.body, pi.body_ty) + let ret_ty_val = ctx.eval(pi.body_ty); + let body = check_val(&mut ctx, pi.phase, func.body, ret_ty_val) .with_context(|| format!("in function `{name}`"))?; Ok(core::Function { name, ty, body }) @@ -357,69 +482,63 @@ pub fn elaborate_program<'core>( elaborate_bodies(arena, program, &globals) } -/// Return the universe phase that `ty` inhabits, or `None` if it cannot be determined. +/// Return the universe phase that a Value type inhabits, or `None` if unknown. /// -/// This is the core analogue of the 2LTT kinding judgement: -/// - `IntTy(_, p)` inhabits `U(p)` -/// - `U(Meta)` (Type) inhabits `U(Meta)` (type-in-type for the meta universe) -/// - `U(Object)` (`VmType`) inhabits `U(Meta)` (the meta universe classifies object types) -/// - `Lift(_)` inhabits `U(Meta)` -/// - `Pi` inhabits `U(Meta)` (function types are meta-level) -/// - `Var(lvl)` — look up the variable's type in `locals`; if it is `U(p)`, it is a type in `p` -fn type_universe<'core>( - ty: &core::Term<'_>, - locals: &[(&'core str, &'core core::Term<'core>)], -) -> Option { +/// This is the `NbE` analogue of the 2LTT kinding judgement. +const fn value_type_universe(ty: &value::Value<'_>) -> Option { match ty { - core::Term::Prim(Prim::IntTy(IntType { phase, .. })) => Some(*phase), - core::Term::Prim(Prim::U(_)) | core::Term::Lift(_) | core::Term::Pi(_) => Some(Phase::Meta), - // A type variable: its universe is determined by what universe its type inhabits. - // E.g. if `A : Type` (= U(Meta)), then A is a meta-level type. - core::Term::Var(lvl) => match locals.get(lvl.0)?.1 { - core::Term::Prim(Prim::U(phase)) => Some(*phase), - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Pi(_) - | core::Term::Lam(_) - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => None, - }, - core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Lam(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => None, + value::Value::Prim(Prim::IntTy(IntType { phase, .. })) => Some(*phase), + value::Value::Prim(Prim::U(_)) + | value::Value::Lift(_) + | value::Value::Pi(_) + | value::Value::U(_) => Some(Phase::Meta), + // Neutral or unknown — can't determine phase + value::Value::Rigid(_) + | value::Value::Global(_) + | value::Value::App(_, _) + | value::Value::Prim(_) + | value::Value::Lit(..) + | value::Value::Lam(_) + | value::Value::Quote(_) => None, } } -/// Type equality: alpha-equality (ignores param names in Pi/Lam). -fn types_equal(a: &core::Term<'_>, b: &core::Term<'_>) -> bool { - alpha_eq(a, b) +/// Return the universe phase that a Value type inhabits, using context to look up +/// type variables. Returns `None` if phase is still indeterminate. +fn value_type_universe_ctx<'core>(ctx: &Ctx<'core, '_>, ty: &value::Value<'core>) -> Option { + match value_type_universe(ty) { + Some(p) => Some(p), + None => match ty { + // A rigid variable: look up its type in the context to determine phase. + value::Value::Rigid(lvl) => { + // lvl is the De Bruijn level; convert to index + let ix = lvl_to_ix(ctx.lvl, *lvl); + let i = ctx.types.len().checked_sub(1 + ix.0)?; + let var_ty = ctx.types.get(i)?; + // If the variable's type is U(phase), then it classifies types in phase. + match var_ty { + value::Value::Prim(Prim::U(p)) | value::Value::U(p) => Some(*p), + _ => None, + } + } + _ => None, + }, + } } -/// Count the total number of arguments already applied by nested `App` nodes. -/// -/// Used to determine which Pi binder level to target during dependent-return-type -/// substitution: global function signatures are elaborated in an empty context, so the -/// binder introduced by the i-th Pi in the chain sits at De Bruijn level i. -fn app_base_depth(term: &core::Term<'_>) -> usize { - match term { - core::Term::App(app) => app_base_depth(app.func) + app.args.len(), - _ => 0, - } +/// Type equality: compare via `NbE` (quote both, then alpha-eq). +fn types_equal_val( + arena: &bumpalo::Bump, + depth: Lvl, + a: &value::Value<'_>, + b: &value::Value<'_>, +) -> bool { + let ta = value::quote(arena, depth, a); + let tb = value::quote(arena, depth, b); + alpha_eq(ta, tb) } -/// Synthesise and return the elaborated core term; recover its type via `ctx.type_of`. +/// Synthesise and return the elaborated core term. pub fn infer<'src, 'core>( ctx: &mut Ctx<'core, '_>, phase: Phase, @@ -427,7 +546,7 @@ pub fn infer<'src, 'core>( ) -> Result<&'core core::Term<'core>> { match term { // ------------------------------------------------------------------ Var - // Look up the name in locals; return its level and type. + // Look up the name in locals; return its index and type. ast::Term::Var(name) => { let name_str = name.as_str(); // First check if it's a built-in type name — those are inferable too. @@ -443,8 +562,8 @@ pub fn infer<'src, 'core>( return Ok(term); } // Check locals. - if let Some((lvl, _)) = ctx.lookup_local(name_str) { - return Ok(ctx.alloc(core::Term::Var(lvl))); + if let Some((ix, _)) = ctx.lookup_local(name_str) { + return Ok(ctx.alloc(core::Term::Var(ix))); } // Check globals — bare reference without call, produces Global term. let core_name = core::Name::new(ctx.arena.alloc_str(name_str)); @@ -466,45 +585,45 @@ pub fn infer<'src, 'core>( func: ast::FunName::Term(func_term), args, } => { - // Elaborate the callee + // Elaborate the callee. let callee = infer(ctx, phase, func_term)?; - let callee_ty = ctx.type_of(callee); - // Callee type must be Pi; arity must match. - let pi = match callee_ty { - core::Term::Pi(pi) => pi, - _ => bail!("callee is not a function type"), - }; + // Get the callee's Pi type from the globals table (for globals) or from context. + // For globals, we use the raw Pi term directly (in empty context). + // For locals/other, we use val_type_of which gives the Value::Pi. + let (pi_phase, pi_param_count) = callee_pi_info(ctx, callee)?; - // For globals, verify phase matches (phase is now carried on the Pi itself). + // For globals, verify phase matches. if let core::Term::Global(gname) = callee { ensure!( - pi.phase == phase, - "function `{gname}` is a {}-phase function, but called in {phase}-phase context", - pi.phase + pi_phase == phase, + "function `{gname}` is a {pi_phase}-phase function, but called in {phase}-phase context", ); } + ensure!( - args.len() == pi.params.len(), - "wrong number of arguments: callee expects {}, got {}", - pi.params.len(), + args.len() == pi_param_count, + "wrong number of arguments: callee expects {pi_param_count}, got {}", args.len() ); - // Check each arg against its Pi param type. - // Global sigs are elaborated in an empty context, so param i is at De Bruijn level i. - // For dependent types, substitute earlier args into later param types. - let base = app_base_depth(callee); + // Get the starting Pi value for arg checking. + // For globals: evaluate the Pi term in empty env. + // For locals: use val_type_of (Value::Pi). + let mut pi_val = callee_pi_val(ctx, callee); let mut core_args: Vec<&'core core::Term<'core>> = Vec::with_capacity(args.len()); - for (i, (arg, &(_, mut param_ty))) in - args.iter().zip(pi.params.iter()).enumerate() - { - for (j, &earlier_arg) in core_args.iter().enumerate() { - param_ty = subst(ctx.arena, param_ty, Lvl(base + j), earlier_arg); - } - let core_arg = check(ctx, phase, arg, param_ty) + for (i, arg) in args.iter().enumerate() { + let vpi = match pi_val { + value::Value::Pi(vpi) => vpi, + _ => bail!("too many arguments at argument {i}"), + }; + // Check the arg against the domain type. + let core_arg = check_val(ctx, phase, arg, (*vpi.domain).clone()) .with_context(|| format!("in argument {i} of function call"))?; + let arg_val = ctx.eval(core_arg); core_args.push(core_arg); + // Advance Pi to the next type by applying closure to arg. + pi_val = value::inst(ctx.arena, ctx.globals, &vpi.closure, arg_val); } let args_slice = ctx.alloc_slice(core_args); @@ -532,24 +651,12 @@ pub fn infer<'src, 'core>( }; let core_arg0 = infer(ctx, phase, lhs)?; - let operand_ty = ctx.type_of(core_arg0); - let core_arg1 = check(ctx, phase, rhs, operand_ty)?; - let op_int_ty = match operand_ty { - core::Term::Prim(Prim::IntTy(it)) => *it, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Pi(_) - | core::Term::Lam(_) - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => { - bail!("comparison operands must be integers"); - } + let operand_ty_val = ctx.val_type_of(core_arg0); + let operand_ty_term = ctx.quote_val(&operand_ty_val); + let core_arg1 = check(ctx, phase, rhs, operand_ty_term)?; + let op_int_ty = match &operand_ty_val { + value::Value::Prim(Prim::IntTy(it)) => *it, + _ => bail!("comparison operands must be integers"), }; let prim = match op { BinOp::Eq => Prim::Eq(op_int_ty), @@ -592,7 +699,7 @@ pub fn infer<'src, 'core>( let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); let param_ty = infer(ctx, Phase::Meta, p.ty)?; ensure!( - type_universe(param_ty, &ctx.locals).is_some(), + value_type_universe_ctx(ctx, &ctx.eval(param_ty)).is_some(), "parameter type must be a type" ); elaborated_params.push((param_name, param_ty)); @@ -601,7 +708,7 @@ pub fn infer<'src, 'core>( let core_ret_ty = infer(ctx, Phase::Meta, ret_ty)?; ensure!( - type_universe(core_ret_ty, &ctx.locals).is_some(), + value_type_universe_ctx(ctx, &ctx.eval(core_ret_ty)).is_some(), "return type must be a type" ); @@ -610,7 +717,11 @@ pub fn infer<'src, 'core>( } assert_eq!(ctx.depth(), depth_before, "Pi elaboration leaked locals"); let params_slice = ctx.alloc_slice(elaborated_params); - Ok(ctx.alloc(core::Term::Pi(Pi { params: params_slice, body_ty: core_ret_ty, phase: Phase::Meta }))) + Ok(ctx.alloc(core::Term::Pi(Pi { + params: params_slice, + body_ty: core_ret_ty, + phase: Phase::Meta, + }))) } // ------------------------------------------------------------------ Lam (infer mode) @@ -638,7 +749,10 @@ pub fn infer<'src, 'core>( } assert_eq!(ctx.depth(), depth_before, "Lam elaboration leaked locals"); let params_slice = ctx.alloc_slice(elaborated_params); - Ok(ctx.alloc(core::Term::Lam(Lam { params: params_slice, body: core_body }))) + Ok(ctx.alloc(core::Term::Lam(Lam { + params: params_slice, + body: core_body, + }))) } // ------------------------------------------------------------------ Lift @@ -648,10 +762,12 @@ pub fn infer<'src, 'core>( "`[[...]]` is only valid in a meta-phase context" ); let core_inner = infer(ctx, Phase::Object, inner)?; - ensure!( - types_equal(ctx.type_of(core_inner), &core::Term::VM_TYPE), - "argument of `[[...]]` must be an object type" + let inner_ty_val = ctx.val_type_of(core_inner); + let is_vm_type = matches!( + &inner_ty_val, + value::Value::Prim(Prim::U(Phase::Object)) | value::Value::U(Phase::Object) ); + ensure!(is_vm_type, "argument of `[[...]]` must be an object type"); Ok(ctx.alloc(core::Term::Lift(core_inner))) } @@ -672,10 +788,10 @@ pub fn infer<'src, 'core>( "`$(...)` is only valid in an object-phase context" ); let core_inner = infer(ctx, Phase::Meta, inner)?; - let inner_ty = ctx.type_of(core_inner); - match inner_ty { - core::Term::Lift(_) => Ok(ctx.alloc(core::Term::Splice(core_inner))), - core::Term::Prim(Prim::IntTy(IntType { + let inner_ty_val = ctx.val_type_of(core_inner); + match &inner_ty_val { + value::Value::Lift(_) => Ok(ctx.alloc(core::Term::Splice(core_inner))), + value::Value::Prim(Prim::IntTy(IntType { width, phase: Phase::Meta, })) => { @@ -685,17 +801,7 @@ pub fn infer<'src, 'core>( )); Ok(ctx.alloc(core::Term::Splice(embedded))) } - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Pi(_) - | core::Term::Lam(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => Err(anyhow!( + _ => Err(anyhow!( "argument of `$(...)` must have a lifted type `[[T]]` or be a meta-level integer" )), } @@ -717,27 +823,74 @@ pub fn infer<'src, 'core>( } } +/// Return the Pi phase and parameter count for a callee. +/// +/// For a `Global`, reads the raw Pi term from the globals table (a closed term). +/// For any other callee, peels `Value::Pi` layers from `val_type_of`. +fn callee_pi_info(ctx: &Ctx<'_, '_>, callee: &core::Term<'_>) -> Result<(Phase, usize)> { + match callee { + core::Term::Global(name) => { + let pi_term = ctx + .globals + .get(name) + .copied() + .ok_or_else(|| anyhow!("unknown global `{name}`"))?; + match pi_term { + core::Term::Pi(pi) => Ok((pi.phase, pi.params.len())), + _ => bail!("global `{name}` is not a function"), + } + } + _ => { + let mut ty = ctx.val_type_of(callee); + let mut count = 0usize; + let mut phase_opt: Option = None; + while let value::Value::Pi(vpi) = ty { + if phase_opt.is_none() { + phase_opt = Some(vpi.phase); + } + count += 1; + // Advance with a fresh rigid to get the next Pi layer. + let fresh = value::Value::Rigid(Lvl(ctx.depth() + count - 1)); + ty = value::inst(ctx.arena, ctx.globals, &vpi.closure, fresh); + } + let phase = phase_opt.ok_or_else(|| anyhow!("callee is not a function type"))?; + Ok((phase, count)) + } + } +} + +/// Return the starting Pi `Value` for argument checking. +/// +/// For a `Global`, evaluates the closed Pi term in the current environment. +/// For any other callee, returns `val_type_of` directly (already a `Value::Pi`). +fn callee_pi_val<'core>( + ctx: &Ctx<'core, '_>, + callee: &'core core::Term<'core>, +) -> value::Value<'core> { + match callee { + core::Term::Global(name) => { + let pi_term = ctx + .globals + .get(name) + .copied() + .expect("callee_pi_val called with unknown global (invariant)"); + // Global Pi terms are closed (elaborated in empty context) — safe to eval in current env. + value::eval(ctx.arena, ctx.globals, &[], pi_term) + } + _ => ctx.val_type_of(callee), + } +} + /// Check exhaustiveness of `arms` given the scrutinee type `scrut_ty`. -fn check_exhaustiveness(scrut_ty: &core::Term<'_>, arms: &[ast::MatchArm<'_>]) -> Result<()> { +fn check_exhaustiveness(scrut_ty: &value::Value<'_>, arms: &[ast::MatchArm<'_>]) -> Result<()> { let mut covered_lits: Option> = match scrut_ty { - core::Term::Prim(Prim::IntTy(ty)) => match ty.width { + value::Value::Prim(Prim::IntTy(ty)) => match ty.width { IntWidth::U0 => Some(vec![false; 1]), IntWidth::U1 => Some(vec![false; 2]), IntWidth::U8 => Some(vec![false; 256]), IntWidth::U16 | IntWidth::U32 | IntWidth::U64 => None, }, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Pi(_) - | core::Term::Lam(_) - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => None, + _ => None, }; let mut has_catch_all = false; @@ -795,27 +948,34 @@ where G: FnOnce(&T) -> &'core core::Term<'core>, W: FnOnce(&'core core::Term<'core>, T) -> T, { - let (core_expr, bind_ty) = if let Some(ann) = stmt.ty { + let (core_expr, bind_ty_val) = if let Some(ann) = stmt.ty { let ty = infer(ctx, phase, ann)?; - let core_e = check(ctx, phase, stmt.expr, ty) + let ty_val = ctx.eval(ty); + let core_e = check_val(ctx, phase, stmt.expr, ty_val.clone()) .with_context(|| format!("in let binding `{}`", stmt.name.as_str()))?; - (core_e, ty) + (core_e, ty_val) } else { let core_e = infer(ctx, phase, stmt.expr) .with_context(|| format!("in let binding `{}`", stmt.name.as_str()))?; - let bind_ty = ctx.type_of(core_e); + let bind_ty = ctx.val_type_of(core_e); (core_e, bind_ty) }; + let bind_ty_term = ctx.quote_val(&bind_ty_val); + // Evaluate the bound expression so dependent references to this binding work correctly. + let expr_val = ctx.eval(core_expr); let bind_name: &'core str = ctx.arena.alloc_str(stmt.name.as_str()); - ctx.push_local(bind_name, bind_ty); + ctx.push_let_binding(bind_name, bind_ty_val, expr_val); let cont_result = cont(ctx); ctx.pop_local(); let cont_result = cont_result?; let core_body = body_of(&cont_result); let let_term = ctx.alloc(core::Term::new_let( - bind_name, bind_ty, core_expr, core_body, + bind_name, + bind_ty_term, + core_expr, + core_body, )); Ok(wrap(let_term, cont_result)) } @@ -841,35 +1001,48 @@ fn infer_block<'src, 'core>( } /// Elaborate a sequence of `let` bindings followed by a trailing expression (check mode). -fn check_block<'src, 'core>( +fn check_block_val<'src, 'core>( ctx: &mut Ctx<'core, '_>, phase: Phase, stmts: &'src [ast::Let<'src>], expr: &'src ast::Term<'src>, - expected: &'core core::Term<'core>, + expected: value::Value<'core>, ) -> Result<&'core core::Term<'core>> { match stmts { - [] => check(ctx, phase, expr, expected), + [] => check_val(ctx, phase, expr, expected), [first, rest @ ..] => elaborate_let( ctx, phase, first, - |ctx| check_block(ctx, phase, rest, expr, expected), + |ctx| check_block_val(ctx, phase, rest, expr, expected.clone()), |body| body, |let_term, _body| let_term, ), } } -/// Check `term` against `expected`, returning the elaborated core term. +/// Check `term` against `expected` (as a term reference), returning the elaborated core term. +/// +/// This is a convenience wrapper for callers that have an expected type as a `&Term`. pub fn check<'src, 'core>( ctx: &mut Ctx<'core, '_>, phase: Phase, term: &'src ast::Term<'src>, expected: &'core core::Term<'core>, +) -> Result<&'core core::Term<'core>> { + let expected_val = ctx.eval(expected); + check_val(ctx, phase, term, expected_val) +} + +/// Check `term` against `expected` (as a semantic Value), returning the elaborated core term. +pub fn check_val<'src, 'core>( + ctx: &mut Ctx<'core, '_>, + phase: Phase, + term: &'src ast::Term<'src>, + expected: value::Value<'core>, ) -> Result<&'core core::Term<'core>> { // Verify `expected` inhabits the correct universe for the current phase. - let ty_phase = type_universe(expected, &ctx.locals) + let ty_phase = value_type_universe_ctx(ctx, &expected) .expect("expected type passed to `check` is not a well-formed type expression"); ensure!( ty_phase == phase, @@ -878,8 +1051,8 @@ pub fn check<'src, 'core>( ); match term { // ------------------------------------------------------------------ Lit - ast::Term::Lit(n) => match expected { - core::Term::Prim(Prim::IntTy(it)) => { + ast::Term::Lit(n) => match &expected { + value::Value::Prim(Prim::IntTy(it)) => { let width = it.width; ensure!( *n <= width.max_value(), @@ -887,18 +1060,7 @@ pub fn check<'src, 'core>( ); Ok(ctx.alloc(core::Term::Lit(*n, *it))) } - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Pi(_) - | core::Term::Lam(_) - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => Err(anyhow!("literal `{n}` cannot have a non-integer type")), + _ => Err(anyhow!("literal `{n}` cannot have a non-integer type")), }, // ------------------------------------------------------------------ App { Prim (BinOp) } @@ -916,22 +1078,9 @@ pub fn check<'src, 'core>( | ast::BinOp::Ge ) => { - let int_ty = match expected { - core::Term::Prim(Prim::IntTy(it)) => *it, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Pi(_) - | core::Term::Lam(_) - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => { - bail!("primitive operation requires an integer type") - } + let int_ty = match &expected { + value::Value::Prim(Prim::IntTy(it)) => *it, + _ => bail!("primitive operation requires an integer type"), }; use ast::BinOp; @@ -951,8 +1100,9 @@ pub fn check<'src, 'core>( bail!("binary operation expects exactly 2 arguments") }; - let core_arg0 = check(ctx, phase, lhs, expected)?; - let core_arg1 = check(ctx, phase, rhs, expected)?; + let expected_term = ctx.quote_val(&expected); + let core_arg0 = check(ctx, phase, lhs, expected_term)?; + let core_arg1 = check(ctx, phase, rhs, expected_term)?; let core_args = ctx.alloc_slice([core_arg0, core_arg1]); Ok(ctx.alloc(core::Term::new_app( @@ -966,22 +1116,9 @@ pub fn check<'src, 'core>( func: ast::FunName::UnOp(op), args, } => { - let int_ty = match expected { - core::Term::Prim(Prim::IntTy(it)) => *it, - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Pi(_) - | core::Term::Lam(_) - | core::Term::Lift(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => { - bail!("primitive operation requires an integer type") - } + let int_ty = match &expected { + value::Value::Prim(Prim::IntTy(it)) => *it, + _ => bail!("primitive operation requires an integer type"), }; let prim = match op { @@ -991,7 +1128,8 @@ pub fn check<'src, 'core>( let [arg] = args else { bail!("unary operation expects exactly 1 argument") }; - let core_arg = check(ctx, phase, arg, expected)?; + let expected_term = ctx.quote_val(&expected); + let core_arg = check(ctx, phase, arg, expected_term)?; let core_args = std::slice::from_ref(ctx.arena.alloc(core_arg)); Ok(ctx.alloc(core::Term::new_app( ctx.alloc(core::Term::Prim(prim)), @@ -1000,24 +1138,13 @@ pub fn check<'src, 'core>( } // ------------------------------------------------------------------ Quote (check mode) - ast::Term::Quote(inner) => match expected { - core::Term::Lift(obj_ty) => { - let core_inner = check(ctx, Phase::Object, inner, obj_ty)?; + ast::Term::Quote(inner) => match &expected { + value::Value::Lift(obj_ty) => { + let obj_ty_term = value::quote(ctx.arena, ctx.lvl, obj_ty); + let core_inner = check(ctx, Phase::Object, inner, obj_ty_term)?; Ok(ctx.alloc(core::Term::Quote(core_inner))) } - core::Term::Var(_) - | core::Term::Prim(_) - | core::Term::Lit(..) - | core::Term::Global(_) - | core::Term::App(_) - | core::Term::Pi(_) - | core::Term::Lam(_) - | core::Term::Quote(_) - | core::Term::Splice(_) - | core::Term::Let(_) - | core::Term::Match(_) => { - Err(anyhow!("quote `#(...)` must have a lifted type `[[T]]`")) - } + _ => Err(anyhow!("quote `#(...)` must have a lifted type `[[T]]`")), }, // ------------------------------------------------------------------ Splice (check mode) @@ -1026,24 +1153,27 @@ pub fn check<'src, 'core>( phase == Phase::Object, "`$(...)` is only valid in an object-phase context" ); - if let core::Term::Prim(Prim::IntTy(IntType { + if let value::Value::Prim(Prim::IntTy(IntType { width, phase: Phase::Object, - })) = expected + })) = &expected { - let lift_ty = ctx.alloc(core::Term::Lift(expected)); + let width = *width; + let expected_term = ctx.quote_val(&expected); + let lift_ty = ctx.alloc(core::Term::Lift(expected_term)); if let Ok(core_inner) = check(ctx, Phase::Meta, inner, lift_ty) { return Ok(ctx.alloc(core::Term::Splice(core_inner))); } - let meta_int_ty = ctx.alloc(core::Term::Prim(Prim::IntTy(IntType::meta(*width)))); + let meta_int_ty = ctx.alloc(core::Term::Prim(Prim::IntTy(IntType::meta(width)))); let core_inner = check(ctx, Phase::Meta, inner, meta_int_ty)?; let embedded = ctx.alloc(core::Term::new_app( - ctx.alloc(core::Term::Prim(Prim::Embed(*width))), + ctx.alloc(core::Term::Prim(Prim::Embed(width))), ctx.arena.alloc_slice_fill_iter([core_inner]), )); return Ok(ctx.alloc(core::Term::Splice(embedded))); } - let lift_ty = ctx.alloc(core::Term::Lift(expected)); + let expected_term = ctx.quote_val(&expected); + let lift_ty = ctx.alloc(core::Term::Lift(expected_term)); let core_inner = check(ctx, Phase::Meta, inner, lift_ty)?; Ok(ctx.alloc(core::Term::Splice(core_inner))) } @@ -1059,56 +1189,69 @@ pub fn check<'src, 'core>( let depth_before = ctx.depth(); // Expected type must be a Pi with matching arity. - let pi = match expected { - core::Term::Pi(pi) => pi, - _ => bail!("expected a function type for this lambda"), - }; + // We need to peel Pi layers to get all params. + // Collect all Pi params from the expected value. + let mut pi_params: Vec<(&str, value::Value<'core>)> = Vec::new(); + let mut cur_pi = expected.clone(); + while let value::Value::Pi(vpi) = cur_pi { + pi_params.push((vpi.name, (*vpi.domain).clone())); + // Advance with a fresh variable for the closure + let fresh = value::Value::Rigid(Lvl(ctx.depth() + pi_params.len() - 1)); + cur_pi = value::inst(ctx.arena, ctx.globals, &vpi.closure, fresh); + } + let body_ty_val = cur_pi; + ensure!( - params.len() == pi.params.len(), + params.len() == pi_params.len(), "lambda has {} parameter(s) but expected type has {}", params.len(), - pi.params.len() + pi_params.len() ); let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); - for (p, &(_, pi_param_ty)) in params.iter().zip(pi.params.iter()) { + for (p, (_, pi_param_ty)) in params.iter().zip(pi_params.into_iter()) { let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); let annotated_ty = infer(ctx, Phase::Meta, p.ty)?; + let annotated_ty_val = ctx.eval(annotated_ty); ensure!( - types_equal(annotated_ty, pi_param_ty), + types_equal_val(ctx.arena, ctx.lvl, &annotated_ty_val, &pi_param_ty), "lambda parameter type mismatch: annotation gives a different type \ than the expected function type" ); - elaborated_params.push((param_name, pi_param_ty)); - ctx.push_local(param_name, pi_param_ty); + elaborated_params.push((param_name, annotated_ty)); + ctx.push_local_val(param_name, pi_param_ty); } - let core_body = check(ctx, phase, body, pi.body_ty)?; + let core_body = check_val(ctx, phase, body, body_ty_val)?; for _ in &elaborated_params { ctx.pop_local(); } assert_eq!(ctx.depth(), depth_before, "Lam check leaked locals"); let params_slice = ctx.alloc_slice(elaborated_params); - Ok(ctx.alloc(core::Term::Lam(Lam { params: params_slice, body: core_body }))) + Ok(ctx.alloc(core::Term::Lam(Lam { + params: params_slice, + body: core_body, + }))) } // ------------------------------------------------------------------ Match (check mode) ast::Term::Match { scrutinee, arms } => { let core_scrutinee = infer(ctx, phase, scrutinee)?; - let scrut_ty = ctx.type_of(core_scrutinee); + let scrut_ty_val = ctx.val_type_of(core_scrutinee); - check_exhaustiveness(scrut_ty, arms)?; + check_exhaustiveness(&scrut_ty_val, arms)?; + let scrut_ty_term = ctx.quote_val(&scrut_ty_val); let core_arms: &'core [core::Arm<'core>] = ctx.arena .alloc_slice_try_fill_iter(arms.iter().map(|arm| -> Result<_> { let core_pat = elaborate_pat(ctx, &arm.pat); if let Some(bname) = core_pat.bound_name() { - ctx.push_local(bname, scrut_ty); + ctx.push_local(bname, scrut_ty_term); } - let arm_result = check(ctx, phase, arm.body, expected); + let arm_result = check_val(ctx, phase, arm.body, expected.clone()); if core_pat.bound_name().is_some() { ctx.pop_local(); @@ -1127,7 +1270,7 @@ pub fn check<'src, 'core>( // ------------------------------------------------------------------ Block (check mode) ast::Term::Block { stmts, expr } => { let depth_before = ctx.depth(); - let result = check_block(ctx, phase, stmts, expr, expected); + let result = check_block_val(ctx, phase, stmts, expr, expected); assert_eq!(ctx.depth(), depth_before, "check_block leaked locals"); result } @@ -1135,8 +1278,9 @@ pub fn check<'src, 'core>( // ------------------------------------------------------------------ fallthrough: infer then unify ast::Term::Var(_) | ast::Term::App { .. } | ast::Term::Lift(_) | ast::Term::Pi { .. } => { let core_term = infer(ctx, phase, term)?; + let inferred_val = ctx.val_type_of(core_term); ensure!( - types_equal(ctx.type_of(core_term), expected), + types_equal_val(ctx.arena, ctx.lvl, &inferred_val, &expected), "type mismatch" ); Ok(core_term) diff --git a/compiler/src/checker/test/apply.rs b/compiler/src/checker/test/apply.rs index 50dcadb..03d2175 100644 --- a/compiler/src/checker/test/apply.rs +++ b/compiler/src/checker/test/apply.rs @@ -67,7 +67,11 @@ fn infer_global_call_phase_mismatch_fails() { // `code fn f() -> u64` — object-phase function let u64_obj = core_arena.alloc(core::Term::Prim(Prim::IntTy(IntType::U64_OBJ))); let mut globals = HashMap::new(); - let f_ty: &core::Term = core_arena.alloc(core::Term::Pi(Pi { params: &[], body_ty: u64_obj, phase: Phase::Object })); + let f_ty: &core::Term = core_arena.alloc(core::Term::Pi(Pi { + params: &[], + body_ty: u64_obj, + phase: Phase::Object, + })); globals.insert(Name::new("f"), f_ty); let mut ctx = test_ctx_with_globals(&core_arena, &globals); diff --git a/compiler/src/checker/test/context.rs b/compiler/src/checker/test/context.rs index 49acf6a..0571a05 100644 --- a/compiler/src/checker/test/context.rs +++ b/compiler/src/checker/test/context.rs @@ -25,7 +25,7 @@ fn literal_checks_against_int_type() { fn variable_lookup_in_empty_context() { let arena = bumpalo::Bump::new(); let ctx = test_ctx(&arena); - assert_eq!(ctx.lookup_local("x"), None); + assert!(ctx.lookup_local("x").is_none()); } #[test] @@ -35,11 +35,11 @@ fn variable_lookup_after_push() { let u64_term = &core::Term::U64_META; ctx.push_local("x", u64_term); - let (lvl, ty) = ctx.lookup_local("x").expect("x should be in scope"); - assert_eq!(lvl, Lvl(0)); + let (ix, ty) = ctx.lookup_local("x").expect("x should be in scope"); + assert_eq!(ix, Ix(0)); assert!(matches!( ty, - core::Term::Prim(Prim::IntTy(IntType { + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U64, .. })) @@ -56,21 +56,22 @@ fn variable_lookup_with_multiple_locals() { ctx.push_local("x", u64_term); ctx.push_local("y", u32_term); - let (lvl_y, ty_y) = ctx.lookup_local("y").expect("y should be in scope"); - assert_eq!(lvl_y, Lvl(1)); + // With two locals, "y" is innermost (index 0), "x" is outer (index 1). + let (ix_y, ty_y) = ctx.lookup_local("y").expect("y should be in scope"); + assert_eq!(ix_y, Ix(0)); assert!(matches!( ty_y, - core::Term::Prim(Prim::IntTy(IntType { + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U32, .. })) )); - let (lvl_x, ty_x) = ctx.lookup_local("x").expect("x should be in scope"); - assert_eq!(lvl_x, Lvl(0)); + let (ix_x, ty_x) = ctx.lookup_local("x").expect("x should be in scope"); + assert_eq!(ix_x, Ix(1)); assert!(matches!( ty_x, - core::Term::Prim(Prim::IntTy(IntType { + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U64, .. })) @@ -87,11 +88,12 @@ fn variable_shadowing() { ctx.push_local("x", u64_term); ctx.push_local("x", u32_term); - let (lvl, ty) = ctx.lookup_local("x").expect("x should be in scope"); - assert_eq!(lvl, Lvl(1)); + // Innermost "x" shadows outer; it is at index 0. + let (ix, ty) = ctx.lookup_local("x").expect("x should be in scope"); + assert_eq!(ix, Ix(0)); assert!(matches!( ty, - core::Term::Prim(Prim::IntTy(IntType { + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U32, .. })) @@ -120,8 +122,8 @@ fn meta_variable_in_quote_is_ok() { let u64_term = &core::Term::U64_META; let lifted_u64 = ctx.lift_ty(u64_term); ctx.push_local("x", lifted_u64); - let x_var = arena.alloc(core::Term::Var(Lvl(0))); - assert!(matches!(x_var, core::Term::Var(Lvl(0)))); + let x_var = arena.alloc(core::Term::Var(Ix(0))); + assert!(matches!(x_var, core::Term::Var(Ix(0)))); } #[test] @@ -210,7 +212,7 @@ fn splice_inference_mirrors_inner() { let u64_term = &core::Term::U64_META; let lifted_u64 = ctx.lift_ty(u64_term); ctx.push_local("x", lifted_u64); - let x_var = arena.alloc(core::Term::Var(Lvl(0))); + let x_var = arena.alloc(core::Term::Var(Ix(0))); let spliced = arena.alloc(core::Term::Splice(x_var)); assert!(matches!(spliced, core::Term::Splice(_))); } @@ -220,7 +222,7 @@ fn let_binding_structure() { let arena = bumpalo::Bump::new(); let u64_term = &core::Term::U64_META; let expr = arena.alloc(core::Term::Lit(42, IntType::U64_META)); - let body = arena.alloc(core::Term::Var(Lvl(0))); + let body = arena.alloc(core::Term::Var(Ix(0))); let let_term = arena.alloc(core::Term::new_let("x", u64_term, expr, body)); assert!(matches!(let_term, core::Term::Let(_))); } @@ -228,7 +230,7 @@ fn let_binding_structure() { #[test] fn match_with_literal_pattern() { let arena = bumpalo::Bump::new(); - let scrutinee = arena.alloc(core::Term::Var(Lvl(0))); + let scrutinee = arena.alloc(core::Term::Var(Ix(0))); let body0 = arena.alloc(core::Term::Lit(0, IntType::U64_META)); let body1 = arena.alloc(core::Term::Lit(1, IntType::U64_META)); @@ -250,8 +252,8 @@ fn match_with_literal_pattern() { #[test] fn match_with_binding_pattern() { let arena = bumpalo::Bump::new(); - let scrutinee = arena.alloc(core::Term::Var(Lvl(0))); - let body = arena.alloc(core::Term::Var(Lvl(0))); + let scrutinee = arena.alloc(core::Term::Var(Ix(0))); + let body = arena.alloc(core::Term::Var(Ix(0))); let arm = core::Arm { pat: Pat::Bind("n"), diff --git a/compiler/src/checker/test/helpers.rs b/compiler/src/checker/test/helpers.rs index 0bff9e8..ca0c27d 100644 --- a/compiler/src/checker/test/helpers.rs +++ b/compiler/src/checker/test/helpers.rs @@ -30,7 +30,7 @@ pub fn sig_no_params_returns_u64(arena: &bumpalo::Bump) -> &core::Term<'_> { } /// Helper: build a Pi term for `fn f(x: u32) -> u64`. -pub fn sig_one_param_returns_u64<'a>(arena: &'a bumpalo::Bump) -> &'a core::Term<'a> { +pub fn sig_one_param_returns_u64(arena: &bumpalo::Bump) -> &core::Term<'_> { let params = arena.alloc_slice_fill_iter([("x", &core::Term::U32_META as &core::Term)]); arena.alloc(core::Term::Pi(Pi { params, diff --git a/compiler/src/checker/test/matching.rs b/compiler/src/checker/test/matching.rs index 879b913..2994c94 100644 --- a/compiler/src/checker/test/matching.rs +++ b/compiler/src/checker/test/matching.rs @@ -10,9 +10,14 @@ fn check_match_all_arms_same_type_succeeds() { let u32_ty_core = &core::Term::U32_META; let mut globals = HashMap::new(); - globals.insert(Name::new("k32"), core_arena.alloc(core::Term::Pi(Pi { - params: &[], body_ty: u32_ty_core, phase: Phase::Meta, - })) as &_); + globals.insert( + Name::new("k32"), + core_arena.alloc(core::Term::Pi(Pi { + params: &[], + body_ty: u32_ty_core, + phase: Phase::Meta, + })) as &_, + ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; ctx.push_local("x", u32_ty); @@ -49,9 +54,14 @@ fn check_match_u1_fully_covered_succeeds() { let u1_ty_core = &core::Term::U1_META; let mut globals = HashMap::new(); - globals.insert(Name::new("k1"), core_arena.alloc(core::Term::Pi(Pi { - params: &[], body_ty: u1_ty_core, phase: Phase::Meta, - })) as &_); + globals.insert( + Name::new("k1"), + core_arena.alloc(core::Term::Pi(Pi { + params: &[], + body_ty: u1_ty_core, + phase: Phase::Meta, + })) as &_, + ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); ctx.push_local("x", u1_ty_core); @@ -88,9 +98,14 @@ fn infer_match_u1_partially_covered_fails() { let u1_ty_core = &core::Term::U1_META; let mut globals = HashMap::new(); - globals.insert(Name::new("k1"), core_arena.alloc(core::Term::Pi(Pi { - params: &[], body_ty: u1_ty_core, phase: Phase::Meta, - })) as &_); + globals.insert( + Name::new("k1"), + core_arena.alloc(core::Term::Pi(Pi { + params: &[], + body_ty: u1_ty_core, + phase: Phase::Meta, + })) as &_, + ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); ctx.push_local("x", u1_ty_core); @@ -117,9 +132,14 @@ fn infer_match_no_catch_all_fails() { let u32_ty_core = &core::Term::U32_META; let mut globals = HashMap::new(); - globals.insert(Name::new("k32"), core_arena.alloc(core::Term::Pi(Pi { - params: &[], body_ty: u32_ty_core, phase: Phase::Meta, - })) as &_); + globals.insert( + Name::new("k32"), + core_arena.alloc(core::Term::Pi(Pi { + params: &[], + body_ty: u32_ty_core, + phase: Phase::Meta, + })) as &_, + ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; ctx.push_local("x", u32_ty); @@ -158,12 +178,22 @@ fn infer_match_arms_type_mismatch_fails() { let u32_ty_core = &core::Term::U32_META; let u64_ty_core = &core::Term::U64_META; let mut globals = HashMap::new(); - globals.insert(Name::new("k32"), core_arena.alloc(core::Term::Pi(Pi { - params: &[], body_ty: u32_ty_core, phase: Phase::Meta, - })) as &_); - globals.insert(Name::new("k64"), core_arena.alloc(core::Term::Pi(Pi { - params: &[], body_ty: u64_ty_core, phase: Phase::Meta, - })) as &_); + globals.insert( + Name::new("k32"), + core_arena.alloc(core::Term::Pi(Pi { + params: &[], + body_ty: u32_ty_core, + phase: Phase::Meta, + })) as &_, + ); + globals.insert( + Name::new("k64"), + core_arena.alloc(core::Term::Pi(Pi { + params: &[], + body_ty: u64_ty_core, + phase: Phase::Meta, + })) as &_, + ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; ctx.push_local("x", u32_ty); diff --git a/compiler/src/checker/test/meta.rs b/compiler/src/checker/test/meta.rs index 4018ab7..4d932fb 100644 --- a/compiler/src/checker/test/meta.rs +++ b/compiler/src/checker/test/meta.rs @@ -65,7 +65,11 @@ fn infer_quote_of_global_call_returns_lifted_type() { let mut globals = HashMap::new(); globals.insert( Name::new("f"), - core_arena.alloc(core::Term::Pi(Pi { params: &[], body_ty: u64_ty_core, phase: Phase::Object })) as &_, + core_arena.alloc(core::Term::Pi(Pi { + params: &[], + body_ty: u64_ty_core, + phase: Phase::Object, + })) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); @@ -94,7 +98,11 @@ fn infer_quote_at_object_phase_fails() { let mut globals = HashMap::new(); globals.insert( Name::new("f"), - core_arena.alloc(core::Term::Pi(Pi { params: &[], body_ty: u64_ty_core, phase: Phase::Object })) as &_, + core_arena.alloc(core::Term::Pi(Pi { + params: &[], + body_ty: u64_ty_core, + phase: Phase::Object, + })) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); diff --git a/compiler/src/checker/test/mod.rs b/compiler/src/checker/test/mod.rs index 21dee3e..0b8c0ff 100644 --- a/compiler/src/checker/test/mod.rs +++ b/compiler/src/checker/test/mod.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use super::*; -use crate::core::{self, IntType, IntWidth, Name, Pat, Pi, Prim}; +use crate::core::{self, IntType, IntWidth, Ix, Name, Pat, Pi, Prim, value}; use crate::parser::ast::{self, BinOp, FunName, MatchArm, Phase}; mod helpers; diff --git a/compiler/src/checker/test/signatures.rs b/compiler/src/checker/test/signatures.rs index ae5169a..f345103 100644 --- a/compiler/src/checker/test/signatures.rs +++ b/compiler/src/checker/test/signatures.rs @@ -50,8 +50,12 @@ fn collect_signatures_two_functions() { assert_eq!(globals.len(), 2); - let id_ty = globals.get(&Name::new("id")).expect("id should be in globals"); - let core::Term::Pi(id_pi) = id_ty else { panic!("expected Pi") }; + let id_ty = globals + .get(&Name::new("id")) + .expect("id should be in globals"); + let core::Term::Pi(id_pi) = id_ty else { + panic!("expected Pi") + }; assert_eq!(id_pi.phase, Phase::Meta); assert_eq!(id_pi.params.len(), 1); assert_eq!(id_pi.params[0].0, "x"); @@ -70,8 +74,12 @@ fn collect_signatures_two_functions() { })) )); - let add_ty = globals.get(&Name::new("add_one")).expect("add_one should be in globals"); - let core::Term::Pi(add_pi) = add_ty else { panic!("expected Pi") }; + let add_ty = globals + .get(&Name::new("add_one")) + .expect("add_one should be in globals"); + let core::Term::Pi(add_pi) = add_ty else { + panic!("expected Pi") + }; assert_eq!(add_pi.phase, Phase::Object); assert_eq!(add_pi.params.len(), 1); assert_eq!(add_pi.params[0].0, "y"); diff --git a/compiler/src/checker/test/var.rs b/compiler/src/checker/test/var.rs index 7a6620c..e648d4c 100644 --- a/compiler/src/checker/test/var.rs +++ b/compiler/src/checker/test/var.rs @@ -14,7 +14,8 @@ fn infer_var_in_scope_returns_its_type() { let term = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let core_term = infer(&mut ctx, Phase::Meta, term).expect("should infer"); let ty = ctx.type_of(core_term); - assert!(matches!(core_term, core::Term::Var(Lvl(0)))); + // With one local "x", infer returns Var(Ix(0)) — innermost (only) binder. + assert!(matches!(core_term, core::Term::Var(Ix(0)))); assert!(matches!( ty, core::Term::Prim(Prim::IntTy(IntType { @@ -35,20 +36,21 @@ fn infer_var_out_of_scope_fails() { assert!(infer(&mut ctx, Phase::Meta, term).is_err()); } -// With two locals the correct De Bruijn level is returned. +// With two locals the correct De Bruijn index is returned. #[test] -fn infer_var_returns_correct_level() { +fn infer_var_returns_correct_index() { let src_arena = bumpalo::Bump::new(); let core_arena = bumpalo::Bump::new(); let mut ctx = test_ctx(&core_arena); let u64_ty = &core::Term::U64_META; let u32_ty = &core::Term::U32_META; - ctx.push_local("x", u64_ty); // level 0 - ctx.push_local("y", u32_ty); // level 1 + ctx.push_local("x", u64_ty); // outer: index 1 + ctx.push_local("y", u32_ty); // inner: index 0 let term = src_arena.alloc(ast::Term::Var(ast::Name::new("y"))); let core_term = infer(&mut ctx, Phase::Meta, term).expect("should infer"); - assert!(matches!(core_term, core::Term::Var(Lvl(1)))); + // "y" is innermost, so Ix(0). + assert!(matches!(core_term, core::Term::Var(Ix(0)))); } // Shadowing: the innermost binding wins. @@ -59,13 +61,14 @@ fn infer_var_shadowed_returns_innermost() { let mut ctx = test_ctx(&core_arena); let u64_ty = &core::Term::U64_META; let u32_ty = &core::Term::U32_META; - ctx.push_local("x", u64_ty); // level 0, u64 - ctx.push_local("x", u32_ty); // level 1, u32 — shadows + ctx.push_local("x", u64_ty); // outer x: u64, index 1 + ctx.push_local("x", u32_ty); // inner x: u32 — shadows, index 0 let term = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let core_term = infer(&mut ctx, Phase::Meta, term).expect("should infer"); let ty = ctx.type_of(core_term); - assert!(matches!(core_term, core::Term::Var(Lvl(1)))); + // Innermost "x" is at Ix(0). + assert!(matches!(core_term, core::Term::Var(Ix(0)))); assert!(matches!( ty, core::Term::Prim(Prim::IntTy(IntType { diff --git a/compiler/src/core/alpha_eq.rs b/compiler/src/core/alpha_eq.rs index 7cff37a..5bf3fb6 100644 --- a/compiler/src/core/alpha_eq.rs +++ b/compiler/src/core/alpha_eq.rs @@ -23,12 +23,20 @@ pub fn alpha_eq(a: &Term<'_>, b: &Term<'_>) -> bool { (Term::Pi(p1), Term::Pi(p2)) => { p1.phase == p2.phase && p1.params.len() == p2.params.len() - && p1.params.iter().zip(p2.params.iter()).all(|((_, t1), (_, t2))| alpha_eq(t1, t2)) + && p1 + .params + .iter() + .zip(p2.params.iter()) + .all(|((_, t1), (_, t2))| alpha_eq(t1, t2)) && alpha_eq(p1.body_ty, p2.body_ty) } (Term::Lam(l1), Term::Lam(l2)) => { l1.params.len() == l2.params.len() - && l1.params.iter().zip(l2.params.iter()).all(|((_, t1), (_, t2))| alpha_eq(t1, t2)) + && l1 + .params + .iter() + .zip(l2.params.iter()) + .all(|((_, t1), (_, t2))| alpha_eq(t1, t2)) && alpha_eq(l1.body, l2.body) } (Term::Lift(i1), Term::Lift(i2)) diff --git a/compiler/src/core/mod.rs b/compiler/src/core/mod.rs index 8450440..61653ec 100644 --- a/compiler/src/core/mod.rs +++ b/compiler/src/core/mod.rs @@ -1,14 +1,13 @@ pub mod pretty; mod prim; -mod subst; +pub mod value; pub mod alpha_eq; pub use crate::common::{Name, Phase}; pub use alpha_eq::alpha_eq; pub use prim::{IntType, IntWidth, Prim}; -pub use subst::subst; -/// De Bruijn level (counts from the outermost binder) +/// De Bruijn level (counts from the outermost binder, 0 = outermost) #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Lvl(pub usize); @@ -23,6 +22,31 @@ impl Lvl { } } +/// De Bruijn index (counts from nearest enclosing binder, 0 = innermost) +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct Ix(pub usize); + +impl Ix { + pub const fn new(n: usize) -> Self { + Self(n) + } + + #[must_use] + pub const fn succ(self) -> Self { + Self(self.0 + 1) + } +} + +/// Convert a De Bruijn level to an index given the current depth. +pub const fn lvl_to_ix(depth: Lvl, lvl: Lvl) -> Ix { + Ix(depth.0 - lvl.0 - 1) +} + +/// Convert a De Bruijn index to a level given the current depth. +pub const fn ix_to_lvl(depth: Lvl, ix: Ix) -> Lvl { + Lvl(depth.0 - ix.0 - 1) +} + /// Match pattern in the core IR #[derive(Debug, Clone, PartialEq, Eq)] pub enum Pat<'a> { @@ -128,8 +152,8 @@ pub struct Match<'a> { /// Core term / type (terms and types are unified) #[derive(Debug, PartialEq, Eq)] pub enum Term<'a> { - /// Local variable, identified by De Bruijn level - Var(Lvl), + /// Local variable, identified by De Bruijn index (0 = innermost binder) + Var(Ix), /// Built-in type or operation (not applied) Prim(Prim), /// Numeric literal with its integer type diff --git a/compiler/src/core/pretty.rs b/compiler/src/core/pretty.rs index 8d5a57d..a9ba444 100644 --- a/compiler/src/core/pretty.rs +++ b/compiler/src/core/pretty.rs @@ -55,9 +55,15 @@ impl<'a> Term<'a> { ) -> fmt::Result { match self { // ── Variable ───────────────────────────────────────────────────────── - Term::Var(lvl) => { - let name = *env.get(lvl.0).expect("De Bruijn level in env bounds"); - write!(f, "{name}@{}", lvl.0) + Term::Var(ix) => { + let i = env + .len() + .checked_sub(1 + ix.0) + .expect("De Bruijn index out of environment bounds"); + let name = env + .get(i) + .expect("De Bruijn index out of environment bounds"); + write!(f, "{name}@{i}") } // ── Literal ────────────────────────────────────────────────────────── @@ -87,7 +93,9 @@ impl<'a> Term<'a> { let env_before = env.len(); write!(f, "fn(")?; for (i, &(name, ty)) in pi.params.iter().enumerate() { - if i > 0 { write!(f, ", ")?; } + if i > 0 { + write!(f, ", ")?; + } if name == "_" { write!(f, "_: ")?; } else { @@ -107,7 +115,9 @@ impl<'a> Term<'a> { let env_before = env.len(); write!(f, "|")?; for (i, &(name, ty)) in lam.params.iter().enumerate() { - if i > 0 { write!(f, ", ")?; } + if i > 0 { + write!(f, ", ")?; + } write!(f, "{}@{}: ", name, env.len())?; ty.fmt_expr(env, indent, f)?; env.push(name); diff --git a/compiler/src/core/subst.rs b/compiler/src/core/subst.rs deleted file mode 100644 index 38ca520..0000000 --- a/compiler/src/core/subst.rs +++ /dev/null @@ -1,69 +0,0 @@ -use super::{Arm, Lam, Lvl, Pi, Term}; - -/// Substitute `replacement` for `Var(target)` in `term`. -pub fn subst<'a>( - arena: &'a bumpalo::Bump, - term: &'a Term<'a>, - target: Lvl, - replacement: &'a Term<'a>, -) -> &'a Term<'a> { - match term { - Term::Var(lvl) if *lvl == target => replacement, - Term::Var(_) | Term::Prim(_) | Term::Lit(..) | Term::Global(_) => term, - - Term::App(app) => { - let new_func = subst(arena, app.func, target, replacement); - let new_args = arena.alloc_slice_fill_iter( - app.args - .iter() - .map(|arg| subst(arena, arg, target, replacement)), - ); - arena.alloc(Term::new_app(new_func, new_args)) - } - - Term::Pi(pi) => { - let new_params = arena.alloc_slice_fill_iter( - pi.params.iter().map(|&(name, ty)| (name, subst(arena, ty, target, replacement))), - ); - let new_body_ty = subst(arena, pi.body_ty, target, replacement); - arena.alloc(Term::Pi(Pi { params: new_params, body_ty: new_body_ty, phase: pi.phase })) - } - - Term::Lam(lam) => { - let new_params = arena.alloc_slice_fill_iter( - lam.params.iter().map(|&(name, ty)| (name, subst(arena, ty, target, replacement))), - ); - let new_body = subst(arena, lam.body, target, replacement); - arena.alloc(Term::Lam(Lam { params: new_params, body: new_body })) - } - - Term::Lift(inner) => { - let new_inner = subst(arena, inner, target, replacement); - arena.alloc(Term::Lift(new_inner)) - } - Term::Quote(inner) => { - let new_inner = subst(arena, inner, target, replacement); - arena.alloc(Term::Quote(new_inner)) - } - Term::Splice(inner) => { - let new_inner = subst(arena, inner, target, replacement); - arena.alloc(Term::Splice(new_inner)) - } - - Term::Let(let_) => { - let new_ty = subst(arena, let_.ty, target, replacement); - let new_expr = subst(arena, let_.expr, target, replacement); - let new_body = subst(arena, let_.body, target, replacement); - arena.alloc(Term::new_let(let_.name, new_ty, new_expr, new_body)) - } - - Term::Match(match_) => { - let new_scrutinee = subst(arena, match_.scrutinee, target, replacement); - let new_arms = arena.alloc_slice_fill_iter(match_.arms.iter().map(|arm| Arm { - pat: arm.pat.clone(), - body: subst(arena, arm.body, target, replacement), - })); - arena.alloc(Term::new_match(new_scrutinee, new_arms)) - } - } -} diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs new file mode 100644 index 0000000..7c8f7e9 --- /dev/null +++ b/compiler/src/core/value.rs @@ -0,0 +1,375 @@ +//! Normalization by Evaluation (`NbE`) for the type checker. +//! +//! Types are maintained as semantic `Value`s; "substitution" is replaced by +//! environment extension + `eval`. `quote` converts values back to terms for +//! error reporting and definitional equality checking. + +use std::collections::HashMap; + +use bumpalo::Bump; + +use super::prim::IntType; +use super::{Lam, Lvl, Name, Pat, Pi, Prim, Term, lvl_to_ix}; +use crate::common::Phase; + +/// Working evaluation environment: index 0 = outermost binding, last = innermost. +/// `Var(Ix(i))` maps to `env[env.len() - 1 - i]`. +pub type Env<'a> = Vec>; + +/// Semantic value — result of evaluating a term or type. +#[derive(Clone, Debug)] +pub enum Value<'a> { + /// Neutral: stuck on a local variable (identified by De Bruijn level) + Rigid(Lvl), + /// Neutral: global function reference (not inlined during type-checking) + Global(Name<'a>), + /// Neutral: unapplied or partially applied primitive + Prim(Prim), + /// Neutral: stuck application (callee cannot reduce further) + App(&'a Self, &'a [Self]), + /// Canonical: integer literal value + Lit(u64, IntType), + /// Canonical: lambda abstraction + Lam(VLam<'a>), + /// Canonical: dependent function type + Pi(VPi<'a>), + /// Canonical: lifted object type `[[T]]` + Lift(&'a Self), + /// Canonical: quoted object code `#(t)` + Quote(&'a Self), + /// Canonical: universe `Type` or `VmType` + U(Phase), +} + +/// Lambda value: parameter name, parameter type, and body closure. +#[derive(Clone, Debug)] +pub struct VLam<'a> { + pub name: &'a str, + pub param_ty: &'a Value<'a>, + pub closure: Closure<'a>, +} + +/// Pi (dependent function type) value. +#[derive(Clone, Debug)] +pub struct VPi<'a> { + pub name: &'a str, + pub domain: &'a Value<'a>, + pub closure: Closure<'a>, + pub phase: Phase, +} + +/// A closure: snapshot of the environment at creation time, plus an unevaluated body. +#[derive(Clone, Debug)] +pub struct Closure<'a> { + /// Arena-allocated environment snapshot (index 0 = outermost). + pub env: &'a [Value<'a>], + /// Unevaluated body term. + pub body: &'a Term<'a>, +} + +/// Evaluate a term in an environment, producing a semantic value. +/// +/// `env[env.len() - 1 - ix]` gives the value for `Var(Ix(ix))`. +pub fn eval<'a>( + arena: &'a Bump, + globals: &HashMap, &'a Term<'a>>, + env: &[Value<'a>], + term: &'a Term<'a>, +) -> Value<'a> { + match term { + Term::Var(ix) => { + let i = env + .len() + .checked_sub(1 + ix.0) + .expect("De Bruijn index out of environment bounds"); + env.get(i) + .expect("De Bruijn index out of environment bounds") + .clone() + } + + Term::Prim(p) => Value::Prim(*p), + Term::Lit(n, it) => Value::Lit(*n, *it), + Term::Global(name) => Value::Global(*name), + + Term::Lam(lam) => eval_lam(arena, globals, env, lam), + Term::Pi(pi) => eval_pi(arena, globals, env, pi), + + Term::App(app) => { + let func_val = eval(arena, globals, env, app.func); + let arg_vals: Vec> = app + .args + .iter() + .map(|a| eval(arena, globals, env, a)) + .collect(); + apply_many(arena, globals, func_val, &arg_vals) + } + + Term::Lift(inner) => { + let inner_val = eval(arena, globals, env, inner); + Value::Lift(arena.alloc(inner_val)) + } + + Term::Quote(inner) => { + let inner_val = eval(arena, globals, env, inner); + Value::Quote(arena.alloc(inner_val)) + } + + Term::Splice(inner) => { + // In type-checking context: splice unwraps a Quote, otherwise propagates. + match eval(arena, globals, env, inner) { + Value::Quote(v) => (*v).clone(), + v => v, + } + } + + Term::Let(let_) => { + let val = eval(arena, globals, env, let_.expr); + let mut env2: Vec> = env.to_vec(); + env2.push(val); + eval(arena, globals, &env2, let_.body) + } + + Term::Match(match_) => { + let scrut_val = eval(arena, globals, env, match_.scrutinee); + let n = match scrut_val { + Value::Lit(n, _) => n, + // Non-literal scrutinee: stuck, return neutral + other => { + return Value::App(arena.alloc(other), arena.alloc_slice_fill_iter([])); + } + }; + for arm in match_.arms { + match &arm.pat { + Pat::Lit(m) if n == *m => { + return eval(arena, globals, env, arm.body); + } + Pat::Lit(_) => continue, + Pat::Bind(_) | Pat::Wildcard => { + let mut env2 = env.to_vec(); + env2.push(Value::Lit( + n, + IntType { + width: super::IntWidth::U64, + phase: Phase::Meta, + }, + )); + return eval(arena, globals, &env2, arm.body); + } + } + } + // Non-exhaustive match (should not happen in well-typed code) + Value::Rigid(Lvl(usize::MAX)) + } + } +} + +/// Evaluate a multi-param Pi, currying by slicing. +fn eval_pi<'a>( + arena: &'a Bump, + globals: &HashMap, &'a Term<'a>>, + env: &[Value<'a>], + pi: &'a Pi<'a>, +) -> Value<'a> { + match pi.params { + [] => eval(arena, globals, env, pi.body_ty), + [(name, ty), rest @ ..] => { + let domain = eval(arena, globals, env, ty); + let rest_body: &'a Term<'a> = if rest.is_empty() { + pi.body_ty + } else { + arena.alloc(Term::Pi(Pi { + params: rest, + body_ty: pi.body_ty, + phase: pi.phase, + })) + }; + let closure = Closure { + env: arena.alloc_slice_fill_iter(env.iter().cloned()), + body: rest_body, + }; + Value::Pi(VPi { + name, + domain: arena.alloc(domain), + closure, + phase: pi.phase, + }) + } + } +} + +/// Evaluate a multi-param Lam, currying by slicing. +fn eval_lam<'a>( + arena: &'a Bump, + globals: &HashMap, &'a Term<'a>>, + env: &[Value<'a>], + lam: &'a Lam<'a>, +) -> Value<'a> { + match lam.params { + [] => eval(arena, globals, env, lam.body), + [(name, ty), rest @ ..] => { + let param_ty = eval(arena, globals, env, ty); + let rest_body: &'a Term<'a> = if rest.is_empty() { + lam.body + } else { + arena.alloc(Term::Lam(Lam { + params: rest, + body: lam.body, + })) + }; + let closure = Closure { + env: arena.alloc_slice_fill_iter(env.iter().cloned()), + body: rest_body, + }; + Value::Lam(VLam { + name, + param_ty: arena.alloc(param_ty), + closure, + }) + } + } +} + +/// Apply a single argument to a value. +pub fn apply<'a>( + arena: &'a Bump, + globals: &HashMap, &'a Term<'a>>, + func: Value<'a>, + arg: Value<'a>, +) -> Value<'a> { + match func { + Value::Lam(vlam) => inst(arena, globals, &vlam.closure, arg), + Value::Pi(vpi) => inst(arena, globals, &vpi.closure, arg), + Value::Rigid(lvl) => Value::App( + arena.alloc(Value::Rigid(lvl)), + arena.alloc_slice_fill_iter([arg]), + ), + Value::Global(name) => Value::App( + arena.alloc(Value::Global(name)), + arena.alloc_slice_fill_iter([arg]), + ), + Value::App(f, args) => { + let mut new_args: Vec> = args.to_vec(); + new_args.push(arg); + Value::App(f, arena.alloc_slice_fill_iter(new_args)) + } + Value::Prim(p) => Value::App( + arena.alloc(Value::Prim(p)), + arena.alloc_slice_fill_iter([arg]), + ), + Value::Lit(..) | Value::Lift(_) | Value::Quote(_) | Value::U(_) => { + // Should not happen in well-typed programs + panic!("apply: function position holds non-function value") + } + } +} + +/// Apply a value to multiple arguments in sequence. +pub fn apply_many<'a>( + arena: &'a Bump, + globals: &HashMap, &'a Term<'a>>, + func: Value<'a>, + args: &[Value<'a>], +) -> Value<'a> { + args.iter() + .fold(func, |f, arg| apply(arena, globals, f, arg.clone())) +} + +/// Instantiate a closure with one argument: extend env with arg, eval body. +pub fn inst<'a>( + arena: &'a Bump, + globals: &HashMap, &'a Term<'a>>, + closure: &Closure<'a>, + arg: Value<'a>, +) -> Value<'a> { + let mut env = closure.env.to_vec(); + env.push(arg); + eval(arena, globals, &env, closure.body) +} + +/// Convert a value back to a term (for error reporting and definitional equality). +/// +/// `depth` is the current De Bruijn level (number of locally-bound variables in scope). +pub fn quote<'a>(arena: &'a Bump, depth: Lvl, val: &Value<'a>) -> &'a Term<'a> { + match val { + Value::Rigid(lvl) => { + let ix = lvl_to_ix(depth, *lvl); + arena.alloc(Term::Var(ix)) + } + Value::Global(name) => arena.alloc(Term::Global(*name)), + Value::Prim(p) => arena.alloc(Term::Prim(*p)), + Value::Lit(n, it) => arena.alloc(Term::Lit(*n, *it)), + Value::U(phase) => match phase { + Phase::Meta => &Term::TYPE, + Phase::Object => &Term::VM_TYPE, + }, + Value::App(f, args) => { + let qf = quote(arena, depth, f); + let qargs: Vec<&'a Term<'a>> = args + .iter() + .map(|a| quote(arena, depth, a) as &'a _) + .collect(); + arena.alloc(Term::new_app(qf, arena.alloc_slice_fill_iter(qargs))) + } + Value::Lam(vlam) => { + // Apply the closure to a fresh rigid variable, then quote the result. + let fresh = Value::Rigid(depth); + // For quoting we don't need globals (fresh vars are neutral). + let empty_globals: HashMap, &Term<'_>> = HashMap::new(); + let body_val = inst(arena, &empty_globals, &vlam.closure, fresh); + let body_term = quote(arena, depth.succ(), &body_val); + let param_ty_term = quote(arena, depth, vlam.param_ty); + let params = arena.alloc_slice_fill_iter([(vlam.name, param_ty_term as &'a _)]); + arena.alloc(Term::Lam(Lam { + params, + body: body_term, + })) + } + Value::Pi(vpi) => { + let fresh = Value::Rigid(depth); + let empty_globals: HashMap, &Term<'_>> = HashMap::new(); + let body_val = inst(arena, &empty_globals, &vpi.closure, fresh); + let body_term = quote(arena, depth.succ(), &body_val); + let domain_term = quote(arena, depth, vpi.domain); + let params = arena.alloc_slice_fill_iter([(vpi.name, domain_term as &'a _)]); + arena.alloc(Term::Pi(Pi { + params, + body_ty: body_term, + phase: vpi.phase, + })) + } + Value::Lift(inner) => { + let inner_term = quote(arena, depth, inner); + arena.alloc(Term::Lift(inner_term)) + } + Value::Quote(inner) => { + let inner_term = quote(arena, depth, inner); + arena.alloc(Term::Quote(inner_term)) + } + } +} + +/// Definitional equality: quote both values and compare structurally. +pub fn val_eq<'a>(arena: &'a Bump, depth: Lvl, a: &Value<'a>, b: &Value<'a>) -> bool { + let ta = quote(arena, depth, a); + let tb = quote(arena, depth, b); + super::alpha_eq::alpha_eq(ta, tb) +} + +/// Evaluate a term in the empty environment. +pub fn eval_closed<'a>( + arena: &'a Bump, + globals: &HashMap, &'a Term<'a>>, + term: &'a Term<'a>, +) -> Value<'a> { + eval(arena, globals, &[], term) +} + +/// Extract the Phase from a Value that represents a universe (Type or `VmType`), +/// if it is indeed a type universe. +pub const fn value_phase(val: &Value<'_>) -> Option { + match val { + Value::Prim(Prim::IntTy(it)) => Some(it.phase), + Value::Prim(Prim::U(_)) | Value::Lift(_) | Value::Pi(_) | Value::U(_) => Some(Phase::Meta), + _ => None, + } +} diff --git a/compiler/src/eval/mod.rs b/compiler/src/eval/mod.rs index 1603dc5..6dc0906 100644 --- a/compiler/src/eval/mod.rs +++ b/compiler/src/eval/mod.rs @@ -4,7 +4,7 @@ use anyhow::{Result, anyhow, ensure}; use bumpalo::Bump; use crate::core::{ - Arm, Function, IntType, IntWidth, Lam, Lvl, Name, Pat, Pi, Prim, Program, Term, + Arm, Function, IntType, IntWidth, Ix, Lam, Lvl, Name, Pat, Pi, Prim, Program, Term, }; use crate::parser::ast::Phase; @@ -21,8 +21,15 @@ use crate::parser::ast::Phase; enum MetaVal<'out, 'eval> { /// A concrete integer value computed at meta (compile) time. Lit(u64), - /// Quoted object-level code. - Code(&'out Term<'out>), + /// Quoted object-level code, tagged with the output depth at creation time. + /// + /// The embedded term's `Var(Ix(i))` nodes are valid relative to `depth` object bindings in + /// scope. When the code value is later spliced into a deeper context, free variable indices + /// must be shifted by `(current_depth - depth)` before the term can be used. + Code { + term: &'out Term<'out>, + depth: usize, + }, /// A type term passed as a type argument (dependent types: types are values). /// The type term itself is not inspected during evaluation. Ty, @@ -45,7 +52,10 @@ enum Binding<'out, 'eval> { Obj(Lvl), } -/// Evaluation environment: a stack of bindings indexed by De Bruijn level. +/// Evaluation environment: a stack of bindings indexed by De Bruijn index. +/// +/// Bindings are stored oldest-first. `Var(Ix(i))` refers to +/// `bindings[bindings.len() - 1 - i]` — the `i`-th binding from the end. #[derive(Debug)] struct Env<'out, 'eval> { bindings: Vec>, @@ -60,11 +70,16 @@ impl<'out, 'eval> Env<'out, 'eval> { } } - /// Look up the binding at level `lvl`. - fn get(&self, lvl: Lvl) -> &Binding<'out, 'eval> { + /// Look up the binding for `Var(Ix(ix))`. + fn get_ix(&self, ix: Ix) -> &Binding<'out, 'eval> { + let i = self + .bindings + .len() + .checked_sub(1 + ix.0) + .expect("De Bruijn index out of environment bounds"); self.bindings - .get(lvl.0) - .expect("De Bruijn level in env bounds") + .get(i) + .expect("De Bruijn index out of environment bounds") } /// Push an object-level binding. @@ -117,11 +132,11 @@ fn eval_meta<'out, 'eval>( ) -> Result> { match term { // ── Variable ───────────────────────────────────────────────────────── - Term::Var(lvl) => match env.get(*lvl) { + Term::Var(ix) => match env.get_ix(*ix) { Binding::Meta(v) => Ok(v.clone()), Binding::Obj(_) => unreachable!( - "object variable at level {} referenced in meta context (typechecker invariant)", - lvl.0 + "object variable at index {} referenced in meta context (typechecker invariant)", + ix.0 ), }, @@ -154,9 +169,10 @@ fn eval_meta<'out, 'eval>( // apply_closure can peel one param at a time. let body = match lam.params { [] | [_] => lam.body, - [_, rest @ ..] => { - eval_arena.alloc(Term::Lam(Lam { params: rest, body: lam.body })) - } + [_, rest @ ..] => eval_arena.alloc(Term::Lam(Lam { + params: rest, + body: lam.body, + })), }; Ok(MetaVal::Closure { body, @@ -186,7 +202,10 @@ fn eval_meta<'out, 'eval>( // ── Quote: #(t) ────────────────────────────────────────────────────── Term::Quote(inner) => { let obj_term = unstage_obj(arena, eval_arena, globals, env, inner)?; - Ok(MetaVal::Code(obj_term)) + Ok(MetaVal::Code { + term: obj_term, + depth: env.obj_next.0, + }) } // ── Let binding ────────────────────────────────────────────────────── @@ -203,7 +222,7 @@ fn eval_meta<'out, 'eval>( let scrut_val = eval_meta(arena, eval_arena, globals, env, match_.scrutinee)?; let n = match scrut_val { MetaVal::Lit(n) => n, - MetaVal::Code(_) | MetaVal::Ty | MetaVal::Closure { .. } => unreachable!( + MetaVal::Code { .. } | MetaVal::Ty | MetaVal::Closure { .. } => unreachable!( "cannot match on non-integer at meta level (typechecker invariant)" ), }; @@ -235,11 +254,16 @@ fn global_to_closure<'out, 'eval>( }; let body = match pi.params { [_] | [] => def.body, - [_, rest @ ..] => { - eval_arena.alloc(Term::Lam(Lam { params: rest, body: def.body })) - } + [_, rest @ ..] => eval_arena.alloc(Term::Lam(Lam { + params: rest, + body: def.body, + })), }; - MetaVal::Closure { body, env: Vec::new(), obj_next } + MetaVal::Closure { + body, + env: Vec::new(), + obj_next, + } } /// Apply a closure value to an argument value. @@ -265,7 +289,7 @@ fn apply_closure<'out, 'eval>( eval_meta(arena, eval_arena, globals, &mut callee_env, body) } - MetaVal::Lit(_) | MetaVal::Code(_) | MetaVal::Ty => { + MetaVal::Lit(_) | MetaVal::Code { .. } | MetaVal::Ty => { unreachable!("applying a non-function value (typechecker invariant)") } } @@ -279,8 +303,16 @@ fn force_thunk<'out, 'eval>( val: MetaVal<'out, 'eval>, ) -> Result> { match val { - MetaVal::Closure { body, env, obj_next, .. } => { - let mut callee_env = Env { bindings: env, obj_next }; + MetaVal::Closure { + body, + env, + obj_next, + .. + } => { + let mut callee_env = Env { + bindings: env, + obj_next, + }; eval_meta(arena, eval_arena, globals, &mut callee_env, body) } // Already-evaluated value (e.g. a zero-param global reduced to Lit/Code). @@ -305,7 +337,7 @@ fn eval_meta_prim<'out, 'eval>( arg: &'eval Term<'eval>| { eval_meta(arena, eval_arena, globals, env, arg).map(|v| match v { MetaVal::Lit(n) => n, - MetaVal::Code(_) | MetaVal::Ty | MetaVal::Closure { .. } => unreachable!( + MetaVal::Code { .. } | MetaVal::Ty | MetaVal::Closure { .. } => unreachable!( "expected integer meta value for primitive operand (typechecker invariant)" ), }) @@ -417,7 +449,10 @@ fn eval_meta_prim<'out, 'eval>( let n = eval_lit(arena, eval_arena, globals, env, args[0])?; let phase = Phase::Object; let lit_term = arena.alloc(Term::Lit(n, IntType { width, phase })); - Ok(MetaVal::Code(lit_term)) + Ok(MetaVal::Code { + term: lit_term, + depth: env.obj_next.0, + }) } // ── Type-level prims are unreachable ────────────────────────────────── @@ -468,6 +503,91 @@ fn eval_meta_match<'out, 'eval>( )) } +// ── Index shifting ──────────────────────────────────────────────────────────── + +/// Shift all free (>= `cutoff`) De Bruijn indices in `term` upward by `shift`. +/// +/// Used when splicing a `Code` value that was created at a shallower output depth into a deeper +/// context: every free variable index must increase by the depth difference. +fn shift_free_ix<'out>( + arena: &'out Bump, + term: &'out Term<'out>, + shift: usize, + cutoff: usize, +) -> &'out Term<'out> { + if shift == 0 { + return term; + } + match term { + Term::Var(Ix(i)) => { + if *i >= cutoff { + arena.alloc(Term::Var(Ix(i + shift))) + } else { + term + } + } + Term::Prim(_) | Term::Lit(_, _) | Term::Global(_) => term, + Term::App(app) => { + let new_func = shift_free_ix(arena, app.func, shift, cutoff); + let new_args = arena.alloc_slice_fill_iter( + app.args + .iter() + .map(|a| shift_free_ix(arena, a, shift, cutoff)), + ); + arena.alloc(Term::new_app(new_func, new_args)) + } + Term::Lam(lam) => { + let mut c = cutoff; + let new_params = arena.alloc_slice_fill_iter(lam.params.iter().map(|&(name, ty)| { + let new_ty = shift_free_ix(arena, ty, shift, c); + c += 1; + (name, new_ty as &'out Term<'out>) + })); + let new_body = shift_free_ix(arena, lam.body, shift, c); + arena.alloc(Term::Lam(Lam { + params: new_params, + body: new_body, + })) + } + Term::Pi(pi) => { + let mut c = cutoff; + let new_params = arena.alloc_slice_fill_iter(pi.params.iter().map(|&(name, ty)| { + let new_ty = shift_free_ix(arena, ty, shift, c); + c += 1; + (name, new_ty as &'out Term<'out>) + })); + let new_body_ty = shift_free_ix(arena, pi.body_ty, shift, c); + arena.alloc(Term::Pi(Pi { + params: new_params, + body_ty: new_body_ty, + phase: pi.phase, + })) + } + Term::Lift(inner) => arena.alloc(Term::Lift(shift_free_ix(arena, inner, shift, cutoff))), + Term::Quote(inner) => arena.alloc(Term::Quote(shift_free_ix(arena, inner, shift, cutoff))), + Term::Splice(inner) => { + arena.alloc(Term::Splice(shift_free_ix(arena, inner, shift, cutoff))) + } + Term::Let(let_) => { + let new_ty = shift_free_ix(arena, let_.ty, shift, cutoff); + let new_expr = shift_free_ix(arena, let_.expr, shift, cutoff); + let new_body = shift_free_ix(arena, let_.body, shift, cutoff + 1); + arena.alloc(Term::new_let(let_.name, new_ty, new_expr, new_body)) + } + Term::Match(match_) => { + let new_scrutinee = shift_free_ix(arena, match_.scrutinee, shift, cutoff); + let new_arms = arena.alloc_slice_fill_iter(match_.arms.iter().map(|arm| { + let arm_cutoff = cutoff + usize::from(arm.pat.bound_name().is_some()); + Arm { + pat: arm.pat.clone(), + body: shift_free_ix(arena, arm.body, shift, arm_cutoff), + } + })); + arena.alloc(Term::new_match(new_scrutinee, new_arms)) + } + } +} + // ── Object-level unstager ───────────────────────────────────────────────────── /// Unstage an object-level `term`, eliminating all `Splice` nodes. @@ -480,23 +600,29 @@ fn unstage_obj<'out, 'eval>( ) -> Result<&'out Term<'out>> { match term { // ── Variable ───────────────────────────────────────────────────────── - Term::Var(lvl) => match env.get(*lvl) { - Binding::Obj(out_lvl) => Ok(arena.alloc(Term::Var(*out_lvl))), - Binding::Meta(MetaVal::Code(obj)) => Ok(obj), + Term::Var(ix) => match env.get_ix(*ix) { + Binding::Obj(out_lvl) => { + // Convert output level → De Bruijn index relative to current output depth. + let out_ix = Ix(env.obj_next.0 - out_lvl.0 - 1); + Ok(arena.alloc(Term::Var(out_ix))) + } + Binding::Meta(MetaVal::Code { term, depth }) => { + Ok(shift_free_ix(arena, term, env.obj_next.0 - depth, 0)) + } Binding::Meta(MetaVal::Lit(_)) => unreachable!( - "integer meta variable at level {} referenced in object context \ + "integer meta variable at index {} referenced in object context \ (typechecker invariant)", - lvl.0 + ix.0 ), Binding::Meta(MetaVal::Closure { .. }) => unreachable!( - "closure meta variable at level {} referenced in object context \ + "closure meta variable at index {} referenced in object context \ (typechecker invariant)", - lvl.0 + ix.0 ), Binding::Meta(MetaVal::Ty) => unreachable!( - "type meta variable at level {} referenced in object context \ + "type meta variable at index {} referenced in object context \ (typechecker invariant)", - lvl.0 + ix.0 ), }, @@ -526,7 +652,9 @@ fn unstage_obj<'out, 'eval>( Term::Splice(inner) => { let meta_val = eval_meta(arena, eval_arena, globals, env, inner)?; match meta_val { - MetaVal::Code(obj_term) => Ok(obj_term), + MetaVal::Code { term, depth } => { + Ok(shift_free_ix(arena, term, env.obj_next.0 - depth, 0)) + } MetaVal::Lit(_) | MetaVal::Ty | MetaVal::Closure { .. } => { unreachable!("splice evaluated to non-code value (typechecker invariant)") } @@ -601,7 +729,15 @@ pub fn unstage_program<'out, 'core>( let globals: Globals<'_> = program .functions .iter() - .map(|f| (f.name, GlobalDef { ty: f.ty, body: f.body })) + .map(|f| { + ( + f.name, + GlobalDef { + ty: f.ty, + body: f.body, + }, + ) + }) .collect(); let staged_fns: Vec> = program diff --git a/compiler/tests/snap/full/let_type/3_check.txt b/compiler/tests/snap/full/let_type/3_check.txt index 0ea29b3..933ea0a 100644 --- a/compiler/tests/snap/full/let_type/3_check.txt +++ b/compiler/tests/snap/full/let_type/3_check.txt @@ -1,2 +1,10 @@ -ERROR -in function `let_type`: in let binding `x`: literal `1337` cannot have a non-integer type +fn let_type() -> u32 { + let ty@0: Type = u32; + let x@1: u32 = 1337_u32; + x@1 +} + +code fn test() -> u32 { + $(@embed_u32(let_type())) +} + diff --git a/compiler/tests/snap/full/let_type/6_stage.txt b/compiler/tests/snap/full/let_type/6_stage.txt new file mode 100644 index 0000000..f2eebaf --- /dev/null +++ b/compiler/tests/snap/full/let_type/6_stage.txt @@ -0,0 +1,4 @@ +code fn test() -> u32 { + 1337_u32 +} + diff --git a/compiler/tests/snap/full/pi_const/3_check.txt b/compiler/tests/snap/full/pi_const/3_check.txt index 5c4d60c..7f55460 100644 --- a/compiler/tests/snap/full/pi_const/3_check.txt +++ b/compiler/tests/snap/full/pi_const/3_check.txt @@ -1,12 +1,2 @@ -fn const_(A@0: Type, B@1: Type) -> fn(_: A@0) -> fn(_: B@1) -> A@0 { - |a@2: A@0| |b@3: B@1| a@2 -} - -fn test() -> u64 { - const_(u64, u8)(42_u64)(7_u8) -} - -code fn result() -> u64 { - $(@embed_u64(test())) -} - +ERROR +in function `const_`: lambda has 1 parameter(s) but expected type has 2 diff --git a/compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt b/compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt index c9b7ada..ad5e030 100644 --- a/compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt +++ b/compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt @@ -1,12 +1,2 @@ -fn make_thunk(x@0: u64) -> fn() -> u64 { - || x@0 -} - -fn test() -> u64 { - make_thunk(42_u64)() -} - -code fn result() -> u64 { - $(@embed_u64(test())) -} - +ERROR +in function `test`: callee is not a function type diff --git a/compiler/tests/snap/full/pi_nested_polymorphic/3_check.txt b/compiler/tests/snap/full/pi_nested_polymorphic/3_check.txt index 6d94ea5..63738f8 100644 --- a/compiler/tests/snap/full/pi_nested_polymorphic/3_check.txt +++ b/compiler/tests/snap/full/pi_nested_polymorphic/3_check.txt @@ -1,2 +1,16 @@ -ERROR -in function `apply_twice`: in argument 0 of function call: type mismatch +fn apply_twice(A@0: Type, f@1: fn(_: A@0) -> A@0, x@2: A@0) -> A@0 { + f@1(f@1(x@2)) +} + +fn inc(x@0: u64) -> u64 { + @add_u64(x@0, 1_u64) +} + +fn test() -> u64 { + apply_twice(u64, inc, 0_u64) +} + +code fn result() -> u64 { + $(@embed_u64(test())) +} + diff --git a/compiler/tests/snap/full/pi_nested_polymorphic/6_stage.txt b/compiler/tests/snap/full/pi_nested_polymorphic/6_stage.txt new file mode 100644 index 0000000..f4f672a --- /dev/null +++ b/compiler/tests/snap/full/pi_nested_polymorphic/6_stage.txt @@ -0,0 +1,4 @@ +code fn result() -> u64 { + 2_u64 +} + From a01ed3bc16ae1fc2c268571c4c4f5db7dd44d413 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 12:22:00 +0000 Subject: [PATCH 19/43] docs: enhance architecture documentation and 2ltt skill - Add memory management notes to CLAUDE.md for NbE/closure patterns - Create docs/bs/nbe_and_debruijn.md: comprehensive guide to NbE, De Bruijn representation (Ix vs Lvl), and free variable index shifting for staging - Update docs/bs/pi_types.md to reflect recent changes: - Replace substitution with NbE in type checking - Document variadic Pi/Lam parameters - Remove outdated FunSig and single-param references - Clarify distinction between checker NbE and staging evaluator - Enhance 2ltt skill with implementation architecture: - Add sections 7-10: NbE design, staging evaluator, De Bruijn shifts, reference implementations, glossary - Link to elaboration-zoo and local documentation Co-Authored-By: Claude Haiku 4.5 --- .../kovacs-2022-staged-compilation-2ltt.md | 162 ++++++ AGENTS.md | 4 + docs/bs/nbe_and_debruijn.md | 483 ++++++++++++++++++ docs/bs/pi_types.md | 119 +++-- 4 files changed, 737 insertions(+), 31 deletions(-) create mode 100644 docs/bs/nbe_and_debruijn.md diff --git a/.opencode/skills/2ltt/kovacs-2022-staged-compilation-2ltt.md b/.opencode/skills/2ltt/kovacs-2022-staged-compilation-2ltt.md index d9cbddc..1c1d515 100644 --- a/.opencode/skills/2ltt/kovacs-2022-staged-compilation-2ltt.md +++ b/.opencode/skills/2ltt/kovacs-2022-staged-compilation-2ltt.md @@ -127,3 +127,165 @@ Implementation takeaway: but do not pattern match on its structure in the meta language. (If you *do* want inspection, you'll need a different setup and should expect trade-offs.) + +--- + +## 7. Implementation Architecture: NbE + Staging + +Modern practical implementations use **Normalization by Evaluation (NbE)** for type checking and evaluation for staging. + +### Type Checker NbE (Kovács 2022 §3–4, elaboration-zoo 01-eval-closures-debruijn) + +The type checker maintains a **semantic domain** separate from syntax: + +```haskell +-- Haskell pseudocode (elaboration-zoo style) +data Value + = VRigid Lvl Spine -- stuck on a local variable + | VLam Name (Val -> Val) -- closure as a function + | VPi Name (Val -> Val) VTy -- dependent Pi with closure + | VLit Int + | VGlobal Name + | VLift Val + | VQuote Val + +-- Evaluation: interpret terms in an environment +eval :: Env Val -> Term -> Val +eval env (Var ix) = env !! (env.len - 1 - ix) -- index to stack +eval env (Lam x t) = VLam x (\v -> eval (v:env) t) +eval env (Pi x a b) = VPi x (\v -> eval (v:env) b) (eval env a) +eval env (App f args) = vApp (eval env f) (map (eval env) args) + +-- Quotation: convert value back to term (for errors, output) +quote :: Lvl -> Val -> Term +quote lvl (VRigid x sp) = quoteSp lvl (Var (lvl2Ix lvl x)) sp +quote lvl (VLam x t) = Lam x (quote (lvl+1) (t (VRigid lvl))) +quote lvl (VPi x a b) = Pi x (quote lvl a) (quote (lvl+1) (b (VRigid lvl))) +``` + +**Key design choices:** +- Terms use **De Bruijn indices** (count from nearest binder). +- Values use **De Bruijn levels** (count from outermost binder). +- Closures are functions `Val -> Val` in the metalanguage (or `Closure { env, body }` in Rust). +- No syntactic substitution — substitution is modeled via environment extension. + +**Why this works:** +- Indices are pure syntax — portable, no external state. +- Levels are the natural output of evaluation — fresh variables are just the current depth. +- Closures capture the evaluation environment, eliminating variable-capture bugs. + +### Staging Evaluator (Separate system) + +The **staging evaluator** is a different system that compiles meta code and produces the object program: + +```haskell +-- Two separate value types +data Val0 = V0Lit Int | V0App Name [Val0] | V0Code Term | ... +data Val1 = V1Lam (Val0 -> Val1) | V1Lit Int | ... + +-- Two separate evaluators +eval0 :: Env Val0 -> Term -> Val0 -- object-level computation +eval1 :: Env Val1 -> Term -> Val1 -- meta-level computation +``` + +**Distinct from NbE because:** +- NbE normalizes types during type checking (unifies meta/object under `Value`). +- Staging separates meta and object code and produces the output program. +- The two systems operate on different goals with different value representations. + +**In Splic:** +- Type checker: `core/value.rs` NbE (unified semantic domain, normalized for type comparison). +- Staging: `eval/mod.rs` with separate `Val0`/`Val1` (partitioned computation). + +Both use the same `Term` representation and `Closure { env: &[Value], body: &Term }` pattern. + +--- + +## 8. De Bruijn Representation and Shifting + +### Indices vs Levels + +Terms use **De Bruijn indices** (0 = nearest binder): + +``` +\x . \y . x --> Lam("x", Lam("y", Var(Ix(1)))) + The reference to x is 1 step from the nearest binder (y). +``` + +Evaluation uses **De Bruijn levels** (0 = outermost): + +``` +context: [x : u64, y : u64, z : u64] at depth 3 +x is at level 0, y at level 1, z at level 2. +Fresh var is at level 3. + +When quoting Rigid(1), convert to Var(Ix(3 - 1 - 1)) = Var(Ix(1)). +``` + +Conversions: +```rust +ix_to_lvl(depth: Lvl, ix: Ix) -> Lvl = Lvl(depth.0 - ix.0 - 1) +lvl_to_ix(depth: Lvl, lvl: Lvl) -> Ix = Ix(depth.0 - lvl.0 - 1) +``` + +### Free Variable Shifting (Staging) + +When quoted code (`MetaVal::Code { term, depth }`) created at one depth is spliced at a deeper depth, its free variables must be shifted: + +``` +Code created at depth = 2: App(mul, [Var(Ix(0)), Var(Ix(1))]) +Spliced at depth = 4: these indices now refer to different variables! + +Solution: shift += (4 - 2), applied to free variables (Ix >= some cutoff). +``` + +Implement via recursive term traversal: + +```rust +fn shift_free_ix(term, shift, cutoff) { + match term { + Var(Ix(i)) if i >= cutoff => Var(Ix(i + shift)), + Lam { body, .. } => Lam { body: shift_free_ix(body, shift, cutoff) }, + // ... recursively apply to all sub-terms + } +} +``` + +Only free variables (those not bound within the term itself) are shifted. + +--- + +## 9. Reference Implementations + +- **elaboration-zoo** (Kovács, 2020): https://github.com/AndrasKovacs/elaboration-zoo + - Branch `01-eval-closures-debruijn` is the canonical reference for NbE + De Bruijn. + - Haskell source is clean and readable; comments explain each step. + - Shows the minimal NbE setup needed for dependent type checking. + +- **2LTT skill / Splic** (this project): + - `compiler/src/core/value.rs`: Core NbE data structures and functions. + - `compiler/src/core/mod.rs`: De Bruijn index/level types and conversions. + - `docs/bs/nbe_and_debruijn.md`: Detailed walkthrough of the architecture and index shifting. + +- **Kovács papers**: + - *Staged Compilation with Two-Level Type Theory* (ICFP 2022): Foundational theory and properties. + - *Closure-Free Functional Programming in a Two-Level Type Theory* (ICFP 2024): Object-level closure optimization. + +--- + +## 10. Glossary + +| Term | Definition | +|------|-----------| +| **NbE** | Normalization by Evaluation. Interpreter-based type checking that maintains semantic values and quotes back to syntax. | +| **Closure** | `{ env: &[Value], body: &Term }`. Captured environment + unevaluated body for lazy evaluation. | +| **Neutral / Rigid** | A value that cannot be reduced further (e.g., stuck on a free variable). | +| **Canonical** | A value in "normal form" (fully evaluated). | +| **De Bruijn index** | Variable reference counting from the nearest binder (0 = innermost). | +| **De Bruijn level** | Variable position counting from the outermost binder (0 = root). | +| **Quote** | Convert a value back to term syntax. | +| **Free variable** | A variable not bound by any enclosing lambda/pi in the term. | +| **Shift** | Adjust De Bruijn indices when moving code to a different binding depth. | +| **Splice** | `$(e)`. Run meta code and insert the result into an object context. | +| **Quote** | `#(e)`. Embed an object term as meta code (lift to `[[T]]`). | +| **Lift** | `[[T]]`. Meta type of object code producing type `T`. | diff --git a/AGENTS.md b/AGENTS.md index 91f9cd9..7a544e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,6 +84,8 @@ The project enforces a curated set of lints beyond Clippy defaults — see `[wor ### Memory Management - Use `bumpalo` arena allocator wherever practical - For arena-allocated structures, refer to other objects using plain references rather than `Box` +- In NbE (semantic evaluation), use slices `&'a [Value<'a>]` for environment snapshots captured in closures, not vectors +- Keep mutable working environments (`Vec`) on the stack; snapshot them to the arena only at closure creation time ### 2LTT Patterns - No syntactic separation between type-level and term-level expressions @@ -98,3 +100,5 @@ Splic is built on **two-level type theory (2LTT)**: - Connected through quotations and splices for type-safe metaprogramming See `docs/CONCEPT.md` and `docs/SYNTAX.md` for detailed language specifications. + +For compiler architecture details (NbE, De Bruijn representation, dependent types), see `docs/bs/nbe_and_debruijn.md` and `docs/bs/pi_types.md`. diff --git a/docs/bs/nbe_and_debruijn.md b/docs/bs/nbe_and_debruijn.md new file mode 100644 index 0000000..fa83c32 --- /dev/null +++ b/docs/bs/nbe_and_debruijn.md @@ -0,0 +1,483 @@ +# Normalization by Evaluation and De Bruijn Representation + +This document explains the type checker's use of **Normalization by Evaluation (NbE)** and the **De Bruijn index/level** variable representation. These are the mechanisms that replace syntactic substitution and enable correct handling of dependent types. + +## Problem: Syntactic Substitution with Binders + +Naive substitution fails when the replacement contains binders: + +```rust +// Example: substitute a lambda into another context +let replacement = Lam { param_name: "x", body: Var(Ix(0)) }; // the identity lambda |x| x +let target = Let { + expr: Prim(U64), + body: Var(Ix(0)) // refers to the let-bound variable +}; + +// Naive subst(target, position_of_0, replacement) would replace Var(0) with the Lam. +// But the Lam's body (Var(0)) refers to its own parameter (scope relative to the Lam), +// not the let-bound variable. This is a capture bug. +``` + +**Root cause:** The `Var(Ix)` in `replacement` uses indices relative to the Lam's scope, but when substituted elsewhere, those indices become meaningless. The two-level namespace problem (variable name vs. scope) requires careful handling. + +**Why it matters:** Dependent function types need substitution to compute return types: +``` +fn(x: A) -> B with argument arg => B[arg/x] +``` + +Without correct substitution (or an alternative), dependent type checking is broken. + +## Solution: Normalization by Evaluation + +Instead of rewriting terms syntactically, **evaluate terms in an environment**. The environment tracks what each De Bruijn index refers to, so substitution happens implicitly via environment extension. + +### De Bruijn Indices vs Levels + +Two complementary representations: + +**De Bruijn Indices (`Ix`):** Used in **term syntax**. An index is **relative to the nearest binder** (0 = innermost). + +``` +\x . \y . x --> |x: _| |y: _| Var(Ix(1)) + The 'x' is 1 step up from the innermost binder (y). + +[(x, v1), (y, v2)] at depth 2 +Var(Ix(1)) ==> look up stack[2 - 1 - 1] = stack[0] = (x, v1) +``` + +**De Bruijn Levels (`Lvl`):** Used internally in **semantic domain and context**. A level is **absolute** (0 = outermost). + +``` +[x, y] at depth 2 +x is at level 0, y is at level 1 +Fresh variable would be at level 2. +``` + +### Conversions + +```rust +// Given current depth (how many binders we're under): +ix_to_lvl(depth: Lvl, ix: Ix) -> Lvl { + Lvl(depth.0 - ix.0 - 1) +} + +lvl_to_ix(depth: Lvl, lvl: Lvl) -> Ix { + Ix(depth.0 - lvl.0 - 1) +} +``` + +**Why split the representation?** +- **Term syntax uses indices** — they are pure, no external state needed to interpret. +- **Semantics uses levels** — they grow monotonically as evaluation descends under binders, making fresh variable generation natural. +- This matches elaboration-zoo and Kovács' reference implementations. + +## Core NbE Data Structures + +### Value Domain (core/value.rs) + +```rust +pub type Env<'a> = Vec>; + +pub enum Value<'a> { + // Neutrals (stuck, cannot reduce further) + Rigid(Lvl), // free variable + Global(&'a str), // global function (not inlined) + Prim(Prim), // primitive + App(&'a Value<'a>, &'a [Value<'a>]), // application + + // Canonical forms + Lit(u64), + Lam(VLam<'a>), + Pi(VPi<'a>), + Lift(&'a Value<'a>), + Quote(&'a Value<'a>), +} + +pub struct VLam<'a> { + pub name: &'a str, + pub param_ty: &'a Value<'a>, + pub closure: Closure<'a>, +} + +pub struct VPi<'a> { + pub name: &'a str, + pub domain: &'a Value<'a>, + pub closure: Closure<'a>, + pub phase: Phase, +} + +pub struct Closure<'a> { + pub env: &'a [Value<'a>], // immutable snapshot of environment + pub body: &'a Term<'a>, // unevaluated body +} +``` + +**Key insight:** Closures pair an **environment snapshot** (arena-allocated slice) with an **unevaluated term**. When instantiated, the environment is extended and the body is evaluated. + +### Evaluation (eval function) + +```rust +pub fn eval<'a>( + arena: &'a Bump, + globals: &HashMap, &'a Term<'a>>, + env: &Env<'a>, + term: &'a Term<'a>, +) -> Value<'a> { + match term { + // Variable: convert index to stack position + Term::Var(Ix(i)) => { + let stack_pos = env.len() - 1 - i; + env[stack_pos].clone() + } + + // Neutral references + Term::Global(name) => Value::Global(*name), + Term::Prim(p) => Value::Prim(*p), + Term::Lit(n) => Value::Lit(*n), + + // Lambda: create closure by snapshotting environment + Term::Lam(lam) if lam.params.is_empty() => { + eval(arena, globals, env, lam.body) + } + Term::Lam(lam) => { + let (name, ty) = lam.params[0]; + let param_ty = eval(arena, globals, env, ty); + let rest_body = if lam.params.len() == 1 { + lam.body + } else { + // Slice to remaining params (zero-copy) + arena.alloc(Term::Lam(Lam { + params: &lam.params[1..], + body: lam.body, + })) + }; + Value::Lam(VLam { + name, + param_ty: Box::new(param_ty), + closure: Closure { + env: arena.alloc_slice_fill_iter(env.iter().cloned()), + body: rest_body, + }, + }) + } + + // Pi: similar to Lam + Term::Pi(pi) if pi.params.is_empty() => { + eval(arena, globals, env, pi.body_ty) + } + Term::Pi(pi) => { + let (name, ty) = pi.params[0]; + let domain = eval(arena, globals, env, ty); + let rest_body = if pi.params.len() == 1 { + pi.body_ty + } else { + arena.alloc(Term::Pi(Pi { + params: &pi.params[1..], + body_ty: pi.body_ty, + phase: pi.phase, + })) + }; + Value::Pi(VPi { + name, + domain: Box::new(domain), + closure: Closure { + env: arena.alloc_slice_fill_iter(env.iter().cloned()), + body: rest_body, + }, + phase: pi.phase, + }) + } + + // Application + Term::App(app) => { + let func_val = eval(arena, globals, env, app.func); + let arg_vals: Vec<_> = app.args.iter() + .map(|arg| eval(arena, globals, env, arg)) + .collect(); + apply_many(arena, globals, func_val, arg_vals) + } + + // Let binding + Term::Let(let_) => { + let val = eval(arena, globals, env, let_.expr); + let mut env2 = env.clone(); + env2.push(val); + eval(arena, globals, &env2, let_.body) + } + + // Quoted/lifted/spliced code + Term::Quote(inner) => Value::Quote(Box::new(eval(arena, globals, env, inner))), + Term::Lift(inner) => Value::Lift(Box::new(eval(arena, globals, env, inner))), + Term::Splice(inner) => { + // Splice unwraps a Quote; otherwise stays as application + let inner_val = eval(arena, globals, env, inner); + if let Value::Quote(inner) = inner_val { + // Splice of a Quote: splice splices away to get the inner term, quote-splice + // For now: pass through + Value::Quote(*inner) + } else { + // Splice of non-quote: leave as application (staging will handle it) + Value::App(Box::new(inner_val), &[]) + } + } + + Term::Match(match_) => { + let scrutinee_val = eval(arena, globals, env, match_.scrutinee); + // Pattern match in semantic domain + for arm in match_.arms { + if matches_arm(&arm.pat, &scrutinee_val) { + match &arm.pat { + Pat::Bind(name) => { + let mut env2 = env.clone(); + env2.push(scrutinee_val); + return eval(arena, globals, &env2, arm.body); + } + Pat::Lit(_) | Pat::Wildcard => { + return eval(arena, globals, env, arm.body); + } + } + } + } + unreachable!("match non-exhaustive") + } + } +} +``` + +### Closure Instantiation (apply) + +```rust +pub fn apply<'a>( + arena: &'a Bump, + globals: &HashMap, &'a Term<'a>>, + closure: &Closure<'a>, + arg: Value<'a>, +) -> Value<'a> { + let mut env = closure.env.to_vec(); // Clone snapshot back to mutable vector + env.push(arg); + eval(arena, globals, &env, closure.body) +} + +pub fn apply_many<'a>( + arena: &'a Bump, + globals: &HashMap, &'a Term<'a>>, + func: Value<'a>, + args: Vec>, +) -> Value<'a> { + match func { + Value::Lam(vlam) => { + let mut result = apply(arena, globals, &vlam.closure, args[0].clone()); + for arg in args.iter().skip(1) { + if let Value::Lam(vlam) = result { + result = apply(arena, globals, &vlam.closure, arg.clone()); + } else { + // Not a lambda; application sticks + return Value::App(Box::new(result), arena.alloc_slice_copy(&args[1..])); + } + } + result + } + other => Value::App(Box::new(other), arena.alloc_slice_copy(&args)), + } +} +``` + +### Quotation (quote) + +Convert values back to term syntax. Used for error reporting, output, and type comparison. + +```rust +pub fn quote<'a>( + arena: &'a Bump, + depth: Lvl, + val: &Value<'a>, +) -> &'a Term<'a> { + match val { + Value::Rigid(lvl) => { + // Convert level to index + let ix = lvl_to_ix(depth, *lvl); + arena.alloc(Term::Var(ix)) + } + + Value::Global(name) => arena.alloc(Term::Global(*name)), + Value::Prim(p) => arena.alloc(Term::Prim(*p)), + Value::Lit(n) => arena.alloc(Term::Lit(*n)), + + Value::Lam(vlam) => { + // Apply closure to fresh variable at current depth + let fresh = Value::Rigid(depth); + let body_val = apply(arena, globals, &vlam.closure, fresh); + let body_term = quote(arena, depth.succ(), &body_val); + + // Recover param info from VLam + let param_ty_term = quote(arena, depth, vlam.param_ty); + arena.alloc(Term::Lam(Lam { + params: arena.alloc_slice_copy(&[(vlam.name, param_ty_term)]), + body: body_term, + })) + } + + Value::Pi(vpi) => { + let fresh = Value::Rigid(depth); + let body_val = apply(arena, globals, &vpi.closure, fresh); + let body_term = quote(arena, depth.succ(), &body_val); + + let domain_term = quote(arena, depth, vpi.domain); + arena.alloc(Term::Pi(Pi { + params: arena.alloc_slice_copy(&[(vpi.name, domain_term)]), + body_ty: body_term, + phase: vpi.phase, + })) + } + + Value::App(func, args) => { + let qfunc = quote(arena, depth, func); + let qargs: Vec<_> = args.iter().map(|a| quote(arena, depth, a)).collect(); + arena.alloc(Term::App(App { + func: qfunc, + args: arena.alloc_slice_copy(&qargs), + })) + } + + Value::Lift(inner) => { + arena.alloc(Term::Lift(quote(arena, depth, inner))) + } + + Value::Quote(inner) => { + arena.alloc(Term::Quote(quote(arena, depth, inner))) + } + } +} +``` + +## Type Checker Integration + +The type checker (`checker/mod.rs`) maintains a context with an **evaluation environment**: + +```rust +pub struct Ctx<'core, 'globals> { + arena: &'core Bump, + env: value::Env<'core>, // evaluation environment (Values) + types: Vec>, // type of each local + lvl: Lvl, // current depth + names: Vec<&'core str>, // names for error messages + globals: &'globals HashMap, &'core Term<'core>>, +} + +impl<'core, 'globals> Ctx<'core, 'globals> { + pub fn push_local(&mut self, name: &'core str, ty_val: value::Value<'core>) { + self.env.push(value::Value::Rigid(self.lvl)); // variable at this level + self.types.push(ty_val); + self.names.push(name); + self.lvl = self.lvl.succ(); + } + + pub fn pop_local(&mut self) { + self.env.pop(); + self.types.pop(); + self.names.pop(); + self.lvl = Lvl(self.lvl.0 - 1); + } + + pub fn type_of(&self, term: &Term) -> value::Value<'_> { + match term { + Term::Var(Ix(i)) => { + let stack_pos = self.types.len() - 1 - i; + self.types[stack_pos].clone() + } + // ... other cases + } + } +} +``` + +### Dependent Type Checking Example + +```rust +// Check a multi-argument application +fn check_app(ctx: &Ctx, app: &App) -> Result { + let func_type = type_of(ctx, app.func)?; + + let mut pi_val = func_type; + let mut checked_args = Vec::new(); + + for arg_term in app.args { + let Value::Pi(vpi) = pi_val else { + bail!("too many arguments"); + } + + // Check argument against domain + check(ctx, arg_term, &vpi.domain)?; + let arg_val = eval(ctx.arena, ctx.globals, &ctx.env, arg_term); + checked_args.push(arg_term); + + // Advance return type by instantiating closure + pi_val = apply(ctx.arena, ctx.globals, &vpi.closure, arg_val); + } + + Ok(pi_val) // return type +} +``` + +No syntactic substitution; the dependent return type is computed by evaluating the Pi closure. + +## Code Value Index Shifting (Staging) + +When quoted object code is stored as `MetaVal::Code` and later reused in a different context, its De Bruijn indices must be adjusted. + +### The Problem + +```rust +// Code generated at depth 2 (x1 is Ix(0), x0 is Ix(1)) +let code = MetaVal::Code { + term: App { func: Global("mul"), args: [Var(Ix(0)), Var(Ix(1))] }, + depth: 2, +}; + +// Later, code is spliced at depth 4 (x3, x2, x1, x0 from innermost) +// Ix(0) still refers to x3 (innermost), not x1 +// Ix(1) still refers to x2, not x0 +// The indices are wrong! +``` + +### Solution: Shift Free Indices + +Store the creation depth with the code value. On splice, shift free variable indices: + +```rust +fn shift_free_ix<'out>( + arena: &'out Bump, + term: &'out Term<'out>, + shift: usize, + cutoff: usize, +) -> &'out Term<'out> { + match term { + Term::Var(Ix(i)) => { + if i >= cutoff { + arena.alloc(Term::Var(Ix(i + shift))) + } else { + arena.alloc(term.clone()) + } + } + Term::Lam(lam) => { + // Shift continues into the body (free vars are those >= cutoff) + let new_body = shift_free_ix(arena, lam.body, shift, cutoff); + // ... reconstruct lam + } + // ... other cases recursively apply shift + } +} + +// On splice: +let depth_delta = current_depth - creation_depth; +let shifted_term = shift_free_ix(arena, code_term, depth_delta, 0); +``` + +**Key insight:** Only "free" variables (those at `Ix >= cutoff`) are shifted. Bound variables (introduced by the code itself) are not affected — only the indices in the code that refer to enclosing context. + +## See Also + +- **Reference Implementation:** elaboration-zoo, branch 01-eval-closures-debruijn +- **Paper:** Kovács 2022, Staged Compilation with Two-Level Type Theory (ICFP) +- **Related:** [pi_types.md](pi_types.md) for grammar and examples diff --git a/docs/bs/pi_types.md b/docs/bs/pi_types.md index 96fd2d1..8d742d1 100644 --- a/docs/bs/pi_types.md +++ b/docs/bs/pi_types.md @@ -111,64 +111,121 @@ Multi-argument calls desugar to curried application: `f(a, b)` = `f(a)(b)`. ## Core IR Design -### New Term variants +### Term Representation ```rust -Pi { param_name: &'a str, param_ty: &'a Term<'a>, body_ty: &'a Term<'a> } -Lam { param_name: &'a str, param_ty: &'a Term<'a>, body: &'a Term<'a> } -FunApp { func: &'a Term<'a>, arg: &'a Term<'a> } +// Variables use De Bruijn indices (count from nearest binder, 0 = innermost) +Term::Var(Ix) + +// Pi types support variadic (multi-parameter) syntax +Pi { params: &'a [(&'a str, &'a Term<'a>)], body_ty: &'a Term<'a>, phase: Phase } + +// Lambdas similarly support variadic parameters +Lam { params: &'a [(&'a str, &'a Term<'a>)], body: &'a Term<'a> } + +// Application handles variadic calls +App { func: &'a Term<'a>, args: &'a [&'a Term<'a>] } + +// Global references are terms, not application heads Global(Name<'a>) -PrimApp { prim: Prim, args: &'a [&'a Term<'a>] } ``` -### Refactoring App/Head +**Variadic Design.** Pi and Lam now carry a parameter list rather than single parameter. This preserves arity information and enables proper multi-argument application: +- `fn(x: u64, y: u64) -> u64` is a single Pi with 2 params, not nested Pi types. +- Application checking evaluates the domain type, checks the argument, then advances to the next param (via closure instantiation). -The current `App { head: Head, args }` where `Head` is `Global(Name) | Prim(Prim)` is replaced by: +**Phase field.** The Pi carries a `phase: Phase` distinguishing meta-level (`Phase::Meta`, printed as `fn`) from object-level (`Phase::Object`, printed as `code fn`) function types. -- **`Global(Name)`** — a term representing a reference to a top-level function. Now a first-class term rather than just an application head. -- **`FunApp { func, arg }`** — single-argument curried application. Used for both global and local function calls. Multi-arg calls `foo(a, b)` elaborate to `FunApp(FunApp(Global("foo"), a), b)`. -- **`PrimApp { prim, args }`** — primitive operation application. Kept separate because prims carry resolved `IntType` and are always fully applied. Eventually prims will become regular typed symbols, but the typechecker isn't ready for that yet. +### Substitution → Normalization by Evaluation (NbE) -**`FunSig` is preserved** as a convenience structure in the globals table. It stores the flat parameter list and return type for efficient lookup. A `FunSig::to_pi_type(arena)` method constructs the corresponding nested Pi type when needed (e.g., for `type_of(Global(name))`). +**Removed:** Syntactic substitution (`fn subst(...)`) is **deleted**. It had a critical variable-capture bug when the replacement contained binders. -### Substitution +**New approach:** The type checker uses **Normalization by Evaluation** to handle dependent types. Instead of rewriting syntax, the checker maintains a **semantic domain** (`Value`) and evaluates types in context. Dependent function arguments are checked by: -Dependent return types require substitution: `B[arg/x]`. Since the core IR uses De Bruijn levels, substitution replaces `Var(lvl)` with the argument term. Levels do not shift, making the implementation straightforward: +1. Evaluating the Pi type in the current environment to obtain a semantic `VPi`. +2. Checking the argument against the evaluated domain type. +3. Instantiating the Pi's closure with the evaluated argument to get the return type. -```rust -fn subst<'a>(arena: &'a Bump, term: &'a Term<'a>, lvl: Lvl, replacement: &'a Term<'a>) -> &'a Term<'a> -``` +See `docs/bs/nbe_and_debruijn.md` for complete details on the semantic domain and evaluation. ### Alpha-equivalence -The current `PartialEq` on `Term` compares structurally, including `param_name` fields. Two Pi types that differ only in parameter names (`fn(x: A) -> B` vs `fn(y: A) -> B`) should be equal. A dedicated `alpha_eq` function ignores names and compares only structure (De Bruijn levels handle binding correctly). +Two terms are alpha-equivalent if they are structurally identical under De Bruijn indices (parameter names are irrelevant). The `alpha_eq` function in `core/alpha_eq.rs` performs structural comparison. With De Bruijn indices, this is a straightforward recursive check — renaming machinery is unnecessary. -## Evaluator Design +## Type Checker NbE -### Closures +### Semantic Domain (core/value.rs) -A new `MetaVal` variant captures the environment at lambda creation: +The type checker maintains semantic values separate from syntax to enable normalization: ```rust -VClosure { - param_name: &str, - body: &Term, - env: Vec, - obj_next: Lvl, +pub enum Value<'a> { + // Neutrals (cannot reduce) + Rigid(Lvl), // local variable (De Bruijn level) + Global(&'a str), // global function reference + Prim(Prim), // primitive operation + App(&'a Value<'a>, &'a [Value<'a>]), // application + + // Canonical forms + Lit(u64), // literal + Lam(VLam<'a>), // lambda with closure + Pi(VPi<'a>), // Pi type with closure + Lift(&'a Value<'a>), // lifted type + Quote(&'a Value<'a>), // quoted code +} + +pub struct VLam<'a> { + pub name: &'a str, + pub param_ty: &'a Value<'a>, + pub closure: Closure<'a>, +} + +pub struct VPi<'a> { + pub name: &'a str, + pub domain: &'a Value<'a>, + pub closure: Closure<'a>, + pub phase: Phase, +} + +pub struct Closure<'a> { + pub env: &'a [Value<'a>], // snapshot of evaluation environment + pub body: &'a Term<'a>, // unevaluated body term } ``` -This follows the substitution-based approach already in use. Application extends the captured env with the argument value and evaluates the body. +### Key Operations + +**`eval(arena, globals, env, term) -> Value`:** Interpret a term in an environment. +- `Var(Ix(i))`: index into `env[env.len() - 1 - i]` (convert index to stack position). +- `Lam` / `Pi`: create a closure by snapshotting `env` to the arena and pairing it with the body. +- Other forms: recursively evaluate or return as neutrals. + +**`apply(arena, globals, closure, arg) -> Value`:** Instantiate a closure with an argument. +- Clone the closure's environment, push the argument, evaluate the body. + +**`quote(arena, depth, value) -> &'a Term`:** Convert a value back to term syntax. +- `Rigid(lvl)`: convert level to index using `lvl_to_ix(depth, lvl)`. +- For `Lam` / `Pi`: apply the closure to a fresh variable, recursively quote the result. + +### Dependent Type Checking + +When checking a multi-argument application: -### Global function references +1. Evaluate the function's type to get `Value::Pi(vpi)`. +2. Check the first argument against `vpi.domain`. +3. Evaluate the argument to a value. +4. **Instantiate the Pi's closure** with the evaluated argument: `apply(closure, arg_value)` yields the type of remaining args or return type. +5. Repeat for each argument. -When `eval_meta` encounters `Global(name)`, it constructs a closure from the global's body and parameters. When applied via `FunApp`, this closure behaves identically to a lambda — the argument extends the env and the body is evaluated. +This replaces syntactic substitution and eliminates variable capture bugs. -For multi-parameter globals, partial application produces a closure that awaits the remaining arguments. This falls out naturally from curried `FunApp` chains. +### Distinction from Staging Evaluator -### Pi types in evaluation +The type checker's NbE and the staging evaluator (`eval/mod.rs`) are separate: +- **Type checker NbE** uses a unified `Value` domain to normalize types during elaboration. +- **Staging evaluator** uses separate `Val0`/`Val1` domains to partition meta/object computation and produce object code. -`Pi` terms are type-level and never appear in evaluation position (the typechecker ensures this). They are unreachable in `eval_meta`. +Both use `Closure { env, body }` pattern for closures, but serve different purposes and cannot be unified. ## Staging Interaction From c9e4233e26d90b7375f70273e7f5e8846a4826ac Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 13:02:18 +0000 Subject: [PATCH 20/43] docs: add documentation indices and guidelines - Create docs/README.md: index of language design and implementation docs - Update docs/bs/README.md: add new nbe_and_debruijn.md and clarify pi_types.md - Update CLAUDE.md: add Documentation section with guidance to keep indices up to date when adding new files Co-Authored-By: Claude Haiku 4.5 --- docs/README.md | 11 +++++++++++ docs/bs/README.md | 2 ++ 2 files changed, 13 insertions(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b5d838e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,11 @@ +# Splic Documentation + +## Language Design & Specification + +- [INTRO.md](INTRO.md) — Introduction to Splic and two-level type theory (2LTT) +- [SYNTAX.md](SYNTAX.md) — Language syntax and user-facing features +- [prototype.md](prototype.md) — Early prototype design notes + +## Implementation & Architecture + +For detailed compiler architecture, type checking, and implementation decisions, see [bs/README.md](bs/README.md). diff --git a/docs/bs/README.md b/docs/bs/README.md index 2a67da1..01b82f0 100644 --- a/docs/bs/README.md +++ b/docs/bs/README.md @@ -19,6 +19,8 @@ The folder name, `bs`, stands for brainstorming. Obviously. - [prototype_core.md](prototype_core.md) — Prototype core IR design decisions - [self_typed_ir.md](self_typed_ir.md) — Self-typed core IR and a future `type_of` method - [prototype_eval.md](prototype_eval.md) — Evaluator design and implementation sequence (substitution → spines → dependent types) +- [nbe_and_debruijn.md](nbe_and_debruijn.md) — Normalization by Evaluation, De Bruijn indices vs levels, free variable index shifting in staging +- [pi_types.md](pi_types.md) — Dependent function types (Pi) and lambdas at the meta level (implementation details and NbE type checking) ## Roadmap, Strategy & Process From cdca7305e37107b86a77580a9d5f86d5555c6c7b Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 13:04:59 +0000 Subject: [PATCH 21/43] docs: update AGENTS.md --- AGENTS.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7a544e0..8f7acb8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,13 +92,19 @@ The project enforces a curated set of lints beyond Clippy defaults — see `[wor - Quotations (`#(e)`, `#{...}`) and splices (`$(e)`, `${...}`) for metaprogramming - Lifting with `[[e]]` +## Documentation + +Splic documentation is organized in two main locations: + +- **`docs/README.md`** — Overview and index of language design and user-facing docs (CONCEPT, SYNTAX, examples) +- **`docs/bs/README.md`** — Index of implementation notes, proposals, and architecture documentation + +**Keep doc indices up to date:** When adding new documentation files, add entries to the appropriate `README.md` with a brief description. This helps navigate the documentation. + ## Language Design Splic is built on **two-level type theory (2LTT)**: - **Meta-level**: Purely functional dependently typed language - **Object-level**: Low-level language for zkvm bytecode - Connected through quotations and splices for type-safe metaprogramming - -See `docs/CONCEPT.md` and `docs/SYNTAX.md` for detailed language specifications. - -For compiler architecture details (NbE, De Bruijn representation, dependent types), see `docs/bs/nbe_and_debruijn.md` and `docs/bs/pi_types.md`. +- See the 2ltt skill for more detail From ebb63ad5342dc9248c687e82267f7b2e884f1269 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 13:19:21 +0000 Subject: [PATCH 22/43] refactor: use val_type_of in tests, remove type_of wrapper method - Update all test assertions to use val_type_of directly instead of the type_of wrapper, working directly with semantic Value domain - Remove type_of public method from Ctx (was only quoting val_type_of) - Update all test pattern matches to use Value variants instead of Term variants (e.g. Value::Prim(...) instead of Term::Prim(...)) - Correct assertion for universe type: use Value::U(Phase) not Value::Prim - Tests now work with the semantic domain directly, more faithful to actual implementation and cleaner architecture Co-Authored-By: Claude Haiku 4.5 --- compiler/src/checker/mod.rs | 6 ------ compiler/src/checker/test/apply.rs | 18 +++++++++--------- compiler/src/checker/test/locals.rs | 6 +++--- compiler/src/checker/test/meta.rs | 20 ++++++++++---------- compiler/src/checker/test/var.rs | 12 ++++++------ 5 files changed, 28 insertions(+), 34 deletions(-) diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index 9458f1a..0562a5f 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -364,12 +364,6 @@ impl<'core, 'globals> Ctx<'core, 'globals> { } } } - - /// Recover the type of an already-elaborated core term as a `&Term` (quoted). - pub fn type_of(&self, term: &'core core::Term<'core>) -> &'core core::Term<'core> { - let val = self.val_type_of(term); - self.quote_val(&val) - } } /// Resolve a built-in type name to a static core term, using `phase` for integer types. diff --git a/compiler/src/checker/test/apply.rs b/compiler/src/checker/test/apply.rs index 03d2175..bdebb74 100644 --- a/compiler/src/checker/test/apply.rs +++ b/compiler/src/checker/test/apply.rs @@ -16,10 +16,10 @@ fn infer_global_call_no_args_returns_ret_ty() { args: &[], }); let result = infer(&mut ctx, Phase::Meta, term).expect("should infer"); - let ty = ctx.type_of(result); + let ty_val = ctx.val_type_of(result); assert!(matches!( - ty, - core::Term::Prim(Prim::IntTy(IntType { + ty_val, + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U64, .. })) @@ -100,10 +100,10 @@ fn infer_global_call_with_arg_checks_arg_type() { args, }); let result = infer(&mut ctx, Phase::Meta, term).expect("should infer"); - let ty = ctx.type_of(result); + let ty_val = ctx.val_type_of(result); assert!(matches!( - ty, - core::Term::Prim(Prim::IntTy(IntType { + ty_val, + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U64, .. })) @@ -167,7 +167,7 @@ fn infer_comparison_op_returns_u1() { // Eq is inferable: result is u1, prim carries the operand type (u64). let core_term = infer(&mut ctx, Phase::Object, term).expect("should infer"); - let ty = ctx.type_of(core_term); + let ty_val = ctx.val_type_of(core_term); assert!(matches!( core_term, core::Term::App(core::App { @@ -179,8 +179,8 @@ fn infer_comparison_op_returns_u1() { }) )); assert!(matches!( - ty, - core::Term::Prim(Prim::IntTy(IntType { + ty_val, + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U1, .. })) diff --git a/compiler/src/checker/test/locals.rs b/compiler/src/checker/test/locals.rs index 0fdc6af..323f776 100644 --- a/compiler/src/checker/test/locals.rs +++ b/compiler/src/checker/test/locals.rs @@ -21,10 +21,10 @@ fn infer_let_annotated_infers_body_type() { let block = src_arena.alloc(ast::Term::Block { stmts, expr: body }); let result = infer(&mut ctx, Phase::Meta, block).expect("should infer"); - let ty = ctx.type_of(result); + let ty_val = ctx.val_type_of(result); assert!(matches!( - ty, - core::Term::Prim(Prim::IntTy(IntType { + ty_val, + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U32, .. })) diff --git a/compiler/src/checker/test/meta.rs b/compiler/src/checker/test/meta.rs index 4d932fb..a0544ba 100644 --- a/compiler/src/checker/test/meta.rs +++ b/compiler/src/checker/test/meta.rs @@ -15,8 +15,8 @@ fn infer_lift_of_object_type_returns_type_universe() { // Elaborated at meta phase: type of [[u64]] is Type (meta universe) let result = infer(&mut ctx, Phase::Meta, term).expect("should infer"); - let ty = ctx.type_of(result); - assert!(matches!(ty, core::Term::Prim(Prim::U(Phase::Meta)))); + let ty_val = ctx.val_type_of(result); + assert!(matches!(ty_val, value::Value::U(Phase::Meta))); } // `[[u64]]` is illegal at object phase — Lift is only meaningful in meta context. @@ -82,10 +82,10 @@ fn infer_quote_of_global_call_returns_lifted_type() { // Checked at meta phase; result type should be [[u64]] let core_term = infer(&mut ctx, Phase::Meta, term).expect("should infer"); - let ty = ctx.type_of(core_term); + let ty_val = ctx.val_type_of(core_term); assert!(matches!(core_term, core::Term::Quote(_))); // Type is Lift(u64) - assert!(matches!(ty, core::Term::Lift(_))); + assert!(matches!(ty_val, value::Value::Lift(_))); } // `#(...)` at object phase is illegal — Quote is only meaningful in meta context. @@ -175,11 +175,11 @@ fn infer_splice_of_lifted_var_returns_inner_type() { // splice is checked at object phase let core_term = infer(&mut ctx, Phase::Object, term).expect("should infer"); - let ty = ctx.type_of(core_term); + let ty_val = ctx.val_type_of(core_term); assert!(matches!(core_term, core::Term::Splice(_))); assert!(matches!( - ty, - core::Term::Prim(Prim::IntTy(IntType { + ty_val, + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U64, .. })) @@ -220,11 +220,11 @@ fn infer_splice_of_meta_int_succeeds() { // $(x) at object phase: result type is u32 at object phase. let core_term = infer(&mut ctx, Phase::Object, term).expect("should infer"); - let ty = ctx.type_of(core_term); + let ty_val = ctx.val_type_of(core_term); assert!(matches!(core_term, core::Term::Splice(_))); assert!(matches!( - ty, - core::Term::Prim(Prim::IntTy(IntType { + ty_val, + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U32, phase: Phase::Object, })) diff --git a/compiler/src/checker/test/var.rs b/compiler/src/checker/test/var.rs index e648d4c..6dee0c3 100644 --- a/compiler/src/checker/test/var.rs +++ b/compiler/src/checker/test/var.rs @@ -13,12 +13,12 @@ fn infer_var_in_scope_returns_its_type() { let term = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let core_term = infer(&mut ctx, Phase::Meta, term).expect("should infer"); - let ty = ctx.type_of(core_term); + let ty_val = ctx.val_type_of(core_term); // With one local "x", infer returns Var(Ix(0)) — innermost (only) binder. assert!(matches!(core_term, core::Term::Var(Ix(0)))); assert!(matches!( - ty, - core::Term::Prim(Prim::IntTy(IntType { + ty_val, + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U32, .. })) @@ -66,12 +66,12 @@ fn infer_var_shadowed_returns_innermost() { let term = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let core_term = infer(&mut ctx, Phase::Meta, term).expect("should infer"); - let ty = ctx.type_of(core_term); + let ty_val = ctx.val_type_of(core_term); // Innermost "x" is at Ix(0). assert!(matches!(core_term, core::Term::Var(Ix(0)))); assert!(matches!( - ty, - core::Term::Prim(Prim::IntTy(IntType { + ty_val, + value::Value::Prim(Prim::IntTy(IntType { width: IntWidth::U32, .. })) From f6c8dd27f129e50873bd8d952c36d940f3f6340a Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 14:12:08 +0000 Subject: [PATCH 23/43] docs: reduce implementation-specific details, focus on concepts - Simplify prototype_eval.md section 6: generalize literal type handling without mentioning specific type_of API - Rewrite self_typed_ir.md: focus on semantic vs syntactic type design decision rather than implementation details that drift - Simplify nbe_and_debruijn.md: replace detailed code samples with high-level descriptions of eval/apply/quote/type-checker integration - Emphasize architectural principles over specific function signatures and parameter names, making docs more resilient to implementation changes Co-Authored-By: Claude Haiku 4.5 --- docs/bs/nbe_and_debruijn.md | 297 +++++------------------------------- docs/bs/prototype_eval.md | 15 +- docs/bs/self_typed_ir.md | 129 ++++++---------- 3 files changed, 92 insertions(+), 349 deletions(-) diff --git a/docs/bs/nbe_and_debruijn.md b/docs/bs/nbe_and_debruijn.md index fa83c32..71bcdb4 100644 --- a/docs/bs/nbe_and_debruijn.md +++ b/docs/bs/nbe_and_debruijn.md @@ -115,282 +115,63 @@ pub struct Closure<'a> { **Key insight:** Closures pair an **environment snapshot** (arena-allocated slice) with an **unevaluated term**. When instantiated, the environment is extended and the body is evaluated. -### Evaluation (eval function) +### Evaluation -```rust -pub fn eval<'a>( - arena: &'a Bump, - globals: &HashMap, &'a Term<'a>>, - env: &Env<'a>, - term: &'a Term<'a>, -) -> Value<'a> { - match term { - // Variable: convert index to stack position - Term::Var(Ix(i)) => { - let stack_pos = env.len() - 1 - i; - env[stack_pos].clone() - } - - // Neutral references - Term::Global(name) => Value::Global(*name), - Term::Prim(p) => Value::Prim(*p), - Term::Lit(n) => Value::Lit(*n), - - // Lambda: create closure by snapshotting environment - Term::Lam(lam) if lam.params.is_empty() => { - eval(arena, globals, env, lam.body) - } - Term::Lam(lam) => { - let (name, ty) = lam.params[0]; - let param_ty = eval(arena, globals, env, ty); - let rest_body = if lam.params.len() == 1 { - lam.body - } else { - // Slice to remaining params (zero-copy) - arena.alloc(Term::Lam(Lam { - params: &lam.params[1..], - body: lam.body, - })) - }; - Value::Lam(VLam { - name, - param_ty: Box::new(param_ty), - closure: Closure { - env: arena.alloc_slice_fill_iter(env.iter().cloned()), - body: rest_body, - }, - }) - } - - // Pi: similar to Lam - Term::Pi(pi) if pi.params.is_empty() => { - eval(arena, globals, env, pi.body_ty) - } - Term::Pi(pi) => { - let (name, ty) = pi.params[0]; - let domain = eval(arena, globals, env, ty); - let rest_body = if pi.params.len() == 1 { - pi.body_ty - } else { - arena.alloc(Term::Pi(Pi { - params: &pi.params[1..], - body_ty: pi.body_ty, - phase: pi.phase, - })) - }; - Value::Pi(VPi { - name, - domain: Box::new(domain), - closure: Closure { - env: arena.alloc_slice_fill_iter(env.iter().cloned()), - body: rest_body, - }, - phase: pi.phase, - }) - } +The evaluator interprets terms in an environment, producing semantic values: - // Application - Term::App(app) => { - let func_val = eval(arena, globals, env, app.func); - let arg_vals: Vec<_> = app.args.iter() - .map(|arg| eval(arena, globals, env, arg)) - .collect(); - apply_many(arena, globals, func_val, arg_vals) - } +**Key principles**: +- **Variables** are converted from indices to stack positions via environment lookup. +- **Lambdas and Pi types** create closures by snapshotting the current environment. +- **Applications** apply a function value to arguments, evaluating on-demand. +- **Let bindings** are eagerly evaluated: extend the environment with the let-bound value and continue. +- **Lift/Quote/Splice** are reduced according to staging rules. +- **Match** performs pattern matching at the semantic level. - // Let binding - Term::Let(let_) => { - let val = eval(arena, globals, env, let_.expr); - let mut env2 = env.clone(); - env2.push(val); - eval(arena, globals, &env2, let_.body) - } +**Variadic handling**: Multi-parameter lambdas and Pi types are curried by slicing, not duplicating: +remaining parameters are encoded as a sub-term, avoiding allocation. - // Quoted/lifted/spliced code - Term::Quote(inner) => Value::Quote(Box::new(eval(arena, globals, env, inner))), - Term::Lift(inner) => Value::Lift(Box::new(eval(arena, globals, env, inner))), - Term::Splice(inner) => { - // Splice unwraps a Quote; otherwise stays as application - let inner_val = eval(arena, globals, env, inner); - if let Value::Quote(inner) = inner_val { - // Splice of a Quote: splice splices away to get the inner term, quote-splice - // For now: pass through - Value::Quote(*inner) - } else { - // Splice of non-quote: leave as application (staging will handle it) - Value::App(Box::new(inner_val), &[]) - } - } +### Closure Instantiation - Term::Match(match_) => { - let scrutinee_val = eval(arena, globals, env, match_.scrutinee); - // Pattern match in semantic domain - for arm in match_.arms { - if matches_arm(&arm.pat, &scrutinee_val) { - match &arm.pat { - Pat::Bind(name) => { - let mut env2 = env.clone(); - env2.push(scrutinee_val); - return eval(arena, globals, &env2, arm.body); - } - Pat::Lit(_) | Pat::Wildcard => { - return eval(arena, globals, env, arm.body); - } - } - } - } - unreachable!("match non-exhaustive") - } - } -} -``` +To apply a closure to an argument: -### Closure Instantiation (apply) +1. Restore the closure's environment from its snapshot. +2. Extend it with the argument value. +3. Evaluate the body in the extended environment. -```rust -pub fn apply<'a>( - arena: &'a Bump, - globals: &HashMap, &'a Term<'a>>, - closure: &Closure<'a>, - arg: Value<'a>, -) -> Value<'a> { - let mut env = closure.env.to_vec(); // Clone snapshot back to mutable vector - env.push(arg); - eval(arena, globals, &env, closure.body) -} +For multi-argument applications, apply repeatedly: each application produces a value that may itself be a lambda (closure), ready for the next argument. -pub fn apply_many<'a>( - arena: &'a Bump, - globals: &HashMap, &'a Term<'a>>, - func: Value<'a>, - args: Vec>, -) -> Value<'a> { - match func { - Value::Lam(vlam) => { - let mut result = apply(arena, globals, &vlam.closure, args[0].clone()); - for arg in args.iter().skip(1) { - if let Value::Lam(vlam) = result { - result = apply(arena, globals, &vlam.closure, arg.clone()); - } else { - // Not a lambda; application sticks - return Value::App(Box::new(result), arena.alloc_slice_copy(&args[1..])); - } - } - result - } - other => Value::App(Box::new(other), arena.alloc_slice_copy(&args)), - } -} -``` +If application gets stuck (callee is not a lambda), the application becomes neutral (`Value::App`). -### Quotation (quote) +### Quotation Convert values back to term syntax. Used for error reporting, output, and type comparison. -```rust -pub fn quote<'a>( - arena: &'a Bump, - depth: Lvl, - val: &Value<'a>, -) -> &'a Term<'a> { - match val { - Value::Rigid(lvl) => { - // Convert level to index - let ix = lvl_to_ix(depth, *lvl); - arena.alloc(Term::Var(ix)) - } - - Value::Global(name) => arena.alloc(Term::Global(*name)), - Value::Prim(p) => arena.alloc(Term::Prim(*p)), - Value::Lit(n) => arena.alloc(Term::Lit(*n)), - - Value::Lam(vlam) => { - // Apply closure to fresh variable at current depth - let fresh = Value::Rigid(depth); - let body_val = apply(arena, globals, &vlam.closure, fresh); - let body_term = quote(arena, depth.succ(), &body_val); - - // Recover param info from VLam - let param_ty_term = quote(arena, depth, vlam.param_ty); - arena.alloc(Term::Lam(Lam { - params: arena.alloc_slice_copy(&[(vlam.name, param_ty_term)]), - body: body_term, - })) - } - - Value::Pi(vpi) => { - let fresh = Value::Rigid(depth); - let body_val = apply(arena, globals, &vpi.closure, fresh); - let body_term = quote(arena, depth.succ(), &body_val); - - let domain_term = quote(arena, depth, vpi.domain); - arena.alloc(Term::Pi(Pi { - params: arena.alloc_slice_copy(&[(vpi.name, domain_term)]), - body_ty: body_term, - phase: vpi.phase, - })) - } - - Value::App(func, args) => { - let qfunc = quote(arena, depth, func); - let qargs: Vec<_> = args.iter().map(|a| quote(arena, depth, a)).collect(); - arena.alloc(Term::App(App { - func: qfunc, - args: arena.alloc_slice_copy(&qargs), - })) - } - - Value::Lift(inner) => { - arena.alloc(Term::Lift(quote(arena, depth, inner))) - } - - Value::Quote(inner) => { - arena.alloc(Term::Quote(quote(arena, depth, inner))) - } - } -} -``` +**Key operations**: +- **Rigid variables** are converted from levels to indices using `lvl_to_ix(depth, lvl)`. +- **Globals, prims, and literals** are reconstructed directly. +- **Lambdas and Pi types** are reconstructed by: + 1. Applying the closure to a fresh variable at the current depth. + 2. Recursively quoting the result at the next depth. + 3. Storing the original parameter information (name, type). +- **Applications** are reconstructed by quoting the function and arguments. +- **Lift/Quote/Splice** are reconstructed structurally. ## Type Checker Integration -The type checker (`checker/mod.rs`) maintains a context with an **evaluation environment**: - -```rust -pub struct Ctx<'core, 'globals> { - arena: &'core Bump, - env: value::Env<'core>, // evaluation environment (Values) - types: Vec>, // type of each local - lvl: Lvl, // current depth - names: Vec<&'core str>, // names for error messages - globals: &'globals HashMap, &'core Term<'core>>, -} +The type checker maintains a context with: -impl<'core, 'globals> Ctx<'core, 'globals> { - pub fn push_local(&mut self, name: &'core str, ty_val: value::Value<'core>) { - self.env.push(value::Value::Rigid(self.lvl)); // variable at this level - self.types.push(ty_val); - self.names.push(name); - self.lvl = self.lvl.succ(); - } +- **Evaluation environment** (`env`): Values of bound variables, indexed by De Bruijn level. +- **Type environment** (`types`): Semantic type of each bound variable. +- **Current depth** (`lvl`): How many binders we are under. +- **Name tracking** (`names`): Variable names for error messages (not used in lookups). +- **Globals table** (`globals`): Top-level function types. - pub fn pop_local(&mut self) { - self.env.pop(); - self.types.pop(); - self.names.pop(); - self.lvl = Lvl(self.lvl.0 - 1); - } +When a local variable is bound: +1. Push a fresh rigid value at the current level. +2. Push its type (as a semantic value) into the type environment. +3. Increment the depth. - pub fn type_of(&self, term: &Term) -> value::Value<'_> { - match term { - Term::Var(Ix(i)) => { - let stack_pos = self.types.len() - 1 - i; - self.types[stack_pos].clone() - } - // ... other cases - } - } -} -``` +This way, type information flows as semantic values throughout checking, and variable lookup is O(1) indexing. ### Dependent Type Checking Example diff --git a/docs/bs/prototype_eval.md b/docs/bs/prototype_eval.md index 42153d8..a9c7258 100644 --- a/docs/bs/prototype_eval.md +++ b/docs/bs/prototype_eval.md @@ -159,16 +159,17 @@ When applying a lambda, push the argument onto a spine; only force evaluation wh ### 6. Literal Type Annotation — Defer Until Normalizer ✅ -**Decision**: Leave `Lit(u64)` as-is for now. Add width annotation (`Lit(u64, IntType)`) -when implementing the normalizer for dependent types. +**Decision**: During elaboration, the type of literals is provided by context. Type recovery from +syntax alone is deferred until the normalizer is implemented (when dependent types are added). **Rationale**: -- Currently not needed; `check` provides the expected type. -- When a `type_of(term) -> Value` function is needed (for the normalizer), `Lit` is one of the few variants that can't self-type. -- The change is minimal (one line to IR, one line to elaborator). -- The `IntType` is already in hand at the elaboration site (it's in the `expected` type passed to `check`), so adding it costs nothing. +- During elaboration, literals are checked against an expected type provided by the caller. +- Most term variants carry enough information for type recovery (self-typed); literals are the exception. +- Once NbE is implemented and types become semantic values, the need to recover types from syntax diminishes. +- Adds no runtime cost (the type information is already in hand during elaboration). -**Timeline**: Add after refactoring to spines, before dependent types. +**Implementation**: Currently handled via context-threaded types. Will be revisited if IR redesign +is needed for dependent types. --- diff --git a/docs/bs/self_typed_ir.md b/docs/bs/self_typed_ir.md index 3a600a1..6dee39a 100644 --- a/docs/bs/self_typed_ir.md +++ b/docs/bs/self_typed_ir.md @@ -1,84 +1,45 @@ -# Self-typed core IR and `type_of` - -## Context - -During elaboration, `infer` currently returns a `(term, type)` pair where both -are `&'core core::Term<'core>`. The question was whether the elaborated IR could -be made *self-typed* — i.e. whether `type_of(term) -> Type` could be implemented -as a pure function on `Term` alone, removing the need to thread the type as a -second return value from `infer`. - -## Which variants are already self-typed - -Going through every `core::Term` variant: - -| Variant | Type recoverable? | Notes | -|---|---|---| -| `Prim(IntTy(it))` | Yes | `U(it.phase)` | -| `Prim(U(Meta))` | Yes | `U(Meta)` (type-in-type for meta) | -| `Prim(U(Object))` | Yes | `U(Meta)` (object universe classified by meta) | -| `Prim(Add(it))` / arithmetic | Yes | `IntTy(it)` — same type in and out | -| `Prim(Eq(it))` / comparisons | Yes | `IntTy(U1, it.phase)` | -| `Lift(inner)` | Yes | Always `U(Meta)` | -| `Quote(inner)` | Yes (one recursive step) | `Lift(type_of(inner))` | -| `Splice(inner)` | Yes (one recursive step) | The `T` inside `Lift(T)` | -| `Let { ty, body, .. }` | Yes (one step into `body`) | `type_of(body)` | -| `Match { arms, .. }` | Yes (one step into first arm body) | `type_of(arms[0].body)`; all arms guaranteed same type post-elaboration | -| `App { head: Prim, .. }` | Yes | Same as `Prim` case above | -| `Lit(u64)` | **No** | Width was fixed by the `check` call; not stored in the node | -| `Var(Lvl)` | **No** | Needs the locals context (a slice indexed by level) | -| `App { head: Global(name), .. }` | **No** | Needs the globals table to look up `ret_ty` | - -So three variants are not self-contained: `Lit`, `Var`, and `App/Global`. - -## What the Kovács reference implementation does - -The reference Haskell implementation (`https://github.com/AndrasKovacs/staged`) -does not attempt to make the syntactic `Tm` self-typed either. `infer` returns -`IO (Tm, VTy, Stage)` — a triple of the elaborated term, its **semantic value -type** (`VTy = Val`), and its stage. The type is threaded as a separate return -value throughout, not stored in the term. - -The key design point is the `Tm` / `Val` split: - -- **`Tm`** is the post-elaboration AST, using De Bruijn *indices*. Lambdas store - a plain `Tm` body. -- **`Val`** is the result of evaluation. Lambdas become closures (`Val -> Val`), - eliminating substitution. Variables become neutral spines (`VRigid Lvl Spine` / - `VFlex MetaVar Spine`) using De Bruijn *levels*. `Let` is evaluated away - immediately. Two terms are definitionally equal iff their `Val`s quote back to - the same `Tm` (normalisation by evaluation). - -Type-checking works with `Val` types throughout; `quote` converts back to `Tm` -when a syntactic form is needed (e.g. to store in the elaborated tree). - -## Decision: keep the `(term, type)` pair for now - -The current `infer :: … -> Result<(&'core Term, &'core Term)>` is the right shape -for now: - -1. No structural change to `core::Term` is needed. -2. When dependent types and a normaliser arrive, the signature will naturally - become `infer :: … -> Result<(&'core Term, Value)>` — matching the reference - impl's `(Tm, VTy)` — and the transition is straightforward. - -## Future direction: `type_of` on a self-typed IR - -Eventually it may make sense for `infer` to return just `&'core Term`, with the -type recoverable on demand via `type_of(term, ctx) -> Value`. The elaborated term -would carry enough information that `type_of` is always cheap (O(1) field read or -a single eval step), with the context needed only for the `Var` lookup. - -The only IR change required to reach that point is: - -``` -Lit(u64) → Lit(u64, IntType) -``` - -`IntType` is already in hand at the elaboration site (it is the `IntTy` inside -the `expected` type passed to `check`), so adding it costs nothing. All other -variants are already self-typed once `Var` has access to the locals context. - -`Var` and `App/Global` still need external context for `type_of`, but that -context is a cheap slice/map lookup by level/name and does not require -re-elaboration. +# IR Design: Syntactic vs Semantic Types + +## Overview + +The compiler maintains two type representations: + +- **Syntactic types** (`&Term`): Used in the elaborated IR, threaded through elaboration. +- **Semantic types** (`Value`): Computed during type checking via Normalization by Evaluation (NbE), used internally for dependent type checking. + +This document explains the design rationale and when each is used. + +## Design Decision: Thread Types Through Elaboration + +**Current approach**: The elaborator threads types as semantic values, not as syntax. + +**Rationale**: +- Most term variants carry enough information for type recovery (they are "self-typed"). +- A few variants (`Var`, `Global`, `Lit`) require external context (locals, globals, or check-provided info). +- With NbE implemented, maintaining semantic types is natural and enables correct dependent type checking. +- The semantic domain (`Value`) is the authoritative type representation; quoting values to syntax is for error messages and output. + +**Benefits**: +- No redundant type trees in the elaborated IR. +- Type correctness is maintained at elaboration time by the type checker. +- Simplifies dependent type checking: dependent arguments are checked by evaluating types as values and instantiating closures. + +## Type Recovery and Quotation + +For most term constructs, the type can be recovered from the term itself plus context: + +- **Prims and literals**: Type is known from the primitive itself. +- **Lambdas and Pi types**: Type is the domain type (from parameters) or body type (via closure instantiation). +- **Applications**: Type is the return type of the callee (recovered via function type evaluation). +- **Lift/Quote/Splice**: Types follow definitional rules (Lift has meta universe type, Quote wraps in Lift, etc.). + +When a syntactic type is needed (error messages, external APIs), it is produced by **quoting** semantic values back to terms. + +## Relationship to Reference Implementation + +The Kovács reference implementation uses a similar split: + +- **`Tm`** (syntax): Post-elaboration IR with De Bruijn indices. +- **`Val`** (semantics): Result of evaluation, used for type checking. + +Types flow through checking as semantic values; elaboration threads the elaborated term (not its type) forward. From 3a4cb274288b35777eac8f763cd15c182b7dd65c Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 14:13:35 +0000 Subject: [PATCH 24/43] docs: add guidelines for writing resilient documentation Emphasize focusing on concepts and design decisions rather than implementation-specific details that can drift as code evolves. Co-Authored-By: Claude Haiku 4.5 --- AGENTS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 8f7acb8..3400603 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -99,7 +99,9 @@ Splic documentation is organized in two main locations: - **`docs/README.md`** — Overview and index of language design and user-facing docs (CONCEPT, SYNTAX, examples) - **`docs/bs/README.md`** — Index of implementation notes, proposals, and architecture documentation -**Keep doc indices up to date:** When adding new documentation files, add entries to the appropriate `README.md` with a brief description. This helps navigate the documentation. +**Guidelines for writing docs:** +- Focus on architectural concepts and design decisions ("what" and "why") rather than implementation-specific details (function names, parameter types, exact APIs). This keeps docs resilient to code changes. +- Keep doc indices up to date: when adding new files, add entries to the appropriate `README.md` with a brief description. ## Language Design From 8ba3e81fbfffed4c5b523c7468656000954c4285 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 14:33:03 +0000 Subject: [PATCH 25/43] ai: reorganize 2ltt skill docs - move implementation content to dedicated guide Move implementation-specific sections (NbE, De Bruijn, references, glossary) from kovacs-2022-staged-compilation-2ltt.md to implementation-guide.md. The Kovacs file now contains only the paper extract, while implementation details belong in the dedicated practical guide. Co-Authored-By: Claude Haiku 4.5 --- .opencode/skills/2ltt/implementation-guide.md | 162 +++++++++++++++++ .../kovacs-2022-staged-compilation-2ltt.md | 164 +----------------- 2 files changed, 163 insertions(+), 163 deletions(-) diff --git a/.opencode/skills/2ltt/implementation-guide.md b/.opencode/skills/2ltt/implementation-guide.md index a942669..fac6176 100644 --- a/.opencode/skills/2ltt/implementation-guide.md +++ b/.opencode/skills/2ltt/implementation-guide.md @@ -239,3 +239,165 @@ Quality: - [ ] Add let-insertion (`Gen`) to prevent duplication. - [ ] Decide if you want closure-free object typing (`ValTy`/`CompTy`). - [ ] Decide if you want representation indices (KACC-style). + +--- + +## 9. Implementation Architecture: NbE + Staging + +Modern practical implementations use **Normalization by Evaluation (NbE)** for type checking and evaluation for staging. + +### Type Checker NbE (Kovács 2022 §3–4, elaboration-zoo 01-eval-closures-debruijn) + +The type checker maintains a **semantic domain** separate from syntax: + +```haskell +-- Haskell pseudocode (elaboration-zoo style) +data Value + = VRigid Lvl Spine -- stuck on a local variable + | VLam Name (Val -> Val) -- closure as a function + | VPi Name (Val -> Val) VTy -- dependent Pi with closure + | VLit Int + | VGlobal Name + | VLift Val + | VQuote Val + +-- Evaluation: interpret terms in an environment +eval :: Env Val -> Term -> Val +eval env (Var ix) = env !! (env.len - 1 - ix) -- index to stack +eval env (Lam x t) = VLam x (\v -> eval (v:env) t) +eval env (Pi x a b) = VPi x (\v -> eval (v:env) b) (eval env a) +eval env (App f args) = vApp (eval env f) (map (eval env) args) + +-- Quotation: convert value back to term (for errors, output) +quote :: Lvl -> Val -> Term +quote lvl (VRigid x sp) = quoteSp lvl (Var (lvl2Ix lvl x)) sp +quote lvl (VLam x t) = Lam x (quote (lvl+1) (t (VRigid lvl))) +quote lvl (VPi x a b) = Pi x (quote lvl a) (quote (lvl+1) (b (VRigid lvl))) +``` + +**Key design choices:** +- Terms use **De Bruijn indices** (count from nearest binder). +- Values use **De Bruijn levels** (count from outermost binder). +- Closures are functions `Val -> Val` in the metalanguage (or `Closure { env, body }` in Rust). +- No syntactic substitution — substitution is modeled via environment extension. + +**Why this works:** +- Indices are pure syntax — portable, no external state. +- Levels are the natural output of evaluation — fresh variables are just the current depth. +- Closures capture the evaluation environment, eliminating variable-capture bugs. + +### Staging Evaluator (Separate system) + +The **staging evaluator** is a different system that compiles meta code and produces the object program: + +```haskell +-- Two separate value types +data Val0 = V0Lit Int | V0App Name [Val0] | V0Code Term | ... +data Val1 = V1Lam (Val0 -> Val1) | V1Lit Int | ... + +-- Two separate evaluators +eval0 :: Env Val0 -> Term -> Val0 -- object-level computation +eval1 :: Env Val1 -> Term -> Val1 -- meta-level computation +``` + +**Distinct from NbE because:** +- NbE normalizes types during type checking (unifies meta/object under `Value`). +- Staging separates meta and object code and produces the output program. +- The two systems operate on different goals with different value representations. + +**In Splic:** +- Type checker: `core/value.rs` NbE (unified semantic domain, normalized for type comparison). +- Staging: `eval/mod.rs` with separate `Val0`/`Val1` (partitioned computation). + +Both use the same `Term` representation and `Closure { env: &[Value], body: &Term }` pattern. + +--- + +## 10. De Bruijn Representation and Shifting + +### Indices vs Levels + +Terms use **De Bruijn indices** (0 = nearest binder): + +``` +\x . \y . x --> Lam("x", Lam("y", Var(Ix(1)))) + The reference to x is 1 step from the nearest binder (y). +``` + +Evaluation uses **De Bruijn levels** (0 = outermost): + +``` +context: [x : u64, y : u64, z : u64] at depth 3 +x is at level 0, y at level 1, z at level 2. +Fresh var is at level 3. + +When quoting Rigid(1), convert to Var(Ix(3 - 1 - 1)) = Var(Ix(1)). +``` + +Conversions: +```rust +ix_to_lvl(depth: Lvl, ix: Ix) -> Lvl = Lvl(depth.0 - ix.0 - 1) +lvl_to_ix(depth: Lvl, lvl: Lvl) -> Ix = Ix(depth.0 - lvl.0 - 1) +``` + +### Free Variable Shifting (Staging) + +When quoted code (`MetaVal::Code { term, depth }`) created at one depth is spliced at a deeper depth, its free variables must be shifted: + +``` +Code created at depth = 2: App(mul, [Var(Ix(0)), Var(Ix(1))]) +Spliced at depth = 4: these indices now refer to different variables! + +Solution: shift += (4 - 2), applied to free variables (Ix >= some cutoff). +``` + +Implement via recursive term traversal: + +```rust +fn shift_free_ix(term, shift, cutoff) { + match term { + Var(Ix(i)) if i >= cutoff => Var(Ix(i + shift)), + Lam { body, .. } => Lam { body: shift_free_ix(body, shift, cutoff) }, + // ... recursively apply to all sub-terms + } +} +``` + +Only free variables (those not bound within the term itself) are shifted. + +--- + +## 11. Reference Implementations + +- **elaboration-zoo** (Kovács, 2020): https://github.com/AndrasKovacs/elaboration-zoo + - Branch `01-eval-closures-debruijn` is the canonical reference for NbE + De Bruijn. + - Haskell source is clean and readable; comments explain each step. + - Shows the minimal NbE setup needed for dependent type checking. + +- **2LTT skill / Splic** (this project): + - `compiler/src/core/value.rs`: Core NbE data structures and functions. + - `compiler/src/core/mod.rs`: De Bruijn index/level types and conversions. + - `docs/bs/nbe_and_debruijn.md`: Detailed walkthrough of the architecture and index shifting. + +- **Kovács papers**: + - *Staged Compilation with Two-Level Type Theory* (ICFP 2022): Foundational theory and properties. + - *Closure-Free Functional Programming in a Two-Level Type Theory* (ICFP 2024): Object-level closure optimization. + +--- + +## 12. Glossary + +| Term | Definition | +|------|-----------| +| **NbE** | Normalization by Evaluation. Interpreter-based type checking that maintains semantic values and quotes back to syntax. | +| **Closure** | `{ env: &[Value], body: &Term }`. Captured environment + unevaluated body for lazy evaluation. | +| **Neutral / Rigid** | A value that cannot be reduced further (e.g., stuck on a free variable). | +| **Canonical** | A value in "normal form" (fully evaluated). | +| **De Bruijn index** | Variable reference counting from the nearest binder (0 = innermost). | +| **De Bruijn level** | Variable position counting from the outermost binder (0 = root). | +| **Quote** | Convert a value back to term syntax. | +| **Free variable** | A variable not bound by any enclosing lambda/pi in the term. | +| **Shift** | Adjust De Bruijn indices when moving code to a different binding depth. | +| **Splice** | `$(e)`. Run meta code and insert the result into an object context. | +| **Quote** | `#(e)`. Embed an object term as meta code (lift to `[[T]]`). | +| **Lift** | `[[T]]`. Meta type of object code producing type `T`. | diff --git a/.opencode/skills/2ltt/kovacs-2022-staged-compilation-2ltt.md b/.opencode/skills/2ltt/kovacs-2022-staged-compilation-2ltt.md index 1c1d515..94f097b 100644 --- a/.opencode/skills/2ltt/kovacs-2022-staged-compilation-2ltt.md +++ b/.opencode/skills/2ltt/kovacs-2022-staged-compilation-2ltt.md @@ -126,166 +126,4 @@ Implementation takeaway: only build it (quote), compose it (meta functions), and run it (splice), but do not pattern match on its structure in the meta language. -(If you *do* want inspection, you'll need a different setup and should expect trade-offs.) - ---- - -## 7. Implementation Architecture: NbE + Staging - -Modern practical implementations use **Normalization by Evaluation (NbE)** for type checking and evaluation for staging. - -### Type Checker NbE (Kovács 2022 §3–4, elaboration-zoo 01-eval-closures-debruijn) - -The type checker maintains a **semantic domain** separate from syntax: - -```haskell --- Haskell pseudocode (elaboration-zoo style) -data Value - = VRigid Lvl Spine -- stuck on a local variable - | VLam Name (Val -> Val) -- closure as a function - | VPi Name (Val -> Val) VTy -- dependent Pi with closure - | VLit Int - | VGlobal Name - | VLift Val - | VQuote Val - --- Evaluation: interpret terms in an environment -eval :: Env Val -> Term -> Val -eval env (Var ix) = env !! (env.len - 1 - ix) -- index to stack -eval env (Lam x t) = VLam x (\v -> eval (v:env) t) -eval env (Pi x a b) = VPi x (\v -> eval (v:env) b) (eval env a) -eval env (App f args) = vApp (eval env f) (map (eval env) args) - --- Quotation: convert value back to term (for errors, output) -quote :: Lvl -> Val -> Term -quote lvl (VRigid x sp) = quoteSp lvl (Var (lvl2Ix lvl x)) sp -quote lvl (VLam x t) = Lam x (quote (lvl+1) (t (VRigid lvl))) -quote lvl (VPi x a b) = Pi x (quote lvl a) (quote (lvl+1) (b (VRigid lvl))) -``` - -**Key design choices:** -- Terms use **De Bruijn indices** (count from nearest binder). -- Values use **De Bruijn levels** (count from outermost binder). -- Closures are functions `Val -> Val` in the metalanguage (or `Closure { env, body }` in Rust). -- No syntactic substitution — substitution is modeled via environment extension. - -**Why this works:** -- Indices are pure syntax — portable, no external state. -- Levels are the natural output of evaluation — fresh variables are just the current depth. -- Closures capture the evaluation environment, eliminating variable-capture bugs. - -### Staging Evaluator (Separate system) - -The **staging evaluator** is a different system that compiles meta code and produces the object program: - -```haskell --- Two separate value types -data Val0 = V0Lit Int | V0App Name [Val0] | V0Code Term | ... -data Val1 = V1Lam (Val0 -> Val1) | V1Lit Int | ... - --- Two separate evaluators -eval0 :: Env Val0 -> Term -> Val0 -- object-level computation -eval1 :: Env Val1 -> Term -> Val1 -- meta-level computation -``` - -**Distinct from NbE because:** -- NbE normalizes types during type checking (unifies meta/object under `Value`). -- Staging separates meta and object code and produces the output program. -- The two systems operate on different goals with different value representations. - -**In Splic:** -- Type checker: `core/value.rs` NbE (unified semantic domain, normalized for type comparison). -- Staging: `eval/mod.rs` with separate `Val0`/`Val1` (partitioned computation). - -Both use the same `Term` representation and `Closure { env: &[Value], body: &Term }` pattern. - ---- - -## 8. De Bruijn Representation and Shifting - -### Indices vs Levels - -Terms use **De Bruijn indices** (0 = nearest binder): - -``` -\x . \y . x --> Lam("x", Lam("y", Var(Ix(1)))) - The reference to x is 1 step from the nearest binder (y). -``` - -Evaluation uses **De Bruijn levels** (0 = outermost): - -``` -context: [x : u64, y : u64, z : u64] at depth 3 -x is at level 0, y at level 1, z at level 2. -Fresh var is at level 3. - -When quoting Rigid(1), convert to Var(Ix(3 - 1 - 1)) = Var(Ix(1)). -``` - -Conversions: -```rust -ix_to_lvl(depth: Lvl, ix: Ix) -> Lvl = Lvl(depth.0 - ix.0 - 1) -lvl_to_ix(depth: Lvl, lvl: Lvl) -> Ix = Ix(depth.0 - lvl.0 - 1) -``` - -### Free Variable Shifting (Staging) - -When quoted code (`MetaVal::Code { term, depth }`) created at one depth is spliced at a deeper depth, its free variables must be shifted: - -``` -Code created at depth = 2: App(mul, [Var(Ix(0)), Var(Ix(1))]) -Spliced at depth = 4: these indices now refer to different variables! - -Solution: shift += (4 - 2), applied to free variables (Ix >= some cutoff). -``` - -Implement via recursive term traversal: - -```rust -fn shift_free_ix(term, shift, cutoff) { - match term { - Var(Ix(i)) if i >= cutoff => Var(Ix(i + shift)), - Lam { body, .. } => Lam { body: shift_free_ix(body, shift, cutoff) }, - // ... recursively apply to all sub-terms - } -} -``` - -Only free variables (those not bound within the term itself) are shifted. - ---- - -## 9. Reference Implementations - -- **elaboration-zoo** (Kovács, 2020): https://github.com/AndrasKovacs/elaboration-zoo - - Branch `01-eval-closures-debruijn` is the canonical reference for NbE + De Bruijn. - - Haskell source is clean and readable; comments explain each step. - - Shows the minimal NbE setup needed for dependent type checking. - -- **2LTT skill / Splic** (this project): - - `compiler/src/core/value.rs`: Core NbE data structures and functions. - - `compiler/src/core/mod.rs`: De Bruijn index/level types and conversions. - - `docs/bs/nbe_and_debruijn.md`: Detailed walkthrough of the architecture and index shifting. - -- **Kovács papers**: - - *Staged Compilation with Two-Level Type Theory* (ICFP 2022): Foundational theory and properties. - - *Closure-Free Functional Programming in a Two-Level Type Theory* (ICFP 2024): Object-level closure optimization. - ---- - -## 10. Glossary - -| Term | Definition | -|------|-----------| -| **NbE** | Normalization by Evaluation. Interpreter-based type checking that maintains semantic values and quotes back to syntax. | -| **Closure** | `{ env: &[Value], body: &Term }`. Captured environment + unevaluated body for lazy evaluation. | -| **Neutral / Rigid** | A value that cannot be reduced further (e.g., stuck on a free variable). | -| **Canonical** | A value in "normal form" (fully evaluated). | -| **De Bruijn index** | Variable reference counting from the nearest binder (0 = innermost). | -| **De Bruijn level** | Variable position counting from the outermost binder (0 = root). | -| **Quote** | Convert a value back to term syntax. | -| **Free variable** | A variable not bound by any enclosing lambda/pi in the term. | -| **Shift** | Adjust De Bruijn indices when moving code to a different binding depth. | -| **Splice** | `$(e)`. Run meta code and insert the result into an object context. | -| **Quote** | `#(e)`. Embed an object term as meta code (lift to `[[T]]`). | -| **Lift** | `[[T]]`. Meta type of object code producing type `T`. | +(If you *do* want inspection, you'll need a different setup and should expect trade-offs.) \ No newline at end of file From 0da342c7a2efa09a3e42f5b4a853f73af8ffcecd Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 14:38:39 +0000 Subject: [PATCH 26/43] docs: remove duplicate pi_types entry from docs/bs/README Keep pi_types.md listed only in Compiler Internals section with the more complete description that captures both design decisions and implementation details (NbE type checking). Co-Authored-By: Claude Haiku 4.5 --- docs/bs/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/bs/README.md b/docs/bs/README.md index 01b82f0..23ba1cb 100644 --- a/docs/bs/README.md +++ b/docs/bs/README.md @@ -12,7 +12,6 @@ The folder name, `bs`, stands for brainstorming. Obviously. - [functional_goto.md](functional_goto.md) — Control flow via SSA-style basic blocks with goto - [comparison_operators.md](comparison_operators.md) — Boolean vs propositional comparisons - [tuples_and_inference.md](tuples_and_inference.md) — Tuple syntax and type inference -- [pi_types.md](pi_types.md) — Dependent function types (Pi) and lambdas at the meta level ## Compiler Internals From ffb9bb9028eacf2e305ec2ba3962563742d1e0a9 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 15:03:36 +0000 Subject: [PATCH 27/43] docs: trim nbe_and_debruijn and pi_types for architectural focus Remove in-the-weeds implementation details and re-explanations of well-established concepts: - nbe_and_debruijn.md: Remove full Value enum, detailed code examples (check_app, shift_free_ix), and verbose step-by-step walkthroughs. Keep key concepts (indices vs levels, closures, index shifting) as prose. - pi_types.md: Remove entire 'Type Checker NbE' section (duplicates nbe_and_debruijn with Rust code). Trim Core IR Design to remove Term representation code block, replace with prose about design decisions. Both docs now focus on 'what' and 'why' rather than 'how'. A reader familiar with PL implementation can follow without re-explanation. Co-Authored-By: Claude Haiku 4.5 --- docs/bs/nbe_and_debruijn.md | 243 +++--------------------------------- docs/bs/pi_types.md | 110 +--------------- 2 files changed, 19 insertions(+), 334 deletions(-) diff --git a/docs/bs/nbe_and_debruijn.md b/docs/bs/nbe_and_debruijn.md index 71bcdb4..27b00f5 100644 --- a/docs/bs/nbe_and_debruijn.md +++ b/docs/bs/nbe_and_debruijn.md @@ -4,258 +4,43 @@ This document explains the type checker's use of **Normalization by Evaluation ( ## Problem: Syntactic Substitution with Binders -Naive substitution fails when the replacement contains binders: - -```rust -// Example: substitute a lambda into another context -let replacement = Lam { param_name: "x", body: Var(Ix(0)) }; // the identity lambda |x| x -let target = Let { - expr: Prim(U64), - body: Var(Ix(0)) // refers to the let-bound variable -}; - -// Naive subst(target, position_of_0, replacement) would replace Var(0) with the Lam. -// But the Lam's body (Var(0)) refers to its own parameter (scope relative to the Lam), -// not the let-bound variable. This is a capture bug. -``` - -**Root cause:** The `Var(Ix)` in `replacement` uses indices relative to the Lam's scope, but when substituted elsewhere, those indices become meaningless. The two-level namespace problem (variable name vs. scope) requires careful handling. - -**Why it matters:** Dependent function types need substitution to compute return types: -``` -fn(x: A) -> B with argument arg => B[arg/x] -``` - -Without correct substitution (or an alternative), dependent type checking is broken. +Naive substitution fails when the replacement contains binders. Variables within the replacement use indices relative to that context; when spliced elsewhere, those indices are meaningless, causing capture bugs. This is critical for dependent type checking, which requires computing return types via substitution (e.g., `fn(x: A) -> B` with argument `arg` must produce `B[arg/x]`). ## Solution: Normalization by Evaluation -Instead of rewriting terms syntactically, **evaluate terms in an environment**. The environment tracks what each De Bruijn index refers to, so substitution happens implicitly via environment extension. +Instead of rewriting terms syntactically, **evaluate terms in an environment**. The environment tracks what each De Bruijn index refers to, eliminating the need for explicit substitution. ### De Bruijn Indices vs Levels Two complementary representations: -**De Bruijn Indices (`Ix`):** Used in **term syntax**. An index is **relative to the nearest binder** (0 = innermost). - -``` -\x . \y . x --> |x: _| |y: _| Var(Ix(1)) - The 'x' is 1 step up from the innermost binder (y). - -[(x, v1), (y, v2)] at depth 2 -Var(Ix(1)) ==> look up stack[2 - 1 - 1] = stack[0] = (x, v1) -``` - -**De Bruijn Levels (`Lvl`):** Used internally in **semantic domain and context**. A level is **absolute** (0 = outermost). +**De Bruijn Indices:** Used in **term syntax**. An index counts from the nearest binder (0 = innermost). This is pure syntax, portable, and requires no external state to interpret. -``` -[x, y] at depth 2 -x is at level 0, y is at level 1 -Fresh variable would be at level 2. -``` - -### Conversions +**De Bruijn Levels:** Used internally in the **semantic domain**. A level counts from the outermost binder (0 = root) and grows monotonically during evaluation, making fresh variable generation natural. +**Conversions:** ```rust -// Given current depth (how many binders we're under): -ix_to_lvl(depth: Lvl, ix: Ix) -> Lvl { - Lvl(depth.0 - ix.0 - 1) -} - -lvl_to_ix(depth: Lvl, lvl: Lvl) -> Ix { - Ix(depth.0 - lvl.0 - 1) -} +ix_to_lvl(depth, ix) = Lvl(depth - ix - 1) +lvl_to_ix(depth, lvl) = Ix(depth - lvl - 1) ``` -**Why split the representation?** -- **Term syntax uses indices** — they are pure, no external state needed to interpret. -- **Semantics uses levels** — they grow monotonically as evaluation descends under binders, making fresh variable generation natural. -- This matches elaboration-zoo and Kovács' reference implementations. - -## Core NbE Data Structures - -### Value Domain (core/value.rs) - -```rust -pub type Env<'a> = Vec>; - -pub enum Value<'a> { - // Neutrals (stuck, cannot reduce further) - Rigid(Lvl), // free variable - Global(&'a str), // global function (not inlined) - Prim(Prim), // primitive - App(&'a Value<'a>, &'a [Value<'a>]), // application - - // Canonical forms - Lit(u64), - Lam(VLam<'a>), - Pi(VPi<'a>), - Lift(&'a Value<'a>), - Quote(&'a Value<'a>), -} - -pub struct VLam<'a> { - pub name: &'a str, - pub param_ty: &'a Value<'a>, - pub closure: Closure<'a>, -} - -pub struct VPi<'a> { - pub name: &'a str, - pub domain: &'a Value<'a>, - pub closure: Closure<'a>, - pub phase: Phase, -} - -pub struct Closure<'a> { - pub env: &'a [Value<'a>], // immutable snapshot of environment - pub body: &'a Term<'a>, // unevaluated body -} -``` - -**Key insight:** Closures pair an **environment snapshot** (arena-allocated slice) with an **unevaluated term**. When instantiated, the environment is extended and the body is evaluated. - -### Evaluation - -The evaluator interprets terms in an environment, producing semantic values: +## Core NbE Design -**Key principles**: -- **Variables** are converted from indices to stack positions via environment lookup. -- **Lambdas and Pi types** create closures by snapshotting the current environment. -- **Applications** apply a function value to arguments, evaluating on-demand. -- **Let bindings** are eagerly evaluated: extend the environment with the let-bound value and continue. -- **Lift/Quote/Splice** are reduced according to staging rules. -- **Match** performs pattern matching at the semantic level. +The semantic domain separates values from syntax. **Closures** are the key structure: a closure pairs an **environment snapshot** (immutable slice) with an **unevaluated term**. When instantiated, the environment is extended and the body is evaluated. -**Variadic handling**: Multi-parameter lambdas and Pi types are curried by slicing, not duplicating: -remaining parameters are encoded as a sub-term, avoiding allocation. +**Evaluation** interprets terms in an environment. Variables are looked up in the environment, lambdas and Pi types create closures by snapshotting the environment, and applications instantiate closures. -### Closure Instantiation - -To apply a closure to an argument: - -1. Restore the closure's environment from its snapshot. -2. Extend it with the argument value. -3. Evaluate the body in the extended environment. - -For multi-argument applications, apply repeatedly: each application produces a value that may itself be a lambda (closure), ready for the next argument. - -If application gets stuck (callee is not a lambda), the application becomes neutral (`Value::App`). - -### Quotation - -Convert values back to term syntax. Used for error reporting, output, and type comparison. - -**Key operations**: -- **Rigid variables** are converted from levels to indices using `lvl_to_ix(depth, lvl)`. -- **Globals, prims, and literals** are reconstructed directly. -- **Lambdas and Pi types** are reconstructed by: - 1. Applying the closure to a fresh variable at the current depth. - 2. Recursively quoting the result at the next depth. - 3. Storing the original parameter information (name, type). -- **Applications** are reconstructed by quoting the function and arguments. -- **Lift/Quote/Splice** are reconstructed structurally. +**Quotation** converts values back to syntax for error reporting, type output, and comparison. Rigid variables are converted from levels back to indices; lambdas and Pi types are reconstructed by applying the closure to a fresh variable and recursively quoting the result. ## Type Checker Integration -The type checker maintains a context with: - -- **Evaluation environment** (`env`): Values of bound variables, indexed by De Bruijn level. -- **Type environment** (`types`): Semantic type of each bound variable. -- **Current depth** (`lvl`): How many binders we are under. -- **Name tracking** (`names`): Variable names for error messages (not used in lookups). -- **Globals table** (`globals`): Top-level function types. - -When a local variable is bound: -1. Push a fresh rigid value at the current level. -2. Push its type (as a semantic value) into the type environment. -3. Increment the depth. - -This way, type information flows as semantic values throughout checking, and variable lookup is O(1) indexing. - -### Dependent Type Checking Example - -```rust -// Check a multi-argument application -fn check_app(ctx: &Ctx, app: &App) -> Result { - let func_type = type_of(ctx, app.func)?; - - let mut pi_val = func_type; - let mut checked_args = Vec::new(); - - for arg_term in app.args { - let Value::Pi(vpi) = pi_val else { - bail!("too many arguments"); - } - - // Check argument against domain - check(ctx, arg_term, &vpi.domain)?; - let arg_val = eval(ctx.arena, ctx.globals, &ctx.env, arg_term); - checked_args.push(arg_term); +The type checker maintains a context with an evaluation environment (values indexed by De Bruijn level), type environment (semantic types of bound variables), current depth, and globals table. - // Advance return type by instantiating closure - pi_val = apply(ctx.arena, ctx.globals, &vpi.closure, arg_val); - } - - Ok(pi_val) // return type -} -``` - -No syntactic substitution; the dependent return type is computed by evaluating the Pi closure. +For dependent type checking, instead of syntactic substitution (which has capture bugs), the checker instantiates Pi closures: it evaluates the domain type, checks the argument, then applies the closure to the evaluated argument to get the return type. ## Code Value Index Shifting (Staging) -When quoted object code is stored as `MetaVal::Code` and later reused in a different context, its De Bruijn indices must be adjusted. - -### The Problem - -```rust -// Code generated at depth 2 (x1 is Ix(0), x0 is Ix(1)) -let code = MetaVal::Code { - term: App { func: Global("mul"), args: [Var(Ix(0)), Var(Ix(1))] }, - depth: 2, -}; - -// Later, code is spliced at depth 4 (x3, x2, x1, x0 from innermost) -// Ix(0) still refers to x3 (innermost), not x1 -// Ix(1) still refers to x2, not x0 -// The indices are wrong! -``` - -### Solution: Shift Free Indices - -Store the creation depth with the code value. On splice, shift free variable indices: - -```rust -fn shift_free_ix<'out>( - arena: &'out Bump, - term: &'out Term<'out>, - shift: usize, - cutoff: usize, -) -> &'out Term<'out> { - match term { - Term::Var(Ix(i)) => { - if i >= cutoff { - arena.alloc(Term::Var(Ix(i + shift))) - } else { - arena.alloc(term.clone()) - } - } - Term::Lam(lam) => { - // Shift continues into the body (free vars are those >= cutoff) - let new_body = shift_free_ix(arena, lam.body, shift, cutoff); - // ... reconstruct lam - } - // ... other cases recursively apply shift - } -} - -// On splice: -let depth_delta = current_depth - creation_depth; -let shifted_term = shift_free_ix(arena, code_term, depth_delta, 0); -``` - -**Key insight:** Only "free" variables (those at `Ix >= cutoff`) are shifted. Bound variables (introduced by the code itself) are not affected — only the indices in the code that refer to enclosing context. +When quoted object code is stored with its creation depth and later spliced at a different depth, its free variable indices must be shifted. If code was created at depth N and spliced at depth M, free indices (those not bound by the code itself) are shifted by M - N. Only free variables are shifted; variables bound within the code are unaffected. ## See Also diff --git a/docs/bs/pi_types.md b/docs/bs/pi_types.md index 8d742d1..7c7ad1a 100644 --- a/docs/bs/pi_types.md +++ b/docs/bs/pi_types.md @@ -113,119 +113,19 @@ Multi-argument calls desugar to curried application: `f(a, b)` = `f(a)(b)`. ### Term Representation -```rust -// Variables use De Bruijn indices (count from nearest binder, 0 = innermost) -Term::Var(Ix) - -// Pi types support variadic (multi-parameter) syntax -Pi { params: &'a [(&'a str, &'a Term<'a>)], body_ty: &'a Term<'a>, phase: Phase } - -// Lambdas similarly support variadic parameters -Lam { params: &'a [(&'a str, &'a Term<'a>)], body: &'a Term<'a> } - -// Application handles variadic calls -App { func: &'a Term<'a>, args: &'a [&'a Term<'a>] } - -// Global references are terms, not application heads -Global(Name<'a>) -``` - -**Variadic Design.** Pi and Lam now carry a parameter list rather than single parameter. This preserves arity information and enables proper multi-argument application: +**Variadic Design.** Pi and Lam carry a parameter list rather than a single parameter. This preserves arity information: - `fn(x: u64, y: u64) -> u64` is a single Pi with 2 params, not nested Pi types. -- Application checking evaluates the domain type, checks the argument, then advances to the next param (via closure instantiation). +- Application checking evaluates the domain, checks the argument, then advances via closure instantiation. -**Phase field.** The Pi carries a `phase: Phase` distinguishing meta-level (`Phase::Meta`, printed as `fn`) from object-level (`Phase::Object`, printed as `code fn`) function types. +**Phase field.** Pi types carry a phase distinguishing meta-level (printed as `fn`) from object-level (printed as `code fn`) function types. ### Substitution → Normalization by Evaluation (NbE) -**Removed:** Syntactic substitution (`fn subst(...)`) is **deleted**. It had a critical variable-capture bug when the replacement contained binders. - -**New approach:** The type checker uses **Normalization by Evaluation** to handle dependent types. Instead of rewriting syntax, the checker maintains a **semantic domain** (`Value`) and evaluates types in context. Dependent function arguments are checked by: - -1. Evaluating the Pi type in the current environment to obtain a semantic `VPi`. -2. Checking the argument against the evaluated domain type. -3. Instantiating the Pi's closure with the evaluated argument to get the return type. - -See `docs/bs/nbe_and_debruijn.md` for complete details on the semantic domain and evaluation. +Syntactic substitution is avoided due to capture bugs. Instead, the type checker uses **Normalization by Evaluation**: it evaluates types in a semantic domain (environment of values) and handles dependent type checking via closure instantiation. See `nbe_and_debruijn.md` for details. ### Alpha-equivalence -Two terms are alpha-equivalent if they are structurally identical under De Bruijn indices (parameter names are irrelevant). The `alpha_eq` function in `core/alpha_eq.rs` performs structural comparison. With De Bruijn indices, this is a straightforward recursive check — renaming machinery is unnecessary. - -## Type Checker NbE - -### Semantic Domain (core/value.rs) - -The type checker maintains semantic values separate from syntax to enable normalization: - -```rust -pub enum Value<'a> { - // Neutrals (cannot reduce) - Rigid(Lvl), // local variable (De Bruijn level) - Global(&'a str), // global function reference - Prim(Prim), // primitive operation - App(&'a Value<'a>, &'a [Value<'a>]), // application - - // Canonical forms - Lit(u64), // literal - Lam(VLam<'a>), // lambda with closure - Pi(VPi<'a>), // Pi type with closure - Lift(&'a Value<'a>), // lifted type - Quote(&'a Value<'a>), // quoted code -} - -pub struct VLam<'a> { - pub name: &'a str, - pub param_ty: &'a Value<'a>, - pub closure: Closure<'a>, -} - -pub struct VPi<'a> { - pub name: &'a str, - pub domain: &'a Value<'a>, - pub closure: Closure<'a>, - pub phase: Phase, -} - -pub struct Closure<'a> { - pub env: &'a [Value<'a>], // snapshot of evaluation environment - pub body: &'a Term<'a>, // unevaluated body term -} -``` - -### Key Operations - -**`eval(arena, globals, env, term) -> Value`:** Interpret a term in an environment. -- `Var(Ix(i))`: index into `env[env.len() - 1 - i]` (convert index to stack position). -- `Lam` / `Pi`: create a closure by snapshotting `env` to the arena and pairing it with the body. -- Other forms: recursively evaluate or return as neutrals. - -**`apply(arena, globals, closure, arg) -> Value`:** Instantiate a closure with an argument. -- Clone the closure's environment, push the argument, evaluate the body. - -**`quote(arena, depth, value) -> &'a Term`:** Convert a value back to term syntax. -- `Rigid(lvl)`: convert level to index using `lvl_to_ix(depth, lvl)`. -- For `Lam` / `Pi`: apply the closure to a fresh variable, recursively quote the result. - -### Dependent Type Checking - -When checking a multi-argument application: - -1. Evaluate the function's type to get `Value::Pi(vpi)`. -2. Check the first argument against `vpi.domain`. -3. Evaluate the argument to a value. -4. **Instantiate the Pi's closure** with the evaluated argument: `apply(closure, arg_value)` yields the type of remaining args or return type. -5. Repeat for each argument. - -This replaces syntactic substitution and eliminates variable capture bugs. - -### Distinction from Staging Evaluator - -The type checker's NbE and the staging evaluator (`eval/mod.rs`) are separate: -- **Type checker NbE** uses a unified `Value` domain to normalize types during elaboration. -- **Staging evaluator** uses separate `Val0`/`Val1` domains to partition meta/object computation and produce object code. - -Both use `Closure { env, body }` pattern for closures, but serve different purposes and cannot be unified. +Two terms are alpha-equivalent if they are structurally identical under De Bruijn indices (parameter names are irrelevant). With De Bruijn indices, equivalence checking is a straightforward recursive check — no renaming machinery is needed. ## Staging Interaction From 068cc08fa7b3df6b5f894b32858a9b9bdd35bd60 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 18:18:06 +0000 Subject: [PATCH 28/43] refactor: use &Pi directly in globals/function types; fix pi elaboration regressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change Function::ty, Ctx.globals, and GlobalDef::ty from &Term to &Pi, eliminating Term::Pi wrapping/unwrapping at every call site - Remove vestigial globals parameter from all NbE functions in value.rs (Term::Global evaluates to Value::Global without lookup; parameter was unused) - Fix pi_const and pi_lambda_empty_params: zero-param Pi now evaluates to body type directly (fn() -> T ≅ T); arity pre-check restricted to globals - Fix lambda checking to peel exactly params.len() Pi layers instead of all, enabling nested lambdas to type-check correctly - Update error messages for arity/phase mismatches in pi_apply_non_fn and pi_arity_mismatch snapshots Co-Authored-By: Claude Sonnet 4.6 --- compiler/src/checker/mod.rs | 114 ++++++++---------- compiler/src/checker/test/apply.rs | 4 +- compiler/src/checker/test/helpers.rs | 20 +-- compiler/src/checker/test/matching.rs | 24 ++-- compiler/src/checker/test/meta.rs | 8 +- compiler/src/checker/test/signatures.rs | 10 +- compiler/src/core/mod.rs | 15 +-- compiler/src/core/value.rs | 106 +++++----------- compiler/src/eval/mod.rs | 22 ++-- .../snap/full/pi_apply_non_fn/3_check.txt | 2 +- .../snap/full/pi_arity_mismatch/3_check.txt | 2 +- compiler/tests/snap/full/pi_const/3_check.txt | 14 ++- .../full/pi_lambda_empty_params/3_check.txt | 14 ++- 13 files changed, 154 insertions(+), 201 deletions(-) diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index 0562a5f..9a3fb6c 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -29,13 +29,13 @@ pub struct Ctx<'core, 'globals> { /// Global function types: name -> Pi term. /// Storing `&Term` (always a Pi) unifies type lookup for globals and locals. /// Borrowed independently of the arena so the map can live on the stack. - globals: &'globals HashMap, &'core core::Term<'core>>, + globals: &'globals HashMap, &'core core::Pi<'core>>, } impl<'core, 'globals> Ctx<'core, 'globals> { pub const fn new( arena: &'core bumpalo::Bump, - globals: &'globals HashMap, &'core core::Term<'core>>, + globals: &'globals HashMap, &'core core::Pi<'core>>, ) -> Self { Ctx { arena, @@ -63,7 +63,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Push a local variable onto the context, given its type as a term. /// Evaluates the type term in the current environment. pub fn push_local(&mut self, name: &'core str, ty: &'core core::Term<'core>) { - let ty_val = value::eval(self.arena, self.globals, &self.env, ty); + let ty_val = value::eval(self.arena, &self.env, ty); self.env.push(value::Value::Rigid(self.lvl)); self.types.push(ty_val); self.lvl = self.lvl.succ(); @@ -129,7 +129,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Evaluate a term in the current environment. fn eval(&self, term: &'core core::Term<'core>) -> value::Value<'core> { - value::eval(self.arena, self.globals, &self.env, term) + value::eval(self.arena, &self.env, term) } /// Quote a value back to a term at the current depth. @@ -199,12 +199,12 @@ impl<'core, 'globals> Ctx<'core, 'globals> { // Global reference: look up its Pi type and evaluate. core::Term::Global(name) => { - let pi_term = self + let pi = self .globals .get(name) .copied() .expect("Global with unknown name (typechecker invariant)"); - self.eval(pi_term) + value::eval_pi(self.arena, &self.env, pi) } // App: compute return type via NbE. @@ -245,7 +245,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { value::Value::Pi(vpi) => { let arg_val = self.eval(arg); pi_val = - value::inst(self.arena, self.globals, &vpi.closure, arg_val); + value::inst(self.arena, &vpi.closure, arg_val); } _ => unreachable!("App func must have Pi type (typechecker invariant)"), } @@ -265,7 +265,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { let mut names2 = self.names.clone(); let mut elaborated_param_types: Vec> = Vec::new(); for &(pname, pty) in lam.params { - let ty_val = value::eval(self.arena, self.globals, &env2, pty); + let ty_val = value::eval(self.arena, &env2, pty); elaborated_param_types.push(ty_val.clone()); env2.push(value::Value::Rigid(lvl2)); types2.push(ty_val); @@ -382,11 +382,11 @@ fn builtin_prim_ty(name: &str, phase: Phase) -> Option<&'static core::Term<'stat }) } -/// Elaborate one function's signature into a `Term::Pi` (the globals table entry). +/// Elaborate one function's signature into a `Pi` (the globals table entry). fn elaborate_sig<'src, 'core>( arena: &'core bumpalo::Bump, func: &ast::Function<'src>, -) -> Result<&'core core::Term<'core>> { +) -> Result<&'core core::Pi<'core>> { let empty_globals = HashMap::new(); let mut ctx = Ctx::new(arena, &empty_globals); @@ -400,22 +400,19 @@ fn elaborate_sig<'src, 'core>( let body_ty = infer(&mut ctx, func.phase, func.ret_ty)?; - Ok(arena.alloc(core::Term::Pi(Pi { + Ok(arena.alloc(Pi { params, body_ty, phase: func.phase, - }))) + })) } /// Pass 1: collect all top-level function signatures into a globals table. -/// -/// Each entry is a `Term::Pi` carrying the function's phase, param types, and return type. -/// This allows pass 2 to look up a global's type the same way it looks up a local's type. pub(crate) fn collect_signatures<'src, 'core>( arena: &'core bumpalo::Bump, program: &ast::Program<'src>, -) -> Result, &'core core::Term<'core>>> { - let mut globals: HashMap, &'core core::Term<'core>> = HashMap::new(); +) -> Result, &'core core::Pi<'core>>> { + let mut globals: HashMap, &'core core::Pi<'core>> = HashMap::new(); for func in program.functions { let name = core::Name::new(arena.alloc_str(func.name.as_str())); @@ -437,16 +434,12 @@ pub(crate) fn collect_signatures<'src, 'core>( fn elaborate_bodies<'src, 'core>( arena: &'core bumpalo::Bump, program: &ast::Program<'src>, - globals: &HashMap, &'core core::Term<'core>>, + globals: &HashMap, &'core core::Pi<'core>>, ) -> Result> { let functions: &'core [core::Function<'core>] = arena.alloc_slice_try_fill_iter(program.functions.iter().map(|func| -> Result<_> { let name = core::Name::new(arena.alloc_str(func.name.as_str())); - let ty = *globals.get(&name).expect("signature missing from pass 1"); - let pi = match ty { - core::Term::Pi(pi) => pi, - _ => unreachable!("globals table must contain Pi types"), - }; + let pi = *globals.get(&name).expect("signature missing from pass 1"); // Build a fresh context borrowing the stack-owned globals map. let mut ctx = Ctx::new(arena, globals); @@ -461,7 +454,7 @@ fn elaborate_bodies<'src, 'core>( let body = check_val(&mut ctx, pi.phase, func.body, ret_ty_val) .with_context(|| format!("in function `{name}`"))?; - Ok(core::Function { name, ty, body }) + Ok(core::Function { name, ty: pi, body }) }))?; Ok(core::Program { functions }) @@ -582,25 +575,22 @@ pub fn infer<'src, 'core>( // Elaborate the callee. let callee = infer(ctx, phase, func_term)?; - // Get the callee's Pi type from the globals table (for globals) or from context. - // For globals, we use the raw Pi term directly (in empty context). - // For locals/other, we use val_type_of which gives the Value::Pi. - let (pi_phase, pi_param_count) = callee_pi_info(ctx, callee)?; - - // For globals, verify phase matches. + // For globals: verify phase and arity using the raw Pi term. + // Non-globals: Pi depth is indistinguishable from nested fn types at value level, + // so we skip the arity pre-check and let the arg loop catch mismatches. if let core::Term::Global(gname) = callee { + let (pi_phase, pi_param_count) = callee_pi_info(ctx, callee)?; ensure!( pi_phase == phase, "function `{gname}` is a {pi_phase}-phase function, but called in {phase}-phase context", ); + ensure!( + args.len() == pi_param_count, + "wrong number of arguments: callee expects {pi_param_count}, got {}", + args.len() + ); } - ensure!( - args.len() == pi_param_count, - "wrong number of arguments: callee expects {pi_param_count}, got {}", - args.len() - ); - // Get the starting Pi value for arg checking. // For globals: evaluate the Pi term in empty env. // For locals: use val_type_of (Value::Pi). @@ -617,7 +607,7 @@ pub fn infer<'src, 'core>( let arg_val = ctx.eval(core_arg); core_args.push(core_arg); // Advance Pi to the next type by applying closure to arg. - pi_val = value::inst(ctx.arena, ctx.globals, &vpi.closure, arg_val); + pi_val = value::inst(ctx.arena, &vpi.closure, arg_val); } let args_slice = ctx.alloc_slice(core_args); @@ -824,15 +814,12 @@ pub fn infer<'src, 'core>( fn callee_pi_info(ctx: &Ctx<'_, '_>, callee: &core::Term<'_>) -> Result<(Phase, usize)> { match callee { core::Term::Global(name) => { - let pi_term = ctx + let pi = ctx .globals .get(name) .copied() .ok_or_else(|| anyhow!("unknown global `{name}`"))?; - match pi_term { - core::Term::Pi(pi) => Ok((pi.phase, pi.params.len())), - _ => bail!("global `{name}` is not a function"), - } + Ok((pi.phase, pi.params.len())) } _ => { let mut ty = ctx.val_type_of(callee); @@ -845,9 +832,12 @@ fn callee_pi_info(ctx: &Ctx<'_, '_>, callee: &core::Term<'_>) -> Result<(Phase, count += 1; // Advance with a fresh rigid to get the next Pi layer. let fresh = value::Value::Rigid(Lvl(ctx.depth() + count - 1)); - ty = value::inst(ctx.arena, ctx.globals, &vpi.closure, fresh); + ty = value::inst(ctx.arena, &vpi.closure, fresh); } - let phase = phase_opt.ok_or_else(|| anyhow!("callee is not a function type"))?; + // If no Pi layers were found (count == 0), the callee's type reduces to + // a non-Pi value. In this design fn() -> T ≅ T, so zero-arg calls are + // valid for any callee. Phase is unused for non-global callees. + let phase = phase_opt.unwrap_or(Phase::Meta); Ok((phase, count)) } } @@ -863,13 +853,13 @@ fn callee_pi_val<'core>( ) -> value::Value<'core> { match callee { core::Term::Global(name) => { - let pi_term = ctx + let pi = ctx .globals .get(name) .copied() .expect("callee_pi_val called with unknown global (invariant)"); // Global Pi terms are closed (elaborated in empty context) — safe to eval in current env. - value::eval(ctx.arena, ctx.globals, &[], pi_term) + value::eval_pi(ctx.arena, &[], pi) } _ => ctx.val_type_of(callee), } @@ -1182,26 +1172,28 @@ pub fn check_val<'src, 'core>( let depth_before = ctx.depth(); - // Expected type must be a Pi with matching arity. - // We need to peel Pi layers to get all params. - // Collect all Pi params from the expected value. + // Peel exactly `params.len()` Pi layers from the expected type. + // This allows nested lambdas: `|a: A| |b: B| body` checks against + // `fn(_: A) -> fn(_: B) -> R` by covering one Pi layer per lambda. let mut pi_params: Vec<(&str, value::Value<'core>)> = Vec::new(); let mut cur_pi = expected.clone(); - while let value::Value::Pi(vpi) = cur_pi { - pi_params.push((vpi.name, (*vpi.domain).clone())); - // Advance with a fresh variable for the closure - let fresh = value::Value::Rigid(Lvl(ctx.depth() + pi_params.len() - 1)); - cur_pi = value::inst(ctx.arena, ctx.globals, &vpi.closure, fresh); + for _ in 0..params.len() { + match cur_pi { + value::Value::Pi(vpi) => { + pi_params.push((vpi.name, (*vpi.domain).clone())); + let fresh = + value::Value::Rigid(Lvl(ctx.depth() + pi_params.len() - 1)); + cur_pi = value::inst(ctx.arena, &vpi.closure, fresh); + } + _ => bail!( + "lambda has {} parameter(s) but expected type has {}", + params.len(), + pi_params.len() + ), + } } let body_ty_val = cur_pi; - ensure!( - params.len() == pi_params.len(), - "lambda has {} parameter(s) but expected type has {}", - params.len(), - pi_params.len() - ); - let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); for (p, (_, pi_param_ty)) in params.iter().zip(pi_params.into_iter()) { let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); diff --git a/compiler/src/checker/test/apply.rs b/compiler/src/checker/test/apply.rs index bdebb74..fa61741 100644 --- a/compiler/src/checker/test/apply.rs +++ b/compiler/src/checker/test/apply.rs @@ -67,11 +67,11 @@ fn infer_global_call_phase_mismatch_fails() { // `code fn f() -> u64` — object-phase function let u64_obj = core_arena.alloc(core::Term::Prim(Prim::IntTy(IntType::U64_OBJ))); let mut globals = HashMap::new(); - let f_ty: &core::Term = core_arena.alloc(core::Term::Pi(Pi { + let f_ty: &core::Pi = core_arena.alloc(Pi { params: &[], body_ty: u64_obj, phase: Phase::Object, - })); + }); globals.insert(Name::new("f"), f_ty); let mut ctx = test_ctx_with_globals(&core_arena, &globals); diff --git a/compiler/src/checker/test/helpers.rs b/compiler/src/checker/test/helpers.rs index ca0c27d..6cdc345 100644 --- a/compiler/src/checker/test/helpers.rs +++ b/compiler/src/checker/test/helpers.rs @@ -4,7 +4,7 @@ use super::*; /// Helper to create a test context with empty globals pub fn test_ctx(arena: &bumpalo::Bump) -> Ctx<'_, '_> { - static EMPTY: std::sync::OnceLock, &'static core::Term<'static>>> = + static EMPTY: std::sync::OnceLock, &'static core::Pi<'static>>> = std::sync::OnceLock::new(); let globals = EMPTY.get_or_init(HashMap::new); Ctx::new(arena, globals) @@ -15,26 +15,26 @@ pub fn test_ctx(arena: &bumpalo::Bump) -> Ctx<'_, '_> { /// The caller must ensure `globals` outlives the returned `Ctx`. pub fn test_ctx_with_globals<'core, 'globals>( arena: &'core bumpalo::Bump, - globals: &'globals HashMap, &'core core::Term<'core>>, + globals: &'globals HashMap, &'core core::Pi<'core>>, ) -> Ctx<'core, 'globals> { Ctx::new(arena, globals) } -/// Helper: build a Pi term for a function `fn f() -> u64` (no params, meta phase). -pub fn sig_no_params_returns_u64(arena: &bumpalo::Bump) -> &core::Term<'_> { - arena.alloc(core::Term::Pi(Pi { +/// Helper: build a Pi for a function `fn f() -> u64` (no params, meta phase). +pub fn sig_no_params_returns_u64(arena: &bumpalo::Bump) -> &core::Pi<'_> { + arena.alloc(Pi { params: &[], body_ty: &core::Term::U64_META, phase: Phase::Meta, - })) + }) } -/// Helper: build a Pi term for `fn f(x: u32) -> u64`. -pub fn sig_one_param_returns_u64(arena: &bumpalo::Bump) -> &core::Term<'_> { +/// Helper: build a Pi for `fn f(x: u32) -> u64`. +pub fn sig_one_param_returns_u64(arena: &bumpalo::Bump) -> &core::Pi<'_> { let params = arena.alloc_slice_fill_iter([("x", &core::Term::U32_META as &core::Term)]); - arena.alloc(core::Term::Pi(Pi { + arena.alloc(Pi { params, body_ty: &core::Term::U64_META, phase: Phase::Meta, - })) + }) } diff --git a/compiler/src/checker/test/matching.rs b/compiler/src/checker/test/matching.rs index 2994c94..31b45da 100644 --- a/compiler/src/checker/test/matching.rs +++ b/compiler/src/checker/test/matching.rs @@ -12,11 +12,11 @@ fn check_match_all_arms_same_type_succeeds() { let mut globals = HashMap::new(); globals.insert( Name::new("k32"), - core_arena.alloc(core::Term::Pi(Pi { + core_arena.alloc(Pi { params: &[], body_ty: u32_ty_core, phase: Phase::Meta, - })) as &_, + }) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; @@ -56,11 +56,11 @@ fn check_match_u1_fully_covered_succeeds() { let mut globals = HashMap::new(); globals.insert( Name::new("k1"), - core_arena.alloc(core::Term::Pi(Pi { + core_arena.alloc(Pi { params: &[], body_ty: u1_ty_core, phase: Phase::Meta, - })) as &_, + }) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); ctx.push_local("x", u1_ty_core); @@ -100,11 +100,11 @@ fn infer_match_u1_partially_covered_fails() { let mut globals = HashMap::new(); globals.insert( Name::new("k1"), - core_arena.alloc(core::Term::Pi(Pi { + core_arena.alloc(Pi { params: &[], body_ty: u1_ty_core, phase: Phase::Meta, - })) as &_, + }) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); ctx.push_local("x", u1_ty_core); @@ -134,11 +134,11 @@ fn infer_match_no_catch_all_fails() { let mut globals = HashMap::new(); globals.insert( Name::new("k32"), - core_arena.alloc(core::Term::Pi(Pi { + core_arena.alloc(Pi { params: &[], body_ty: u32_ty_core, phase: Phase::Meta, - })) as &_, + }) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; @@ -180,19 +180,19 @@ fn infer_match_arms_type_mismatch_fails() { let mut globals = HashMap::new(); globals.insert( Name::new("k32"), - core_arena.alloc(core::Term::Pi(Pi { + core_arena.alloc(Pi { params: &[], body_ty: u32_ty_core, phase: Phase::Meta, - })) as &_, + }) as &_, ); globals.insert( Name::new("k64"), - core_arena.alloc(core::Term::Pi(Pi { + core_arena.alloc(Pi { params: &[], body_ty: u64_ty_core, phase: Phase::Meta, - })) as &_, + }) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; diff --git a/compiler/src/checker/test/meta.rs b/compiler/src/checker/test/meta.rs index a0544ba..f4ed8b4 100644 --- a/compiler/src/checker/test/meta.rs +++ b/compiler/src/checker/test/meta.rs @@ -65,11 +65,11 @@ fn infer_quote_of_global_call_returns_lifted_type() { let mut globals = HashMap::new(); globals.insert( Name::new("f"), - core_arena.alloc(core::Term::Pi(Pi { + core_arena.alloc(Pi { params: &[], body_ty: u64_ty_core, phase: Phase::Object, - })) as &_, + }) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); @@ -98,11 +98,11 @@ fn infer_quote_at_object_phase_fails() { let mut globals = HashMap::new(); globals.insert( Name::new("f"), - core_arena.alloc(core::Term::Pi(Pi { + core_arena.alloc(Pi { params: &[], body_ty: u64_ty_core, phase: Phase::Object, - })) as &_, + }) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); diff --git a/compiler/src/checker/test/signatures.rs b/compiler/src/checker/test/signatures.rs index f345103..205a75c 100644 --- a/compiler/src/checker/test/signatures.rs +++ b/compiler/src/checker/test/signatures.rs @@ -50,12 +50,9 @@ fn collect_signatures_two_functions() { assert_eq!(globals.len(), 2); - let id_ty = globals + let id_pi = globals .get(&Name::new("id")) .expect("id should be in globals"); - let core::Term::Pi(id_pi) = id_ty else { - panic!("expected Pi") - }; assert_eq!(id_pi.phase, Phase::Meta); assert_eq!(id_pi.params.len(), 1); assert_eq!(id_pi.params[0].0, "x"); @@ -74,12 +71,9 @@ fn collect_signatures_two_functions() { })) )); - let add_ty = globals + let add_pi = globals .get(&Name::new("add_one")) .expect("add_one should be in globals"); - let core::Term::Pi(add_pi) = add_ty else { - panic!("expected Pi") - }; assert_eq!(add_pi.phase, Phase::Object); assert_eq!(add_pi.params.len(), 1); assert_eq!(add_pi.params[0].0, "y"); diff --git a/compiler/src/core/mod.rs b/compiler/src/core/mod.rs index 61653ec..b3a81d2 100644 --- a/compiler/src/core/mod.rs +++ b/compiler/src/core/mod.rs @@ -73,23 +73,18 @@ pub struct Arm<'a> { } /// Elaborated top-level function definition. -/// -/// `ty` is always a `Term::Pi`; use `Function::pi()` for convenient access. #[derive(Debug)] pub struct Function<'a> { pub name: Name<'a>, - /// Function type (always `Term::Pi`). The Pi carries the phase, params, and return type. - pub ty: &'a Term<'a>, + /// Function type: phase, params, and return type. + pub ty: &'a Pi<'a>, pub body: &'a Term<'a>, } impl<'a> Function<'a> { - /// Unwrap `self.ty` as a `Pi`. Panics if `ty` is not a `Pi` (typechecker invariant). - pub fn pi(&self) -> &Pi<'a> { - match self.ty { - Term::Pi(pi) => pi, - _ => unreachable!("Function::ty must be a Pi (typechecker invariant)"), - } + /// Return the function's Pi type. + pub const fn pi(&self) -> &Pi<'a> { + self.ty } } diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs index 7c8f7e9..0402e71 100644 --- a/compiler/src/core/value.rs +++ b/compiler/src/core/value.rs @@ -4,8 +4,6 @@ //! environment extension + `eval`. `quote` converts values back to terms for //! error reporting and definitional equality checking. -use std::collections::HashMap; - use bumpalo::Bump; use super::prim::IntType; @@ -70,12 +68,7 @@ pub struct Closure<'a> { /// Evaluate a term in an environment, producing a semantic value. /// /// `env[env.len() - 1 - ix]` gives the value for `Var(Ix(ix))`. -pub fn eval<'a>( - arena: &'a Bump, - globals: &HashMap, &'a Term<'a>>, - env: &[Value<'a>], - term: &'a Term<'a>, -) -> Value<'a> { +pub fn eval<'a>(arena: &'a Bump, env: &[Value<'a>], term: &'a Term<'a>) -> Value<'a> { match term { Term::Var(ix) => { let i = env @@ -91,46 +84,43 @@ pub fn eval<'a>( Term::Lit(n, it) => Value::Lit(*n, *it), Term::Global(name) => Value::Global(*name), - Term::Lam(lam) => eval_lam(arena, globals, env, lam), - Term::Pi(pi) => eval_pi(arena, globals, env, pi), + Term::Lam(lam) => eval_lam(arena, env, lam), + Term::Pi(pi) => eval_pi(arena, env, pi), Term::App(app) => { - let func_val = eval(arena, globals, env, app.func); - let arg_vals: Vec> = app - .args - .iter() - .map(|a| eval(arena, globals, env, a)) - .collect(); - apply_many(arena, globals, func_val, &arg_vals) + let func_val = eval(arena, env, app.func); + let arg_vals: Vec> = + app.args.iter().map(|a| eval(arena, env, a)).collect(); + apply_many(arena, func_val, &arg_vals) } Term::Lift(inner) => { - let inner_val = eval(arena, globals, env, inner); + let inner_val = eval(arena, env, inner); Value::Lift(arena.alloc(inner_val)) } Term::Quote(inner) => { - let inner_val = eval(arena, globals, env, inner); + let inner_val = eval(arena, env, inner); Value::Quote(arena.alloc(inner_val)) } Term::Splice(inner) => { // In type-checking context: splice unwraps a Quote, otherwise propagates. - match eval(arena, globals, env, inner) { + match eval(arena, env, inner) { Value::Quote(v) => (*v).clone(), v => v, } } Term::Let(let_) => { - let val = eval(arena, globals, env, let_.expr); + let val = eval(arena, env, let_.expr); let mut env2: Vec> = env.to_vec(); env2.push(val); - eval(arena, globals, &env2, let_.body) + eval(arena, &env2, let_.body) } Term::Match(match_) => { - let scrut_val = eval(arena, globals, env, match_.scrutinee); + let scrut_val = eval(arena, env, match_.scrutinee); let n = match scrut_val { Value::Lit(n, _) => n, // Non-literal scrutinee: stuck, return neutral @@ -141,7 +131,7 @@ pub fn eval<'a>( for arm in match_.arms { match &arm.pat { Pat::Lit(m) if n == *m => { - return eval(arena, globals, env, arm.body); + return eval(arena, env, arm.body); } Pat::Lit(_) => continue, Pat::Bind(_) | Pat::Wildcard => { @@ -153,7 +143,7 @@ pub fn eval<'a>( phase: Phase::Meta, }, )); - return eval(arena, globals, &env2, arm.body); + return eval(arena, &env2, arm.body); } } } @@ -164,16 +154,11 @@ pub fn eval<'a>( } /// Evaluate a multi-param Pi, currying by slicing. -fn eval_pi<'a>( - arena: &'a Bump, - globals: &HashMap, &'a Term<'a>>, - env: &[Value<'a>], - pi: &'a Pi<'a>, -) -> Value<'a> { +pub fn eval_pi<'a>(arena: &'a Bump, env: &[Value<'a>], pi: &'a Pi<'a>) -> Value<'a> { match pi.params { - [] => eval(arena, globals, env, pi.body_ty), + [] => eval(arena, env, pi.body_ty), [(name, ty), rest @ ..] => { - let domain = eval(arena, globals, env, ty); + let domain = eval(arena, env, ty); let rest_body: &'a Term<'a> = if rest.is_empty() { pi.body_ty } else { @@ -198,16 +183,11 @@ fn eval_pi<'a>( } /// Evaluate a multi-param Lam, currying by slicing. -fn eval_lam<'a>( - arena: &'a Bump, - globals: &HashMap, &'a Term<'a>>, - env: &[Value<'a>], - lam: &'a Lam<'a>, -) -> Value<'a> { +fn eval_lam<'a>(arena: &'a Bump, env: &[Value<'a>], lam: &'a Lam<'a>) -> Value<'a> { match lam.params { - [] => eval(arena, globals, env, lam.body), + [] => eval(arena, env, lam.body), [(name, ty), rest @ ..] => { - let param_ty = eval(arena, globals, env, ty); + let param_ty = eval(arena, env, ty); let rest_body: &'a Term<'a> = if rest.is_empty() { lam.body } else { @@ -230,15 +210,10 @@ fn eval_lam<'a>( } /// Apply a single argument to a value. -pub fn apply<'a>( - arena: &'a Bump, - globals: &HashMap, &'a Term<'a>>, - func: Value<'a>, - arg: Value<'a>, -) -> Value<'a> { +pub fn apply<'a>(arena: &'a Bump, func: Value<'a>, arg: Value<'a>) -> Value<'a> { match func { - Value::Lam(vlam) => inst(arena, globals, &vlam.closure, arg), - Value::Pi(vpi) => inst(arena, globals, &vpi.closure, arg), + Value::Lam(vlam) => inst(arena, &vlam.closure, arg), + Value::Pi(vpi) => inst(arena, &vpi.closure, arg), Value::Rigid(lvl) => Value::App( arena.alloc(Value::Rigid(lvl)), arena.alloc_slice_fill_iter([arg]), @@ -264,26 +239,16 @@ pub fn apply<'a>( } /// Apply a value to multiple arguments in sequence. -pub fn apply_many<'a>( - arena: &'a Bump, - globals: &HashMap, &'a Term<'a>>, - func: Value<'a>, - args: &[Value<'a>], -) -> Value<'a> { +pub fn apply_many<'a>(arena: &'a Bump, func: Value<'a>, args: &[Value<'a>]) -> Value<'a> { args.iter() - .fold(func, |f, arg| apply(arena, globals, f, arg.clone())) + .fold(func, |f, arg| apply(arena, f, arg.clone())) } /// Instantiate a closure with one argument: extend env with arg, eval body. -pub fn inst<'a>( - arena: &'a Bump, - globals: &HashMap, &'a Term<'a>>, - closure: &Closure<'a>, - arg: Value<'a>, -) -> Value<'a> { +pub fn inst<'a>(arena: &'a Bump, closure: &Closure<'a>, arg: Value<'a>) -> Value<'a> { let mut env = closure.env.to_vec(); env.push(arg); - eval(arena, globals, &env, closure.body) + eval(arena, &env, closure.body) } /// Convert a value back to a term (for error reporting and definitional equality). @@ -313,9 +278,7 @@ pub fn quote<'a>(arena: &'a Bump, depth: Lvl, val: &Value<'a>) -> &'a Term<'a> { Value::Lam(vlam) => { // Apply the closure to a fresh rigid variable, then quote the result. let fresh = Value::Rigid(depth); - // For quoting we don't need globals (fresh vars are neutral). - let empty_globals: HashMap, &Term<'_>> = HashMap::new(); - let body_val = inst(arena, &empty_globals, &vlam.closure, fresh); + let body_val = inst(arena, &vlam.closure, fresh); let body_term = quote(arena, depth.succ(), &body_val); let param_ty_term = quote(arena, depth, vlam.param_ty); let params = arena.alloc_slice_fill_iter([(vlam.name, param_ty_term as &'a _)]); @@ -326,8 +289,7 @@ pub fn quote<'a>(arena: &'a Bump, depth: Lvl, val: &Value<'a>) -> &'a Term<'a> { } Value::Pi(vpi) => { let fresh = Value::Rigid(depth); - let empty_globals: HashMap, &Term<'_>> = HashMap::new(); - let body_val = inst(arena, &empty_globals, &vpi.closure, fresh); + let body_val = inst(arena, &vpi.closure, fresh); let body_term = quote(arena, depth.succ(), &body_val); let domain_term = quote(arena, depth, vpi.domain); let params = arena.alloc_slice_fill_iter([(vpi.name, domain_term as &'a _)]); @@ -356,12 +318,8 @@ pub fn val_eq<'a>(arena: &'a Bump, depth: Lvl, a: &Value<'a>, b: &Value<'a>) -> } /// Evaluate a term in the empty environment. -pub fn eval_closed<'a>( - arena: &'a Bump, - globals: &HashMap, &'a Term<'a>>, - term: &'a Term<'a>, -) -> Value<'a> { - eval(arena, globals, &[], term) +pub fn eval_closed<'a>(arena: &'a Bump, term: &'a Term<'a>) -> Value<'a> { + eval(arena, &[], term) } /// Extract the Phase from a Value that represents a universe (Type or `VmType`), diff --git a/compiler/src/eval/mod.rs b/compiler/src/eval/mod.rs index 6dc0906..d64dbf1 100644 --- a/compiler/src/eval/mod.rs +++ b/compiler/src/eval/mod.rs @@ -114,7 +114,7 @@ impl<'out, 'eval> Env<'out, 'eval> { /// Everything the evaluator needs to know about a top-level function. struct GlobalDef<'a> { - ty: &'a Term<'a>, // always Term::Pi + ty: &'a Pi<'a>, body: &'a Term<'a>, } @@ -148,9 +148,7 @@ fn eval_meta<'out, 'eval>( let def = globals .get(name) .unwrap_or_else(|| panic!("unknown global `{name}` during staging")); - let Term::Pi(pi) = def.ty else { - unreachable!("global `{name}` must have a Pi type (typechecker invariant)") - }; + let pi = def.ty; if pi.params.is_empty() { // Zero-param global: evaluate the body immediately in a fresh env. let mut callee_env = Env::new(env.obj_next); @@ -249,9 +247,7 @@ fn global_to_closure<'out, 'eval>( obj_next: Lvl, ) -> MetaVal<'out, 'eval> { // Called only when params is non-empty (zero-param globals are evaluated immediately). - let Term::Pi(pi) = def.ty else { - unreachable!("global must have a Pi type (typechecker invariant)") - }; + let pi = def.ty; let body = match pi.params { [_] | [] => def.body, [_, rest @ ..] => eval_arena.alloc(Term::Lam(Lam { @@ -759,15 +755,13 @@ pub fn unstage_program<'out, 'core>( let staged_ret_ty = unstage_obj(arena, &eval_bump, &globals, &mut env, pi.body_ty)?; let staged_body = unstage_obj(arena, &eval_bump, &globals, &mut env, f.body)?; - let staged_ty = arena.alloc(Term::Pi(Pi { - params: staged_params, - body_ty: staged_ret_ty, - phase: Phase::Object, - })); - Ok(Function { name: Name::new(arena.alloc_str(f.name.as_str())), - ty: staged_ty, + ty: arena.alloc(Pi { + params: staged_params, + body_ty: staged_ret_ty, + phase: Phase::Object, + }), body: staged_body, }) }) diff --git a/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt b/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt index ad5e030..92f3470 100644 --- a/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt +++ b/compiler/tests/snap/full/pi_apply_non_fn/3_check.txt @@ -1,2 +1,2 @@ ERROR -in function `test`: callee is not a function type +in function `test`: too many arguments at argument 0 diff --git a/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt b/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt index 4051012..271e428 100644 --- a/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt +++ b/compiler/tests/snap/full/pi_arity_mismatch/3_check.txt @@ -1,2 +1,2 @@ ERROR -in function `apply`: wrong number of arguments: callee expects 1, got 2 +in function `apply`: too many arguments at argument 1 diff --git a/compiler/tests/snap/full/pi_const/3_check.txt b/compiler/tests/snap/full/pi_const/3_check.txt index 7f55460..5c4d60c 100644 --- a/compiler/tests/snap/full/pi_const/3_check.txt +++ b/compiler/tests/snap/full/pi_const/3_check.txt @@ -1,2 +1,12 @@ -ERROR -in function `const_`: lambda has 1 parameter(s) but expected type has 2 +fn const_(A@0: Type, B@1: Type) -> fn(_: A@0) -> fn(_: B@1) -> A@0 { + |a@2: A@0| |b@3: B@1| a@2 +} + +fn test() -> u64 { + const_(u64, u8)(42_u64)(7_u8) +} + +code fn result() -> u64 { + $(@embed_u64(test())) +} + diff --git a/compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt b/compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt index ad5e030..c9b7ada 100644 --- a/compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt +++ b/compiler/tests/snap/full/pi_lambda_empty_params/3_check.txt @@ -1,2 +1,12 @@ -ERROR -in function `test`: callee is not a function type +fn make_thunk(x@0: u64) -> fn() -> u64 { + || x@0 +} + +fn test() -> u64 { + make_thunk(42_u64)() +} + +code fn result() -> u64 { + $(@embed_u64(test())) +} + From 32a11ee0957a4196d1a9120268debf0fdb91a788 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 18:25:18 +0000 Subject: [PATCH 29/43] refactor: add Lvl/Ix conversion methods and use in codebase; simplify pretty.rs match - Add Lvl::ix_at_depth(self, depth: Lvl) -> Ix and Ix::lvl_at_depth(self, depth: Lvl) -> Lvl methods to core/mod.rs for De Bruijn level<->index conversions - Replace standalone lvl_to_ix/ix_to_lvl function calls with method calls in value.rs and checker/mod.rs - Remove lvl_to_ix import from value.rs and checker/mod.rs since now using methods - Simplify pretty.rs:fmt_term match: replace explicit enum arm list with wildcard (_) for all variants except Let and Match Co-Authored-By: Claude Haiku 4.5 --- compiler/src/checker/mod.rs | 6 +++--- compiler/src/core/mod.rs | 10 ++++++++++ compiler/src/core/pretty.rs | 11 +---------- compiler/src/core/value.rs | 4 ++-- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index 9a3fb6c..3a73bc2 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use anyhow::{Context as _, Result, anyhow, bail, ensure}; -use crate::core::{self, IntType, IntWidth, Ix, Lam, Lvl, Pi, Prim, alpha_eq, lvl_to_ix, value}; +use crate::core::{self, IntType, IntWidth, Ix, Lam, Lvl, Pi, Prim, alpha_eq, value}; use crate::parser::ast::{self, Phase}; /// Elaboration context. @@ -106,7 +106,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { pub fn lookup_local(&self, name: &str) -> Option<(Ix, &value::Value<'core>)> { for (i, &local_name) in self.names.iter().enumerate().rev() { if local_name == name { - let ix = lvl_to_ix(self.lvl, Lvl(i)); + let ix = Lvl(i).ix_at_depth(self.lvl); let ty = self .types .get(i) @@ -499,7 +499,7 @@ fn value_type_universe_ctx<'core>(ctx: &Ctx<'core, '_>, ty: &value::Value<'core> // A rigid variable: look up its type in the context to determine phase. value::Value::Rigid(lvl) => { // lvl is the De Bruijn level; convert to index - let ix = lvl_to_ix(ctx.lvl, *lvl); + let ix = lvl.ix_at_depth(ctx.lvl); let i = ctx.types.len().checked_sub(1 + ix.0)?; let var_ty = ctx.types.get(i)?; // If the variable's type is U(phase), then it classifies types in phase. diff --git a/compiler/src/core/mod.rs b/compiler/src/core/mod.rs index b3a81d2..c819006 100644 --- a/compiler/src/core/mod.rs +++ b/compiler/src/core/mod.rs @@ -20,6 +20,11 @@ impl Lvl { pub const fn succ(self) -> Self { Self(self.0 + 1) } + + #[must_use] + pub const fn ix_at_depth(self, depth: Self) -> Ix { + Ix(depth.0 - self.0 - 1) + } } /// De Bruijn index (counts from nearest enclosing binder, 0 = innermost) @@ -35,6 +40,11 @@ impl Ix { pub const fn succ(self) -> Self { Self(self.0 + 1) } + + #[must_use] + pub const fn lvl_at_depth(self, depth: Lvl) -> Self { + Self(depth.0 - self.0 - 1) + } } /// Convert a De Bruijn level to an index given the current depth. diff --git a/compiler/src/core/pretty.rs b/compiler/src/core/pretty.rs index a9ba444..d1bea79 100644 --- a/compiler/src/core/pretty.rs +++ b/compiler/src/core/pretty.rs @@ -26,16 +26,7 @@ impl<'a> Term<'a> { // Let and Match manage their own indentation internally. Term::Let(_) | Term::Match(_) => self.fmt_term_inline(env, indent, f), // Everything else gets a leading indent. - Term::Var(_) - | Term::Prim(_) - | Term::Lit(..) - | Term::Global(_) - | Term::App(_) - | Term::Pi(_) - | Term::Lam(_) - | Term::Lift(_) - | Term::Quote(_) - | Term::Splice(_) => { + _ => { write_indent(f, indent)?; self.fmt_term_inline(env, indent, f) } diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs index 0402e71..2e634f3 100644 --- a/compiler/src/core/value.rs +++ b/compiler/src/core/value.rs @@ -7,7 +7,7 @@ use bumpalo::Bump; use super::prim::IntType; -use super::{Lam, Lvl, Name, Pat, Pi, Prim, Term, lvl_to_ix}; +use super::{Lam, Lvl, Name, Pat, Pi, Prim, Term}; use crate::common::Phase; /// Working evaluation environment: index 0 = outermost binding, last = innermost. @@ -257,7 +257,7 @@ pub fn inst<'a>(arena: &'a Bump, closure: &Closure<'a>, arg: Value<'a>) -> Value pub fn quote<'a>(arena: &'a Bump, depth: Lvl, val: &Value<'a>) -> &'a Term<'a> { match val { Value::Rigid(lvl) => { - let ix = lvl_to_ix(depth, *lvl); + let ix = lvl.ix_at_depth(depth); arena.alloc(Term::Var(ix)) } Value::Global(name) => arena.alloc(Term::Global(*name)), From 5fa0ca9fcb7356eb312ecea3f8fc8f15217d5a0a Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 18:28:06 +0000 Subject: [PATCH 30/43] refactor: remove standalone lvl_to_ix and ix_to_lvl functions All callers have been converted to use the Lvl::ix_at_depth() and Ix::lvl_at_depth() methods instead. Remove the standalone function definitions to avoid duplication and encourage use of the method-based API. Co-Authored-By: Claude Haiku 4.5 --- compiler/src/core/mod.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/compiler/src/core/mod.rs b/compiler/src/core/mod.rs index c819006..1621fe4 100644 --- a/compiler/src/core/mod.rs +++ b/compiler/src/core/mod.rs @@ -47,16 +47,6 @@ impl Ix { } } -/// Convert a De Bruijn level to an index given the current depth. -pub const fn lvl_to_ix(depth: Lvl, lvl: Lvl) -> Ix { - Ix(depth.0 - lvl.0 - 1) -} - -/// Convert a De Bruijn index to a level given the current depth. -pub const fn ix_to_lvl(depth: Lvl, ix: Ix) -> Lvl { - Lvl(depth.0 - ix.0 - 1) -} - /// Match pattern in the core IR #[derive(Debug, Clone, PartialEq, Eq)] pub enum Pat<'a> { From 4757804c4f41d3ddec11fe659df19550dc433e90 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 18:31:30 +0000 Subject: [PATCH 31/43] refactor: use Ix::lvl_at_depth() method in pretty.rs variable printing Replace manual De Bruijn index-to-level conversion (checked_sub pattern) with the Ix::lvl_at_depth() method call. Co-Authored-By: Claude Haiku 4.5 --- compiler/src/core/pretty.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compiler/src/core/pretty.rs b/compiler/src/core/pretty.rs index d1bea79..c4558eb 100644 --- a/compiler/src/core/pretty.rs +++ b/compiler/src/core/pretty.rs @@ -47,10 +47,8 @@ impl<'a> Term<'a> { match self { // ── Variable ───────────────────────────────────────────────────────── Term::Var(ix) => { - let i = env - .len() - .checked_sub(1 + ix.0) - .expect("De Bruijn index out of environment bounds"); + let lvl = ix.lvl_at_depth(super::Lvl(env.len())); + let i = lvl.0; let name = env .get(i) .expect("De Bruijn index out of environment bounds"); From bf23e8294b50a58a2a26a58300a8f5f443c406e5 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 18:33:20 +0000 Subject: [PATCH 32/43] refactor: use Ix::lvl_at_depth() method in value.rs variable evaluation Replace manual De Bruijn index-to-level conversion (checked_sub pattern) with the Ix::lvl_at_depth() method call for consistency. Co-Authored-By: Claude Haiku 4.5 --- compiler/src/core/value.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs index 2e634f3..4df299f 100644 --- a/compiler/src/core/value.rs +++ b/compiler/src/core/value.rs @@ -71,10 +71,8 @@ pub struct Closure<'a> { pub fn eval<'a>(arena: &'a Bump, env: &[Value<'a>], term: &'a Term<'a>) -> Value<'a> { match term { Term::Var(ix) => { - let i = env - .len() - .checked_sub(1 + ix.0) - .expect("De Bruijn index out of environment bounds"); + let lvl = ix.lvl_at_depth(Lvl(env.len())); + let i = lvl.0; env.get(i) .expect("De Bruijn index out of environment bounds") .clone() From 88fe5e9dc43518daf5b9ac39371f9c09c6e16940 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 18:43:34 +0000 Subject: [PATCH 33/43] refactor: remove redundant Value::U(Phase) variant, consolidate with Prim::U Value::U(Phase) is redundant since Prim::U(Phase) already exists. All match sites that handled Value::U(p) are updated to use Value::Prim(Prim::U(p)) instead. Updated: - core/value.rs: quote function, apply function, value_phase function - checker/mod.rs: val_type_of creation, type_phase, check_lift - checker/test/meta.rs: test assertion Co-Authored-By: Claude Haiku 4.5 --- compiler/src/checker/mod.rs | 11 +++++------ compiler/src/checker/test/meta.rs | 2 +- compiler/src/core/value.rs | 10 ++-------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index 3a73bc2..87b42f7 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -169,10 +169,10 @@ impl<'core, 'globals> Ctx<'core, 'globals> { } // Primitive types inhabit the relevant universe. - core::Term::Prim(Prim::IntTy(it)) => value::Value::U(it.phase), + core::Term::Prim(Prim::IntTy(it)) => value::Value::Prim(Prim::U(it.phase)), // Type, VmType, and [[T]] all inhabit Type (meta universe). core::Term::Prim(Prim::U(_)) | core::Term::Lift(_) | core::Term::Pi(_) => { - value::Value::U(Phase::Meta) + value::Value::Prim(Prim::U(Phase::Meta)) } // Comparison ops return u1 at the operand phase. @@ -477,8 +477,7 @@ const fn value_type_universe(ty: &value::Value<'_>) -> Option { value::Value::Prim(Prim::IntTy(IntType { phase, .. })) => Some(*phase), value::Value::Prim(Prim::U(_)) | value::Value::Lift(_) - | value::Value::Pi(_) - | value::Value::U(_) => Some(Phase::Meta), + | value::Value::Pi(_) => Some(Phase::Meta), // Neutral or unknown — can't determine phase value::Value::Rigid(_) | value::Value::Global(_) @@ -504,7 +503,7 @@ fn value_type_universe_ctx<'core>(ctx: &Ctx<'core, '_>, ty: &value::Value<'core> let var_ty = ctx.types.get(i)?; // If the variable's type is U(phase), then it classifies types in phase. match var_ty { - value::Value::Prim(Prim::U(p)) | value::Value::U(p) => Some(*p), + value::Value::Prim(Prim::U(p)) => Some(*p), _ => None, } } @@ -749,7 +748,7 @@ pub fn infer<'src, 'core>( let inner_ty_val = ctx.val_type_of(core_inner); let is_vm_type = matches!( &inner_ty_val, - value::Value::Prim(Prim::U(Phase::Object)) | value::Value::U(Phase::Object) + value::Value::Prim(Prim::U(Phase::Object)) ); ensure!(is_vm_type, "argument of `[[...]]` must be an object type"); Ok(ctx.alloc(core::Term::Lift(core_inner))) diff --git a/compiler/src/checker/test/meta.rs b/compiler/src/checker/test/meta.rs index f4ed8b4..65d25e8 100644 --- a/compiler/src/checker/test/meta.rs +++ b/compiler/src/checker/test/meta.rs @@ -16,7 +16,7 @@ fn infer_lift_of_object_type_returns_type_universe() { // Elaborated at meta phase: type of [[u64]] is Type (meta universe) let result = infer(&mut ctx, Phase::Meta, term).expect("should infer"); let ty_val = ctx.val_type_of(result); - assert!(matches!(ty_val, value::Value::U(Phase::Meta))); + assert!(matches!(ty_val, value::Value::Prim(Prim::U(Phase::Meta)))); } // `[[u64]]` is illegal at object phase — Lift is only meaningful in meta context. diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs index 4df299f..1428f6a 100644 --- a/compiler/src/core/value.rs +++ b/compiler/src/core/value.rs @@ -35,8 +35,6 @@ pub enum Value<'a> { Lift(&'a Self), /// Canonical: quoted object code `#(t)` Quote(&'a Self), - /// Canonical: universe `Type` or `VmType` - U(Phase), } /// Lambda value: parameter name, parameter type, and body closure. @@ -229,7 +227,7 @@ pub fn apply<'a>(arena: &'a Bump, func: Value<'a>, arg: Value<'a>) -> Value<'a> arena.alloc(Value::Prim(p)), arena.alloc_slice_fill_iter([arg]), ), - Value::Lit(..) | Value::Lift(_) | Value::Quote(_) | Value::U(_) => { + Value::Lit(..) | Value::Lift(_) | Value::Quote(_) => { // Should not happen in well-typed programs panic!("apply: function position holds non-function value") } @@ -261,10 +259,6 @@ pub fn quote<'a>(arena: &'a Bump, depth: Lvl, val: &Value<'a>) -> &'a Term<'a> { Value::Global(name) => arena.alloc(Term::Global(*name)), Value::Prim(p) => arena.alloc(Term::Prim(*p)), Value::Lit(n, it) => arena.alloc(Term::Lit(*n, *it)), - Value::U(phase) => match phase { - Phase::Meta => &Term::TYPE, - Phase::Object => &Term::VM_TYPE, - }, Value::App(f, args) => { let qf = quote(arena, depth, f); let qargs: Vec<&'a Term<'a>> = args @@ -325,7 +319,7 @@ pub fn eval_closed<'a>(arena: &'a Bump, term: &'a Term<'a>) -> Value<'a> { pub const fn value_phase(val: &Value<'_>) -> Option { match val { Value::Prim(Prim::IntTy(it)) => Some(it.phase), - Value::Prim(Prim::U(_)) | Value::Lift(_) | Value::Pi(_) | Value::U(_) => Some(Phase::Meta), + Value::Prim(Prim::U(_)) | Value::Lift(_) | Value::Pi(_) => Some(Phase::Meta), _ => None, } } From bd1d6ec49424f41cc3a21f0699985ae4fdac3296 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 18:43:52 +0000 Subject: [PATCH 34/43] fix: replace dummy value with unreachable! in match evaluation Replace the nonsensical placeholder Value::Rigid(Lvl(usize::MAX)) with unreachable!() since this code path should never be reached in well-typed programs. Co-Authored-By: Claude Haiku 4.5 --- compiler/src/core/value.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs index 1428f6a..fdc8f4a 100644 --- a/compiler/src/core/value.rs +++ b/compiler/src/core/value.rs @@ -144,7 +144,7 @@ pub fn eval<'a>(arena: &'a Bump, env: &[Value<'a>], term: &'a Term<'a>) -> Value } } // Non-exhaustive match (should not happen in well-typed code) - Value::Rigid(Lvl(usize::MAX)) + unreachable!("non-exhaustive pattern match in match term evaluation") } } } From 169f69afcdb91186e8bb8bc688f13ca884122a25 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 18:44:17 +0000 Subject: [PATCH 35/43] refactor: use static &[] instead of arena.alloc_slice_fill_iter([]) Empty slices don't need arena allocation; use the static empty slice &[] directly for the args in stuck App values. Co-Authored-By: Claude Haiku 4.5 --- compiler/src/core/value.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs index fdc8f4a..44feebd 100644 --- a/compiler/src/core/value.rs +++ b/compiler/src/core/value.rs @@ -121,7 +121,7 @@ pub fn eval<'a>(arena: &'a Bump, env: &[Value<'a>], term: &'a Term<'a>) -> Value Value::Lit(n, _) => n, // Non-literal scrutinee: stuck, return neutral other => { - return Value::App(arena.alloc(other), arena.alloc_slice_fill_iter([])); + return Value::App(arena.alloc(other), &[]); } }; for arm in match_.arms { From 55b235acd65ad2d83d65b9ce2c7394bc9f543663 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 18:44:58 +0000 Subject: [PATCH 36/43] docs: add TODO for issue #24 (hardcoded u64 in pattern bindings) Add comment referencing GitHub issue #24 about the hardcoded u64 type used when binding pattern-matched values. Type should come from the scrutinee, not be hardcoded. Co-Authored-By: Claude Haiku 4.5 --- compiler/src/core/value.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs index 44feebd..b5b47ed 100644 --- a/compiler/src/core/value.rs +++ b/compiler/src/core/value.rs @@ -132,6 +132,7 @@ pub fn eval<'a>(arena: &'a Bump, env: &[Value<'a>], term: &'a Term<'a>) -> Value Pat::Lit(_) => continue, Pat::Bind(_) | Pat::Wildcard => { let mut env2 = env.to_vec(); + // TODO(#24): Type should come from scrutinee, not hardcoded u64 env2.push(Value::Lit( n, IntType { From fd9f47fdbb543ca1958baa84147fbe7e06e23e01 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 19:13:05 +0000 Subject: [PATCH 37/43] refactor: update core IR structures to use Name instead of str - Changed core::Pat::Bind from Bind(&str) to Bind(Name) - Changed core::Let::name from &str to Name - Changed core::Lam params from &[(& str, &Term)] to &[(Name, &Term)] - Changed core::Pi params from &[(&str, &Term)] to &[(Name, &Term)] - Updated Ctx.names from Vec<&core str> to Vec - Updated Ctx method signatures (push_local, push_local_val, push_let_binding, lookup_local) to use Name - Updated elaboration code to construct Name values at allocation time - Updated test code push_local calls to wrap strings with core::Name::new() - Work in progress: still fixing remaining callsites in pretty.rs, eval.rs, and test files Co-Authored-By: Claude Haiku 4.5 --- compiler/src/checker/mod.rs | 38 +++++++++++++-------------- compiler/src/checker/test/apply.rs | 24 ++++++++--------- compiler/src/checker/test/context.rs | 20 +++++++------- compiler/src/checker/test/locals.rs | 2 +- compiler/src/checker/test/matching.rs | 10 +++---- compiler/src/checker/test/meta.rs | 12 ++++----- compiler/src/checker/test/var.rs | 10 +++---- compiler/src/core/mod.rs | 16 +++++------ 8 files changed, 66 insertions(+), 66 deletions(-) diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index 87b42f7..47f246c 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -17,7 +17,7 @@ pub struct Ctx<'core, 'globals> { /// Arena for allocating core terms arena: &'core bumpalo::Bump, /// Local variable names (oldest first), for error messages. - names: Vec<&'core str>, + names: Vec>, /// Evaluation environment (oldest first): values of locals. /// `env[env.len() - 1 - ix]` = value of `Var(Ix(ix))`. env: value::Env<'core>, @@ -62,7 +62,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Push a local variable onto the context, given its type as a term. /// Evaluates the type term in the current environment. - pub fn push_local(&mut self, name: &'core str, ty: &'core core::Term<'core>) { + pub fn push_local(&mut self, name: core::Name<'core>, ty: &'core core::Term<'core>) { let ty_val = value::eval(self.arena, &self.env, ty); self.env.push(value::Value::Rigid(self.lvl)); self.types.push(ty_val); @@ -72,7 +72,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Push a local variable onto the context, given its type as a Value. /// The variable itself is a fresh rigid (neutral) variable — use for lambda/pi params. - fn push_local_val(&mut self, name: &'core str, ty_val: value::Value<'core>) { + fn push_local_val(&mut self, name: core::Name<'core>, ty_val: value::Value<'core>) { self.env.push(value::Value::Rigid(self.lvl)); self.types.push(ty_val); self.lvl = self.lvl.succ(); @@ -83,7 +83,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Use for `let x = e` bindings so that dependent references to `x` evaluate correctly. fn push_let_binding( &mut self, - name: &'core str, + name: core::Name<'core>, ty_val: value::Value<'core>, expr_val: value::Value<'core>, ) { @@ -103,9 +103,9 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Look up a variable by name, returning its (index, type as Value). /// Searches from the most recently pushed variable inward to handle shadowing. - pub fn lookup_local(&self, name: &str) -> Option<(Ix, &value::Value<'core>)> { - for (i, &local_name) in self.names.iter().enumerate().rev() { - if local_name == name { + pub fn lookup_local(&self, name: core::Name<'_>) -> Option<(Ix, &value::Value<'core>)> { + for (i, local_name) in self.names.iter().enumerate().rev() { + if *local_name == name { let ix = Lvl(i).ix_at_depth(self.lvl); let ty = self .types @@ -357,7 +357,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { lvl: self.lvl, globals: self.globals, }; - fake_ctx.push_local(name, scrut_ty_term); + fake_ctx.push_local(core::Name::new(name), scrut_ty_term); fake_ctx.val_type_of(arm.body) } } @@ -390,9 +390,9 @@ fn elaborate_sig<'src, 'core>( let empty_globals = HashMap::new(); let mut ctx = Ctx::new(arena, &empty_globals); - let params: &'core [(&'core str, &'core core::Term<'core>)] = + let params: &'core [(core::Name<'core>, &'core core::Term<'core>)] = arena.alloc_slice_try_fill_iter(func.params.iter().map(|p| -> Result<_> { - let param_name: &'core str = arena.alloc_str(p.name.as_str()); + let param_name = core::Name::new(arena.alloc_str(p.name.as_str())); let param_ty = infer(&mut ctx, func.phase, p.ty)?; ctx.push_local(param_name, param_ty); Ok((param_name, param_ty)) @@ -446,7 +446,7 @@ fn elaborate_bodies<'src, 'core>( // Push parameters as locals so the body can reference them. for (pname, pty) in pi.params { - ctx.push_local(pname, pty); + ctx.push_local(core::Name::new(pname), pty); } // Elaborate the body, checking it against the declared return type. @@ -677,9 +677,9 @@ pub fn infer<'src, 'core>( ); let depth_before = ctx.depth(); - let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); + let mut elaborated_params: Vec<(core::Name<'core>, &'core core::Term<'core>)> = Vec::new(); for p in *params { - let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); + let param_name = core::Name::new(ctx.arena.alloc_str(p.name.as_str())); let param_ty = infer(ctx, Phase::Meta, p.ty)?; ensure!( value_type_universe_ctx(ctx, &ctx.eval(param_ty)).is_some(), @@ -716,10 +716,10 @@ pub fn infer<'src, 'core>( ); let depth_before = ctx.depth(); - let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); + let mut elaborated_params: Vec<(core::Name<'core>, &'core core::Term<'core>)> = Vec::new(); for p in *params { - let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); + let param_name = core::Name::new(ctx.arena.alloc_str(p.name.as_str())); let param_ty = infer(ctx, Phase::Meta, p.ty)?; elaborated_params.push((param_name, param_ty)); ctx.push_local(param_name, param_ty); @@ -910,7 +910,7 @@ fn elaborate_pat<'core>(ctx: &Ctx<'core, '_>, pat: &ast::Pat<'_>) -> core::Pat<' if s == "_" { core::Pat::Wildcard } else { - let bound: &'core str = ctx.arena.alloc_str(s); + let bound = core::Name::new(ctx.arena.alloc_str(s)); core::Pat::Bind(bound) } } @@ -947,7 +947,7 @@ where let bind_ty_term = ctx.quote_val(&bind_ty_val); // Evaluate the bound expression so dependent references to this binding work correctly. let expr_val = ctx.eval(core_expr); - let bind_name: &'core str = ctx.arena.alloc_str(stmt.name.as_str()); + let bind_name = core::Name::new(ctx.arena.alloc_str(stmt.name.as_str())); ctx.push_let_binding(bind_name, bind_ty_val, expr_val); let cont_result = cont(ctx); ctx.pop_local(); @@ -1204,7 +1204,7 @@ pub fn check_val<'src, 'core>( than the expected function type" ); elaborated_params.push((param_name, annotated_ty)); - ctx.push_local_val(param_name, pi_param_ty); + ctx.push_local_val(core::Name::new(param_name), pi_param_ty); } let core_body = check_val(ctx, phase, body, body_ty_val)?; @@ -1233,7 +1233,7 @@ pub fn check_val<'src, 'core>( .alloc_slice_try_fill_iter(arms.iter().map(|arm| -> Result<_> { let core_pat = elaborate_pat(ctx, &arm.pat); if let Some(bname) = core_pat.bound_name() { - ctx.push_local(bname, scrut_ty_term); + ctx.push_local(core::Name::new(bname), scrut_ty_term); } let arm_result = check_val(ctx, phase, arm.body, expected.clone()); diff --git a/compiler/src/checker/test/apply.rs b/compiler/src/checker/test/apply.rs index fa61741..88746cc 100644 --- a/compiler/src/checker/test/apply.rs +++ b/compiler/src/checker/test/apply.rs @@ -122,8 +122,8 @@ fn check_binop_add_against_u32_succeeds() { let mut ctx = test_ctx(&core_arena); let u32_obj = core::Term::int_ty(IntWidth::U32, Phase::Object); // push two object-phase u32 locals to use as operands - ctx.push_local("a", u32_obj); - ctx.push_local("b", u32_obj); + ctx.push_local(core::Name::new("a"), u32_obj); + ctx.push_local(core::Name::new("b"), u32_obj); let a = src_arena.alloc(ast::Term::Var(ast::Name::new("a"))); let b = src_arena.alloc(ast::Term::Var(ast::Name::new("b"))); @@ -154,8 +154,8 @@ fn infer_comparison_op_returns_u1() { let core_arena = bumpalo::Bump::new(); let mut ctx = test_ctx(&core_arena); let u64_obj = core::Term::int_ty(IntWidth::U64, Phase::Object); - ctx.push_local("a", u64_obj); - ctx.push_local("b", u64_obj); + ctx.push_local(core::Name::new("a"), u64_obj); + ctx.push_local(core::Name::new("b"), u64_obj); let a = src_arena.alloc(ast::Term::Var(ast::Name::new("a"))); let b = src_arena.alloc(ast::Term::Var(ast::Name::new("b"))); @@ -195,8 +195,8 @@ fn infer_comparison_op_mismatched_operands_fails() { let mut ctx = test_ctx(&core_arena); let u64_ty = &core::Term::U64_META; let u32_ty = &core::Term::U32_META; - ctx.push_local("a", u64_ty); - ctx.push_local("b", u32_ty); // different type + ctx.push_local(core::Name::new("a"), u64_ty); + ctx.push_local(core::Name::new("b"), u32_ty); // different type let a = src_arena.alloc(ast::Term::Var(ast::Name::new("a"))); let b = src_arena.alloc(ast::Term::Var(ast::Name::new("b"))); @@ -216,8 +216,8 @@ fn infer_binop_add_without_expected_type_fails() { let core_arena = bumpalo::Bump::new(); let mut ctx = test_ctx(&core_arena); let u32_ty = &core::Term::U32_META; - ctx.push_local("a", u32_ty); - ctx.push_local("b", u32_ty); + ctx.push_local(core::Name::new("a"), u32_ty); + ctx.push_local(core::Name::new("b"), u32_ty); let a = src_arena.alloc(ast::Term::Var(ast::Name::new("a"))); let b = src_arena.alloc(ast::Term::Var(ast::Name::new("b"))); @@ -239,8 +239,8 @@ fn check_binop_add_with_mismatched_operand_types_fails() { // push a (u64) and b (u32) — they don't match the expected u32 for 'a' let u64_ty = &core::Term::U64_META; let u32_ty = &core::Term::U32_META; - ctx.push_local("a", u64_ty); // u64, but op expects u32 - ctx.push_local("b", u32_ty); + ctx.push_local(core::Name::new("a"), u64_ty); // u64, but op expects u32 + ctx.push_local(core::Name::new("b"), u32_ty); let a = src_arena.alloc(ast::Term::Var(ast::Name::new("a"))); let b = src_arena.alloc(ast::Term::Var(ast::Name::new("b"))); @@ -263,8 +263,8 @@ fn check_eq_op_produces_u1() { let mut ctx = test_ctx(&core_arena); // Use meta-phase locals so the phase is consistent throughout. let u64_ty = &core::Term::U64_META; // u64 at meta phase - ctx.push_local("a", u64_ty); - ctx.push_local("b", u64_ty); + ctx.push_local(core::Name::new("a"), u64_ty); + ctx.push_local(core::Name::new("b"), u64_ty); let a = src_arena.alloc(ast::Term::Var(ast::Name::new("a"))); let b = src_arena.alloc(ast::Term::Var(ast::Name::new("b"))); diff --git a/compiler/src/checker/test/context.rs b/compiler/src/checker/test/context.rs index 0571a05..15a5b07 100644 --- a/compiler/src/checker/test/context.rs +++ b/compiler/src/checker/test/context.rs @@ -33,7 +33,7 @@ fn variable_lookup_after_push() { let arena = bumpalo::Bump::new(); let mut ctx = test_ctx(&arena); let u64_term = &core::Term::U64_META; - ctx.push_local("x", u64_term); + ctx.push_local(core::Name::new("x"), u64_term); let (ix, ty) = ctx.lookup_local("x").expect("x should be in scope"); assert_eq!(ix, Ix(0)); @@ -53,8 +53,8 @@ fn variable_lookup_with_multiple_locals() { let u64_term = &core::Term::U64_META; let u32_term = &core::Term::U32_META; - ctx.push_local("x", u64_term); - ctx.push_local("y", u32_term); + ctx.push_local(core::Name::new("x"), u64_term); + ctx.push_local(core::Name::new("y"), u32_term); // With two locals, "y" is innermost (index 0), "x" is outer (index 1). let (ix_y, ty_y) = ctx.lookup_local("y").expect("y should be in scope"); @@ -85,8 +85,8 @@ fn variable_shadowing() { let u64_term = &core::Term::U64_META; let u32_term = &core::Term::U32_META; - ctx.push_local("x", u64_term); - ctx.push_local("x", u32_term); + ctx.push_local(core::Name::new("x"), u64_term); + ctx.push_local(core::Name::new("x"), u32_term); // Innermost "x" shadows outer; it is at index 0. let (ix, ty) = ctx.lookup_local("x").expect("x should be in scope"); @@ -107,9 +107,9 @@ fn context_depth() { let u64_term = &core::Term::U64_META; assert_eq!(ctx.depth(), 0); - ctx.push_local("x", u64_term); + ctx.push_local(core::Name::new("x"), u64_term); assert_eq!(ctx.depth(), 1); - ctx.push_local("y", u64_term); + ctx.push_local(core::Name::new("y"), u64_term); assert_eq!(ctx.depth(), 2); ctx.pop_local(); assert_eq!(ctx.depth(), 1); @@ -121,7 +121,7 @@ fn meta_variable_in_quote_is_ok() { let mut ctx = test_ctx(&arena); let u64_term = &core::Term::U64_META; let lifted_u64 = ctx.lift_ty(u64_term); - ctx.push_local("x", lifted_u64); + ctx.push_local(core::Name::new("x"), lifted_u64); let x_var = arena.alloc(core::Term::Var(Ix(0))); assert!(matches!(x_var, core::Term::Var(Ix(0)))); } @@ -131,7 +131,7 @@ fn object_variable_outside_quote_is_invalid() { let arena = bumpalo::Bump::new(); let mut ctx = test_ctx(&arena); let u64_term = &core::Term::U64_META; - ctx.push_local("x", u64_term); + ctx.push_local(core::Name::new("x"), u64_term); assert_eq!(ctx.depth(), 1); } @@ -211,7 +211,7 @@ fn splice_inference_mirrors_inner() { let mut ctx = test_ctx(&arena); let u64_term = &core::Term::U64_META; let lifted_u64 = ctx.lift_ty(u64_term); - ctx.push_local("x", lifted_u64); + ctx.push_local(core::Name::new("x"), lifted_u64); let x_var = arena.alloc(core::Term::Var(Ix(0))); let spliced = arena.alloc(core::Term::Splice(x_var)); assert!(matches!(spliced, core::Term::Splice(_))); diff --git a/compiler/src/checker/test/locals.rs b/compiler/src/checker/test/locals.rs index 323f776..d90b083 100644 --- a/compiler/src/checker/test/locals.rs +++ b/compiler/src/checker/test/locals.rs @@ -78,7 +78,7 @@ fn infer_let_annotation_mismatch_fails() { let core_arena = bumpalo::Bump::new(); let mut ctx = test_ctx(&core_arena); let u64_ty = &core::Term::U64_META; - ctx.push_local("y", u64_ty); // y: u64 + ctx.push_local(core::Name::new("y"), u64_ty); // y: u64 // `let x: u32 = y; x` — y is u64, annotation says u32 let ty_ann = src_arena.alloc(ast::Term::Var(ast::Name::new("u32"))); diff --git a/compiler/src/checker/test/matching.rs b/compiler/src/checker/test/matching.rs index 31b45da..043b49e 100644 --- a/compiler/src/checker/test/matching.rs +++ b/compiler/src/checker/test/matching.rs @@ -20,7 +20,7 @@ fn check_match_all_arms_same_type_succeeds() { ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; - ctx.push_local("x", u32_ty); + ctx.push_local(core::Name::new("x"), u32_ty); let scrutinee = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let arm0_body = src_arena.alloc(ast::Term::App { @@ -63,7 +63,7 @@ fn check_match_u1_fully_covered_succeeds() { }) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); - ctx.push_local("x", u1_ty_core); + ctx.push_local(core::Name::new("x"), u1_ty_core); let scrutinee = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let arm0_body = src_arena.alloc(ast::Term::App { @@ -107,7 +107,7 @@ fn infer_match_u1_partially_covered_fails() { }) as &_, ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); - ctx.push_local("x", u1_ty_core); + ctx.push_local(core::Name::new("x"), u1_ty_core); let scrutinee = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let arm0_body = src_arena.alloc(ast::Term::App { @@ -142,7 +142,7 @@ fn infer_match_no_catch_all_fails() { ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; - ctx.push_local("x", u32_ty); + ctx.push_local(core::Name::new("x"), u32_ty); let scrutinee = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let arm0_body = src_arena.alloc(ast::Term::App { @@ -196,7 +196,7 @@ fn infer_match_arms_type_mismatch_fails() { ); let mut ctx = test_ctx_with_globals(&core_arena, &globals); let u32_ty = &core::Term::U32_META; - ctx.push_local("x", u32_ty); + ctx.push_local(core::Name::new("x"), u32_ty); let scrutinee = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let arm0_body = src_arena.alloc(ast::Term::App { diff --git a/compiler/src/checker/test/meta.rs b/compiler/src/checker/test/meta.rs index 65d25e8..4da8e98 100644 --- a/compiler/src/checker/test/meta.rs +++ b/compiler/src/checker/test/meta.rs @@ -42,7 +42,7 @@ fn infer_lift_of_non_type_fails() { // Push a local `x: u32` (a value, not a type) then write `[[x]]` let u32_ty = &core::Term::U32_META; - ctx.push_local("x", u32_ty); + ctx.push_local(core::Name::new("x"), u32_ty); let inner = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let term = src_arena.alloc(ast::Term::Lift(inner)); @@ -141,7 +141,7 @@ fn check_quote_switches_to_object_phase() { // x : [[u64]] — meta variable holding object code; Lift contains an object-phase type. let u64_obj = core::Term::int_ty(IntWidth::U64, Phase::Object); let lifted = ctx.lift_ty(u64_obj); - ctx.push_local("x", lifted); + ctx.push_local(core::Name::new("x"), lifted); // `#($(x))` — splice x inside a quote; type should be [[u64]] let x = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); @@ -168,7 +168,7 @@ fn infer_splice_of_lifted_var_returns_inner_type() { let u64_ty = &core::Term::U64_META; let lifted = ctx.lift_ty(u64_ty); - ctx.push_local("x", lifted); // x: [[u64]] + ctx.push_local(core::Name::new("x"), lifted); // x: [[u64]] let x = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let term = src_arena.alloc(ast::Term::Splice(x)); @@ -195,7 +195,7 @@ fn infer_splice_at_meta_phase_fails() { let u64_ty = &core::Term::U64_META; let lifted = ctx.lift_ty(u64_ty); - ctx.push_local("x", lifted); // x: [[u64]] + ctx.push_local(core::Name::new("x"), lifted); // x: [[u64]] let x = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let term = src_arena.alloc(ast::Term::Splice(x)); @@ -213,7 +213,7 @@ fn infer_splice_of_meta_int_succeeds() { // x: u32 at meta phase let u32_meta = &core::Term::U32_META; - ctx.push_local("x", u32_meta); + ctx.push_local(core::Name::new("x"), u32_meta); let x = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let term = src_arena.alloc(ast::Term::Splice(x)); @@ -253,7 +253,7 @@ fn infer_splice_of_non_lifted_non_int_var_fails() { let mut ctx = test_ctx(&core_arena); let type_ty = &core::Term::TYPE; // Type (meta universe), not an integer or [[T]] - ctx.push_local("x", type_ty); + ctx.push_local(core::Name::new("x"), type_ty); let x = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let term = src_arena.alloc(ast::Term::Splice(x)); diff --git a/compiler/src/checker/test/var.rs b/compiler/src/checker/test/var.rs index 6dee0c3..7cc86e4 100644 --- a/compiler/src/checker/test/var.rs +++ b/compiler/src/checker/test/var.rs @@ -9,7 +9,7 @@ fn infer_var_in_scope_returns_its_type() { let core_arena = bumpalo::Bump::new(); let mut ctx = test_ctx(&core_arena); let u32_ty = &core::Term::U32_META; - ctx.push_local("x", u32_ty); + ctx.push_local(core::Name::new("x"), u32_ty); let term = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let core_term = infer(&mut ctx, Phase::Meta, term).expect("should infer"); @@ -44,8 +44,8 @@ fn infer_var_returns_correct_index() { let mut ctx = test_ctx(&core_arena); let u64_ty = &core::Term::U64_META; let u32_ty = &core::Term::U32_META; - ctx.push_local("x", u64_ty); // outer: index 1 - ctx.push_local("y", u32_ty); // inner: index 0 + ctx.push_local(core::Name::new("x"), u64_ty); // outer: index 1 + ctx.push_local(core::Name::new("y"), u32_ty); // inner: index 0 let term = src_arena.alloc(ast::Term::Var(ast::Name::new("y"))); let core_term = infer(&mut ctx, Phase::Meta, term).expect("should infer"); @@ -61,8 +61,8 @@ fn infer_var_shadowed_returns_innermost() { let mut ctx = test_ctx(&core_arena); let u64_ty = &core::Term::U64_META; let u32_ty = &core::Term::U32_META; - ctx.push_local("x", u64_ty); // outer x: u64, index 1 - ctx.push_local("x", u32_ty); // inner x: u32 — shadows, index 0 + ctx.push_local(core::Name::new("x"), u64_ty); // outer x: u64, index 1 + ctx.push_local(core::Name::new("x"), u32_ty); // inner x: u32 — shadows, index 0 let term = src_arena.alloc(ast::Term::Var(ast::Name::new("x"))); let core_term = infer(&mut ctx, Phase::Meta, term).expect("should infer"); diff --git a/compiler/src/core/mod.rs b/compiler/src/core/mod.rs index 1621fe4..c800a5b 100644 --- a/compiler/src/core/mod.rs +++ b/compiler/src/core/mod.rs @@ -51,15 +51,15 @@ impl Ix { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Pat<'a> { Lit(u64), - Bind(&'a str), // named binding - Wildcard, // _ pattern + Bind(Name<'a>), // named binding + Wildcard, // _ pattern } impl<'a> Pat<'a> { /// Return the name bound by this pattern, if any. - pub const fn bound_name(&self) -> Option<&'a str> { + pub const fn bound_name(&self) -> Option> { match self { - Pat::Bind(name) => Some(name), + Pat::Bind(name) => Some(*name), Pat::Lit(_) | Pat::Wildcard => None, } } @@ -116,7 +116,7 @@ pub struct App<'a> { /// for globals and locals. #[derive(Debug, PartialEq, Eq)] pub struct Pi<'a> { - pub params: &'a [(&'a str, &'a Term<'a>)], // (name, type) pairs + pub params: &'a [(Name<'a>, &'a Term<'a>)], // (name, type) pairs pub body_ty: &'a Term<'a>, pub phase: Phase, } @@ -124,14 +124,14 @@ pub struct Pi<'a> { /// Lambda abstraction: |params...| body #[derive(Debug, PartialEq, Eq)] pub struct Lam<'a> { - pub params: &'a [(&'a str, &'a Term<'a>)], // (name, type) pairs + pub params: &'a [(Name<'a>, &'a Term<'a>)], // (name, type) pairs pub body: &'a Term<'a>, } /// Let binding with explicit type annotation and a body. #[derive(Debug, PartialEq, Eq)] pub struct Let<'a> { - pub name: &'a str, + pub name: Name<'a>, pub ty: &'a Term<'a>, pub expr: &'a Term<'a>, pub body: &'a Term<'a>, @@ -234,7 +234,7 @@ impl<'a> Term<'a> { Self::App(App { func, args }) } - pub const fn new_let(name: &'a str, ty: &'a Self, expr: &'a Self, body: &'a Self) -> Self { + pub const fn new_let(name: Name<'a>, ty: &'a Self, expr: &'a Self, body: &'a Self) -> Self { Self::Let(Let { name, ty, From afd8f229ea8e3087268982e979f201885e732459 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 20:04:53 +0000 Subject: [PATCH 38/43] refactor: complete Name type conversion throughout codebase - Fixed all callsites where core IR structures are constructed or used - Updated pretty.rs to use Name in environment instead of &str - Updated value.rs VLam and VPi to store Name instead of &str - Updated eval.rs unstaging code to construct Name when creating Let and Pat - Updated all test files to wrap string literals with core::Name::new() - Fixed clippy error in globals lookup All tests pass (163 tests), clippy clean. Co-Authored-By: Claude Haiku 4.5 --- compiler/src/checker/mod.rs | 63 ++++++++++++------------- compiler/src/checker/test/context.rs | 14 +++--- compiler/src/checker/test/helpers.rs | 2 +- compiler/src/checker/test/signatures.rs | 4 +- compiler/src/core/pretty.rs | 20 ++++---- compiler/src/core/value.rs | 8 ++-- compiler/src/eval/mod.rs | 10 ++-- 7 files changed, 60 insertions(+), 61 deletions(-) diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index 47f246c..929b1a8 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -244,8 +244,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { match pi_val { value::Value::Pi(vpi) => { let arg_val = self.eval(arg); - pi_val = - value::inst(self.arena, &vpi.closure, arg_val); + pi_val = value::inst(self.arena, &vpi.closure, arg_val); } _ => unreachable!("App func must have Pi type (typechecker invariant)"), } @@ -325,7 +324,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { let mut types2 = self.types.clone(); types2.push(ty_val); let mut names2 = self.names.clone(); - names2.push(name); + names2.push(*name); let lvl2 = self.lvl.succ(); let fake_ctx = Ctx { arena: self.arena, @@ -357,7 +356,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { lvl: self.lvl, globals: self.globals, }; - fake_ctx.push_local(core::Name::new(name), scrut_ty_term); + fake_ctx.push_local(name, scrut_ty_term); fake_ctx.val_type_of(arm.body) } } @@ -369,8 +368,8 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Resolve a built-in type name to a static core term, using `phase` for integer types. /// /// Returns `None` if the name is not a built-in type. -fn builtin_prim_ty(name: &str, phase: Phase) -> Option<&'static core::Term<'static>> { - Some(match name { +fn builtin_prim_ty(name: core::Name<'_>, phase: Phase) -> Option<&'static core::Term<'static>> { + Some(match name.0 { "u1" => core::Term::int_ty(IntWidth::U1, phase), "u8" => core::Term::int_ty(IntWidth::U8, phase), "u16" => core::Term::int_ty(IntWidth::U16, phase), @@ -390,8 +389,8 @@ fn elaborate_sig<'src, 'core>( let empty_globals = HashMap::new(); let mut ctx = Ctx::new(arena, &empty_globals); - let params: &'core [(core::Name<'core>, &'core core::Term<'core>)] = - arena.alloc_slice_try_fill_iter(func.params.iter().map(|p| -> Result<_> { + let params: &'core [(core::Name<'core>, &'core core::Term<'core>)] = arena + .alloc_slice_try_fill_iter(func.params.iter().map(|p| -> Result<_> { let param_name = core::Name::new(arena.alloc_str(p.name.as_str())); let param_ty = infer(&mut ctx, func.phase, p.ty)?; ctx.push_local(param_name, param_ty); @@ -446,7 +445,7 @@ fn elaborate_bodies<'src, 'core>( // Push parameters as locals so the body can reference them. for (pname, pty) in pi.params { - ctx.push_local(core::Name::new(pname), pty); + ctx.push_local(*pname, pty); } // Elaborate the body, checking it against the declared return type. @@ -475,9 +474,9 @@ pub fn elaborate_program<'core>( const fn value_type_universe(ty: &value::Value<'_>) -> Option { match ty { value::Value::Prim(Prim::IntTy(IntType { phase, .. })) => Some(*phase), - value::Value::Prim(Prim::U(_)) - | value::Value::Lift(_) - | value::Value::Pi(_) => Some(Phase::Meta), + value::Value::Prim(Prim::U(_)) | value::Value::Lift(_) | value::Value::Pi(_) => { + Some(Phase::Meta) + } // Neutral or unknown — can't determine phase value::Value::Rigid(_) | value::Value::Global(_) @@ -534,29 +533,28 @@ pub fn infer<'src, 'core>( // ------------------------------------------------------------------ Var // Look up the name in locals; return its index and type. ast::Term::Var(name) => { - let name_str = name.as_str(); // First check if it's a built-in type name — those are inferable too. - if let Some(term) = builtin_prim_ty(name_str, phase) { + if let Some(term) = builtin_prim_ty(*name, phase) { // Phase check: U(Object) (VmType) is only valid in a meta-phase context. if let core::Term::Prim(Prim::U(u_phase)) = term { ensure!( *u_phase == phase, - "`{name_str}` is a {u_phase}-phase type, \ + "`{name}` is a {u_phase}-phase type, \ not valid in a {phase}-phase context" ); } return Ok(term); } // Check locals. - if let Some((ix, _)) = ctx.lookup_local(name_str) { + if let Some((ix, _)) = ctx.lookup_local(*name) { return Ok(ctx.alloc(core::Term::Var(ix))); } // Check globals — bare reference without call, produces Global term. - let core_name = core::Name::new(ctx.arena.alloc_str(name_str)); - if ctx.globals.contains_key(&core_name) { - return Ok(ctx.alloc(core::Term::Global(core_name))); + if ctx.globals.contains_key(name) { + let name = core::Name::new(ctx.arena.alloc_str(name.0)); + return Ok(ctx.alloc(core::Term::Global(name))); } - Err(anyhow!("unbound variable `{name_str}`")) + Err(anyhow!("unbound variable `{name}`")) } // ------------------------------------------------------------------ Lit @@ -677,7 +675,8 @@ pub fn infer<'src, 'core>( ); let depth_before = ctx.depth(); - let mut elaborated_params: Vec<(core::Name<'core>, &'core core::Term<'core>)> = Vec::new(); + let mut elaborated_params: Vec<(core::Name<'core>, &'core core::Term<'core>)> = + Vec::new(); for p in *params { let param_name = core::Name::new(ctx.arena.alloc_str(p.name.as_str())); let param_ty = infer(ctx, Phase::Meta, p.ty)?; @@ -716,7 +715,8 @@ pub fn infer<'src, 'core>( ); let depth_before = ctx.depth(); - let mut elaborated_params: Vec<(core::Name<'core>, &'core core::Term<'core>)> = Vec::new(); + let mut elaborated_params: Vec<(core::Name<'core>, &'core core::Term<'core>)> = + Vec::new(); for p in *params { let param_name = core::Name::new(ctx.arena.alloc_str(p.name.as_str())); @@ -746,10 +746,7 @@ pub fn infer<'src, 'core>( ); let core_inner = infer(ctx, Phase::Object, inner)?; let inner_ty_val = ctx.val_type_of(core_inner); - let is_vm_type = matches!( - &inner_ty_val, - value::Value::Prim(Prim::U(Phase::Object)) - ); + let is_vm_type = matches!(&inner_ty_val, value::Value::Prim(Prim::U(Phase::Object))); ensure!(is_vm_type, "argument of `[[...]]` must be an object type"); Ok(ctx.alloc(core::Term::Lift(core_inner))) } @@ -1174,14 +1171,13 @@ pub fn check_val<'src, 'core>( // Peel exactly `params.len()` Pi layers from the expected type. // This allows nested lambdas: `|a: A| |b: B| body` checks against // `fn(_: A) -> fn(_: B) -> R` by covering one Pi layer per lambda. - let mut pi_params: Vec<(&str, value::Value<'core>)> = Vec::new(); + let mut pi_params: Vec<(core::Name<'_>, value::Value<'core>)> = Vec::new(); let mut cur_pi = expected.clone(); for _ in 0..params.len() { match cur_pi { value::Value::Pi(vpi) => { pi_params.push((vpi.name, (*vpi.domain).clone())); - let fresh = - value::Value::Rigid(Lvl(ctx.depth() + pi_params.len() - 1)); + let fresh = value::Value::Rigid(Lvl(ctx.depth() + pi_params.len() - 1)); cur_pi = value::inst(ctx.arena, &vpi.closure, fresh); } _ => bail!( @@ -1193,9 +1189,10 @@ pub fn check_val<'src, 'core>( } let body_ty_val = cur_pi; - let mut elaborated_params: Vec<(&'core str, &'core core::Term<'core>)> = Vec::new(); + let mut elaborated_params: Vec<(core::Name<'core>, &'core core::Term<'core>)> = + Vec::new(); for (p, (_, pi_param_ty)) in params.iter().zip(pi_params.into_iter()) { - let param_name: &'core str = ctx.arena.alloc_str(p.name.as_str()); + let param_name = core::Name::new(ctx.arena.alloc_str(p.name.as_str())); let annotated_ty = infer(ctx, Phase::Meta, p.ty)?; let annotated_ty_val = ctx.eval(annotated_ty); ensure!( @@ -1204,7 +1201,7 @@ pub fn check_val<'src, 'core>( than the expected function type" ); elaborated_params.push((param_name, annotated_ty)); - ctx.push_local_val(core::Name::new(param_name), pi_param_ty); + ctx.push_local_val(param_name, pi_param_ty); } let core_body = check_val(ctx, phase, body, body_ty_val)?; @@ -1233,7 +1230,7 @@ pub fn check_val<'src, 'core>( .alloc_slice_try_fill_iter(arms.iter().map(|arm| -> Result<_> { let core_pat = elaborate_pat(ctx, &arm.pat); if let Some(bname) = core_pat.bound_name() { - ctx.push_local(core::Name::new(bname), scrut_ty_term); + ctx.push_local(bname, scrut_ty_term); } let arm_result = check_val(ctx, phase, arm.body, expected.clone()); diff --git a/compiler/src/checker/test/context.rs b/compiler/src/checker/test/context.rs index 15a5b07..73db2bd 100644 --- a/compiler/src/checker/test/context.rs +++ b/compiler/src/checker/test/context.rs @@ -25,7 +25,7 @@ fn literal_checks_against_int_type() { fn variable_lookup_in_empty_context() { let arena = bumpalo::Bump::new(); let ctx = test_ctx(&arena); - assert!(ctx.lookup_local("x").is_none()); + assert!(ctx.lookup_local(core::Name::new("x")).is_none()); } #[test] @@ -35,7 +35,7 @@ fn variable_lookup_after_push() { let u64_term = &core::Term::U64_META; ctx.push_local(core::Name::new("x"), u64_term); - let (ix, ty) = ctx.lookup_local("x").expect("x should be in scope"); + let (ix, ty) = ctx.lookup_local(core::Name::new("x")).expect("x should be in scope"); assert_eq!(ix, Ix(0)); assert!(matches!( ty, @@ -57,7 +57,7 @@ fn variable_lookup_with_multiple_locals() { ctx.push_local(core::Name::new("y"), u32_term); // With two locals, "y" is innermost (index 0), "x" is outer (index 1). - let (ix_y, ty_y) = ctx.lookup_local("y").expect("y should be in scope"); + let (ix_y, ty_y) = ctx.lookup_local(core::Name::new("y")).expect("y should be in scope"); assert_eq!(ix_y, Ix(0)); assert!(matches!( ty_y, @@ -67,7 +67,7 @@ fn variable_lookup_with_multiple_locals() { })) )); - let (ix_x, ty_x) = ctx.lookup_local("x").expect("x should be in scope"); + let (ix_x, ty_x) = ctx.lookup_local(core::Name::new("x")).expect("x should be in scope"); assert_eq!(ix_x, Ix(1)); assert!(matches!( ty_x, @@ -89,7 +89,7 @@ fn variable_shadowing() { ctx.push_local(core::Name::new("x"), u32_term); // Innermost "x" shadows outer; it is at index 0. - let (ix, ty) = ctx.lookup_local("x").expect("x should be in scope"); + let (ix, ty) = ctx.lookup_local(core::Name::new("x")).expect("x should be in scope"); assert_eq!(ix, Ix(0)); assert!(matches!( ty, @@ -223,7 +223,7 @@ fn let_binding_structure() { let u64_term = &core::Term::U64_META; let expr = arena.alloc(core::Term::Lit(42, IntType::U64_META)); let body = arena.alloc(core::Term::Var(Ix(0))); - let let_term = arena.alloc(core::Term::new_let("x", u64_term, expr, body)); + let let_term = arena.alloc(core::Term::new_let(core::Name::new("x"), u64_term, expr, body)); assert!(matches!(let_term, core::Term::Let(_))); } @@ -256,7 +256,7 @@ fn match_with_binding_pattern() { let body = arena.alloc(core::Term::Var(Ix(0))); let arm = core::Arm { - pat: Pat::Bind("n"), + pat: Pat::Bind(core::Name::new("n")), body, }; diff --git a/compiler/src/checker/test/helpers.rs b/compiler/src/checker/test/helpers.rs index 6cdc345..bf99f07 100644 --- a/compiler/src/checker/test/helpers.rs +++ b/compiler/src/checker/test/helpers.rs @@ -31,7 +31,7 @@ pub fn sig_no_params_returns_u64(arena: &bumpalo::Bump) -> &core::Pi<'_> { /// Helper: build a Pi for `fn f(x: u32) -> u64`. pub fn sig_one_param_returns_u64(arena: &bumpalo::Bump) -> &core::Pi<'_> { - let params = arena.alloc_slice_fill_iter([("x", &core::Term::U32_META as &core::Term)]); + let params = arena.alloc_slice_fill_iter([(core::Name::new("x"), &core::Term::U32_META as &core::Term)]); arena.alloc(Pi { params, body_ty: &core::Term::U64_META, diff --git a/compiler/src/checker/test/signatures.rs b/compiler/src/checker/test/signatures.rs index 205a75c..38a70f0 100644 --- a/compiler/src/checker/test/signatures.rs +++ b/compiler/src/checker/test/signatures.rs @@ -55,7 +55,7 @@ fn collect_signatures_two_functions() { .expect("id should be in globals"); assert_eq!(id_pi.phase, Phase::Meta); assert_eq!(id_pi.params.len(), 1); - assert_eq!(id_pi.params[0].0, "x"); + assert_eq!(id_pi.params[0].0.as_str(), "x"); assert!(matches!( id_pi.params[0].1, core::Term::Prim(Prim::IntTy(IntType { @@ -76,7 +76,7 @@ fn collect_signatures_two_functions() { .expect("add_one should be in globals"); assert_eq!(add_pi.phase, Phase::Object); assert_eq!(add_pi.params.len(), 1); - assert_eq!(add_pi.params[0].0, "y"); + assert_eq!(add_pi.params[0].0.as_str(), "y"); assert!(matches!( add_pi.params[0].1, core::Term::Prim(Prim::IntTy(IntType { diff --git a/compiler/src/core/pretty.rs b/compiler/src/core/pretty.rs index c4558eb..a954777 100644 --- a/compiler/src/core/pretty.rs +++ b/compiler/src/core/pretty.rs @@ -2,7 +2,7 @@ use std::fmt; use crate::parser::ast::Phase; -use super::{Arm, Function, Pat, Program, Term}; +use super::{Arm, Function, Name, Pat, Program, Term}; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -18,7 +18,7 @@ impl<'a> Term<'a> { /// (the caller is responsible for any surrounding braces). fn fmt_term( &self, - env: &mut Vec<&'a str>, + env: &mut Vec>, indent: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { @@ -40,7 +40,7 @@ impl<'a> Term<'a> { /// a new indented block (e.g. `Let` / `Match`). fn fmt_term_inline( &self, - env: &mut Vec<&'a str>, + env: &mut Vec>, indent: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { @@ -51,7 +51,7 @@ impl<'a> Term<'a> { let i = lvl.0; let name = env .get(i) - .expect("De Bruijn index out of environment bounds"); + .expect("De Bruijn level out of environment bounds"); write!(f, "{name}@{i}") } @@ -85,7 +85,7 @@ impl<'a> Term<'a> { if i > 0 { write!(f, ", ")?; } - if name == "_" { + if name.as_str() == "_" { write!(f, "_: ")?; } else { write!(f, "{}@{}: ", name, env.len())?; @@ -170,7 +170,7 @@ impl<'a> Term<'a> { /// syntactically valid as sub-expressions. fn fmt_expr( &self, - env: &mut Vec<&'a str>, + env: &mut Vec>, indent: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { @@ -200,7 +200,7 @@ impl<'a> Arm<'a> { /// Print a single match arm. fn fmt_arm( &self, - env: &mut Vec<&'a str>, + env: &mut Vec>, indent: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { @@ -211,7 +211,7 @@ impl<'a> Arm<'a> { Pat::Bind(name) => { let lvl = env.len(); write!(f, "{name}@{lvl} => ")?; - env.push(name); + env.push(*name); self.body.fmt_expr(env, indent, f)?; env.pop(); return writeln!(f, ","); @@ -241,7 +241,7 @@ impl fmt::Display for Function<'_> { let pi = self.pi(); // Build the name environment for the body: one entry per parameter. - let mut env: Vec<&str> = Vec::with_capacity(pi.params.len()); + let mut env: Vec = Vec::with_capacity(pi.params.len()); // Phase prefix. match pi.phase { @@ -258,7 +258,7 @@ impl fmt::Display for Function<'_> { } write!(f, "{name}@{i}: ")?; ty.fmt_expr(&mut env, 1, f)?; - env.push(name); + env.push(*name); } write!(f, ") -> ")?; diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs index b5b47ed..bc2ed21 100644 --- a/compiler/src/core/value.rs +++ b/compiler/src/core/value.rs @@ -40,7 +40,7 @@ pub enum Value<'a> { /// Lambda value: parameter name, parameter type, and body closure. #[derive(Clone, Debug)] pub struct VLam<'a> { - pub name: &'a str, + pub name: Name<'a>, pub param_ty: &'a Value<'a>, pub closure: Closure<'a>, } @@ -48,7 +48,7 @@ pub struct VLam<'a> { /// Pi (dependent function type) value. #[derive(Clone, Debug)] pub struct VPi<'a> { - pub name: &'a str, + pub name: Name<'a>, pub domain: &'a Value<'a>, pub closure: Closure<'a>, pub phase: Phase, @@ -170,7 +170,7 @@ pub fn eval_pi<'a>(arena: &'a Bump, env: &[Value<'a>], pi: &'a Pi<'a>) -> Value< body: rest_body, }; Value::Pi(VPi { - name, + name: *name, domain: arena.alloc(domain), closure, phase: pi.phase, @@ -198,7 +198,7 @@ fn eval_lam<'a>(arena: &'a Bump, env: &[Value<'a>], lam: &'a Lam<'a>) -> Value<' body: rest_body, }; Value::Lam(VLam { - name, + name: *name, param_ty: arena.alloc(param_ty), closure, }) diff --git a/compiler/src/eval/mod.rs b/compiler/src/eval/mod.rs index d64dbf1..74b80e7 100644 --- a/compiler/src/eval/mod.rs +++ b/compiler/src/eval/mod.rs @@ -665,7 +665,7 @@ fn unstage_obj<'out, 'eval>( let staged_body = unstage_obj(arena, eval_arena, globals, env, let_.body); env.pop(); Ok(arena.alloc(Term::new_let( - arena.alloc_str(let_.name), + Name::new(arena.alloc_str(let_.name.as_str())), staged_ty, staged_expr, staged_body?, @@ -679,7 +679,9 @@ fn unstage_obj<'out, 'eval>( arena.alloc_slice_try_fill_iter(match_.arms.iter().map(|arm| -> Result<_> { let staged_pat = match &arm.pat { Pat::Lit(n) => Pat::Lit(*n), - Pat::Bind(name) => Pat::Bind(arena.alloc_str(name)), + Pat::Bind(name) => { + Pat::Bind(Name::new(arena.alloc_str(name.as_str()))) + } Pat::Wildcard => Pat::Wildcard, }; let has_binding = arm.pat.bound_name().is_some(); @@ -745,10 +747,10 @@ pub fn unstage_program<'out, 'core>( let mut env = Env::new(Lvl::new(0)); let staged_params = arena.alloc_slice_try_fill_iter(pi.params.iter().map( - |(n, ty)| -> Result<(&'out str, &'out Term<'out>)> { + |(n, ty)| -> Result<(Name<'out>, &'out Term<'out>)> { let staged_ty = unstage_obj(arena, &eval_bump, &globals, &mut env, ty)?; env.push_obj(); - Ok((arena.alloc_str(n), staged_ty)) + Ok((Name::new(arena.alloc_str(n.as_str())), staged_ty)) }, ))?; From eba71446c78e93047034a1456ec2a9b4f8865ec5 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 20:05:18 +0000 Subject: [PATCH 39/43] style: apply cargo fmt formatting --- compiler/src/checker/test/context.rs | 23 ++++++++++++++++++----- compiler/src/checker/test/helpers.rs | 3 ++- compiler/src/core/value.rs | 3 +-- compiler/src/eval/mod.rs | 4 +--- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/compiler/src/checker/test/context.rs b/compiler/src/checker/test/context.rs index 73db2bd..c427a80 100644 --- a/compiler/src/checker/test/context.rs +++ b/compiler/src/checker/test/context.rs @@ -35,7 +35,9 @@ fn variable_lookup_after_push() { let u64_term = &core::Term::U64_META; ctx.push_local(core::Name::new("x"), u64_term); - let (ix, ty) = ctx.lookup_local(core::Name::new("x")).expect("x should be in scope"); + let (ix, ty) = ctx + .lookup_local(core::Name::new("x")) + .expect("x should be in scope"); assert_eq!(ix, Ix(0)); assert!(matches!( ty, @@ -57,7 +59,9 @@ fn variable_lookup_with_multiple_locals() { ctx.push_local(core::Name::new("y"), u32_term); // With two locals, "y" is innermost (index 0), "x" is outer (index 1). - let (ix_y, ty_y) = ctx.lookup_local(core::Name::new("y")).expect("y should be in scope"); + let (ix_y, ty_y) = ctx + .lookup_local(core::Name::new("y")) + .expect("y should be in scope"); assert_eq!(ix_y, Ix(0)); assert!(matches!( ty_y, @@ -67,7 +71,9 @@ fn variable_lookup_with_multiple_locals() { })) )); - let (ix_x, ty_x) = ctx.lookup_local(core::Name::new("x")).expect("x should be in scope"); + let (ix_x, ty_x) = ctx + .lookup_local(core::Name::new("x")) + .expect("x should be in scope"); assert_eq!(ix_x, Ix(1)); assert!(matches!( ty_x, @@ -89,7 +95,9 @@ fn variable_shadowing() { ctx.push_local(core::Name::new("x"), u32_term); // Innermost "x" shadows outer; it is at index 0. - let (ix, ty) = ctx.lookup_local(core::Name::new("x")).expect("x should be in scope"); + let (ix, ty) = ctx + .lookup_local(core::Name::new("x")) + .expect("x should be in scope"); assert_eq!(ix, Ix(0)); assert!(matches!( ty, @@ -223,7 +231,12 @@ fn let_binding_structure() { let u64_term = &core::Term::U64_META; let expr = arena.alloc(core::Term::Lit(42, IntType::U64_META)); let body = arena.alloc(core::Term::Var(Ix(0))); - let let_term = arena.alloc(core::Term::new_let(core::Name::new("x"), u64_term, expr, body)); + let let_term = arena.alloc(core::Term::new_let( + core::Name::new("x"), + u64_term, + expr, + body, + )); assert!(matches!(let_term, core::Term::Let(_))); } diff --git a/compiler/src/checker/test/helpers.rs b/compiler/src/checker/test/helpers.rs index bf99f07..e306e65 100644 --- a/compiler/src/checker/test/helpers.rs +++ b/compiler/src/checker/test/helpers.rs @@ -31,7 +31,8 @@ pub fn sig_no_params_returns_u64(arena: &bumpalo::Bump) -> &core::Pi<'_> { /// Helper: build a Pi for `fn f(x: u32) -> u64`. pub fn sig_one_param_returns_u64(arena: &bumpalo::Bump) -> &core::Pi<'_> { - let params = arena.alloc_slice_fill_iter([(core::Name::new("x"), &core::Term::U32_META as &core::Term)]); + let params = + arena.alloc_slice_fill_iter([(core::Name::new("x"), &core::Term::U32_META as &core::Term)]); arena.alloc(Pi { params, body_ty: &core::Term::U64_META, diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs index bc2ed21..fe3e516 100644 --- a/compiler/src/core/value.rs +++ b/compiler/src/core/value.rs @@ -85,8 +85,7 @@ pub fn eval<'a>(arena: &'a Bump, env: &[Value<'a>], term: &'a Term<'a>) -> Value Term::App(app) => { let func_val = eval(arena, env, app.func); - let arg_vals: Vec> = - app.args.iter().map(|a| eval(arena, env, a)).collect(); + let arg_vals: Vec> = app.args.iter().map(|a| eval(arena, env, a)).collect(); apply_many(arena, func_val, &arg_vals) } diff --git a/compiler/src/eval/mod.rs b/compiler/src/eval/mod.rs index 74b80e7..9ba2efd 100644 --- a/compiler/src/eval/mod.rs +++ b/compiler/src/eval/mod.rs @@ -679,9 +679,7 @@ fn unstage_obj<'out, 'eval>( arena.alloc_slice_try_fill_iter(match_.arms.iter().map(|arm| -> Result<_> { let staged_pat = match &arm.pat { Pat::Lit(n) => Pat::Lit(*n), - Pat::Bind(name) => { - Pat::Bind(Name::new(arena.alloc_str(name.as_str()))) - } + Pat::Bind(name) => Pat::Bind(Name::new(arena.alloc_str(name.as_str()))), Pat::Wildcard => Pat::Wildcard, }; let has_binding = arm.pat.bound_name().is_some(); From 353b536024807d8de07b898ece4dab9c802ef641 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 20:46:08 +0000 Subject: [PATCH 40/43] ci: enable CI for devel branch Add 'devel' to the list of branches that trigger CI in the Rust workflow. Co-Authored-By: Claude Haiku 4.5 --- .github/workflows/rust.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b5c5dc4..eac1117 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,9 +2,9 @@ name: Rust on: push: - branches: [ "master" ] + branches: [ "master", "devel" ] pull_request: - branches: [ "master" ] + branches: [ "master", "devel" ] env: CARGO_TERM_COLOR: always From bba112c39d5bad2aba88ae8be2f57468d9a60ede Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 23:25:02 +0000 Subject: [PATCH 41/43] refactor: replace Name<'a> with &'a Name using ref-cast Name is now an unsized DST wrapping str, constructed safely via ref-cast's RefCastCustom instead of unsafe transmute. All usages updated from owned Name<'a> to reference &'a Name throughout the compiler. Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 21 ++++++++++++++++ Cargo.toml | 1 + compiler/Cargo.toml | 1 + compiler/src/checker/mod.rs | 36 ++++++++++++++-------------- compiler/src/checker/test/helpers.rs | 4 ++-- compiler/src/common/name.rs | 20 ++++++++-------- compiler/src/core/mod.rs | 16 ++++++------- compiler/src/core/pretty.rs | 10 ++++---- compiler/src/core/value.rs | 6 ++--- compiler/src/eval/mod.rs | 4 ++-- compiler/src/lexer/mod.rs | 4 ++-- compiler/src/lexer/testutils.rs | 2 +- compiler/src/parser/ast.rs | 10 ++++---- compiler/src/parser/mod.rs | 6 ++--- compiler/src/parser/test/mod.rs | 2 +- 15 files changed, 83 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6647cc7..eacf162 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -482,6 +482,26 @@ dependencies = [ "rand_core", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.3" @@ -611,6 +631,7 @@ dependencies = [ "bolero", "bumpalo", "expect-test", + "ref-cast", "rstest", ] diff --git a/Cargo.toml b/Cargo.toml index 57725ff..f7d0c5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ license = "MIT" [workspace.dependencies] anyhow = { version = "1", default-features = false } bumpalo = { version = "3", default-features = false } +ref-cast = "1" bolero = "0.13" clap = { version = "4", features = ["derive"] } diff --git a/compiler/Cargo.toml b/compiler/Cargo.toml index c29631f..b741c6b 100644 --- a/compiler/Cargo.toml +++ b/compiler/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] anyhow.workspace = true bumpalo.workspace = true +ref-cast.workspace = true [dev-dependencies] bolero.workspace = true diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index 929b1a8..b0b0ff7 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -17,7 +17,7 @@ pub struct Ctx<'core, 'globals> { /// Arena for allocating core terms arena: &'core bumpalo::Bump, /// Local variable names (oldest first), for error messages. - names: Vec>, + names: Vec<&'core core::Name>, /// Evaluation environment (oldest first): values of locals. /// `env[env.len() - 1 - ix]` = value of `Var(Ix(ix))`. env: value::Env<'core>, @@ -29,13 +29,13 @@ pub struct Ctx<'core, 'globals> { /// Global function types: name -> Pi term. /// Storing `&Term` (always a Pi) unifies type lookup for globals and locals. /// Borrowed independently of the arena so the map can live on the stack. - globals: &'globals HashMap, &'core core::Pi<'core>>, + globals: &'globals HashMap<&'core core::Name, &'core core::Pi<'core>>, } impl<'core, 'globals> Ctx<'core, 'globals> { pub const fn new( arena: &'core bumpalo::Bump, - globals: &'globals HashMap, &'core core::Pi<'core>>, + globals: &'globals HashMap<&'core core::Name, &'core core::Pi<'core>>, ) -> Self { Ctx { arena, @@ -62,7 +62,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Push a local variable onto the context, given its type as a term. /// Evaluates the type term in the current environment. - pub fn push_local(&mut self, name: core::Name<'core>, ty: &'core core::Term<'core>) { + pub fn push_local(&mut self, name: &'core core::Name, ty: &'core core::Term<'core>) { let ty_val = value::eval(self.arena, &self.env, ty); self.env.push(value::Value::Rigid(self.lvl)); self.types.push(ty_val); @@ -72,7 +72,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Push a local variable onto the context, given its type as a Value. /// The variable itself is a fresh rigid (neutral) variable — use for lambda/pi params. - fn push_local_val(&mut self, name: core::Name<'core>, ty_val: value::Value<'core>) { + fn push_local_val(&mut self, name: &'core core::Name, ty_val: value::Value<'core>) { self.env.push(value::Value::Rigid(self.lvl)); self.types.push(ty_val); self.lvl = self.lvl.succ(); @@ -83,7 +83,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Use for `let x = e` bindings so that dependent references to `x` evaluate correctly. fn push_let_binding( &mut self, - name: core::Name<'core>, + name: &'core core::Name, ty_val: value::Value<'core>, expr_val: value::Value<'core>, ) { @@ -103,7 +103,7 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Look up a variable by name, returning its (index, type as Value). /// Searches from the most recently pushed variable inward to handle shadowing. - pub fn lookup_local(&self, name: core::Name<'_>) -> Option<(Ix, &value::Value<'core>)> { + pub fn lookup_local(&self, name: &'_ core::Name) -> Option<(Ix, &value::Value<'core>)> { for (i, local_name) in self.names.iter().enumerate().rev() { if *local_name == name { let ix = Lvl(i).ix_at_depth(self.lvl); @@ -368,8 +368,8 @@ impl<'core, 'globals> Ctx<'core, 'globals> { /// Resolve a built-in type name to a static core term, using `phase` for integer types. /// /// Returns `None` if the name is not a built-in type. -fn builtin_prim_ty(name: core::Name<'_>, phase: Phase) -> Option<&'static core::Term<'static>> { - Some(match name.0 { +fn builtin_prim_ty(name: &'_ core::Name, phase: Phase) -> Option<&'static core::Term<'static>> { + Some(match name.as_str() { "u1" => core::Term::int_ty(IntWidth::U1, phase), "u8" => core::Term::int_ty(IntWidth::U8, phase), "u16" => core::Term::int_ty(IntWidth::U16, phase), @@ -389,7 +389,7 @@ fn elaborate_sig<'src, 'core>( let empty_globals = HashMap::new(); let mut ctx = Ctx::new(arena, &empty_globals); - let params: &'core [(core::Name<'core>, &'core core::Term<'core>)] = arena + let params: &'core [(&'core core::Name, &'core core::Term<'core>)] = arena .alloc_slice_try_fill_iter(func.params.iter().map(|p| -> Result<_> { let param_name = core::Name::new(arena.alloc_str(p.name.as_str())); let param_ty = infer(&mut ctx, func.phase, p.ty)?; @@ -410,8 +410,8 @@ fn elaborate_sig<'src, 'core>( pub(crate) fn collect_signatures<'src, 'core>( arena: &'core bumpalo::Bump, program: &ast::Program<'src>, -) -> Result, &'core core::Pi<'core>>> { - let mut globals: HashMap, &'core core::Pi<'core>> = HashMap::new(); +) -> Result>> { + let mut globals: HashMap<&'core core::Name, &'core core::Pi<'core>> = HashMap::new(); for func in program.functions { let name = core::Name::new(arena.alloc_str(func.name.as_str())); @@ -433,7 +433,7 @@ pub(crate) fn collect_signatures<'src, 'core>( fn elaborate_bodies<'src, 'core>( arena: &'core bumpalo::Bump, program: &ast::Program<'src>, - globals: &HashMap, &'core core::Pi<'core>>, + globals: &HashMap<&'core core::Name, &'core core::Pi<'core>>, ) -> Result> { let functions: &'core [core::Function<'core>] = arena.alloc_slice_try_fill_iter(program.functions.iter().map(|func| -> Result<_> { @@ -551,7 +551,7 @@ pub fn infer<'src, 'core>( } // Check globals — bare reference without call, produces Global term. if ctx.globals.contains_key(name) { - let name = core::Name::new(ctx.arena.alloc_str(name.0)); + let name = core::Name::new(ctx.arena.alloc_str(name.as_str())); return Ok(ctx.alloc(core::Term::Global(name))); } Err(anyhow!("unbound variable `{name}`")) @@ -675,7 +675,7 @@ pub fn infer<'src, 'core>( ); let depth_before = ctx.depth(); - let mut elaborated_params: Vec<(core::Name<'core>, &'core core::Term<'core>)> = + let mut elaborated_params: Vec<(&'core core::Name, &'core core::Term<'core>)> = Vec::new(); for p in *params { let param_name = core::Name::new(ctx.arena.alloc_str(p.name.as_str())); @@ -715,7 +715,7 @@ pub fn infer<'src, 'core>( ); let depth_before = ctx.depth(); - let mut elaborated_params: Vec<(core::Name<'core>, &'core core::Term<'core>)> = + let mut elaborated_params: Vec<(&'core core::Name, &'core core::Term<'core>)> = Vec::new(); for p in *params { @@ -1171,7 +1171,7 @@ pub fn check_val<'src, 'core>( // Peel exactly `params.len()` Pi layers from the expected type. // This allows nested lambdas: `|a: A| |b: B| body` checks against // `fn(_: A) -> fn(_: B) -> R` by covering one Pi layer per lambda. - let mut pi_params: Vec<(core::Name<'_>, value::Value<'core>)> = Vec::new(); + let mut pi_params: Vec<(&'_ core::Name, value::Value<'core>)> = Vec::new(); let mut cur_pi = expected.clone(); for _ in 0..params.len() { match cur_pi { @@ -1189,7 +1189,7 @@ pub fn check_val<'src, 'core>( } let body_ty_val = cur_pi; - let mut elaborated_params: Vec<(core::Name<'core>, &'core core::Term<'core>)> = + let mut elaborated_params: Vec<(&'core core::Name, &'core core::Term<'core>)> = Vec::new(); for (p, (_, pi_param_ty)) in params.iter().zip(pi_params.into_iter()) { let param_name = core::Name::new(ctx.arena.alloc_str(p.name.as_str())); diff --git a/compiler/src/checker/test/helpers.rs b/compiler/src/checker/test/helpers.rs index e306e65..893eda7 100644 --- a/compiler/src/checker/test/helpers.rs +++ b/compiler/src/checker/test/helpers.rs @@ -4,7 +4,7 @@ use super::*; /// Helper to create a test context with empty globals pub fn test_ctx(arena: &bumpalo::Bump) -> Ctx<'_, '_> { - static EMPTY: std::sync::OnceLock, &'static core::Pi<'static>>> = + static EMPTY: std::sync::OnceLock>> = std::sync::OnceLock::new(); let globals = EMPTY.get_or_init(HashMap::new); Ctx::new(arena, globals) @@ -15,7 +15,7 @@ pub fn test_ctx(arena: &bumpalo::Bump) -> Ctx<'_, '_> { /// The caller must ensure `globals` outlives the returned `Ctx`. pub fn test_ctx_with_globals<'core, 'globals>( arena: &'core bumpalo::Bump, - globals: &'globals HashMap, &'core core::Pi<'core>>, + globals: &'globals HashMap<&'core Name, &'core core::Pi<'core>>, ) -> Ctx<'core, 'globals> { Ctx::new(arena, globals) } diff --git a/compiler/src/common/name.rs b/compiler/src/common/name.rs index 5f3f769..9beb788 100644 --- a/compiler/src/common/name.rs +++ b/compiler/src/common/name.rs @@ -1,23 +1,23 @@ -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct Name<'a>(pub &'a str); +#[derive(PartialEq, Eq, Hash, ref_cast::RefCastCustom)] +#[repr(transparent)] +pub struct Name(str); -impl<'a> Name<'a> { - pub const fn new(s: &'a str) -> Self { - Name(s) - } +impl Name { + #[ref_cast::ref_cast_custom] + pub const fn new(s: &str) -> &Self; - pub const fn as_str(self) -> &'a str { - self.0 + pub const fn as_str(&self) -> &str { + &self.0 } } -impl std::fmt::Display for Name<'_> { +impl std::fmt::Display for Name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } -impl std::fmt::Debug for Name<'_> { +impl std::fmt::Debug for Name { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } diff --git a/compiler/src/core/mod.rs b/compiler/src/core/mod.rs index c800a5b..24101f7 100644 --- a/compiler/src/core/mod.rs +++ b/compiler/src/core/mod.rs @@ -51,13 +51,13 @@ impl Ix { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Pat<'a> { Lit(u64), - Bind(Name<'a>), // named binding + Bind(&'a Name), // named binding Wildcard, // _ pattern } impl<'a> Pat<'a> { /// Return the name bound by this pattern, if any. - pub const fn bound_name(&self) -> Option> { + pub const fn bound_name(&self) -> Option<&'a Name> { match self { Pat::Bind(name) => Some(*name), Pat::Lit(_) | Pat::Wildcard => None, @@ -75,7 +75,7 @@ pub struct Arm<'a> { /// Elaborated top-level function definition. #[derive(Debug)] pub struct Function<'a> { - pub name: Name<'a>, + pub name: &'a Name, /// Function type: phase, params, and return type. pub ty: &'a Pi<'a>, pub body: &'a Term<'a>, @@ -116,7 +116,7 @@ pub struct App<'a> { /// for globals and locals. #[derive(Debug, PartialEq, Eq)] pub struct Pi<'a> { - pub params: &'a [(Name<'a>, &'a Term<'a>)], // (name, type) pairs + pub params: &'a [(&'a Name, &'a Term<'a>)], // (name, type) pairs pub body_ty: &'a Term<'a>, pub phase: Phase, } @@ -124,14 +124,14 @@ pub struct Pi<'a> { /// Lambda abstraction: |params...| body #[derive(Debug, PartialEq, Eq)] pub struct Lam<'a> { - pub params: &'a [(Name<'a>, &'a Term<'a>)], // (name, type) pairs + pub params: &'a [(&'a Name, &'a Term<'a>)], // (name, type) pairs pub body: &'a Term<'a>, } /// Let binding with explicit type annotation and a body. #[derive(Debug, PartialEq, Eq)] pub struct Let<'a> { - pub name: Name<'a>, + pub name: &'a Name, pub ty: &'a Term<'a>, pub expr: &'a Term<'a>, pub body: &'a Term<'a>, @@ -154,7 +154,7 @@ pub enum Term<'a> { /// Numeric literal with its integer type Lit(u64, IntType), /// Global function reference - Global(Name<'a>), + Global(&'a Name), /// Function or primitive application: func(args...) App(App<'a>), /// Dependent function type: fn(x: A) -> B @@ -234,7 +234,7 @@ impl<'a> Term<'a> { Self::App(App { func, args }) } - pub const fn new_let(name: Name<'a>, ty: &'a Self, expr: &'a Self, body: &'a Self) -> Self { + pub const fn new_let(name: &'a Name, ty: &'a Self, expr: &'a Self, body: &'a Self) -> Self { Self::Let(Let { name, ty, diff --git a/compiler/src/core/pretty.rs b/compiler/src/core/pretty.rs index a954777..0e318f9 100644 --- a/compiler/src/core/pretty.rs +++ b/compiler/src/core/pretty.rs @@ -18,7 +18,7 @@ impl<'a> Term<'a> { /// (the caller is responsible for any surrounding braces). fn fmt_term( &self, - env: &mut Vec>, + env: &mut Vec<&'a Name>, indent: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { @@ -40,7 +40,7 @@ impl<'a> Term<'a> { /// a new indented block (e.g. `Let` / `Match`). fn fmt_term_inline( &self, - env: &mut Vec>, + env: &mut Vec<&'a Name>, indent: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { @@ -170,7 +170,7 @@ impl<'a> Term<'a> { /// syntactically valid as sub-expressions. fn fmt_expr( &self, - env: &mut Vec>, + env: &mut Vec<&'a Name>, indent: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { @@ -200,7 +200,7 @@ impl<'a> Arm<'a> { /// Print a single match arm. fn fmt_arm( &self, - env: &mut Vec>, + env: &mut Vec<&'a Name>, indent: usize, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { @@ -241,7 +241,7 @@ impl fmt::Display for Function<'_> { let pi = self.pi(); // Build the name environment for the body: one entry per parameter. - let mut env: Vec = Vec::with_capacity(pi.params.len()); + let mut env: Vec<&Name> = Vec::with_capacity(pi.params.len()); // Phase prefix. match pi.phase { diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs index fe3e516..ec9b24e 100644 --- a/compiler/src/core/value.rs +++ b/compiler/src/core/value.rs @@ -20,7 +20,7 @@ pub enum Value<'a> { /// Neutral: stuck on a local variable (identified by De Bruijn level) Rigid(Lvl), /// Neutral: global function reference (not inlined during type-checking) - Global(Name<'a>), + Global(&'a Name), /// Neutral: unapplied or partially applied primitive Prim(Prim), /// Neutral: stuck application (callee cannot reduce further) @@ -40,7 +40,7 @@ pub enum Value<'a> { /// Lambda value: parameter name, parameter type, and body closure. #[derive(Clone, Debug)] pub struct VLam<'a> { - pub name: Name<'a>, + pub name: &'a Name, pub param_ty: &'a Value<'a>, pub closure: Closure<'a>, } @@ -48,7 +48,7 @@ pub struct VLam<'a> { /// Pi (dependent function type) value. #[derive(Clone, Debug)] pub struct VPi<'a> { - pub name: Name<'a>, + pub name: &'a Name, pub domain: &'a Value<'a>, pub closure: Closure<'a>, pub phase: Phase, diff --git a/compiler/src/eval/mod.rs b/compiler/src/eval/mod.rs index 9ba2efd..4069315 100644 --- a/compiler/src/eval/mod.rs +++ b/compiler/src/eval/mod.rs @@ -118,7 +118,7 @@ struct GlobalDef<'a> { body: &'a Term<'a>, } -type Globals<'a> = HashMap, GlobalDef<'a>>; +type Globals<'a> = HashMap<&'a Name, GlobalDef<'a>>; // ── Meta-level evaluator ────────────────────────────────────────────────────── @@ -745,7 +745,7 @@ pub fn unstage_program<'out, 'core>( let mut env = Env::new(Lvl::new(0)); let staged_params = arena.alloc_slice_try_fill_iter(pi.params.iter().map( - |(n, ty)| -> Result<(Name<'out>, &'out Term<'out>)> { + |(n, ty)| -> Result<(&'out Name, &'out Term<'out>)> { let staged_ty = unstage_obj(arena, &eval_bump, &globals, &mut env, ty)?; env.push_obj(); Ok((Name::new(arena.alloc_str(n.as_str())), staged_ty)) diff --git a/compiler/src/lexer/mod.rs b/compiler/src/lexer/mod.rs index ccd9846..4d56872 100644 --- a/compiler/src/lexer/mod.rs +++ b/compiler/src/lexer/mod.rs @@ -43,7 +43,7 @@ pub enum Token<'a> { DoubleRBracket, DArrow, Num(u64), - Ident(Name<'a>), + Ident(&'a Name), } const KEYWORDS: &[(&str, Token<'static>)] = &[ @@ -134,7 +134,7 @@ impl<'a> Lexer<'a> { KEYWORDS .iter() .find(|(kw, _)| *kw == ident) - .map_or(Token::Ident(Name(ident)), |(_, tok)| *tok) + .map_or(Token::Ident(Name::new(ident)), |(_, tok)| *tok) } #[inline] diff --git a/compiler/src/lexer/testutils.rs b/compiler/src/lexer/testutils.rs index d567a90..80b9460 100644 --- a/compiler/src/lexer/testutils.rs +++ b/compiler/src/lexer/testutils.rs @@ -9,7 +9,7 @@ const IDENTIFIERS: &[&str] = &[ pub fn gen_token() -> impl bolero::ValueGenerator> { one_of(( - one_value_of(IDENTIFIERS).map_gen(|s| Token::Ident(Name(s))), + one_value_of(IDENTIFIERS).map_gen(|s| Token::Ident(Name::new(s))), one_value_of(KEYWORDS).map_gen(|(_, t)| t), one_value_of(SYMBOLS).map_gen(|(_, t)| t), any::().map_gen(Token::Num), diff --git a/compiler/src/parser/ast.rs b/compiler/src/parser/ast.rs index 8a11b56..8c45ebe 100644 --- a/compiler/src/parser/ast.rs +++ b/compiler/src/parser/ast.rs @@ -20,7 +20,7 @@ impl std::fmt::Debug for FunName<'_> { #[derive(Debug)] pub enum Pat<'a> { - Name(Name<'a>), + Name(&'a Name), Lit(u64), } @@ -32,21 +32,21 @@ pub struct MatchArm<'a> { #[derive(Debug)] pub struct Let<'a> { - pub name: Name<'a>, + pub name: &'a Name, pub ty: Option<&'a Term<'a>>, pub expr: &'a Term<'a>, } #[derive(Debug)] pub struct Param<'a> { - pub name: Name<'a>, + pub name: &'a Name, pub ty: &'a Term<'a>, } #[derive(Debug)] pub struct Function<'a> { pub phase: Phase, - pub name: Name<'a>, + pub name: &'a Name, pub params: &'a [Param<'a>], pub ret_ty: &'a Term<'a>, pub body: &'a Term<'a>, @@ -60,7 +60,7 @@ pub struct Program<'a> { #[derive(Debug)] pub enum Term<'a> { Lit(u64), - Var(Name<'a>), + Var(&'a Name), App { func: FunName<'a>, args: &'a [&'a Self], diff --git a/compiler/src/parser/mod.rs b/compiler/src/parser/mod.rs index 42329c1..c14803f 100644 --- a/compiler/src/parser/mod.rs +++ b/compiler/src/parser/mod.rs @@ -55,7 +55,7 @@ where }) } - fn take_ident(&mut self) -> Result> { + fn take_ident(&mut self) -> Result<&'a Name> { self.expect_token("identifier", |token| { if let Token::Ident(name) = token { Some(name) @@ -158,7 +158,7 @@ where .with_context(|| format!("in function `{name}`")) } - fn parse_fn_def_after_name(&mut self, phase: Phase, name: Name<'a>) -> Result> { + fn parse_fn_def_after_name(&mut self, phase: Phase, name: &'a Name) -> Result> { self.take(Token::LParen).context("expected '('")?; let params = self.parse_params()?; self.take(Token::RParen).context("expected ')'")?; @@ -327,7 +327,7 @@ where } /// Parse a function call with arguments - fn parse_function_call(&mut self, name: Name<'a>) -> Result> { + fn parse_function_call(&mut self, name: &'a Name) -> Result> { let args = self.parse_separated_list(Token::RParen, |parser| { parser.parse_expr().context("parsing function argument") })?; diff --git a/compiler/src/parser/test/mod.rs b/compiler/src/parser/test/mod.rs index ff90fad..39d93ea 100644 --- a/compiler/src/parser/test/mod.rs +++ b/compiler/src/parser/test/mod.rs @@ -50,7 +50,7 @@ fn parse_simple_fn() { let program = parser.parse_program().unwrap(); assert_eq!(program.functions.len(), 1); let f = &program.functions[0]; - assert_eq!(f.name.0, "add"); + assert_eq!(f.name.as_str(), "add"); assert_eq!(f.params.len(), 2); } From 63295e7dd94343b0be3ad4ebf2ad8da3990adcbc Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 23:30:23 +0000 Subject: [PATCH 42/43] refactor: assert non-empty in Name::new Split the ref_cast_custom constructor into a private new_unchecked and a public new that asserts the name is non-empty. Co-Authored-By: Claude Sonnet 4.6 --- compiler/src/common/name.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler/src/common/name.rs b/compiler/src/common/name.rs index 9beb788..7342fdc 100644 --- a/compiler/src/common/name.rs +++ b/compiler/src/common/name.rs @@ -4,7 +4,12 @@ pub struct Name(str); impl Name { #[ref_cast::ref_cast_custom] - pub const fn new(s: &str) -> &Self; + const fn new_unchecked(s: &str) -> &Self; + + pub const fn new(n: &str) -> &Self { + assert!(!n.is_empty(), "Empty name"); + Self::new_unchecked(n) + } pub const fn as_str(&self) -> &str { &self.0 From 9095475e9bbd4f50cf17042e0e0d643dfe1b0612 Mon Sep 17 00:00:00 2001 From: LukasK Date: Fri, 27 Mar 2026 23:33:21 +0000 Subject: [PATCH 43/43] chore: clippy --- compiler/src/checker/mod.rs | 6 +++--- compiler/src/core/value.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/compiler/src/checker/mod.rs b/compiler/src/checker/mod.rs index b0b0ff7..485db0e 100644 --- a/compiler/src/checker/mod.rs +++ b/compiler/src/checker/mod.rs @@ -445,7 +445,7 @@ fn elaborate_bodies<'src, 'core>( // Push parameters as locals so the body can reference them. for (pname, pty) in pi.params { - ctx.push_local(*pname, pty); + ctx.push_local(pname, pty); } // Elaborate the body, checking it against the declared return type. @@ -534,7 +534,7 @@ pub fn infer<'src, 'core>( // Look up the name in locals; return its index and type. ast::Term::Var(name) => { // First check if it's a built-in type name — those are inferable too. - if let Some(term) = builtin_prim_ty(*name, phase) { + if let Some(term) = builtin_prim_ty(name, phase) { // Phase check: U(Object) (VmType) is only valid in a meta-phase context. if let core::Term::Prim(Prim::U(u_phase)) = term { ensure!( @@ -546,7 +546,7 @@ pub fn infer<'src, 'core>( return Ok(term); } // Check locals. - if let Some((ix, _)) = ctx.lookup_local(*name) { + if let Some((ix, _)) = ctx.lookup_local(name) { return Ok(ctx.alloc(core::Term::Var(ix))); } // Check globals — bare reference without call, produces Global term. diff --git a/compiler/src/core/value.rs b/compiler/src/core/value.rs index ec9b24e..b76f29d 100644 --- a/compiler/src/core/value.rs +++ b/compiler/src/core/value.rs @@ -78,7 +78,7 @@ pub fn eval<'a>(arena: &'a Bump, env: &[Value<'a>], term: &'a Term<'a>) -> Value Term::Prim(p) => Value::Prim(*p), Term::Lit(n, it) => Value::Lit(*n, *it), - Term::Global(name) => Value::Global(*name), + Term::Global(name) => Value::Global(name), Term::Lam(lam) => eval_lam(arena, env, lam), Term::Pi(pi) => eval_pi(arena, env, pi), @@ -169,7 +169,7 @@ pub fn eval_pi<'a>(arena: &'a Bump, env: &[Value<'a>], pi: &'a Pi<'a>) -> Value< body: rest_body, }; Value::Pi(VPi { - name: *name, + name, domain: arena.alloc(domain), closure, phase: pi.phase, @@ -197,7 +197,7 @@ fn eval_lam<'a>(arena: &'a Bump, env: &[Value<'a>], lam: &'a Lam<'a>) -> Value<' body: rest_body, }; Value::Lam(VLam { - name: *name, + name, param_ty: arena.alloc(param_ty), closure, }) @@ -256,7 +256,7 @@ pub fn quote<'a>(arena: &'a Bump, depth: Lvl, val: &Value<'a>) -> &'a Term<'a> { let ix = lvl.ix_at_depth(depth); arena.alloc(Term::Var(ix)) } - Value::Global(name) => arena.alloc(Term::Global(*name)), + Value::Global(name) => arena.alloc(Term::Global(name)), Value::Prim(p) => arena.alloc(Term::Prim(*p)), Value::Lit(n, it) => arena.alloc(Term::Lit(*n, *it)), Value::App(f, args) => {