agent: builder model: opus max_turns: 90 budget_usd: 18 —
let, handed where a function value is kept, is kept (rustc)Score: compiler bug, false acceptance (nikaia check passes, rustc refuses) on the plainest way to name a function value; bugs-first default of docs/priorities.md. Found by b1034b (3571b0bf).
A lambda bound by an untyped let and then handed where a function value is kept (a fn parameter the callee keeps, a returned fn, a struct field of fn type) passes nikaia check and reaches rustc as a bare closure where Kept<dyn Fn ..> is wanted. With the type written (let add: fn(i64) -> i64 = fn(x) { x + 3 }) the same program runs (4 5, probed), and an inline lambda in the same place runs too. Part I 5.4: a function value in a result or a field is kept.
Probes, run by the architect on the fast binary of 08:06 (each its own program):
fn keep(f: fn(i64) -> i64) -> fn(i64) -> i64 { return f }, main: let add = fn(x) { x + 3 }, let t = keep(add), println(f"{t(1)}"): rustc Mismatched types at let t = keep(add), expected Kept<dyn Fn(i64) -> Pin<Box<dyn Future<Output = i64>>>>, found closure.fn mk(k: i64) -> fn(i64) -> i64 { let g = fn(x) { x + k }; return g }, main: let t = mk(2), println(f"{t(1)}"): the same rustc error at return g.struct H { f: fn(i64) -> i64 }, let g = fn(x) { x + 1 }, let h = H { f: g }, println(f"{h.f(1)}"): NK2605 at check (H.f can fail with ?); with H { f: fn(x) { x + 1 } } it prints 2. Unverified that it shares the cause.Traced (a, b, by reading the code): check_core_a_kept_lambda (crates/nikaia-std/src/tools/check_core_leaves.nika:211) marks a closure kept (facts.kept_lambdas, result, hulls, captures), and a Variable only when it names a function of this program (:241-245, state.lookup(named) == null && world.own.functions.contains_key(named), then check_core_a_kept_function :272). A local bound to a run-only lambda falls through both, so the emitter writes the plain closure. A typed let works: check_core_stmt.nika hands its closure to check_core_a_kept_lambda with the declared type (the assignment at :1245 does the same).
Not this brief: add(1) after keep(add) (or after apply(add)) is NK1245, the type of an untyped lambda’s parameter taken from a later call: #833 (decision). Do not put such a row in the test. Do not write fs[0](1) or same(inc)(1) (parser, #1042).
check_core_leaves.nika:211 check_core_a_kept_lambda (Expr::Variable arm :241), :272 check_core_a_kept_function (the model: a named function where a function is kept is the closure that calls it, host.kept_call, fact kept_functions).check_core_stmt.nika:531 check_core_stmt_let; check_lambdas.nika:68/81/125 (a_kept_lambdas_result, a_kept_lambdas_hulls, a_kept_lambda_takes_these); check_facts.nika:261 kept_lambdas and :275 kept_functions (keyed by node_key/id_of).emit_expr.nika:498 emit_expr_kept_function (writes nikaia_std::func::Kept(Arc::new(move |..| ..)); it reads kept_calls by the name’s text, so a local named like a program function must not collide), emit_core_flow.nika:205/243 (reads kept_lambdas), emit_core_args.nika:327/749 (kept_fn_copies vs kept_functions). A kept closure that calls a captured non-Copy closure per call needs the hull of #1034 step 1 (kept_hulls, ec386c71) or a clone.crates/nikaia-std/src/tools/lowered/ (regenerated by lower-std).tests/compiler/tests/a_kept_lambda_calls_a_captured_fn.nika, a_kept_fn_copied_from_a_name_or_field.nika, a_returned_lambda_keeps_its_fn_parameter.nika.let, handed where a function value is kept, is kept. Two ways, your choice, say which in Body:: (i) once a kept use is seen, the let’s closure node becomes a kept lambda with the wanted type (into kept_lambdas, result/hulls/captures checked as kept), so the let lowers as a Kept as a typed let does; you need a fact from the local’s binding to its closure node: if none exists, add it to check_facts.nika (written in check_core_stmt_let); or (ii) the use is the closure that calls the local, as a named function is (check_core_a_kept_function, with a lookup of a local of Ty::Fn). Any lowering rustc accepts. A run-only use (apply(add) to a lent parameter) keeps its present lowering: check it with nikaia lower and say so under Tests:.tests/compiler/tests/a_let_lambda_handed_where_a_function_is_kept.nika + .stdout, rows a and b in one program (keep and mk side by side, main printing t(1) of each: 4, 3), failing on the unfixed binary. Row c joins it only if your fix makes it print 2; else c goes under Found: with its message (then the test keeps a and b, Fixes: #1043 stays). Commit check: a let-bound lambda handed where a function is kept is kept (Fixes #1043).The test passes through nikaia test and fails on the unfixed compiler; the three model tests and the tests nikaia run scripts/where.nika -- check_core_a_kept_lambda lists pass; lower-std fixpoint clean; cargo build --profile fast -p nikaia; the guard set of .claude/agents/builder.md green; pushed. Changelog: one line (a lambda named by an untyped let and handed where a function value is kept is kept, instead of a lowering rustc refused). Stop about 15 turns before max_turns, commit what is finished and report partial.
An edit is needed outside the check_core_*.nika, check_lambdas.nika, check_facts.nika, emit_expr.nika, emit_core_*.nika files, their lowered twins (and lowered.rs), files lower-std rewrites because of your change, and the new test; an error in another agent’s file. tests/compiler failures from a binary vanishing mid-run (another agent rebuilding) are not yours: run the named tests singly instead.
Not yours, never stage: every file git status shows modified or untracked at your start (today among them crates/nikaia/src/parser/mod.rs, crates/rs2nika/convert/*, a stray file whose name starts ork in the tree)), and every docs/briefs/* file except your report.
docs/builder-facts.md first. develop in the main checkout; git status at start: other agents’ uncommitted files are not yours, never touch or stage them.git add <files> only; commit after each step; git push origin develop fast-forward, no rebase (rejected: report Pushed: rejected, go on committing).cargo fmt, cargo clippy --all-targets -- -D warnings, scripts/pre-push-check.sh, and the gate of .claude/agents/builder.md (#852).CHANGELOG.md, badges; no new NK code. New tests in Nikaia only; never the full suite. No foreground sleep, no polling; scripts in files.std or the toolchain lacks stops the work: no code around it unless an ADR or Part I-III refuses it (name it). Else Result: blocked, Found: gap + probe, Needs: owner decision (CLAUDE.md, A missing form stops the work).NIKAIA_CACHE_LIMIT=2G; at most two clones with target/ in the machine; delete your own clone’s target/ and target/release/nikaia-scratch at the end; report GB freed.Pushed: line names the pushed sha. A report without a Pushed: line, or with a placeholder left in (TC, DISK), is not done and cuts the batch (k3a1 2026-10-29: Result: done, nothing committed)./home/user/Nikaia/docs/briefs/b1043-let-lambda-kept.md.report.md)Fields of docs/architect.md, Briefs and reports: Result: done|noop|partial|blocked (the bare word alone on that line: Result: done, never a summary after it, which cuts the batch, b869a 2026-10-27; Owner: is a question for the owner or none, never a role), Files, Commit, Body (optional, one paragraph for the commit message), Fixes, Pushed, Tests, Gate (green|red), Deviation, Found, Needs, Changelog, Owner, Disk freed, and for a move Rs2nika: (the kept N of M lines of rs2nika --score, then the three costliest hand edits; none for a task that moves no Rust, none (redesign) for a brief with Shape: redesign and no Convert: line). none where nothing, and then the bare word: Deviation: none (a note) cuts the batch (batch.nika compares exactly); a note goes into the commit message. Found: is only for a defect outside your change (it cuts the batch); a process event such as another session resetting or rewriting your files is no Found:, it goes under Needs: (m3a 2026-10-28); a converter draft that came out wrong is no Found:, it is one of the costliest edits under Rs2nika:, which the converter lane reads (e12-4b 2026-10-27); how you solved your own step goes into the commit message. Deviation: is only a step of the brief built otherwise than written or not built (a choice the brief left to you, such as keep what X needs and say so, is no deviation: it goes into the commit message; nor is building two steps in one commit when every step is built, e12-3a 2026-10-27, nor a step another commit on develop already built, shown by your test failing with that commit’s change reverted, b889 2026-10-27: name the commit in Body:, nor a test’s expected .stdout/.stderr taken from what the program really prints where the brief’s sketch of it was short or wrong, b934 2026-10-29: say so in Body:, nor a lowering that follows the ADR governing it where the brief’s expected lowering contradicts that ADR, b933 2026-10-29: name the ADR in Body:, nor a lowering other than the brief’s where rustc cannot take the brief’s, b1029 2026-10-31: say why in Body:, nor re-keying an allow-list entry (KNOWN) whose hash changed for the same program and the same reason, b-fixforward-sweep2 2026-10-28: say so in Body:, nor an existing test’s expected .stdout/.stderr updated because your change rightly changes the message it prints, b1012b 2026-10-31: name the test in Body:); how you verified (e.g. a test not run against the unfixed compiler) goes under Tests:. A noop says why under Tests: (what was probed), not under Deviation:. Final answer, one line: done <sha>, report <path> (or BLOCKED: ... / PUSH REJECTED ...).
Attribution: Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01GFeMqxRbtUMiGVyDMxsvEq