Skip to content

[pull] master from GaijinEntertainment:master#982

Merged
pull[bot] merged 14 commits into
forksnd:masterfrom
GaijinEntertainment:master
May 11, 2026
Merged

[pull] master from GaijinEntertainment:master#982
pull[bot] merged 14 commits into
forksnd:masterfrom
GaijinEntertainment:master

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented May 11, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

borisbat and others added 14 commits May 10, 2026 21:49
New rules
- PERF018: `for (i in range(length(arr))) { ... arr[i] ... }` when `i` is
  only an index of arr -- suggests `for (c in arr)`. AST-driven detection
  via for-source / for-variable / for-body callbacks + ExprAt qualifying-
  index counter (`in_qualifying_idx`). Conservative: only bare-Var arr
  resolves; field paths and arithmetic on `i` disqualify.
- STYLE020: `from_JV(v, type<T>, defV)` resolving to one of json_boost's
  scalar overloads -- suggests `v ?? defV` (operator ?? lives at lines
  85-140 of json_boost). Skips inside json_boost itself + generic
  instantiations of its templates.
- STYLE021: a `var v : table<string; JsonValue?>` followed by 2+
  contiguous `v |> insert(<const>, <any>)` calls -- suggests
  `var v = JV((k1=..., k2=...))` (named-tuple JV dispatch at
  json_boost.das:638).

STYLE005 rewrite
- Was: gated behind `--postfix-conditionals`, fired on multi-line braced
  forms, skipped same-line forms (which it called "already postfix").
- Is now: flag `if (cond) { return/break/continue }` (braces around a
  single early-exit terminator). Leaves braceless `if (cond) return` and
  postfix `return X if (cond)` alone. Discriminator is AST-only:
  `if_true.at != inner_terminator.at` <=> source had braces (synthetic
  blocks share LineInfo with the inner statement).
- Drop `postfix_conditionals` from `style_lint` / `style_lint_collect`
  public API + the matching CLI flag in utils/lint/main.das.

Lint runner
- utils/lint/main.das: treat "missing prerequisite" compile errors
  (e.g. daspkg-installed `anthropic/anthropic`) as SKIP rather than
  FAIL -- those files are out-of-tree from a stock daslang build's POV.
- utils/mcp/subtools/lint_tool.das: drop the now-removed
  `postfix_conditionals` arg from style_lint_collect calls.

Real fixes surfaced by the new STYLE005
- daslib/builtin.das: `clone` overloads -- drop the brace-wrap around
  the early-return.
- utils/mcp/subtools/lint_tool.das: postfix-form rewrites for 5
  single-terminator guards.
- utils/requirefix/main.das: `require fio` -> `require daslib/fio` (was
  silently broken under the lint runner's empty-project file access).

Tests
- utils/lint/tests/perf018_index_only_for_loop.das (expect 31208:3) --
  bad: 3 index-only patterns; good: index-used, two-arrays, sliding-
  window, for-each.
- utils/lint/tests/style005_braced_terminator.das (expect 31209:5) --
  replaces style005_postfix_conditional.das. Bad: braced return/break/
  continue, one-line braced form. Good: braceless, postfix,
  multi-statement block.
- utils/lint/tests/style020_from_jv_to_coalesce.das (expect 31209:6) --
  scalar overloads flagged; vector/struct/already-`??` forms silent.

Follow-up branch will land utils/fix-lint-errors/ (auto-fixer for the
mechanical rules) and the daslib + utils sweep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New tool: utils/fix-lint-errors/main.das.

Pipeline (matches utils/lint/main.das structure):
- scan_das_files() walks input paths.
- collect_edits() compile_file's each .das, runs FixerVisitor over the AST,
  collects SourceEdit records.
- apply_edits_to_file() sorts edits by descending end position and applies
  them as line-range splices (so earlier offsets stay valid as we go).

FixerVisitor — one rule for now:
- STYLE005: `if (cond) { <terminator> }` → `if (cond) <terminator>`.
  AST shape gate: ifte.if_true is ExprBlock, single-statement body, the
  inner is ExprReturn/Break/Continue, AND `blk.at != inner.at` (synthetic
  Blocks share LineInfo with the inner stmt). Replacement text is the
  inner-of-braces source, trimmed — captures keyword+subexpr+any `;`
  cleanly, where extract_text(inner.at) would only get the keyword.

Coordinate convention recorded in extract_block_inner's docstring:
LineInfo.column is 0-based, last_column exclusive 0-based. Got this wrong
on first pass — the `{` was off by one and the rewrite emitted broken
output like `retur}`. Now correct.

CLI: --dry-run prints would-be edits without writing, --quiet suppresses
PASS lines, --help prints usage. Positional args are files or dirs.

Smoke-tested on synthetic probes covering braceless, postfix-desugared,
and multi-line braced forms. Real daslib/utils sweep is the next commit
(this branch).
Adds four mechanical rewrite rules to the auto-fixer:

  PERF013   a += 1 / a -= 1              -> a++ / a--
  PERF017   length(x) == 0 / != 0        -> empty(x) / !empty(x)
  STYLE017  if (cond) return true; else return false  -> return cond/!cond
            (both forms: in-place else-branch and adjacent return)
  STYLE018  b == true / b != false       -> b ; b == false / b != true -> !b

Each rule shares the same Edit pipeline (visitor override pushes a
SourceEdit; apply_edits writes them back from end-of-file). New
infrastructure:

  - extract_source / extract_source_at  multi-line LineInfo -> bytes
  - leftmost_pos_of / rightmost_pos_of  chains walk into root/leaf since
    ExprField.at covers only `.` and ExprAt.at covers only `[`
  - source_span_of  adds atEnclosure handling for ExprCall
  - is_simple_subject  gate for rules that need clean source spans
  - apply_edits_to_file overlap filter  drops edits subsumed by a larger
    one (e.g. STYLE005 inside STYLE017(b) span)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two more mechanical rewrite rules to the auto-fixer:

  PERF015   a > b ? a : b   -> max(a, b)   (and min variants)
  PERF016   x < 0 ? -x : x  -> abs(x)      (and other ternary abs forms)

New infrastructure:

  - extract_expr_source — wraps leftmost+rightmost+extract_source_at into one
    call, used for splicing operand text into replacements
  - is_const_zero / is_neg_of / expr_describe_equal — mirror the matching
    helpers in daslib/perf_lint.das so the fixer detects the same patterns
  - rightmost_pos_of now recurses into ExprOp1 (-x's right end lives in
    subexpr) and ExprRef2Value (typer wrappers don't shift source)
  - lo_min / hi_max — guarded min/max for source spans, with sentinel-0
    handling so unset bounds don't accidentally win

The PERF015/16 span computation takes min(leftmost) over both cmp.left and
cmp.right because the typer canonicalizes Yoda forms (0 > x becomes x < 0
in AST), so cmp.left isn't always the source-left operand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Improvements based on what real daslib code surfaced:

1. visit_with_generics  match the lint visitor's coverage. The previous
   `visit()` skipped uninstantiated generics, missing ~30% of hits.

2. in_target_file gate  visit_with_generics walks AST that may live in
   other files (builtin instantiations triggered from us). Without the
   gate, edits with cross-file coordinates corrupted the target file.

3. Chain helpers handle auto-deref  leftmost_pos_of now recurses past
   ExprPtr2Ref and ExprRef2Value. `fun.moreFlags.isTemplate` was losing
   its `fun` prefix because the typer inserts ExprPtr2Ref between fun
   and .moreFlags whose `.at` is the dot, not fun.

4. rightmost_pos_of handles ExprOp1  `-x`'s right end lives in subexpr.
   Without this, PERF016 `... ? -x : x` truncated to `abs(x)x`.

5. is_simple_subject recurses chains  rejects ExprPtr2Ref anywhere in
   the chain since parens around `(*pv)` aren't in AST. PERF013/PERF017
   /STYLE018 bail rather than corrupting source.

6. STYLE017(a) disabled  match blocks parse as nested ExprIfThenElse
   chains where the wildcard arm's body becomes the innermost else,
   making them indistinguishable from real if/else. The lint reports
   these as STYLE017 too; auto-fixing them produced broken match code.
   Hand-fix the lint reports.

7. file_requires_math gate  PERF015/PERF016 rewrite to math::min/max/
   abs. Files without `require math` would fail to compile post-rewrite.
   Source-line scan determines applicability.

8. Auto-revert safety net  after writing a file, try_compile re-reads
   it. If compile fails, the original is restored and reported as
   REVERT instead of corrupting state. This catches the edge cases the
   per-rule gates miss; daslib sweep went from 60 cascade failures to
   8 cleanly-reverted files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drives the daslib bulk sweep from 8 cleanly-reverted files down to 0 by
diagnosing each break in turn and fixing the underlying span/shape bug,
rather than relying on the auto-revert safety net.

Bugs fixed:

1. Implicit-self field access — `length(field)` inside a class method
   gets parsed as ExprField(value=ExprVar(self), name=...) where the
   field's `.at` already covers the source field name (NOT just the `.`
   separator). is_implicit_self_field discriminator + at-aware leftmost
   /rightmost. Was producing `!empty(exprForTerminator) > 0) {))`.

2. `[macro_function]` skip — these set `fn.moreFlags.macroFunction` not
   `isTemplate`. Bodies contain macro-substituted AST whose source
   positions don't reliably point to user-written source. Bail.

3. Source-text sanity gate — extract_expr_source rejects extractions
   that start with punctuation (`.`, `]`, `,`, `)`, `}`). Catches any
   remaining chain-helper miss without corrupting the file.

4. ExprOp1/Op2/Op3 in left/rightmost — binary cond chains like
   `A || B || C` need recursion into ExprOp2 operands; `-x` and `!x`
   need rightmost to recurse into subexpr. `-x` was producing `abs(x)x`;
   `A || B || C` produced `|| pt.annotation.name != "TypeDecl" || ...`.

5. Pipe-form calls — `arr |> length` parses to ExprCall whose name is
   AT `length` (later in source) and args[0] precedes it. leftmost
   takes min(call.at, arg[0]'s leftmost); rightmost takes max over
   atEnclosure, call.at, and all args.

6. Parenthesized comparisons — `(a < b) ? a : b` had the closing `)`
   swallowed by PERF015/16 spans. is_parenthesized_cmp peeks the source
   line for `)` right after cmp.right and bails.

7. Property-method calls — `obj.foo` (with `isProperty`) parses to
   ExprCall name=".foo" or "`.foo" whose source extent depends on
   hidden arg positions. cond_uses_property_call walks the cond tree
   and STYLE017 bails if any property call is involved.

8. STYLE005 multi-statement sanity — extracted inner text MUST start
   with `return`/`break`/`continue`. Defends against `visit_with_generics`
   surfacing optimized AST shapes whose block-length count doesn't
   match user source.

9. Bonus: --no-verify flag for debugging — disables the auto-revert
   safety net so the broken-rewrite output can be inspected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applied utils/fix-lint-errors over daslib/ — every change is the
deterministic mechanical equivalent the lint rule already prescribes.
All 79 files compile cleanly post-rewrite; no compile-verify reverts.

Breakdown (1131 edits):
  STYLE005 — `if (cond) { return X }` -> `if (cond) return X`  (most hits)
  PERF017  — `length(x) == 0 / != 0` -> `empty(x) / !empty(x)`
  PERF013  — `a += 1 / -= 1` -> `a++ / a--`
  PERF015  — ternary min/max -> `min(a, b) / max(a, b)`
  PERF016  — ternary abs -> `abs(x)`
  STYLE017 — `if (c) return true; return false;` -> `return c` (or !c)
  STYLE018 — `b == true / != false` -> b / !b

Net: −1987 lines (lots of brace-wrapping noise removed). Lint count
across daslib dropped from ~1500 to 449. The remaining 449 are
structural rewrites the auto-fixer correctly skips:

  STYLE016 (112) — adjacent guards with identical early-exit; needs
                   `||`-combination, has cross-statement scope
  STYLE005 (181) — cases where the body's source extent isn't safely
                   reliable (property-method chains, generics)
  PERF017  (78)  — same — `length()` involves unsafe-to-rewrite chains
  PERF014  (10)  — closed-interval char-class ranges (not auto-fixable
                   without a small AST replanting)
  remainder      — STYLE011/013, PERF006/007/012/018: each rule is
                   structural and warrants hand-curation per call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `from_JV(v, type<T>, defV)` overloads in `daslib/json_boost` are
`[template(ent)]` generics, so a user call goes through two levels of
template instantiation before the visitor sees it. expr.func.name is
mangled and the `ent` witness is stripped from arguments — so the original
check (`expr.func.name == "from_JV"` and `arguments[1]._type` for the type
witness) never matched. STYLE020 silently fired 0 times since it was
introduced.

Walk `fromGeneric` to the root and match on the root's name/module instead.
Use `expr._type` (the call result type) for the supported-scalar check —
robust whether the visitor sees the pre- or post-instantiation shape.

Lint test fixtures (style005 always-on bleed):
- style017_bool_return: refactor `good_same_value` / `good_compute` to
  braceless form so STYLE005 doesn't also fire on the STYLE017 negatives.
- style016_adjacent_guards: refactor bad-case bodies to braceless so the
  fixture exercises STYLE016 alone (STYLE005 has its own fixture). The
  `bad_else_chain` keeps the outer braces (for the elif AST shape) but the
  elif body switches to braceless.

All 21 lint tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The post-rewrite verification compile was running with
`ignore_shared_modules = true`, `no_optimizations = true`, and
`no_infer_time_folding = true`. These strict settings produced
false-positive reverts on files that transitively require dastest
(UnrollMacro needs `range(const)`, which needs const-folding) or daspkg
packages like `sqlite/sqlite_boost` (which need shared modules loaded).

Drop the three flags. Keep `export_all = true` (matches the lint pass) and
`lint_check = false` (the lint pass already ran and reported a clean
compile, so re-running lint is wasteful). The collect_edits pre-pass still
runs with the original strict settings; only the post-rewrite verification
is relaxed.

This change recovers ~10 files per utils/ sweep that previously
auto-reverted despite producing clean compiles (e.g. utils/benchctl/*).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three corruption fixes surfaced by the utils/ sweep:

1) Skip `fn.flags.generated`. Functions synthesized by struct annotations
   (e.g. `[CommandLineArgs]` → `parse_args`, `get_command_info`) carry
   the struct's LineInfo throughout their body. A `length(args) == 0`
   inside the synthesized parse_args body would rewrite to a span
   starting at the struct definition, eating ~889 lines on
   utils/daspkg/commands.das.

2) Guard STYLE017(b) against postfix-desugared `return X if (cond)`.
   The parser desugars postfix conditional returns into an
   ExprIfThenElse whose `.at` points at the original `return`, not
   `if`. STYLE017(b)'s span computation would then clobber the leading
   `return X` token. Detect by peeking the source line: bail if it
   does not start (after whitespace, optional `}`) with `if` or `elif`.

3) Guard STYLE017(b) against conds containing the typer-lowered form
   of `x is T` (`x.__rtti == "..."`). The synthesized ExprConstString
   carries an `at` that no longer covers the original type-name source
   span, so rightmost_pos_of stops at `is` and the rewrite drops the
   trailing typename (`|| e is ExprOp1)` → `|| e is)`).

Self-apply on utils/fix-lint-errors/main.das now produces 32 clean
edits and a passing post-rewrite verify; full utils/ sweep reverts
drop to just the find-dupe `anthropic/` daspkg-missing pair (2 fails).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applied utils/fix-lint-errors to utils/. The fixer's three new guards
(macro-generated skip, postfix-if discriminator, typer-lowered-is bail)
land 0 auto-reverts. The only skipped files are utils/find-dupe/judge_live
and utils/find-dupe/main, which require the daspkg-only `anthropic/`
package; users who install it via `daspkg install --root utils/find-dupe`
can re-run the fixer to pick those up.

Net -1517 LOC across utils/. Lint count drops from 1154 → 347.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cosmetic whitespace normalization from `mcp__daslang__format_file` to keep
the test fixture canonical-formatted; CI-fmt parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CLAUDE.md: add a "Braceless early-exit" bullet under Error handling
  pointing readers to STYLE005, and four new rows in the idiomatic-forms
  table (STYLE005 / PERF018 / STYLE020 / STYLE021).
- doc/source/reference/language/lint.rst: STYLE005 section rewritten
  (always-on, braceless preferred), PERF018 section added after PERF017,
  STYLE020 + STYLE021 sections added after STYLE019. Drop the stale
  `--postfix-conditionals` CLI flag from the standalone-utility options.
- skills/perf_lint.md: PERF018 row appended to the rules table.
- skills/style_lint.md: STYLE005 row description rewritten; STYLE020 +
  STYLE021 rows added; Public API drops the `postfix_conditionals`
  parameter (removed when STYLE005 became always-on).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…and-fixer

lint: PERF018/STYLE005/STYLE020/STYLE021 + auto-fixer + daslib+utils sweep
@pull pull Bot locked and limited conversation to collaborators May 11, 2026
@pull pull Bot added the ⤵️ pull label May 11, 2026
@pull pull Bot merged commit b978482 into forksnd:master May 11, 2026
1 of 5 checks passed
@pull pull Bot had a problem deploying to github-pages May 11, 2026 15:06 Error
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant