agent: builder model: opus max_turns: 90 budget_usd: 18 —

Brief: #1043, a lambda bound by an untyped 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).

Issue #1043 (copied)

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):

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).

Locations

To build

  1. A local bound to a lambda by an untyped 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:.
  2. Test 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).

Done when

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.

Stop and report if

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.

Rules (CLAUDE.md; this block is pasted into every brief, also a fix-forward)

Report (at most 15 lines, to exactly /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

This site is open source. Improve this page.