v / vlib / v2 / transformer / transformer_flat_diff_test.v
2002 lines · 1651 sloc · 52.18 KB · 7d6fd95995c08b27f8ddf5d469c9cdd889d10154
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.
4// vtest build: macos
5//
6// Differential test harness for the transformer migration to flat output.
7//
8// Goal: when the transformer eventually writes its result directly into a
9// FlatAst (so the post-transform []ast.File is never allocated) the new
10// path must produce bit-identical output to the legacy path on every
11// program that exercises a non-trivial rewrite.
12//
13// Until that lands, the harness pins two invariants that already matter:
14//
15// 1. Determinism — transform_files run twice on the same parsed input
16// produces the same canonical signature. This guards against accidental
17// iteration-order or pointer-identity leakage as the body changes.
18//
19// 2. Flat-input parity — transform_files(files) and
20// transform_files_from_flat(&flat, []) must produce the same signature.
21// The from-flat streaming path was added 2026-05-26 and previously had
22// no per-construct regression test; it is the closest production path
23// to the future transform-writes-flat work, so any divergence here
24// surfaces a pre-pass / rehydration bug immediately.
25//
26// Adding a fixture: pick the smallest V program that hits the rewrite arm
27// (operator overloading, if-guard, smartcast, ...) and add a test that calls
28// assert_transform_signatures_equal. When a fixture fails, the signatures
29// are dumped to /tmp so a `diff` localises the diverging node.
30module transformer
31
32import os
33import time
34import v2.ast
35import v2.parser
36import v2.pref as vpref
37import v2.token
38import v2.types
39
40// ParsedTransformerFixture bundles parse + check artefacts. The transformer
41// mutates env / checker state internally; running both transformer paths
42// against the *same* env is intentional — it's how the production pipeline
43// behaves and any divergence the env causes is part of what we want to catch.
44struct ParsedTransformerFixture {
45 files []ast.File
46 flat ast.FlatAst
47 env &types.Environment
48 prefs &vpref.Preferences
49}
50
51fn parse_transformer_fixture(src string) ParsedTransformerFixture {
52 tmp := '/tmp/v2_transformer_diff_${os.getpid()}_${transformer_rand_suffix()}.v'
53 os.write_file(tmp, src) or { panic('write_file: ${err}') }
54 defer {
55 os.rm(tmp) or {}
56 }
57 prefs := &vpref.Preferences{
58 backend: .cleanc
59 no_parallel: true
60 }
61 mut fs := token.FileSet.new()
62 mut par := parser.Parser.new(prefs)
63 files := par.parse_files([tmp], mut fs)
64 flat := ast.flatten_files(files)
65 mut env := types.Environment.new()
66 mut checker := types.Checker.new(prefs, fs, env)
67 checker.check_files(files)
68 return ParsedTransformerFixture{
69 files: files
70 flat: flat
71 env: env
72 prefs: prefs
73 }
74}
75
76fn transformer_rand_suffix() string {
77 t := u64(time.now().unix_milli()) & 0xFFFFFFFF
78 return '${t:x}'
79}
80
81// transform_signature is the canonical, comparable string form of a
82// transformer output. It flattens the result and reuses ast.signature() so
83// two runs producing structurally identical (but pointer-distinct) trees
84// hash to the same string.
85fn transform_signature(files []ast.File) string {
86 flat := ast.flatten_files(files)
87 return flat.signature()
88}
89
90// run_legacy_transform runs the canonical []ast.File-in, []ast.File-out path.
91fn run_legacy_transform(p ParsedTransformerFixture) []ast.File {
92 mut t := Transformer.new_with_pref(p.env, p.prefs)
93 return t.transform_files(p.files)
94}
95
96// run_from_flat_transform runs the streaming-from-flat path, which feeds
97// the same per-file transform but rehydrates each ast.File on demand from
98// FlatAst instead of holding the full source array. Today both paths share
99// transform_file; the test exists so when the path diverges further (e.g.
100// transformer-writes-flat) any drift is caught.
101fn run_from_flat_transform(p ParsedTransformerFixture) []ast.File {
102 mut t := Transformer.new_with_pref(p.env, p.prefs)
103 return t.transform_files_from_flat(&p.flat, [])
104}
105
106// dump_signature_pair writes the two signatures to /tmp so a `diff` can
107// localise the diverging node when a fixture fails. Returns the two paths
108// in the assert message.
109fn dump_signature_pair(label string, a string, b string) (string, string) {
110 suffix := transformer_rand_suffix()
111 pa := '/tmp/v2_transform_sig_${label}_a_${suffix}.txt'
112 pb := '/tmp/v2_transform_sig_${label}_b_${suffix}.txt'
113 os.write_file(pa, a) or {}
114 os.write_file(pb, b) or {}
115 return pa, pb
116}
117
118fn assert_transform_signatures_equal(label string, a []ast.File, b []ast.File) {
119 sa := transform_signature(a)
120 sb := transform_signature(b)
121 if sa == sb {
122 return
123 }
124 pa, pb := dump_signature_pair(label, sa, sb)
125 eprintln('[${label}] transformer signatures diverged.')
126 eprintln(' A: ${pa}')
127 eprintln(' B: ${pb}')
128 eprintln(' diff with: diff -u ${pa} ${pb}')
129 assert false, '${label}: transformer outputs differ (see /tmp dumps above)'
130}
131
132// --- fixture catalog ---
133//
134// Each fixture is the smallest V program that exercises one transformer
135// rewrite path. The mix here is deliberately chosen to cover:
136// - plain top-level (no rewrites) → fixture_plain_fn
137// - operator overloading → fixture_infix_operator
138// - array.contains / array methods → fixture_array_contains
139// - if-guard (rewritten on every backend)→ fixture_if_guard
140// - or-block expression expansion → fixture_or_block
141// - sumtype is / smartcast → fixture_sumtype_is_as
142// - string interpolation → fixture_string_interp
143// - global init → fixture_global_init
144// - file-scope const decl → fixture_const_decl
145// - paren-wrapped const value → fixture_paren_expr
146// - prefix-op const value → fixture_prefix_expr
147// - modifier expr (mut arg) → fixture_modifier_expr
148// - lambda expr (|y| y + 1 in call arg) → fixture_lambda_expr
149// - fn literal (anonymous fn body) → fixture_fn_literal
150// - postfix expr (a++ / a-- in stmt body) → fixture_postfix_expr
151// - cast expr (int(x), f64(y), u8(z)) → fixture_cast_expr
152// - field init expr (struct shorthand arg) → fixture_field_init
153// - as-cast expr (sumtype as Variant) → fixture_as_cast_expr
154// - unsafe expr (unsafe { ... }, nil norm) → fixture_unsafe_expr
155// - lock expr (lock x { body } value-form) → fixture_lock_expr
156// - or expr (or-block nested in call args) → fixture_or_expr
157// - selector expr (chained + non-Ident lhs) → fixture_selector_expr
158// - index expr (default + gated + map + slice) → fixture_index_expr
159// - comptime expr ($if guarded body) → fixture_comptime_expr
160// - init expr (struct literal w/ defaults) → fixture_init_expr
161// - return stmt (multi-value + sumtype wrap) → fixture_return_stmt
162// - ident expr (Ident in ported expr ancestors) → fixture_ident
163// - keyword operator (sizeof/isreftype default) → fixture_keyword_operator
164// - assert stmt (simple + cmp value-print branch) → fixture_assert_stmt
165// - tuple if-assign (x, y := if c { a, b } else) → fixture_tuple_if_assign
166// - tuple call-assign (a, b := pair()) → fixture_tuple_call_assign
167// - lock stmt (lock c { c.v = 1 } void-form) → fixture_lock_stmt
168// - if-guard assign (x := if v := maybe() { v } else)→ fixture_if_guard_assign
169// - if-expr assign (x = if cond { a } else { b }) → fixture_if_expr_assign
170// - return-if expr (return if cond { a } else { b }) → fixture_return_if_expr
171// - return-match (return match x { ... }) → fixture_return_match
172// - or stmt (maybe() or { panic("") } stmt) → fixture_or_stmt
173// - or assign (a := maybe() or { 0 }) → fixture_or_assign
174// - or return (return maybe() or { 0 }) → fixture_or_return
175// - for-in map (for k, v in m { ... }) → fixture_for_in_map
176
177// Fixtures intentionally avoid `module main`, `println`, and any builtin
178// dependency — the harness skips the .vh cache load to stay light, so the
179// checker only sees what's in the fixture source. Each fixture must still
180// type-check standalone.
181
182const fixture_plain_fn = '
183fn add(a int, b int) int {
184 return a + b
185}
186
187fn use_add() int {
188 return add(1, 2)
189}
190'
191
192const fixture_infix_operator = '
193struct Vec {
194 x int
195 y int
196}
197
198fn (a Vec) + (b Vec) Vec {
199 return Vec{a.x + b.x, a.y + b.y}
200}
201
202fn use_add() int {
203 v := Vec{1, 2} + Vec{3, 4}
204 return v.x
205}
206'
207
208const fixture_array_contains = '
209fn has(xs []int, n int) bool {
210 return n in xs
211}
212
213fn use_has() bool {
214 return has([1, 2, 3], 2)
215}
216'
217
218const fixture_if_guard = '
219fn maybe(n int) ?int {
220 if n > 0 {
221 return n
222 }
223 return none
224}
225
226fn use_guard() int {
227 if v := maybe(3) {
228 return v
229 }
230 return 0
231}
232'
233
234const fixture_or_block = '
235fn maybe(n int) ?int {
236 if n > 0 {
237 return n
238 }
239 return none
240}
241
242fn use_or() int {
243 x := maybe(2) or { -1 }
244 return x
245}
246'
247
248const fixture_sumtype_is_as = '
249type Shape = Circle | Square
250
251struct Circle {
252 r f64
253}
254
255struct Square {
256 side f64
257}
258
259fn area(s Shape) f64 {
260 return match s {
261 Circle { 3.14 * s.r * s.r }
262 Square { s.side * s.side }
263 }
264}
265
266fn use_shape() f64 {
267 c := Shape(Circle{r: 2.0})
268 if c is Circle {
269 return c.r
270 }
271 return area(c)
272}
273'
274
275const fixture_string_interp = r'
276fn describe(xs []int) string {
277 return "len=${xs.len} first=${xs[0]}"
278}
279
280fn use_describe() string {
281 return describe([10, 20, 30])
282}
283'
284
285const fixture_global_init = '
286__global g_counter = 0
287
288fn bump() {
289 g_counter++
290}
291
292fn use_bump() int {
293 bump()
294 return g_counter
295}
296'
297
298const fixture_const_decl = '
299const x = 42
300const greeting = "hello"
301const sum = 1 + 2
302
303fn use_const() int {
304 return x + sum
305}
306'
307
308const fixture_paren_expr = '
309const paren_lit = (10)
310const paren_nested = ((5))
311
312fn use_paren() int {
313 return paren_lit + paren_nested
314}
315'
316
317const fixture_prefix_expr = '
318const five = 5
319const neg_five = -five
320const inv_bits = ~0xFF
321const not_flag = !true
322
323fn use_prefix() int {
324 if not_flag {
325 return inv_bits
326 }
327 return neg_five
328}
329'
330
331const fixture_modifier_expr = '
332fn bump(mut x int) int {
333 x = x + 1
334 return x
335}
336
337fn use_modifier() int {
338 mut a := 10
339 r := bump(mut a)
340 return r + a
341}
342'
343
344const fixture_lambda_expr = '
345fn apply(f fn (int) int, x int) int {
346 return f(x)
347}
348
349fn use_lambda() int {
350 return apply(|y| y + 1, 5)
351}
352'
353
354const fixture_fn_literal = '
355fn make_doubler() fn (int) int {
356 return fn (x int) int {
357 return x * 2
358 }
359}
360
361fn use_fn_literal() int {
362 doubler := fn (n int) int {
363 return n + n
364 }
365 return doubler(21)
366}
367'
368
369const fixture_postfix_expr = '
370fn use_postfix() int {
371 mut a := 5
372 a++
373 a++
374 a--
375 return a
376}
377'
378
379const fixture_cast_expr = '
380fn use_cast() int {
381 a := f64(3.5)
382 b := int(a)
383 c := u8(b + 1)
384 return int(c) + b
385}
386'
387
388const fixture_field_init = '
389struct Cfg {
390 name string
391 n int
392}
393
394fn make(c Cfg) int {
395 return c.n
396}
397
398fn use_field_init() int {
399 return make(name: "hello", n: 5)
400}
401'
402
403const fixture_or_expr = '
404fn maybe(n int) ?int {
405 if n > 0 {
406 return n
407 }
408 return none
409}
410
411fn pair(a int, b int) int {
412 return a + b
413}
414
415fn use_or_expr(n int) int {
416 return pair(maybe(n) or { -1 }, 2)
417}
418'
419
420const fixture_lock_expr = '
421struct Counter {
422mut:
423 value int
424}
425
426fn use_lock() int {
427 shared c := Counter{
428 value: 0
429 }
430 x := lock c {
431 c.value = 5
432 c.value
433 }
434 return x
435}
436'
437
438const fixture_unsafe_expr = '
439fn use_unsafe() int {
440 p := unsafe { nil }
441 q := unsafe {
442 a := 1
443 a + 2
444 }
445 if p == unsafe { nil } {
446 return q
447 }
448 return 0
449}
450'
451
452const fixture_as_cast_expr = '
453type Shape = Circle | Square
454
455struct Circle {
456 r f64
457}
458
459struct Square {
460 side f64
461}
462
463fn pick(s Shape, k int) f64 {
464 if k == 0 {
465 return (s as Circle).r
466 }
467 return (s as Square).side
468}
469'
470
471const fixture_selector_expr = '
472struct Point {
473 x int
474 y int
475}
476
477struct Box {
478mut:
479 p Point
480 size int
481}
482
483fn use_selector(mut b Box, arr []Point) int {
484 a := b.p.x
485 c := b.size
486 d := arr[0].y
487 b.p.y = 42
488 return a + c + d + b.p.y
489}
490'
491
492const fixture_index_expr = "
493fn use_index(arr []int, m map[string]int) int {
494 a := arr[0] + arr[1]
495 b := arr#[0]
496 c := m['key']
497 d := arr[1..3].len
498 return a + b + c + d
499}
500"
501
502const fixture_comptime_expr = '
503fn use_comptime() int {
504 \$if linux {
505 return 1
506 }
507 return 2
508}
509'
510
511const fixture_keyword_operator = '
512const sz_int = sizeof(int)
513const sz_bool = sizeof(bool)
514const ref_int = isreftype(int)
515
516fn use_keyword_operator() int {
517 return int(sz_int) + int(sz_bool) + int(ref_int)
518}
519'
520
521const fixture_ident = '
522const base = 7
523const neg_base = -base
524const wrapped_base = (base)
525const inv_base = ~base
526
527fn use_ident(mut x int) int {
528 mut a := base
529 mut b := neg_base
530 a = a + wrapped_base
531 b = b - inv_base
532 return a + b + x
533}
534'
535
536const fixture_return_stmt = '
537type Shape2 = Circle2 | Square2
538
539struct Circle2 {
540 r f64
541}
542
543struct Square2 {
544 side f64
545}
546
547fn pair() (int, int) {
548 return 1, 2
549}
550
551fn make_circle(r f64) Shape2 {
552 return Circle2{r: r}
553}
554
555fn use_shape(s Shape2) Shape2 {
556 if s is Circle2 {
557 return s
558 }
559 return s
560}
561'
562
563const fixture_init_expr = '
564struct Item {
565 name string
566 qty int
567 tag string = "default"
568}
569
570struct Crate {
571mut:
572 a Item
573 b Item
574 n int
575}
576
577fn use_init() int {
578 x := Item{
579 name: "apple"
580 qty: 3
581 }
582 c := Crate{
583 a: Item{
584 name: "banana"
585 qty: 5
586 }
587 b: Item{
588 name: "cherry"
589 qty: 7
590 tag: "ripe"
591 }
592 n: 11
593 }
594 return x.qty + c.a.qty + c.b.qty + c.n
595}
596'
597
598const fixture_assert_stmt = '
599fn use_assert(a int, b int) {
600 assert a > 0
601 assert a == b
602 assert a > 0 && b > 0
603}
604'
605
606const fixture_tuple_if_assign = '
607fn use_tuple_if(cond bool) int {
608 x, y := if cond { 10, 20 } else { 30, 40 }
609 return x + y
610}
611'
612
613const fixture_tuple_call_assign = '
614fn pair() (int, int) {
615 return 1, 2
616}
617
618fn use_tuple_call() int {
619 a, b := pair()
620 return a + b
621}
622'
623
624const fixture_lock_stmt = '
625struct LockBox {
626mut:
627 value int
628}
629
630fn use_lock_stmt() {
631 shared c := LockBox{
632 value: 0
633 }
634 lock c {
635 c.value = 5
636 }
637}
638'
639
640const fixture_or_stmt = '
641fn maybe3(n int) ?int {
642 if n > 0 {
643 return n
644 }
645 return none
646}
647
648fn act(n int) {
649}
650
651fn use_or_stmt(n int) {
652 act(maybe3(n) or { -1 })
653}
654'
655
656const fixture_or_assign = '
657fn maybe4(n int) ?int {
658 if n > 0 {
659 return n
660 }
661 return none
662}
663
664fn use_or_assign(n int) {
665 a := maybe4(n) or { 0 }
666 b := maybe4(n) or { -1 }
667 c := maybe4(a + b) or { 7 }
668 _ = c
669}
670'
671
672const fixture_or_return = '
673fn maybe5(n int) ?int {
674 if n > 0 {
675 return n
676 }
677 return none
678}
679
680fn use_or_return(n int) int {
681 return maybe5(n) or { -1 }
682}
683
684fn use_or_return_propagate(n int) ?int {
685 return maybe5(n) or { return none }
686}
687'
688
689const fixture_for_in_map = '
690fn use_for_in_map() int {
691 mut m := map[string]int{}
692 m["a"] = 1
693 m["b"] = 2
694 m["c"] = 3
695 mut total := 0
696 for k, v in m {
697 _ = k
698 total += v
699 }
700 for _, v in m {
701 total += v
702 }
703 for k, _ in m {
704 _ = k
705 }
706 return total
707}
708'
709
710const fixture_map_index_array_push = '
711fn use_map_index_array_push() {
712 mut m := map[int][]int{}
713 m[1] << 2
714}
715'
716
717const fixture_return_match = '
718fn classify(n int) string {
719 return match n {
720 0 { "zero" }
721 1, 2, 3 { "small" }
722 else { "big" }
723 }
724}
725'
726
727const fixture_return_if_expr = '
728fn use_return_if(cond bool) int {
729 return if cond { 10 } else { 20 }
730}
731
732fn use_return_if_chained(a int) int {
733 return if a > 10 { 1 } else if a > 5 { 2 } else { 3 }
734}
735'
736
737const fixture_if_expr_assign = '
738fn use_if_expr_assign(cond bool) int {
739 mut x := 0
740 x = if cond { 10 } else { 20 }
741 return x
742}
743
744fn use_if_expr_assign_chained(a int) int {
745 mut y := 0
746 y = if a > 10 { 1 } else if a > 5 { 2 } else { 3 }
747 return y
748}
749'
750
751const fixture_if_guard_assign = '
752fn maybe2(n int) ?int {
753 if n > 0 {
754 return n
755 }
756 return none
757}
758
759fn use_if_guard_assign() int {
760 x := if v := maybe2(3) {
761 v
762 } else {
763 0
764 }
765 return x
766}
767
768fn use_if_guard_assign_map() int {
769 m := {
770 "a": 1
771 "b": 2
772 }
773 y := if r := m["a"] {
774 r
775 } else {
776 -1
777 }
778 return y
779}
780'
781
782const fixture_generic_fn = '
783fn id[T](x T) T {
784 return x
785}
786
787fn use_id() int {
788 return id[int](3)
789}
790'
791
792fn all_transformer_fixtures() []string {
793 return [
794 fixture_plain_fn,
795 fixture_infix_operator,
796 fixture_array_contains,
797 fixture_if_guard,
798 fixture_or_block,
799 fixture_sumtype_is_as,
800 fixture_string_interp,
801 fixture_global_init,
802 fixture_const_decl,
803 fixture_paren_expr,
804 fixture_prefix_expr,
805 fixture_modifier_expr,
806 fixture_lambda_expr,
807 fixture_fn_literal,
808 fixture_postfix_expr,
809 fixture_cast_expr,
810 fixture_field_init,
811 fixture_as_cast_expr,
812 fixture_unsafe_expr,
813 fixture_lock_expr,
814 fixture_or_expr,
815 fixture_selector_expr,
816 fixture_index_expr,
817 fixture_comptime_expr,
818 fixture_init_expr,
819 fixture_return_stmt,
820 fixture_ident,
821 fixture_keyword_operator,
822 fixture_assert_stmt,
823 fixture_tuple_if_assign,
824 fixture_tuple_call_assign,
825 fixture_lock_stmt,
826 fixture_if_guard_assign,
827 fixture_if_expr_assign,
828 fixture_return_if_expr,
829 fixture_return_match,
830 fixture_or_stmt,
831 fixture_or_assign,
832 fixture_or_return,
833 fixture_for_in_map,
834 ]
835}
836
837// --- determinism: legacy × legacy on the same fixture ---
838//
839// These are the floor: if these ever fail the transformer has stateful
840// drift that will break every other test in this file. Each fixture is
841// re-parsed for each run so per-Transformer accumulator state never
842// leaks between calls.
843
844fn run_determinism(label string, src string) {
845 p := parse_transformer_fixture(src)
846 a := run_legacy_transform(p)
847 b := run_legacy_transform(p)
848 assert_transform_signatures_equal(label, a, b)
849}
850
851fn test_transform_is_deterministic_plain_fn() {
852 run_determinism('det_plain_fn', fixture_plain_fn)
853}
854
855fn test_transform_is_deterministic_infix_operator() {
856 run_determinism('det_infix_operator', fixture_infix_operator)
857}
858
859fn test_transform_is_deterministic_array_contains() {
860 run_determinism('det_array_contains', fixture_array_contains)
861}
862
863fn test_transform_is_deterministic_if_guard() {
864 run_determinism('det_if_guard', fixture_if_guard)
865}
866
867fn test_transform_is_deterministic_or_block() {
868 run_determinism('det_or_block', fixture_or_block)
869}
870
871fn test_transform_is_deterministic_sumtype_is_as() {
872 run_determinism('det_sumtype_is_as', fixture_sumtype_is_as)
873}
874
875fn test_transform_is_deterministic_string_interp() {
876 run_determinism('det_string_interp', fixture_string_interp)
877}
878
879fn test_transform_is_deterministic_global_init() {
880 run_determinism('det_global_init', fixture_global_init)
881}
882
883fn test_transform_is_deterministic_const_decl() {
884 run_determinism('det_const_decl', fixture_const_decl)
885}
886
887fn test_transform_is_deterministic_paren_expr() {
888 run_determinism('det_paren_expr', fixture_paren_expr)
889}
890
891fn test_transform_is_deterministic_prefix_expr() {
892 run_determinism('det_prefix_expr', fixture_prefix_expr)
893}
894
895fn test_transform_is_deterministic_modifier_expr() {
896 run_determinism('det_modifier_expr', fixture_modifier_expr)
897}
898
899fn test_transform_is_deterministic_lambda_expr() {
900 run_determinism('det_lambda_expr', fixture_lambda_expr)
901}
902
903fn test_transform_is_deterministic_fn_literal() {
904 run_determinism('det_fn_literal', fixture_fn_literal)
905}
906
907fn test_transform_is_deterministic_postfix_expr() {
908 run_determinism('det_postfix_expr', fixture_postfix_expr)
909}
910
911fn test_transform_is_deterministic_cast_expr() {
912 run_determinism('det_cast_expr', fixture_cast_expr)
913}
914
915fn test_transform_is_deterministic_field_init() {
916 run_determinism('det_field_init', fixture_field_init)
917}
918
919fn test_transform_is_deterministic_as_cast_expr() {
920 run_determinism('det_as_cast_expr', fixture_as_cast_expr)
921}
922
923fn test_transform_is_deterministic_unsafe_expr() {
924 run_determinism('det_unsafe_expr', fixture_unsafe_expr)
925}
926
927fn test_transform_is_deterministic_lock_expr() {
928 run_determinism('det_lock_expr', fixture_lock_expr)
929}
930
931fn test_transform_is_deterministic_or_expr() {
932 run_determinism('det_or_expr', fixture_or_expr)
933}
934
935fn test_transform_is_deterministic_selector_expr() {
936 run_determinism('det_selector_expr', fixture_selector_expr)
937}
938
939fn test_transform_is_deterministic_index_expr() {
940 run_determinism('det_index_expr', fixture_index_expr)
941}
942
943fn test_transform_is_deterministic_comptime_expr() {
944 run_determinism('det_comptime_expr', fixture_comptime_expr)
945}
946
947fn test_transform_is_deterministic_init_expr() {
948 run_determinism('det_init_expr', fixture_init_expr)
949}
950
951fn test_transform_is_deterministic_return_stmt() {
952 run_determinism('det_return_stmt', fixture_return_stmt)
953}
954
955fn test_transform_is_deterministic_ident() {
956 run_determinism('det_ident', fixture_ident)
957}
958
959fn test_transform_is_deterministic_keyword_operator() {
960 run_determinism('det_keyword_operator', fixture_keyword_operator)
961}
962
963fn test_transform_is_deterministic_assert_stmt() {
964 run_determinism('det_assert_stmt', fixture_assert_stmt)
965}
966
967fn test_transform_is_deterministic_tuple_if_assign() {
968 run_determinism('det_tuple_if_assign', fixture_tuple_if_assign)
969}
970
971fn test_transform_is_deterministic_tuple_call_assign() {
972 run_determinism('det_tuple_call_assign', fixture_tuple_call_assign)
973}
974
975fn test_transform_is_deterministic_lock_stmt() {
976 run_determinism('det_lock_stmt', fixture_lock_stmt)
977}
978
979fn test_transform_is_deterministic_if_guard_assign() {
980 run_determinism('det_if_guard_assign', fixture_if_guard_assign)
981}
982
983fn test_transform_is_deterministic_if_expr_assign() {
984 run_determinism('det_if_expr_assign', fixture_if_expr_assign)
985}
986
987fn test_transform_is_deterministic_return_if_expr() {
988 run_determinism('det_return_if_expr', fixture_return_if_expr)
989}
990
991fn test_transform_is_deterministic_return_match() {
992 run_determinism('det_return_match', fixture_return_match)
993}
994
995fn test_transform_is_deterministic_or_stmt() {
996 run_determinism('det_or_stmt', fixture_or_stmt)
997}
998
999fn test_transform_is_deterministic_or_assign() {
1000 run_determinism('det_or_assign', fixture_or_assign)
1001}
1002
1003fn test_transform_is_deterministic_or_return() {
1004 run_determinism('det_or_return', fixture_or_return)
1005}
1006
1007fn test_transform_is_deterministic_for_in_map() {
1008 run_determinism('det_for_in_map', fixture_for_in_map)
1009}
1010
1011// --- parity: transform_files vs transform_files_from_flat ---
1012//
1013// The streaming-from-flat path is the seed for the upcoming
1014// transformer-writes-flat work. Each fixture is parsed ONCE so the
1015// filenames embedded in pos info match; both transformer paths get
1016// independent Transformer instances over the same parsed input and shared
1017// env. A drift here indicates either a pre_pass_from_flat seeding bug or
1018// in-flight rehydration losing a node, both of which are exactly what
1019// this harness exists to catch.
1020
1021fn run_parity(label string, src string) {
1022 p := parse_transformer_fixture(src)
1023 leg := run_legacy_transform(p)
1024 flt := run_from_flat_transform(p)
1025 assert_transform_signatures_equal(label, leg, flt)
1026}
1027
1028fn test_flat_parity_plain_fn() {
1029 run_parity('parity_plain_fn', fixture_plain_fn)
1030}
1031
1032fn test_flat_parity_infix_operator() {
1033 run_parity('parity_infix_operator', fixture_infix_operator)
1034}
1035
1036fn test_flat_parity_array_contains() {
1037 run_parity('parity_array_contains', fixture_array_contains)
1038}
1039
1040fn test_flat_parity_if_guard() {
1041 run_parity('parity_if_guard', fixture_if_guard)
1042}
1043
1044fn test_flat_parity_or_block() {
1045 run_parity('parity_or_block', fixture_or_block)
1046}
1047
1048fn test_flat_parity_sumtype_is_as() {
1049 run_parity('parity_sumtype_is_as', fixture_sumtype_is_as)
1050}
1051
1052fn test_flat_parity_string_interp() {
1053 run_parity('parity_string_interp', fixture_string_interp)
1054}
1055
1056fn test_flat_parity_global_init() {
1057 run_parity('parity_global_init', fixture_global_init)
1058}
1059
1060fn test_flat_parity_const_decl() {
1061 run_parity('parity_const_decl', fixture_const_decl)
1062}
1063
1064fn test_flat_parity_paren_expr() {
1065 run_parity('parity_paren_expr', fixture_paren_expr)
1066}
1067
1068fn test_flat_parity_prefix_expr() {
1069 run_parity('parity_prefix_expr', fixture_prefix_expr)
1070}
1071
1072fn test_flat_parity_modifier_expr() {
1073 run_parity('parity_modifier_expr', fixture_modifier_expr)
1074}
1075
1076fn test_flat_parity_lambda_expr() {
1077 run_parity('parity_lambda_expr', fixture_lambda_expr)
1078}
1079
1080fn test_flat_parity_fn_literal() {
1081 run_parity('parity_fn_literal', fixture_fn_literal)
1082}
1083
1084fn test_flat_parity_postfix_expr() {
1085 run_parity('parity_postfix_expr', fixture_postfix_expr)
1086}
1087
1088fn test_flat_parity_cast_expr() {
1089 run_parity('parity_cast_expr', fixture_cast_expr)
1090}
1091
1092fn test_flat_parity_field_init() {
1093 run_parity('parity_field_init', fixture_field_init)
1094}
1095
1096fn test_flat_parity_as_cast_expr() {
1097 run_parity('parity_as_cast_expr', fixture_as_cast_expr)
1098}
1099
1100fn test_flat_parity_unsafe_expr() {
1101 run_parity('parity_unsafe_expr', fixture_unsafe_expr)
1102}
1103
1104fn test_flat_parity_lock_expr() {
1105 run_parity('parity_lock_expr', fixture_lock_expr)
1106}
1107
1108fn test_flat_parity_or_expr() {
1109 run_parity('parity_or_expr', fixture_or_expr)
1110}
1111
1112fn test_flat_parity_selector_expr() {
1113 run_parity('parity_selector_expr', fixture_selector_expr)
1114}
1115
1116fn test_flat_parity_index_expr() {
1117 run_parity('parity_index_expr', fixture_index_expr)
1118}
1119
1120fn test_flat_parity_comptime_expr() {
1121 run_parity('parity_comptime_expr', fixture_comptime_expr)
1122}
1123
1124fn test_flat_parity_init_expr() {
1125 run_parity('parity_init_expr', fixture_init_expr)
1126}
1127
1128fn test_flat_parity_return_stmt() {
1129 run_parity('parity_return_stmt', fixture_return_stmt)
1130}
1131
1132fn test_flat_parity_ident() {
1133 run_parity('parity_ident', fixture_ident)
1134}
1135
1136fn test_flat_parity_keyword_operator() {
1137 run_parity('parity_keyword_operator', fixture_keyword_operator)
1138}
1139
1140fn test_flat_parity_assert_stmt() {
1141 run_parity('parity_assert_stmt', fixture_assert_stmt)
1142}
1143
1144fn test_flat_parity_tuple_if_assign() {
1145 run_parity('parity_tuple_if_assign', fixture_tuple_if_assign)
1146}
1147
1148fn test_flat_parity_tuple_call_assign() {
1149 run_parity('parity_tuple_call_assign', fixture_tuple_call_assign)
1150}
1151
1152fn test_flat_parity_lock_stmt() {
1153 run_parity('parity_lock_stmt', fixture_lock_stmt)
1154}
1155
1156fn test_flat_parity_if_guard_assign() {
1157 run_parity('parity_if_guard_assign', fixture_if_guard_assign)
1158}
1159
1160fn test_flat_parity_if_expr_assign() {
1161 run_parity('parity_if_expr_assign', fixture_if_expr_assign)
1162}
1163
1164fn test_flat_parity_return_if_expr() {
1165 run_parity('parity_return_if_expr', fixture_return_if_expr)
1166}
1167
1168fn test_flat_parity_return_match() {
1169 run_parity('parity_return_match', fixture_return_match)
1170}
1171
1172fn test_flat_parity_or_stmt() {
1173 run_parity('parity_or_stmt', fixture_or_stmt)
1174}
1175
1176fn test_flat_parity_or_assign() {
1177 run_parity('parity_or_assign', fixture_or_assign)
1178}
1179
1180fn test_flat_parity_or_return() {
1181 run_parity('parity_or_return', fixture_or_return)
1182}
1183
1184fn test_flat_parity_for_in_map() {
1185 run_parity('parity_for_in_map', fixture_for_in_map)
1186}
1187
1188// --- parity: check_files vs check_flat upstream ---
1189//
1190// The flat-input checker (Checker.check_flat) is the next migration row in
1191// the parse → check → transform → markused → SSA chain. This family asserts
1192// that the env produced
1193// by check_flat is shape-compatible with the env from check_files — by
1194// running an identical transform over each and comparing signatures of
1195// the result.
1196//
1197// The transformer reads from env (type lookups, method tables) for many
1198// rewrites; any divergence in env state surfaces as a transformer-output
1199// drift here, with the diff narrowing the culprit to one fixture. This
1200// is the same pattern as the markused harness, but at the layer above.
1201
1202fn run_check_flat_parity(label string, src string) {
1203 // Parse ONCE: same filename embedded in pos info, then run two
1204 // independent checkers (legacy / flat) against shared parser output
1205 // but separate env instances. Each env then feeds an independent
1206 // Transformer; outputs must produce identical signatures.
1207 tmp := '/tmp/v2_transformer_diff_${os.getpid()}_${transformer_rand_suffix()}.v'
1208 os.write_file(tmp, src) or { panic('write_file: ${err}') }
1209 defer {
1210 os.rm(tmp) or {}
1211 }
1212 prefs := &vpref.Preferences{
1213 backend: .cleanc
1214 no_parallel: true
1215 }
1216 mut fs := token.FileSet.new()
1217 mut par := parser.Parser.new(prefs)
1218 files := par.parse_files([tmp], mut fs)
1219 flat := ast.flatten_files(files)
1220
1221 mut env_legacy := types.Environment.new()
1222 mut ck_legacy := types.Checker.new(prefs, fs, env_legacy)
1223 ck_legacy.check_files(files)
1224
1225 mut env_flat := types.Environment.new()
1226 mut ck_flat := types.Checker.new(prefs, fs, env_flat)
1227 ck_flat.check_flat(&flat)
1228
1229 mut t_a := Transformer.new_with_pref(env_legacy, prefs)
1230 leg := t_a.transform_files(files)
1231 mut t_b := Transformer.new_with_pref(env_flat, prefs)
1232 flt := t_b.transform_files(files)
1233 assert_transform_signatures_equal(label, leg, flt)
1234}
1235
1236fn test_check_flat_parity_plain_fn() {
1237 run_check_flat_parity('check_flat_plain_fn', fixture_plain_fn)
1238}
1239
1240fn test_check_flat_parity_infix_operator() {
1241 run_check_flat_parity('check_flat_infix_operator', fixture_infix_operator)
1242}
1243
1244fn test_check_flat_parity_array_contains() {
1245 run_check_flat_parity('check_flat_array_contains', fixture_array_contains)
1246}
1247
1248fn test_check_flat_parity_if_guard() {
1249 run_check_flat_parity('check_flat_if_guard', fixture_if_guard)
1250}
1251
1252fn test_check_flat_parity_or_block() {
1253 run_check_flat_parity('check_flat_or_block', fixture_or_block)
1254}
1255
1256fn test_check_flat_parity_sumtype_is_as() {
1257 run_check_flat_parity('check_flat_sumtype_is_as', fixture_sumtype_is_as)
1258}
1259
1260fn test_check_flat_parity_string_interp() {
1261 run_check_flat_parity('check_flat_string_interp', fixture_string_interp)
1262}
1263
1264fn test_check_flat_parity_global_init() {
1265 run_check_flat_parity('check_flat_global_init', fixture_global_init)
1266}
1267
1268fn test_check_flat_parity_const_decl() {
1269 run_check_flat_parity('check_flat_const_decl', fixture_const_decl)
1270}
1271
1272fn test_check_flat_parity_paren_expr() {
1273 run_check_flat_parity('check_flat_paren_expr', fixture_paren_expr)
1274}
1275
1276fn test_check_flat_parity_prefix_expr() {
1277 run_check_flat_parity('check_flat_prefix_expr', fixture_prefix_expr)
1278}
1279
1280fn test_check_flat_parity_modifier_expr() {
1281 run_check_flat_parity('check_flat_modifier_expr', fixture_modifier_expr)
1282}
1283
1284fn test_check_flat_parity_lambda_expr() {
1285 run_check_flat_parity('check_flat_lambda_expr', fixture_lambda_expr)
1286}
1287
1288fn test_check_flat_parity_fn_literal() {
1289 run_check_flat_parity('check_flat_fn_literal', fixture_fn_literal)
1290}
1291
1292fn test_check_flat_parity_postfix_expr() {
1293 run_check_flat_parity('check_flat_postfix_expr', fixture_postfix_expr)
1294}
1295
1296fn test_check_flat_parity_cast_expr() {
1297 run_check_flat_parity('check_flat_cast_expr', fixture_cast_expr)
1298}
1299
1300fn test_check_flat_parity_field_init() {
1301 run_check_flat_parity('check_flat_field_init', fixture_field_init)
1302}
1303
1304fn test_check_flat_parity_as_cast_expr() {
1305 run_check_flat_parity('check_flat_as_cast_expr', fixture_as_cast_expr)
1306}
1307
1308fn test_check_flat_parity_unsafe_expr() {
1309 run_check_flat_parity('check_flat_unsafe_expr', fixture_unsafe_expr)
1310}
1311
1312fn test_check_flat_parity_lock_expr() {
1313 run_check_flat_parity('check_flat_lock_expr', fixture_lock_expr)
1314}
1315
1316fn test_check_flat_parity_or_expr() {
1317 run_check_flat_parity('check_flat_or_expr', fixture_or_expr)
1318}
1319
1320fn test_check_flat_parity_selector_expr() {
1321 run_check_flat_parity('check_flat_selector_expr', fixture_selector_expr)
1322}
1323
1324fn test_check_flat_parity_index_expr() {
1325 run_check_flat_parity('check_flat_index_expr', fixture_index_expr)
1326}
1327
1328fn test_check_flat_parity_comptime_expr() {
1329 run_check_flat_parity('check_flat_comptime_expr', fixture_comptime_expr)
1330}
1331
1332fn test_check_flat_parity_init_expr() {
1333 run_check_flat_parity('check_flat_init_expr', fixture_init_expr)
1334}
1335
1336fn test_check_flat_parity_return_stmt() {
1337 run_check_flat_parity('check_flat_return_stmt', fixture_return_stmt)
1338}
1339
1340fn test_check_flat_parity_ident() {
1341 run_check_flat_parity('check_flat_ident', fixture_ident)
1342}
1343
1344fn test_check_flat_parity_keyword_operator() {
1345 run_check_flat_parity('check_flat_keyword_operator', fixture_keyword_operator)
1346}
1347
1348fn test_check_flat_parity_assert_stmt() {
1349 run_check_flat_parity('check_flat_assert_stmt', fixture_assert_stmt)
1350}
1351
1352fn test_check_flat_parity_tuple_if_assign() {
1353 run_check_flat_parity('check_flat_tuple_if_assign', fixture_tuple_if_assign)
1354}
1355
1356fn test_check_flat_parity_tuple_call_assign() {
1357 run_check_flat_parity('check_flat_tuple_call_assign', fixture_tuple_call_assign)
1358}
1359
1360fn test_check_flat_parity_lock_stmt() {
1361 run_check_flat_parity('check_flat_lock_stmt', fixture_lock_stmt)
1362}
1363
1364fn test_check_flat_parity_if_guard_assign() {
1365 run_check_flat_parity('check_flat_if_guard_assign', fixture_if_guard_assign)
1366}
1367
1368fn test_check_flat_parity_if_expr_assign() {
1369 run_check_flat_parity('check_flat_if_expr_assign', fixture_if_expr_assign)
1370}
1371
1372fn test_check_flat_parity_return_if_expr() {
1373 run_check_flat_parity('check_flat_return_if_expr', fixture_return_if_expr)
1374}
1375
1376fn test_check_flat_parity_return_match() {
1377 run_check_flat_parity('check_flat_return_match', fixture_return_match)
1378}
1379
1380fn test_check_flat_parity_or_stmt() {
1381 run_check_flat_parity('check_flat_or_stmt', fixture_or_stmt)
1382}
1383
1384fn test_check_flat_parity_or_assign() {
1385 run_check_flat_parity('check_flat_or_assign', fixture_or_assign)
1386}
1387
1388fn test_check_flat_parity_or_return() {
1389 run_check_flat_parity('check_flat_or_return', fixture_or_return)
1390}
1391
1392fn test_check_flat_parity_for_in_map() {
1393 run_check_flat_parity('check_flat_for_in_map', fixture_for_in_map)
1394}
1395
1396// --- parity: transform_files vs transform_files_to_flat ---
1397//
1398// transform_files_to_flat is the API wedge for the future
1399// "transformer writes directly into a FlatBuilder" port. Today it
1400// composes transform_files_from_flat with a boundary flatten_files();
1401// callers that only need flat output route through here and will get the
1402// eventual peak-memory win
1403// without further changes.
1404//
1405// This row pins the contract: the FlatAst returned by
1406// transform_files_to_flat must have the same signature as
1407// flatten_files(transform_files(files)). When the internals are rewritten
1408// to skip the boundary flatten, this is the regression net.
1409
1410fn run_to_flat_parity(label string, src string) {
1411 p := parse_transformer_fixture(src)
1412 leg := run_legacy_transform(p)
1413 leg_sig := transform_signature(leg)
1414
1415 mut t := Transformer.new_with_pref(p.env, p.prefs)
1416 new_flat, _ := t.transform_files_to_flat(&p.flat, [])
1417 flt_sig := new_flat.signature()
1418
1419 if leg_sig == flt_sig {
1420 return
1421 }
1422 pa, pb := dump_signature_pair(label, leg_sig, flt_sig)
1423 eprintln('[${label}] transform_files_to_flat signature diverged from legacy.')
1424 eprintln(' legacy: ${pa}')
1425 eprintln(' to_flat: ${pb}')
1426 eprintln(' diff with: diff -u ${pa} ${pb}')
1427 assert false, '${label}: transform_files_to_flat output diverged (see /tmp dumps above)'
1428}
1429
1430fn test_to_flat_parity_plain_fn() {
1431 run_to_flat_parity('to_flat_plain_fn', fixture_plain_fn)
1432}
1433
1434fn test_to_flat_parity_infix_operator() {
1435 run_to_flat_parity('to_flat_infix_operator', fixture_infix_operator)
1436}
1437
1438fn test_to_flat_parity_array_contains() {
1439 run_to_flat_parity('to_flat_array_contains', fixture_array_contains)
1440}
1441
1442fn test_to_flat_parity_if_guard() {
1443 run_to_flat_parity('to_flat_if_guard', fixture_if_guard)
1444}
1445
1446fn test_to_flat_parity_or_block() {
1447 run_to_flat_parity('to_flat_or_block', fixture_or_block)
1448}
1449
1450fn test_to_flat_parity_sumtype_is_as() {
1451 run_to_flat_parity('to_flat_sumtype_is_as', fixture_sumtype_is_as)
1452}
1453
1454fn test_to_flat_parity_string_interp() {
1455 run_to_flat_parity('to_flat_string_interp', fixture_string_interp)
1456}
1457
1458fn test_to_flat_parity_global_init() {
1459 run_to_flat_parity('to_flat_global_init', fixture_global_init)
1460}
1461
1462fn test_to_flat_parity_const_decl() {
1463 run_to_flat_parity('to_flat_const_decl', fixture_const_decl)
1464}
1465
1466fn test_to_flat_parity_paren_expr() {
1467 run_to_flat_parity('to_flat_paren_expr', fixture_paren_expr)
1468}
1469
1470fn test_to_flat_parity_prefix_expr() {
1471 run_to_flat_parity('to_flat_prefix_expr', fixture_prefix_expr)
1472}
1473
1474fn test_to_flat_parity_modifier_expr() {
1475 run_to_flat_parity('to_flat_modifier_expr', fixture_modifier_expr)
1476}
1477
1478fn test_to_flat_parity_lambda_expr() {
1479 run_to_flat_parity('to_flat_lambda_expr', fixture_lambda_expr)
1480}
1481
1482fn test_to_flat_parity_fn_literal() {
1483 run_to_flat_parity('to_flat_fn_literal', fixture_fn_literal)
1484}
1485
1486fn test_to_flat_parity_postfix_expr() {
1487 run_to_flat_parity('to_flat_postfix_expr', fixture_postfix_expr)
1488}
1489
1490fn test_to_flat_parity_cast_expr() {
1491 run_to_flat_parity('to_flat_cast_expr', fixture_cast_expr)
1492}
1493
1494fn test_to_flat_parity_field_init() {
1495 run_to_flat_parity('to_flat_field_init', fixture_field_init)
1496}
1497
1498fn test_to_flat_parity_as_cast_expr() {
1499 run_to_flat_parity('to_flat_as_cast_expr', fixture_as_cast_expr)
1500}
1501
1502fn test_to_flat_parity_unsafe_expr() {
1503 run_to_flat_parity('to_flat_unsafe_expr', fixture_unsafe_expr)
1504}
1505
1506fn test_to_flat_parity_lock_expr() {
1507 run_to_flat_parity('to_flat_lock_expr', fixture_lock_expr)
1508}
1509
1510fn test_to_flat_parity_or_expr() {
1511 run_to_flat_parity('to_flat_or_expr', fixture_or_expr)
1512}
1513
1514fn test_to_flat_parity_selector_expr() {
1515 run_to_flat_parity('to_flat_selector_expr', fixture_selector_expr)
1516}
1517
1518fn test_to_flat_parity_index_expr() {
1519 run_to_flat_parity('to_flat_index_expr', fixture_index_expr)
1520}
1521
1522fn test_to_flat_parity_comptime_expr() {
1523 run_to_flat_parity('to_flat_comptime_expr', fixture_comptime_expr)
1524}
1525
1526fn test_to_flat_parity_init_expr() {
1527 run_to_flat_parity('to_flat_init_expr', fixture_init_expr)
1528}
1529
1530fn test_to_flat_parity_return_stmt() {
1531 run_to_flat_parity('to_flat_return_stmt', fixture_return_stmt)
1532}
1533
1534fn test_to_flat_parity_ident() {
1535 run_to_flat_parity('to_flat_ident', fixture_ident)
1536}
1537
1538fn test_to_flat_parity_keyword_operator() {
1539 run_to_flat_parity('to_flat_keyword_operator', fixture_keyword_operator)
1540}
1541
1542fn test_to_flat_parity_assert_stmt() {
1543 run_to_flat_parity('to_flat_assert_stmt', fixture_assert_stmt)
1544}
1545
1546fn test_to_flat_parity_tuple_if_assign() {
1547 run_to_flat_parity('to_flat_tuple_if_assign', fixture_tuple_if_assign)
1548}
1549
1550fn test_to_flat_parity_tuple_call_assign() {
1551 run_to_flat_parity('to_flat_tuple_call_assign', fixture_tuple_call_assign)
1552}
1553
1554fn test_to_flat_parity_lock_stmt() {
1555 run_to_flat_parity('to_flat_lock_stmt', fixture_lock_stmt)
1556}
1557
1558fn test_to_flat_parity_if_guard_assign() {
1559 run_to_flat_parity('to_flat_if_guard_assign', fixture_if_guard_assign)
1560}
1561
1562fn test_to_flat_parity_if_expr_assign() {
1563 run_to_flat_parity('to_flat_if_expr_assign', fixture_if_expr_assign)
1564}
1565
1566fn test_to_flat_parity_return_if_expr() {
1567 run_to_flat_parity('to_flat_return_if_expr', fixture_return_if_expr)
1568}
1569
1570fn test_to_flat_parity_return_match() {
1571 run_to_flat_parity('to_flat_return_match', fixture_return_match)
1572}
1573
1574fn test_to_flat_parity_or_stmt() {
1575 run_to_flat_parity('to_flat_or_stmt', fixture_or_stmt)
1576}
1577
1578fn test_to_flat_parity_or_assign() {
1579 run_to_flat_parity('to_flat_or_assign', fixture_or_assign)
1580}
1581
1582fn test_to_flat_parity_or_return() {
1583 run_to_flat_parity('to_flat_or_return', fixture_or_return)
1584}
1585
1586fn test_to_flat_parity_for_in_map() {
1587 run_to_flat_parity('to_flat_for_in_map', fixture_for_in_map)
1588}
1589
1590// --- parity: per-file flat-write API vs reference rehydrate+transform+append ---
1591//
1592// `transform_file_index_to_flat` (flat_write.v) is the per-file entry point
1593// for the multi-session port that progressively rewrites the transformer to
1594// emit flat nodes directly into a FlatBuilder. Today the body is a behaviour-
1595// preserving decomposition of the loop inside `transform_files_from_flat`;
1596// sessions 2..N replace one rewrite site at a time inside the body.
1597//
1598// This row pins the per-file invariant: driving the new API in a loop must
1599// produce a FlatBuilder bit-equal to manually doing rehydrate + transform +
1600// append_file for each file index. Each future session must keep this
1601// invariant green after porting its rewrite site.
1602//
1603// The 4th row (to_flat_*) covers the wedge-level invariant against legacy.
1604// This row is one layer below: it isolates the per-file API so a regression
1605// inside the new body fails here independently of any post_pass change.
1606
1607fn run_per_file_parity(label string, src string) {
1608 p := parse_transformer_fixture(src)
1609
1610 // Reference: manual rehydrate + transform + append per file index.
1611 mut t_ref := Transformer.new_with_pref(p.env, p.prefs)
1612 t_ref.pre_pass_from_flat(&p.flat)
1613 mut ref_builder := ast.new_flat_builder()
1614 for fi in 0 .. p.flat.files.len {
1615 src_arr := p.flat.to_files_range(fi, fi + 1)
1616 if src_arr.len == 0 {
1617 continue
1618 }
1619 transformed := t_ref.transform_file_pub(src_arr[0])
1620 ref_builder.append_file(transformed)
1621 }
1622 ref_sig := ref_builder.flat.signature()
1623
1624 // New per-file API driven in the same loop, fresh Transformer instance so
1625 // no internal counter state leaks between the two paths.
1626 mut t_api := Transformer.new_with_pref(p.env, p.prefs)
1627 t_api.pre_pass_from_flat(&p.flat)
1628 mut api_builder := ast.new_flat_builder()
1629 for fi in 0 .. p.flat.files.len {
1630 t_api.transform_file_index_to_flat(&p.flat, fi, mut api_builder)
1631 }
1632 api_sig := api_builder.flat.signature()
1633
1634 if ref_sig == api_sig {
1635 return
1636 }
1637 pa, pb := dump_signature_pair(label, ref_sig, api_sig)
1638 eprintln('[${label}] per-file API signature diverged from reference loop.')
1639 eprintln(' ref: ${pa}')
1640 eprintln(' api: ${pb}')
1641 eprintln(' diff with: diff -u ${pa} ${pb}')
1642 assert false, '${label}: transform_file_index_to_flat output diverged (see /tmp dumps above)'
1643}
1644
1645fn test_per_file_parity_plain_fn() {
1646 run_per_file_parity('per_file_plain_fn', fixture_plain_fn)
1647}
1648
1649fn test_per_file_parity_infix_operator() {
1650 run_per_file_parity('per_file_infix_operator', fixture_infix_operator)
1651}
1652
1653fn test_per_file_parity_array_contains() {
1654 run_per_file_parity('per_file_array_contains', fixture_array_contains)
1655}
1656
1657fn test_per_file_parity_if_guard() {
1658 run_per_file_parity('per_file_if_guard', fixture_if_guard)
1659}
1660
1661fn test_per_file_parity_or_block() {
1662 run_per_file_parity('per_file_or_block', fixture_or_block)
1663}
1664
1665fn test_per_file_parity_sumtype_is_as() {
1666 run_per_file_parity('per_file_sumtype_is_as', fixture_sumtype_is_as)
1667}
1668
1669fn test_per_file_parity_string_interp() {
1670 run_per_file_parity('per_file_string_interp', fixture_string_interp)
1671}
1672
1673fn test_per_file_parity_global_init() {
1674 run_per_file_parity('per_file_global_init', fixture_global_init)
1675}
1676
1677fn test_per_file_parity_const_decl() {
1678 run_per_file_parity('per_file_const_decl', fixture_const_decl)
1679}
1680
1681fn test_per_file_parity_paren_expr() {
1682 run_per_file_parity('per_file_paren_expr', fixture_paren_expr)
1683}
1684
1685fn test_per_file_parity_prefix_expr() {
1686 run_per_file_parity('per_file_prefix_expr', fixture_prefix_expr)
1687}
1688
1689fn test_per_file_parity_modifier_expr() {
1690 run_per_file_parity('per_file_modifier_expr', fixture_modifier_expr)
1691}
1692
1693fn test_per_file_parity_lambda_expr() {
1694 run_per_file_parity('per_file_lambda_expr', fixture_lambda_expr)
1695}
1696
1697fn test_per_file_parity_fn_literal() {
1698 run_per_file_parity('per_file_fn_literal', fixture_fn_literal)
1699}
1700
1701fn test_per_file_parity_postfix_expr() {
1702 run_per_file_parity('per_file_postfix_expr', fixture_postfix_expr)
1703}
1704
1705fn test_per_file_parity_cast_expr() {
1706 run_per_file_parity('per_file_cast_expr', fixture_cast_expr)
1707}
1708
1709fn test_per_file_parity_field_init() {
1710 run_per_file_parity('per_file_field_init', fixture_field_init)
1711}
1712
1713fn test_per_file_parity_as_cast_expr() {
1714 run_per_file_parity('per_file_as_cast_expr', fixture_as_cast_expr)
1715}
1716
1717fn test_per_file_parity_unsafe_expr() {
1718 run_per_file_parity('per_file_unsafe_expr', fixture_unsafe_expr)
1719}
1720
1721fn test_per_file_parity_lock_expr() {
1722 run_per_file_parity('per_file_lock_expr', fixture_lock_expr)
1723}
1724
1725fn test_per_file_parity_or_expr() {
1726 run_per_file_parity('per_file_or_expr', fixture_or_expr)
1727}
1728
1729fn test_per_file_parity_selector_expr() {
1730 run_per_file_parity('per_file_selector_expr', fixture_selector_expr)
1731}
1732
1733fn test_per_file_parity_index_expr() {
1734 run_per_file_parity('per_file_index_expr', fixture_index_expr)
1735}
1736
1737fn test_per_file_parity_comptime_expr() {
1738 run_per_file_parity('per_file_comptime_expr', fixture_comptime_expr)
1739}
1740
1741fn test_per_file_parity_init_expr() {
1742 run_per_file_parity('per_file_init_expr', fixture_init_expr)
1743}
1744
1745fn test_per_file_parity_return_stmt() {
1746 run_per_file_parity('per_file_return_stmt', fixture_return_stmt)
1747}
1748
1749fn test_per_file_parity_ident() {
1750 run_per_file_parity('per_file_ident', fixture_ident)
1751}
1752
1753fn test_per_file_parity_keyword_operator() {
1754 run_per_file_parity('per_file_keyword_operator', fixture_keyword_operator)
1755}
1756
1757fn test_per_file_parity_assert_stmt() {
1758 run_per_file_parity('per_file_assert_stmt', fixture_assert_stmt)
1759}
1760
1761fn test_per_file_parity_tuple_if_assign() {
1762 run_per_file_parity('per_file_tuple_if_assign', fixture_tuple_if_assign)
1763}
1764
1765fn test_per_file_parity_tuple_call_assign() {
1766 run_per_file_parity('per_file_tuple_call_assign', fixture_tuple_call_assign)
1767}
1768
1769fn test_per_file_parity_lock_stmt() {
1770 run_per_file_parity('per_file_lock_stmt', fixture_lock_stmt)
1771}
1772
1773fn test_per_file_parity_if_guard_assign() {
1774 run_per_file_parity('per_file_if_guard_assign', fixture_if_guard_assign)
1775}
1776
1777fn test_per_file_parity_if_expr_assign() {
1778 run_per_file_parity('per_file_if_expr_assign', fixture_if_expr_assign)
1779}
1780
1781fn test_per_file_parity_return_if_expr() {
1782 run_per_file_parity('per_file_return_if_expr', fixture_return_if_expr)
1783}
1784
1785fn test_per_file_parity_return_match() {
1786 run_per_file_parity('per_file_return_match', fixture_return_match)
1787}
1788
1789fn test_per_file_parity_or_stmt() {
1790 run_per_file_parity('per_file_or_stmt', fixture_or_stmt)
1791}
1792
1793fn test_per_file_parity_or_assign() {
1794 run_per_file_parity('per_file_or_assign', fixture_or_assign)
1795}
1796
1797fn test_per_file_parity_or_return() {
1798 run_per_file_parity('per_file_or_return', fixture_or_return)
1799}
1800
1801fn test_per_file_parity_for_in_map() {
1802 run_per_file_parity('per_file_for_in_map', fixture_for_in_map)
1803}
1804
1805// --- parity: transform_files_to_flat_direct per-file subtree vs legacy ---
1806//
1807// `transform_files_to_flat_direct` is the native low-memory path: it keeps the
1808// existing whole-program prepare/monomorphize step, but emits each transformed
1809// file directly into a FlatBuilder instead of collecting a transformed
1810// []ast.File result. Its observable tree must match legacy transform output.
1811fn run_to_flat_direct_parity(label string, src string) {
1812 p := parse_transformer_fixture(src)
1813 leg := run_legacy_transform(p)
1814 leg_flat := ast.flatten_files(leg)
1815
1816 mut t := Transformer.new_with_pref(p.env, p.prefs)
1817 new_flat := t.transform_files_to_flat_direct(p.files)
1818
1819 if leg_flat.files.len != new_flat.files.len {
1820 assert false, '${label}: file count mismatch: legacy=${leg_flat.files.len} direct=${new_flat.files.len}'
1821 return
1822 }
1823
1824 for i in 0 .. leg_flat.files.len {
1825 leg_stmts := leg_flat.child_at(leg_flat.files[i].file_id, 2)
1826 new_stmts := new_flat.child_at(new_flat.files[i].file_id, 2)
1827 leg_sub_sig := leg_flat.subtree_signature(leg_stmts)
1828 new_sub_sig := new_flat.subtree_signature(new_stmts)
1829 if leg_sub_sig == new_sub_sig {
1830 continue
1831 }
1832 pa, pb := dump_signature_pair('${label}_file${i}', leg_sub_sig, new_sub_sig)
1833 eprintln('[${label}] transform_files_to_flat_direct file ${i} subtree diverged from legacy.')
1834 eprintln(' legacy: ${pa}')
1835 eprintln(' direct: ${pb}')
1836 eprintln(' diff with: diff -u ${pa} ${pb}')
1837 assert false, '${label}: transform_files_to_flat_direct output diverged at file ${i} (see /tmp dumps above)'
1838 }
1839}
1840
1841fn test_to_flat_direct_parity_plain_fn() {
1842 run_to_flat_direct_parity('to_flat_direct_plain_fn', fixture_plain_fn)
1843}
1844
1845fn test_to_flat_direct_parity_if_guard_assign() {
1846 run_to_flat_direct_parity('to_flat_direct_if_guard_assign', fixture_if_guard_assign)
1847}
1848
1849fn run_flat_input_to_flat_direct_matches_file_input_direct(label string, src string) {
1850 p_files := parse_transformer_fixture(src)
1851 p_flat := parse_transformer_fixture(src)
1852
1853 mut t_files := Transformer.new_with_pref(p_files.env, p_files.prefs)
1854 files_direct := t_files.transform_files_to_flat_direct(p_files.files)
1855
1856 mut t_flat := Transformer.new_with_pref(p_flat.env, p_flat.prefs)
1857 flat_direct := t_flat.transform_flat_to_flat_direct(&p_flat.flat, [])
1858
1859 if files_direct.files.len != flat_direct.files.len {
1860 assert false, '${label}: file count mismatch: files=${files_direct.files.len} flat=${flat_direct.files.len}'
1861 return
1862 }
1863 for i in 0 .. files_direct.files.len {
1864 files_stmts := files_direct.child_at(files_direct.files[i].file_id, 2)
1865 flat_stmts := flat_direct.child_at(flat_direct.files[i].file_id, 2)
1866 files_sig := files_direct.subtree_signature(files_stmts)
1867 flat_sig := flat_direct.subtree_signature(flat_stmts)
1868 if files_sig == flat_sig {
1869 continue
1870 }
1871 pa, pb := dump_signature_pair('${label}_file${i}', files_sig, flat_sig)
1872 eprintln('[${label}] file ${i} subtree diverged.')
1873 eprintln(' files input: ${pa}')
1874 eprintln(' flat input: ${pb}')
1875 eprintln(' diff with: diff -u ${pa} ${pb}')
1876 assert false, '${label}: output diverged at file ${i} (see /tmp dumps above)'
1877 }
1878}
1879
1880fn test_flat_input_to_flat_direct_matches_file_input_direct() {
1881 run_flat_input_to_flat_direct_matches_file_input_direct('flat_input_direct_if_guard_assign',
1882 fixture_if_guard_assign)
1883}
1884
1885fn test_flat_input_to_flat_direct_monomorphizes_generics_like_file_input() {
1886 run_flat_input_to_flat_direct_matches_file_input_direct('flat_input_direct_generic_fn',
1887 fixture_generic_fn)
1888}
1889
1890fn test_flat_input_to_flat_direct_map_index_array_push() {
1891 run_flat_input_to_flat_direct_matches_file_input_direct('flat_input_direct_map_index_array_push',
1892 fixture_map_index_array_push)
1893}
1894
1895fn test_to_flat_direct_parity_for_in_map() {
1896 run_to_flat_direct_parity('to_flat_direct_for_in_map', fixture_for_in_map)
1897}
1898
1899// --- parity: transform_files_to_flat_via_driver per-file subtree vs legacy ---
1900//
1901// `transform_files_to_flat_via_driver` is the s162 wedge that routes through
1902// the s161 `post_pass_to_flat` driver instead of the legacy `post_pass +
1903// flatten_files` boundary. Structurally equivalent to legacy on every file's
1904// stmts list, but NOT bit-equal in full `signature()` because intern order of
1905// post_pass-added strings differs between the two paths: the legacy path
1906// interns those strings DURING `flatten_files` walk (so later files' mod
1907// strings end up at higher indices), the new path interns all bare files
1908// first and post-pass-added strings AFTER (so later files' mod strings end
1909// up at lower indices). The `.file.extra` slot stores `intern(mod)` as a raw
1910// int, so the leak shows up in `signature()` even though the trees are
1911// content-identical.
1912//
1913// Workaround: compare per-file `subtree_signature(child_at(file_id, 2))`.
1914// File root edge 2 is the stmts list; its subtree never touches `.file.extra`
1915// so intern-order differences vanish. Every file's stmts list — including
1916// post_pass-mutated parts like prepended init calls or appended init fns —
1917// must match the legacy result.
1918//
1919// When SSA migrates to consume flat AST and `transform_files_to_flat` can
1920// drop its `[]ast.File` return, this row pins the alternative driver path so
1921// the swap doesn't regress.
1922fn run_to_flat_via_driver_parity(label string, src string) {
1923 p := parse_transformer_fixture(src)
1924 leg := run_legacy_transform(p)
1925 leg_flat := ast.flatten_files(leg)
1926
1927 mut t := Transformer.new_with_pref(p.env, p.prefs)
1928 new_flat, _ := t.transform_files_to_flat_via_driver(&p.flat, [])
1929
1930 if leg_flat.files.len != new_flat.files.len {
1931 assert false, '${label}: file count mismatch: legacy=${leg_flat.files.len} driver=${new_flat.files.len}'
1932 return
1933 }
1934
1935 for i in 0 .. leg_flat.files.len {
1936 leg_stmts := leg_flat.child_at(leg_flat.files[i].file_id, 2)
1937 new_stmts := new_flat.child_at(new_flat.files[i].file_id, 2)
1938 leg_sub_sig := leg_flat.subtree_signature(leg_stmts)
1939 new_sub_sig := new_flat.subtree_signature(new_stmts)
1940 if leg_sub_sig == new_sub_sig {
1941 continue
1942 }
1943 pa, pb := dump_signature_pair('${label}_file${i}', leg_sub_sig, new_sub_sig)
1944 eprintln('[${label}] transform_files_to_flat_via_driver file ${i} subtree diverged from legacy.')
1945 eprintln(' legacy: ${pa}')
1946 eprintln(' driver: ${pb}')
1947 eprintln(' diff with: diff -u ${pa} ${pb}')
1948 assert false, '${label}: transform_files_to_flat_via_driver output diverged at file ${i} (see /tmp dumps above)'
1949 }
1950}
1951
1952fn test_to_flat_via_driver_parity_plain_fn() {
1953 run_to_flat_via_driver_parity('to_flat_via_driver_plain_fn', fixture_plain_fn)
1954}
1955
1956fn test_to_flat_via_driver_parity_infix_operator() {
1957 run_to_flat_via_driver_parity('to_flat_via_driver_infix_operator', fixture_infix_operator)
1958}
1959
1960fn test_to_flat_via_driver_parity_array_contains() {
1961 run_to_flat_via_driver_parity('to_flat_via_driver_array_contains', fixture_array_contains)
1962}
1963
1964fn test_to_flat_via_driver_parity_if_guard() {
1965 run_to_flat_via_driver_parity('to_flat_via_driver_if_guard', fixture_if_guard)
1966}
1967
1968fn test_to_flat_via_driver_parity_or_block() {
1969 run_to_flat_via_driver_parity('to_flat_via_driver_or_block', fixture_or_block)
1970}
1971
1972fn test_to_flat_via_driver_parity_global_init() {
1973 run_to_flat_via_driver_parity('to_flat_via_driver_global_init', fixture_global_init)
1974}
1975
1976fn test_to_flat_via_driver_parity_const_decl() {
1977 run_to_flat_via_driver_parity('to_flat_via_driver_const_decl', fixture_const_decl)
1978}
1979
1980fn test_to_flat_via_driver_parity_string_interp() {
1981 run_to_flat_via_driver_parity('to_flat_via_driver_string_interp', fixture_string_interp)
1982}
1983
1984fn test_to_flat_via_driver_parity_assert_stmt() {
1985 run_to_flat_via_driver_parity('to_flat_via_driver_assert_stmt', fixture_assert_stmt)
1986}
1987
1988fn test_to_flat_via_driver_parity_return_stmt() {
1989 run_to_flat_via_driver_parity('to_flat_via_driver_return_stmt', fixture_return_stmt)
1990}
1991
1992// test_all_fixtures_produce_nonempty_signature guards against silent harness
1993// breakage: every fixture has at least main() so the signature must contain
1994// at least one FILE / fn body. A zero-length signature means parse / check /
1995// transform broke before we even compared.
1996fn test_all_fixtures_produce_nonempty_signature() {
1997 for i, src in all_transformer_fixtures() {
1998 p := parse_transformer_fixture(src)
1999 sig := transform_signature(run_legacy_transform(p))
2000 assert sig.len > 0, 'fixture #${i}: empty signature — pipeline broken before transformer'
2001 }
2002}
2003