v / vlib / v2 / transformer / transformer_v2_darwin_test.v
515 lines · 490 sloc · 11.16 KB · a7153322629091f3f2d79f0bf318d0fb2c13c425
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
5module transformer
6
7import os
8import v2.ast
9import v2.parser
10import v2.pref
11import v2.token
12import v2.types
13
14// Integration test: run the full pipeline (parse → type check → transform) on
15// cmd/v2/v2.v and verify that every expression with a valid position carries a
16// type in the environment after transformation.
17
18struct ExprTypeChecker {
19 env &types.Environment
20 file_set &token.FileSet
21mut:
22 total int
23 missing int
24 details []string
25 by_kind map[string]int
26 in_generic_fn bool
27 generic_miss int
28 cur_fn_name string
29 fn_miss map[string]int
30 seen_expr_ids map[int]bool
31}
32
33fn test_v2_transformer_all_exprs_have_types() {
34 vroot := detect_vroot()
35 v2_dir := os.join_path(vroot, 'cmd', 'v2')
36 assert os.is_dir(v2_dir), 'cmd/v2 directory not found at ${v2_dir}'
37
38 prefs := &pref.Preferences{
39 backend: .cleanc
40 vroot: vroot
41 no_parallel: true
42 }
43
44 // --- Parse ---
45 mut p := parser.Parser.new(prefs)
46 mut file_set := token.FileSet.new()
47
48 // Parse core modules (same order as builder)
49 core_module_paths := [
50 'builtin',
51 'strconv',
52 'strings',
53 'hash',
54 'math.bits',
55 'os',
56 'time',
57 'term',
58 'term.termios',
59 'os.cmdline',
60 'encoding.binary',
61 'crypto.sha256',
62 'strings.textscanner',
63 ]
64 mut ast_files := []ast.File{}
65 for mod_path in core_module_paths {
66 module_dir := prefs.get_vlib_module_path(mod_path)
67 module_files := get_v_files_from_dir(module_dir)
68 parsed := p.parse_files(module_files, mut file_set)
69 ast_files << parsed
70 }
71
72 // Parse user files (only v2.v, not test files in the same directory)
73 user_files := [os.join_path(v2_dir, 'v2.v')]
74 parsed_user := p.parse_files(user_files, mut file_set)
75 ast_files << parsed_user
76
77 // Parse imports
78 mut parsed_imports := []string{}
79 parsed_imports << core_module_paths
80 for afi := 0; afi < ast_files.len; afi++ {
81 ast_file := ast_files[afi]
82 for mod in ast_file.imports {
83 if mod.name in parsed_imports {
84 continue
85 }
86 mod_dir := prefs.get_module_path(mod.name, ast_file.name)
87 module_files := get_v_files_from_dir(mod_dir)
88 parsed := p.parse_files(module_files, mut file_set)
89 ast_files << parsed
90 parsed_imports << mod.name
91 }
92 }
93
94 assert ast_files.len > 0, 'no files parsed'
95
96 // --- Type Check ---
97 env := types.Environment.new()
98 mut checker := types.Checker.new(prefs, file_set, env)
99 checker.check_files(ast_files)
100
101 // --- Transform ---
102 mut trans := Transformer.new_with_pref(env, prefs)
103 transformed := trans.transform_files(ast_files)
104
105 // --- Verify: every expression with a valid pos must have a type ---
106 mut etc := ExprTypeChecker{
107 env: env
108 file_set: file_set
109 seen_expr_ids: map[int]bool{}
110 }
111
112 for file in transformed {
113 for stmt in file.stmts {
114 etc.check_stmt(stmt)
115 }
116 }
117
118 // Allow a small number of missing types from transformer-generated synthetic
119 // expressions (temp variables, lowered operator calls, etc.) that don't go
120 // through the checker. Track this threshold and reduce it as coverage improves.
121 max_missing := 1701
122 if etc.missing > max_missing {
123 mut msg := '${etc.missing} of ${etc.total} expressions missing types (max allowed: ${max_missing}).\n'
124 msg += 'breakdown by kind:\n'
125 for kind, count in etc.by_kind {
126 msg += ' ${kind}: ${count}\n'
127 }
128 msg += 'by function (${etc.fn_miss.len} fns):\n'
129 mut fn_counts := []int{}
130 mut fn_names := []string{}
131 for fn_name, count in etc.fn_miss {
132 fn_counts << count
133 fn_names << fn_name
134 }
135 for i := 0; i < fn_counts.len; i++ {
136 for j := i + 1; j < fn_counts.len; j++ {
137 if fn_counts[j] > fn_counts[i] {
138 fn_counts[i], fn_counts[j] = fn_counts[j], fn_counts[i]
139 fn_names[i], fn_names[j] = fn_names[j], fn_names[i]
140 }
141 }
142 }
143 fn_limit := if fn_counts.len < 50 { fn_counts.len } else { 50 }
144 for i := 0; i < fn_limit; i++ {
145 msg += ' ${fn_names[i]}: ${fn_counts[i]}\n'
146 }
147 limit := if etc.details.len < 100 { etc.details.len } else { 100 }
148 msg += 'first ${limit} missing:\n'
149 for detail in etc.details[..limit] {
150 msg += ' ${detail}\n'
151 }
152 assert false, msg
153 }
154
155 assert etc.total > 0, 'no expressions found in transformed AST'
156}
157
158// --- Helpers ---
159
160fn detect_vroot() string {
161 mut dir := os.getwd()
162 for _ in 0 .. 8 {
163 if os.is_dir(os.join_path(dir, 'vlib', 'builtin')) {
164 return dir
165 }
166 dir = os.dir(dir)
167 }
168 home_vroot := os.join_path(os.home_dir(), 'code', 'v')
169 if os.is_dir(os.join_path(home_vroot, 'vlib', 'builtin')) {
170 return home_vroot
171 }
172 panic('cannot detect vroot')
173}
174
175fn get_v_files_from_dir(dir string) []string {
176 entries := os.ls(dir) or { []string{} }
177 mut v_files := []string{}
178 for file in entries {
179 if !file.ends_with('.v') || file.ends_with('.js.v') || file.contains('_test.') {
180 continue
181 }
182 if file.contains('.arm64.') || file.contains('.arm32.') || file.contains('.amd64.') {
183 continue
184 }
185 if pref.file_has_incompatible_os_suffix(file, os.user_os()) {
186 continue
187 }
188 if file.ends_with('prealloc.c.v') {
189 continue
190 }
191 if file.contains('_d_') {
192 continue
193 }
194 v_files << os.join_path(dir, file)
195 }
196 return v_files
197}
198
199// --- AST walkers ---
200
201// has_type checks whether the environment has a type set for the given expression ID.
202// This checks directly against the Void(1) sentinel (meaning "unset") rather than
203// filtering all Void types, so expressions explicitly typed as void (Void(0)) are
204// correctly recognized as having a type.
205fn (c &ExprTypeChecker) has_type(id int) bool {
206 return c.env.has_expr_type(id)
207}
208
209fn (mut c ExprTypeChecker) check_expr(expr ast.Expr) {
210 pos := expr.pos()
211 if pos.is_valid() {
212 if pos.id in c.seen_expr_ids {
213 return
214 }
215 c.seen_expr_ids[pos.id] = true
216 c.total++
217 if c.has_type(pos.id) {
218 // ok
219 } else {
220 if c.in_generic_fn {
221 c.generic_miss++
222 }
223 c.missing++
224 c.fn_miss[c.cur_fn_name] = c.fn_miss[c.cur_fn_name] + 1
225 kind := expr.type_name()
226 c.by_kind[kind] = c.by_kind[kind] + 1
227 if c.details.len < 100 {
228 file := c.file_set.file(pos)
229 position := file.position(pos)
230 extra := match expr {
231 ast.Ident { ' name="${expr.name}"' }
232 ast.BasicLiteral { ' val="${expr.value}"' }
233 ast.StringLiteral { ' val="${expr.value}"' }
234 ast.SelectorExpr { ' .sel' }
235 ast.CallExpr { ' call' }
236 ast.InfixExpr { ' op=${expr.op}' }
237 ast.IndexExpr { ' idx' }
238 ast.CastExpr { ' cast' }
239 ast.PrefixExpr { ' op=${expr.op}' }
240 ast.ParenExpr { ' paren' }
241 ast.ModifierExpr { ' mod=${expr.kind}' }
242 ast.KeywordOperator { ' kw' }
243 ast.PostfixExpr { ' op=${expr.op}' }
244 ast.IfExpr { ' if' }
245 else { '' }
246 }
247
248 c.details << '${position} id=${pos.id} kind=${kind}${extra}'
249 }
250 }
251 }
252
253 // Recurse into sub-expressions
254 match expr {
255 ast.ArrayInitExpr {
256 c.check_expr(expr.typ)
257 for e in expr.exprs {
258 c.check_expr(e)
259 }
260 c.check_expr(expr.init)
261 c.check_expr(expr.cap)
262 c.check_expr(expr.len)
263 }
264 ast.AsCastExpr {
265 c.check_expr(expr.expr)
266 c.check_expr(expr.typ)
267 }
268 ast.AssocExpr {
269 c.check_expr(expr.typ)
270 c.check_expr(expr.expr)
271 for f in expr.fields {
272 c.check_expr(f.value)
273 }
274 }
275 ast.BasicLiteral {}
276 ast.CallExpr {
277 c.check_expr(expr.lhs)
278 for arg in expr.args {
279 c.check_expr(arg)
280 }
281 }
282 ast.CallOrCastExpr {
283 c.check_expr(expr.lhs)
284 c.check_expr(expr.expr)
285 }
286 ast.CastExpr {
287 c.check_expr(expr.typ)
288 c.check_expr(expr.expr)
289 }
290 ast.ComptimeExpr {
291 c.check_expr(expr.expr)
292 }
293 ast.FnLiteral {
294 for cv in expr.captured_vars {
295 c.check_expr(cv)
296 }
297 for s in expr.stmts {
298 c.check_stmt(s)
299 }
300 }
301 ast.GenericArgs {
302 c.check_expr(expr.lhs)
303 for arg in expr.args {
304 c.check_expr(arg)
305 }
306 }
307 ast.GenericArgOrIndexExpr {
308 c.check_expr(expr.lhs)
309 c.check_expr(expr.expr)
310 }
311 ast.Ident {}
312 ast.IfExpr {
313 c.check_expr(expr.cond)
314 for s in expr.stmts {
315 c.check_stmt(s)
316 }
317 c.check_expr(expr.else_expr)
318 }
319 ast.IfGuardExpr {
320 c.check_stmt(ast.Stmt(expr.stmt))
321 }
322 ast.InfixExpr {
323 c.check_expr(expr.lhs)
324 c.check_expr(expr.rhs)
325 }
326 ast.IndexExpr {
327 c.check_expr(expr.lhs)
328 c.check_expr(expr.expr)
329 }
330 ast.InitExpr {
331 c.check_expr(expr.typ)
332 for f in expr.fields {
333 c.check_expr(f.value)
334 }
335 }
336 ast.KeywordOperator {
337 for e in expr.exprs {
338 c.check_expr(e)
339 }
340 }
341 ast.LambdaExpr {
342 c.check_expr(expr.expr)
343 }
344 ast.LockExpr {
345 for e in expr.lock_exprs {
346 c.check_expr(e)
347 }
348 for e in expr.rlock_exprs {
349 c.check_expr(e)
350 }
351 for s in expr.stmts {
352 c.check_stmt(s)
353 }
354 }
355 ast.MapInitExpr {
356 c.check_expr(expr.typ)
357 for k in expr.keys {
358 c.check_expr(k)
359 }
360 for v in expr.vals {
361 c.check_expr(v)
362 }
363 }
364 ast.MatchExpr {
365 c.check_expr(expr.expr)
366 for br in expr.branches {
367 for cond in br.cond {
368 c.check_expr(cond)
369 }
370 for s in br.stmts {
371 c.check_stmt(s)
372 }
373 }
374 }
375 ast.ModifierExpr {
376 c.check_expr(expr.expr)
377 }
378 ast.OrExpr {
379 c.check_expr(expr.expr)
380 for s in expr.stmts {
381 c.check_stmt(s)
382 }
383 }
384 ast.ParenExpr {
385 c.check_expr(expr.expr)
386 }
387 ast.PostfixExpr {
388 c.check_expr(expr.expr)
389 }
390 ast.PrefixExpr {
391 c.check_expr(expr.expr)
392 }
393 ast.RangeExpr {
394 c.check_expr(expr.start)
395 c.check_expr(expr.end)
396 }
397 ast.SelectExpr {
398 c.check_stmt(expr.stmt)
399 for s in expr.stmts {
400 c.check_stmt(s)
401 }
402 c.check_expr(expr.next)
403 }
404 ast.SelectorExpr {
405 c.check_expr(expr.lhs)
406 }
407 ast.SqlExpr {
408 c.check_expr(expr.expr)
409 }
410 ast.StringInterLiteral {
411 for inter in expr.inters {
412 c.check_expr(inter.expr)
413 c.check_expr(inter.format_expr)
414 }
415 }
416 ast.StringLiteral {}
417 ast.Tuple {
418 for e in expr.exprs {
419 c.check_expr(e)
420 }
421 }
422 ast.UnsafeExpr {
423 for s in expr.stmts {
424 c.check_stmt(s)
425 }
426 }
427 else {}
428 }
429}
430
431fn (mut c ExprTypeChecker) check_stmt(stmt ast.Stmt) {
432 match stmt {
433 ast.AssertStmt {
434 c.check_expr(stmt.expr)
435 c.check_expr(stmt.extra)
436 }
437 ast.AssignStmt {
438 for e in stmt.lhs {
439 c.check_expr(e)
440 }
441 for e in stmt.rhs {
442 c.check_expr(e)
443 }
444 }
445 ast.BlockStmt {
446 for s in stmt.stmts {
447 c.check_stmt(s)
448 }
449 }
450 ast.ComptimeStmt {
451 c.check_stmt(stmt.stmt)
452 }
453 ast.ConstDecl {
454 for f in stmt.fields {
455 c.check_expr(f.value)
456 }
457 }
458 ast.DeferStmt {
459 for s in stmt.stmts {
460 c.check_stmt(s)
461 }
462 }
463 ast.ExprStmt {
464 c.check_expr(stmt.expr)
465 }
466 ast.FnDecl {
467 prev_generic := c.in_generic_fn
468 prev_fn := c.cur_fn_name
469 c.cur_fn_name = stmt.name
470 if stmt.typ.generic_params.len > 0 {
471 c.in_generic_fn = true
472 }
473 for s in stmt.stmts {
474 c.check_stmt(s)
475 }
476 c.in_generic_fn = prev_generic
477 c.cur_fn_name = prev_fn
478 }
479 ast.ForStmt {
480 c.check_stmt(stmt.init)
481 c.check_expr(stmt.cond)
482 c.check_stmt(stmt.post)
483 for s in stmt.stmts {
484 c.check_stmt(s)
485 }
486 }
487 ast.ForInStmt {
488 c.check_expr(stmt.key)
489 c.check_expr(stmt.value)
490 c.check_expr(stmt.expr)
491 }
492 ast.LabelStmt {
493 c.check_stmt(stmt.stmt)
494 }
495 ast.ReturnStmt {
496 for e in stmt.exprs {
497 c.check_expr(e)
498 }
499 }
500 ast.EnumDecl {
501 for f in stmt.fields {
502 c.check_expr(f.value)
503 }
504 }
505 ast.GlobalDecl {
506 for f in stmt.fields {
507 c.check_expr(f.value)
508 }
509 }
510 ast.InterfaceDecl {}
511 ast.StructDecl {}
512 ast.TypeDecl {}
513 else {}
514 }
515}
516