LINT028 and LINT029: argument mutation is a finding - #3910
Draft
aleksisch wants to merge 7 commits into
Draft
Conversation
aleksisch
force-pushed
the
aleksisch/lint-arg-mutation
branch
16 times, most recently
from
September 1, 2026 19:38
53506c1 to
3f12cf3
Compare
…s 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.
Functions that built their result through a mutable argument now return it. Out-parameter clusters that traveled together became a returned struct or a tuple; single collectors return their array or table; the recursive walks in the lint runner, the AST-fuzz generator and the LSP merge each subtree into the parent, which retired two shared dedup tables - sorting already puts a nested root's repeats adjacent. Two live bugs surfaced. river_run's drop_texture and drop_fbo cleared a by-value copy, so the caller's GL handle stayed stale and a reinit could double-delete it; they return the cleared handle now. dasTerminal's OSC8 hyperlink dedup state traveled by value through the checkpoint chain, so every row restarted from stale state and re-emitted escape sequences. doc-verify called mark_elisions for effect after it became a returning function, silently marking nothing at three sites.
LINT029 catches a mutated by-ref argument - a `var` array or table, or an explicit `&`. Functions communicate through return values; a call site whose arguments are all read-only is one a reader can settle locally. Exempt: a returned parameter (how a non-copyable value moves out), block and lambda parameters (a callback's slot is its caller's contract), and every `daslib/` folder, where the builder/state idiom is the library's own. Struct parameters are not exempt: wrapping two flagged containers in a one-field struct made the finding disappear without changing what the function does, which is the dodge the rule exists to catch. A struct whose every field access yields a pointer or a handle stays exempt - the var is what keeps the CONTAINED handle non-const. A by-value argument is left alone. Writing one cannot reach the caller, so the parameter IS the local - forcing a copy into a temporary buys a name and nothing else. LINT023 keeps the half that finds a real defect: a by-value argument written and never read back, where the write is simply dead. A file that must decline a rule says so at the top: options _nolint = "LINT029,PERF030" silences those codes for that file only, layered last so a whole-file shape the rule cannot see has a hatch. Unknown codes are ignored, so a typo silences nothing.
The 152 by-ref out-parameters under utils/internal/das-herd are gone: argv builders and parsers return their arrays, GitCommandScan and WorktreeOperationPlan are RETURNED rather than filled, scalar out-pairs became named tuples, and the selection state machine takes its state and hands it back. Seven survive as nolint - the jobque capture-release contract and imgui_boost's per-widget storage, both of which need the caller's own handle. That conversion made PERF030 fire 103 times on a self-returning move, where the callee hands back the same value it took, so the target is already moved-from when the result lands. The rule cannot see that shape. Rather than repeat one nolint per line, a module can now decline a rule outright with `options _lint_disable = "CODE,CODE"`: it layers last, over CLI flags and repo config, and both the [lint_macro] path and the standalone runner honor it.
validate.das requires daslib/lint, so the rule armed on the validator's own source and it stopped compiling - the language server was dead for every file it was asked about. Its three collectors now return what they found: compile_error_diagnostics, lint_diagnostics and collect_lint_diagnostics hand back arrays the caller appends. nav.das follows: decl_ranges returns the (full, selection) pair it used to write through two references, and doc_child / ws_symbol / ws_function_symbols return the at-most-one node they used to push.
The first files of the dasImgui and dasLLAMA sweeps, each finished whole before the next was started: plot samplers renamed for what they produce (sine_samples, bar_samples, histogram_samples), the grammar canary and text-edit fixtures returning their rows and spans, and the Metal shape helpers handing back what they used to write through an argument.
[skip ci] dasImgui goes 615 -> 454. The icons module is the headline: IconCtx was never written, only the ImGui draw list behind its pointer field, and daslang propagates const from a struct into a pointer field's pointee - so `var` was mandatory for a struct nobody mutated. Lifting the draw list out into a first parameter across the seven primitives, 104 glyphs and the dispatch cleared all 127 findings there, same calls in the same order. Text flow, find, the edit providers and the markdown model return what they used to fill. dasLLAMA goes 856 -> 778, twelve files to zero. Tokenizer derivation, the exchange validators, kernel access and the chat renderers return their results; the validator tree merges each subtree's errors instead of threading one list, and applies its cap once at the public entries. The nolints name what pins them: a per-layer requant loop at dasllama_blocks.das:1542, the tokenizer's merge heaps whose REVIEW.md forbids superlinear scaling, and consumers in dasllama-ladder that REVIEW_EXCHANGE.md fixes as the rails' gate.
aleksisch
force-pushed
the
aleksisch/lint-arg-mutation
branch
from
September 1, 2026 20:03
3f12cf3 to
b76d9e7
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