FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

lint: LINT017 / LINT018 -- catch 32-bit truncation at the call site · feiyunwill/daScript@1144bf7 · GitHub

Commit 1144bf7

Browse files
andcommitted
lint: LINT017 / LINT018 -- catch 32-bit truncation at the call site
LINT017 flags int64(length(x)) / uint64(capacity(x)) and friends: the inner call returns int, so the wrap past 2^31 already happened before the widening cast ran. The cast looks like it buys 64-bit range and buys nothing. LINT018 flags int(...) on a memcpy/memcmp size argument, now that both carry uint/int64/uint64 overloads and the narrowing is pure loss. Both are LINT rather than PERF -- this is code not handling 64 bits correctly, not code that is merely slow. The LINT017 pair table is hardcoded on purpose. An exists-check is not meaningful for user functions (a macro can add or remove them mid-compile), so the only well-defined domain is the builtin/daslib set, which is enumerable. Each pair is additionally gated on the receiver type, which is what keeps a same-named user overload silent -- and the fixed-array length() generic in daslib/builtin.das, which genuinely has no long_ twin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 90f1db9 commit 1144bf7

3 files changed

Lines changed: 142 additions & 0 deletions

File tree

‎daslib/lint.das‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,57 @@ class LintVisitor : AstVisitor {
388388
validate_argument(arg, "LINT013", "block argument")
389389
}
390390

391+
// --- LINT017: int64/uint64 cast of a call that has a long_ counterpart ---
392+
393+
def lint017_long_twin(fname : string; icall : ExprCall?) : string {
394+
// Receiver-gated, so a same-named user overload or the fixed-array `length` generic in daslib/builtin.das (which has no long_ twin) stays silent.
395+
let nargs = length(icall.arguments)
396+
if (nargs == 0 || icall.arguments[0]._type == null) return ""
397+
let bt = icall.arguments[0]._type.baseType
398+
if (fname == "length" && (bt == Type.tArray || bt == Type.tTable || bt == Type.tString)) return "long_length"
399+
if (fname == "capacity" && (bt == Type.tArray || bt == Type.tTable)) return "long_capacity"
400+
if (fname == "count" && (bt == Type.tArray || bt == Type.tIterator)) return "long_count"
401+
if (fname == "find_index" && bt == Type.tArray && nargs == 2) return "long_find_index"
402+
if ((fname == "fread" || fname == "fwrite") && nargs == 2) return "long_{fname}"
403+
return ""
404+
}
405+
406+
def check_lint017_long_cast(expr : ExprCall?) : void {
407+
if (noLint || genericDepth > 0
408+
|| (expr.name != "int64" && expr.name != "uint64")
409+
|| length(expr.arguments) != 1) return
410+
let inner = expr.arguments[0]
411+
if (!(inner is ExprCall)) return
412+
let icall = inner as ExprCall
413+
let iname = string(icall.func != null && icall.func.fromGeneric != null
414+
? icall.func.fromGeneric.name
415+
: icall.name)
416+
let twin = lint017_long_twin(iname, icall)
417+
if (empty(twin)) return
418+
lint_error("LINT017: {expr.name}({iname}(...)) computes a 32-bit result and widens it, so it still wraps past 2^31; call {twin}(...) instead", expr.at)
419+
}
420+
421+
// --- LINT018: narrowing the size argument of memcpy / memcmp ---
422+
423+
def check_lint018_mem_size_narrowing(expr : ExprCall?) : void {
424+
if (noLint || genericDepth > 0
425+
|| (expr.name != "memcpy" && expr.name != "memcmp")
426+
|| length(expr.arguments) != 3) return
427+
let size = expr.arguments[2]
428+
if (!(size is ExprCall)) return
429+
let scall = size as ExprCall
430+
if (scall.name != "int" || length(scall.arguments) != 1) return
431+
let src = scall.arguments[0]._type
432+
if (src == null) return
433+
let bt = src.baseType
434+
if (bt != Type.tInt64 && bt != Type.tUInt64 && bt != Type.tUInt) return
435+
lint_error("LINT018: int(...) truncates the {expr.name} size above 2^31; {expr.name} has uint/int64/uint64 overloads, so drop the cast", expr.at)
436+
}
437+
391438
def override preVisitExprCall(expr : ExprCall?) : void {
392439
check_string_push_clone(expr)
440+
check_lint017_long_cast(expr)
441+
check_lint018_mem_size_narrowing(expr)
393442
}
394443

395444
// `assume name = <block>` keeps a template block at the alias-definition site; every use of
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
options gen2
2+
options auto_inline_functions = false // lint fixtures assert SOURCE shapes; splices rewrite them
3+
// LINT017: int64/uint64 cast of a call that has a long_ counterpart
4+
//
5+
// Problem: int64(length(x)) computes the 32-bit length FIRST and only then widens
6+
// it, so it still wraps past 2^31 — the cast buys nothing. long_length(x) is
7+
// 64-bit the whole way through.
8+
//
9+
// Bad pattern:
10+
// let n = int64(length(arr))
11+
//
12+
// Good pattern:
13+
// let n = long_length(arr)
14+
15+
require daslib/linq
16+
17+
// bad: int64 of array length (1 warning)
18+
def bad_int64_length(arr : array<int>) : int64 {
19+
return int64(length(arr))
20+
}
21+
22+
// bad: uint64 of array capacity (1 warning)
23+
def bad_uint64_capacity(arr : array<int>) : uint64 {
24+
return uint64(capacity(arr))
25+
}
26+
27+
// bad: int64 of string length (1 warning)
28+
def bad_int64_string_length(s : string) : int64 {
29+
return int64(length(s))
30+
}
31+
32+
// bad: int64 of table length (1 warning)
33+
def bad_int64_table_length(t : table<string; int>) : int64 {
34+
return int64(length(t))
35+
}
36+
37+
// bad: int64 of linq count (1 warning)
38+
def bad_int64_count(arr : array<int>) : int64 {
39+
return int64(count(arr))
40+
}
41+
42+
// good: 64-bit all the way through
43+
def good_long_length(arr : array<int>) : int64 {
44+
return long_length(arr)
45+
}
46+
47+
// good: a fixed array has no long_length twin, so the cast is the only spelling
48+
def good_fixed_array(fa : int[4]) : int64 {
49+
return int64(length(fa))
50+
}
51+
52+
// good: widening an ordinary int is not a truncation
53+
def good_plain_widen(n : int) : int64 {
54+
return int64(n)
55+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
options gen2
2+
options auto_inline_functions = false // lint fixtures assert SOURCE shapes; splices rewrite them
3+
// LINT018: narrowing the size argument of memcpy / memcmp
4+
//
5+
// Problem: int(n) truncates above 2^31, so the copy or compare silently covers
6+
// the wrong number of bytes. memcpy and memcmp both carry uint / int64 / uint64
7+
// size overloads, so the size can be passed straight through.
8+
//
9+
// Bad pattern:
10+
// memcpy(dst, src, int(nbytes)) // nbytes : int64
11+
//
12+
// Good pattern:
13+
// memcpy(dst, src, nbytes)
14+
15+
// bad: int() narrowing of an int64 size (1 warning)
16+
def bad_memcpy_int64(dst, src : void?; nbytes : int64) {
17+
unsafe(memcpy(dst, src, int(nbytes)))
18+
}
19+
20+
// bad: int() narrowing of a uint64 size (1 warning)
21+
def bad_memcpy_uint64(dst, src : void?; nbytes : uint64) {
22+
unsafe(memcpy(dst, src, int(nbytes)))
23+
}
24+
25+
// bad: int() narrowing on memcmp (1 warning)
26+
def bad_memcmp_int64(dst, src : void?; nbytes : int64) : int {
27+
return unsafe(memcmp(dst, src, int(nbytes)))
28+
}
29+
30+
// good: the 64-bit size goes straight to the 64-bit overload
31+
def good_memcpy_int64(dst, src : void?; nbytes : int64) {
32+
unsafe(memcpy(dst, src, nbytes))
33+
}
34+
35+
// good: an int-typed size needs no cast at all
36+
def good_memcpy_int(dst, src : void?; nbytes : int) {
37+
unsafe(memcpy(dst, src, nbytes))
38+
}

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL