Skip to content

Commit d41cf11

Browse files
authored
Merge pull request #3576 from GaijinEntertainment/bbatkin/lints-and-64bit
64-bit hygiene: string length into the base module, wider memcpy/memcmp, and three rules that catch the truncation
2 parents 26bf730 + 32b3efa commit d41cf11

26 files changed

Lines changed: 445 additions & 26 deletions

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ Read `skills/writing_skills.md` first — it carries the full checklist. The thr
187187

188188
- No implicit type promotion: `int + float` is a compile error — both sides must match
189189
- No `bool(int)` cast — use `x != 0`; no `string(bool)` — use `"{flag}"`
190+
- **`length` and `empty` on a string need no `require`** — both live in the base module, so `length(s)` compiles in a bare file (the rest of the string surface still needs `require strings`). 64-bit forms: `long_length(s)`. The `int`-returning `length` panics past 2^31 rather than wrapping — same always-on guard array/table length already carry
190191
- `int("123")` does NOT work — use `to_int` from `require strings`. **`to_int` silently returns `0` on garbage** (`to_int("foo")``0`, `to_int("12abc")``12`). When you need to validate user/external input — including any string that flows into a shell command, file path, or system call — use `try_to_int` / `try_to_float` from `daslib/strings_convert` instead. Those return `Result<T; ConversionError>` distinguishing `invalid_argument` / `out_of_range` / `trailing_garbage`, so `";rm -rf;"` rejects cleanly instead of becoming `0`. Same for `to_float``try_to_float`
191192
- Hex literals are `uint` by default — use `int(0x3F)` for int
192193
- **16/8-bit type lattice**: `float16` scalar (`half` = builtin alias; literal suffix `1.5h`) + packed vectors `half2/3/4/8`, `short2/3/4/8`, `ushort2/3/4/8`, `byte2/3/4/8/16` (**byte = SIGNED int8**), `ubyte2/3/4/8/16`. Tightly packed (half3 = 6B), element-aligned, pass by value. fp16 family has **closed arithmetic** (per-op promote-to-float-round-back — bit-identical to native fp16); int families are **storage + converts only** (`byte4 + byte4` is a compile error; widen via `int4(b4)`, narrow via ctor = C-truncate or `short4_sat(i4)`-style saturating forms). `.s`-hex swizzles on every vector (`v.s3210`, `b16.sf`), `.lo`/`.hi` on 8/16-lane forms; `xyzw` stays ≤4 lanes. Conformance harness: `tests/type_lattice/` (GENERATED — regen via `daslang utils/dasgen/gen_type_conformance.das`). Lattice vectors have NO ExprConst nodes by design (`isFoldable` false; `Program::makeConst` returns null — callers must handle)
@@ -354,6 +355,9 @@ A generic that should accept `array<T>`, `array<array<T>>`, … (any nesting)
354355
| hand-rolled `is X` / `as X` / null-guard / `ExprRef2Value`-peel ladders in macro code | `qmatch(e, $e(a) + $e(b))` for source-syntax shapes; `match (e) { if (ExprField(name = "key", value = ExprVar(...))) { ... } }` for node-class shapes | both matchers peel `ExprRef2Value` automatically; `\|\|` alternation, `&&` guards, and `match_expr(local)` cover most ladders. Limits + the qmatch↔match division of labor: `skills/das_macros.md` "`match` (daslib/match)" |
355356
| `unsafe(reinterpret<T?>(unsafe(addr(x))))` (reinterpret-of-addr, pointer target) | `unsafe(addr<T?>(x))` | STYLE034: same AST, one unsafe gate instead of two. Non-pointer puns (`reinterpret<uint64>(addr(x))`) have no `addr<T?>` spelling and stay silent; the sugar's own desugared output is exempt via `castFlags.fromAddrSugar` |
356357
| `b == T('(')` where `b : T` and T is a non-`int` built-in numeric scalar (also `!=`, ranges, and Yoda forms) | make `b : int`, then write `b == '('` | STYLE035: character literals are `int`; changing the plain variable once removes every repeated numeric cast. The warning is deduplicated at the declaration. |
358+
| `int64(length(x))` / `uint64(capacity(x))` (also `count`, `find_index`, `fread`, `fwrite`) | `long_length(x)` / `long_capacity(x)` / … | LINT017: the inner call returns `int`, so the 2^31 limit is hit *before* the widening cast — as a wrap, or as the panic guard on array/table/string length — and the cast buys nothing. Pair table is hardcoded (an exists-check is meaningless for user functions, since a macro can add/remove them) and gated on receiver type, which keeps same-named user overloads and the fixed-array `length` generic (no `long_` twin) silent |
359+
| `memcpy(d, s, int(n))` / `memcmp(d, s, int(n))` where `n` is `int64`/`uint64`/`uint` | `memcpy(d, s, n)` | LINT018: `memcpy`/`memcmp` now carry `uint`/`int64`/`uint64` size overloads, so the `int(...)` narrowing is pure loss — above 2^31 it copies the wrong byte count |
360+
| `cast<T -const>(x)` / `reinterpret<T -#>(x)` / `addr<void? -const>(x)` — any of `-const` `-&` `-[]` `-#` `==const` `==&` on a **concrete** cast target | drop the contract | STYLE036: these are *substitution* contracts — they act only while a generic binds, and infer clears them once consumed, so a flag still set at lint time proves it did nothing. `auto`/unresolved-alias targets are excluded (there substitution hasn't run and the contract is real); a concrete typedef is NOT excluded (`reinterpret<CI? -const>` with `typedef CI = int const` keeps the const) |
357361

358362
For path/filename ops use `fio` helpers (`base_name`/`dir_name`/`path_join`/etc.) — see `skills/filesystem.md`. Never hand-roll `rfind("/")` / slice — misses Windows separators.
359363

daslib/array_boost.das

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def array_view(
248248
var res : array<TT -const>#
249249
if (length > 0) {
250250
let data = addr<TT -const?>(bytes[byte_offset])
251-
if (intptr(reinterpret<void? -const>(data)) % uint64(element_align) != 0_ul) {
251+
if (intptr(reinterpret<void?>(data)) % uint64(element_align) != 0_ul) {
252252
panic("array_view: unaligned typed view")
253253
}
254254
_builtin_make_temp_array(res, data, length)
@@ -262,7 +262,7 @@ def array_view(
262262
var res : array<TT -const>#
263263
if (length > 0_l) {
264264
let data = addr<TT -const?>(bytes[byte_offset])
265-
if (intptr(reinterpret<void? -const>(data)) % uint64(element_align) != 0_ul) {
265+
if (intptr(reinterpret<void?>(data)) % uint64(element_align) != 0_ul) {
266266
panic("array_view: unaligned typed view")
267267
}
268268
_builtin_make_temp_array_i64(res, data, length)

daslib/async_boost.das

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ module async_boost shared private
99
//! Provides ``[async]`` function annotation and ``await`` call macro that
1010
//! rewrite annotated functions into generator-based coroutines.
1111

12-
require strings
1312
require daslib/ast_boost
1413
require daslib/templates_boost
1514
require daslib/macro_boost

daslib/debug.das

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1303,7 +1303,7 @@ class private DAgent : DapiDebugAgent {
13031303
return
13041304
}
13051305
}
1306-
frame.state |> emplace <| (uid = STATE_VARS + uint64(length(frame.state)), name = category, vars = [ variable])
1306+
frame.state |> emplace <| (uid = STATE_VARS + uint64(long_length(frame.state)), name = category, vars = [ variable])
13071307
}
13081308
})
13091309
}

daslib/debug_eval.das

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ def func_call_length(var st : TokenStream; result : Result) : Result {
205205
}
206206
if (TypeInfo(basicType = Type.tString)) {
207207
let pstr = unsafe(reinterpret<string?>(getPD(st, result)))
208-
return <- Result(int64(length(*pstr)))
208+
return <- Result(long_length(*pstr))
209209
}
210210
if (TypeInfo(basicType = Type.tArray)) {
211211
let parr = unsafe(reinterpret<DapiArray?>(getPD(st, result)))

daslib/delegate.das

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ module delegate shared private
2222

2323
require daslib/macro_boost
2424
require daslib/typemacro_boost
25-
require strings
2625

2726
// ── helpers ────────────────────────────────────────────────────────────
2827

daslib/flatten_opt_common.das

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ def public subst_consts(var e : ExpressionPtr; constmap : table<string; Expressi
612612
var tmpl : Template
613613
for (nm in keys(constmap)) {
614614
// ?[] yields a const view; const-strip the raw pointer (apply_template clones per use anyway).
615-
var ex = unsafe(reinterpret<Expression? -const>(constmap?[nm] ?? null))
615+
var ex = unsafe(reinterpret<Expression?>(constmap?[nm] ?? null))
616616
tmpl |> replaceVariable(nm, ex)
617617
}
618618
var res = apply_template(tmpl, e.at, e, false)
@@ -718,7 +718,7 @@ def public store_base(e : Expression const?) : ExpressionPtr {
718718
if (e is ExprSwizzle) return store_base((e as ExprSwizzle).value)
719719
if (e is ExprPtr2Ref) return store_base((e as ExprPtr2Ref).subexpr)
720720
if (e is ExprRef2Value) return store_base((e as ExprRef2Value).subexpr)
721-
return e is ExprVar ? unsafe(reinterpret<Expression? -const>(e)) : null
721+
return e is ExprVar ? unsafe(reinterpret<Expression?>(e)) : null
722722
}
723723

724724
// Names reassigned somewhere in the block (live/loop masks `__flat_*`, any reassigned user `var`).

daslib/flatten_opt_preshade.das

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ def private regroup_to_share(var blk : ExprBlock?; no_fast_math : bool) : bool {
606606
def private alias_source(e : Expression const?) : ExpressionPtr {
607607
if (e == null) return null
608608
if (e is ExprRef2Value) return alias_source((e as ExprRef2Value).subexpr)
609-
return e is ExprVar ? unsafe(reinterpret<Expression? -const>(e)) : null
609+
return e is ExprVar ? unsafe(reinterpret<Expression?>(e)) : null
610610
}
611611

612612
def private eliminate_aliases(var blk : ExprBlock?) : bool {

daslib/lint.das

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,58 @@ 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+
// fio's `file` is a pointer-to-handle; the type gate keeps a user-defined 2-arg fread/fwrite from being told to call a long_ twin that does not exist.
403+
if ((fname == "fread" || fname == "fwrite") && bt == Type.tPointer && nargs == 2) return "long_{fname}"
404+
return ""
405+
}
406+
407+
def check_lint017_long_cast(expr : ExprCall?) : void {
408+
if (noLint || genericDepth > 0
409+
|| (expr.name != "int64" && expr.name != "uint64")
410+
|| length(expr.arguments) != 1) return
411+
let inner = expr.arguments[0]
412+
if (!(inner is ExprCall)) return
413+
let icall = inner as ExprCall
414+
let iname = string(icall.func != null && icall.func.fromGeneric != null
415+
? icall.func.fromGeneric.name
416+
: icall.name)
417+
let twin = lint017_long_twin(iname, icall)
418+
if (empty(twin)) return
419+
lint_error("LINT017: {expr.name}({iname}(...)) widens an already-32-bit result, so the 2^31 limit is still hit inside {iname} (as a wrap, or as the panic guard on array/table/string length) before the cast runs; call {twin}(...) instead", expr.at)
420+
}
421+
422+
// --- LINT018: narrowing the size argument of memcpy / memcmp ---
423+
424+
def check_lint018_mem_size_narrowing(expr : ExprCall?) : void {
425+
if (noLint || genericDepth > 0
426+
|| (expr.name != "memcpy" && expr.name != "memcmp")
427+
|| length(expr.arguments) != 3) return
428+
let size = expr.arguments[2]
429+
if (!(size is ExprCall)) return
430+
let scall = size as ExprCall
431+
if (scall.name != "int" || length(scall.arguments) != 1) return
432+
let src = scall.arguments[0]._type
433+
if (src == null) return
434+
let bt = src.baseType
435+
if (bt != Type.tInt64 && bt != Type.tUInt64 && bt != Type.tUInt) return
436+
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)
437+
}
438+
391439
def override preVisitExprCall(expr : ExprCall?) : void {
392440
check_string_push_clone(expr)
441+
check_lint017_long_cast(expr)
442+
check_lint018_mem_size_narrowing(expr)
393443
}
394444

395445
// `assume name = <block>` keeps a template block at the alias-definition site; every use of

daslib/style_lint.das

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2367,6 +2367,26 @@ class StyleLintVisitor : AstVisitor {
23672367
&& !expr.subexpr.genFlags.generated) {
23682368
style_warning("STYLE034: reinterpret<{describe(expr.castType)}>(addr(...)) collapses to addr<{describe(expr.castType)}>(...); one unsafe() covers both halves", expr.at)
23692369
}
2370+
// ===== STYLE036 — type contract that cannot act on a resolved cast type =====
2371+
if (!expr.genFlags.generated && expr.castType != null) {
2372+
let inert = style036_inert_contract(expr.castType)
2373+
if (!empty(inert)) {
2374+
let tail = inert == "-const" ? "; drop it, along with any 'const' it was meant to cancel" : "; drop it"
2375+
style_warning("STYLE036: '{inert}' does nothing on the already-resolved cast type {describe(expr.castType)}{tail}", expr.at)
2376+
}
2377+
}
2378+
}
2379+
2380+
def style036_inert_contract(t : TypeDecl?) : string {
2381+
// Substitution contracts: infer clears them when a generic binds (ast_typedecl.cpp:223,422), so one still set on a concrete target was never consumed — but an auto/unresolved-alias target has simply not been substituted yet (linq's `reinterpret<ARGT -const>` does strip const).
2382+
if (t.isAutoOrAlias) return ""
2383+
if (t.flags.removeConstant) return "-const"
2384+
if (t.flags.removeRef) return "-&"
2385+
if (t.flags.removeDim) return "-[]"
2386+
if (t.flags.removeTemporary) return "-#"
2387+
if (t.flags.explicitConst) return "==const"
2388+
if (t.flags.explicitRef) return "==&"
2389+
return ""
23702390
}
23712391

23722392
def override preVisitExprAt(expr : ExprAt?) : void {

0 commit comments

Comments
 (0)