v / vlib / v2 / ast / flat_append_flat_test.v
210 lines · 185 sloc · 6.35 KB · 164b30309e6337b95ec2baacacd0f101fafd3d97
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 ast
5
6// Tests for `FlatBuilder.append_flat` — the flat-to-flat merge primitive behind
7// the flat parallel transform. It concatenates a whole `src` FlatAst into a
8// builder, relocating node ids / edge targets and re-interning strings so the
9// merged result decodes identically to `src` standalone. The key correctness
10// risks: (1) node-id / edge relocation, (2) name_id string remap across a
11// DIFFERENT intern order in the destination, (3) re-interning the three kinds
12// whose `extra` slot holds a string id (file mod, stmt_directive value,
13// stmt_import alias) while leaving packed-int extras (counts/flags) untouched.
14
15fn make_ident_stmt(name string) Stmt {
16 return Stmt(ExprStmt{
17 expr: Expr(Ident{
18 name: name
19 })
20 })
21}
22
23fn make_for_with_idents(names []string) Stmt {
24 mut body := []Stmt{}
25 for n in names {
26 body << make_ident_stmt(n)
27 }
28 return Stmt(ForStmt{
29 init: Stmt(EmptyStmt{})
30 cond: Expr(BasicLiteral{
31 kind: .number
32 value: '1'
33 })
34 stmts: body
35 })
36}
37
38// build a small flat with a file root whose stmt list is `stmts`.
39fn build_named_flat(file_name string, mod string, stmts []Stmt) (FlatBuilder, FlatNodeId) {
40 mut b := new_flat_builder()
41 mut ids := []FlatNodeId{}
42 for s in stmts {
43 ids << b.emit_stmt(s)
44 }
45 file_id := b.append_file_with_stmt_ids(File{
46 name: file_name
47 mod: mod
48 }, ids)
49 return b, file_id
50}
51
52// Seed a destination builder with DIFFERENT strings so the src->dst string
53// remap is non-trivial (src ids must not coincide with dst ids).
54fn seeded_dst() FlatBuilder {
55 mut dst := new_flat_builder()
56 dst.intern('zzz_pre_a')
57 dst.intern('zzz_pre_b')
58 dst.intern('zzz_pre_c')
59 dst.emit_stmt(make_ident_stmt('zzz_pre_d'))
60 return dst
61}
62
63fn test_append_flat_preserves_subtree_structure_and_name_strings() {
64 mut a, a_file := build_named_flat('a.v', 'main', [
65 make_for_with_idents(['alpha', 'beta']),
66 make_ident_stmt('gamma'),
67 ])
68 // edge 2 of a .file node is the stmts list; rooting the signature there
69 // avoids the intern-order-dependent .file `extra` slot.
70 a_stmts := a.flat.child_at(a_file, 2)
71 a_sig := a.flat.subtree_signature(a_stmts)
72
73 mut dst := seeded_dst()
74 pre_nodes := dst.flat.nodes.len
75 pre_edges := dst.flat.edges.len
76
77 off := dst.append_flat(a.flat)
78
79 assert off == pre_nodes
80 assert dst.flat.nodes.len == pre_nodes + a.flat.nodes.len
81 assert dst.flat.edges.len == pre_edges + a.flat.edges.len
82 // Structure + every interned ident/value survives the id+string relocation.
83 assert dst.flat.subtree_signature(a_stmts + off) == a_sig
84}
85
86fn test_append_flat_two_sources_stay_independent() {
87 mut a, a_file := build_named_flat('a.v', 'main', [
88 make_for_with_idents(['alpha', 'beta']),
89 ])
90 mut b, b_file := build_named_flat('b.v', 'other', [make_ident_stmt('delta'),
91 make_ident_stmt('epsilon')])
92 a_sig := a.flat.subtree_signature(a.flat.child_at(a_file, 2))
93 b_sig := b.flat.subtree_signature(b.flat.child_at(b_file, 2))
94
95 mut dst := seeded_dst()
96 off_a := dst.append_flat(a.flat)
97 off_b := dst.append_flat(b.flat)
98
99 assert dst.flat.files.len == 2
100 assert dst.flat.subtree_signature(a.flat.child_at(a_file, 2) + off_a) == a_sig
101 assert dst.flat.subtree_signature(b.flat.child_at(b_file, 2) + off_b) == b_sig
102 // b's offset accounts for everything already present (seed + a).
103 assert off_b == off_a + a.flat.nodes.len
104}
105
106fn test_append_flat_reinterns_string_extra_kinds() {
107 mut a := new_flat_builder()
108 imp_id := a.emit_stmt(Stmt(ImportStmt{
109 name: 'os'
110 alias: 'myos'
111 is_aliased: true
112 }))
113 dir_id := a.emit_stmt(Stmt(Directive{
114 name: 'flag'
115 value: '-lpthread'
116 }))
117 file_id := a.append_file_with_stmt_ids(File{
118 name: 'a.v'
119 mod: 'mymod'
120 }, [imp_id, dir_id])
121
122 mut dst := new_flat_builder()
123 // Shift the intern table so src string ids differ from dst string ids.
124 dst.intern('os')
125 dst.intern('zzz')
126 dst.intern('flag_other')
127 dst.intern('unrelated_mod')
128
129 off := dst.append_flat(a.flat)
130
131 // stmt_import.alias lives in `extra` — must resolve to the merged table.
132 merged_imp := Cursor{
133 flat: &dst.flat
134 id: imp_id + off
135 }.stmt() as ImportStmt
136 assert merged_imp.name == 'os'
137 assert merged_imp.alias == 'myos'
138 assert merged_imp.is_aliased
139
140 // stmt_directive.value lives in `extra`.
141 merged_dir := Cursor{
142 flat: &dst.flat
143 id: dir_id + off
144 }.stmt() as Directive
145 assert merged_dir.name == 'flag'
146 assert merged_dir.value == '-lpthread'
147
148 // file mod is carried both in FlatFile.mod_idx (decoder reads this) and in
149 // the .file node's `extra` (must also be re-interned for consistency).
150 mf := dst.flat.files[dst.flat.files.len - 1]
151 assert dst.flat.string_at(mf.name_idx) == 'a.v'
152 assert dst.flat.string_at(mf.mod_idx) == 'mymod'
153 assert dst.flat.string_at(dst.flat.nodes[file_id + off].extra) == 'mymod'
154}
155
156fn test_append_flat_empty_source_is_noop() {
157 mut dst := seeded_dst()
158 pre_nodes := dst.flat.nodes.len
159 pre_edges := dst.flat.edges.len
160 pre_files := dst.flat.files.len
161 empty := FlatAst{}
162 off := dst.append_flat(&empty)
163 assert off == pre_nodes
164 assert dst.flat.nodes.len == pre_nodes
165 assert dst.flat.edges.len == pre_edges
166 assert dst.flat.files.len == pre_files
167}
168
169fn test_copy_subtree_from_preserves_stmt_shape_and_strings() {
170 mut src := new_flat_builder()
171 imp_id := src.emit_stmt(Stmt(ImportStmt{
172 name: 'os'
173 alias: 'myos'
174 is_aliased: true
175 }))
176 dir_id := src.emit_stmt(Stmt(Directive{
177 name: 'flag'
178 value: '-lm'
179 }))
180 for_id := src.emit_stmt(make_for_with_idents(['alpha', 'beta']))
181
182 mut dst := seeded_dst()
183 copied_imp := dst.copy_subtree_from(&src.flat, imp_id)
184 copied_dir := dst.copy_subtree_from(&src.flat, dir_id)
185 copied_for := dst.copy_subtree_from(&src.flat, for_id)
186
187 assert dst.flat.subtree_signature(copied_for) == src.flat.subtree_signature(for_id)
188
189 merged_imp := Cursor{
190 flat: &dst.flat
191 id: copied_imp
192 }.stmt() as ImportStmt
193 assert merged_imp.name == 'os'
194 assert merged_imp.alias == 'myos'
195 assert merged_imp.is_aliased
196
197 merged_dir := Cursor{
198 flat: &dst.flat
199 id: copied_dir
200 }.stmt() as Directive
201 assert merged_dir.name == 'flag'
202 assert merged_dir.value == '-lm'
203}
204
205fn test_copy_subtree_from_invalid_root_returns_invalid() {
206 mut src := new_flat_builder()
207 mut dst := new_flat_builder()
208 assert dst.copy_subtree_from(&src.flat, -1) == invalid_flat_node_id
209 assert dst.copy_subtree_from(&src.flat, 100) == invalid_flat_node_id
210}
211