diff --git a/src/diagnostic/registry.rs b/src/diagnostic/registry.rs index c4ceaf12..782879e0 100644 --- a/src/diagnostic/registry.rs +++ b/src/diagnostic/registry.rs @@ -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 ''` 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 { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4926a94c..cd4fbb50 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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 { let start = self.peek_span(); self.expect(&Token::LParen)?; diff --git a/src/vm/mod.rs b/src/vm/mod.rs index 22b801b2..086825c8 100644 --- a/src/vm/mod.rs +++ b/src/vm/mod.rs @@ -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 @@ -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!("", fn_name)) } }