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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T[]>`); the `_from` suffix names the bulk intent. Source must be `array<T>` or C-array — range/iterator sources are not flagged. `emplace` is out of scope (const iter-var can't be moved) |
| `var a : array<T>; for (x in SRC) { if (COND) { a \|> push(EXPR) } }` (or `table<K;V>` + `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<K;V>; 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<string; JsonValue?>` 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)" |

Expand Down
128 changes: 125 additions & 3 deletions daslib/style_lint.das
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ module style_lint shared private
//! STYLE020 — from_JV(v, type<T>, defV) on a scalar — use 'v ?? defV' (operator ?? from json_boost)
//! STYLE021 — table<string; JsonValue?> 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(<sub-expr>)'
//! STYLE026 — nested 'unsafe { ... }' block; outer wrap already covers the scope — drop the inner
//! STYLE027 — var array<T> / table<K;V> with empty default-init followed by a for-loop that only push/insert into it; use a comprehension
//! 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<K;V> (or table<K> set) var initialized via a run of insert / '[]=' statements; use a table (set) literal move-assign

require daslib/ast_boost
require daslib/is_local
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -1259,10 +1262,14 @@ class StyleLintVisitor : AstVisitor {
// --- STYLE021: table<string; JsonValue?> 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 {
Expand Down Expand Up @@ -1445,6 +1452,121 @@ class StyleLintVisitor : AstVisitor {
return (arg as ExprVar).variable == target
}

// --- STYLE031: table<K;V> / table<K> 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<string>
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<string; JsonValue?> 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<K> 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<K;V> 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 =====
Expand Down
37 changes: 36 additions & 1 deletion doc/source/reference/language/lint.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1584,7 +1584,8 @@ Detection requires the variable's static type to be exactly
``table<string; JsonValue?>``, 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 <style031>` instead
(a table literal accepts computed keys, ``JV((...))`` does not).

.. code-block:: das

Expand Down Expand Up @@ -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<K;V>`` (or a ``table<K>`` 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<string; JsonValue?>`` runs with constant keys are owned by
STYLE021 (the ``JV((k1=..., k2=...))`` form is the stronger suggestion).

.. code-block:: das

// Bad
var t : table<string; int>
t |> insert("a", 1) // STYLE031
t["b"] = 2
var s : table<int>
s |> insert(5) // STYLE031 (set flavor)
s |> insert(7)

// Good
var t <- { "a" => 1, "b" => 2 }
var s <- { 5, 7 }

-----
Tests
-----
Expand Down
Loading
Loading