diff --git a/CLAUDE.md b/CLAUDE.md index ad985b387..a2f7f373b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -285,6 +285,7 @@ Full migration table (when reading older docs that say `var inscope` or `<-` for | `unsafe { ...; unsafe { ... }; ... }` (nested `unsafe { }` block) | drop the inner wrap | STYLE026: outer `unsafe` already covers the whole inner scope, so the inner block is pure noise. Closure / lambda / generator bodies are NOT nested for this rule — they execute in a separate context where the outer wrap does not propagate | | `for (s in A) { B \|> push(s) }` / `push_clone(s)` (iter-var only) | `B \|> push_from(A)` / `push_clone_from(A)` | PERF022: the bulk overload in builtin.das reserves combined capacity up front. Single name `push`/`push_clone` is overloaded between single-element and bulk (ambiguous when destination is `array`); the `_from` suffix names the bulk intent. Source must be `array` or C-array — range/iterator sources are not flagged. `emplace` is out of scope (const iter-var can't be moved) | | `var a : array; for (x in SRC) { if (COND) { a \|> push(EXPR) } }` (or `table` + `insert`/`a[k]=v`) | `var a <- [for (x in SRC); EXPR; where COND]` (or `\{for (...); k => v; where ...\}`) | STYLE027: var with empty default-init followed by a for-loop that only push/insert into it. Accepts depth ≤ 2 nested fors and if-filters at any depth. `emplace` excluded — move-source-zeroing differs from comprehension element-construction. Iterator-comprehension form (`[$f ...]`) NOT suggested | +| `var t : table; t \|> insert(k1, v1); t \|> insert(k2, v2)` (or `t[k] = v` runs, or 2-arg set inserts) | `var t <- { k1 => v1, k2 => v2 }` (set: `var s <- { k1, k2 }`) | STYLE031: ≥ 2 contiguous inserts/`[]=` after an empty table decl collapse to a literal move-assign. Computed keys fine; runs with a duplicate CONST key stay silent (literal duplicates are `error[30706]`, inserts overwrite). `table` const-key runs get STYLE021's `JV((k1=...))` form instead | | `var X = clone_expression(E); ... $e(X) ...` (only-uses-are-qmacro-splice) | drop the pre-clone, inline `$e(E)` at each splice site | PERF023: `qmacro`/`qmacro_block`/`qmacro_expr`/`qmacro_block_to_array` go through `apply_template` (templates_boost.das:251), which calls `clone_expression` on every substitution input. Pre-cloning is wasted work. Detection: post-expansion `$e(X)` becomes `add_ptr_ref(X)` inside an `ExprMakeBlock`; visitor tracks splice-wrapper depth via preVisitExprCall/visitExprCall counter on `add_ptr_ref`, classifies each candidate `ExprVar` reference as "safe" when depth>0. Fires only when ALL uses are safe AND ≥1 is observed. Multi-clone-of-same-source flagged too — apply_template clones each substitution independently | | 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)" | diff --git a/daslib/style_lint.das b/daslib/style_lint.das index 460e58dc5..920915794 100644 --- a/daslib/style_lint.das +++ b/daslib/style_lint.das @@ -29,6 +29,7 @@ module style_lint shared private //! STYLE020 — from_JV(v, type, defV) on a scalar — use 'v ?? defV' (operator ?? from json_boost) //! STYLE021 — table built by repeated insert — use 'var v = JV((k1=..., k2=...))' //! STYLE022 — bitfield compound-bit-set/clear ('foo |= BfT.m' / 'foo &= ~BfT.m') — use field-assign 'foo.m = true' / 'foo.m = false' +//! STYLE023 — bitfield test via int-cast ('int(bf & BfT.m) != 0' / '== 0') — use 'bf.m' / '!bf.m' directly //! STYLE024 — redundant 'unsafe' wrap; inner expression / block body has no operation requiring unsafe — drop the wrap //! STYLE025 — 'unsafe { ... }' block where only one statement needs unsafe — narrow to 'unsafe()' //! STYLE026 — nested 'unsafe { ... }' block; outer wrap already covers the scope — drop the inner @@ -36,6 +37,7 @@ module style_lint shared private //! STYLE028 — 'self->method(...)' inside a class method is optional — drop 'self->' and call 'method(...)' directly (compiler auto-promotes to the same invoke) //! STYLE029 — non-public 'require X' used only for ONE module X re-exports — require it directly and drop X (skipped when ≥2 re-exports are used: aggregation facade; or when X provides macros or [init]) //! STYLE030 — non-public 'require X' that is entirely unused — no symbol from X (or anything it re-exports) is referenced; drop it (skipped when X provides any macro or an [init], or only re-exports builtins used through it, or X or its public re-export closure exports a function matching an unresolved call inside an uninstanced generic body). Suppress a deliberate keep with '// nolint:STYLE030' +//! STYLE031 — table (or table set) var initialized via a run of insert / '[]=' statements; use a table (set) literal move-assign require daslib/ast_boost require daslib/is_local @@ -872,6 +874,7 @@ class StyleLintVisitor : AstVisitor { check_style013_struct_field_init(blk, expr as ExprLet) check_style021_jv_table_init(blk, expr as ExprLet) check_style027_for_loop_comprehension(blk, expr as ExprLet) + check_style031_table_init(blk, expr as ExprLet) } } @@ -1259,10 +1262,14 @@ class StyleLintVisitor : AstVisitor { // --- STYLE021: table built via repeated insert → JV(named-tuple) --- def is_jv_pointer_type(t : TypeDecl?) : bool { + // JsonValue is a das struct (daslib/json.das), not a handled type — match + // tPointer → tStructure by name + defining module. if (t == null || t.baseType != Type.tPointer || t.firstType == null - || t.firstType.baseType != Type.tHandle - || t.firstType.annotation == null) return false - return t.firstType.annotation.name == "JsonValue" + || t.firstType.baseType != Type.tStructure + || t.firstType.structType == null) return false + return (t.firstType.structType.name == "JsonValue" + && t.firstType.structType._module != null + && t.firstType.structType._module.name == "json") } def is_jv_table_type(t : TypeDecl?) : bool { @@ -1445,6 +1452,121 @@ class StyleLintVisitor : AstVisitor { return (arg as ExprVar).variable == target } + // --- STYLE031: table / table var then contiguous insert/[]= instead of literal --- + + def is_table_insert_of(call : ExprCall?; target : Variable?; nargs : int) : bool { + // Map flavor lowers to insert(t, k, v) (3 args), set flavor to insert(t, k) (2 args). + if (call.func == null || call.func.fromGeneric == null + || call.func.fromGeneric.name != "insert" + || length(call.arguments) != nargs) return false + var arg = call.arguments[0] + if (arg is ExprRef2Value) { + arg = (arg as ExprRef2Value.subexpr) + } + if (!(arg is ExprVar)) return false + return (arg as ExprVar).variable == target + } + + def at_assign_key(stmt : Expression?) : Expression? { + // Precondition: is_at_assign_of(stmt, target) held — LHS is ExprAt. + var lhs : Expression? = null + if (stmt is ExprCopy) { + lhs = (stmt as ExprCopy).left + } elif (stmt is ExprClone) { + lhs = (stmt as ExprClone).left + } elif (stmt is ExprMove) { + lhs = (stmt as ExprMove).left + } + return null if (lhs == null) + return (lhs as ExprAt).index + } + + def const_key_repr(key0 : Expression?) : string { + //! Type-tagged stringification of the key when it is a constant literal; + //! "" for computed keys (not comparable at lint time). + var key = key0 + if (key != null && key is ExprRef2Value) { + key = (key as ExprRef2Value).subexpr + } + if (key == null) return "" + if (key is ExprConstString) return "s:{(key as ExprConstString).value}" + if (key is ExprConstInt) return "i:{(key as ExprConstInt).value}" + if (key is ExprConstUInt) return "u:{(key as ExprConstUInt).value}" + if (key is ExprConstInt64) return "i64:{(key as ExprConstInt64).value}" + if (key is ExprConstUInt64) return "u64:{(key as ExprConstUInt64).value}" + if (key is ExprConstFloat) return "f:{(key as ExprConstFloat).value}" + if (key is ExprConstDouble) return "d:{(key as ExprConstDouble).value}" + if (key is ExprConstBool) return "b:{(key as ExprConstBool).value}" + if (key is ExprConstEnumeration) return "e:{(key as ExprConstEnumeration).value}" + return "" + } + + def check_style031_table_init(blk : ExprBlock?; elet : ExprLet?) : void { + if (current_function != null && current_function.fromGeneric != null) return + // Find this ExprLet's index in the enclosing block's statement list. + var idx = -1 + let n = length(blk.list) + for (k in range(n)) { + if (blk.list[k] == elet) { + idx = k + break + } + } + if (idx < 0 || idx + 1 >= n) return + for (v in elet.variables) { + // Only flag uninitialized table locals from user code (skip generic + // host instantiations and compiler-synthesized vars). + if (v.init != null + || v.flags.generated || v.flags.inScope + || v._type == null || v._type.baseType != Type.tTable) continue + let isSet = v._type.secondType == null || v._type.secondType.baseType == Type.tVoid + var count = 0 + var dup_const_key = false + { + var seen_keys : table + for (j in range(idx + 1, n)) { + let stmt = blk.list[j] + var key : Expression? = null + if (stmt is ExprCall && is_table_insert_of(stmt as ExprCall, v, isSet ? 2 : 3)) { + key = (stmt as ExprCall).arguments[1] + } elif (!isSet && is_at_assign_of(stmt, v)) { + key = at_assign_key(stmt) + } else { + break + } + let repr = const_key_repr(key) + if (repr != "") { + if (key_exists(seen_keys, repr)) { + dup_const_key = true + } + seen_keys |> insert(repr) + } + count++ + } + delete seen_keys + } + // Duplicate constant keys stay silent: sequential inserts overwrite + // (last wins), but a literal rejects them at compile time (error 30706). + if (count < 2 || dup_const_key) continue + // STYLE021 owns the table const-key run (the + // JV((k1=..., k2=...)) form is the stronger suggestion). + if (is_jv_table_type(v._type)) { + var jv = 0 + for (j in range(idx + 1, n)) { + let stmt = blk.list[j] + if (!(stmt is ExprCall) || !is_insert_of_const_key(stmt as ExprCall, v)) break + jv++ + } + continue if (jv >= 2) + } + if (isSet) { + style_warning("STYLE031: table set initialized by a run of insert calls; use a set literal move-assign `var {v.name} <- \{k1, k2, ...\}`", v.at) + } else { + style_warning("STYLE031: table initialized by a run of insert/`[]=` statements; use a table literal move-assign `var {v.name} <- \{k1 => v1, k2 => v2, ...\}`", v.at) + } + } + } + // --- STYLE014/STYLE015: large comment blocks (post-visit pass) --- // ===== STYLE029 — transitive-only require ===== diff --git a/doc/source/reference/language/lint.rst b/doc/source/reference/language/lint.rst index 6852ac797..81d50b7ae 100644 --- a/doc/source/reference/language/lint.rst +++ b/doc/source/reference/language/lint.rst @@ -1584,7 +1584,8 @@ Detection requires the variable's static type to be exactly ``table``, zero initial value, and a contiguous run of ≥ 2 ``insert`` calls whose key is an ``ExprConstString`` and whose receiver resolves to the same variable. Computed keys disqualify the -chain. +chain — such runs fall through to :ref:`STYLE031 ` instead +(a table literal accepts computed keys, ``JV((...))`` does not). .. code-block:: das @@ -1813,6 +1814,40 @@ it. Suppress a deliberate keep with ``// nolint:STYLE030``. // Good — remove the require +.. _style031: + +STYLE031 — table ``var`` initialized by a run of ``insert`` / ``[]=`` +====================================================================== + +Declaring an empty ``table`` (or a ``table`` set) and populating +it with ≥ 2 contiguous ``t |> insert(k, v)`` calls or ``t[k] = v`` +assignments is the table counterpart of STYLE012. A table (set) literal +move-assign expresses the same construction in one statement. + +Detection anchors on the uninitialized table declaration and accepts a +mixed run of exact-arity inserts (3-arg map form, 2-arg set form) and +``t[k] = v`` at-assigns (map only). Computed keys are fine — a +runtime-duplicate key in a literal is last-wins, identical to sequential +inserts. A run containing a duplicate **constant** key stays silent: +sequential inserts overwrite, but a literal rejects duplicate constant +keys at compile time (error 30706), so the rewrite would not compile. +``table`` runs with constant keys are owned by +STYLE021 (the ``JV((k1=..., k2=...))`` form is the stronger suggestion). + +.. code-block:: das + + // Bad + var t : table + t |> insert("a", 1) // STYLE031 + t["b"] = 2 + var s : table + s |> insert(5) // STYLE031 (set flavor) + s |> insert(7) + + // Good + var t <- { "a" => 1, "b" => 2 } + var s <- { 5, 7 } + ----- Tests ----- diff --git a/skills/style_lint.md b/skills/style_lint.md index 7d00cfdeb..ebe0c8b2b 100644 --- a/skills/style_lint.md +++ b/skills/style_lint.md @@ -33,7 +33,7 @@ The `style_lint` module detects non-idiomatic patterns in daslang code at compil | STYLE018 | `b == true` / `b == false` / `b != true` / `b != false` (and Yoda forms) | Use `b` / `!b` directly. Skipped when both sides are bool literals (e.g. `true == true`). | | STYLE019 | `min(max(x, lo), hi)` (and the `max(min(x, hi), lo)` mirror) | Use `clamp(x, lo, hi)` from math module. Inner/outer must resolve to math::min/max specifically, not user overloads. | | STYLE020 | `from_JV(v, type, defV)` resolving to a json_boost scalar overload | Use `v ?? defV`. Detection walks `expr.func.fromGeneric` to the root (two levels for json_boost's `[template(ent)]` generics) and matches root.name/root._module against `from_JV` / `json_boost`. Uses `expr._type` for the supported-scalar check (robust under pre- and post-instantiation arg shapes). Vector/table/struct/enum/bitfield overloads stay silent — no matching `??`. | -| STYLE021 | `var v : table` followed by ≥ 2 contiguous `v \|> insert(, ...)` | Use the named-tuple JV form: `var v = JV((k1=val1, k2=val2, ...))` (`daslib/json_boost.das:638`). Computed keys disqualify the whole chain. | +| STYLE021 | `var v : table` followed by ≥ 2 contiguous `v \|> insert(, ...)` | Use the named-tuple JV form: `var v = JV((k1=val1, k2=val2, ...))` (`daslib/json_boost.das:638`). Computed keys disqualify the chain — those runs fall through to STYLE031 (a table literal accepts computed keys, `JV((...))` does not). `JsonValue` is matched as the das **struct** from module `json` (`tPointer`→`tStructure`, name + module) — the original `tHandle`+annotation check could never match a das struct, so the rule was dead code from landing until 2026-06-12. | | STYLE022 | `foo \|= BfT.m` / `foo &= ~BfT.m` where `foo._type.baseType == tBitfield` and the RHS resolves to exactly one named bit | Use `foo.m = true` / `foo.m = false` (bitfield-as-field assignment). RHS is matched in two shapes: `ExprField(value=ExprVar(BfT), name="m")` under lint policies, and `ExprConstBitfield()` under normal compile policies (single-bit mask mapped back to bit name via `TypeDecl.argNames`). The `&=` form requires explicit `~`; bare `foo &= BfT.m` stays silent (different semantics). Multi-bit RHS (`Mode.read \| Mode.write`) and dynamic RHS skipped. **Note**: only safe when the AOT C++ side has `__bit_set` overloads matching the underlying integer type — `include/daScript/simulate/aot.h` provides `Bitfield&`, `Bitfield8/16/64&`, and raw `uint8/16/32/64_t&` overloads (the raw-integer set covers handle-bound bitfield fields like `Function::flags`, which is `uint32_t` on the C++ side). | | STYLE023 | `int_cast(bf & BfT.m) !=/== 0` where `bf._type.baseType == tBitfield`, the cast is one of `int`/`uint`/`int64`/`uint64`, and the RHS of `&` resolves to one named bit | Use `bf.m` (for `!= 0`) or `!bf.m` (for `== 0`). Matches both operand orders (`cast != 0` and `0 != cast`). Single-bit detection mirrors STYLE022 (both `ExprField` and `ExprConstBitfield` shapes); multi-bit masks and dynamic RHS skipped. Triggers on any of the four `ExprConst{Int,UInt,Int64,UInt64}` zero literals so signed/unsigned and 32/64-bit cast forms all fire. | | STYLE024 | Redundant `unsafe` wrap — `unsafe(expr)` (parser flag `userSaidItsSafe` on the inner expression) **or** `unsafe { ... }` block whose body contains no statement matching a known inherently-unsafe AST shape | Drop the wrap. The "inherently-unsafe" recognizer (`expr_needs_unsafe`) is a recursive walk that flags `ExprCast` with `upcastCast`/`reinterpretCast`, `ExprDelete`, `ExprAddr` (`@@`), `ExprRef2Ptr` (`addr(x)`), `ExprAsVariant`, `ExprSafeAsVariant` (`?as` — marked unconditionally like `as`, since `?as` on a non-pointer variant requires unsafe and operand pointer-ness is erased by `autoDereference` at lint time), `ExprAt` on a `table<>` value, `ExprField` whose value is variant-typed, and `ExprCallFunc` whose `func.flags.unsafeOperation` (or `moreFlags.unsafeOutsideOfFor` outside a for-loop source) is set. Subtrees with `genFlags.generated == true` are skipped entirely — macro-synthesized AST is excluded from the rule. Note: the parser-flag form (`unsafe(expr)`) only survives folding under `no_optimizations + no_infer_time_folding`, so const-foldable inner expressions (e.g. `unsafe(1 + 2)`) only fire under the lint runner (`utils/lint/main.das`). Block-form fires under regular compile too. | @@ -41,6 +41,9 @@ The `style_lint` module detects non-idiomatic patterns in daslang code at compil | STYLE026 | Nested `unsafe { ... }` — an `unsafe` block appears inside another open `unsafe` block, with no closure/lambda/generator boundary between them | Drop the inner wrap. Tracked by `unsafe_block_stack` — one slot per closure level, pushed on function entry and `blockFlags.isClosure` entry, popped on exit. `preVisitExprUnsafe` flags any entry where `stack[top] > 0`. Closure bodies push a fresh 0-slot because they execute in a separate context where the outer caller's `unsafe { }` does not propagate. | | STYLE027 | `var a : array` / `var a : table` with empty default-init, immediately followed by an `ExprFor` whose body — recursively, with at most one additional nested `ExprFor` (depth ≤ 2) — consists ONLY of `push`/`push_clone` (array) or `insert(k,v)` / `a[k] = v` (table) calls into `a`, optionally wrapped in `if (cond) { ... }` filters at any depth | Use a comprehension. Array: `a <- [for (x in SRC); EXPR; where COND]`. Table: `a <- {for (x in SRC); KEY => VAL; where COND}`. Drop the `where COND` when there is no filter. `emplace` is intentionally excluded (move-source-zeroing semantics differs from comprehension element-construction); generator/iterator comprehension (`[$f for x in src; ...]`) is also out of scope. | | STYLE028 | `self->method(args)` inside a class method (`current_function.flags.isClassMethod`). Detected on `ExprInvoke` with `isInvokeMethod=true` plus source-line inspection at `expr.at.column`: arrow-form column points at `-` of `->`, dot-form at `.`, bare call at the identifier's first char. After type inference `self->m`, `self.m`, and compiler-promoted bare `m` all share the same AST shape (`arguments[0]=ExprField(value=ExprTypeDecl, name=method)` per ast_infer_type.cpp:5612 + ast_generate.cpp:270), so AST alone can't discriminate — column-byte check (`-` then `>`) plus `strip_right(slice(line, 0, col))` ending in `self` (with a non-identifier char before, so `myself`/`_self` don't qualify) confirms the user wrote `self->`. | Drop `self->`; call `method(args)` directly. The compiler auto-promotes bare `method(args)` inside a class method to the same invoke. `self.method(args)` is also accepted when an explicit receiver reads better. Free-function `obj->method(args)` is out of scope — only `self` receivers fire. Generic instantiations skipped via `current_function.fromGeneric != null`. | +| STYLE029 | Non-public `require X` whose only use is ONE module that X re-exports | Require that module directly and drop X. Skipped when ≥ 2 of X's re-exports are used (aggregation facade is legitimate), or when X provides any macro or an `[init]`. Require analysis runs only when STYLE029/STYLE030 are enabled by lint policy. | +| STYLE030 | Non-public `require X` that is entirely unused — no symbol from X (or anything it re-exports) is referenced | Drop the require. Skipped when X provides any macro or an `[init]`, when it only re-exports builtins used through it, or when X (or its public re-export closure) exports a function matching an unresolved call inside an uninstanced generic body. Suppress a deliberate keep with `// nolint:STYLE030`. | +| STYLE031 | `var t : table` (or `table` set) with empty default-init, followed by ≥ 2 contiguous statements that are `t \|> insert(k, v)` (exact 3-arg map / 2-arg set form) or `t[k] = v` at-assigns (map only; `=`/`:=`/`<-` all count, mixing with inserts is fine) | Use a table literal move-assign `var t <- { k1 => v1, k2 => v2 }` (set: `var s <- { k1, k2 }`). Runs containing a **duplicate constant key** stay silent — sequential inserts overwrite (last wins) but a literal rejects duplicates at compile time (error 30706). Computed keys are allowed (a runtime-duplicate in a literal is last-wins, identical to inserts). `table` runs that STYLE021 fires on are skipped — `JV((k1=...))` is the stronger suggestion; computed-key JV runs still get STYLE031. | Note: `get_ptr()` related patterns (null comparison, field access) are in `perf_lint` as PERF010/PERF011 since they have performance implications. @@ -62,6 +65,7 @@ The `<|` pipe and `$()` are desugared during parsing — in the compiled AST, `f - **STYLE011:** Tracks uninitialized variables from `preVisitExprLetVariable` (excluding `generated` and `inScope`). In `preVisitExprBlockExpression`, checks if the next statement is `ExprCopy`/`ExprClone`/`ExprMove` whose left side references a tracked variable. - **STYLE012:** Two dispatch points. (a) Plain-var arm — in `preVisitExprBlockExpression`, when `expr is ExprLet`, locates the let's index inside `blk.list` (pointer equality via `smart_ptr ==`). For each variable `v` in the let that is `var a : array` with no init (also excluding `generated`, `inScope`, and generic-host instantiations), walks forward through `blk.list` counting contiguous `ExprCall` statements where `call.func.fromGeneric.name` is `"push"` or `"emplace"` (exactly 2 args — 3-arg positional insert excluded) and `arguments[0]` (unwrapping `ExprRef2Value`) resolves to `v`. Stops at the first non-matching statement. Emits when count ≥ 2 — a single `push` is often the more readable form, so the rule targets "I forgot how to init an array" bugs rather than every possible rewrite. `push_clone` is deliberately excluded because there is no clean array-literal equivalent. (b) Chain arm — `check_style012_chain_runs` from `preVisitExprBlock` scans each block for contiguous runs of 2-arg `push`/`emplace` calls (receiver type `tArray`, non-generated) whose receivers are structurally equal pure chains: `chain_equal` compares var identity at root and var-indexes, field names, and const index values; `peel_chain_adapter` strips typer-synthesized `ExprRef2Value`/`ExprPtr2Ref`. Runs need ≥ 1 chain link (plain vars stay with arm (a)). A depth-1 `v.field` chain whose root is declared in the immediately preceding `ExprLet` classifies via `ctor_field_state`: provably empty (struct value, no struct-field initializer, init null or `ExprMakeStruct` without where-block not mentioning the field) → constructor-fold message; provably initialized (MakeStruct entry or struct-field initializer) → silent, deliberate append; unknown (pointer root, non-MakeStruct init) and all deeper or unrooted chains → generic literal-move-assign message with a nolint hint for intentional appends. - **STYLE027:** Same dispatch as STYLE012 (from `preVisitExprBlockExpression` when `expr is ExprLet`). Anchors on `ExprLet` whose variable has `init == null`, `_type.baseType in (tArray, tTable)`, empty `dim`, and not `generated`/`inScope`. Requires the immediately following block statement to be an `ExprFor` (not `genFlags.generated`). Then `for_loop_matches_comprehension(forE, v, isArray, for_budget=1)` recursively verifies the body: `ExprBlock` with exactly one stmt, where the stmt is either a Leaf (`is_named_target_call(call, v, 2, "push"|"push_clone")` for arrays; `is_named_target_call(call, v, 3, "insert")` or `is_at_assign_of(stmt, v)` for tables), an `ExprIfThenElse` with `if_false==null` + `if_flags.isStatic==false` + single-stmt then-branch (recurse), or a nested `ExprFor` (recurse with budget − 1). Any `genFlags.generated` subtree short-circuits to false so lowered comprehensions don't re-flag. `is_at_assign_of` peels `ExprCopy`/`ExprClone`/`ExprMove` whose LHS is `ExprAt` with subexpr resolving (after one `ExprRef2Value` peel) to `ExprVar(target)`. `emplace` is intentionally excluded by name from the leaf matchers; the budget cap forbids depth-3+ nested fors. +- **STYLE031:** Same dispatch as STYLE012 (from `preVisitExprBlockExpression` when `expr is ExprLet`). Anchors on `ExprLet` variables with `init == null`, `_type.baseType == tTable`, not `generated`/`inScope`/generic hosts. Set flavor detected via `secondType == null || secondType.baseType == tVoid` and switches the insert matcher to exact 2-arg form. Walks forward over contiguous statements matching `is_table_insert_of` (fromGeneric name `"insert"`, exact arity, arg[0] → the variable after one `ExprRef2Value` peel) or `is_at_assign_of` (reused from STYLE027; map flavor only), extracting each key (`arguments[1]` / `ExprAt.index`). `const_key_repr` produces a type-tagged stringification of `ExprConst{String,Int,UInt,Int64,UInt64,Float,Double,Bool,Enumeration}.value` keys ("" for computed keys — note `is` on handled types is exact-type, so each const class is matched explicitly; `describe()` is not usable here because the keys flow from const visitor params and describe requires a pointee-mutable `Expression?`); a repeated repr sets `dup_const_key` and silences the variable (literal = compile error 30706, inserts = silent overwrite). Before emitting, a JV-table check re-walks the run with STYLE021's `is_insert_of_const_key` and defers to STYLE021 when it would fire (≥ 2). - **STYLE013:** Same dispatch as STYLE012 (from `preVisitExprBlockExpression` when `expr is ExprLet`). Filters via `is_struct_or_struct_ptr` — accepts `_type.baseType == Type.tStructure` (value case) OR `tPointer` whose `firstType.baseType == tStructure` (pointer case), both with empty `dim`. Excludes `generated`/`inScope`/generic instantiations. `is_default_struct_init` accepts the let if (a) `init == null` AND the variable is struct-valued (null-init pointers are deliberately NOT flagged — `var a : Foo?` followed by `a.x = ...` is a guaranteed NPE, a different bug); (b) `init is ExprMakeStruct` with no `_block` and no `MakeFieldDecl`s in any row (zero-arg `Foo()`); or (c) `init is ExprNew` with `length(arguments) == 0` (zero-arg `new Foo()` or `new Foo`). Walks forward through `blk.list` counting contiguous `ExprCopy`/`ExprClone`/`ExprMove` statements; `is_field_assign_of` checks LHS as `ExprField(value=?ExprVar(v))` — the peel handles pointer auto-deref `ptr.field` which lowers to `ExprField(value=ExprRef2Value(ExprVar(ptr)))`. Emits one warning per variable when count ≥ 2; message includes the actual struct name and the field-name list collected from the matched assignment run, plus a per-flavor "Foo(...)" vs "new Foo(...)" suggestion. - **STYLE014/STYLE015:** Post-visit pass — `scan_long_comment_blocks(prog)` runs after the visitor finishes. Walks `prog.getThisModule` only (transitively-required modules are skipped). Per native function/structure (filters: `_module == mod`, not `generated`, not `fromGeneric`, not `moreFlags.isTemplate` for fns / not `flags.isTemplate` for structs, not a name with `<` — the angle-bracket heuristic catches template-instantiated structs whose `at` points to the template's source file). Records `(file, start, end, is_private)` per function and the per-file `first_decl_line` / `max_line`. Then for each unique file, walks source line-by-line via `get_file_source_line`, accumulates contiguous `//` / `//!` lines into a block, and on block end checks: (a) skip if block starts before `first_decl_line` (module-leading docstring), (b) skip if first line contains `@nolint`, (c) find smallest enclosing function range — if private and block size > 1, emit STYLE015; otherwise if size > 3, emit STYLE014. The existing `// nolint:STYLE014` / `// nolint:STYLE015` per-line mechanism (matched by `is_suppressed`) catches the `//` form on the first line of the block; the `//!@nolint` form is matched directly via `find(first_line_text, "@nolint") >= 0` because the `is_suppressed` machinery looks for `:STYLEnnn` after `nolint`. diff --git a/tests/language/test_value_table_key.das b/tests/language/test_value_table_key.das index 09c609357..9c77682cf 100644 --- a/tests/language/test_value_table_key.das +++ b/tests/language/test_value_table_key.das @@ -4,7 +4,7 @@ require dastest/testing_boost public [test] def test_value_table_key(t : T?) { - var tab : table + var tab : table // nolint:STYLE031 — insert with value keys is under test tab |> insert(EntityId(1), "hello") tab |> insert(EntityId(2), "world") tab |> insert(EntityId(3), "!") @@ -12,12 +12,12 @@ def test_value_table_key(t : T?) { tab |> erase(EntityId(3)) t |> equal(length(tab), 2) - var found = tab |> get(EntityId(2)) <| $(val) { + var found = tab |> get(EntityId(2)) $(val) { t |> equal(val, "world") } t |> success(found) - found = tab |> get(EntityId(1)) <| $(val) { + found = tab |> get(EntityId(1)) $(val) { t |> equal(val, "hello") } t |> success(found) @@ -43,7 +43,7 @@ def test_value_table_key(t : T?) { } t |> equal(count, 2) - var set : table + var set : table // nolint:STYLE031 — set insert with value keys is under test set |> insert(EntityId(1)) set |> insert(EntityId(2)) set |> insert(EntityId(3)) diff --git a/tests/linq/test_linq_table_source.das b/tests/linq/test_linq_table_source.das index 87f1b36c6..d709ced0e 100644 --- a/tests/linq/test_linq_table_source.das +++ b/tests/linq/test_linq_table_source.das @@ -19,12 +19,12 @@ def make_int_table(n : int) : table { } def make_pt_table : table { - var t : table - t |> insert("a", Pt(x = 1, y = 10)) - t |> insert("b", Pt(x = 2, y = 20)) - t |> insert("c", Pt(x = 3, y = 30)) - t |> insert("d", Pt(x = 4, y = 40)) - return <- t + return <- { + "a" => Pt(x = 1, y = 10), + "b" => Pt(x = 2, y = 20), + "c" => Pt(x = 3, y = 30), + "d" => Pt(x = 4, y = 40) + } } [test] diff --git a/tests/long_array_table/test_dapi_layout.das b/tests/long_array_table/test_dapi_layout.das index 92967717a..e9a36274f 100644 --- a/tests/long_array_table/test_dapi_layout.das +++ b/tests/long_array_table/test_dapi_layout.das @@ -32,7 +32,7 @@ def test_dapi_array_data_pointer_non_null(t : T?) { [test] def test_dapi_table_size_tombstones_match(t : T?) { - var tab : table + var tab : table // nolint:STYLE031 — layout after the canonical insert path is under test tab |> insert(1, 100) tab |> insert(2, 200) tab |> insert(3, 300) diff --git a/tests/long_array_table/test_fusion_table_i64.das b/tests/long_array_table/test_fusion_table_i64.das index a317ab807..210a86cd7 100644 --- a/tests/long_array_table/test_fusion_table_i64.das +++ b/tests/long_array_table/test_fusion_table_i64.das @@ -8,7 +8,7 @@ require dastest/testing_boost public [test] def test_table_i64_key_index_write_read(t : T?) { - var tab : table + var tab : table // nolint:STYLE031 — fused tab[k]=v write path is under test tab[1_l] = 10 tab[1000000000000_l] = 20 tab[-1_l] = 30 @@ -28,7 +28,7 @@ def test_table_i64_key_overwrite(t : T?) { [test] def test_table_u64_key_index(t : T?) { - var tab : table + var tab : table // nolint:STYLE031 — fused tab[k]=v write path is under test tab[1_ul] = 100 tab[18446744073709551615_ul] = 200 t |> equal(tab[1_ul], 100) diff --git a/tests/long_array_table/test_long_length.das b/tests/long_array_table/test_long_length.das index 3ee70c398..9c4e33ac8 100644 --- a/tests/long_array_table/test_long_length.das +++ b/tests/long_array_table/test_long_length.das @@ -17,9 +17,6 @@ def test_long_length_after_resize(t : T?) { [test] def test_long_length_table(t : T?) { - var tab : table - tab |> insert(1, 100) - tab |> insert(2, 200) - tab |> insert(3, 300) + let tab <- { 1 => 100, 2 => 200, 3 => 300 } t |> equal(long_length(tab), 3_l) } diff --git a/tests/table_packed/test_packed.das b/tests/table_packed/test_packed.das index c882bcb6f..e9750eb6f 100644 --- a/tests/table_packed/test_packed.das +++ b/tests/table_packed/test_packed.das @@ -67,7 +67,7 @@ def test_packed_string(t : T?) { // strcmp and no promotion — the 64-bit hashes differ. Both must remain findable with their values. [test] def test_packed_string_hashkey32_collision(t : T?) { - var tab : table + var tab : table // nolint:STYLE031 — packed-mode insert path is under test unsafe(tab[kcopy(1832)]) = 111 unsafe(tab[kcopy(16303)]) = 222 t |> equal(length(tab), 2) diff --git a/tests/table_packed/test_packed_constkey.das b/tests/table_packed/test_packed_constkey.das index da450ce16..9f846b9ad 100644 --- a/tests/table_packed/test_packed_constkey.das +++ b/tests/table_packed/test_packed_constkey.das @@ -10,7 +10,7 @@ require dastest/testing_boost [test] def test_constkey_index(t : T?) { - var tab : table + var tab : table // nolint:STYLE031 — TableIndex_WithHash insert node is under test tab["foo"] = 1 // TableIndex_WithHash (insert) tab["bar"] = 2 tab["baz"] = 3 @@ -26,7 +26,7 @@ def test_constkey_index(t : T?) { [test] def test_constkey_safe_index(t : T?) { - var tab : table + var tab : table // nolint:STYLE031 — TableIndex_WithHash insert node is under test tab["foo"] = 1 tab["bar"] = 2 t |> equal(tab?["foo"] ?? -1, 1) // SafeTableIndex_WithHash (hit) @@ -38,7 +38,7 @@ def test_constkey_safe_index(t : T?) { [test] def test_constkey_builtin_key_exists(t : T?) { - var tab : table + var tab : table // nolint:STYLE031 — TableIndex_WithHash insert node is under test tab["foo"] = 1 tab["bar"] = 2 t |> equal(__builtin_table_key_exists(tab, "foo"), true) // KeyExists_WithHash (hit) @@ -49,7 +49,7 @@ def test_constkey_builtin_key_exists(t : T?) { [test] def test_constkey_builtin_find(t : T?) { - var tab : table + var tab : table // nolint:STYLE031 — TableIndex_WithHash insert node is under test tab["foo"] = 10 tab["bar"] = 20 let pf = __builtin_table_find(tab, "foo") // TableFind_WithHash (hit) @@ -68,7 +68,7 @@ def test_constkey_builtin_find(t : T?) { // third literal must miss. [test] def test_constkey_hashkey32_collision(t : T?) { - var tab : table + var tab : table // nolint:STYLE031 — TableIndex_WithHash insert node is under test tab["k1832"] = 111 // TableIndex_WithHash tab["k16303"] = 222 t |> equal(length(tab), 2) diff --git a/tests/with_boost/test_with_lock_panics.das b/tests/with_boost/test_with_lock_panics.das index f6fbeaf9e..90b26400c 100644 --- a/tests/with_boost/test_with_lock_panics.das +++ b/tests/with_boost/test_with_lock_panics.das @@ -24,7 +24,7 @@ def test_push_inside_panics(t : T?) { var arr <- [A(f1=1), A(f1=2)] var panicked = false try { - with_(arr[0]) $(a) { + with_(arr[0]) $(_a) { arr |> push(A(f1 = 99)) } } recover { @@ -38,7 +38,7 @@ def test_erase_inside_panics(t : T?) { var arr <- [A(f1=1), A(f1=2), A(f1=3)] var panicked = false try { - with_(arr[1]) $(a) { + with_(arr[1]) $(_a) { arr |> erase(0) } } recover { @@ -52,7 +52,7 @@ def test_resize_inside_panics(t : T?) { var arr <- [A(f1=1)] var panicked = false try { - with_(arr[0]) $(a) { + with_(arr[0]) $(_a) { arr |> resize(100) } } recover { @@ -66,7 +66,7 @@ def test_clear_inside_panics(t : T?) { var arr <- [A(f1=1)] var panicked = false try { - with_(arr[0]) $(a) { + with_(arr[0]) $(_a) { arr |> clear } } recover { @@ -81,7 +81,7 @@ def test_table_insert_inside_panics(t : T?) { tab |> insert("a", A(f1 = 1)) var panicked = false try { - with_(tab["a"]) $(v) { + with_(tab["a"]) $(_v) { tab |> insert("b", A(f1 = 2)) } } recover { @@ -92,12 +92,10 @@ def test_table_insert_inside_panics(t : T?) { [test] def test_table_erase_inside_panics(t : T?) { - var tab : table - tab |> insert("a", A(f1 = 1)) - tab |> insert("b", A(f1 = 2)) + var tab <- { "a" => A(f1 = 1), "b" => A(f1 = 2) } var panicked = false try { - with_(tab["a"]) $(v) { + with_(tab["a"]) $(_v) { tab |> erase("b") } } recover { @@ -112,7 +110,7 @@ def test_multi_arg_inner_push_panics(t : T?) { var b <- [A(f1=2)] var panicked = false try { - with_(a[0], b[0]) $(va, vb) { + with_(a[0], b[0]) $(_va, _vb) { b |> push(A(f1 = 99)) } } recover { diff --git a/tests/with_boost/test_with_table.das b/tests/with_boost/test_with_table.das index 614a9a50e..6d0c0e2b1 100644 --- a/tests/with_boost/test_with_table.das +++ b/tests/with_boost/test_with_table.das @@ -49,9 +49,7 @@ def test_table_default_name(t : T?) { def test_table_workhorse_value(t : T?) { //! Table with workhorse value type — block-arg is `int&`, mutation //! persists. - var tab : table - tab |> insert("alpha", 10) - tab |> insert("beta", 20) + var tab <- { "alpha" => 10, "beta" => 20 } with_(tab["alpha"]) $(v) { v = 200 } diff --git a/tutorials/language/10_tables.das b/tutorials/language/10_tables.das index d6b2bc6fd..1755b4aea 100644 --- a/tutorials/language/10_tables.das +++ b/tutorials/language/10_tables.das @@ -20,7 +20,7 @@ def main { // === Declaring a table === // table — note the semicolon between key and value types. - var ages : table + var ages : table // nolint:STYLE031 — tutorial demonstrates insert API // === Inserting values === // Use insert() with pipe syntax diff --git a/tutorials/language/30_json.das b/tutorials/language/30_json.das index 343e92f72..6218c7ff0 100644 --- a/tutorials/language/30_json.das +++ b/tutorials/language/30_json.das @@ -218,7 +218,7 @@ def modifying_json() { print("\n=== Modifying JSON ===\n") // Build an object by hand - var tab : table + var tab : table // nolint:STYLE021 — tutorial demonstrates incremental building tab |> insert("name", JV("Alice")) tab |> insert("age", JV(30)) var obj = JV(tab) diff --git a/tutorials/language/41_serialization.das b/tutorials/language/41_serialization.das index 9abd6b95c..9390dbebe 100644 --- a/tutorials/language/41_serialization.das +++ b/tutorials/language/41_serialization.das @@ -112,10 +112,7 @@ def demo_arrays() { def demo_tables() { print("\n=== table serialization ===\n") - var inventory : table - inventory |> insert("sword", 1) - inventory |> insert("potion", 5) - inventory |> insert("arrow", 20) + var inventory <- { "sword" => 1, "potion" => 5, "arrow" => 20 } var data <- mem_archive_save(inventory) print(" serialized size: {length(data)} bytes\n") diff --git a/tutorials/language/47_data_walker.das b/tutorials/language/47_data_walker.das index 370667e26..a92a82d23 100644 --- a/tutorials/language/47_data_walker.das +++ b/tutorials/language/47_data_walker.das @@ -129,12 +129,12 @@ class StructPrinter : DapiDataWalker { def override afterStructure(ps : void?; si : StructInfo) : void { indent-- - self->pad() + pad() print("\}\n") } def override beforeStructureField(ps : void?; si : StructInfo; pv : void?; vi : VarInfo; last : bool) : void { - self->pad() + pad() print("{vi.name} = ") } @@ -192,19 +192,19 @@ class ContainerPrinter : DapiDataWalker { // --- arrays --- def override beforeArrayData(ps : void?; stride : uint; count : uint64; ti : TypeInfo) : void { - self->pad() + pad() print("array[{int64(count)}] = [\n") indent++ } def override afterArrayData(ps : void?; stride : uint; count : uint64; ti : TypeInfo) : void { indent-- - self->pad() + pad() print("]\n") } def override beforeArrayElement(ps : void?; ti : TypeInfo; pe : void?; index : uint64; last : bool) : void { - self->pad() + pad() print("[{int64(index)}] = ") } @@ -214,19 +214,19 @@ class ContainerPrinter : DapiDataWalker { // --- tables --- def override beforeTable(pa : DapiTable; ti : TypeInfo) : void { - self->pad() + pad() print("table[{int(pa.size)}] = \{\n") indent++ } def override afterTable(pa : DapiTable; ti : TypeInfo) : void { indent-- - self->pad() + pad() print("\}\n") } def override beforeTableKey(pa : DapiTable; ti : TypeInfo; pk : void?; ki : TypeInfo; index : uint64; last : bool) : void { - self->pad() + pad() } def override afterTableKey(pa : DapiTable; ti : TypeInfo; pk : void?; ki : TypeInfo; index : uint64; last : bool) : void { @@ -262,10 +262,7 @@ def demo_arrays_and_tables() { adapter |> walk_data(addr(nums), typeinfo rtti_typeinfo(nums)) } - var scores : table - scores |> insert("Alice", 95) - scores |> insert("Bob", 87) - scores |> insert("Charlie", 72) + var scores <- { "Alice" => 95, "Bob" => 87, "Charlie" => 72 } print("\nWalking table:\n") unsafe { adapter |> walk_data(addr(scores), typeinfo rtti_typeinfo(scores)) @@ -298,19 +295,19 @@ class TupleVariantPrinter : DapiDataWalker { } def override beforeTuple(ps : void?; ti : TypeInfo) : void { - self->pad() + pad() print("tuple(\n") indent++ } def override afterTuple(ps : void?; ti : TypeInfo) : void { indent-- - self->pad() + pad() print(")\n") } def override beforeTupleEntry(ps : void?; ti : TypeInfo; pv : void?; idx : int; last : bool) : void { - self->pad() + pad() print("_{idx} = ") } @@ -319,7 +316,7 @@ class TupleVariantPrinter : DapiDataWalker { } def override beforeVariant(ps : void?; ti : TypeInfo) : void { - self->pad() + pad() print("variant = ") } @@ -706,12 +703,12 @@ class FilteringWalker : DapiDataWalker { def override afterStructure(ps : void?; si : StructInfo) : void { indent-- - self->pad() + pad() print("\}\n") } def override beforeStructureField(ps : void?; si : StructInfo; pv : void?; vi : VarInfo; last : bool) : void { - self->pad() + pad() print("{vi.name} = ") } diff --git a/utils/lint/tests/style021_jv_table_insert.das b/utils/lint/tests/style021_jv_table_insert.das new file mode 100644 index 000000000..e11011669 --- /dev/null +++ b/utils/lint/tests/style021_jv_table_insert.das @@ -0,0 +1,45 @@ +options gen2 +// STYLE021: table built by repeated const-key insert — +// use the named-tuple JV form `var v = JV((k1=..., k2=...))` (json_boost). +// +// Division of labor with STYLE031: const-string-key runs into a JV table are +// owned by STYLE021 (the JV named-tuple form is the stronger suggestion); +// computed-key runs fall through to STYLE031 (a table literal accepts +// computed keys, JV((...)) does not). + +expect 31209:2 + +require daslib/style_lint +require daslib/json_boost + +def make_key(i : int) : string => "k{i}" + +// --- Bad patterns (must fire — 1 x STYLE021 + 1 x STYLE031) --- + +def bad_jv_const_keys() : int { + var args : table // STYLE021 — JV((k1=..., k2=...)) + args |> insert("k1", JV(1)) + args |> insert("k2", JV(2)) + return length(args) +} + +def bad_jv_computed_keys(i : int) : int { + var args : table // STYLE031 — computed keys, table literal + args |> insert(make_key(i), JV(1)) + args |> insert(make_key(i + 1), JV(2)) + return length(args) +} + +// --- Good patterns (must NOT fire) --- + +def good_named_tuple_jv() : JsonValue? { + // The idiomatic form STYLE021 suggests. + var args = JV((k1 = 1, k2 = 2)) + return args +} + +def good_single_insert() : int { + var args : table + args |> insert("k1", JV(1)) + return length(args) +} diff --git a/utils/lint/tests/style031_table_insert_run.das b/utils/lint/tests/style031_table_insert_run.das new file mode 100644 index 000000000..b73c39c2c --- /dev/null +++ b/utils/lint/tests/style031_table_insert_run.das @@ -0,0 +1,121 @@ +options gen2 +// STYLE031: table (or table set) with empty default-init followed by a +// run of insert / `[]=` statements — use a table (set) literal move-assign. +// +// Problem: Declaring an empty `table` and then populating it with a run of +// `t |> insert(k, v)` / `t[k] = v` statements is the table variant of STYLE012. +// A table literal expresses the same construction in one move-assign: +// +// var t <- { "a" => 1, "b" => 2 } // map +// var s <- { 5, 7 } // set (table) +// +// Match rules: +// * ExprLet declares the table local with NO initializer +// * >= 2 contiguous statements immediately after are `insert` into it (3-arg +// map form / 2-arg set form) or `t[k] = v` at-assigns (map only) +// * runs containing duplicate CONSTANT keys stay silent — sequential inserts +// overwrite (last wins), but a literal rejects duplicates at compile time +// (error 30706); computed keys are fine (runtime duplicate = last wins in +// both forms) +// * table const-key runs are owned by STYLE021 (the +// JV((k1=..., k2=...)) form is the stronger suggestion) + +expect 31209:7 + +require daslib/style_lint +require daslib/json_boost + +struct Pt { + x : int = 0 + y : int = 0 +} + +def make_key(i : int) : string => "k{i}" + +// --- Bad patterns (must fire — 6 x STYLE031 + 1 x STYLE021 below) --- + +def bad_map_insert_run() : table { + var t : table // STYLE031 — map insert run + t |> insert("a", Pt(x = 1, y = 10)) + t |> insert("b", Pt(x = 2, y = 20)) + t |> insert("c", Pt(x = 3, y = 30)) + t |> insert("d", Pt(x = 4, y = 40)) + return <- t +} + +def bad_set_insert_run() : int { + var s : table // STYLE031 — set insert run + s |> insert(5) + s |> insert(7) + return length(s) +} + +def bad_at_assign_run() : int { + var t : table // STYLE031 — []= run + t["a"] = 1 + t["b"] = 2 + return length(t) +} + +def bad_mixed_insert_and_at_assign() : int { + var t : table // STYLE031 — mixed insert + []= + t |> insert("a", 1) + t["b"] = 2 + return length(t) +} + +def bad_computed_keys(i : int) : int { + var t : table // STYLE031 — computed keys collapse fine + t |> insert(make_key(i), 1) + t |> insert(make_key(i + 1), 2) + return length(t) +} + +def bad_int_keys() : int { + var t : table // STYLE031 — int-keyed run + t |> insert(1, "a") + t |> insert(2, "b") + return length(t) +} + +def bad_jv_table_const_keys() : int { + var args : table // STYLE021 fires; STYLE031 stays silent + args |> insert("k1", JV(1)) + args |> insert("k2", JV(2)) + return length(args) +} + +// --- Good patterns (must NOT fire) --- + +def good_single_insert() : int { + // A single insert is often the more readable form. + var t : table + t |> insert("a", 1) + return length(t) +} + +def good_preinitialized() : int { + // Init is non-empty — the literal rewrite would lose the existing entries. + var t <- { "a" => 1 } + t |> insert("b", 2) + t |> insert("c", 3) + return length(t) +} + +def good_duplicate_const_key() : int { + // Sequential inserts overwrite (last wins); a literal with a duplicate + // constant key is a compile error (30706) — rewrite must not be suggested. + var t : table + t |> insert("a", 1) + t |> insert("a", 2) + return length(t) +} + +def good_intervening_statement() : int { + // Decl and the insert run are not adjacent. + var t : table + let n = 42 + t |> insert("a", n) + t |> insert("b", n) + return length(t) +}