v / vlib / v2 / ast / cursor_test.v
362 lines · 348 sloc · 9.41 KB · d358576851079d4716834cc86627307d28acd1ae
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 the Cursor API live alongside the implementation since they
7// operate on hand-built FlatAst fixtures — building them via the parser
8// would introduce a circular dep (parser depends on ast). The fixtures
9// mirror the shape that parse_file emits for a few representative inputs.
10
11// build_minimal_file constructs a FlatAst with one file containing:
12// module foo
13// fn add(a int, b int) int { return a + b }
14// using the FlatBuilder API directly. We then exercise Cursor over it.
15fn build_minimal_file() FlatAst {
16 mut b := new_flat_builder()
17 src := File{
18 name: 'inline_minimal.v'
19 mod: 'foo'
20 stmts: [
21 Stmt(ModuleStmt{
22 name: 'foo'
23 }),
24 Stmt(FnDecl{
25 name: 'add'
26 typ: FnType{
27 params: [
28 Parameter{
29 name: 'a'
30 typ: Expr(Ident{
31 name: 'int'
32 })
33 },
34 Parameter{
35 name: 'b'
36 typ: Expr(Ident{
37 name: 'int'
38 })
39 },
40 ]
41 return_type: Expr(Ident{
42 name: 'int'
43 })
44 }
45 stmts: [
46 Stmt(ReturnStmt{
47 exprs: [
48 Expr(InfixExpr{
49 op: .plus
50 lhs: Expr(Ident{
51 name: 'a'
52 })
53 rhs: Expr(Ident{
54 name: 'b'
55 })
56 }),
57 ]
58 }),
59 ]
60 }),
61 ]
62 }
63 b.append_file(src)
64 return b.flat
65}
66
67fn test_file_cursor_basics() {
68 flat := build_minimal_file()
69 assert flat.files.len == 1
70 fc := flat.file_cursor(0)
71 assert fc.name() == 'inline_minimal.v'
72 assert fc.mod() == 'foo'
73 root := fc.root()
74 assert root.is_valid()
75 assert root.kind() == .file
76}
77
78fn test_file_cursor_stmts_iteration() {
79 flat := build_minimal_file()
80 fc := flat.file_cursor(0)
81 stmts := fc.stmts()
82 // module decl + fn decl
83 assert stmts.len() == 2
84 assert stmts.at(0).kind() == .stmt_module
85 assert stmts.at(0).name() == 'foo'
86 assert stmts.at(1).kind() == .stmt_fn_decl
87 assert stmts.at(1).name() == 'add'
88}
89
90fn test_fn_decl_flags_and_body() {
91 flat := build_minimal_file()
92 fc := flat.file_cursor(0)
93 fn_cur := fc.stmts().at(1)
94 assert fn_cur.kind() == .stmt_fn_decl
95 // is_method/is_public/is_static all false for plain `fn add(...)`
96 assert !fn_cur.flag(flag_is_method)
97 assert !fn_cur.flag(flag_is_public)
98 assert !fn_cur.flag(flag_is_static)
99 // edge layout per read_stmt: 0=receiver, 1=typ, 2=attrs, 3=stmts(aux_list)
100 stmts := fn_cur.list_at(3)
101 assert stmts.len() == 1
102 ret := stmts.at(0)
103 assert ret.kind() == .stmt_return
104}
105
106fn test_fn_decl_signature_from_cursor() {
107 flat := build_minimal_file()
108 fn_cur := flat.file_cursor(0).stmts().at(1)
109 decl := fn_cur.fn_decl_signature()
110 assert decl.name == 'add'
111 assert !decl.is_method
112 assert decl.stmts.len == 0
113 assert decl.typ.params.len == 2
114 assert decl.typ.params[0].name == 'a'
115 assert decl.typ.params[0].typ is Ident
116 assert (decl.typ.params[0].typ as Ident).name == 'int'
117 assert decl.typ.params[1].name == 'b'
118 assert decl.typ.return_type is Ident
119 assert (decl.typ.return_type as Ident).name == 'int'
120}
121
122fn test_fn_decl_from_cursor_reads_body() {
123 flat := build_minimal_file()
124 fn_cur := flat.file_cursor(0).stmts().at(1)
125 decl := fn_cur.fn_decl()
126 assert decl.name == 'add'
127 assert decl.stmts.len == 1
128 assert decl.stmts[0] is ReturnStmt
129}
130
131fn test_stmt_and_expr_escape_hatches_from_cursor() {
132 flat := build_minimal_file()
133 ret_cur := flat.file_cursor(0).stmts().at(1).list_at(3).at(0)
134 ret_stmt := ret_cur.stmt()
135 assert ret_stmt is ReturnStmt
136 infix_expr := ret_cur.edge(0).expr()
137 assert infix_expr is InfixExpr
138 assert (infix_expr as InfixExpr).op == .plus
139}
140
141fn test_return_stmt_exprs_via_edges() {
142 flat := build_minimal_file()
143 fc := flat.file_cursor(0)
144 ret := fc.stmts().at(1).list_at(3).at(0)
145 assert ret.kind() == .stmt_return
146 // ReturnStmt stores its exprs as direct edges of the parent (not aux_list)
147 assert ret.edge_count() == 1
148 infix := ret.edge(0)
149 assert infix.kind() == .expr_infix
150 // InfixExpr edges: 0=lhs, 1=rhs (per read_expr)
151 assert infix.edge_count() == 2
152 lhs := infix.edge(0)
153 rhs := infix.edge(1)
154 assert lhs.kind() == .expr_ident
155 assert lhs.name() == 'a'
156 assert rhs.kind() == .expr_ident
157 assert rhs.name() == 'b'
158}
159
160fn test_invalid_cursor_sentinels() {
161 flat := build_minimal_file()
162 fc := flat.file_cursor(0)
163 bogus := fc.stmts().at(99) // out of range
164 assert !bogus.is_valid()
165 bogus_edge := fc.root().edge(99) // out of range
166 assert !bogus_edge.is_valid()
167 empty_list := fc.root().list_at(99)
168 assert empty_list.len() == 0
169}
170
171fn test_type_expr_from_cursor_reads_signature_types() {
172 mut b := new_flat_builder()
173 src := File{
174 name: 'inline_type_expr.v'
175 mod: 'foo'
176 stmts: [
177 Stmt(ModuleStmt{
178 name: 'foo'
179 }),
180 Stmt(FnDecl{
181 name: 'use_box'
182 typ: FnType{
183 params: [
184 Parameter{
185 name: 'items'
186 typ: Expr(Type(ArrayType{
187 elem_type: Expr(GenericArgs{
188 lhs: Expr(Ident{
189 name: 'Box'
190 })
191 args: [
192 Expr(Ident{
193 name: 'int'
194 }),
195 ]
196 })
197 }))
198 },
199 ]
200 return_type: Expr(Type(OptionType{
201 base_type: Expr(Ident{
202 name: 'string'
203 })
204 }))
205 }
206 }),
207 ]
208 }
209 b.append_file(src)
210 fn_cur := b.flat.file_cursor(0).stmts().at(1)
211 fn_type := fn_cur.edge(1)
212 params := fn_type.list_at(1)
213 param_typ := params.at(0).edge(0).type_expr()
214 assert param_typ is Type
215 array_typ := param_typ as Type
216 assert array_typ is ArrayType
217 array_expr := array_typ as ArrayType
218 assert array_expr.elem_type is GenericArgs
219 generic := array_expr.elem_type as GenericArgs
220 assert generic.lhs is Ident
221 assert (generic.lhs as Ident).name == 'Box'
222 assert generic.args.len == 1
223 assert generic.args[0] is Ident
224 assert (generic.args[0] as Ident).name == 'int'
225 return_typ := fn_type.edge(2).type_expr()
226 assert return_typ is Type
227 option_typ := return_typ as Type
228 assert option_typ is OptionType
229 option_expr := option_typ as OptionType
230 assert option_expr.base_type is Ident
231 assert (option_expr.base_type as Ident).name == 'string'
232}
233
234fn test_decl_readers_from_cursor() {
235 mut b := new_flat_builder()
236 src := File{
237 name: 'inline_decl_readers.v'
238 mod: 'foo'
239 stmts: [
240 Stmt(ModuleStmt{
241 name: 'foo'
242 }),
243 Stmt(ConstDecl{
244 fields: [
245 FieldInit{
246 name: 'answer'
247 value: Expr(BasicLiteral{
248 kind: .number
249 value: '42'
250 })
251 },
252 ]
253 }),
254 Stmt(EnumDecl{
255 name: 'Mode'
256 attributes: [
257 Attribute{
258 name: 'flag'
259 },
260 ]
261 fields: [
262 FieldDecl{
263 name: 'read'
264 value: Expr(BasicLiteral{
265 kind: .number
266 value: '1'
267 })
268 },
269 ]
270 }),
271 Stmt(StructDecl{
272 name: 'Person'
273 attributes: [
274 Attribute{
275 name: 'heap'
276 },
277 ]
278 fields: [
279 FieldDecl{
280 name: 'name'
281 typ: Expr(Ident{
282 name: 'string'
283 })
284 value: Expr(StringLiteral{
285 value: 'unknown'
286 })
287 attributes: [
288 Attribute{
289 name: 'json'
290 value: Expr(StringLiteral{
291 value: 'full_name'
292 })
293 },
294 ]
295 is_public: true
296 },
297 ]
298 }),
299 ]
300 }
301 b.append_file(src)
302 stmts := b.flat.file_cursor(0).stmts()
303 const_decl := stmts.at(1).const_decl()
304 assert const_decl.fields.len == 1
305 assert const_decl.fields[0].name == 'answer'
306 assert const_decl.fields[0].value is BasicLiteral
307 assert (const_decl.fields[0].value as BasicLiteral).value == '42'
308 enum_decl := stmts.at(2).enum_decl(true)
309 assert enum_decl.name == 'Mode'
310 assert enum_decl.attributes.has('flag')
311 assert enum_decl.fields.len == 1
312 assert enum_decl.fields[0].value is BasicLiteral
313 assert (enum_decl.fields[0].value as BasicLiteral).value == '1'
314 struct_decl := stmts.at(3).struct_decl()
315 assert struct_decl.name == 'Person'
316 assert struct_decl.attributes.has('heap')
317 assert struct_decl.fields.len == 1
318 assert struct_decl.fields[0].name == 'name'
319 assert struct_decl.fields[0].is_public
320 assert struct_decl.fields[0].typ is Ident
321 assert (struct_decl.fields[0].typ as Ident).name == 'string'
322 assert struct_decl.fields[0].value is StringLiteral
323 assert (struct_decl.fields[0].value as StringLiteral).value == 'unknown'
324 assert struct_decl.fields[0].attributes.len == 1
325 assert struct_decl.fields[0].attributes[0].name == 'json'
326 assert struct_decl.fields[0].attributes[0].value is StringLiteral
327 assert (struct_decl.fields[0].attributes[0].value as StringLiteral).value == 'full_name'
328}
329
330// count_unique_kinds_via_cursor walks the whole graph rooted at fc through
331// edges with dedup (the FlatBuilder shares aux_list nodes for empty lists,
332// so a naive DFS revisits them). Used to cross-check that Cursor traversal
333// reaches every reachable node in the FlatAst.
334fn count_unique_kinds_via_cursor(fc FileCursor) (map[string]int, int) {
335 flat := fc.flat
336 mut seen := []bool{len: flat.nodes.len}
337 mut out := map[string]int{}
338 mut stack := []Cursor{cap: 64}
339 mut unique := 0
340 stack << fc.root()
341 for stack.len > 0 {
342 c := stack.pop()
343 if !c.is_valid() || seen[c.id] {
344 continue
345 }
346 seen[c.id] = true
347 unique++
348 out[c.kind().str()]++
349 for i in 0 .. c.edge_count() {
350 stack << c.edge(i)
351 }
352 }
353 return out, unique
354}
355
356fn test_cursor_walk_matches_reachable_count() {
357 flat := build_minimal_file()
358 _, unique := count_unique_kinds_via_cursor(flat.file_cursor(0))
359 // Walking from the single file root via Cursor must reach the same
360 // set of unique nodes as the reference reachability walker.
361 assert unique == flat.count_reachable_nodes()
362}
363