gen-c: a string's .len is strlen, in both spellings - #3490
Merged
Conversation
Closes #3489 Five passes named the `len` family and moved on, each for a stated reason. Splitting it by WHAT THE BASE ACTUALLY IS shows that a third of it needs no decision at all: `string` lowers to `const char*`, and C's answer for its length is `strlen`. THREE SPELLINGS, NOT ONE: `s.len()` ONE `ExprCall` named `s.len` 1322 in the specs `s.len` an `ExprFieldAccess` 687 `len(s)` a call to a free function 142 diagnostics The fourth time in this campaign that one rule had several spellings and only one was taught. The first two now go through ONE helper, so a change to the rule cannot reach one and miss the other. AND THE BASE DECIDES WHETHER AN ANSWER EXISTS: `string` 223 sites -> strlen(s) slice `[]T` 449 sites -> NEEDS A REPRESENTATION (#3464) Rust's `.len()` on `&str`, Zig's `.len` on `[]const u8` and C's `strlen` all count bytes, so the three agree and nothing is invented. MEASURED, whole corpus, -ferror-limit=0: errors 11 642 -> 11 401 (-241) files better/worse 14 / 0 `strlen(` emitted 0 -> 247, in 15 files igla_coder_eval 178 -> 103 `#include <string.h>` IS DECIDED BY THE SAME TWO HELPERS THE EMITTER USES. A missing include is an undeclared function -- the very family this repair shrinks -- and deciding it in a second place is how that happens. The mutant that drops it dies on five tests. THE THIRD SPELLING WAS MEASURED AND NOT HANDLED. Of the 302 argument shapes of `len(x)`, ZERO are a `string` parameter: 171 identifiers that are not parameters, 89 not plain identifiers, 19 slices, 14 `u32`. A branch for it would be unreachable today. A MUTANT THAT DROPPED THE PER-ITEM RESET SURVIVED, AND THE FIXTURE WAS THE REASON. The leak test declared an `i32` named `s` in a test block and asserted `s == 3` -- but a leaked set only shows up where the rule would FIRE. Rewritten to `assert(s.len() == 0)`, which becomes `strlen(s)` on an `i32` if the set leaks; the mutant dies. 1337 `.len` sites remain (886 calls, 451 fields), left loud on purpose: a slice lowers to a bare pointer, and `strlen` on one would read past the end of anything that is not NUL-terminated bytes. Tests: 6 new, full suite 3543 passed 0 failed. Six mutants, all killed. 14 stale seals refreshed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gHashTag
enabled auto-merge (squash)
September 8, 2026 12:15
Contributor
|
📓 NotebookLM Notebook linked to this PR
This notebook contains session context, decisions, and artifacts for this work. |
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
gen-c: a string's
.lenis strlen, in both spellingsCloses #3489
Five passes named the
lenfamily and moved on, each for a statedreason. Splitting it by WHAT THE BASE ACTUALLY IS shows that a third of
it needs no decision at all:
stringlowers toconst char*, and C'sanswer for its length is
strlen.THREE SPELLINGS, NOT ONE:
s.len()ONEExprCallnameds.len1322 in the specss.lenanExprFieldAccess687len(s)a call to a free function 142 diagnosticsThe fourth time in this campaign that one rule had several spellings and
only one was taught. The first two now go through ONE helper, so a
change to the rule cannot reach one and miss the other.
AND THE BASE DECIDES WHETHER AN ANSWER EXISTS:
string223 sites -> strlen(s)slice
[]T449 sites -> NEEDS A REPRESENTATION (#3464)Rust's
.len()on&str, Zig's.lenon[]const u8and C'sstrlenall count bytes, so the three agree and nothing is invented.
MEASURED, whole corpus, -ferror-limit=0:
errors 11 642 -> 11 401 (-241)
files better/worse 14 / 0
strlen(emitted 0 -> 247, in 15 filesigla_coder_eval 178 -> 103
#include <string.h>IS DECIDED BY THE SAME TWO HELPERS THE EMITTERUSES. A missing include is an undeclared function -- the very family
this repair shrinks -- and deciding it in a second place is how that
happens. The mutant that drops it dies on five tests.
THE THIRD SPELLING WAS MEASURED AND NOT HANDLED. Of the 302 argument
shapes of
len(x), ZERO are astringparameter: 171 identifiers thatare not parameters, 89 not plain identifiers, 19 slices, 14
u32. Abranch for it would be unreachable today.
A MUTANT THAT DROPPED THE PER-ITEM RESET SURVIVED, AND THE FIXTURE WAS
THE REASON. The leak test declared an
i32namedsin a test blockand asserted
s == 3-- but a leaked set only shows up where the rulewould FIRE. Rewritten to
assert(s.len() == 0), which becomesstrlen(s)on ani32if the set leaks; the mutant dies.1337
.lensites remain (886 calls, 451 fields), left loud on purpose:a slice lowers to a bare pointer, and
strlenon one would read pastthe end of anything that is not NUL-terminated bytes.
Tests: 6 new, full suite 3543 passed 0 failed. Six mutants, all killed.
14 stale seals refreshed.
🤖 Generated with Claude Code