| 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 | module transformer |
| 6 | |
| 7 | import os |
| 8 | import v2.ast |
| 9 | import v2.parser |
| 10 | import v2.pref |
| 11 | import v2.token |
| 12 | import 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 | |
| 18 | struct ExprTypeChecker { |
| 19 | env &types.Environment |
| 20 | file_set &token.FileSet |
| 21 | mut: |
| 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 | |
| 33 | fn 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 | |
| 160 | fn 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 | |
| 175 | fn 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. |
| 205 | fn (c &ExprTypeChecker) has_type(id int) bool { |
| 206 | return c.env.has_expr_type(id) |
| 207 | } |
| 208 | |
| 209 | fn (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 | |
| 431 | fn (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 | |