Skip to content

Commit ac93e30

Browse files
borisbatclaude
andcommitted
lint: repo-level .lint_config (TOML) with STYLE005 off by default
Reviewers asked for TOML over a custom line-based syntax. So: - New module daslib/toml.das — full TOML 1.0 parser that reads into the same JsonValue? tree shape produced by daslib/json. Existing json_boost accessors (v ?? def, from_JV, etc.) work on TOML inputs unchanged. - .lint_config is now TOML. A single [rules] table; each entry is a bool (true = on, false = off). - STYLE005 is off by default. Bad TOML / missing file / no [rules] / non-bool entries → no-op, defaults stand. - Same dispatch as before: the three [lint_macro] paths, utils/lint/main.das (skipped under --enable whitelist mode), and the MCP lint subtool all consult the file via daslib/lint_config. TOML 1.0 surface: comments, basic/literal/multi-line strings with escapes, decimal/hex/octal/binary integers with _ separators, floats with exponent and inf/nan, booleans, datetimes (preserved as raw RFC-3339 strings since JSON has no date type), inline + multi-line arrays, inline tables, [section] / [a.b] dotted headers, [[a]] arrays of tables, dotted keys. Fail-fast error via `error` out-param. Drive-by daslang AOT C++ emitter fix (daslib/aot_cpp.das): to_cpp_double's daslang-side override only handled finite doubles — '{val:.17e}' formats non-finite values as the bare tokens `inf`/`nan`, which either don't exist in C++ (inf) or resolve to <cmath>'s double nan(const char*) overload (nan). Both bake into AOT-emitted code and break every C++ build. Mirrored the C++-side to_cpp_double behavior (runtime_string.cpp:526): emit ((double)INFINITY) / ((double)(-INFINITY)) / ((double)NAN) for non-finite, DBL_MIN/DBL_MAX boundaries for the extremes. Uses self-comparison + magnitude tests to avoid requiring math (which cascade-broke this file's match-based type helpers under lint-mode no_infer_time_folding). Drive-by aot_cpp.das lint cleanups surfaced once this PR's lint covered the file (it had never been included in any prior PR's changed-files set): - Add explicit fallthrough returns after match blocks so lint-mode exhaustiveness check passes without infer-time folding (das_to_cppString, das_to_cppCTypeString, isConstRedundantForCpp). - PERF020: drop redundant int(...) / string(...) casts on already-typed values (tupleSize / variantSize / variantAlign / info.name). - STYLE016: consolidate isConstRedundantForCpp's run of same-result match arms into one OR-chain. - STYLE010 false positives on match's `if (_)` wildcard suppressed via // nolint:STYLE010 (the macro expands `_` to `if (true)`; rule is right about the source shape, wrong about the macro generator). - TOML fixture for STYLE005: per-module opt-in 'options _enable_default_off_rules = true' in utils/lint/tests/style005_braced_terminator.das so the rule fires for that fixture's 5 expected sites despite the new default-off. Tests: tests/daslib/test_toml.das (42 cases — every value type, section / dotted / AoT shapes, parse-failure paths). Existing tests/lint/test_lint_config.das rewritten for the TOML shape (16 cases). All 60 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 20aa59c commit ac93e30

19 files changed

Lines changed: 2237 additions & 46 deletions

File tree

daslib/aot_cpp.das

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,9 @@ def das_to_cppString(t : Type) {
8686
if (Type.tLambda) { return "Lambda"; }
8787
if (Type.tTuple) { return "Tuple"; }
8888
if (Type.tVariant) { return "Variant"; }
89-
if (_) { panic("Missed type {t}"); return ""; }
9089
}
90+
panic("Missed type {t}")
91+
return ""
9192
}
9293

9394
def das_to_cppCTypeString(t : Type) {
@@ -141,8 +142,9 @@ def das_to_cppCTypeString(t : Type) {
141142
if (Type.tTuple) { return "tTuple"; }
142143
if (Type.tVariant) { return "tVariant"; }
143144
if (Type.tHandle) { return "tHandle"; }
144-
if (_) { panic("Missed type {t}"); return ""; }
145145
}
146+
panic("Missed type {t}")
147+
return ""
146148
}
147149

148150

@@ -151,27 +153,15 @@ def isConstRedundantForCpp(typeDecl : TypeDeclPtr) {
151153
if (!empty(typeDecl.dim)) return false;
152154
if (typeDecl.isVectorType) return true;
153155
match (typeDecl.baseType) {
154-
if (Type.tBool) { return true; }
155-
if (Type.tInt8) { return true; }
156-
if (Type.tUInt8) { return true; }
157-
if (Type.tInt16) { return true; }
158-
if (Type.tUInt16) { return true; }
159-
if (Type.tInt64) { return true; }
160-
if (Type.tUInt64) { return true; }
161-
if (Type.tInt) { return true; }
162-
if (Type.tUInt) { return true; }
163-
if (Type.tFloat) { return true; }
164-
if (Type.tDouble) { return true; }
165-
if (Type.tEnumeration) { return true; }
166-
if (Type.tEnumeration8) { return true; }
167-
if (Type.tEnumeration16) { return true; }
168-
if (Type.tEnumeration64) { return true; }
169-
if (Type.tBitfield) { return true; }
170-
if (Type.tBitfield8) { return true; }
171-
if (Type.tBitfield16) { return true; }
172-
if (Type.tBitfield64) { return true; } // nolint:STYLE017 (match arm chain)
173-
if (_) { return false; }
156+
if (Type.tBool || Type.tInt8 || Type.tUInt8 || Type.tInt16 || Type.tUInt16 ||
157+
Type.tInt64 || Type.tUInt64 || Type.tInt || Type.tUInt ||
158+
Type.tFloat || Type.tDouble ||
159+
Type.tEnumeration || Type.tEnumeration8 || Type.tEnumeration16 || Type.tEnumeration64 ||
160+
Type.tBitfield || Type.tBitfield8 || Type.tBitfield16 || Type.tBitfield64) {
161+
return true
162+
}
174163
}
164+
return false
175165
}
176166

177167

@@ -255,7 +245,7 @@ def aotSuffixNameEx(funcName : string; suffix : string) {
255245
if ('.') { t_ch = "Dot"; }
256246
if ('`') { t_ch = "Tick"; }
257247
if (',') { t_ch = "Comma"; }
258-
if (_) {
248+
if (_) { // nolint:STYLE010 — match macro expands wildcard to `if (true)`
259249
let repr = build_string() $(ss) {
260250
ss |> write_char(hex_char(ch >> 4))
261251
ss |> write_char(hex_char(ch));
@@ -345,7 +335,7 @@ def describeCppTypeEx(typeDecl : TypeDeclPtr;
345335
if (cfg.cross_platform) {
346336
write(writer, "AutoTuple<{types}>")
347337
} else {
348-
write(writer, "TTuple<{int(typeDecl.tupleSize)},{types}>")
338+
write(writer, "TTuple<{typeDecl.tupleSize},{types}>")
349339
}
350340
} elif (baseType == Type.tVariant) {
351341
let types = (each(typeDecl.argTypes)
@@ -355,7 +345,7 @@ def describeCppTypeEx(typeDecl : TypeDeclPtr;
355345
if (cfg.cross_platform) {
356346
write(writer, "AutoVariant<{types}>");
357347
} else {
358-
write(writer, "TVariant<{int(typeDecl.variantSize)},{int(typeDecl.variantAlign)},{types}>")
348+
write(writer, "TVariant<{typeDecl.variantSize},{typeDecl.variantAlign},{types}>")
359349
}
360350
} elif (baseType == Type.tStructure) {
361351
if (typeDecl.structType != null) {
@@ -814,7 +804,7 @@ class public AotDebugInfoHelper {
814804
writeDim(writer, fld, suffix);
815805
writeArgTypes(writer, fld, suffix);
816806
writeArgNames(writer, fld, suffix);
817-
write(*writer, "VarInfo {funcInfoName(info)}_field_{fi} = \{ {describeCppVarFuncInfo(string(info.name), fld,suffix)} \};\n");
807+
write(*writer, "VarInfo {funcInfoName(info)}_field_{fi} = \{ {describeCppVarFuncInfo(info.name, fld,suffix)} \};\n");
818808
}
819809

820810
let fields = (each(range(info.count))
@@ -1885,7 +1875,7 @@ class public CppAot : AstVisitor {
18851875
if ("&") { op = "&&"; }
18861876
if ("|") { op = "||"; }
18871877
if ("^" || "^^") { op = "!="; }
1888-
if (_) { op = that_op; }
1878+
if (_) { op = that_op; } // nolint:STYLE010 — match macro expands wildcard to `if (true)`
18891879
}
18901880
write(*ss, " {op} ");
18911881
} else {
@@ -2382,8 +2372,24 @@ class public CppAot : AstVisitor {
23822372
return c;
23832373
}
23842374

2385-
def to_cpp_double(val : double) {
2386-
return "{val:.17e}";
2375+
def to_cpp_double(val : double) : string {
2376+
// Mirror the C++-side to_cpp_double (runtime_string.cpp:526): emit
2377+
// portable C++ tokens for non-finite and boundary doubles. Without
2378+
// this, daslang's '{val:.17e}' formatter prints bare 'inf' / 'nan',
2379+
// which either don't exist in C++ (inf) or resolve to '<cmath>'s
2380+
// 'double nan(const char*)' overload (nan) — both bake into the
2381+
// AOT-emitted code and break the C++ build. Bit-pattern tests
2382+
// (val != val for NaN, magnitude vs DBL_MAX for ±inf) avoid pulling
2383+
// in `require math`, which interferes with this file's `match`-based
2384+
// type-table helpers (control-flow inference cascade).
2385+
if (val == DBL_MIN) return "DBL_MIN"
2386+
if (val == -DBL_MIN) return "(-DBL_MIN)"
2387+
if (val == DBL_MAX) return "DBL_MAX"
2388+
if (val == -DBL_MAX) return "(-DBL_MAX)"
2389+
if (val != val) return "((double)NAN)" // nolint:LINT007 — canonical IEEE-754 NaN test
2390+
if (val > DBL_MAX) return "((double)INFINITY)"
2391+
if (val < -DBL_MAX) return "((double)(-INFINITY))"
2392+
return "{val:.17e}"
23872393
}
23882394

23892395
def writeOutDouble(val : double) {
@@ -2644,7 +2650,7 @@ class public CppAot : AstVisitor {
26442650
if (1) { write(*ss, "y"); }
26452651
if (2) { write(*ss, "z"); }
26462652
if (3) { write(*ss, "w"); }
2647-
if (_) { write(*ss, "?"); }
2653+
if (_) { write(*ss, "?"); } // nolint:STYLE010 — match macro expands wildcard to `if (true)`
26482654
}
26492655
}
26502656
write(*ss, "*/");

daslib/lint.das

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ module lint shared private
1515
require daslib/strings_boost
1616
require daslib/ast_boost
1717
require daslib/strings_boost
18+
require daslib/lint_config
1819

1920
[lint_macro]
2021
class LintEverything : AstPassMacro {
2122
//! Pass macro that enables lint checking for modules.
2223
def override apply(prog : ProgramPtr; mod : Module?) : bool {
23-
paranoid(prog, true)
24+
var disabled : table<string>
25+
seed_default_disabled(disabled)
26+
load_lint_config(disabled)
27+
paranoid(prog, true, disabled)
2428
return true
2529
}
2630
}
@@ -438,7 +442,16 @@ class LintVisitor : AstVisitor {
438442

439443
def public paranoid(prog : ProgramPtr; compile_time_errors : bool) {
440444
//! Runs the paranoid lint visitor on the program to check for common coding issues.
445+
let empty_set : table<string>
446+
paranoid(prog, compile_time_errors, empty_set)
447+
}
448+
449+
def public paranoid(prog : ProgramPtr; compile_time_errors : bool; disabled_codes : table<string>) {
450+
//! Filter-aware compile-time variant. ``disabled_codes`` suppresses
451+
//! the listed rule IDs (e.g. "LINT003") alongside the usual
452+
//! ``// nolint:CODE`` directives.
441453
var astVisitor = new LintVisitor(compile_time_errors = compile_time_errors)
454+
astVisitor.disabled_codes := disabled_codes
442455
make_visitor(*astVisitor) $(adapter) {
443456
astVisitor.astVisitorAdapter = adapter
444457
visit(prog, astVisitor.astVisitorAdapter)

daslib/lint_config.das

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
options gen2
2+
options indenting = 4
3+
options no_unused_function_arguments = false
4+
5+
module lint_config shared private
6+
7+
//! Repo-level lint enable/disable config.
8+
//!
9+
//! Loads ``{get_das_root()}/.lint_config`` (a TOML file) and folds its
10+
//! ``[rules]`` table into a ``disabled_codes`` set consumed by the three
11+
//! ``[lint_macro]`` pass-macros (``daslib/lint``, ``daslib/perf_lint``,
12+
//! ``daslib/style_lint``), by the standalone runner ``utils/lint/main.das``,
13+
//! and by the MCP ``lint`` subtool.
14+
//!
15+
//! Format (TOML 1.0). One ``[rules]`` table; each entry sets a rule to
16+
//! ``true`` (on) or ``false`` (off):
17+
//!
18+
//! [rules]
19+
//! STYLE005 = true # re-enable a default-off rule
20+
//! PERF007 = false # disable a default-on rule
21+
//!
22+
//! Anything that is not a boolean entry under ``[rules]`` is ignored.
23+
//! If the file is missing, unreadable, or fails to parse the call is a
24+
//! no-op — built-in defaults stand.
25+
26+
require fio
27+
require daslib/json
28+
require daslib/toml
29+
30+
//! Loads ``{get_das_root()}/.lint_config`` into ``disabled_codes``.
31+
//! No-op when the file is missing or unreadable.
32+
def public load_lint_config(var disabled_codes : table<string>) : void {
33+
load_lint_config_from_path("{get_das_root()}/.lint_config", disabled_codes)
34+
}
35+
36+
//! Parser entry point. Reads ``path`` (TOML) and folds the ``[rules]``
37+
//! table into ``disabled_codes``. Public so tests can exercise the parser
38+
//! hermetically without writing into the repo root.
39+
def public load_lint_config_from_path(path : string; var disabled_codes : table<string>) : void {
40+
let body = fread(path)
41+
if (empty(body)) return
42+
var error : string
43+
var root : JsonValue? = read_toml(body, error)
44+
// bad TOML or no [rules] table → defaults stand
45+
if (root == null || !(root.value is _object)) return
46+
var root_obj & = unsafe(root.value as _object) // nolint:LINT003 — table [] requires mutable binding
47+
if (!key_exists(root_obj, "rules")) return
48+
var rules_obj : JsonValue? = root_obj["rules"]
49+
if (rules_obj == null || !(rules_obj.value is _object)) return
50+
var rules & = unsafe(rules_obj.value as _object) // nolint:LINT003 — iteration requires mutable view
51+
for (code, val_jv in keys(rules), values(rules)) {
52+
if (val_jv == null || !(val_jv.value is _bool)) continue
53+
let enabled = val_jv.value as _bool
54+
if (enabled) {
55+
// CODE = true → on → remove from default-disabled
56+
if (key_exists(disabled_codes, code)) {
57+
disabled_codes |> erase(code)
58+
}
59+
} else {
60+
// CODE = false → off → add to disabled
61+
if (!key_exists(disabled_codes, code)) {
62+
disabled_codes |> insert(code)
63+
}
64+
}
65+
}
66+
}
67+
68+
//! Seeds ``disabled_codes`` with the canonical default-off rule set
69+
//! (currently just STYLE005). Call before ``load_lint_config`` so that
70+
//! a ``STYLE005 = true`` directive in ``.lint_config`` can re-enable it.
71+
//! Single source of truth used by the three ``[lint_macro]`` paths,
72+
//! ``utils/lint/main.das``, and the MCP ``lint`` subtool.
73+
def public seed_default_disabled(var disabled_codes : table<string>) : void {
74+
if (!key_exists(disabled_codes, "STYLE005")) {
75+
disabled_codes |> insert("STYLE005")
76+
}
77+
}

daslib/perf_lint.das

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module perf_lint shared private
3939

4040
require daslib/ast_boost
4141
require strings
42+
require daslib/lint_config
4243

4344
// ---------------------------------------------------------------------------
4445
// Visitor
@@ -1869,7 +1870,16 @@ class PerfLintVisitor : AstVisitor {
18691870
def public perf_lint(prog : ProgramPtr; compile_time_errors : bool) : int {
18701871
//! Runs the performance lint visitor on the compiled program.
18711872
//! Returns the number of warnings found.
1873+
let empty_set : table<string>
1874+
return perf_lint(prog, compile_time_errors, empty_set)
1875+
}
1876+
1877+
def public perf_lint(prog : ProgramPtr; compile_time_errors : bool; disabled_codes : table<string>) : int {
1878+
//! Filter-aware compile-time variant. ``disabled_codes`` suppresses
1879+
//! the listed rule IDs (e.g. "PERF007") alongside the usual
1880+
//! ``// nolint:CODE`` directives.
18721881
var astVisitor = new PerfLintVisitor(compile_time_errors = compile_time_errors)
1882+
astVisitor.disabled_codes := disabled_codes
18731883
make_visitor(*astVisitor) $(astVisitorAdapter) {
18741884
visit(prog, astVisitorAdapter)
18751885
}
@@ -1908,7 +1918,10 @@ def public perf_lint_collect(prog : ProgramPtr; var warnings : array<string>;
19081918
[lint_macro]
19091919
class PerfLintMacro : AstPassMacro {
19101920
def override apply(prog : ProgramPtr; mod : Module?) : bool {
1911-
perf_lint(prog, true)
1921+
var disabled : table<string>
1922+
seed_default_disabled(disabled)
1923+
load_lint_config(disabled)
1924+
perf_lint(prog, true, disabled)
19121925
return true
19131926
}
19141927
}

daslib/style_lint.das

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module style_lint shared private
3939

4040
require daslib/ast_boost
4141
require daslib/is_local
42+
require daslib/lint_config
4243
require strings
4344

4445
// ---------------------------------------------------------------------------
@@ -378,7 +379,7 @@ class StyleLintVisitor : AstVisitor {
378379
if (col + 1u >= llen) return
379380
var is_arrow = false
380381
peek_data(lineTxt) $(data) {
381-
is_arrow = int(data[col]) == int('-') && int(data[col + 1u]) == int('>')
382+
is_arrow = int(data[col]) == '-' && int(data[col + 1u]) == '>'
382383
}
383384
if (!is_arrow) return
384385
let prefix = strip_right(slice(lineTxt, 0, col |> int))
@@ -389,7 +390,7 @@ class StyleLintVisitor : AstVisitor {
389390
var prev_ok = false
390391
peek_data(prefix) $(pd) {
391392
let ch = int(pd[plen - 5])
392-
prev_ok = !(is_alpha(ch) || is_number(ch) || ch == int('_'))
393+
prev_ok = !(is_alpha(ch) || is_number(ch) || ch == '_')
393394
}
394395
if (!prev_ok) return
395396
}
@@ -1741,7 +1742,16 @@ def public style_lint(prog : ProgramPtr; compile_time_errors : bool; comment_hyg
17411742
//! Runs the style lint visitor on the compiled program.
17421743
//! Returns the number of warnings found.
17431744
//! Pass ``comment_hygiene = true`` to enable STYLE014/STYLE015 checks.
1745+
let empty_set : table<string>
1746+
return style_lint(prog, compile_time_errors, empty_set, [comment_hygiene = comment_hygiene])
1747+
}
1748+
1749+
def public style_lint(prog : ProgramPtr; compile_time_errors : bool; disabled_codes : table<string>; comment_hygiene : bool = false) : int {
1750+
//! Filter-aware compile-time variant. ``disabled_codes`` suppresses
1751+
//! the listed rule IDs (e.g. "STYLE005") alongside the usual
1752+
//! ``// nolint:CODE`` directives.
17441753
var astVisitor = new StyleLintVisitor(compile_time_errors = compile_time_errors, comment_hygiene = comment_hygiene)
1754+
astVisitor.disabled_codes := disabled_codes
17451755
make_visitor(*astVisitor) $(astVisitorAdapter) {
17461756
visit_with_generics(prog, astVisitorAdapter)
17471757
}
@@ -1797,7 +1807,19 @@ def public style_lint_collect(prog : ProgramPtr; var warnings : array<string>;
17971807
class StyleLintMacro : AstPassMacro {
17981808
def override apply(prog : ProgramPtr; mod : Module?) : bool {
17991809
let hygiene = prog._options |> find_arg("_comment_hygiene") ?as tBool ?? false
1800-
style_lint(prog, true, [comment_hygiene = hygiene])
1810+
// Module-local opt-in: 'options _enable_default_off_rules = true' skips
1811+
// the default-disabled seed (currently STYLE005) so the rules-off-by-
1812+
// default fire for this module unless .lint_config disables them again.
1813+
// The `.lint_config` load still runs afterward, so repo-level config can
1814+
// override on top. Used by rule-specific test fixtures whose `expect`
1815+
// assertions depend on default-off rules firing.
1816+
let force_default_off = prog._options |> find_arg("_enable_default_off_rules") ?as tBool ?? false
1817+
var disabled : table<string>
1818+
if (!force_default_off) {
1819+
seed_default_disabled(disabled)
1820+
}
1821+
load_lint_config(disabled)
1822+
style_lint(prog, true, disabled, [comment_hygiene = hygiene])
18011823
return true
18021824
}
18031825
}

0 commit comments

Comments
 (0)