Skip to content

Commit e886888

Browse files
Churkin Alekseyclaude
authored andcommitted
lint: fix LINT002 false positive on a devirtualized block holder
Invoke devirtualization splices the block literal in place and consumes the holder local's only read. The patch slot runs before lint, and optimize - which reaps the now-dead `let` - runs after it, so lint saw a variable with no access flag left and reported it unused: def apply_via_call(x : int) : int let add_two = $(v : int) : int { return v + 2 } return apply_block(add_two, x) // LINT002: unused variable add_two At lint time add_two carries only access_init, while a function-pointer local in the same position carries access_get|access_init|access_pass. That asymmetry makes the gap look specific to block-typed variables, but it is not - @@fn simply never devirtualizes. `options disable_auto_inline` compiles the same file clean, which is what pins this on the splice rather than on passing a variable as an argument. The devirtualizer now marks the holder marked_used, the same "a transform ate the reads" contract the unused-argument checks in ast_lint.cpp already honor, and daslib/lint.das honors that flag for locals as it already did for arguments. Optimize still reaps the dead `let`, so there is no codegen cost. Found in dagor, where the reported variables could not be renamed away without breaking the build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0247958 commit e886888

3 files changed

Lines changed: 43 additions & 1 deletion

File tree

daslib/lint.das

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@ class LintVisitor : AstVisitor {
303303
}
304304

305305
def validate_var(v : VariablePtr; can_make_const : bool) {
306-
if (v.flags.generated) return
306+
// marked_used: a transform consumed the reads (devirtualized block holder), or author intent
307+
if (v.flags.generated || v.flags.marked_used) return
307308
let name = string(v.name)
308309
// skip system vars (`__name`) and compiler-generated names (backtick-mangled, e.g. tuple destructuring)
309310
if (name |> starts_with("__") || find(name, "`") >= 0) return

src/ast/ast_inline.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1976,6 +1976,13 @@ namespace das {
19761976
continue;
19771977
}
19781978
literal = bit->second.literal;
1979+
// the splice consumes this read, and it is usually the holder's
1980+
// ONLY one - lint runs after the patch slot but before optimize
1981+
// reaps the now-dead `let`, so it would see a variable the user
1982+
// demonstrably uses with no access flag left and report LINT002.
1983+
// marked_used says "a transform ate the reads", same contract the
1984+
// unused-argument checks already honor
1985+
v->variable->marked_used = true;
19791986
}
19801987
}
19811988
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
options gen2
2+
3+
require dastest/testing_boost
4+
require daslib/lint
5+
6+
// LINT002 false positive: invoke devirtualization splices the block literal in place and eats
7+
// the holder's only read. Lint runs after the patch slot but before optimize reaps the dead
8+
// `let`, so it saw a variable with no access flag and reported error[50503]. The devirtualizer
9+
// now marks such holders marked_used. A function-pointer local never devirtualizes, so it was
10+
// always clean - that asymmetry is what made this look block-typed-specific.
11+
12+
def apply_via_invoke(x : int) : int {
13+
let add_one = $(v : int) : int { return v + 1 }
14+
return invoke(add_one, invoke(add_one, x))
15+
}
16+
17+
def apply_block(blk : block<(v : int) : int>; x : int) : int {
18+
return invoke(blk, x)
19+
}
20+
21+
def apply_via_call(x : int) : int {
22+
let add_two = $(v : int) : int { return v + 2 }
23+
return apply_block(add_two, x)
24+
}
25+
26+
[test]
27+
def test_block_var_read_by_invoke(t : T?) {
28+
t |> equal(apply_via_invoke(1), 3)
29+
}
30+
31+
[test]
32+
def test_block_var_passed_to_function(t : T?) {
33+
t |> equal(apply_via_call(1), 3)
34+
}

0 commit comments

Comments
 (0)