Keep the discarded read-line calls, drop the unused names (angler unused-let-binding) - #2
Conversation
angler grew an unused-let-binding rule after bufio's last green run, and it fires twice on test/bufio.carp: 'first-line' is bound but never read. The binding is dead, the call is not — (BufReader.read-line &br) advances the reader so the following read returns the second line, which is what both assertions pin. Prefixing the name with an underscore is angler's own exemption for a deliberate discard (binding-name in angler.carp), the same spelling core's 'ignore' macro expands to. The call, its position in the binding list and its drop point are all unchanged: the generated C differs only in the four lines that name the variable.
There was a problem hiding this comment.
Build & Tests
Checked out claude/lint-unused-let-binding at bcffa71 and ran bufio's four CI gates locally with angler and carp-fmt built from current master (fd6d682, both binaries newer than that commit — I checked, since a stale local linter is exactly what hides this class of bug).
| Gate (the repo's exact CI command) | main |
this branch |
|---|---|---|
carp -x test/bufio.carp |
17 / 0 | 17 / 0, exit 0 |
Lint (angler $(find …), 3 files) |
exit 1, 2 findings | exit 0 |
Format check (carp-fmt --check) |
exit 0 | exit 0 |
carp -x gendocs.carp |
exit 0, no doc diff | exit 0, no doc diff |
CI is green on ubuntu-latest and macos-latest, and both runs are at the current head bcffa71. Branch is 0 behind / 1 ahead of origin/main.
Findings
None. I tried to break each claim in the body and every one held.
The premise is exact. Reverting only test/bufio.carp to main and re-running the same command reproduces the two findings verbatim:
./test/bufio.carp:27:4: [unused-let-binding] let-do binding 'first-line' is never used
./test/bufio.carp:145:6: [unused-let-binding] let-do binding 'first-line' is never used
The dates line up too: the rule landed in 00d1f8b on 2026-08-05, and bufio's last green run on main was 2026-06-19 (8549872). Since the Lint step clones angler --depth 1 and builds it fresh on every run, nothing in this repo had to change for it to go red.
The underscore is the supported exemption, not a trick. angler.carp:1353-1360 — binding-name returns Maybe.Nothing for any single-segment symbol where (starts-with-bytes? s "_"), so the pair is skipped entirely. angler's own suite pins it: test/angler.carp:507 asserts "an underscore-prefixed binding is not reported", separately from the bare-_ case at :504.
Behaviour-neutral, confirmed independently. I built both trees with carp -b test/bufio.carp and diffed the generated main.c. Both are 15475 lines and differ in exactly four, all of them the variable's name:
14754c14754 < Result__String_String first_MINUS_line = _77;
> Result__String_String _first_MINUS_line = _77;
14780c14780 < Result_delete__String_String(first_MINUS_line);
> Result_delete__String_String(_first_MINUS_line);
15245c15245 < Result__String_String first_MINUS_line = _683;
> Result__String_String _first_MINUS_line = _683;
15278c15278 < Result_delete__String_String(first_MINUS_line);
> Result_delete__String_String(_first_MINUS_line);
Same call sites, same order, same drop points — the rename does not move ownership.
The calls really are load-bearing, and the tests notice. Deleting the two bindings outright — the fix the linter's wording invites — gives exit 2, 15 passed / 2 failed, and it is exactly the two assertions that depend on the reader having advanced:
Test 'read-line returns second line' failed:
Test 'clear-read discards buffered data' failed:
That is the one way to get this change wrong, and the body identifies it correctly.
On the alternative the body raises: the (ignore …) shape does not fit site 1 without restructuring, and that is not a stylistic preference — let-do bindings are evaluated in order, so a body form cannot be interleaved between the discarded read and r's binding. Getting the ordering right would mean an extra nested let-do, i.e. rewriting the test rather than clearing a lint. The rename is the smaller change and I would keep it.
Verdict: merge
Two characters, and they take the repo from red-on-next-push to green. The premise, the exemption, the C-level neutrality and the load-bearing-call argument all reproduce exactly as described. Nothing user-visible changed, so no changelog entry is correct — and bufio has none anyway. Worth noting for a follow-up: strbuf/test/strbuf.carp:164 is the other repo in the org with the same shape.
bufio's CI would have gone red on the next push, through no fault of its own. The Lint step builds
anglerfresh from master and runs it over every.carpfile outsideout/,docs/,.carp-src/andexamples/— tests included — and angler gained anunused-let-bindingrule on 2026-08-05 (00d1f8b), after bufio's last green run on 2026-06-19. Running bufio's exact Lint command with an angler built from current master exits 1:The linter is right that the binding is dead, but the call it binds is not.
(BufReader.read-line &br)advances the reader, so the next read returns the second line — which is exactly what both assertions pin ("read-line returns second line", and "clear-read discards buffered data", where the first line has to be buffered beforeclear-readhas anything to discard). Deleting the binding is the obvious-looking fix and it is wrong.What changed
first-line→_first-linein both places. A leading underscore is angler's own exemption for a deliberate discard (binding-name, angler.carp), and it is the same spelling core'signoremacro expands to —(let [_ form] ()). The call, its position in the binding list, and its drop point are all unchanged.Verification
Everything below was run locally with
anglerandcarp-fmtbuilt from current master.carp -x test/bufio.carpcarp -x gendocs.carpTest output is byte-identical before and after (diffed with ANSI codes stripped).
Behaviour-neutral, at the C level.
carp -b test/bufio.carpon both trees; the generatedmain.cdiffers in exactly four lines, all of them the variable's name:Same call sites, same order, same drop points.
The calls really are load-bearing. Deleting the two bindings outright — the naive way to clear the finding — drops the suite to 15 passed / 2 failed, and it is precisely the two affected assertions that fail:
Alternative considered
Moving the discarded read into the
let-dobody as(ignore (BufReader.read-line &br)), which is the idiom the rest of this file already uses for discarded results. It reads well at the second site, but the first site would need an extra nestedlet-doto keep the reads ordered beforeris bound, and it moves the drop point earlier. That is a test rewrite rather than a lint fix, so I kept the rename. Happy to switch if you prefer theignoreshape.No CHANGELOG entry: bufio has no changelog, and nothing user-visible changed.
Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.