LINT028 and LINT029: argument mutation is a finding - #3910
Merged
Conversation
aleksisch
force-pushed
the
aleksisch/lint-arg-mutation
branch
19 times, most recently
from
September 2, 2026 09:01
b5f32db to
1345624
Compare
aleksisch
marked this pull request as ready for review
September 2, 2026 09:02
aleksisch
force-pushed
the
aleksisch/lint-arg-mutation
branch
3 times, most recently
from
September 2, 2026 10:12
170d880 to
0246ab2
Compare
…e that says so ImgProbe's finalizer called fmap_close directly. For a chunk-backed probe - anything image_from_carrier builds in memory - image_map points at an image-page-aligned offset INTO a das-heap array<uint8>, so that munmap succeeded and punched a hole in the process heap. The array stayed alive in g_image_chunks, nothing complained, and the next large malloc walked into the unmapped pages: dastest died with SIGSEGV at a page-aligned address while building typeinfo for the NEXT test file. One file per process hid it from CI, which is how it survived. image_backing_release is the one release that tells a chunk from a mapping, and REVIEW_IMAGE.md already called releasing a backing anywhere else a defect. Nothing checked it, so REVIEW.das checks it now: fmap_close is legal inside that release, or inside a function that opened the mapping it closes, and nowhere else. The whole dasLLAMA suite runs to the end instead of crashing.
The rule read the whole body and let ANY read rescue a candidate, so a read that
precedes every store kept a dead write alive - the shape of a handle-clearing
helper:
def drop_texture(var tex : uint) {
if (tex != 0u) { // the read - BEFORE
glDeleteTextures(1, addr(tex))
tex = 0u // nothing reads it after
}
}
The parameter is a by-value copy, so the caller's handle stays live and a reinit
deletes it twice. river_run shipped exactly that in both drop_texture and
drop_fbo, and dasTerminal shipped its twin - its three checkpoint helpers took
the OSC8 hyperlink dedup state by value, so each one restarted from stale state
and re-emitted the escape sequence. All five parameters take a reference now,
which is the whole fix: a three-row checkpoint sharing one hyperlink emits one
OSC8 sequence where it emitted four.
A use now disqualifies a candidate only once a store has been seen. Two
placements are taken to run after the store whatever their position in the
source, because they can: a closure, lambda or generator body, which runs when
the block is invoked, and any loop body, where the next iteration puts every
read after every store. That is deliberately not branch_depth, which also counts
`if` - and the read that exposes the dead write sits inside one. A `label` or a
`goto` anywhere in a function drops the rule for all of it: a backward jump can
put a read after a store above it, so source order describes nothing.
LINT029 is the wider contract behind LINT023: a mutated by-ref argument - a `var` array or table, or an explicit `&` - is state the caller owns, and a function should return its result instead. Receiver position exempts nothing; a struct whose every field access yields a pointer or a handle does, because there `var` is what keeps the CONTAINED handle non-const. Returned parameters, block and lambda slots, and every `daslib/` folder stay outside it. It ships OFF, beside STYLE005 in seed_default_disabled, because it is advice rather than a defect report - CLAUDE.md carries the preference it encodes and says how to read a finding. A file arms it with options _lint = "LINT029", the mirror of options _nolint, which silences the listed codes for one file. Both layer last, after the repo config and the environment, so the file has the final say and an enable beats a disable; an unknown code changes nothing.
aleksisch
force-pushed
the
aleksisch/lint-arg-mutation
branch
from
September 2, 2026 10:57
0246ab2 to
2dbe148
Compare
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.
Two new lint rules make argument mutation a finding, and the tree is converted to match them where the conversion is done.
LINT028 catches a by-value
varargument that the body both writes and reads. The argument is silently a local: its writes never reach the caller, and after the first write the name no longer holds what the caller passed. The fix is a const parameter plus a local copy. This rule is complete across the tree, and it found two live bugs. Inexamples/games/river_run,drop_textureanddrop_fbozeroed a by-value copy, so the caller's GL handle stayed stale and a reinit could double-delete it. Inmodules/dasTerminal, the OSC8 hyperlink dedup state traveled by value through the checkpoint chain, so every row restarted from stale state and re-emitted escape sequences. Both are now by-reference.LINT029 catches a mutated by-ref argument - a
vararray or table, or an explicit&. Functions communicate through return values; a call site where every argument is read-only is one a reader can reason about locally. Four shapes stay exempt: a returned parameter (that is how a non-copyable value moves out), a struct or handle receiver in any position (the method idiom), block and lambda parameters (a callback's slot is its caller's contract), and everydaslib/folder (library code lives on the builder/state idiom). A deliberate out-parameter keeps itsvarunder// nolint:LINT029.This is a draft because the LINT029 conversion is partial. The lint tooling, dastest, the LSP subtools, daspkg, das-herd, doc-verify and dasLLAMA's performance tools are converted and their suites pass. About 1270 findings remain, mostly in dasLLAMA, the GPU modules and examples.
Where to look:
daslib/lint.dasfor both rules and the exemption arms,daslib/lint_config.dasforlint029_source_exempt, and the three fixtures underutils/lint/tests/.Validation, claims, ledger
Validation
tests/lint(82),dastest/tests(30),tests/json(396),tests/linq(2071),tests/lsp(2), das-herd watcher (123),tests/fio,tests/match, ast-fuzz (16), dasweb-verify (14), dasTerminal semantics.tests/sql_conformancecannot run here: dasSQLITE's native module is not built in this worktree (error 20605). Same for dasLLAMA and dasVulkan runtime tests - those sources lint and compile clean but were not executed.build succeeded; no new warning touches the lint pages.Claims - stated, not tested
[lint_macro]only arms in modules thatrequire daslib/lint, so findings surface through the standalone runner and the CI lint lane. A break would look like an unrelated program failing to compile with error 50503.Not done
modules/dasSMT(STYLE014/015/039, PERF030, LINT016/019) are untouched - that module is not part of this arc.🤖 Generated with Claude Code