v / vlib / v2 / transformer / flat_write.v
13734 lines · 13301 sloc · 551.79 KB
Raw
1// Copyright (c) 2026 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module transformer
5
6import v2.ast
7import v2.token
8import v2.types
9
10// flat_write.v scaffolds the "transformer writes flat directly" multi-session
11// port. The wedge `transform_files_to_flat` (transformer.v ~line 1855) today
12// composes the legacy per-file transform with a boundary `ast.flatten_files()`
13// so callers can adopt the flat-output API now. The peak-memory win documented
14// in project_v2_flat_migration.md only materialises when the per-file rewrite
15// sites stop materialising legacy `ast.File` / `ast.Stmt` / `ast.Expr` values
16// and emit `FlatNode`s into the supplied `ast.FlatBuilder` directly.
17//
18// The entry point is `transform_file_index_to_flat` below. Its body is the
19// staging ground for the port: every session in the plan replaces one rewrite
20// site inside `transform_file` with a flat-emitting equivalent, leaving the
21// outer signature stable. The differential harness in
22// transformer_flat_diff_test.v pins both the wedge invariant (4th row,
23// `to_flat_*`) and the per-file invariant (5th row, `per_file_*`) so each
24// session's change must keep both signatures bit-equal.
25//
26// ----- Migration phases -----
27//
28// The 55-site audit from session 1 is the inventory of work, but the port
29// itself decomposes into roughly six phases. Each phase ships one
30// architectural seam; the per-rewrite-site ports of phase 4 are then
31// mechanical follow-ups that plug into the seams.
32//
33// Phase 1 (DONE, session 1): scaffolding.
34// `transform_file_index_to_flat` exists. Harness 5th row pins per-file
35// parity against a manual rehydrate+transform+append loop. Body is the
36// identity decomposition: rehydrate + transform_file + append_file.
37//
38// Phase 2 (DONE, session 2): file-level emission decomposition.
39// `ast.FlatBuilder.append_file_with_stmt_ids` accepts pre-emitted stmt
40// FlatNodeIds for the file's stmt list. `transform_file_index_to_flat`
41// now mirrors `transform_file`'s prologue itself, runs `transform_stmts`
42// for the body, and emits each transformed stmt via the new per-stmt
43// seam (`transform_stmt_to_flat` → `out.emit_stmt(...)`). The post-stmt
44// file root is assembled via `append_file_with_stmt_ids`. The per-stmt
45// seam is where phases 3..5 plug per-variant direct-emit logic.
46//
47// Phase 3 (DONE, session 3): per-stmt direct-emit dispatch.
48// `transform_file_index_to_flat` now bypasses `transform_stmts` at the
49// file level (top-level stmts never trigger its multi-stmt expansions —
50// all those paths live in function bodies). `transform_stmt_to_flat`
51// carries an 11-variant leaf-arm match for stmt kinds that are identity
52// in `transform_stmt`'s `else { stmt }` case (AsmStmt, Directive,
53// EmptyStmt, EnumDecl, FlowControlStmt, ImportStmt, InterfaceDecl,
54// ModuleStmt, StructDecl, TypeDecl, []Attribute); these direct-emit and
55// skip `transform_stmt` entirely. Non-leaf arms take the legacy
56// round-trip `out.emit_stmt(t.transform_stmt(stmt))`. The dispatch
57// surface is now ready for phase 4 ports.
58//
59// Phase 4 (IN PROGRESS — per-rewrite-site ports).
60// For each non-leaf Stmt variant (AssignStmt, BlockStmt, ConstDecl,
61// DeferStmt, ExprStmt, FnDecl, ForStmt, ForInStmt, GlobalDecl,
62// ComptimeStmt, LabelStmt, ReturnStmt, AssertStmt) rewrite the rewrite
63// logic inside `transform_X` to emit `FlatNode`s via `out.emit(...)`
64// directly instead of constructing legacy `ast.Stmt` / `ast.Expr`
65// values. The inventory below is the checklist.
66//
67// Session 1 (2026-05-26): expression dispatch surface + GlobalDecl port.
68// `transform_expr_to_flat` mirrors `transform_stmt_to_flat`'s shape with
69// 9 leaf-arm direct-emit variants (BasicLiteral, EmptyExpr, Keyword,
70// LifetimeExpr, RangeExpr, SelectExpr, StringLiteral, Tuple, Type — all
71// identity in `transform_expr`'s `else { expr }` case); non-leaf arms
72// take the legacy round-trip `out.emit_expr(t.transform_expr(expr))`.
73// The GlobalDecl arm in `transform_stmt_to_flat` direct-emits via
74// `transform_expr_to_flat` for each field's typ/value plus the new
75// `emit_field_decl_by_ids` / `emit_global_decl_by_ids` builder helpers,
76// skipping the `transform_global_decl` round-trip entirely. The
77// `__global init` fixture in the 5th harness row pins it.
78//
79// Session 2 (2026-05-26): ConstDecl port + harness fixture.
80// The ConstDecl arm in `transform_stmt_to_flat` direct-emits via
81// `transform_expr_to_flat` for each field's value plus the new
82// `emit_field_init_by_id` / `emit_const_decl_by_ids` builder helpers,
83// skipping the `transform_const_decl` round-trip entirely. Mirrors the
84// GlobalDecl shape but uses FieldInit (name + value only, no typ/attrs)
85// and carries the `is_public` flag. A new `fixture_const_decl` covers
86// leaf (BasicLiteral, StringLiteral) and non-leaf (InfixExpr) const
87// values across all 5 harness rows.
88//
89// Session 3 (2026-05-26): FnDecl dispatch arm (shape port).
90// The FnDecl arm in `transform_stmt_to_flat` calls `transform_fn_decl`
91// for the body work (prologue / scope setup / `transform_stmts` /
92// defer lowering — the next port target) but decomposes the resulting
93// `ast.FnDecl` and emits via the new `emit_parameter` / `emit_type` /
94// `emit_fn_decl_by_ids` builder helpers. Body stmts go through
95// `out.emit_stmt(body_stmt)` per stmt and the list is assembled via
96// `emit_aux_list_from_ids`. Bit-equal to the legacy
97// `out.emit_stmt(t.transform_fn_decl(stmt))` round-trip; the value is
98// that the dispatch arm now exists, so the next session can refactor
99// `transform_fn_decl` to emit body stmts straight into the builder
100// without touching this dispatch site. Every existing fixture
101// exercises this arm — all 46 harness tests pin it.
102//
103// Session 4 (2026-05-26): FnDecl wrapper-struct elision.
104// Extracts `transform_fn_decl_parts(decl) (attrs, stmts)` in fn.v as
105// the body-work driver — returns the two variable parts of the lowered
106// FnDecl (final attribute list and final transformed + defer-lowered
107// stmts) and leaves the immutable name / typ / receiver / language /
108// is_public / is_method / is_static / pos fields to be re-attached by
109// the caller. Existing `transform_fn_decl` becomes a thin wrapper that
110// calls the helper and assembles the `ast.FnDecl` (no behavioural
111// change for legacy callers). The FnDecl flat-write arm now calls
112// `transform_fn_decl_parts` directly, so the `ast.FnDecl` wrapper
113// struct that session 3 built and then decomposed is never allocated.
114// First real (if modest) memory saving in the flat-write port: one
115// FnDecl struct per fn under flat-output paths.
116//
117// Session 5 (2026-05-26): ParenExpr expr direct-emit + harness fixture.
118// The ParenExpr arm in `transform_expr_to_flat` direct-emits via a
119// recursive `transform_expr_to_flat` for the inner expression plus the
120// new `emit_paren_expr_by_id` builder helper, skipping the
121// `ast.ParenExpr` struct allocation per occurrence (the `transform_expr`
122// ParenExpr arm just recurses + rebuilds the wrapper — identity in
123// shape). First non-leaf *expression* port — paves the way for porting
124// larger expression rewrites (PrefixExpr, CastExpr, InfixExpr ...) into
125// the dispatch surface. New `fixture_paren_expr` (file-scope consts
126// wrapping leaf and nested-paren values) covers the arm across all 5
127// harness rows — 46 → 51 transformer-diff tests.
128//
129// Session 6 (2026-05-26): PrefixExpr expr direct-emit + harness fixture.
130// The PrefixExpr arm in `transform_expr_to_flat` direct-emits via a
131// recursive `transform_expr_to_flat` for the inner expression plus the
132// new `emit_prefix_expr_by_id` builder helper, skipping the
133// `ast.PrefixExpr` struct allocation for the common case (unary `-`,
134// `~`, `!`). Two legacy fallback guards keep semantics intact:
135// `expr.op == .amp` (transform_prefix_expr removes `&` redundancy with
136// attribute-aware lowering — not bit-equal to a structural rebuild) and
137// `expr.op == .arrow && expr.expr is ast.OrExpr` (channel-recv with
138// or-block is lowered by `expand_chan_recv_or_expr` — full
139// statement-list synthesis). Both stay on the round-trip
140// `out.emit_expr(t.transform_expr(expr))` path. New
141// `fixture_prefix_expr` (file-scope consts with `-`, `~`, `!`) covers
142// the arm across all 5 harness rows — 51 → 56 transformer-diff tests.
143//
144// Session 7 (2026-05-26): ModifierExpr expr direct-emit + harness fixture.
145// The ModifierExpr arm in `transform_expr_to_flat` direct-emits via a
146// recursive `transform_expr_to_flat` for the inner expression plus the
147// new `emit_modifier_expr_by_id` builder helper, skipping the
148// `ast.ModifierExpr` struct allocation per occurrence (`mut x`,
149// `shared x`, `atomic x`, `static x`, `volatile x` — all pure wrappers
150// around the inner expression in `transform_expr`'s arm; kind + pos
151// copied verbatim). Structurally identical to the ParenExpr session
152// except the kind token packs into the meta u16 (mirror of
153// `add_expr(ModifierExpr)`'s encoding). New `fixture_modifier_expr`
154// (a `mut`-parameter fn + a caller that passes `mut a`) covers the arm
155// across all 5 harness rows — 56 → 61 transformer-diff tests.
156//
157// Session 8 (2026-05-26): LambdaExpr expr direct-emit + harness fixture.
158// The LambdaExpr arm in `transform_expr_to_flat` direct-emits via a
159// recursive `transform_expr_to_flat` for the body expression plus a
160// leaf `out.emit_expr(ast.Expr(arg))` per arg Ident and the new
161// `emit_lambda_expr_by_ids` builder helper. The `transform_expr` arm
162// for LambdaExpr is a pure wrapper — args are an `[]Ident` copied
163// verbatim, only the body `expr.expr` is transformed — so direct-emit
164// mirrors that exactly: leaf args go through the legacy `emit_expr`
165// leaf path (Idents are identity), body goes through the recursive
166// direct-emit. Mirrors `add_expr(LambdaExpr)` encoding exactly:
167// edge[0] = body expr, edge[1..] = args. New `fixture_lambda_expr`
168// (an `apply(f fn (int) int, x int)` higher-order fn + a caller that
169// passes `|y| y + 1`) covers the arm across all 5 harness rows —
170// 61 → 66 transformer-diff tests.
171//
172// Session 9 (2026-05-26): FnLiteral expr direct-emit + harness fixture.
173// The FnLiteral arm in `transform_expr_to_flat` direct-emits via:
174// `out.emit_type(ast.Type(expr.typ))` for the FnType (verbatim, leaf),
175// `out.emit_expr(cv)` per captured_var (verbatim, leaf), and
176// `transform_stmts(expr.stmts)` + `out.emit_stmt(...)` per result for
177// the body stmts (still allocates a `[]Stmt` — the stmt-list expansion
178// seam is the next big port target), then the new
179// `emit_fn_literal_by_ids` builder helper. Skips the
180// `ast.FnLiteral` wrapper struct allocation per occurrence. Mirrors
181// `add_expr(FnLiteral)` encoding exactly: edge[0] = type,
182// edge[1..1+captured.len] = captured_vars, edge[1+captured.len..] =
183// stmts; `captured_vars.len` is packed into `extra`. New
184// `fixture_fn_literal` (a `make_doubler()` returning `fn (x int) int`
185// + a `use_fn_literal()` that binds a `fn (n int) int` to a local and
186// calls it) covers the arm across all 5 harness rows — 66 → 71
187// transformer-diff tests.
188//
189// Session 10 (2026-05-26): PostfixExpr expr direct-emit + harness fixture.
190// The PostfixExpr arm in `transform_expr_to_flat` direct-emits via a
191// recursive `transform_expr_to_flat` for the inner expression plus the
192// new `emit_postfix_expr_by_id` builder helper. Skips the
193// `ast.PostfixExpr` struct allocation for the plain postfix ops (`++`
194// / `--`, i.e. `.inc` / `.dec`). One legacy fallback guard:
195// `expr.op == .not || expr.op == .question` — these ops trigger
196// non-trivial lowering in `transform_expr` (native backends keep
197// postfix; non-native rewrite `expr!` / `expr?` into a CastExpr over
198// the inner Result/Option base type plus string-range rename
199// `substr` → `substr_checked` plus unwrap fallback). These produce
200// different expression shapes, so they stay on the legacy round-trip.
201// New `fixture_postfix_expr` (a fn that mut-binds a local then runs
202// `a++` / `a--`) covers the arm across all 5 harness rows — 71 → 76
203// transformer-diff tests.
204//
205// Session 11 (2026-05-26): CastExpr expr direct-emit + harness fixture.
206// The CastExpr arm in `transform_expr_to_flat` direct-emits via the
207// leaf `out.emit_expr(expr.typ)` (type is NOT transformed by
208// `transform_expr` — copied verbatim) plus a recursive
209// `transform_expr_to_flat` for the inner expression, then assembles
210// via the new `emit_cast_expr_by_ids` builder helper. Skips the
211// `ast.CastExpr` struct allocation per occurrence in the common case
212// (primitive casts: `int(x)`, `f64(y)`, `u8(z)`, etc.).
213// One legacy fallback guard: `t.type_expr_name_full(expr.typ)`
214// resolves to a name that `t.is_sum_type(name)` accepts. In that case
215// `transform_expr` may lower the cast into an explicit sum-type
216// initialisation (CallExpr to `<SumType>__from_<Variant>` / InitExpr),
217// which is a *different* expression shape. Both helpers are read-only
218// so the guard is cheap to evaluate. The existing
219// `fixture_sumtype_is_as` already exercises the fallback (it contains
220// `c := Shape(Circle{r: 2.0})`); new `fixture_cast_expr` (primitive
221// casts only) pins the fast path across all 5 harness rows — 76 → 81
222// transformer-diff tests.
223//
224// Session 12 (2026-05-26): FieldInit expr direct-emit + harness fixture.
225// The FieldInit arm in `transform_expr_to_flat` direct-emits via a
226// recursive `transform_expr_to_flat` for the inner `value` plus the
227// existing `emit_field_init_by_id` builder helper (the same helper
228// ConstDecl uses for its aux_field_init child node — `add_expr(FieldInit)`
229// and `add_field_init(field)` produce bit-equal encodings so a single
230// emit helper covers both call sites). Skips the `ast.FieldInit` struct
231// allocation per occurrence — `transform_expr`'s FieldInit arm just
232// transforms `value` and copies `name` verbatim. FieldInit reaches the
233// expr dispatch when it appears as a standalone call arg in struct-
234// shorthand / named-arg syntax (`foo(name: value)`). New
235// `fixture_field_init` (struct + a `make(name: 'a', n: 1)` call) covers
236// the arm across all 5 harness rows — 81 → 86 transformer-diff tests.
237//
238// Session 13 (2026-05-26): AsCastExpr expr direct-emit + harness fixture.
239// The AsCastExpr arm in `transform_expr_to_flat` direct-emits via the
240// leaf `out.emit_expr(expr.typ)` (type is NOT transformed by
241// `transform_expr` — copied verbatim) plus a recursive
242// `transform_expr_to_flat` for the inner expression, then assembles via
243// the new `emit_as_cast_expr_by_ids` builder helper. First port that
244// mutates Transformer state: to keep the `as` cast intact (a same-
245// expression smartcast would rewrite the operand into a direct sum
246// payload access and hide the original storage type from the backend),
247// the arm clones `smartcast_stack` + `smartcast_expr_counts`, drains
248// matching smartcast entries via a bounded `remove_smartcast_for_expr`
249// loop, transforms the inner expression with the drained state, then
250// restores the snapshot. Direct-emit mirrors the legacy prologue/restore
251// exactly so the snapshot semantics stay identical — the only thing
252// that changes is the inner recursion target (flat vs legacy). Skips
253// the `ast.AsCastExpr` struct allocation per occurrence. New
254// `fixture_as_cast_expr` (sum type Shape + `(s as Circle).r` / `(s as
255// Square).side` arms in a `pick(s Shape, k int) f64` fn) covers the
256// arm across all 5 harness rows — 86 → 91 transformer-diff tests.
257//
258// Session 14 (2026-05-26): UnsafeExpr expr direct-emit + harness fixture.
259// The UnsafeExpr arm in `transform_expr_to_flat` has two paths matching
260// `transform_expr`'s arm:
261// 1. `unsafe { nil }` normalises to a plain `nil` Ident (detected via
262// `t.is_unsafe_nil_expr(expr)`) — emitted as a leaf Ident through
263// `out.emit_expr(...)`. Identity in `transform_expr`'s else case.
264// 2. otherwise transform `expr.stmts` via `transform_stmts` and emit
265// each result via `out.emit_stmt(...)`, then assemble the flat node
266// via the new `emit_unsafe_expr_by_ids` builder helper. Mirrors
267// `add_expr(UnsafeExpr)` encoding exactly (edges are body stmts in
268// order). Skips the `ast.UnsafeExpr` struct allocation per non-nil
269// occurrence.
270// The body stmt-list is still materialised because `transform_stmts`
271// returns one (the stmt-list expansion seam is the next big port
272// target). New `fixture_unsafe_expr` (a `mut p := unsafe { &int(0) }`
273// with a non-trivial body + plain `unsafe { nil }` for the
274// normalisation arm) covers both paths across all 5 harness rows —
275// 91 → 96 transformer-diff tests.
276//
277// Session 15 (2026-05-26): LockExpr expr direct-emit + harness fixture.
278// The LockExpr arm in `transform_expr_to_flat` rewrites
279// `lock x { body }` / `rlock x { body }` into a sequence of mutex
280// lock/unlock calls wrapped in an UnsafeExpr (compound expression).
281// `expand_lock_expr` produces `[lock_calls..., body..., unlock_calls...]`;
282// the legacy arm duplicates the last body stmt after the unlock calls so
283// that GCC compound-expression value semantics return the body's tail
284// value instead of `void`. Direct-emit mirrors the rewrite exactly —
285// same `expand_lock_expr` call, same duplicate-last fix-up, then emits
286// each stmt via `out.emit_stmt(...)` and assembles via the existing
287// `emit_unsafe_expr_by_ids` builder helper (the same helper UnsafeExpr
288// session 14 uses, since the legacy LockExpr arm produces an UnsafeExpr).
289// Crucial encoding detail: the legacy `ast.UnsafeExpr{stmts: stmts}` is
290// constructed with no `pos` field (defaults to zero), so direct-emit must
291// pass `token.Pos{}` (not `expr.pos`). First port to import `v2.token`
292// in `flat_write.v` — needed for the zero-pos literal. New
293// `fixture_lock_expr` (a `Counter` struct with `mut value int` + a
294// `bump(mut c Counter) int` that uses `lock c { c.value += 1; c.value }`)
295// covers the arm across all 5 harness rows — 96 → 101 transformer-diff
296// tests.
297//
298// Session 16 (2026-05-26): OrExpr expr direct-emit + harness fixture.
299// The OrExpr arm in `transform_expr_to_flat` direct-emits the expression-
300// context or-block (the statement-level sites have dedicated handlers and
301// never reach here). `expand_single_or_expr` returns the data-access
302// expression that stands in for the or-block value and appends prefix
303// stmts (typed temp init, optional-state branch). When `prefix_stmts` is
304// non-empty, the legacy arm wraps everything in an `UnsafeExpr` (GCC
305// compound expression) — direct-emit mirrors that exactly via
306// `emit_unsafe_expr_by_ids` (same helper LockExpr session 15 uses),
307// passing `token.Pos{}` since the legacy wrapper has no explicit pos.
308// When empty, the result expression goes through `out.emit_expr(...)`
309// (leaf). Skips the `ast.UnsafeExpr` wrapper allocation per occurrence
310// in the compound-expression case. New `fixture_or_expr` (a fn
311// returning `pair(maybe(n) or { -1 }, 2)` — or-block nested in call
312// args) covers the arm across all 5 harness rows — 101 → 106
313// transformer-diff tests.
314//
315// Session 17 (2026-05-26): IfGuardExpr expr direct-emit (degenerate arm).
316// IfGuardExpr only appears in normal usage as an IfExpr condition, where
317// `transform_if_expr` handles it (statement form via
318// `try_expand_if_guard_stmt`, expression form via the expr.v ~line 1371
319// site). The arm in `transform_expr_to_flat` is the degenerate standalone-
320// reach case: legacy `transform_expr` returns
321// `t.transform_expr(expr.stmt.rhs[0])` when `rhs` is non-empty, otherwise
322// returns `expr` unchanged. Direct-emit mirrors that — recursive
323// `transform_expr_to_flat` for the rhs[0] case, leaf `out.emit_expr(...)`
324// for the fallback. No new fixture: IfGuardExpr is unreachable in
325// standalone position in practice, so no diff-harness fixture exercises
326// it; the port is a completeness sweep that keeps the dispatch surface
327// coherent with the legacy `transform_expr` shape. All 106 existing
328// transformer-diff tests continue to pass.
329//
330// Session 18 (2026-05-26): SelectorExpr deep helper port + fixture.
331// First "deep helper rewrite" port (per the planning conversation that
332// followed session 17). The full body of `transform_selector_expr`
333// (~130 lines in expr.v) is mirrored as `transform_selector_expr_to_flat`
334// in this file. Special-case branches that rewrite the selector into a
335// non-selector shape — typeof.name/idx (3 sub-branches: KeywordOperator,
336// CallExpr, CallOrCastExpr lhs), sumtype representation fields
337// (`_tag` / `_data` / `_*` chained under `_data`), smartcast match
338// (direct + field-access), `os.args` → `arguments()`, module-qualified
339// enum (`mod.Type.value` → `mod__Type__value` Ident or nested module
340// refs), and same-module enum (`Op.value` → `mod__Op__value` Ident) —
341// all build the legacy `ast.Expr` and emit via `out.emit_expr(...)`
342// (their results are Idents, BasicLiterals, or CallExprs that route
343// through `add_expr`'s normal encoding). Only the default `x.field`
344// path is direct-emit: recurse on lhs via `transform_expr_to_flat`,
345// emit rhs Ident via the leaf `out.emit_expr(...)`, and assemble via
346// the new `emit_selector_expr_by_ids` builder helper, skipping the
347// `ast.SelectorExpr` struct allocation per default-path occurrence.
348//
349// Adds `import v2.types` to this file (needed for the `typ is types.Enum`
350// checks in the module-qualified / same-module enum branches). New
351// `fixture_selector_expr` exercises the default path with chained
352// selectors (`b.p.x`, `b.p.y`), non-Ident lhs (`arr[0].y` — IndexExpr),
353// and a selector-LHS assignment (`b.p.y = 42`) — 106 → 111 transformer-
354// diff tests.
355//
356// Pattern note: this is the first port where the dispatch arm delegates
357// to a dedicated `transform_*_to_flat` helper instead of inlining the
358// logic in the match arm itself. Established because the legacy helper
359// has too many branches to inline (~130 lines of special-case
360// dispatch + lookup). Subsequent deep-helper ports (CallExpr, IfExpr,
361// InfixExpr, ...) will follow the same shape: one helper per legacy
362// `transform_*` with the same structure, special-case branches routed
363// through `emit_expr` for their non-default Expr results, default-path
364// direct-emit via new `emit_*_by_ids` builder helpers.
365//
366// Session 19 (2026-05-26): IndexExpr deep helper port + fixture.
367// Second deep-helper rewrite, same shape as session 18. The body of
368// `transform_index_expr` (~140 lines in expr.v) is mirrored as
369// `transform_index_expr_to_flat`. Two branches produce non-IndexExpr
370// shapes and route through the legacy round-trip:
371// (a) Slice lowering (`expr.expr is ast.RangeExpr`) →
372// `transform_slice_index_expr` produces a CallExpr to
373// `array_slice` / `string_substr`.
374// (b) Map index on a non-eval backend → `map__get` lowering produces
375// an `ast.UnsafeExpr` wrapping prefix temps + cast + deref.
376// Map detection mirrors the legacy sequence exactly: `map_index_lhs_type`
377// → fallback to `lookup_var_type` for Ident lhs → `unwrap_map_type`.
378// All three lookups are immutable `&Transformer` calls so running them
379// up-front to decide dispatch is side-effect-free. Eval-backend map
380// case rebuilds an IndexExpr verbatim — falls through to direct-emit.
381//
382// The gated path (`arr#[i]`), eval-backend map path, and default
383// `arr[i]` path all rebuild an IndexExpr with `lhs` and `expr`
384// recursively transformed and `is_gated` copied verbatim — direct-emit
385// via the new `emit_index_expr_by_ids` builder helper (`is_gated` flag
386// packed into the flags byte to match `add_expr(IndexExpr)` encoding
387// exactly). Skips the `ast.IndexExpr` struct allocation on the common
388// path.
389//
390// New `fixture_index_expr` exercises all four paths in one fn: default
391// `arr[0] + arr[1]`, gated `arr#[0]`, map index `m['key']`, and slice
392// `arr[1..3].len` — 111 → 116 transformer-diff tests.
393//
394// Session 20 (2026-05-26): ComptimeExpr deep helper port + fixture.
395// Third deep-helper rewrite, same shape as sessions 18/19. The body of
396// `transform_comptime_expr` (~50 lines in expr.v) is mirrored as
397// `transform_comptime_expr_to_flat`. Most branches lower the comptime
398// form into a non-ComptimeExpr shape — eval_comptime_if produces an
399// IfExpr result; `$res(...)` (both CallExpr and CallOrCastExpr lhs)
400// becomes a BasicLiteral `false`; `$embed_file(...)` (both forms)
401// becomes an InitExpr or chained CallExpr via
402// `transform_embed_file_comptime_expr`; `@VMODROOT`/`VMODROOT` Ident
403// becomes a StringLiteral via `vmodroot_string_literal`; the
404// `transform_embed_file_comptime_chain` rescue produces a non-comptime
405// shape — all six branches route through `out.emit_expr(...)`. Only
406// the default `ast.ComptimeExpr{expr: transform(inner), pos}` path is
407// direct-emit via the new `emit_comptime_expr_by_id` builder helper.
408// Skips the `ast.ComptimeExpr` struct allocation on the rare
409// default-path occurrence.
410//
411// New `fixture_comptime_expr` exercises a `$if linux { return 1 }`
412// stmt body (the IfExpr branch — legacy fallback) — 116 → 121
413// transformer-diff tests.
414//
415// Session 21 (2026-05-26): InitExpr deep helper port + fixture.
416// Fourth deep-helper rewrite, same shape as sessions 18-20. The body of
417// `transform_init_expr` (~220 lines in struct.v) is large and complex —
418// the per-field transformation involves sumtype wrapping,
419// ArrayInitExpr special-casing, expected-type resolution, and
420// missing-default fill-in via `add_missing_struct_field_defaults`. Only
421// one branch produces a non-InitExpr shape: the typed empty map
422// `map[K]V{}` lowers to a CallExpr to `new_map` (or `MapInitExpr` on the
423// eval backend) — that branch routes through `out.emit_expr(...)`. The
424// default path delegates the field work to legacy
425// `transform_init_expr` (its result is an `ast.InitExpr` with the same
426// `typ` and a fully-transformed fields list) and then direct-emits via
427// the new `emit_init_expr_by_ids` builder helper together with
428// `emit_field_init_by_id` per field. This skips the outer
429// `ast.InitExpr` wrapper allocation per occurrence; the per-field
430// `ast.FieldInit` wrappers still materialise on the default path (the
431// legacy function builds them). A later session can inline the
432// per-field loop directly to also skip those — that's a much bigger
433// port and not required to close this dispatch arm.
434//
435// New `fixture_init_expr` exercises two struct types and a nested
436// struct literal with both fully-specified fields and a field with a
437// declared default (`tag string = "default"`) — the missing-default
438// fill-in path. 121 → 126 transformer-diff tests.
439//
440// Session 22 (2026-05-26): ReturnStmt port + fixture.
441// First stmt-level port since session 3 (FnDecl). `transform_return_stmt`
442// (~110 lines in transformer.v) always returns an `ast.ReturnStmt`
443// (never lowers to a different shape). Its body does per-expression
444// sumtype wrapping (`should_wrap_return_sumtype`), enum-shorthand
445// resolution (`resolve_enum_shorthand`), smartcast handling (Ident
446// re-wrap, var-type sumtype shortcut), and native-backend pre-wrap
447// (`wrap_sumtype_value` for sumtype-returning fns). Too much logic to
448// inline for a single session. Wrap-only port: call legacy
449// `transform_return_stmt`, decompose the resulting ReturnStmt, emit
450// each transformed expr via `out.emit_expr(...)` (already transformed)
451// and assemble via the new `emit_return_stmt_by_ids` helper. Skips the
452// outer `ast.ReturnStmt` wrapper allocation per occurrence; the
453// `[]ast.Expr` list still materialises (legacy builds it).
454//
455// New `fixture_return_stmt` covers three return shapes: multi-value
456// return (`return 1, 2`), sumtype-wrapping return
457// (`return Circle2{r: r}` in a `Shape2`-returning fn), and smartcast
458// return (`if s is Circle2 { return s }` — Ident re-wrap path).
459// 126 → 131 transformer-diff tests.
460//
461// Session 23 (2026-05-26): Ident expr direct-emit + fixture.
462// The Ident arm in `transform_expr_to_flat` direct-emits via the leaf
463// `out.emit_expr(ast.Expr(expr))` for the default path (Ident is
464// identity in `transform_expr`'s arm — same shape returned). Two
465// state-dependent rewrite branches fall back to legacy:
466// 1. `@VMODROOT` → `vmodroot_string_literal(expr.pos)` produces a
467// StringLiteral (different shape).
468// 2. `find_smartcast_for_expr(expr.name)` hit → smartcast lowering
469// via `apply_smartcast_direct_ctx` produces a direct sum payload
470// access (different shape).
471// Both gate checks use the immutable `&Transformer` receiver, safe to
472// probe upfront. Skips the `transform_expr` dispatch call per Ident
473// occurrence reached through a ported ancestor (ParenExpr, PrefixExpr,
474// ModifierExpr, CastExpr, AsCastExpr, FieldInit, LambdaExpr, IndexExpr,
475// SelectorExpr, ComptimeExpr, InitExpr default fields, etc.). No
476// struct allocation savings since the legacy default-path arm also
477// returns the input Ident verbatim — the win is dispatch-skip on the
478// hottest expression variant.
479//
480// New `fixture_ident` covers file-scope Ident references nested inside
481// already-ported expr arms: `const neg_base = -base` (Ident inside
482// PrefixExpr arm), `const wrapped_base = (base)` (Ident inside
483// ParenExpr arm), `const inv_base = ~base` (Ident inside PrefixExpr
484// `~` arm), plus mut-param + multiple Ident references in a fn body
485// (Idents in InfixExpr operands — covered by the legacy InfixExpr
486// fallback path). 131 → 136 transformer-diff tests.
487//
488// Pattern note: first port where the saving is dispatch-skip rather
489// than struct-allocation skip (since Ident's default `transform_expr`
490// arm is pure identity, no allocation). Future leaf-identity arms
491// (KeywordOperator default, etc.) follow the same pattern: gate
492// state-dependent special branches via immutable lookups, direct-emit
493// the identity path.
494//
495// Session 24 (2026-05-26): KeywordOperator expr direct-emit + fixture.
496// KeywordOperator covers the comptime/macro-style call shapes
497// (`typeof(x)`, `sizeof(T)`, `isreftype(T)`, `dump(x)`, `likely(x)` /
498// `unlikely(x)`, `offsetof(T, f)`, `go ...` / `spawn ...`). The arm
499// in `transform_expr_to_flat` follows the session 23 (Ident) pattern:
500// a leaf-identity default path direct-emits via
501// `out.emit_expr(ast.Expr(expr))` and two gated state-dependent
502// special branches fall back to the legacy round-trip:
503// 1. `key_typeof` with `exprs.len > 0` → `resolve_typeof_expr`
504// produces a StringLiteral when the name is non-empty.
505// 2. `key_go` with `exprs.len > 0` → `lower_go_call(expr)` lowers
506// the spawn into a CallExpr to the generated goroutine wrapper.
507// All other ops (`sizeof`, `isreftype`, `dump`, `likely`, `unlikely`,
508// `offsetof`, `spawn`, also `typeof` when `resolve_typeof_expr`
509// returns empty) fall through to identity. Gate is the cheap
510// immutable `expr.op == ...` check — no side effects, no Transformer
511// state access. Same dispatch-skip win as session 23: skip the
512// `transform_expr` function call + match dispatch per occurrence
513// reached through a ported ancestor or a file-scope const value
514// (e.g. `const sz = sizeof(int)`).
515//
516// New `fixture_keyword_operator` covers file-scope KeywordOperators
517// in ConstDecl values: `const sz_int = sizeof(int)`,
518// `const sz_bool = sizeof(bool)`, `const ref_int = isreftype(int)`
519// — all hit the default identity branch since the legacy arm only
520// specially handles `key_typeof` and `key_go`. 136 → 141
521// transformer-diff tests.
522//
523// Session 25 (2026-05-26): GenericArgs expr direct-emit (completeness sweep).
524// GenericArgs covers the type-parameter shape `lhs[T]` (`typeof[int]`,
525// `MyGenericFn[string]`, ...). The `transform_expr` arm has three
526// branches: (1) `is_typeof_generic_args(expr)` → identity; (2)
527// `args.len == 1` + non-callable lhs type → `transform_index_expr`
528// lowers into an IndexExpr (different shape); (3) default →
529// `specialize_generic_callable_expr` lowers into an Ident or CallExpr
530// (different shape). The arm in `transform_expr_to_flat` follows the
531// session 23/24 pattern — gate the identity branch via the immutable
532// `is_typeof_generic_args` lookup (a `&Transformer` name check, no
533// state mutation) and direct-emit via leaf
534// `out.emit_expr(ast.Expr(expr))`; all other paths fall back to the
535// legacy round-trip.
536//
537// Reachability is limited in practice: GenericArgs typically appears
538// as a CallExpr lhs (`typeof[int]()`, `myfn[T](x)`), and CallExpr is
539// unported — so the arm fires only through ported-ancestor recursion
540// (ParenExpr / SelectorExpr deep helper, etc.). Same shape as the
541// session 17 IfGuardExpr completeness sweep: no new fixture, keeps the
542// dispatch surface coherent with the legacy `transform_expr` shape.
543// All 141 existing transformer-diff tests continue to pass.
544//
545// Session 26 (2026-05-26): StringInterLiteral deep helper port + fixture.
546// StringInterLiteral covers V's `'... ${x:d} ...'` interpolation syntax.
547// The legacy `transform_string_inter_literal` always returns a
548// StringInterLiteral (no branch produces a different shape) — it walks
549// each `ast.StringInter`, (a) optionally hoists a smartcasted payload via
550// `transform_expr` + `hoist_expr_to_temp` (mutates `t.pending_stmts`),
551// (b) wraps the inter expression via `transform_sprintf_arg` (type-aware
552// pointer-with-str / string-keeping logic), then (c) fills in the
553// resolved sprintf format via `resolve_sprintf_format`. The legacy body
554// allocates: one `ast.StringInterLiteral` wrapper, one `[]ast.StringInter`
555// list, and one `ast.StringInter` struct per inter.
556//
557// `transform_string_inter_literal_to_flat` mirrors the per-inter loop
558// and emits each StringInter via `emit_string_inter_by_ids(format, width,
559// precision, expr_id, format_expr_id, resolved_fmt)` and the outer
560// literal via `emit_string_inter_literal_by_ids(kind, values, inter_ids,
561// token.Pos{})`. The smartcast/hoist mutations still happen identically
562// (we call the same legacy helpers); only the post-build allocations are
563// skipped. Note: legacy omits `pos` on the rebuilt literal (defaults to
564// `token.Pos{}`) — the port passes `token.Pos{}` explicitly for parity.
565// Same deep-port pattern as sessions 18 (SelectorExpr), 19 (IndexExpr),
566// 20 (ComptimeExpr), 21 (InitExpr).
567//
568// No new fixture in this session — existing fixtures already cover
569// StringInterLiteral nodes reached via ported ancestors (e.g.,
570// `'hi ${name}'` inside a ParenExpr / SelectorExpr / ConstDecl value).
571// All 141 transformer-diff tests continue to pass.
572//
573// Session 27 (2026-05-26): ExprStmt port (stmt-level dispatch-skip).
574// First stmt-level identity-shape port. `transform_stmt`'s `ast.ExprStmt`
575// arm is identity-shape: it wraps the transformed inner expression in a
576// new `ast.ExprStmt`, with one state mutation — when `stmt.expr is
577// ast.IfExpr`, the arm flips `t.skip_if_value_lowering = true` around the
578// recursive `transform_expr` call so `transform_if_expr` knows the IfExpr
579// is in statement position (not value position) and skips temp-variable
580// lowering, then restores the prior flag.
581//
582// The flat-write port mirrors the flag flip around a recursive
583// `transform_expr_to_flat` call (so any deep-helper-ported expr arm
584// reached inside also benefits from the flat path — Ident, SelectorExpr,
585// IndexExpr, InitExpr, ComptimeExpr, StringInterLiteral, ...), then
586// assembles the stmt_expr node via the new `emit_expr_stmt_by_id` helper.
587// Skips the `ast.ExprStmt` wrapper struct allocation and the legacy
588// `out.emit_stmt(t.transform_stmt(stmt))` round-trip's match dispatch
589// (both on `transform_stmt` and on `add_stmt`) on every expression
590// statement.
591//
592// Reachability is excellent: ExprStmt is the most common statement form
593// after AssignStmt — every function body has them, every block has them,
594// every call as a top-level statement is an ExprStmt. No new fixture
595// this session — every existing fixture exercises ExprStmt via fn bodies
596// and asserts. All 141 transformer-diff tests continue to pass.
597//
598// Pattern note: first port that recursively enters `transform_expr_to_flat`
599// from the stmt-level dispatch, propagating the flat-port savings into
600// all statement-position expressions (not just nested expressions inside
601// already-ported ConstDecl/GlobalDecl/ReturnStmt values).
602//
603// Session 28 (2026-05-26): LabelStmt port (stmt-level recursive descent).
604// Second stmt-level identity-shape port after session 27 (ExprStmt).
605// `transform_stmt`'s LabelStmt arm always returns `LabelStmt{name:
606// stmt.name, stmt: transform_stmt(stmt.stmt)}` — `name` is verbatim,
607// single inner stmt recursively transformed. Direct-emit recurses via
608// `transform_stmt_to_flat` for the inner stmt (so any ported stmt arm
609// reached inside the label — ExprStmt, ReturnStmt, ForStmt, etc. — also
610// stays on the flat path) and assembles via the new
611// `emit_label_stmt_by_id(name, stmt_id)` helper. Mirrors
612// `add_stmt(LabelStmt)` encoding exactly (pos = `token.Pos{}`, interned
613// name, single edge to inner stmt). Skips the `ast.LabelStmt` wrapper
614// struct allocation and the `transform_stmt`/`add_stmt` match-dispatch
615// round-trip per occurrence.
616//
617// Reachability is limited to source-level `label: stmt` syntax
618// (typically `outer:` before loops for labelled break/continue), but the
619// port is clean — single child stmt and a verbatim name. No new fixture
620// this session — existing fixtures don't exercise LabelStmt directly,
621// but the port keeps the stmt-dispatch surface coherent. All 141
622// transformer-diff tests continue to pass.
623//
624// Pattern note: first stmt port that recursively enters
625// `transform_stmt_to_flat` itself (sibling of session 27's recursion into
626// `transform_expr_to_flat`), establishing the stmt-level recursive descent
627// pattern. Future stmt-level identity-shape ports (BlockStmt, DeferStmt,
628// ComptimeStmt $for branch) follow the same template — recurse via
629// `transform_stmt_to_flat` for child stmts, then emit via a new
630// `emit_*_by_ids` helper mirroring the matching `add_stmt` arm encoding.
631//
632// Session 29 (2026-05-26): BlockStmt port (wrap-only stmt port).
633// Third stmt-level identity-shape port after sessions 27 (ExprStmt) and
634// 28 (LabelStmt). `transform_stmt`'s BlockStmt arm always returns
635// `BlockStmt{stmts: transform_stmts(stmt.stmts)}` — identity-shape with
636// the body driver `transform_stmts` handling sibling-stmt smartcast
637// state snapshot/restore, pending-stmt hoisting, and one-to-many
638// expansion. Because the driver does extra inter-stmt work (and may
639// expand one input stmt into several outputs), we can't recurse via
640// `transform_stmt_to_flat` per-element without porting the driver
641// itself — that's a separate (bigger) refactor.
642//
643// Wrap-only port: call legacy `transform_stmts(stmt.stmts)` to
644// materialise the transformed `[]ast.Stmt`, emit each via the legacy
645// `out.emit_stmt(...)` (the stmts are already transformed), and
646// assemble via the new `emit_block_stmt_by_ids` helper. Mirrors
647// `add_stmt(BlockStmt)` encoding exactly (pos = `token.Pos{}`, edges =
648// inner stmts in order). Skips the outer `ast.BlockStmt` wrapper struct
649// allocation per occurrence; the inner `[]ast.Stmt` from
650// `transform_stmts` still materialises (until the body driver is
651// ported).
652//
653// Reachability is good — BlockStmt appears as a free-standing `{ ... }`
654// block in source. Most fn bodies don't directly use them (the FnDecl
655// arm calls `transform_stmts` itself), but `unsafe { ... }` and lambda
656// bodies in some shapes wrap their contents in BlockStmt. No new
657// fixture this session. All 141 transformer-diff tests continue to
658// pass.
659//
660// Pattern note: first stmt port that takes the "wrap-only" shape
661// (sessions 9 FnLiteral, 14 UnsafeExpr, 22 ReturnStmt established this
662// pattern at the expr level). The body driver port (which would let
663// BlockStmt / DeferStmt / FnLiteral.stmts / UnsafeExpr.stmts all stream
664// directly into the builder instead of materialising `[]ast.Stmt`) is
665// the next significant target.
666//
667// Session 55 (2026-05-27): InitExpr typed-empty-map direct-emit
668// refactor (seventh rewrite-site port — extends the post-routing-
669// pass mini-sweep to the InitExpr arm). `transform_init_expr_to_flat`
670// previously leaf-encoded the typed empty map branch
671// (`map[K]V{}`, `expr.fields.len == 0` + `MapType` typ) via
672// `out.emit_expr(t.transform_expr(ast.Expr(expr)))`. Session 55
673// direct-calls `transform_init_expr` (skipping the outer
674// `transform_expr` dispatch) and dispatches on result shape —
675// `ast.MapInitExpr` (eval backend) direct-emits via session 40's
676// `emit_map_init_expr_by_ids`; `ast.CallExpr` (non-eval, returns
677// a `new_map(sizeof(K), sizeof(V), &hash, &eq, &clone, &free)`
678// call) direct-emits via session 44's `emit_call_expr_by_ids`;
679// anything else falls through to leaf-emit.
680//
681// Safety: `transform_init_expr` is the helper `transform_expr`'s
682// InitExpr arm calls. The MapInitExpr branch's children
683// (`typ`/`keys`/`vals`) are leaves built locally by the helper
684// (`MapInitExpr{typ: ast.Expr(ast.Type(expr.typ)), keys: [], vals:
685// [], pos}`); the CallExpr branch's args are also leaves built
686// locally (`KeywordOperator{op: .key_sizeof}` + Ident-wrapped
687// `&hash`/`&eq`/... fns) — no re-transform needed, leaf
688// `out.emit_expr` is correct.
689//
690// Reachability: `map[K]V{}` is the canonical empty-map literal
691// and appears in every map-using V module. The dispatch-skip
692// win applies on every typed empty map occurrence; the
693// wrapper-allocation skip applies on every direct-emit path
694// (no longer build an `ast.InitExpr` wrapper before dispatch).
695// No new helper, no new fixture. All 168 transformer-diff tests
696// continue to pass.
697//
698// Session 54 (2026-05-27): CastExpr sum-type lowering direct-emit
699// refactor (sixth rewrite-site port — extends the post-routing-
700// pass mini-sweep to the CastExpr arm). `transform_expr_to_flat`'s
701// CastExpr arm previously gated the sum-type lowering branch and
702// leaf-encoded via `out.emit_expr(t.transform_expr(expr))`,
703// re-entering the legacy dispatch only to reach
704// `wrap_sumtype_value`. Session 54 direct-calls
705// `wrap_sumtype_value(expr.expr, sumtype_name)` and leaf-emits
706// the wrapped result when it returns Some; when it returns none
707// (value already is the sum type, declared receiver matches,
708// generic-param guard, etc.), falls through to the existing
709// default direct-emit path (CastExpr rebuild via
710// `emit_cast_expr_by_ids`).
711//
712// Safety: `wrap_sumtype_value` is the same helper
713// `transform_expr`'s CastExpr arm calls; calling it directly
714// skips one dispatch hop. The wrapped result (CastExpr/InitExpr/
715// CallExpr depending on variant shape — see `build_sumtype_init`)
716// is leaf-encoded via `out.emit_expr`: the result is already
717// fully-formed by the helper (it transforms the inner value
718// before wrapping at types.v:2145), so re-entering
719// `transform_expr_to_flat` would re-apply transforms — same
720// "do not route through dispatch" hazard as sessions 47-51.
721//
722// Reachability: sum-type casts (`MySum(variant_value)`) appear
723// in V code wherever a sum-type variant is materialised from an
724// untyped expression — common in pattern-match arms, ast
725// construction, etc. The dispatch-skip win applies on every
726// sum-type cast occurrence. No new helper, no new fixture. All
727// 168 transformer-diff tests continue to pass.
728//
729// Session 53 (2026-05-27): GenericArgs branch-2/3 direct-emit
730// refactor (fifth rewrite-site port — extends the
731// post-routing-pass mini-sweep to the GenericArgs arm).
732// `transform_expr_to_flat`'s GenericArgs arm previously gated
733// the identity branch (typeof[T]) and leaf-encoded the other two
734// branches via `out.emit_expr(t.transform_expr(expr))`. Session
735// 53 direct-handles each branch: branch 2 (single-arg,
736// non-callable lhs) cross-arm routes to
737// `transform_index_expr_to_flat` (same template as
738// GenericArgOrIndexExpr in session 33), branch 3 direct-calls
739// `specialize_generic_callable_expr` (returns Ident or
740// SelectorExpr — both identity in `transform_expr`) and
741// leaf-emits the result.
742//
743// Safety: `is_typeof_generic_args` / `get_expr_type` /
744// `is_callable_type` are immutable lookups. The IndexExpr
745// re-entry mirrors the legacy disambiguation exactly. The
746// branch-3 result (Ident/SelectorExpr) is leaf-encoded — both
747// shapes are identity in `transform_expr`, so re-entering
748// `transform_expr_to_flat` would only add a dispatch hop with
749// no rewrite.
750//
751// Reachability is limited (GenericArgs typically appears as a
752// CallExpr lhs which is unported — the arm fires through
753// ported-ancestor recursion: ParenExpr, SelectorExpr deep
754// helper, etc.), but the port removes the last legacy round-trip
755// from the arm. No new helper, no new fixture. All 168
756// transformer-diff tests continue to pass.
757//
758// Session 52 (2026-05-27): Ident @VMODROOT / smartcast branch
759// direct-emit refactor (fourth rewrite-site port — extends the
760// post-routing-pass mini-sweep to the Ident arm).
761// `transform_expr_to_flat`'s Ident arm previously leaf-encoded
762// both rewrite branches via `out.emit_expr(t.transform_expr(expr))`,
763// re-entering the legacy dispatch only to reach the two distinct
764// rewrites. Session 52 direct-calls each helper instead:
765// `@VMODROOT` → `vmodroot_string_literal(expr.pos)` (immutable,
766// returns StringLiteral) → leaf-emit; smartcast hit →
767// `apply_smartcast_direct_ctx(expr, ctx)` (the same helper
768// `transform_expr` would call) → leaf-emit.
769//
770// Safety: `vmodroot_string_literal` is a pure value-resolution
771// lookup (no expression rewriting). `apply_smartcast_direct_ctx`
772// returns an already-fully-formed expression (selector chain into
773// sum payload, or smartcast-rewritten expr) — leaf-encoding via
774// `out.emit_expr` is the safe escape hatch (routing through
775// `transform_expr_to_flat` would re-enter the helper-result's arm
776// and could re-apply smartcast rewrites).
777//
778// Reachability: Ident is by far the most-frequent expr variant
779// in V code (every variable reference is an Ident). The default
780// identity path was already direct-emit since session 23; this
781// session removes the last legacy-round-trip path from the arm.
782// `@VMODROOT` is rare (build-config code only); smartcast hits
783// are common inside `if x is Type { ... }` / `match`-narrowed
784// blocks. No new helper, no new fixture. All 168 transformer-diff
785// tests continue to pass.
786//
787// Session 51 (2026-05-27): KeywordOperator typeof/go branch split
788// direct-emit refactor (third rewrite-site port — extends the
789// post-routing-pass mini-sweep to the KeywordOperator arm).
790// `transform_expr_to_flat`'s KeywordOperator arm previously gated
791// both rewrite branches behind a single `(typeof || go) && len > 0`
792// check and leaf-encoded via `out.emit_expr(t.transform_expr(expr))`,
793// re-entering the legacy dispatch only to route to the two distinct
794// rewrites. Session 51 splits the branches: the typeof branch
795// direct-calls `resolve_typeof_expr` (an immutable `&Transformer`
796// lookup) and direct-emits the resulting StringLiteral via leaf
797// `out.emit_expr` (StringLiteral is identity in `transform_expr`);
798// the go branch direct-calls `lower_go_call(expr)` and dispatches
799// on the result shape — `ast.CallExpr` direct-emits via session
800// 44's `emit_call_expr_by_ids`, the `KeywordOperator` identity
801// fallback (unresolved fn_name / non-Call inner) falls through to
802// leaf `out.emit_expr(result)`.
803//
804// Safety: `resolve_typeof_expr` is a pure name-resolution lookup
805// (no mutation, no expression rewriting). `lower_go_call`
806// internally calls `t.transform_expr` on each arg and on the
807// selector lhs (for method calls) — args/receiver in the returned
808// CallExpr are fully-transformed, so direct-emit encodes them via
809// leaf `out.emit_expr` per arg. Routing through
810// `transform_expr_to_flat`'s CallExpr arm would re-run
811// `transform_call_expr` (fn.v:112 "Do NOT route" hazard) —
812// skipped, same as sessions 47/48/49/50.
813//
814// Reachability: `typeof(expr).name` patterns are common in V
815// comptime code (introspection, error messages, generic dispatch
816// hints). `go ...` / `spawn ...` are less common but appear in
817// concurrent code. The dispatch-skip win applies on every
818// KeywordOperator occurrence with these ops; the StringLiteral
819// direct-emit (no wrapper allocation) is a small but pure win on
820// typeof hits. No new helper, no new fixture. All 168
821// transformer-diff tests continue to pass.
822//
823// Session 50 (2026-05-27): IndexExpr map-index-lowering direct-emit
824// refactor (second rewrite-site port — completes the IndexExpr
825// non-identity-shape routing sweep started in session 49).
826// `transform_index_expr_to_flat`'s map-index branch (non-eval
827// backend, lhs unwraps to a Map type) was a leaf-encode shape:
828// `out.emit_expr(t.transform_expr(ast.Expr(expr)))` re-entered
829// `transform_expr`'s IndexExpr arm → `transform_index_expr` →
830// produced an UnsafeExpr containing temp-assign stmts plus a
831// deref-of-cast-of-`map__get` call. Session 50 refactor:
832// direct-call `transform_index_expr` (skipping the outer
833// `transform_expr` match dispatch) and direct-emit the resulting
834// UnsafeExpr via `emit_unsafe_expr_by_ids` (already present in
835// `vlib/v2/ast/flat.v`; no new helper needed).
836//
837// Safety: `transform_index_expr` returns an `ast.UnsafeExpr` with
838// fully-transformed stmts (the helper calls `t.transform_expr` on
839// every child expr internally — lhs, key, default-zero, etc.).
840// Direct-emit encodes each stmt via leaf `out.emit_stmt` — no
841// re-transform. Routing through `transform_stmt_to_flat` per stmt
842// would re-run the legacy dispatch (some arms call into helpers
843// that re-transform); leaf-encoding is the safe escape hatch.
844// The detection chain (`map_index_lhs_type` / `unwrap_map_type`
845// / `is_eval_backend`) stays upfront so we only direct-call
846// `transform_index_expr` when we already know it will produce the
847// UnsafeExpr lowering — `if result is ast.UnsafeExpr` is a
848// defensive type-narrow that catches an upstream shape drift.
849//
850// Reachability: map index reads `m[key]` on non-eval backends are
851// common in compiler-internal code (registries, lookup tables,
852// name resolution). The dispatch-skip win applies on every
853// map-index occurrence on the non-eval path. Same routing
854// template as sessions 47/48/49. No new helper, no new fixture.
855// All 168 transformer-diff tests continue to pass.
856//
857// Session 49 (2026-05-27): IndexExpr slice-lowering direct-emit
858// refactor (first rewrite-site port — sibling of routing-pass
859// sessions 46-48). `transform_index_expr_to_flat`'s slice branch
860// (`expr.expr is ast.RangeExpr`) was a leaf-encode shape: it called
861// `out.emit_expr(t.transform_expr(ast.Expr(expr)))` which re-entered
862// `transform_expr`'s IndexExpr arm → `transform_index_expr` →
863// `transform_slice_index_expr`, materialising a CallExpr to
864// `string__substr` / `array__slice` / `array__slice_ni`. With session
865// 44's `emit_call_expr_by_ids` in place, the slice branch can
866// direct-call `transform_slice_index_expr` (skipping the dispatch +
867// re-entry round-trip) and direct-emit the resulting CallExpr.
868//
869// Safety: `transform_slice_index_expr` returns an `ast.CallExpr`
870// with fully-transformed lhs/args (it calls `t.transform_expr` on
871// range start/end internally and on the lhs upstream of the
872// direct-call). Direct-emit encodes via leaf `out.emit_expr` per
873// arg — no re-transform. Routing through `transform_expr_to_flat`'s
874// CallExpr arm would re-run `transform_call_expr` (the fn.v:112
875// "Do NOT route" hazard) — skipped. Same template as sessions 47
876// (CallOrCastExpr) and 48 (SqlExpr lowered).
877//
878// Reachability is good — slice syntax `a[i..j]`, `s[..n]`, `arr[k..]`
879// is common in V code. The map-index lowering branch (lower in the
880// same function, returns UnsafeExpr) stays leaf-encoded for now;
881// porting it would need a new `emit_unsafe_expr_by_id` helper. This
882// is the first rewrite-site port after the routing-pass mini-sweep
883// finished — every dispatch arm in `transform_expr_to_flat` either
884// direct-emits or routes through a helper that does. No new helper,
885// no new fixture. All 168 transformer-diff tests continue to pass.
886//
887// Session 48 (2026-05-27): SqlExpr lowered-CallExpr direct-emit
888// refactor (routing pass — completes the routing-pass mini-sweep).
889// Session 36 ported SqlExpr with two outcome shapes:
890// identity-SqlExpr direct-emits via `emit_sql_expr_by_id` (added in
891// same session); lowered branch (is_create + table lookup hit) goes
892// through `lower_sql_create_expr` → `t.transform_expr(call)` on a
893// synthesized CallExpr, leaf-encoded via `out.emit_expr(result)`.
894// With session 44's `emit_call_expr_by_ids` in place, the lowered
895// branch's CallExpr result can be direct-emitted via the helper —
896// same template as session 47.
897//
898// Safety: `lower_sql_create_expr`'s `t.transform_expr(call)` enters
899// the CallExpr arm of `transform_expr` which calls
900// `transform_call_expr` once; its result has fully-transformed args.
901// Direct-emit here encodes lhs/args via leaf `out.emit_expr` — no
902// re-transform. Routing through `transform_expr_to_flat` would
903// re-run `transform_call_expr` (the fn.v:112 "Do NOT route" hazard
904// again) — skipped.
905//
906// Reachability is limited (sql ... { create ... } blocks with a
907// looked-up table struct), but the routing keeps the SqlExpr arm
908// coherent with the CallOrCastExpr arm (session 47) and finishes
909// the routing-pass mini-sweep that sessions 45-48 ran: every
910// "always-lowers via helper" arm whose downstream shape has a
911// direct-emit helper now uses it (MatchExpr → IfExpr in session 46,
912// CallOrCastExpr → CallExpr in session 47, SqlExpr lowered →
913// CallExpr in session 48). No new helper, no new fixture. All 168
914// transformer-diff tests continue to pass.
915//
916// Session 47 (2026-05-27): CallOrCastExpr direct-emit refactor
917// (routing pass — sibling of session 46's MatchExpr refactor).
918// Session 42 ported CallOrCastExpr to call `transform_call_or_cast_expr`
919// once and leaf-encode the result via `out.emit_expr(...)`. Session
920// 44 landed `emit_call_expr_by_ids`, unlocking the downstream win:
921// the most common shape `transform_call_or_cast_expr` lowers to is
922// `ast.CallExpr` (every method-resolved call, generic-math inline,
923// filter/map expansion, flag-enum method, smartcast method, all
924// module-prefixed calls, and the unresolved fallback). The CallExpr
925// branch can now be direct-emitted via the helper instead of
926// leaf-encoded.
927//
928// Safety: every CallExpr return path in `transform_call_or_cast_expr`
929// populates `lhs` and `args` from already-transformed values
930// (`transform_call_arg_with_sumtype_check`, `transform_expr` calls
931// upstream of each return). Direct-emit encodes lhs/args via leaf
932// `out.emit_expr` — no re-transform. Going through
933// `transform_expr_to_flat`'s CallExpr arm (which would re-run
934// `transform_call_expr` on the already-lowered result) IS unsafe
935// and explicitly flagged by an "// Do NOT route through
936// transform_call_expr because it would..." comment at fn.v:112; the
937// routing skips the dispatch arm and direct-emits instead.
938//
939// The non-CallExpr shapes (CastExpr, BasicLiteral '0', and rarer
940// `lower_call_or_cast_expr` results) still route through leaf
941// `out.emit_expr(result)`. Reachability of the direct-emit win is
942// high — CallExpr is the dominant downstream shape for the
943// parser's ambiguous `T(x)`/`f(x)` form. No new helper, no new
944// fixture. All 168 transformer-diff tests continue to pass.
945//
946// Session 46 (2026-05-27): MatchExpr direct-emit refactor (routing
947// pass — sibling of session 45's IfExpr port).
948// Session 35 ported MatchExpr to call `transform_match_expr` once
949// and leaf-encode the result via `out.emit_expr(...)`. Session 45
950// landed `emit_if_expr_by_ids` for the IfExpr arm, unlocking
951// MatchExpr's downstream win: `transform_match_expr` always lowers
952// to an `ast.IfExpr` (via `lower_match_expr_to_if`), so the IfExpr
953// can be direct-emitted via the new helper instead of routed
954// through leaf `out.emit_expr` (which still allocates one extra
955// wrapper layer inside `add_expr(IfExpr)`'s `push_expr` /
956// `push_stmt` walk).
957//
958// Safety: `transform_match_expr` runs `transform_match_branch_stmts`
959// per branch and `transform_expr(expr.expr)` upstream of
960// `build_match_branch_cond`, so the IfExpr's `cond` / `stmts` /
961// `else_expr` are already transformed. Direct-emit encodes them via
962// leaf `out.emit_expr` / `out.emit_stmt` (no re-transform). Going
963// through `transform_expr_to_flat`'s IfExpr arm would re-run
964// `transform_if_expr` on the already-transformed result — would
965// double-transform body stmts, which is why this routing skips the
966// IfExpr dispatch arm entirely.
967//
968// Edge case: `lower_match_expr_to_if` returns `ast.empty_expr` when
969// `branches.len == 0` (no branches → no if). The `is ast.IfExpr`
970// guard keeps that path on the leaf `out.emit_expr(result)` exit.
971//
972// Pattern note: routing-pass template — when an "always-lowers via
973// helper" arm produces a result shape that now has a direct-emit
974// helper, switch from leaf-encoding to direct-emit (without
975// re-routing through the dispatch arm, which would double-
976// transform). Same trick applies to CallOrCastExpr (session 42 →
977// `emit_call_expr_by_ids`) and SqlExpr's lowered-CallExpr branch
978// (session 36) — both follow-up sessions. No new helper, no new
979// fixture. All 168 transformer-diff tests continue to pass.
980//
981// Session 45 (2026-05-27): IfExpr port (always-lowers via helper, two
982// outcome shapes) — completes phase 4 expression-arm port sweep.
983// `transform_expr`'s IfExpr arm is a thin wrapper around
984// `transform_if_expr` (expr.v:1268), the largest control-flow helper
985// in the transformer. Many outcome shapes:
986// - &&-chain normalisation with embedded `is` checks → recursive
987// `transform_if_expr(outer_if / inner_if)` rebuild (IfExpr).
988// - smartcast hoisting (`if x is T && rest { ... }`) → IfExpr
989// with smartcast applied to the nested then-branch.
990// - sumtype tag/_type_id rewrite for `is`/`!is` cond → IfExpr.
991// - matched smartcast / type-narrowed branches → IfExpr with
992// hoisted decl-assigns + transformed stmts/else.
993// - direct option/result unwrap (`if x is none`) → IfExpr or
994// UnsafeExpr lowering with deref + tag check.
995// - default → `IfExpr{cond: transform(cond), stmts:
996// transform_stmts(stmts), else_expr: transform(else), pos}`
997// identity-shape rebuild.
998// - value-position lowering: `if_expr_is_value(transformed_if)`
999// AND not skipped → `lower_if_expr_value(transformed_if)` hoists
1000// a temp decl-assign per branch, returning an Ident (NOT an
1001// IfExpr).
1002//
1003// Direct-emit calls `transform_if_expr` once and dispatches on result
1004// type. Identity branch → encode `cond` + `else_expr` via leaf
1005// `out.emit_expr`, each `stmt` via leaf `out.emit_stmt` (helper
1006// already transformed body via `transform_stmts` —
1007// `transform_stmt_to_flat` would double-transform and break parity
1008// for hoisted smartcast / pending_stmts ordering) + assemble via the
1009// new `emit_if_expr_by_ids(cond_id, else_id, stmt_ids, pos)` helper.
1010// Lowered branch → route through leaf `out.emit_expr(result)`.
1011//
1012// Pattern note: extends "always-lowers via helper, two outcome
1013// shapes" (sessions 36 SqlExpr, 40 MapInitExpr, 41 ArrayInitExpr,
1014// 43 InfixExpr, 44 CallExpr). Reachability is high — `if` is one of
1015// the two main control-flow constructs in V; every `if cond { ... }
1016// else { ... }` passes through this arm. The wrapper-allocation skip
1017// applies on the identity branch (statement-position ifs and ifs
1018// without value lowering — most of them in compiler-style code).
1019//
1020// Completes phase 4's expression-arm port sweep — with this landing,
1021// every non-leaf arm in `transform_expr_to_flat` has a dedicated
1022// port. The `else { return out.emit_expr(t.transform_expr(expr)) }`
1023// fallback at the bottom of the dispatch is dead code (next
1024// follow-up: remove it + the `KeywordOperator` / `GenericArgs`
1025// intra-arm fallbacks that still route through the legacy round-trip
1026// for state-dependent rewrites). MatchExpr's arm (session 35) can
1027// now chain through `transform_expr_to_flat` to pick up IfExpr's
1028// direct-emit win on the lowered result, instead of routing through
1029// leaf `out.emit_expr` — another follow-up routing pass.
1030//
1031// New pub helper: `emit_if_expr_by_ids(cond_id, else_id, stmt_ids,
1032// pos)` in `vlib/v2/ast/flat.v` mirrors `add_expr(IfExpr)` encoding
1033// (`emit_simple(.expr_if, pos, [cond, else, stmts...])`). No new
1034// fixture this session. All 168 transformer-diff tests continue to
1035// pass.
1036//
1037// Session 44 (2026-05-27): CallExpr port (always-lowers via helper,
1038// two outcome shapes).
1039// `transform_expr`'s CallExpr arm is a thin wrapper around
1040// `transform_call_expr` (fn.v:1881) — by far the largest single
1041// helper in the transformer. It lowers many call shapes into
1042// non-CallExpr results: generic-math inline, embed_file lowering,
1043// $d resolution, .filter()/.map()/.sort() expansion, flag-enum
1044// methods, array contains/index/last_index, smartcast hoisting,
1045// BasicLiteral '0' elision (for elided fns), CastExpr for bare-type
1046// calls, IfExpr / InitExpr for some intrinsics, etc. The default
1047// tail (fn.v:2603-2610) is identity-shape:
1048// `CallExpr{lhs: transform(lhs),
1049// args: [transform_call_arg_with_sumtype_check(...)],
1050// pos}` —
1051// wrapped by `lower_missing_call_args` (fills omitted optional
1052// args) and `lower_variadic_args` (collapses trailing args into a
1053// `[]T` ArrayInitExpr).
1054//
1055// Direct-emit calls `transform_call_expr` once and dispatches on
1056// result type. Identity branch → encode `lhs` + each `arg` via leaf
1057// `out.emit_expr` (helper already transformed — re-routing through
1058// `transform_expr_to_flat` would double-transform and break parity
1059// for hoisted smartcast temps + variadic lowering) + assemble via
1060// the new `emit_call_expr_by_ids(lhs_id, arg_ids, pos)` helper.
1061// Lowered branch → route through leaf `out.emit_expr(result)` so
1062// the many lowering branches stay single-sourced.
1063//
1064// Pattern note: extends "always-lowers via helper, two outcome
1065// shapes" (sessions 36 SqlExpr, 40 MapInitExpr, 41 ArrayInitExpr,
1066// 43 InfixExpr). CallExpr is the highest-traffic arm of any port in
1067// phase 4 — every function call, method call, intrinsic, and
1068// lowered-array/map operation passes through it. The dispatch-skip
1069// win applies on every occurrence; the wrapper-allocation skip
1070// applies on the default-path identity branch (the most common
1071// case for ordinary user-code calls).
1072//
1073// With CallExpr ported, the CallOrCastExpr arm (session 42) and
1074// the cascade of lowering helpers throughout the transformer that
1075// produce CallExpr results could be retargeted to chain through
1076// `transform_expr_to_flat` instead of leaf `out.emit_expr` — but
1077// that's a follow-up routing pass, not part of this session. No
1078// new fixture this session. All 168 transformer-diff tests continue
1079// to pass.
1080//
1081// New pub helper: `emit_call_expr_by_ids(lhs_id, arg_ids, pos)` in
1082// `vlib/v2/ast/flat.v` mirrors `add_expr(CallExpr)` encoding
1083// (emit_simple `.expr_call` with edges = [lhs, args...]).
1084//
1085// Session 43 (2026-05-27): InfixExpr port (always-lowers via helper,
1086// two outcome shapes).
1087// `transform_expr`'s InfixExpr arm is a thin wrapper around
1088// `transform_infix_expr` (expr.v:2124), which has many outcome shapes
1089// that collapse into "identity InfixExpr" vs "lowered different
1090// shape":
1091// - `&&`-chain smartcast: `join_and_terms(terms)` rebuilds an
1092// InfixExpr chain — identity shape.
1093// - sum-type tag comparison (`is`/`!is`/`==`/`!=` against a
1094// variant) → `make_infix_expr_at(...)` over `_tag` / `_type_id`
1095// — identity shape (InfixExpr).
1096// - string literal concat folding → StringLiteral.
1097// - string + string → CallExpr to `string__plus`.
1098// - operator overload → CallExpr to the user-defined `<op>`
1099// method.
1100// - alias arithmetic, smartcast-of-lhs/rhs, fixed-array concat,
1101// etc. → various CallExpr / IndexExpr / etc.
1102// - default → `InfixExpr{op, lhs: transform(lhs), rhs:
1103// transform(rhs), pos}` — identity shape.
1104//
1105// Direct-emit calls `transform_infix_expr` once and dispatches on
1106// result type. Identity branches → encode lhs/rhs via leaf
1107// `out.emit_expr` (helper already transformed — `transform_expr_to_flat`
1108// would double-transform) + assemble via the new
1109// `emit_infix_expr_by_ids(op, lhs_id, rhs_id, pos)` helper. Lowered
1110// branches → route through leaf `out.emit_expr(result)`.
1111//
1112// Pattern note: extends "always-lowers via helper, two outcome
1113// shapes" (sessions 36 SqlExpr, 40 MapInitExpr, 41 ArrayInitExpr).
1114// The identity branch is reached by the vast majority of arithmetic
1115// / boolean / comparison infix operations in V code — every `a + b`,
1116// `i < n`, `flag && x`, `xs == ys`, ... — so the direct-emit win is
1117// broad.
1118//
1119// Reachability is extremely high — infix is the workhorse of every
1120// function body. The dispatch-skip win applies on every occurrence;
1121// the wrapper-allocation skip applies on every identity-branch
1122// occurrence. No new fixture this session. All 168 transformer-diff
1123// tests continue to pass.
1124//
1125// New pub helper: `emit_infix_expr_by_ids(op, lhs_id, rhs_id, pos)`
1126// in `vlib/v2/ast/flat.v` mirrors `add_expr(InfixExpr)` encoding
1127// (aux1=-1, aux2=-1, meta=u16(op), edges = [lhs, rhs]).
1128//
1129// Session 42 (2026-05-27): CallOrCastExpr port (always-lowers via helper,
1130// never-identity).
1131// `transform_expr`'s CallOrCastExpr arm is a thin wrapper around
1132// `transform_call_or_cast_expr` (fn.v:3955), which virtually always
1133// lowers a CallOrCastExpr (the parser's ambiguity node for `Type(arg)`
1134// vs `func(arg)`) into a different shape: CallExpr (most paths —
1135// generic-math inline, filter/map expansion, sort lambda, flag-enum
1136// methods, array contains/index, smartcast methods, all module-
1137// prefixed and method-resolved calls, the "unresolved fallback"
1138// CallExpr at the very end), CastExpr (when lhs is a known type
1139// expression), BasicLiteral '0' (elided fn or `enum.zero()` for flag
1140// enums), or whatever shape `lower_call_or_cast_expr` /
1141// `t.transform_expr(t.transform_call_or_cast_expr(...))` recursion
1142// produces.
1143//
1144// There is no identity-shape rebuild — every code path produces a
1145// non-CallOrCastExpr. The direct-emit win is purely the dispatch
1146// skip on the outer `transform_expr` arm; the lowered result still
1147// routes through leaf `out.emit_expr(...)` (no re-transform —
1148// `transform_call_or_cast_expr` already transformed receivers and
1149// args; double-transforming would break parity for smartcast
1150// hoisting and similar mutation).
1151//
1152// Pattern note: sibling of AssocExpr (session 34), MatchExpr (35) —
1153// "always-lowers via single helper to a DIFFERENT shape".
1154// Distinct from SqlExpr / MapInitExpr / ArrayInitExpr (sessions
1155// 36/40/41) which have an identity branch worth direct-emitting.
1156// When CallExpr and InfixExpr get their own flat helpers (sessions
1157// 43-44), this arm could switch to
1158// `t.transform_expr_to_flat(result, mut out)` to chain into those
1159// helpers' direct-emit paths — but today they still hit the `else`
1160// fallback, so routing through them adds no win.
1161//
1162// Reachability: CallOrCastExpr is the parser's ambiguous
1163// `T(x)`/`f(x)` form, common for single-arg calls and casts. By
1164// transformer-exit time every CallOrCastExpr becomes one of the
1165// shapes above, so the dispatch-skip win applies broadly. No new
1166// helper, no new fixture. All 168 transformer-diff tests continue to
1167// pass.
1168//
1169// Session 41 (2026-05-27): ArrayInitExpr port (always-lowers via helper,
1170// two outcome shapes).
1171// `transform_expr`'s ArrayInitExpr arm is a thin wrapper around
1172// `transform_array_init_expr` (struct.v:291), which has many outcome
1173// shapes that collapse into "identity ArrayInitExpr" vs "lowered
1174// CallExpr/UnsafeExpr":
1175// (a) invalid data → identity (`return ast.Expr(expr)`).
1176// (b) fixed-size array (`[1, 2, 3]!`, `[5]int{}`) → identity-shape
1177// rebuild with transformed `exprs`/`init`/`cap`/`len`.
1178// (c) eval backend → identity-shape rebuild with transformed
1179// typ/init/cap/len/update_expr fields.
1180// (d) spread `[...base, e1, e2]` → CallExpr to
1181// `new_array_from_array_and_c_array(...)`.
1182// (e) empty dynamic array `[]int{}` →
1183// `__new_array_with_default_noscan(len, cap, sizeof(elem),
1184// init)` CallExpr; or a ForStmt expansion when `init`
1185// references `index`.
1186// (f) keyed dynamic array `[1, 2, 3]` → `new_array_from_c_array(...)`
1187// CallExpr.
1188//
1189// Direct-emit calls `transform_array_init_expr` once and dispatches
1190// on result type. Identity branches (a)/(b)/(c) all return an
1191// `ast.ArrayInitExpr` — direct-emit via the new
1192// `emit_array_init_expr_by_ids` helper, skipping the wrapper
1193// allocation. Children are encoded via leaf `out.emit_expr` (the
1194// helper already transformed them — `transform_expr_to_flat` would
1195// double-transform). Lowering branches (d)/(e)/(f) return a
1196// non-ArrayInitExpr shape — route through leaf
1197// `out.emit_expr(result)` so the lowering logic stays
1198// single-sourced.
1199//
1200// Pattern note: extends "always-lowers via helper, two outcome
1201// shapes" (sessions 36 SqlExpr, 40 MapInitExpr). The two-shape
1202// template handles multiple identity branches uniformly via
1203// post-helper type dispatch. Unlike MapInitExpr (only eval backend
1204// keeps the identity shape), ArrayInitExpr keeps identity on every
1205// backend for fixed-size literals — so the direct-emit win reaches
1206// every backend for that branch, not just eval.
1207//
1208// Reachability is very high — array literals (`[]`, `[1, 2, 3]`,
1209// `[]int{}`, `[5]int{init: -1}`, `[1, 2, 3]!`) are ubiquitous in V
1210// code. Most call/return-arg array literals on non-eval backends hit
1211// branches (d)/(e)/(f), but fixed-size array literals (common in
1212// lookup tables and configuration) hit (b) identity-rebuild on every
1213// backend. No new fixture this session. All 168 transformer-diff
1214// tests continue to pass.
1215//
1216// New pub helper: `emit_array_init_expr_by_ids(typ_id, init_id,
1217// cap_id, len_id, update_expr_id, expr_ids, pos)` in
1218// `vlib/v2/ast/flat.v` mirrors `add_expr(ArrayInitExpr)` encoding
1219// (`emit_simple(.expr_array_init, pos, [typ, init, cap, len,
1220// update_expr, exprs...])`).
1221//
1222// Session 40 (2026-05-27): MapInitExpr port (always-lowers via helper,
1223// two outcome shapes).
1224// `transform_expr`'s MapInitExpr arm is a thin wrapper around
1225// `transform_map_init_expr` (struct.v:1010), which has two outcome
1226// shapes:
1227// (a) eval backend → identity-shape rebuild
1228// `MapInitExpr{typ: transform(typ), keys: [transform(k)...],
1229// vals: [transform(v)...], pos}`. Each child is already
1230// transformed inside the helper.
1231// (b) non-eval backend → lowers to a CallExpr:
1232// `new_map(sizeof(K), sizeof(V), &hash, &eq, &clone, &free)` for
1233// empty literals, `new_map_init(...)` for keyed literals.
1234//
1235// Direct-emit calls `transform_map_init_expr` once and dispatches on
1236// result type: `is ast.MapInitExpr` → encode typ/keys/vals via leaf
1237// `out.emit_expr` (pure `add_expr` — `transform_expr_to_flat` would
1238// double-transform) + assemble via the new
1239// `emit_map_init_expr_by_ids(typ_id, key_ids, val_ids, pos)` helper.
1240// Otherwise (lowered CallExpr) → route through leaf
1241// `out.emit_expr(result)`.
1242//
1243// Pattern note: extends the "always-lowers via helper, two outcome
1244// shapes" template established by SqlExpr (session 36) and AssignStmt
1245// (37). Identity branch picks up the direct-emit win (skips the
1246// `ast.MapInitExpr` wrapper struct allocation + the `transform_expr`
1247// match dispatch); lowered branch single-sources the `new_map`
1248// construction logic in the legacy helper.
1249//
1250// Reachability: map literals (`{}`, `{'a': 1}`, `map[string]int{}`)
1251// are common across the compiler (registries, lookup tables, codegen
1252// state). Most v2 backend builds use the non-eval path, so the
1253// identity-branch direct-emit win applies only on the eval backend;
1254// the dispatch-skip win applies on every backend. No new fixture this
1255// session — existing fixtures exercise map literals via for-in maps,
1256// tag tables, etc. All 168 transformer-diff tests continue to pass.
1257//
1258// New pub helper: `emit_map_init_expr_by_ids(typ_id, key_ids, val_ids,
1259// pos)` in `vlib/v2/ast/flat.v` mirrors `add_expr(MapInitExpr)`
1260// encoding exactly (opcode `expr_map_init`, aux1=-1,
1261// aux2=keys.len, edges = [typ, keys..., vals...]).
1262//
1263// Session 39 (2026-05-27): ForInStmt port (always-lowers via helper,
1264// cross-arm encoding reuse).
1265// `transform_stmt`'s ForInStmt arm is a single helper call to
1266// `transform_for_in_stmt` (for.v:1115), which wraps the ForInStmt as
1267// the `init` of a synthetic ForStmt and delegates to
1268// `transform_for_stmt`. The return type is `ast.ForStmt` — a
1269// DIFFERENT shape from the input (ForInStmt → ForStmt). All the
1270// for-in lowering (RangeExpr → range loop, runes_iterator → array-
1271// with-value-type, fixed-array / dynamic-array / string / map /
1272// untyped) happens inside `transform_for_stmt` once it sees the
1273// ForInStmt in `init`.
1274//
1275// Direct-emit calls `transform_for_in_stmt` once, then encodes the
1276// already-transformed ForStmt children via leaf emitters (no
1277// re-transform via `transform_*_to_flat` — would double-transform):
1278// init/post stmts via `out.emit_stmt`, cond expr via `out.emit_expr`,
1279// body stmts via `out.emit_stmt`. Reuses the existing
1280// `emit_for_stmt_by_ids(init_id, cond_id, post_id, stmt_ids)` helper
1281// from session 38 — no new flat helper needed since the lowered shape
1282// is ForStmt.
1283//
1284// Pattern note: stmt-level sibling of AssocExpr (session 34) and
1285// MatchExpr (session 35) — the "always-lowers via single helper to a
1286// DIFFERENT shape" template. Distinct from session 38 (ForStmt,
1287// identity-shape result) because the input arm is ForInStmt but the
1288// lowered shape is ForStmt. This is also "cross-arm encoding reuse"
1289// — analogous to session 33 (GenericArgOrIndexExpr routes to
1290// `transform_index_expr_to_flat` for the next stage) but here we
1291// reuse the ENCODING helper rather than re-dispatching through
1292// `transform_stmt_to_flat`. We must NOT route through
1293// `transform_stmt_to_flat`'s ForStmt arm because that would re-call
1294// `transform_for_stmt` on the already-fully-transformed ForStmt —
1295// double work and possible smartcast double-application.
1296//
1297// Skips the `ast.ForStmt` wrapper struct allocation for the result
1298// and the outer `transform_stmt` match dispatch on every ForInStmt
1299// occurrence. Reachability is high: `for x in arr`, `for k, v in
1300// map`, `for r in s.runes()` are extremely common (every iterator in
1301// the compiler itself uses them).
1302//
1303// No new flat helper this session — reuses `emit_for_stmt_by_ids`
1304// from session 38. All 168 transformer-diff tests continue to pass.
1305//
1306// Session 38 (2026-05-27): ForStmt port (always-lowers via helper,
1307// identity-shape result).
1308// `transform_stmt`'s ForStmt arm is a single helper call to
1309// `transform_for_stmt` (for.v:488), which always returns an
1310// `ast.ForStmt` regardless of its internal lowering paths. The
1311// helper handles for-in loops by dispatching on iterable type
1312// (RangeExpr → range lowering; runes_iterator() → array-with-
1313// value-type; smartcast array/string variant → array; fixed array
1314// → fixed-array; dynamic array/string → array; native backend any
1315// → untyped; map/channel/other → ForStmt-wrapped ForInStmt; no
1316// type info → untyped) and for plain `for cond { ... }` loops,
1317// sets up smartcast contexts from `is` checks in the condition
1318// (flattened across `&&` chains), recursively transforms init/
1319// cond/post and body via `transform_stmts`, then pops smartcasts
1320// and returns the assembled ForStmt. Opens/closes a child scope
1321// around the whole transform.
1322//
1323// Direct-emit calls `transform_for_stmt` once, then encodes the
1324// already-transformed children via leaf emitters (no re-transform
1325// via `transform_*_to_flat` — would double-transform): init/post
1326// stmts via `out.emit_stmt`, cond expr via `out.emit_expr`, body
1327// stmts via `out.emit_stmt`. Assembles via the new
1328// `emit_for_stmt_by_ids(init_id, cond_id, post_id, stmt_ids)`
1329// helper, which mirrors `add_stmt(ForStmt)` encoding exactly
1330// (pos = `token.Pos{}`, edges = [init, cond, post, body...]).
1331//
1332// Pattern note: stmt-level sibling of SqlExpr branch (b)
1333// (session 36) and AssignStmt's main-path (session 37) — the
1334// "always-lowers via single helper call, identity-shape result"
1335// template. Unlike AssignStmt, there's no pre-arm guard;
1336// `transform_for_stmt` handles everything internally. The
1337// remaining stmt-side helper-driven arm is ForInStmt (which
1338// returns ForStmt — a different shape than its input, so it's a
1339// sibling of AssocExpr session 34 / MatchExpr session 35:
1340// always-lowers via single helper to a different shape).
1341//
1342// Reachability is high — `for` loops appear in nearly every
1343// non-trivial function body. Top-level ForStmts (file scope)
1344// are rare, but the arm also fires from any recursive
1345// `transform_stmt_to_flat` calls (currently ComptimeStmt's
1346// non-$for branch). No new fixture this session. All 168
1347// transformer-diff tests continue to pass.
1348//
1349// New pub helper: `emit_for_stmt_by_ids(init_id, cond_id,
1350// post_id, stmt_ids)` in `vlib/v2/ast/flat.v` (~line 577).
1351//
1352// Session 37 (2026-05-27): AssignStmt port (pre-arm guard + helper).
1353// `transform_stmt`'s AssignStmt arm has a pre-arm dispatch block
1354// (transformer.v lines 2958-2966) plus the match-arm call:
1355// 1. `try_expand_or_expr_assign(stmt)` — stub returning `none`
1356// (transformer.v:3804). No-op, skip from direct-emit too.
1357// 2. `try_transform_map_index_assign(stmt)` — may rewrite map
1358// index assigns (`m[key] = v`, `m[k] += v`, `m[k].f = v`, etc.)
1359// into an `ExprStmt(map__set(...))` or `BlockStmt` hoisting
1360// temp-decl stmts before the call. Returned shape is NOT an
1361// AssignStmt — route through leaf `out.emit_stmt(...)`.
1362// 3. else → `transform_assign_stmt(stmt)` returns an AssignStmt
1363// with `lhs`/`rhs` exprs already transformed (string `+=` →
1364// `string__plus` call, result/option payload writes, etc.).
1365// Direct-emit encodes each lhs/rhs via leaf `out.emit_expr`
1366// (pure `add_expr` — `transform_expr_to_flat` would
1367// double-transform) and assembles via the new
1368// `emit_assign_stmt_by_ids(op, lhs_ids, rhs_ids, pos)` helper.
1369//
1370// Pattern note: stmt-level sibling of SqlExpr (session 36) —
1371// "always-lowers via single helper call" with two outcome shapes
1372// (rewrite vs identity-rebuild), with an additional pre-arm guard
1373// that can short-circuit to a lowered shape. Most assigns reach the
1374// identity-rebuild path (only map-index assigns hit the rewrite),
1375// so the dispatch-skip win applies broadly. The lowered shapes from
1376// `try_transform_map_index_assign` (BlockStmt/ExprStmt) currently
1377// route through legacy `emit_stmt`; when a BlockStmt cross-arm
1378// router lands they can switch to dispatched routing.
1379//
1380// Mirrors `add_stmt(AssignStmt)` encoding exactly: aux1=-1,
1381// aux2=lhs.len (lhs/rhs boundary), meta=u16(op), no flags, edges =
1382// lhs in order followed by rhs in order. Skips the `ast.AssignStmt`
1383// wrapper struct allocation per occurrence + the `transform_stmt`
1384// match dispatch.
1385//
1386// Reachability is very high — assignments appear in nearly every
1387// function body. No new fixture this session; existing fixtures
1388// exercise AssignStmt extensively (decl-assigns, mutations, compound
1389// ops). All 168 transformer-diff tests continue to pass.
1390//
1391// New pub helper: `emit_assign_stmt_by_ids(op, lhs_ids, rhs_ids,
1392// pos)` in `vlib/v2/ast/flat.v` (~line 577).
1393//
1394// Session 36 (2026-05-27): SqlExpr port (always-lowers via helper, two
1395// outcome shapes).
1396// Third sibling at the expr level of the "always-lowers via single
1397// helper call" template (after AssocExpr session 34 and MatchExpr
1398// session 35). `transform_expr`'s SqlExpr arm is a thin wrapper
1399// around `transform_sql_expr` (orm.v), which has two outcome shapes:
1400// (a) `is_create` AND `lower_sql_create_expr` finds the table
1401// struct → lowers to a CallExpr `expr.create(table_name,
1402// table_fields)`, then transforms that CallExpr and returns it
1403// — the result is no longer a SqlExpr. Routed through leaf
1404// `out.emit_expr(...)` (the lowered CallExpr is already
1405// transformed; no re-transform needed).
1406// (b) otherwise (read query, count, or create with missing table)
1407// → identity-shape rebuild: `SqlExpr{expr:
1408// transform_expr(expr.expr), table_name, is_count, is_create,
1409// pos}`. Direct-emit encodes `result.expr` via leaf
1410// `out.emit_expr` (pure `add_expr` encoder, no re-transform —
1411// `transform_expr_to_flat` would double-transform since
1412// `transform_sql_expr` already ran `transform_expr` on it) and
1413// assembles via the new `emit_sql_expr_by_id(table_name,
1414// is_count, is_create, expr_id, pos)` helper. Skips the
1415// `ast.SqlExpr` wrapper struct allocation per occurrence.
1416//
1417// Detection: call `transform_sql_expr` once, then dispatch on the
1418// result's runtime type — `is ast.SqlExpr` → branch (b), else →
1419// branch (a). This keeps the table-lookup logic single-sourced
1420// (don't reimplement `lower_sql_create_expr` here just to detect the
1421// branch upfront) and preserves the exact "rebuild on lookup miss"
1422// semantics.
1423//
1424// Pattern note: extends the "always-lowers via single helper call"
1425// template with two outcome shapes. AssocExpr (34) lowers to a leaf
1426// Ident; MatchExpr (35) lowers to an IfExpr (awaiting flat helper);
1427// SqlExpr (36) either lowers to a CallExpr OR identity-rebuilds.
1428// The branch-on-result-type pattern lets identity-shape rebuilds
1429// pick up the direct-emit win even when the legacy helper has
1430// multiple shapes. Future candidates: arms where a single legacy
1431// helper has both a lowering path and an identity-shape rebuild
1432// path (some CallExpr/InfixExpr lowering arms fit this).
1433//
1434// Reachability is limited to `sql ... { ... }` blocks (rare in the
1435// compiler itself). No new fixture this session — existing fixtures
1436// don't exercise SqlExpr. All 168 transformer-diff tests continue
1437// to pass.
1438//
1439// New pub helper: `emit_sql_expr_by_id(table_name, is_count,
1440// is_create, expr_id, pos)` in `vlib/v2/ast/flat.v` (~line 696).
1441// Mirrors `add_expr(SqlExpr)`'s encoding exactly: opcode `expr_sql`,
1442// pos, interned table_name (aux1), `flag_is_count | flag_is_create`
1443// packed into flags, one edge to inner expr.
1444//
1445// Session 35 (2026-05-27): MatchExpr port (always-lowers via helper).
1446// Sibling of AssocExpr (session 34) at the expr level —
1447// `transform_expr`'s MatchExpr arm is a single helper call to
1448// `t.transform_match_expr(expr)`, which always lowers a `match`
1449// construct to an IfExpr:
1450// - sum-type match: each branch's smartcast is set up (variant
1451// names → tags), then the chain `if x is Variant1 { ... } else
1452// if x is Variant2 { ... } ...` is built; the smartcast stack is
1453// restored after each branch.
1454// - non-sum match: chains `x == c1`/`x in c_list` IfBranches in
1455// declaration order, with `else` becoming the final IfBranch.
1456// The lowered IfExpr is returned as `ast.Expr` (not pre-transformed
1457// through `transform_expr`); the caller's match arm wraps it via
1458// legacy `add_expr(transformed_if)` which re-enters `transform_expr`
1459// on the IfExpr.
1460//
1461// Direct-emit calls `transform_match_expr` and routes the IfExpr
1462// result through the leaf `out.emit_expr(...)` — same as the `else`
1463// fallback would, minus the `transform_expr` match dispatch on
1464// MatchExpr. No dedicated IfExpr flat helper exists yet, so routing
1465// through `transform_expr_to_flat` for the result would only add
1466// one dispatch level without benefit. When an IfExpr flat helper
1467// lands, change this arm to feed the result back through
1468// `transform_expr_to_flat` (cross-arm routing, same pattern as
1469// GenericArgOrIndexExpr → transform_index_expr_to_flat in session
1470// 33).
1471//
1472// Pattern note: same "always-lowers via single helper call" shape
1473// as AssocExpr (session 34) — both arms have a single
1474// `t.lower_*(expr, ...)` body in legacy `transform_expr`. AssocExpr
1475// lowers to a leaf Ident (smartcast context handled via pending
1476// stmts); MatchExpr lowers to an IfExpr awaiting its own flat
1477// helper. Reachability is high — `match` is one of the most common
1478// constructs in the compiler. Win is purely the dispatch skip until
1479// IfExpr gets a flat helper.
1480//
1481// No new fixture this session — existing fixtures already exercise
1482// MatchExpr extensively (sum-type matches, literal matches, range
1483// matches). All 141 transformer-diff tests continue to pass.
1484//
1485// Session 34 (2026-05-26): AssocExpr port (always-lowers via helper).
1486// `transform_expr`'s AssocExpr arm is a single helper call —
1487// `t.lower_assoc_expr(expr, false)` always lowers the struct-update
1488// syntax `{base | field: val}` into a decl-assign of a typed tmp
1489// followed by per-field assignments, all hoisted into
1490// `t.pending_stmts` (which `transform_stmts` then drains as prefix
1491// stmts before the current stmt). The arm returns the tmp Ident
1492// (or `&tmp_ident` PrefixExpr in the `take_addr` form, which is
1493// only invoked by the PrefixExpr `.amp` rewrite branch — not by
1494// this arm).
1495//
1496// Direct-emit calls `lower_assoc_expr` and routes the Ident result
1497// through the leaf `out.emit_expr(...)` — same as the `else`
1498// fallback minus the `transform_expr` match dispatch on AssocExpr.
1499// The win is purely the dispatch skip. Routing through
1500// `transform_expr_to_flat` for the result would add a dispatch
1501// level without benefit since Idents are leaf direct-emit already.
1502//
1503// Pattern note: establishes the "always-lowers via single helper
1504// call" template — sibling of GenericArgOrIndexExpr (session 33)
1505// where cross-arm routing applies because the lowered shape has a
1506// dedicated flat helper. AssocExpr's result is a leaf Ident, so
1507// direct `out.emit_expr` is the right exit. Future siblings:
1508// MatchExpr (lowers to IfExpr via `transform_match_expr`) — same
1509// shape, route through `out.emit_expr` until an IfExpr flat helper
1510// exists; SqlExpr if it has a similar single-helper structure.
1511//
1512// Reachability is moderate — struct-update syntax `{base | f: v}`
1513// is used across the compiler for AST node copying. No new fixture
1514// this session. All 141 transformer-diff tests continue to pass.
1515//
1516// Session 33 (2026-05-26): GenericArgOrIndexExpr port (cross-arm routing).
1517// `transform_expr`'s GenericArgOrIndexExpr arm disambiguates the parser
1518// ambiguity for `x[y]` via the lhs type:
1519// (a) callable lhs → `specialize_generic_callable_expr(lhs, [expr],
1520// pos)` lowers to the generic arg specialization token (Ident or
1521// CallExpr) — different shape, route through leaf
1522// `out.emit_expr(...)`.
1523// (b) otherwise → constructs an `ast.IndexExpr{lhs, expr,
1524// is_gated: false, pos}` and runs it through
1525// `transform_index_expr`. Direct-emit routes through the
1526// existing `transform_index_expr_to_flat` helper so the IndexExpr
1527// arm's default-path direct-emit (lhs + expr via
1528// `transform_expr_to_flat`, assembled via
1529// `emit_index_expr_by_ids`) also applies here.
1530//
1531// Pattern note: first port where one ported `transform_*_to_flat`
1532// helper calls another (`GenericArgOrIndexExpr` → `transform_index_
1533// expr_to_flat`). Establishes the cross-arm routing template — when
1534// an arm always lowers into a shape that already has a flat helper,
1535// forward to that helper directly instead of going through legacy
1536// `transform_expr` dispatch.
1537//
1538// `is_callable_type` is an immutable lookup (`&Transformer` receiver);
1539// `specialize_generic_callable_expr` is `mut`. Gate the mut-call
1540// branch via an upfront immutable probe — same shape as the Ident /
1541// KeywordOperator / GenericArgs ports (sessions 23-25). Reachability
1542// is limited (the parser typically unifies into CallExpr/IndexExpr at
1543// parse time), but the port removes one fallback path from the `else`
1544// branch. No new fixture this session. All 141 transformer-diff tests
1545// continue to pass.
1546//
1547// Session 32 (2026-05-26): AssertStmt port (fallback stmt-level identity).
1548// `transform_stmt`'s AssertStmt arm is the rare fallback path — most
1549// assert stmts get expanded into `if !cond { panic(...) }` by
1550// `transform_stmts` before they reach the per-stmt dispatch. The
1551// fallback rebuilds `AssertStmt{expr: t.transform_expr(stmt.expr)}`
1552// without setting `extra`, so the result always has `extra =
1553// empty_expr` (struct default) and any original `stmt.extra` (the
1554// `"assert cond, msg"` text) is DROPPED.
1555//
1556// Direct-emit mirrors that exactly: transform `stmt.expr` via the
1557// recursive `transform_expr_to_flat` (so any ported deep-helper expr
1558// arm reached inside benefits too) and route through the new
1559// `emit_assert_stmt_by_id` helper, which encodes the second edge as
1560// `add_expr(empty_expr)` — hits the shared cached `empty_expr_id` so
1561// repeat AssertStmts don't duplicate the node. Mirrors
1562// `add_stmt(AssertStmt)` encoding exactly (pos = `token.Pos{}`, two
1563// edges: expr + empty_expr). Skips the `ast.AssertStmt` wrapper
1564// struct allocation per fallback occurrence.
1565//
1566// Reachability is low but the port completes the stmt-level
1567// identity-shape coverage planned in session 28's pattern note
1568// (BlockStmt → session 29, DeferStmt → session 30, ComptimeStmt →
1569// session 31, AssertStmt → this session). No new fixture this
1570// session. All 141 transformer-diff tests continue to pass.
1571//
1572// Session 31 (2026-05-26): ComptimeStmt port (two-branch stmt port).
1573// `transform_stmt`'s ComptimeStmt arm has two branches:
1574// (a) `stmt.stmt is ast.ForStmt` (the `$for field in Type.fields { ... }`
1575// form): rebuild ComptimeStmt(ForStmt{init, cond, post, stmts:
1576// transform_stmts(for_stmt.stmts)}) — `init`/`cond`/`post` are
1577// copied verbatim (NOT transformed by the legacy arm), only the
1578// body stmts go through the body driver.
1579// (b) otherwise (`$if` as stmt, etc.): legacy returns
1580// `transform_stmt(stmt.stmt)` — the ComptimeStmt wrapper is
1581// DROPPED entirely; the result is whatever the inner stmt
1582// transforms into.
1583//
1584// Direct-emit: branch (a) synthesises a `ForStmt{init, cond, post,
1585// transformed_stmts}` and routes it through legacy `out.emit_stmt(...)`
1586// for the encoding (ForStmt itself isn't ported yet), then wraps via
1587// the new `emit_comptime_stmt_by_id` helper. Branch (b) recurses via
1588// `transform_stmt_to_flat(stmt.stmt, ...)` so any ported inner stmt
1589// (ExprStmt, ReturnStmt, ...) also stays on the flat path.
1590// Mirrors `add_stmt(ComptimeStmt)` encoding exactly (pos =
1591// `token.Pos{}`, single edge to inner stmt). Skips the outer
1592// `ast.ComptimeStmt` wrapper struct allocation per `$for` occurrence
1593// AND the wrapper entirely on the non-`$for` recursive path.
1594//
1595// Reachability is reasonable — compile-time reflection via `$for`
1596// shows up in the compiler/runtime helpers. No new fixture this
1597// session. All 141 transformer-diff tests continue to pass.
1598//
1599// Pattern note: first stmt port with TWO branches that diverge at
1600// emit time (wrap-and-build vs. drop-and-recurse). Establishes the
1601// "branch-dispatch" template for future stmt ports where the legacy
1602// arm has conditional shape changes (e.g. AssertStmt expand-in-driver
1603// vs. fallback, PostfixExpr op-based lowering at the expr level
1604// already used this shape on the expr side).
1605//
1606// Session 30 (2026-05-26): DeferStmt port (wrap-only stmt port).
1607// Sibling of session 29 (BlockStmt) — same wrap-only shape with a
1608// `mode` field. `transform_stmt`'s DeferStmt arm always returns
1609// `DeferStmt{mode: stmt.mode, stmts: transform_stmts(stmt.stmts)}` —
1610// identity-shape with the body driver `transform_stmts` doing the
1611// same inter-stmt work (smartcast snapshot/restore, pending-stmt
1612// hoisting, one-to-many expansion). Same driver constraint as the
1613// BlockStmt arm.
1614//
1615// Wrap-only port: call legacy `transform_stmts(stmt.stmts)` to
1616// materialise the transformed `[]ast.Stmt`, emit each via the legacy
1617// `out.emit_stmt(...)` (already transformed), and assemble via the
1618// new `emit_defer_stmt_by_ids(mode, stmt_ids)` helper. Mirrors
1619// `add_stmt(DeferStmt)` encoding exactly: `pos = token.Pos{}`,
1620// `flags |= flag_defer_func` when mode is `.function`, edges = inner
1621// stmts in order. Skips the outer `ast.DeferStmt` wrapper struct
1622// allocation per occurrence; the inner `[]ast.Stmt` still
1623// materialises until the body driver is ported.
1624//
1625// Reachability is limited but clean — `defer { ... }` appears in
1626// resource-cleanup paths (file close, mutex unlock, ...) and
1627// `defer (func) { ... }` (function-scope variant) in fewer places.
1628// No new fixture this session. All 141 transformer-diff tests
1629// continue to pass.
1630//
1631// Phase 5: post-pass port.
1632// The active flat-direct paths run post_pass_to_flat and the post-pass tail
1633// without materialising a transformed []ast.File. Compatibility entry points
1634// still maintain legacy files for the `.v`/eval backends.
1635//
1636// Phase 6: drop compatibility materialisation.
1637// `transform_files_to_flat` and `_via_driver` still return []ast.File for
1638// legacy consumers. Flat-codegen backends use transform_flat_to_flat_direct
1639// or the parallel flat-direct path and keep the transformed program in
1640// FlatAst only.
1641//
1642// ----- Rewrite Site Inventory (55 sites, audited 2026-05-26) -----
1643//
1644// Phase 4's checklist. Categories are coarse — within a category the
1645// rewrites share most of their helper state (e.g. all or-block expansions
1646// share `expand_*_or_expr`).
1647//
1648// Control-flow lowering (~15 sites):
1649// - if.v: if-guard expansion (assign + stmt forms, 2 sites)
1650// - for.v: for-in map / array / range / fixed-array / untyped (6 sites)
1651// - expr.v: IfGuardExpr in IfExpr, match-expression lowering (~7 sites)
1652//
1653// Or-block / Result / Option (~8 sites):
1654// - transformer.v: expand_direct_or_expr_assign, expand_single_or_expr,
1655// string-range or-block, return-or-block (4 sites)
1656// - expr.v: OrExpr in expression context, deref-or, index-or (~4 sites)
1657//
1658// Operator / method lowering (~12 sites):
1659// - expr.v: operator overload, in-operator, infix coercions, prefix lower,
1660// index/slice rewrites, comptime field access (~10 sites)
1661// - fn.v: CallExpr method-to-function rewrites, CallOrCastExpr (2 sites)
1662//
1663// Structure init (~8 sites):
1664// - struct.v: array init, indexed array init, spread, map init, struct
1665// init, default-field fill (6 sites)
1666// - fn.v: generic specialization synthetic args, FnDecl rewrites (2 sites)
1667//
1668// String operations (~3 sites):
1669// - transformer.v: string interpolation lowering, embed_file
1670// - expr.v: string range / string method dispatch
1671//
1672// Defer / lock / go (~4 sites):
1673// - transformer.v: defer lowering, lock/rlock expansion, go-wrapper
1674// - fn.v: native interface vtable elision
1675//
1676// Generic specialization (~2 sites):
1677// - fn.v: monomorphize call rewrite
1678// - transformer.v: lower_assoc_expr
1679//
1680// Misc (~3 sites):
1681// - transformer.v: assert message lowering, assign_stmt multi-return,
1682// transform_stmt dispatcher special cases
1683// - orm.v: SQL expression lowering (1 site)
1684//
1685// ----- Per-file flat-write entry point -----
1686
1687// transform_file_index_to_flat is the per-file entry point for the multi-
1688// session port. It reads one file from `input_flat` through cursors, mirrors
1689// `transform_file`'s prologue, and emits each top-level stmt through the
1690// `transform_stmt_to_flat` seam. Returns the FlatNodeId of the appended file
1691// root, or `ast.invalid_flat_node_id` for an empty / missing source file.
1692//
1693// The file-level loop uses the flat stmt-list driver rather than dispatching
1694// each stmt directly. Top-level comptime `$if` blocks can expand to multiple
1695// declarations, and codegen expects those declarations to live directly under
1696// the file root.
1697//
1698// Callers must invoke `pre_pass_from_flat(input_flat)` before the per-file
1699// loop and `post_pass(mut collected_files)` after, mirroring the wedge. The
1700// per-file API does not run those passes itself so future phases can
1701// interleave file emissions with pre/post bookkeeping without re-running it.
1702pub fn (mut t Transformer) transform_file_index_to_flat(input_flat &ast.FlatAst, fi int, mut out ast.FlatBuilder) ast.FlatNodeId {
1703 return t.transform_flat_file_index_to_flat(input_flat, fi, []ast.Stmt{}, mut out)
1704}
1705
1706// transform_file_index_with_extra_to_flat is the pub entry the parallel
1707// flat-direct transform uses: it forwards the monomorphize-generated `extra`
1708// stmts for file `fi` (from prepare_flat_for_transform) into the cursor-native
1709// per-file transform. The plain `transform_file_index_to_flat` above passes no
1710// extras (sequential generic-free callers).
1711pub fn (mut t Transformer) transform_file_index_with_extra_to_flat(input_flat &ast.FlatAst, fi int, extra_stmts []ast.Stmt, mut out ast.FlatBuilder) ast.FlatNodeId {
1712 return t.transform_flat_file_index_to_flat(input_flat, fi, extra_stmts, mut out)
1713}
1714
1715fn (mut t Transformer) transform_flat_file_index_to_flat(input_flat &ast.FlatAst, fi int, extra_stmts []ast.Stmt, mut out ast.FlatBuilder) ast.FlatNodeId {
1716 if fi < 0 || fi >= input_flat.files.len {
1717 return ast.invalid_flat_node_id
1718 }
1719 fc := input_flat.file_cursor(fi)
1720 t.cur_file_name = fc.name()
1721 t.cur_module = fc.mod()
1722 t.cur_import_aliases = flat_import_aliases_for_generic_collect(input_flat, fi)
1723 if scope := t.get_module_scope(t.cur_module) {
1724 t.scope = scope
1725 } else {
1726 t.scope = unsafe { nil }
1727 }
1728 stmt_ids := t.transform_cursor_stmts_to_flat_direct(fc.stmts(), extra_stmts, mut out)
1729 attrs_id := copy_cursor_aux_list_to_flat(fc.attrs(), mut out)
1730 imports_id := copy_cursor_aux_list_to_flat(fc.imports(), mut out)
1731 return out.append_file_with_flat_lists_and_stmt_ids(fc.name(), fc.mod(), fc.selector_names(),
1732 attrs_id, imports_id, stmt_ids)
1733}
1734
1735// transform_file_to_flat transforms one legacy file and appends the transformed
1736// tree directly to `out`. It is the AST-input counterpart to
1737// `transform_file_index_to_flat`, used by flat-output pipelines after
1738// whole-program generic preparation has produced concrete appended stmts.
1739pub fn (mut t Transformer) transform_file_to_flat(file ast.File, mut out ast.FlatBuilder) ast.FlatNodeId {
1740 // Mirror transform_file's per-file prologue. transform_stmt and the
1741 // rewrite sites read these fields to resolve cross-stmt references.
1742 t.cur_file_name = file.name
1743 t.cur_module = file.mod
1744 t.cur_import_aliases = import_aliases_for_generic_collect(file.imports)
1745 if scope := t.get_module_scope(file.mod) {
1746 t.scope = scope
1747 } else {
1748 t.scope = unsafe { nil }
1749 }
1750 // Top-level comptime `$if` blocks can expand to multiple declarations
1751 // (for example platform-specific worker structs/functions). Use the same
1752 // stmt-list driver as function bodies so selected branch stmts are spliced
1753 // into the file root instead of being hidden inside a top-level BlockStmt.
1754 stmt_ids := t.transform_stmts_to_flat_direct(file.stmts, mut out)
1755 return out.append_file_with_stmt_ids(file, stmt_ids)
1756}
1757
1758// transform_stmts_to_flat is the consolidated seam for "transform a body
1759// stmt list, then encode each result into the flat builder" — the pattern
1760// repeated at every flat-write arm that wraps a body (BlockStmt, DeferStmt,
1761// ComptimeStmt $for body, UnsafeExpr body, FnLiteral body). Currently a
1762// literal pass-through over `transform_stmts` + leaf-encode loop, kept as a
1763// single function so future sessions can replace the body with direct-emit
1764// ports for `transform_stmts`'s expansion sites (comptime $if assign, or-
1765// block assign, tuple if-assign, lock/rlock, for-in-map, assert, ...) in
1766// one place. The FnDecl arm has its own seam (`transform_fn_decl_parts_to_flat`)
1767// because the FnDecl body driver also runs `lower_defer_stmts` between
1768// `transform_stmts` and the flat encoding — porting that needs a separate
1769// thrust.
1770pub fn (mut t Transformer) transform_stmts_to_flat(stmts []ast.Stmt, mut out ast.FlatBuilder) []ast.FlatNodeId {
1771 return t.transform_stmts_to_flat_direct(stmts, mut out)
1772}
1773
1774// transform_stmts_to_flat_direct is the flat-direct mirror of
1775// `transform_stmts` (transformer.v:3072). Drives the per-stmt loop emitting
1776// `FlatNodeId`s straight into `out` instead of materialising a `[]ast.Stmt`
1777// intermediate. Mirrors every expansion site (comptime $if assign, native
1778// interface cast/sincos, or-block assign, tuple if-assign, tuple call-assign,
1779// if-guard assign, if-expr assign, $if expr-stmt, or-expr stmt, if-guard
1780// stmt, flag-enum set/clear, return-match, or-return, return-if, lock/rlock,
1781// map-index push/postfix, native selector postfix, for-in-map, assert) and
1782// the default fall-through via `append_transformed_stmt_to_flat`.
1783//
1784// Stmts produced by expansion helpers (legacy `try_expand_*` /
1785// `expand_*` helpers that still return `ast.Stmt` / `[]ast.Stmt`) are leaf-
1786// encoded via `out.emit_stmt(...)` — these helpers transform the contained
1787// exprs internally, so leaf-encoding is bit-equal to the legacy
1788// `result << expanded_X` push. The `transform_stmt(stmt)` dispatch on the
1789// default fall-through routes through `transform_stmt_to_flat`'s direct-emit
1790// arms via the appender, which skips the legacy stmt-wrapper allocation for
1791// the many already-ported variants.
1792//
1793// Saves the outer `[]ast.Stmt` allocation (one per call) and the stmt-
1794// wrapper allocations on the default path. Expansion sites still materialise
1795// their inner intermediates — those need per-helper `_to_flat` ports in
1796// future sessions to fully drop the legacy materialisation.
1797pub fn (mut t Transformer) transform_stmts_to_flat_direct(stmts []ast.Stmt, mut out ast.FlatBuilder) []ast.FlatNodeId {
1798 mut ids := []ast.FlatNodeId{cap: stmts.len}
1799 block_smartcast_depth := t.smartcast_stack.len
1800 has_smartcast_state := block_smartcast_depth > 0
1801 block_smartcast_stack := if has_smartcast_state {
1802 t.smartcast_stack.clone()
1803 } else {
1804 []SmartcastContext{}
1805 }
1806 block_smartcast_counts := if has_smartcast_state {
1807 t.smartcast_expr_counts.clone()
1808 } else {
1809 map[string]int{}
1810 }
1811 for stmt in stmts {
1812 t.restore_flat_stmt_list_smartcast_context(block_smartcast_depth, block_smartcast_stack,
1813 block_smartcast_counts)
1814 t.transform_stmt_list_item_to_flat(stmt, mut ids, mut out)
1815 }
1816 t.restore_flat_stmt_list_smartcast_context(block_smartcast_depth, block_smartcast_stack,
1817 block_smartcast_counts)
1818 return ids
1819}
1820
1821pub fn (mut t Transformer) transform_cursor_stmts_to_flat_direct(stmts ast.CursorList, extra_stmts []ast.Stmt, mut out ast.FlatBuilder) []ast.FlatNodeId {
1822 mut ids := []ast.FlatNodeId{cap: stmts.len() + extra_stmts.len}
1823 block_smartcast_depth := t.smartcast_stack.len
1824 has_smartcast_state := block_smartcast_depth > 0
1825 block_smartcast_stack := if has_smartcast_state {
1826 t.smartcast_stack.clone()
1827 } else {
1828 []SmartcastContext{}
1829 }
1830 block_smartcast_counts := if has_smartcast_state {
1831 t.smartcast_expr_counts.clone()
1832 } else {
1833 map[string]int{}
1834 }
1835 for i in 0 .. stmts.len() {
1836 t.restore_flat_stmt_list_smartcast_context(block_smartcast_depth, block_smartcast_stack,
1837 block_smartcast_counts)
1838 t.transform_stmt_list_item_cursor_to_flat(stmts.at(i), mut ids, mut out)
1839 }
1840 for stmt in extra_stmts {
1841 t.restore_flat_stmt_list_smartcast_context(block_smartcast_depth, block_smartcast_stack,
1842 block_smartcast_counts)
1843 t.transform_stmt_list_item_to_flat(stmt, mut ids, mut out)
1844 }
1845 t.restore_flat_stmt_list_smartcast_context(block_smartcast_depth, block_smartcast_stack,
1846 block_smartcast_counts)
1847 return ids
1848}
1849
1850fn (mut t Transformer) restore_flat_stmt_list_smartcast_context(block_smartcast_depth int, block_smartcast_stack []SmartcastContext, block_smartcast_counts map[string]int) {
1851 if t.smartcast_stack.len < block_smartcast_depth {
1852 t.smartcast_stack = block_smartcast_stack.clone()
1853 t.smartcast_expr_counts = block_smartcast_counts.clone()
1854 } else if t.smartcast_stack.len > block_smartcast_depth {
1855 t.truncate_smartcasts(block_smartcast_depth)
1856 }
1857}
1858
1859fn (mut t Transformer) transform_stmt_list_item_to_flat(stmt ast.Stmt, mut ids []ast.FlatNodeId, mut out ast.FlatBuilder) {
1860 if stmt is ast.AssignStmt {
1861 assign_stmt := stmt as ast.AssignStmt
1862 if t.try_expand_comptime_if_assign_to_flat(assign_stmt, mut ids, mut out) {
1863 return
1864 }
1865 t.track_interface_assign_to_flat(assign_stmt)
1866 if t.try_expand_interface_cast_assign_to_flat(assign_stmt, mut ids, mut out) {
1867 return
1868 }
1869 if t.try_expand_sincos_assign_to_flat(assign_stmt, mut ids, mut out) {
1870 return
1871 }
1872 if t.try_expand_or_expr_assign_stmts_to_flat(&assign_stmt, mut ids, mut out) {
1873 return
1874 }
1875 if t.try_expand_tuple_if_assign_stmts_to_flat(assign_stmt, mut ids, mut out) {
1876 return
1877 }
1878 if t.try_expand_tuple_call_assign_to_flat(assign_stmt, mut ids, mut out) {
1879 return
1880 }
1881 if t.try_expand_if_guard_assign_stmts_to_flat(assign_stmt, mut ids, mut out) {
1882 return
1883 }
1884 if t.try_expand_if_expr_assign_stmts_to_flat(assign_stmt, mut ids, mut out) {
1885 return
1886 }
1887 }
1888 if stmt is ast.ExprStmt {
1889 if t.try_expand_comptime_if_stmt_to_flat(stmt, mut ids, mut out) {
1890 return
1891 }
1892 }
1893 if stmt is ast.ExprStmt {
1894 if t.try_expand_or_expr_stmt_to_flat(stmt, mut ids, mut out) {
1895 return
1896 }
1897 if t.try_expand_if_guard_stmt_to_flat(stmt, mut ids, mut out) {
1898 return
1899 }
1900 }
1901 if stmt is ast.ExprStmt {
1902 if t.try_emit_flag_enum_set_clear_to_flat(stmt, mut ids, mut out) {
1903 return
1904 }
1905 }
1906 if stmt is ast.ReturnStmt {
1907 if t.try_expand_return_match_expr_to_flat(stmt, mut ids, mut out) {
1908 return
1909 }
1910 if t.try_expand_or_expr_return_to_flat(stmt, mut ids, mut out) {
1911 return
1912 }
1913 if t.try_expand_return_if_expr_to_flat(stmt, mut ids, mut out) {
1914 return
1915 }
1916 }
1917 if stmt is ast.ExprStmt {
1918 if stmt.expr is ast.LockExpr {
1919 t.expand_lock_expr_to_flat(stmt.expr, mut ids, mut out)
1920 return
1921 }
1922 if t.try_emit_map_index_push_to_flat(stmt, mut ids, mut out) {
1923 return
1924 }
1925 if t.try_emit_map_index_postfix_to_flat(stmt, mut ids, mut out) {
1926 return
1927 }
1928 if t.try_emit_selector_postfix_to_flat(stmt, mut ids, mut out) {
1929 return
1930 }
1931 }
1932 if stmt is ast.ForStmt {
1933 if t.try_expand_for_in_map_to_flat(stmt, mut ids, mut out) {
1934 return
1935 }
1936 }
1937 if stmt is ast.AssertStmt {
1938 t.expand_assert_stmt_to_flat(stmt, mut ids, mut out)
1939 return
1940 }
1941 t.append_transformed_stmt_to_flat(mut ids, stmt, mut out)
1942}
1943
1944// transform_stmt_list_item_cursor_to_flat is the cursor-input mirror of
1945// `transform_stmt_list_item_to_flat`. It dispatches on the top-level statement's
1946// FlatNodeKind so converted arms can be handled without routing through the
1947// legacy guard chain, and unconverted kinds fall back to the proven decode path
1948// in one line. This is the seam that lets the transform become cursor-native one
1949// statement kind at a time (eliminating the whole-subtree `.stmt()` decode at
1950// the `transform_cursor_stmts_to_flat_direct` loop).
1951//
1952// First converted set: true-passthrough top-level kinds that carry no
1953// `try_expand_*` guard and that `transform_stmt_to_flat` emits verbatim. Those
1954// copy their flat subtrees directly, so they do not route through `c.stmt()`.
1955
1956// classify_call_fallback_cursor builds a diagnostic key describing WHY a call
1957// fell back to the legacy decode path. Instrumentation for V2_FLAT_FB_STATS.
1958fn (t &Transformer) classify_call_fallback_cursor(c ast.Cursor, prefix string) string {
1959 lhs := c.edge(0)
1960 if lhs.kind() != .expr_selector {
1961 return '${prefix}/lhs=${lhs.kind()}'
1962 }
1963 receiver := lhs.edge(0)
1964 method_name := selector_rhs_name_cursor(lhs)
1965 if t.has_active_smartcast() {
1966 return '${prefix}/sel/smartcast'
1967 }
1968 if method_name_needs_legacy_selector_pipeline(method_name) {
1969 return '${prefix}/sel/legacy-list:${method_name}'
1970 }
1971 recv_type := t.get_expr_type_cursor(receiver) or { return '${prefix}/sel/no-recv-type' }
1972 base := t.unwrap_alias_and_pointer_type(recv_type)
1973 if base is types.Struct {
1974 if !t.type_has_cached_method(base, method_name) {
1975 return '${prefix}/sel/no-cached-method'
1976 }
1977 } else {
1978 // Mirror the non-struct receiver gate: only its rejects classify as
1979 // receiver failures; accepted receivers fall through to late gates.
1980 if method_name in ['filter', 'map', 'any', 'count', 'wait'] {
1981 return '${prefix}/sel/expansion-method:${method_name}'
1982 }
1983 if base is types.Interface || base is types.SumType {
1984 return '${prefix}/sel/recv-dispatch=${base.name()}'
1985 }
1986 if t.resolve_alias_receiver_method_name(recv_type, method_name) == none
1987 && !(t.type_is_string(recv_type)
1988 && t.lookup_method_cached('string', method_name) != none) {
1989 return '${prefix}/sel/recv=${typeof(base).name}:${base.name()}:m=${method_name}'
1990 }
1991 }
1992 if t.resolve_static_type_method_call_cursor(receiver, method_name) != none {
1993 return '${prefix}/sel/static-shadow'
1994 }
1995 call_name := t.resolve_method_call_name_cursor(receiver, method_name) or {
1996 return '${prefix}/sel/no-call-name'
1997 }
1998 if call_name in t.elided_fns {
1999 return '${prefix}/sel/elided'
2000 }
2001 info := t.generic_aware_call_fn_info_cursor(c.edge(0), call_name) or {
2002 return '${prefix}/sel/no-fn-info'
2003 }
2004 arg_count := if c.kind() == .expr_call_or_cast {
2005 arg0 := c.edge(1)
2006 if arg0.is_valid() && arg0.kind() != .expr_empty {
2007 1
2008 } else {
2009 0
2010 }
2011 } else {
2012 c.edge_count() - 1
2013 }
2014 if info.param_types.len != arg_count {
2015 return '${prefix}/sel/arity:${call_name}:${info.param_types.len}vs${arg_count}'
2016 }
2017 if info.is_variadic {
2018 return '${prefix}/sel/variadic'
2019 }
2020 if info.generic_params.len > 0 {
2021 return '${prefix}/sel/generic'
2022 }
2023 for i in 0 .. arg_count {
2024 arg := c.edge(i + 1)
2025 if arg.kind() == .aux_field_init {
2026 return '${prefix}/sel/field-init-arg'
2027 }
2028 if !t.identity_call_arg_can_transform_direct(arg, info.param_types[i]) {
2029 param_base := t.unwrap_alias_type(info.param_types[i])
2030 param_kind := match param_base {
2031 types.Struct { 'struct' }
2032 types.Enum { 'enum' }
2033 types.Array { 'array' }
2034 types.ArrayFixed { 'array_fixed' }
2035 types.Map { 'map' }
2036 types.SumType { 'sumtype' }
2037 types.Interface { 'interface' }
2038 types.FnType { 'fn' }
2039 types.OptionType { 'option' }
2040 types.ResultType { 'result' }
2041 types.Pointer { 'pointer' }
2042 types.String { 'string' }
2043 else { 'other' }
2044 }
2045
2046 return '${prefix}/sel/arg-not-identity/param=${param_kind}'
2047 }
2048 }
2049 return '${prefix}/sel/struct-late-gate-unknown'
2050}
2051
2052fn (mut t Transformer) transform_stmt_list_item_cursor_to_flat(c ast.Cursor, mut ids []ast.FlatNodeId, mut out ast.FlatBuilder) {
2053 match c.kind() {
2054 .stmt_import, .stmt_module, .stmt_directive, .stmt_empty, .stmt_enum_decl,
2055 .stmt_interface_decl, .stmt_type_decl, .stmt_asm, .stmt_flow_control, .stmt_attributes {
2056 id := out.copy_subtree_from(c.flat, c.id)
2057 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2058 }
2059 .stmt_const_decl {
2060 id := t.transform_const_decl_cursor_to_flat(c, mut out)
2061 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2062 }
2063 .stmt_block {
2064 id := t.transform_block_stmt_cursor_to_flat(c, mut out)
2065 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2066 }
2067 .stmt_defer {
2068 id := t.transform_defer_stmt_cursor_to_flat(c, mut out)
2069 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2070 }
2071 .stmt_label {
2072 id := t.transform_label_stmt_cursor_to_flat(c, mut out)
2073 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2074 }
2075 .stmt_global_decl {
2076 id := t.transform_global_decl_cursor_to_flat(c, mut out)
2077 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2078 }
2079 .stmt_struct_decl {
2080 t.count_flat_fallback('stmt_struct_decl')
2081 id := out.emit_stmt(t.transform_struct_decl(c.struct_decl()))
2082 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2083 }
2084 .stmt_assert {
2085 t.expand_assert_stmt_cursor_to_flat(c, mut ids, mut out)
2086 }
2087 .stmt_assign {
2088 if id := t.transform_map_index_assign_cursor_to_flat(c, mut out) {
2089 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2090 return
2091 }
2092 if t.try_expand_if_expr_assign_cursor_to_flat(c, mut ids, mut out) {
2093 return
2094 }
2095 if t.assign_stmt_cursor_needs_legacy_expand(c) {
2096 t.count_flat_fallback('stmt_assign')
2097 t.transform_stmt_list_item_to_flat(assign_stmt_from_cursor(c), mut ids, mut out)
2098 } else {
2099 id := t.transform_assign_stmt_cursor_to_flat(c, mut out)
2100 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2101 }
2102 }
2103 .stmt_comptime {
2104 id := t.transform_comptime_stmt_cursor_to_flat(c, mut out)
2105 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2106 }
2107 .stmt_expr {
2108 if t.try_expand_comptime_if_stmt_cursor_to_flat(c, mut ids, mut out) {
2109 return
2110 }
2111 if c.edge(0).kind() == .expr_lock {
2112 t.expand_lock_expr_cursor_to_flat(c.edge(0), mut ids, mut out)
2113 return
2114 }
2115 if id := t.transform_flag_enum_set_clear_cursor_to_flat(c, mut out) {
2116 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2117 return
2118 }
2119 if t.expr_stmt_cursor_needs_legacy_expand(c) {
2120 t.count_flat_fallback('stmt_expr')
2121 t.transform_stmt_list_item_to_flat(expr_stmt_from_cursor(c), mut ids, mut out)
2122 } else {
2123 expr := c.edge(0)
2124 if expr.kind() == .expr_infix
2125 && unsafe { token.Token(int(expr.aux())) } == .left_shift {
2126 if t.try_emit_map_index_push_to_flat(expr_stmt_from_cursor(c), mut ids, mut out) {
2127 return
2128 }
2129 }
2130 id := t.transform_expr_stmt_cursor_to_flat(c, mut out)
2131 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2132 }
2133 }
2134 .stmt_return {
2135 if t.try_expand_return_if_expr_cursor_to_flat(c, mut ids, mut out) {
2136 return
2137 }
2138 if t.return_stmt_cursor_needs_legacy_expand(c) {
2139 t.count_flat_fallback('stmt_return')
2140 t.transform_stmt_list_item_to_flat(return_stmt_from_cursor(c), mut ids, mut out)
2141 } else {
2142 id := t.transform_return_stmt_cursor_to_flat(c, mut out)
2143 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2144 }
2145 }
2146 .stmt_for_in {
2147 t.count_flat_fallback('stmt_for_in')
2148 t.transform_stmt_list_item_to_flat(for_in_stmt_from_cursor(c), mut ids, mut out)
2149 }
2150 .stmt_fn_decl {
2151 decl := c.fn_decl_signature()
2152 if t.omit_backend_generic_decl(decl) {
2153 return
2154 }
2155 // Stream the body cursor-native when it has no defers (defer
2156 // lowering needs the whole body, so those take the legacy
2157 // whole-decl decode path).
2158 if flat_body_has_defer(c.list_at(3)) {
2159 t.count_flat_fallback('stmt_fn_decl_defer')
2160 decl_with_body := fn_decl_signature_with_body_cursor(c.fn_decl_signature(), c)
2161 t.transform_stmt_list_item_to_flat(decl_with_body, mut ids, mut out)
2162 } else {
2163 id := t.transform_fn_decl_streaming_to_flat(c, mut out)
2164 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2165 }
2166 }
2167 .stmt_for {
2168 // Stream plain `for` loops (cond / classic / bare) cursor-native.
2169 // For-in loops dispatch through cursor-native lowerings for the
2170 // migrated iterable families, and only unported forms fall back to
2171 // the legacy whole-loop decode path.
2172 init_c := c.edge(0)
2173 if init_c.is_valid() && init_c.kind() == .stmt_for_in {
2174 if t.try_expand_range_for_in_cursor_to_flat(c, mut ids, mut out) {
2175 return
2176 }
2177 if t.try_expand_array_for_in_cursor_to_flat(c, mut ids, mut out) {
2178 return
2179 }
2180 if t.try_expand_fixed_array_for_in_cursor_to_flat(c, mut ids, mut out) {
2181 return
2182 }
2183 if t.try_expand_for_in_map_cursor_to_flat(c, mut ids, mut out) {
2184 return
2185 }
2186 if t.try_expand_untyped_for_in_cursor_to_flat(c, mut ids, mut out) {
2187 return
2188 }
2189 if t.try_expand_passthrough_for_in_cursor_to_flat(c, mut ids, mut out) {
2190 return
2191 }
2192 panic('unhandled flat for-in lowering')
2193 } else {
2194 id := t.transform_for_stmt_streaming_to_flat(c, mut out)
2195 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2196 }
2197 }
2198 else {
2199 panic('unexpected flat statement kind: ${c.kind()}')
2200 }
2201 }
2202}
2203
2204// append_transformed_stmt_to_flat is the flat-builder mirror of
2205// `append_transformed_stmt` (transformer.v:3061). Runs `transform_stmt_to_flat`
2206// on the input stmt (direct-emitting via the dispatch seam), then drains any
2207// `t.pending_stmts` produced by sub-transforms (e.g. or-block side effects
2208// pushed by `expand_single_or_expr`) by leaf-encoding them into `out` BEFORE
2209// the just-emitted main stmt — matching the ordering invariant of the legacy
2210// appender ("pending stmts hoist ahead of the transformed result").
2211//
2212// Scaffolding for the `transform_stmts` body driver port (arc #1). Future
2213// sessions fork the legacy `transform_stmts` body into a direct-emit driver
2214// that drives the per-stmt loop via this appender, replacing the inner
2215// `t.append_transformed_stmt(mut result, ast.Stmt(...))` calls with their
2216// `_to_flat` equivalents. Used today by `transform_file_index_to_flat`'s
2217// top-level loop (replacing the previous push/pop/restore pattern).
2218pub fn (mut t Transformer) append_transformed_stmt_to_flat(mut ids []ast.FlatNodeId, stmt ast.Stmt, mut out ast.FlatBuilder) {
2219 id := t.transform_stmt_to_flat(stmt, mut out)
2220 t.append_transformed_stmt_id_to_flat(mut ids, id, mut out)
2221}
2222
2223// append_transformed_stmt_id_to_flat hoists any `t.pending_stmts` produced
2224// while building `id` ahead of it (matching the appender's ordering invariant),
2225// then pushes `id`. Shared by `append_transformed_stmt_to_flat` and the
2226// cursor-native stmt arms (transform_stmt_list_item_cursor_to_flat) so both
2227// drain pending side effects identically.
2228fn (mut t Transformer) append_transformed_stmt_id_to_flat(mut ids []ast.FlatNodeId, id ast.FlatNodeId, mut out ast.FlatBuilder) {
2229 // Flat-side hoists drain first: arms that push pending_flat_stmt_ids
2230 // flush the legacy pending_stmts queue into it beforehand, so emitting
2231 // the flat queue first preserves the legacy chronological order.
2232 if t.pending_flat_stmt_ids.len > 0 {
2233 for fid in t.pending_flat_stmt_ids {
2234 ids << fid
2235 }
2236 t.pending_flat_stmt_ids.clear()
2237 }
2238 if t.pending_stmts.len > 0 {
2239 pending := t.pending_stmts.clone()
2240 t.pending_stmts.clear()
2241 for ps in pending {
2242 ids << out.emit_stmt(ps)
2243 }
2244 }
2245 ids << id
2246}
2247
2248// transform_const_decl_cursor_to_flat is the cursor-input mirror of the
2249// `ast.ConstDecl` arm of `transform_stmt_to_flat` (flat_write.v). It reads the
2250// const decl's is_public flag and field-init list straight from the cursor —
2251// the ConstDecl wrapper + FieldInit structure are never decoded to legacy — and
2252// transforms each field value via the existing `transform_expr_to_flat`. Emit
2253// order matches the flat-output arm exactly (per-field value+field_init, then
2254// the fields aux_list, then the stmt).
2255fn (mut t Transformer) transform_const_decl_cursor_to_flat(c ast.Cursor, mut out ast.FlatBuilder) ast.FlatNodeId {
2256 fields := c.list_at(0)
2257 mut field_ids := []ast.FlatNodeId{cap: fields.len()}
2258 for i in 0 .. fields.len() {
2259 fc := fields.at(i)
2260 value_id := t.transform_expr_cursor_to_flat(fc.edge(0), mut out)
2261 field_ids << out.emit_field_init_by_id(fc.name(), value_id)
2262 }
2263 fields_list_id := out.emit_aux_list_from_ids(field_ids)
2264 return out.emit_const_decl_by_ids(c.flag(ast.flag_is_public), fields_list_id)
2265}
2266
2267// transform_global_decl_cursor_to_flat is the cursor-input mirror of the
2268// `ast.GlobalDecl` arm of `transform_stmt_to_flat`. It reads the decl
2269// attributes, field-decl list, and per-field metadata/flags from the cursor and
2270// transforms each field's typ + value via `transform_expr_to_flat`. Emit order
2271// matches the flat-output arm (decl attrs, then per-field typ/value/attrs/
2272// field_decl, then the fields aux_list, then the stmt).
2273fn (mut t Transformer) transform_global_decl_cursor_to_flat(c ast.Cursor, mut out ast.FlatBuilder) ast.FlatNodeId {
2274 decl_attrs_id := copy_cursor_aux_list_to_flat(c.list_at(0), mut out)
2275 fields := c.list_at(1)
2276 mut field_ids := []ast.FlatNodeId{cap: fields.len()}
2277 for i in 0 .. fields.len() {
2278 fc := fields.at(i)
2279 typ_id := t.transform_expr_cursor_to_flat(fc.edge(0), mut out)
2280 value_id := t.transform_expr_cursor_to_flat(fc.edge(1), mut out)
2281 field_attrs_id := copy_cursor_aux_list_to_flat(fc.list_at(2), mut out)
2282 field := ast.FieldDecl{
2283 name: fc.name()
2284 is_public: fc.flag(ast.flag_is_public)
2285 is_mut: fc.flag(ast.flag_is_mut)
2286 is_module_mut: fc.flag(ast.flag_field_is_module_mut)
2287 is_interface_method: fc.flag(ast.flag_field_is_interface_method)
2288 }
2289 field_ids << out.emit_field_decl_by_ids(field, typ_id, value_id, field_attrs_id)
2290 }
2291 fields_list_id := out.emit_aux_list_from_ids(field_ids)
2292 return out.emit_global_decl_by_ids(c.flag(ast.flag_is_public), decl_attrs_id, fields_list_id)
2293}
2294
2295fn assoc_expr_from_cursor(c ast.Cursor) ast.AssocExpr {
2296 mut fields := []ast.FieldInit{cap: c.edge_count() - 2}
2297 for i in 2 .. c.edge_count() {
2298 field := c.edge(i)
2299 fields << ast.FieldInit{
2300 name: field.name()
2301 value: field.edge(0).expr()
2302 }
2303 }
2304 return ast.AssocExpr{
2305 typ: c.edge(0).expr()
2306 expr: c.edge(1).expr()
2307 fields: fields
2308 pos: c.pos()
2309 }
2310}
2311
2312fn or_expr_from_cursor(c ast.Cursor) ast.OrExpr {
2313 mut stmts := []ast.Stmt{cap: c.edge_count() - 1}
2314 for i in 1 .. c.edge_count() {
2315 stmts << c.edge(i).stmt()
2316 }
2317 return ast.OrExpr{
2318 expr: c.edge(0).expr()
2319 stmts: stmts
2320 pos: c.pos()
2321 }
2322}
2323
2324fn infix_expr_from_cursor(c ast.Cursor) ast.InfixExpr {
2325 return ast.InfixExpr{
2326 op: unsafe { token.Token(int(c.aux())) }
2327 lhs: c.edge(0).expr()
2328 rhs: c.edge(1).expr()
2329 pos: c.pos()
2330 }
2331}
2332
2333fn (t &Transformer) cursor_is_string_concat_operand(c ast.Cursor) bool {
2334 if !c.is_valid() {
2335 return false
2336 }
2337 if t.is_string_expr_cursor(c) {
2338 return true
2339 }
2340 match c.kind() {
2341 .expr_call {
2342 if c.edge_count() == 0 {
2343 return false
2344 }
2345 lhs := c.edge(0)
2346 return lhs.kind() == .expr_ident && lhs.name().starts_with('string__')
2347 }
2348 .expr_paren, .expr_modifier {
2349 return c.edge_count() > 0 && t.cursor_is_string_concat_operand(c.edge(0))
2350 }
2351 .expr_infix {
2352 op := unsafe { token.Token(int(c.aux())) }
2353 return op == .plus && (t.cursor_is_string_concat_operand(c.edge(0))
2354 || t.cursor_is_string_concat_operand(c.edge(1)))
2355 }
2356 else {
2357 return false
2358 }
2359 }
2360}
2361
2362fn (t &Transformer) infix_plus_cursor_can_transform_direct(c ast.Cursor) bool {
2363 if t.cursor_is_string_concat_operand(c.edge(0)) || t.cursor_is_string_concat_operand(c.edge(1)) {
2364 return false
2365 }
2366 if lhs_type := t.get_expr_type_cursor(c.edge(0)) {
2367 if lhs_type is types.Struct {
2368 type_name := t.type_to_c_name(lhs_type)
2369 if type_name == 'time__Time' {
2370 return false
2371 }
2372 }
2373 }
2374 return true
2375}
2376
2377fn cursor_is_enum_shorthand_selector(c ast.Cursor) bool {
2378 if !c.is_valid() || c.kind() != .expr_selector || c.edge_count() == 0 {
2379 return false
2380 }
2381 lhs := c.edge(0)
2382 return lhs.kind() == .expr_empty || (lhs.kind() == .expr_ident && lhs.name() == '')
2383}
2384
2385fn (mut t Transformer) enum_shorthand_cursor_to_flat(c ast.Cursor, enum_type string, mut out ast.FlatBuilder) ?ast.FlatNodeId {
2386 if enum_type == '' || !cursor_is_enum_shorthand_selector(c) {
2387 return none
2388 }
2389 member := c.edge(1)
2390 if typ := t.lookup_type(enum_type) {
2391 t.register_synth_type(c.pos(), typ)
2392 if typ is types.Enum {
2393 return out.emit_ident_by_name(t.enum_member_ident_for_lookup(enum_type, typ,
2394 member.name()), c.pos())
2395 }
2396 }
2397 return out.emit_ident_by_name(enum_member_ident(enum_type, member.name()), c.pos())
2398}
2399
2400fn (mut t Transformer) transform_enum_shorthand_compare_cursor_to_flat(c ast.Cursor, op token.Token, mut out ast.FlatBuilder) ?ast.FlatNodeId {
2401 if op !in [.eq, .ne] || c.edge_count() < 2 {
2402 return none
2403 }
2404 lhs := c.edge(0)
2405 rhs := c.edge(1)
2406 if cursor_is_enum_shorthand_selector(rhs) {
2407 enum_type := t.get_enum_type_name_cursor(lhs)
2408 rhs_id := t.enum_shorthand_cursor_to_flat(rhs, enum_type, mut out) or { return none }
2409 lhs_id := t.transform_expr_cursor_to_flat(lhs, mut out)
2410 return out.emit_infix_expr_by_ids(op, lhs_id, rhs_id, c.pos())
2411 }
2412 if cursor_is_enum_shorthand_selector(lhs) {
2413 enum_type := t.get_enum_type_name_cursor(rhs)
2414 lhs_id := t.enum_shorthand_cursor_to_flat(lhs, enum_type, mut out) or { return none }
2415 rhs_id := t.transform_expr_cursor_to_flat(rhs, mut out)
2416 return out.emit_infix_expr_by_ids(op, lhs_id, rhs_id, c.pos())
2417 }
2418 return none
2419}
2420
2421fn (t &Transformer) sumtype_check_variant_name_cursor(rhs ast.Cursor) (string, string) {
2422 if rhs.kind() == .expr_ident {
2423 return rhs.name(), ''
2424 }
2425 if rhs.kind() == .expr_selector {
2426 mut variant_module := ''
2427 rhs_lhs := rhs.edge(0)
2428 if rhs_lhs.kind() == .expr_ident {
2429 variant_module = rhs_lhs.name()
2430 }
2431 return selector_rhs_name_cursor(rhs), variant_module
2432 }
2433 return t.type_expr_to_variant_name_cursor(rhs), ''
2434}
2435
2436fn (mut t Transformer) transform_interface_self_type_check_cursor_to_flat(lhs ast.Cursor, variant_name string, op token.Token, pos token.Pos, mut out ast.FlatBuilder) ?ast.FlatNodeId {
2437 lhs_type := t.get_expr_type_cursor(lhs) or { return none }
2438 lhs_base := t.unwrap_alias_and_pointer_type(lhs_type)
2439 if lhs_base !is types.Interface {
2440 return none
2441 }
2442 lhs_base_name := lhs_base.name()
2443 if !lhs_base_name.ends_with(variant_name) && lhs_base_name != variant_name {
2444 return none
2445 }
2446 lhs_id := t.transform_expr_cursor_to_flat(lhs, mut out)
2447 type_id := t.synth_selector_cursor_to_flat(lhs_id, '_type_id', types.Type(types.int_), mut out)
2448 zero_id := out.emit_basic_literal_by_value(.number, '0', pos)
2449 cmp_op := if op in [.key_is, .eq] { token.Token.ne } else { token.Token.eq }
2450 return out.emit_infix_expr_by_ids(cmp_op, type_id, zero_id, pos)
2451}
2452
2453fn (mut t Transformer) transform_sumtype_check_cursor_to_flat(c ast.Cursor, op token.Token, mut out ast.FlatBuilder) ?ast.FlatNodeId {
2454 if op !in [.key_is, .not_is, .eq, .ne] || c.edge_count() < 2 || t.has_active_smartcast() {
2455 return none
2456 }
2457 lhs := c.edge(0)
2458 rhs := c.edge(1)
2459 variant_name, variant_module := t.sumtype_check_variant_name_cursor(rhs)
2460 if variant_name == '' {
2461 return none
2462 }
2463 variant_lookup_name := sum_type_variant_name_with_module(variant_module, variant_name)
2464 mut sumtype_name := t.generic_scan_sumtype_name_for_expr_cursor(lhs)
2465 mut lhs_is_enum := t.get_enum_type_name_cursor(lhs) != ''
2466 if lhs_type := t.get_expr_type_cursor(lhs) {
2467 lhs_base := t.unwrap_alias_and_pointer_type(lhs_type)
2468 lhs_is_enum = lhs_is_enum || lhs_base is types.Enum
2469 }
2470 rhs_is_type_like := variant_name.len > 0 && variant_name[0] >= `A` && variant_name[0] <= `Z`
2471 if sumtype_name == '' && !lhs_is_enum && !cursor_is_enum_shorthand_selector(rhs)
2472 && (op in [.key_is, .not_is] || rhs_is_type_like) {
2473 sumtype_name = t.find_sumtype_for_variant(variant_lookup_name)
2474 }
2475 if sumtype_name == '' {
2476 if result_id := t.transform_interface_self_type_check_cursor_to_flat(lhs, variant_name, op,
2477 c.pos(), mut out)
2478 {
2479 return result_id
2480 }
2481 return none
2482 }
2483 variants := t.get_sum_type_variants(sumtype_name)
2484 mut tag_value := -1
2485 for i, variant in variants {
2486 if sum_type_variant_matches_for_sumtype(sumtype_name, variant, variant_lookup_name) {
2487 tag_value = i
2488 break
2489 }
2490 }
2491 if tag_value < 0 {
2492 return none
2493 }
2494 lhs_id := t.transform_expr_cursor_to_flat(lhs, mut out)
2495 tag_id := t.synth_selector_cursor_to_flat(lhs_id, '_tag', types.Type(types.int_), mut out)
2496 tag_value_id := out.emit_basic_literal_by_value(.number, tag_value.str(), c.pos())
2497 cmp_op := if op in [.key_is, .eq] { token.Token.eq } else { token.Token.ne }
2498 return out.emit_infix_expr_by_ids(cmp_op, tag_id, tag_value_id, c.pos())
2499}
2500
2501fn (t &Transformer) scalar_equality_type_can_transform_direct(typ types.Type) bool {
2502 if t.type_is_string(typ) {
2503 return false
2504 }
2505 base := t.unwrap_alias_type(typ)
2506 return match base {
2507 types.Primitive, types.Char, types.ISize, types.Nil, types.Pointer, types.Rune,
2508 types.USize {
2509 true
2510 }
2511 else {
2512 false
2513 }
2514 }
2515}
2516
2517fn (t &Transformer) infix_equality_cursor_can_transform_direct(c ast.Cursor) bool {
2518 lhs := c.edge(0)
2519 rhs := c.edge(1)
2520 if cursor_is_enum_shorthand_selector(lhs) || cursor_is_enum_shorthand_selector(rhs) {
2521 return false
2522 }
2523 lhs_type := t.get_expr_type_cursor(lhs) or { return false }
2524 rhs_type := t.get_expr_type_cursor(rhs) or { return false }
2525 return t.scalar_equality_type_can_transform_direct(lhs_type)
2526 && t.scalar_equality_type_can_transform_direct(rhs_type)
2527}
2528
2529fn (t &Transformer) is_nil_expr_cursor(c ast.Cursor) bool {
2530 return match c.kind() {
2531 .expr_ident {
2532 c.name() == 'nil'
2533 }
2534 .expr_keyword {
2535 unsafe { token.Token(int(c.aux())) } == .key_nil
2536 }
2537 .typ_nil {
2538 true
2539 }
2540 .expr_basic_literal {
2541 unsafe { token.Token(int(c.aux())) } == .number && c.name() == '0'
2542 }
2543 .expr_paren, .expr_cast, .expr_modifier {
2544 c.edge_count() > 0 && t.is_nil_expr_cursor(c.edge(0))
2545 }
2546 else {
2547 false
2548 }
2549 }
2550}
2551
2552fn cursor_is_c_string_literal(c ast.Cursor) bool {
2553 return match c.kind() {
2554 .expr_string {
2555 unsafe { ast.StringLiteralKind(int(c.aux())) } == .c
2556 }
2557 .expr_paren, .expr_modifier {
2558 c.edge_count() > 0 && cursor_is_c_string_literal(c.edge(0))
2559 }
2560 else {
2561 false
2562 }
2563 }
2564}
2565
2566fn cursor_is_foldable_v_string_literal(c ast.Cursor) bool {
2567 return match c.kind() {
2568 .expr_string {
2569 unsafe { ast.StringLiteralKind(int(c.aux())) } == .v
2570 }
2571 .expr_paren {
2572 c.edge_count() > 0 && cursor_is_foldable_v_string_literal(c.edge(0))
2573 }
2574 else {
2575 false
2576 }
2577 }
2578}
2579
2580fn (t &Transformer) is_v_string_expr_cursor(c ast.Cursor) bool {
2581 return !cursor_is_c_string_literal(c) && t.is_string_expr_cursor(c)
2582}
2583
2584fn (t &Transformer) infix_string_compare_cursor_can_transform_direct(c ast.Cursor, op token.Token) bool {
2585 if op !in [.eq, .ne, .lt, .gt, .le, .ge] || c.edge_count() < 2 {
2586 return false
2587 }
2588 lhs := c.edge(0)
2589 rhs := c.edge(1)
2590 return !t.is_nil_expr_cursor(lhs) && !t.is_nil_expr_cursor(rhs)
2591 && t.is_v_string_expr_cursor(lhs) && t.is_v_string_expr_cursor(rhs)
2592}
2593
2594fn (mut t Transformer) string_compare_call_cursor_to_flat(name string, first ast.Cursor, second ast.Cursor, pos token.Pos, mut out ast.FlatBuilder) ast.FlatNodeId {
2595 lhs_id := out.emit_ident_by_name(name, token.Pos{})
2596 first_id := t.transform_expr_cursor_to_flat(first, mut out)
2597 second_id := t.transform_expr_cursor_to_flat(second, mut out)
2598 return out.emit_call_expr_by_ids(lhs_id, [first_id, second_id], pos)
2599}
2600
2601fn (mut t Transformer) transform_string_compare_cursor_to_flat(c ast.Cursor, op token.Token, mut out ast.FlatBuilder) ?ast.FlatNodeId {
2602 if !t.infix_string_compare_cursor_can_transform_direct(c, op) {
2603 return none
2604 }
2605 lhs := c.edge(0)
2606 rhs := c.edge(1)
2607 match op {
2608 .eq {
2609 return t.string_compare_call_cursor_to_flat('string__eq', lhs, rhs, c.pos(), mut out)
2610 }
2611 .ne {
2612 call_id :=
2613 t.string_compare_call_cursor_to_flat('string__eq', lhs, rhs, c.pos(), mut out)
2614 return out.emit_prefix_expr_by_id(.not, call_id, c.pos())
2615 }
2616 .lt {
2617 return t.string_compare_call_cursor_to_flat('string__lt', lhs, rhs, c.pos(), mut out)
2618 }
2619 .gt {
2620 return t.string_compare_call_cursor_to_flat('string__lt', rhs, lhs, c.pos(), mut out)
2621 }
2622 .le {
2623 call_id :=
2624 t.string_compare_call_cursor_to_flat('string__lt', rhs, lhs, c.pos(), mut out)
2625 return out.emit_prefix_expr_by_id(.not, call_id, c.pos())
2626 }
2627 .ge {
2628 call_id :=
2629 t.string_compare_call_cursor_to_flat('string__lt', lhs, rhs, c.pos(), mut out)
2630 return out.emit_prefix_expr_by_id(.not, call_id, c.pos())
2631 }
2632 else {
2633 return none
2634 }
2635 }
2636}
2637
2638fn (t &Transformer) infix_string_concat_cursor_can_transform_direct(c ast.Cursor, op token.Token) bool {
2639 if op != .plus || c.edge_count() < 2 {
2640 return false
2641 }
2642 lhs := c.edge(0)
2643 rhs := c.edge(1)
2644 if lhs.kind() == .expr_infix || rhs.kind() == .expr_infix {
2645 return false
2646 }
2647 if cursor_is_foldable_v_string_literal(lhs) && cursor_is_foldable_v_string_literal(rhs) {
2648 return false
2649 }
2650 return t.is_v_string_expr_cursor(lhs) && t.is_v_string_expr_cursor(rhs)
2651}
2652
2653fn (mut t Transformer) transform_string_concat_cursor_to_flat(c ast.Cursor, op token.Token, mut out ast.FlatBuilder) ?ast.FlatNodeId {
2654 if !t.infix_string_concat_cursor_can_transform_direct(c, op) {
2655 return none
2656 }
2657 lhs_id := out.emit_ident_by_name('string__plus', token.Pos{})
2658 left_id := t.transform_expr_cursor_to_flat(c.edge(0), mut out)
2659 right_id := t.transform_expr_cursor_to_flat(c.edge(1), mut out)
2660 return out.emit_call_expr_by_ids(lhs_id, [left_id, right_id], c.pos())
2661}
2662
2663fn (mut t Transformer) transform_range_membership_cursor_to_flat(c ast.Cursor, op token.Token, mut out ast.FlatBuilder) ?ast.FlatNodeId {
2664 if op !in [.key_in, .not_in] || c.edge_count() < 2 {
2665 return none
2666 }
2667 lhs := c.edge(0)
2668 range := c.edge(1)
2669 if !range.is_valid() || range.kind() != .expr_range || range.edge_count() < 2 {
2670 return none
2671 }
2672 lhs_id := t.transform_expr_cursor_to_flat(lhs, mut out)
2673 start_id := t.transform_expr_cursor_to_flat(range.edge(0), mut out)
2674 lower_id := out.emit_infix_expr_by_ids(.ge, lhs_id, start_id, c.pos())
2675 mut range_check_id := lower_id
2676 end := range.edge(1)
2677 if end.is_valid() && end.kind() != .expr_empty {
2678 range_op := unsafe { token.Token(int(range.aux())) }
2679 upper_op := if range_op == .dotdot { token.Token.lt } else { token.Token.le }
2680 end_id := t.transform_expr_cursor_to_flat(end, mut out)
2681 upper_id := out.emit_infix_expr_by_ids(upper_op, lhs_id, end_id, c.pos())
2682 range_check_id = out.emit_infix_expr_by_ids(.and, lower_id, upper_id, c.pos())
2683 }
2684 if op == .not_in {
2685 return out.emit_prefix_expr_by_id(.not, range_check_id, c.pos())
2686 }
2687 return range_check_id
2688}
2689
2690fn (mut t Transformer) transform_inline_array_membership_elem_cursor_to_flat(elem ast.Cursor, enum_type string, mut out ast.FlatBuilder) ast.FlatNodeId {
2691 if enum_type != '' {
2692 if enum_id := t.enum_shorthand_cursor_to_flat(elem, enum_type, mut out) {
2693 return enum_id
2694 }
2695 }
2696 return t.transform_expr_cursor_to_flat(elem, mut out)
2697}
2698
2699fn (mut t Transformer) transform_inline_array_membership_cursor_to_flat(c ast.Cursor, op token.Token, mut out ast.FlatBuilder) ?ast.FlatNodeId {
2700 if op !in [.key_in, .not_in] || c.edge_count() < 2 {
2701 return none
2702 }
2703 lhs := c.edge(0)
2704 rhs := c.edge(1)
2705 if !rhs.is_valid() || rhs.kind() != .expr_array_init || rhs.edge_count() <= 5 {
2706 return none
2707 }
2708 enum_type := t.get_enum_type_name_cursor(lhs)
2709 lhs_id := t.transform_expr_cursor_to_flat(lhs, mut out)
2710 first_elem_id := t.transform_inline_array_membership_elem_cursor_to_flat(rhs.edge(5),
2711 enum_type, mut out)
2712 mut chain_id := out.emit_infix_expr_by_ids(.eq, lhs_id, first_elem_id, c.pos())
2713 for i in 6 .. rhs.edge_count() {
2714 elem_id :=
2715 t.transform_inline_array_membership_elem_cursor_to_flat(rhs.edge(i), enum_type,