Skip to content

LINT028 and LINT029: argument mutation is a finding - #3910

Merged
borisbat merged 3 commits into
masterfrom
aleksisch/lint-arg-mutation
Sep 2, 2026
Merged

LINT028 and LINT029: argument mutation is a finding#3910
borisbat merged 3 commits into
masterfrom
aleksisch/lint-arg-mutation

Conversation

@aleksisch

Copy link
Copy Markdown
Collaborator

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 var argument 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. In examples/games/river_run, drop_texture and drop_fbo zeroed a by-value copy, so the caller's GL handle stayed stale and a reinit could double-delete it. In modules/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 var array 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 every daslib/ folder (library code lives on the builder/state idiom). A deliberate out-parameter keeps its var under // 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.das for both rules and the exemption arms, daslib/lint_config.das for lint029_source_exempt, and the three fixtures under utils/lint/tests/.

Validation, claims, ledger

Validation

  • The full pre-PR chain was NOT run - this is a draft opened on request, not a merge candidate. The branch is 42 commits behind origin/master and has not been rebased.
  • Lint gate is knowingly red: about 1270 LINT029 findings remain in unconverted areas. Zero compile errors tree-wide (3918 files swept).
  • Suites run and green: 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_conformance cannot 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.
  • Docs: das2rst regenerated clean (no stubs, no Uncategorized), sphinx build succeeded; no new warning touches the lint pages.

Claims - stated, not tested

  • LINT029 does not break ordinary builds. Verified by compiling files that carry findings: the [lint_macro] only arms in modules that require 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.
  • The dasTerminal and river_run fixes restore intended behavior rather than change it. Verified by reading each caller: both callers already assumed the callee had written through. A break would look like a hyperlink re-emitted per row, or a GL handle freed twice.

Not done

  • About 1270 LINT029 findings remain: dasLLAMA (core, harness, benchmarks), dasImgui, dasVulkan, dasSpirv, dasMetal, examples, tutorials and parts of tests.
  • No rebase onto current master, and no full preflight, AOT build or AOT test run.
  • Pre-existing findings in modules/dasSMT (STYLE014/015/039, PERF030, LINT016/019) are untouched - that module is not part of this arc.

🤖 Generated with Claude Code

@aleksisch
aleksisch force-pushed the aleksisch/lint-arg-mutation branch 19 times, most recently from b5f32db to 1345624 Compare September 2, 2026 09:01
@aleksisch
aleksisch marked this pull request as ready for review September 2, 2026 09:02
@aleksisch
aleksisch force-pushed the aleksisch/lint-arg-mutation branch 3 times, most recently from 170d880 to 0246ab2 Compare September 2, 2026 10:12
…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
aleksisch force-pushed the aleksisch/lint-arg-mutation branch from 0246ab2 to 2dbe148 Compare September 2, 2026 10:57
@borisbat
borisbat merged commit 60ff7c1 into master Sep 2, 2026
38 checks passed
@borisbat
borisbat deleted the aleksisch/lint-arg-mutation branch September 2, 2026 12:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants