M3: Closures (= by-value / := by-reference capture)#36
Merged
Conversation
Add lexical closures whose capture mode is inferred from the binding operator,
mirroring Quilon's existing mutability rule:
- `=` bindings are captured BY VALUE (a frozen, read-only snapshot).
- `:=` bindings are captured BY REFERENCE — a single shared GC-boxed cell, so
writes escape the closure and persist/accumulate across calls.
There is no capture list and no marker; the operator that bound the name is the
signal. Closures are monomorphic (concrete-typed params/captures) — generic and
polymorphic-capturing closures are deferred to M4.
Pipeline:
- AST: new `Expr::Lambda` function-literal node.
- Parser: lambda expressions (`x => …`, `(a,b) => …`, `() => …`), with a
one-shot `no_lambda` guard so a for-loop collection (`for n <- xs => body`)
is not mis-parsed as `xs => body`.
- Checker: `check_lambda` infers `Type::Function`; the body is checked in a
layered scope so capture is visible and the mutability gate still applies.
- Codegen: a closure value is `{ ptr fn, ptr env }`; the lifted function takes
the captured environment as a trailing pointer. `=` captures are copied into
the env by value; `:=` captures are heap-boxed via `__alloc` and shared.
A non-capturing nested function is emitted as a plain module function so it
can recurse; captures are threaded through arbitrary nesting depth, and a
closure value may itself be captured by another closure and called.
Tests/docs:
- examples/closures.ql (counter via `:=` cell + adder via `=` value -> exit 42),
wired into the examples gate (JIT + native AOT under clang and gcc).
- tests/closures_test.rs covers accumulation, frozen-snapshot, shared cells,
mixed capture, shadowing, nested recursion, and multi-level capture.
- LANGUAGE.md: Closures section + feature matrix + limitations boundary.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # LANGUAGE.md # src/codegen/generator.rs # src/typechecker/checker.rs # tests/examples_test.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds lexical closures to Quilon. The capture mode is inferred entirely from the binding operator — no capture list, no marker — mirroring the language's existing mutability rule:
=is captured by value (a frozen, read-only snapshot);:=is captured by reference — a single shared GC-boxed cell, so writes escape the closure and persist/accumulate across calls.Closures are monomorphic in M3 (concrete-typed params and captures). Capturing a polymorphic value, generic closures, passing a closure as a function parameter, and returning a closure from a function are deferred to M4; a closure used in an unsupported position is rejected at compile time, never miscompiled.
How (pipeline)
nodes.rs): newExpr::Lambdafunction-literal node.ast_parser.rs): lambda expressions (x => …,(a, b) => …,() => …, with optional-> Type). A one-shotno_lambdaguard prevents a for-loop collection (for n <- xs => body) from being mis-parsed asxs => body. Parameter parsing is shared viaparse_param_list.checker.rs):check_lambdainfersType::Function; the body is checked in a scope layered over the enclosing environment, so captures are visible and the immutability gate still applies inside closures.generator.rs): a closure value is{ ptr fn, ptr env }; the lifted top-level function takes the captured environment as a trailing pointer.=captures are copied by value into the env;:=captures are heap-boxed via the existing__allocintrinsic and shared (writes escape, escape-safe). A non-capturing nested function is emitted as a plain module function so it can recurse; captures thread through arbitrary nesting depth, and a closure value may itself be captured by another closure and called.ast/captures.rs): free-variable computation parameterized by the enclosing scope, so a:=to an outer name is recognized as a reassignment of a captured cell (a use) rather than a fresh local.Tests / docs
examples/closures.ql— a counter that mutates a:=-captured cell across calls (writes escape) plus an adder that captures an=binding by value;^returns exit code 42. Wired intotests/examples_test.rs, which runs it under the JIT and native AOT (clang + gcc) and asserts all three agree.tests/closures_test.rs— 12 run tests::=accumulation, frozen=snapshot, shared cells, mixed capture, parameter shadowing, immutability preserved through capture, non-capturing nested recursion, capturing-and-calling another closure, and multi-level capture.LANGUAGE.md— new Closures section, feature-matrix entry, and the M4 boundary.Green gate
cargo build✅cargo test✅ (incl. the native-AOT examples gate, run with clang + gcc — not JIT only)cargo fmt --check✅cargo clippy --all-targets -- -D warnings✅/code-reviewand/simplifywere run; findings addressed (notably: nested-recursion regression fixed, multi-level mutable capture sharing fixed, capturing-a-closure call fixed, and duplicated signature/return-type/call-result logic factored into shared helpers).🤖 Generated with Claude Code