Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,18 @@ diagnostic in any tier.

For path/filename ops use `fio` helpers (`base_name`/`dir_name`/`path_join`/...) - see `skills/daslang/references/files-and-paths.md`. Never hand-roll `rfind("/")` + slice: misses Windows separators.

**Return the result; do not fill an argument.** A function that hands its answer back
through a `var` parameter puts the caller's state in play at every call site, and a reader has
to open the callee to learn what moved. Return the value - a struct or a named tuple when
there is more than one. The carve-out is performance, and only measured performance: a
caller-owned buffer deliberately reused across calls on a hot path (inference kernels, audio
render callbacks, per-frame GPU/vertex buffers) stays an out-parameter, because returning a
fresh container there allocates per call. **LINT029 checks this and ships OFF** - arm it on a
file you are writing new code in with `options _lint = "LINT029"`, and treat what it says as
"be careful, keep this in mind" rather than a defect: reading a finding and moving on is a
normal outcome, not a suppression to justify. The dead-write half IS a gate - LINT023 is on
everywhere and reports a by-value out-parameter whose write nobody can read.

**Inline literals over temp-var-and-push:** for a short array consumed in one expression write `stack([a, b, c])`, not `var xs : array<T>; xs |> emplace(a); xs |> emplace(b); stack(xs)`. Faster interpreted and easier to read; same for table literals and other bracketed constructors, while it stays readable.

**Minimize `unsafe`:** most `unsafe(reinterpret<T?>)` in macro code exists to strip `const` from raw-pointer field access - fix the root cause by making the function parameter `var`, so field access returns non-const pointers. Reserve `unsafe` for genuinely unsafe operations (pointer arithmetic, `reinterpret` across unrelated types).
Expand Down
26 changes: 25 additions & 1 deletion daslib/ARCHITECTURE_LINT.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,35 @@ Companion to `ARCHITECTURE.md` in this folder; section numbers are unique across
from dead code. Public symbols, lifecycle/macro hooks, virtual methods, and generic
bodies are skipped: their callers are invisible to lint.
- **Argument rules skip what the author does not own, and converge leaf-first.**
LINT012/013/014/023 stay silent on class methods, `[extern]` stubs, `finalize`
LINT012/013/014/023/029 stay silent on class methods, `[extern]` stubs, `finalize`
overloads, and address-taken functions; LINT014 also skips a parameter whose mutability
a callee demands - the leaf is flagged first and each fix re-exposes the next caller.
A returned `var` argument keeps its `var` (a non-copyable result cannot move from
const); a used `_name` parameter is never LINT004'd (interface and keyword-clash names).
- **LINT023 reads the store/use stream in order.** A use disqualifies a candidate only
once a store has been seen, so a read that precedes every store leaves the write dead -
the shape that hides a cleared-but-not-returned handle. `lint023_deferred_depth` covers
the placements whose execution order is not the source order: a closure, lambda or
generator body, and any loop body, where a read is taken to follow the store. A `label`
or a `goto` anywhere in the function sets `lint023_jumps` and the rule reports nothing
there at all: a backward jump can place a read after a store above it, so the order test
has no ground to stand on. It is
deliberately NOT `branch_depth`, which also counts `if` and `try/catch`: a conditional
reorders nothing, and the read that exposes the dead write sits inside one - folding the
two counters together makes the rule silent on the shape it exists to catch.
- **LINT029 ships default-off and is advisory.** `seed_default_disabled` carries it beside
STYLE005, so a tree sees it only once a file arms it (`options _lint = "LINT029"`) or the
repo config turns it on. The findings are a reminder to prefer a returned result, not a
defect report, so skipping one is a normal outcome rather than a suppression to justify.
- **LINT029 fires per argument straight from the compiler's access flags**
(`access_ref` / `access_info_pass_mutable` - the same evidence LINT014 reads in
reverse) and shares LINT014's return-erase. Receiver position exempts nothing: a
mutated struct is a finding wherever it sits, and only a struct whose every field
access yields a pointer or a handle stands down - there `var` is what keeps the
CONTAINED handle non-const. The rule stands down wholesale when the program source
sits in any `daslib/` folder (`lint029_source_exempt`, `daslib/lint_config.das`):
library code lives on the builder/state idiom (`var self`, `var writer`), so the
purity contract binds application code only.
- **Closure bodies are per-rule, not global.** LINT010 counts a closure body as a branch
(it may run later or never - writes inside must not kill outside stores, reads inside
must not keep an outside init live); LINT021 counts the same body as an escape - a
Expand Down
96 changes: 90 additions & 6 deletions daslib/lint.das
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ class LintVisitor : AstVisitor {
@do_not_delete lint014_candidates : array<Variable const?>
lint023_candidates : array<Lint023Candidate>
lint023_store_nodes : table<uint64>
@do_not_delete lint029_candidates : array<Variable const?>
@do_not_delete lint029_mutated : array<Variable const?>
lint029_exempt : bool = false
//! AST visitor that performs lint checks.
exprForTerminator : array<uint64>
compile_time_errors : bool
Expand All @@ -177,6 +180,8 @@ class LintVisitor : AstVisitor {
lint021_narrow_depth : int = 0
lint021_poison_depth : int = 0
lint021_closure_depth : int = 0
lint023_deferred_depth : int = 0
lint023_jumps : bool = false
lint022_refs : RefIndex
@do_not_delete lint022_self_struct : Structure const? = null
lint022_generic_sweep : bool = false
Expand Down Expand Up @@ -340,6 +345,8 @@ class LintVisitor : AstVisitor {
lint021_narrow_depth = 0
lint021_poison_depth = 0
lint021_closure_depth = 0
lint023_deferred_depth = 0
lint023_jumps = false
if (fun.moreFlags.isTemplate) {
noLint = true
return
Expand All @@ -358,6 +365,10 @@ class LintVisitor : AstVisitor {
if (idx >= 0) {
lint014_candidates |> erase(idx)
}
let idx29 = lint029_candidates |> find_index(ev.variable)
if (idx29 >= 0) {
lint029_candidates |> erase(idx29)
}
}
}

Expand All @@ -370,13 +381,23 @@ class LintVisitor : AstVisitor {
lint014_candidates |> clear()
if (!noLint && genericDepth == 0) {
for (c in lint023_candidates) {
if (!c.disqualified && c.stores > 0) {
if (!c.disqualified && c.stores > 0 && !lint023_jumps) {
lint_error("LINT023: mutable by-value argument {c.v.name}: {describe(c.v._type, false, false, false)} is written but never read - the caller never sees a write to a by-value copy; declare it `{describe(c.v._type, false, false, false)}&` if it is an out-parameter, otherwise the write is dead", c.v.at)
}
}
}
lint023_candidates |> clear()
lint023_store_nodes |> clear()
if (!noLint && genericDepth == 0) {
for (v in lint029_candidates) {
let structish = (v._type != null
&& (v._type.baseType == Type.tStructure || v._type.baseType == Type.tHandle))
continue if (structish && lint029_mutated |> find_index(v) < 0)
lint_error("LINT029: by-ref argument {v.name}: {describe(v._type, false, false, false)} is mutated - every call site sees the write; declare the parameter const and return the result instead if this function should be pure", v.at)
}
}
lint029_candidates |> clear()
lint029_mutated |> clear()
if (!noLint && genericDepth == 0) {
for (e in ds_entries) {
if (!e.skip && e.has_pending && e.pending_pure && e.pending_expr != null) {
Expand Down Expand Up @@ -405,6 +426,7 @@ class LintVisitor : AstVisitor {
if (!noLint && genericDepth == 0 && (bf.isClosure || bf.isLambdaBlock || bf.isGeneratorBlock)) {
branch_depth++
lint021_closure_depth++
lint023_deferred_depth++
}
}
def override visitExprBlock(var blk : ExprBlock?) : ExpressionPtr {
Expand All @@ -415,6 +437,7 @@ class LintVisitor : AstVisitor {
if (!noLint && genericDepth == 0 && (bf.isClosure || bf.isLambdaBlock || bf.isGeneratorBlock)) {
branch_depth--
lint021_closure_depth--
lint023_deferred_depth--
}
return <- blk
}
Expand Down Expand Up @@ -449,6 +472,9 @@ class LintVisitor : AstVisitor {
exprForTerminator |> pop()
}
exprForTerminator |> push(0ul)
if (!noLint && genericDepth == 0) {
lint023_jumps = true
}
}

def override preVisitExprCast(expr : ExprCast?) : void {
Expand Down Expand Up @@ -540,6 +566,19 @@ class LintVisitor : AstVisitor {
lint014_candidates |> push(v)
}

def lint029_collect_candidate(fun : FunctionPtr; v : VariablePtr) {
if (lint029_exempt || noLint || genericDepth > 0 || v.flags.generated || v.flags.marked_used || v.isAccessUnused) return
let name = string(v.name)
if ((name |> starts_with("_")) || find(name, "`") >= 0
|| fun.name == "finalize" || fun.moreFlags.addressTaken
|| v._type == null || v._type.flags.constant
|| !(v._type.flags.ref || v._type.isRefType)
|| !(v.access_flags.access_ref || v.access_info.access_info_pass_mutable)) {
return
}
lint029_candidates |> push(v)
}

def lint023_collect_candidate(v : VariablePtr) {
if (noLint || genericDepth > 0 || v.flags.generated || v.flags.marked_used || v.isAccessUnused) return
let name = string(v.name)
Expand Down Expand Up @@ -575,7 +614,7 @@ class LintVisitor : AstVisitor {
if (ci < 0 || lint023_candidates[ci].disqualified) return
if (expr.varFlags.under_clone || key_exists(lint023_store_nodes, intptr(expr))) {
lint023_candidates[ci].stores++
} else {
} elif (lint023_candidates[ci].stores > 0 || lint023_deferred_depth > 0) {
lint023_candidates[ci].disqualified = true
}
}
Expand All @@ -591,6 +630,25 @@ class LintVisitor : AstVisitor {
validate_argument(arg, "LINT012", "function argument")
validate_unwritten_var_argument(fun, arg)
lint023_collect_candidate(arg)
lint029_collect_candidate(fun, arg)
}

def private lint029_note_mutation(v : Variable const?) : void {
if (v != null && lint029_candidates |> find_index(v) >= 0
&& lint029_mutated |> find_index(v) < 0) {
lint029_mutated |> push(v)
}
}

def override preVisitExprField(expr : ExprField?) : void {
if (noLint || genericDepth > 0 || expr.value == null) return
var obj = expr.value
if (obj is ExprRef2Value) {
obj = (obj as ExprRef2Value).subexpr
}
return if (!(obj is ExprVar) || (expr._type != null
&& (expr._type.baseType == Type.tPointer || expr._type.baseType == Type.tHandle)))
lint029_note_mutation((obj as ExprVar).variable)
}

def override preVisitExprBlockArgument(_blk : ExprBlock?; arg : VariablePtr; _lastArg : bool) : void {
Expand Down Expand Up @@ -1064,6 +1122,9 @@ class LintVisitor : AstVisitor {
lint022_refs.names |> insert("{expr.name}")
}
if (noLint || genericDepth > 0) return
if (key_exists(lint023_store_nodes, intptr(expr))) {
lint029_note_mutation(expr.variable)
}
lint023_note_use(expr)
let ci = lint021_find(expr.variable)
if (ci >= 0 && !lint021_candidates[ci].disqualified) {
Expand Down Expand Up @@ -1122,18 +1183,38 @@ class LintVisitor : AstVisitor {
return <- expr
}

//! A label or a goto puts control flow beyond what the source order describes - a backward
//! jump can place a read after a store that appears above it - so the order test cannot judge
//! this function and LINT023 stands down for all of it.
def override preVisitExprGoto(_expr : ExprGoto?) : void {
if (!noLint && genericDepth == 0) {
lint023_jumps = true
}
}
def override preVisitExprFor(_expr : ExprFor?) : void {
if (!noLint && genericDepth == 0) branch_depth++
if (!noLint && genericDepth == 0) {
branch_depth++
lint023_deferred_depth++
}
}
def override visitExprFor(var expr : ExprFor?) : ExpressionPtr {
if (!noLint && genericDepth == 0) branch_depth--
if (!noLint && genericDepth == 0) {
branch_depth--
lint023_deferred_depth--
}
return <- expr
}
def override preVisitExprWhile(_expr : ExprWhile?) : void {
if (!noLint && genericDepth == 0) branch_depth++
if (!noLint && genericDepth == 0) {
branch_depth++
lint023_deferred_depth++
}
}
def override visitExprWhile(var expr : ExprWhile?) : ExpressionPtr {
if (!noLint && genericDepth == 0) branch_depth--
if (!noLint && genericDepth == 0) {
branch_depth--
lint023_deferred_depth--
}
return <- expr
}
def override preVisitExprTryCatch(_expr : ExprTryCatch?) : void {
Expand Down Expand Up @@ -1169,6 +1250,7 @@ def public paranoid(prog : ProgramPtr; compile_time_errors : bool; disabled_code
//! ``// nolint:CODE`` directives.
var astVisitor = new LintVisitor(
compile_time_errors = compile_time_errors,
lint029_exempt = lint029_source_exempt(prog),
multiple_contexts = prog._options |> find_arg("multiple_contexts") ?as tBool ?? prog.policies.multiple_contexts)
astVisitor.disabled_codes := disabled_codes
make_visitor(*astVisitor) $(adapter) {
Expand All @@ -1194,6 +1276,7 @@ def public paranoid_collect(prog : ProgramPtr; var errors : array<string>;
var astVisitor = new LintVisitor(
compile_time_errors = false,
collect_errors = true,
lint029_exempt = lint029_source_exempt(prog),
multiple_contexts = prog._options |> find_arg("multiple_contexts") ?as tBool ?? prog.policies.multiple_contexts)
astVisitor.disabled_codes := disabled_codes
astVisitor.enabled_codes := enabled_codes
Expand All @@ -1216,6 +1299,7 @@ def public paranoid_collect_issues(prog : ProgramPtr; var issues : array<LintIss
var astVisitor = new LintVisitor(
compile_time_errors = false,
collect_errors = true,
lint029_exempt = lint029_source_exempt(prog),
multiple_contexts = prog._options |> find_arg("multiple_contexts") ?as tBool ?? prog.policies.multiple_contexts)
astVisitor.disabled_codes := disabled_codes
astVisitor.enabled_codes := enabled_codes
Expand Down
38 changes: 35 additions & 3 deletions daslib/lint_config.das
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,12 @@ def public load_lint_config_from_path(path : string; var disabled_codes : table<
}
}

//! Seeds ``disabled_codes`` with the canonical default-off rule set (currently just STYLE005). Call before ``load_lint_config`` so that a ``STYLE005 = true`` directive in ``.lint_config`` can re-enable it.
//! Seeds ``disabled_codes`` with the canonical default-off rule set (STYLE005 and LINT029). Call before ``load_lint_config`` so that a ``STYLE005 = true`` directive in ``.lint_config`` can re-enable it.
def public seed_default_disabled(var disabled_codes : table<string>) : void {
if (!key_exists(disabled_codes, "STYLE005")) {
disabled_codes |> insert("STYLE005")
for (code in ["STYLE005", "LINT029"]) {
if (!key_exists(disabled_codes, code)) {
disabled_codes |> insert(code)
}
}
}

Expand Down Expand Up @@ -218,9 +220,35 @@ def public build_lint_macro_disabled(prog : ProgramPtr) : table<string> {
if (key_exists(disabled, code)) disabled |> erase(code)
}
load_env_disabled(disabled)
add_module_rule_overrides(prog, disabled)
return <- disabled
}

//! The module-local rule overrides, layered LAST so the file has the final say: ``options _nolint``
//! silences the listed codes for this file, ``options _lint`` arms them (a default-off rule, or one
//! the repo config turned off). An enable beats a disable; an unknown code changes nothing.
def public add_module_rule_overrides(prog : ProgramPtr; var disabled_codes : table<string>) : void {
apply_module_codes(prog, "_nolint", disabled_codes, true)
apply_module_codes(prog, "_lint", disabled_codes, false)
}

def private apply_module_codes(prog : ProgramPtr; option : string;
var disabled_codes : table<string>; disable : bool) : void {
let configured = prog._options |> find_arg(option)
return if (!(configured is tString))
for (part in split(configured as tString, ",")) {
let code = part |> strip |> to_upper
continue if (empty(code))
if (disable) {
if (!key_exists(disabled_codes, code)) {
disabled_codes |> insert(code)
}
} elif (key_exists(disabled_codes, code)) {
disabled_codes |> erase(code)
}
}
}

def private module_source_path(prog : ProgramPtr) : string {
let mod = prog.getThisModule
if (mod == null) return ""
Expand All @@ -243,6 +271,10 @@ def public is_daslib_source(prog : ProgramPtr) : bool {

//! True when the compiling module's source lives under ``daslib/`` or ``modules/`` -
//! shipped library code whose emitted strings flow into every consumer's logs (STYLE039).
def public lint029_source_exempt(prog : ProgramPtr) : bool {
return path_has_segment(module_source_path(prog), "daslib")
}

def public is_shipped_library_source(prog : ProgramPtr) : bool {
let file = module_source_path(prog)
return path_has_segment(file, "daslib") || path_has_segment(file, "modules")
Expand Down
4 changes: 2 additions & 2 deletions doc/reflections/das2rst.das
Original file line number Diff line number Diff line change
Expand Up @@ -1364,9 +1364,9 @@ def document_module_lint(_root : string) {
def document_module_lint_config(_root : string) {
var mod = find_module("lint_config")
var groups <- array<DocGroup>(
group_by_regex("Configuration", mod, %regex~(load_lint_config|load_lint_config_from_path|load_env_disabled|seed_default_disabled|build_lint_macro_disabled|lint_config_forces_on|rule_docs_only_at)$%%),
group_by_regex("Configuration", mod, %regex~(load_lint_config|load_lint_config_from_path|load_env_disabled|seed_default_disabled|build_lint_macro_disabled|add_module_rule_overrides|lint_config_forces_on|rule_docs_only_at)$%%),
group_by_regex("Path excludes", mod, %regex~(load_path_excludes_from_path|matches_path_excludes|is_lint_path_excluded)$%%),
group_by_regex("Path-based rule defaults", mod, %regex~(is_daslib_source|is_shipped_library_source|is_core_library_source)$%%),
group_by_regex("Path-based rule defaults", mod, %regex~(is_daslib_source|is_shipped_library_source|is_core_library_source|lint029_source_exempt)$%%),
group_by_regex("Lint-surface predicates", mod, %regex~(is_user_authored_body|is_lint_fixture_name|lint_file_skip_reason)$%%),
group_by_regex("Structured findings", mod, %regex~(make_lint_issue)$%%),
group_by_regex("Format policy", mod, %regex~(format_policy_for|partition_by_format_policy)$%%)
Expand Down
Loading
Loading