Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 12 additions & 25 deletions src/diagnostic/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,35 +242,22 @@ do not need braces:
},
ErrorEntry {
code: "ILO-P017",
short: "inline lambda captures outer scope",
long: r#"## ILO-P017: inline lambda captures outer scope
short: "use-import failed",
long: r#"## ILO-P017: use-import failed

Phase 1 inline lambdas — `(p:t>r;body)` passed to a HOF like `srt`, `map`,
`flt`, `fld`, `grp` — cannot close over variables from the enclosing function.
Every name referenced in the body must be a parameter of the lambda, a name
bound locally inside the lambda body, or a known top-level function/builtin.
A `use "path.ilo"` declaration could not be resolved. Possible causes:

**Wrong:**

rank xs:L n threshold:n>L n
srt (x:n>n;-x threshold) xs

The lambda references `threshold` from the enclosing scope.

**Fix A: use the HOF's ctx-arg form.** Every closure-aware HOF accepts an
optional context value that is threaded through every call:

rank xs:L n threshold:n>L n
srt (x:n c:n>n;-x c) threshold xs

**Fix B: define a top-level helper** that takes the value as a param and use
`srt fn ctx xs`:
- The path is not reachable from a file context (inline code via
`ilo '<src>'` has no base directory to resolve against)
- The file does not exist at the given relative path
- The file could not be read (permissions, IO error)

diff x:n c:n>n;-x c
rank xs:L n threshold:n>L n;srt diff threshold xs
The diagnostic message identifies the specific failure mode.

Closure capture is tracked as a Phase 2 follow-up; once it lands, free
variables will be captured by value automatically.
**Note:** ILO-P017 used to be raised by inline lambdas with captures from
the enclosing scope. Closure capture now works on every engine (tree, VM,
Cranelift JIT/AOT) so that path no longer errors; the code was repurposed
for `use`-import resolution.
"#,
},
ErrorEntry {
Expand Down
7 changes: 4 additions & 3 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3588,9 +3588,10 @@ For variable-position list indexing bind the head first: \
/// returns `Expr::Ref("__lit_N")` so HOFs see a fn-ref identical to a
/// named helper.
///
/// Phase 1: closures are rejected. Any reference to a name that isn't a
/// param, isn't a local binding, and isn't a known function/builtin
/// raises ILO-P017 pointing at the Phase 2 follow-up.
/// Free variables in the body (names that aren't params, locals, or known
/// top-level fns/builtins) become Phase 2 captures: appended as extra
/// params on the lifted decl and snapshot by value via `Expr::MakeClosure`
/// at the call site. Phase 2 closure capture works on every engine.
fn parse_inline_lambda(&mut self) -> Result<Expr> {
let start = self.peek_span();
self.expect(&Token::LParen)?;
Expand Down
27 changes: 14 additions & 13 deletions src/vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,13 @@ pub enum CompileError {
UndefinedVariable { name: String },
#[error("undefined function: {name}")]
UndefinedFunction { name: String },
/// Inline lambda with capture (`Expr::MakeClosure`) cannot lower to the
/// register VM yet — HOF dispatch with N captures depends on the parked
/// FnRef NaN-tagging effort. Returning this from `vm::compile` lets the
/// default runner fall through to the tree interpreter cleanly, rather
/// than panicking partway through codegen.
#[error("inline lambda capture for `{fn_name}` is tree-only")]
/// Inline lambda with more than 255 captures cannot be encoded by the
/// register VM's `OP_MAKE_CLOSURE` instruction, which uses an 8-bit
/// capture-count field. Phase 2 closure capture is otherwise fully
/// supported across every engine; this variant only fires on the
/// pathological wide-capture case. Kept around so the compiler can
/// surface a structured error rather than panic.
#[error("inline lambda `{fn_name}` exceeds the 255-capture VM cap")]
UnsupportedClosureCapture { fn_name: String },
/// The register-VM byte-encoded instruction set is capped at 256 live
/// registers per function (8-bit register field). When codegen for a
Expand Down Expand Up @@ -6558,13 +6559,13 @@ impl NanVal {
}
}
Value::Closure { fn_name, .. } => {
// Closures don't round-trip through NanVal — VM/Cranelift HOF
// dispatch with N captures is downstream of this PR. The VM
// `compile_expr` branch for `Expr::MakeClosure` records a
// `CompileError::UnsupportedClosureCapture` before we get here
// in any well-formed program, so this sentinel-string fallback
// is belt-and-braces for stray Closures threaded through the
// tree-bridge dispatcher.
// Closures don't round-trip through NanVal as a bare tagged
// value — the VM uses a dedicated `HeapObj::Closure` plus
// `OP_MAKE_CLOSURE` for native dispatch. This sentinel-string
// fallback is only reached if a tree-level `Value::Closure`
// leaks into the NanVal conversion path (e.g. through a stale
// tree-bridge call site); native compilation goes through
// `Expr::MakeClosure` instead.
NanVal::heap_string(format!("<closure:{}>", fn_name))
}
}
Expand Down
Loading