| 1 | module main |
| 2 | |
| 3 | import os |
| 4 | import v3.bench |
| 5 | import v3.flat |
| 6 | import v3.gen.c as cgen |
| 7 | import v3.markused |
| 8 | import v3.parser |
| 9 | import v3.pref |
| 10 | import v3.transform |
| 11 | import v3.types |
| 12 | |
| 13 | $if !skip_eval ? { |
| 14 | import v3.eval |
| 15 | } |
| 16 | $if !skip_arm64 ? { |
| 17 | import v3.gen.arm64 |
| 18 | import v3.ssa |
| 19 | import v3.ssa.optimize |
| 20 | } |
| 21 | $if !skip_wasm ? { |
| 22 | import v3.gen.wasm |
| 23 | } |
| 24 | |
| 25 | // run_compile_command supports run compile command handling for v3 entry point. |
| 26 | fn run_compile_command(cmd string) os.Result { |
| 27 | exit_code := os.system(cmd) |
| 28 | return os.Result{ |
| 29 | exit_code: exit_code |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | fn prepare_c_flags_for_link(flags []string, c99 bool) ![]string { |
| 34 | support_flags := c_object_compile_support_flags(flags) |
| 35 | mut prepared := []string{} |
| 36 | for flag in flags { |
| 37 | clean := flag.trim_space() |
| 38 | if c_flag_is_object_file(clean) { |
| 39 | prepared << ensure_c_object_file(clean, support_flags, c99)! |
| 40 | } else { |
| 41 | prepared << flag |
| 42 | } |
| 43 | } |
| 44 | return prepared |
| 45 | } |
| 46 | |
| 47 | fn c_object_compile_support_flags(flags []string) []string { |
| 48 | mut support := []string{} |
| 49 | for flag in flags { |
| 50 | clean := flag.trim_space() |
| 51 | if clean.len == 0 || c_flag_is_object_file(clean) || c_flag_is_c_source_file(clean) |
| 52 | || clean.starts_with('-l') { |
| 53 | continue |
| 54 | } |
| 55 | if clean.starts_with('-I') || clean.starts_with('-D') || clean.starts_with('-U') |
| 56 | || clean.starts_with('-std') || clean.starts_with('-f') || clean.starts_with('-W') |
| 57 | || clean == '-pthread' { |
| 58 | support << clean |
| 59 | } |
| 60 | } |
| 61 | return support |
| 62 | } |
| 63 | |
| 64 | fn ensure_c_object_file(obj_path string, support_flags []string, c99 bool) !string { |
| 65 | if os.exists(obj_path) { |
| 66 | return obj_path |
| 67 | } |
| 68 | source_file := c_source_from_object_file(obj_path) or { |
| 69 | return error('missing C object ${obj_path}, and no adjacent .c/.cpp/.S source was found') |
| 70 | } |
| 71 | cache_dir := os.join_path(os.vtmp_dir(), 'v3_thirdparty_objs') |
| 72 | os.mkdir_all(cache_dir)! |
| 73 | std_flag := if source_file.ends_with('.cpp') { '-std=c++11' } else { c_standard_flag(c99) } |
| 74 | cached_obj := os.join_path(cache_dir, c_object_cache_name(obj_path, support_flags, std_flag)) |
| 75 | if os.exists(cached_obj) |
| 76 | && os.file_last_mod_unix(cached_obj) >= os.file_last_mod_unix(source_file) { |
| 77 | return cached_obj |
| 78 | } |
| 79 | compiler := if source_file.ends_with('.cpp') { 'c++' } else { 'cc' } |
| 80 | cmd := '${compiler} ${std_flag} -w ${support_flags.join(' ')} -o ${os.quoted_path(cached_obj)} -c ${os.quoted_path(source_file)}' |
| 81 | res := os.execute(cmd) |
| 82 | if res.exit_code != 0 { |
| 83 | return error('failed to build C object ${obj_path} from ${source_file}:\n${res.output}') |
| 84 | } |
| 85 | return cached_obj |
| 86 | } |
| 87 | |
| 88 | fn c_source_from_object_file(obj_path string) ?string { |
| 89 | base := obj_path.all_before_last('.') |
| 90 | for ext in ['.c', '.cpp', '.S'] { |
| 91 | source_file := base + ext |
| 92 | if os.exists(source_file) { |
| 93 | return source_file |
| 94 | } |
| 95 | } |
| 96 | return none |
| 97 | } |
| 98 | |
| 99 | fn c_object_cache_name(path string, support_flags []string, std_flag string) string { |
| 100 | base := path.replace_each(['/', '_', '\\', '_', ':', '_', '.', '_', ' ', '_']) |
| 101 | // The compile flags (`-D`/`-I`/...) change the object contents, so they must |
| 102 | // be part of the cache key; otherwise a rebuild with different `#flag` defines |
| 103 | // silently links the stale object built with the previous configuration. |
| 104 | mut cache_flags := [std_flag] |
| 105 | cache_flags << support_flags |
| 106 | return '${base}_${c_flags_hash(cache_flags)}.o' |
| 107 | } |
| 108 | |
| 109 | fn c_flags_hash(flags []string) string { |
| 110 | mut h := u64(1469598103934665603) |
| 111 | joined := flags.join(' ') |
| 112 | for c in joined.bytes() { |
| 113 | h = (h ^ u64(c)) * u64(1099511628211) |
| 114 | } |
| 115 | return h.hex() |
| 116 | } |
| 117 | |
| 118 | fn c_flag_is_object_file(flag string) bool { |
| 119 | return flag.ends_with('.o') || flag.ends_with('.obj') |
| 120 | } |
| 121 | |
| 122 | fn c_flag_is_c_source_file(flag string) bool { |
| 123 | return flag.ends_with('.c') || flag.ends_with('.cc') || flag.ends_with('.cpp') |
| 124 | } |
| 125 | |
| 126 | fn c_standard_flag(c99 bool) string { |
| 127 | return if c99 { '-std=c99' } else { '-std=gnu11' } |
| 128 | } |
| 129 | |
| 130 | // main runs the v3 entry point. |
| 131 | fn main() { |
| 132 | args := os.args[1..] |
| 133 | if args.len == 0 { |
| 134 | eprintln('usage: v3 <file.v> [-o output|file.c] [-b c|arm64|eval] [-c99] [-d flag]') |
| 135 | exit(1) |
| 136 | } |
| 137 | |
| 138 | mut input_file := '' |
| 139 | mut output_file := '' |
| 140 | mut backend := 'c' |
| 141 | mut is_prod := false |
| 142 | mut is_strict := false |
| 143 | mut is_selfhost := false |
| 144 | mut no_parallel := false |
| 145 | mut parallel_transform := false |
| 146 | mut building_v := false |
| 147 | mut c99 := false |
| 148 | mut all_backends := false |
| 149 | mut compile_backends := []string{} |
| 150 | mut user_defines := []string{} |
| 151 | mut i := 0 |
| 152 | for i < args.len { |
| 153 | if args[i] == '-o' && i + 1 < args.len { |
| 154 | output_file = args[i + 1] |
| 155 | i += 2 |
| 156 | } else if args[i] == '-b' && i + 1 < args.len { |
| 157 | backend = args[i + 1] |
| 158 | i += 2 |
| 159 | } else if args[i] == '-prod' { |
| 160 | is_prod = true |
| 161 | i++ |
| 162 | } else if args[i] == '-selfhost' { |
| 163 | is_selfhost = true |
| 164 | i++ |
| 165 | } else if args[i] == '-building-v' || args[i] == '-building_v' { |
| 166 | // The V compiler itself uses no generics, so monomorphization (and the rest |
| 167 | // of the generics machinery) is pure overhead when building it. |
| 168 | building_v = true |
| 169 | i++ |
| 170 | } else if args[i] == '-c99' || args[i] == '--c99' { |
| 171 | c99 = true |
| 172 | if 'c99' !in user_defines { |
| 173 | user_defines << 'c99' |
| 174 | } |
| 175 | i++ |
| 176 | } else if args[i] == '-strict' { |
| 177 | is_strict = true |
| 178 | i++ |
| 179 | } else if args[i] == '-no-parallel' || args[i] == '--no-parallel' { |
| 180 | no_parallel = true |
| 181 | i++ |
| 182 | } else if args[i] == '-parallel-transform' || args[i] == '--parallel-transform' { |
| 183 | parallel_transform = true |
| 184 | i++ |
| 185 | } else if args[i] == '-all-backends' || args[i] == '--all-backends' { |
| 186 | all_backends = true |
| 187 | i++ |
| 188 | } else if args[i] in ['-compile-backend', '--compile-backend'] && i + 1 < args.len { |
| 189 | compile_backends << args[i + 1] |
| 190 | i += 2 |
| 191 | } else if args[i] == '-d' && i + 1 < args.len { |
| 192 | user_defines << args[i + 1] |
| 193 | i += 2 |
| 194 | } else if args[i].starts_with('-d') && args[i].len > 2 { |
| 195 | user_defines << args[i][2..] |
| 196 | i++ |
| 197 | } else if args[i] in ['-gc', '-cc'] && i + 1 < args.len { |
| 198 | i += 2 |
| 199 | } else if args[i] in ['-prealloc', '-enable-globals'] { |
| 200 | i++ |
| 201 | } else if args[i].starts_with('-') { |
| 202 | i++ |
| 203 | } else { |
| 204 | input_file = args[i] |
| 205 | i++ |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | if input_file == '' { |
| 210 | eprintln('no input file') |
| 211 | exit(1) |
| 212 | } |
| 213 | |
| 214 | // Compiling the V compiler itself (v3.v) implies building_v: it uses no generics, so |
| 215 | // the monomorphization pass is pure overhead. -building-v can force this for any input. |
| 216 | if input_file.all_after_last('/') == 'v3.v' { |
| 217 | building_v = true |
| 218 | } |
| 219 | |
| 220 | mut bin_file := '' |
| 221 | mut c_only := false |
| 222 | if output_file == '' { |
| 223 | bin_file = input_file.all_before_last('.v') |
| 224 | // The wasm backend writes the binary itself; default to <name>.wasm. |
| 225 | output_file = if backend == 'wasm' { bin_file + '.wasm' } else { bin_file + '.c' } |
| 226 | } else if backend == 'wasm' { |
| 227 | // Honor the exact -o path; the wasm backend writes output_file directly. |
| 228 | bin_file = output_file.all_before_last('.wasm') |
| 229 | } else if backend == 'c' && output_file.ends_with('.c') { |
| 230 | c_only = true |
| 231 | bin_file = output_file.all_before_last('.c') |
| 232 | } else { |
| 233 | bin_file = output_file |
| 234 | output_file = bin_file + '.c' |
| 235 | } |
| 236 | |
| 237 | // Decide which backend modules to compile into the output. By default only the C |
| 238 | // backend is built; the arm64/wasm/eval backends (and the whole SSA pipeline that the |
| 239 | // arm64 backend pulls in: v3.ssa + v3.ssa.optimize) are skipped entirely. When compiling |
| 240 | // the V compiler itself this avoids parsing/checking/transforming/cgen-ing ~30k lines of |
| 241 | // unused backend code, which measurably speeds up the self-host build. The `skip_*` |
| 242 | // defines drive two things in lock-step: `$if !skip_* ?` gates in main() make the parser |
| 243 | // drop the dispatch blocks (so the backend symbols are never referenced), and |
| 244 | // resolve_imports skips parsing the corresponding module directories. |
| 245 | // `-all-backends` keeps everything; `-compile-backend <name>` opts a specific backend back |
| 246 | // in; the active `-b` target backend is always force-included. |
| 247 | mut include_arm64 := all_backends |
| 248 | mut include_wasm := all_backends |
| 249 | mut include_eval := all_backends |
| 250 | for cb in compile_backends { |
| 251 | for name in cb.split(',') { |
| 252 | match name.trim_space() { |
| 253 | 'arm64', 'aarch64' { include_arm64 = true } |
| 254 | 'wasm', 'wasm32' { include_wasm = true } |
| 255 | 'eval' { include_eval = true } |
| 256 | // 'c' is always built; there is no native amd64 backend in v3 yet. |
| 257 | else {} |
| 258 | } |
| 259 | } |
| 260 | } |
| 261 | match backend { |
| 262 | 'arm64' { include_arm64 = true } |
| 263 | 'wasm' { include_wasm = true } |
| 264 | 'eval' { include_eval = true } |
| 265 | else {} |
| 266 | } |
| 267 | |
| 268 | if !include_arm64 { |
| 269 | user_defines << 'skip_arm64' |
| 270 | } |
| 271 | if !include_wasm { |
| 272 | user_defines << 'skip_wasm' |
| 273 | } |
| 274 | if !include_eval { |
| 275 | user_defines << 'skip_eval' |
| 276 | } |
| 277 | |
| 278 | mut b := bench.new() |
| 279 | println('=== v3 benchmark ===') |
| 280 | |
| 281 | // Parse directly to flat AST |
| 282 | mut prefs := pref.new_preferences() |
| 283 | prefs.backend = backend |
| 284 | prefs.c99 = c99 |
| 285 | prefs.user_defines = user_defines |
| 286 | prefs.vroot = resolve_vroot(prefs.vroot) |
| 287 | prefs.selfhost = is_selfhost |
| 288 | prefs.building_v = building_v |
| 289 | mut p := parser.Parser.new(prefs) |
| 290 | |
| 291 | mut files := []string{} |
| 292 | builtin_dir := builtin_dir_for_vroot(prefs.vroot) |
| 293 | files << pref.get_v_files_from_dir(builtin_dir, prefs.user_defines, prefs.target_os) |
| 294 | p.parse_files(files) |
| 295 | mut a := p.a |
| 296 | a.user_code_start = a.nodes.len |
| 297 | |
| 298 | // Parse user input: single file or directory |
| 299 | mut user_files := []string{} |
| 300 | if input_file.ends_with('.v') { |
| 301 | user_files << input_file |
| 302 | user_files = expand_single_test_file_inputs(user_files, prefs) |
| 303 | } else if os.is_dir(input_file) { |
| 304 | user_files = pref.get_v_files_from_dir(input_file, prefs.user_defines, prefs.target_os) |
| 305 | for subdir in vmod_subdirs(input_file) { |
| 306 | subdir_path := os.join_path_single(input_file, subdir) |
| 307 | user_files << pref.get_v_files_from_dir(subdir_path, prefs.user_defines, |
| 308 | prefs.target_os) |
| 309 | } |
| 310 | } else { |
| 311 | user_files << input_file |
| 312 | } |
| 313 | for uf in user_files { |
| 314 | p.parse_into(uf) |
| 315 | } |
| 316 | test_files := test_input_files(user_files, backend) |
| 317 | |
| 318 | // Resolve imports recursively |
| 319 | resolve_imports(mut a, mut p, prefs, user_files) |
| 320 | diagnostic_root := if is_selfhost { |
| 321 | diagnostic_root_for_input(input_file, user_files) |
| 322 | } else { |
| 323 | '' |
| 324 | } |
| 325 | |
| 326 | b.step('parse') |
| 327 | |
| 328 | // Type-collect + check BEFORE transform, so the transformer is type-aware |
| 329 | // (like v2: check runs before transform). The transformer reads cached |
| 330 | // per-expression types for type-dependent lowering. |
| 331 | mut pre_tc := types.TypeChecker.new(a) |
| 332 | pre_tc.reject_unsupported_generics = is_selfhost |
| 333 | pre_tc.collect(a) |
| 334 | pre_tc.diagnose_unknown_calls = true |
| 335 | set_diagnostic_files(mut pre_tc, user_files) |
| 336 | set_unsupported_generic_files(mut pre_tc, a, is_selfhost, diagnostic_root) |
| 337 | pre_tc.check_semantics() |
| 338 | if pre_tc.errors.len > 0 { |
| 339 | print_type_errors(pre_tc.errors) |
| 340 | exit(1) |
| 341 | } |
| 342 | test_harness_errors := validate_test_file_harness_inputs(a, pre_tc, test_files) |
| 343 | if test_harness_errors.len > 0 { |
| 344 | for msg in test_harness_errors { |
| 345 | eprintln(msg) |
| 346 | } |
| 347 | exit(1) |
| 348 | } |
| 349 | b.step('check') |
| 350 | |
| 351 | if backend == 'eval' { |
| 352 | $if !skip_eval ? { |
| 353 | mut runner := eval.new(prefs) |
| 354 | runner.run_files(a) or { |
| 355 | eprintln('error: ${err.msg()}') |
| 356 | exit(1) |
| 357 | } |
| 358 | b.step('eval') |
| 359 | b.print_report() |
| 360 | return |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | // Mark used functions (dead-code elimination). This is done before transform |
| 365 | // so the transformer can skip function bodies that the C backend will prune. |
| 366 | mut used_fns := if test_files.len > 0 { |
| 367 | markused.mark_used_for_tests(a, pre_tc, test_files) |
| 368 | } else { |
| 369 | markused.mark_used(a, pre_tc) |
| 370 | } |
| 371 | b.step('markused') |
| 372 | |
| 373 | // Transform (match lowering, string/in lowering, etc.). Parallel transform is an |
| 374 | // explicit opt-in (`-parallel-transform`), independent of `-no-parallel` (which |
| 375 | // gates the parallel C codegen): the two phases can be threaded independently. |
| 376 | mut transform_was_parallel := false |
| 377 | used_fns, transform_was_parallel = transform.transform_with_used_opt(mut a, &pre_tc, used_fns, |
| 378 | parallel_transform) |
| 379 | b.step_parallel('transform', transform_was_parallel) |
| 380 | |
| 381 | // Reuse the pre-transform checker for metadata only. Transform does not add |
| 382 | // declarations, and v1/v2 do not run a second semantic checker after lowering. |
| 383 | pre_tc.diagnose_unknown_calls = false |
| 384 | pre_tc.reject_unlowered_map_mutation = true |
| 385 | set_diagnostic_files(mut pre_tc, user_files) |
| 386 | set_unsupported_generic_files(mut pre_tc, a, is_selfhost, diagnostic_root) |
| 387 | pre_tc.annotate_types_with_used(used_fns) |
| 388 | b.step('annotate types') |
| 389 | |
| 390 | if backend == 'wasm' { |
| 391 | $if !skip_wasm ? { |
| 392 | // Direct flat-AST-to-WASM native backend. Runs before monomorphize (which |
| 393 | // targets generics, not yet supported here). output_file is the exact path |
| 394 | // requested via -o (or the <name>.wasm default). |
| 395 | mut g := wasm.Gen.new(a, pre_tc, used_fns) |
| 396 | g.gen() |
| 397 | g.write(output_file) or { |
| 398 | eprintln('error writing ${output_file}') |
| 399 | exit(1) |
| 400 | } |
| 401 | for w in g.warnings_list() { |
| 402 | eprintln('wasm: ${w}') |
| 403 | } |
| 404 | b.step('wasm gen') |
| 405 | b.print_report() |
| 406 | return |
| 407 | } |
| 408 | } |
| 409 | |
| 410 | // Monomorphization only adds specialized generic instantiations to `used_fns`. The V |
| 411 | // compiler sources use no generics, so when building V we skip the pass entirely |
| 412 | // (`used_fns` passes through unchanged) instead of scanning the whole AST for nothing. |
| 413 | if !building_v { |
| 414 | used_fns = transform.monomorphize_with_used(mut a, &pre_tc, used_fns) |
| 415 | } |
| 416 | b.step('monomorphize') |
| 417 | |
| 418 | if backend == 'arm64' { |
| 419 | $if !skip_arm64 ? { |
| 420 | // SSA + ARM64 native backend |
| 421 | mut m := ssa.build_with_used(a, used_fns, pre_tc) |
| 422 | b.step('ssa build') |
| 423 | |
| 424 | if is_prod { |
| 425 | optimize.optimize(mut m) |
| 426 | b.step('optimize') |
| 427 | } |
| 428 | |
| 429 | mut g := arm64.Gen.new(m) |
| 430 | g.gen() |
| 431 | b.step('arm64 gen') |
| 432 | |
| 433 | g.write_and_link(bin_file) |
| 434 | b.step('link') |
| 435 | } |
| 436 | } else { |
| 437 | // C backend (default) |
| 438 | c_standard := c_standard_flag(prefs.c99) |
| 439 | mut g := cgen.FlatGen.new() |
| 440 | g.set_c99_mode(prefs.c99) |
| 441 | c_code := g.gen_with_used_test_options(a, used_fns, &pre_tc, no_parallel, test_files) |
| 442 | if !write_text_file_raw(output_file, c_code) { |
| 443 | eprintln('error writing ${output_file}') |
| 444 | exit(1) |
| 445 | } |
| 446 | b.step_parallel('cgen', g.was_parallel()) |
| 447 | if c_only { |
| 448 | b.print_report() |
| 449 | return |
| 450 | } |
| 451 | |
| 452 | opt_flag := if is_prod { '-O2 ' } else { '' } |
| 453 | warn_flags := if is_strict { |
| 454 | '-Wall -Wextra -Werror=implicit-function-declaration -Wno-unused-variable -Wno-unused-parameter -Wno-int-conversion -Wno-missing-braces' |
| 455 | } else { |
| 456 | '-w' |
| 457 | } |
| 458 | resolved_c_flags := prepare_c_flags_for_link(g.c_flags(), prefs.c99) or { |
| 459 | eprintln(err.msg()) |
| 460 | exit(1) |
| 461 | } |
| 462 | c_flags := resolved_c_flags.join(' ') |
| 463 | // Compile inside a per-output build dir, using constant relative source/output basenames, |
| 464 | // then move the result to bin_file. On macOS arm64 tcc bakes the -o basename into the |
| 465 | // ad-hoc code-signature identifier and the input .c path into the symbol table, so building |
| 466 | // `v5.c`->`v5` vs `v6.c`->`v6` directly would make the binaries differ only by those embedded |
| 467 | // names (plus the code-directory hashes covering them). Compiling fixed `src.c`->`out` keeps |
| 468 | // those embedded names identical, so the self-host chain is byte-for-byte reproducible |
| 469 | // (v5 == v6). The build dir is unique per output and never embedded (we cd into it), so |
| 470 | // parallel compilations into a shared directory never clobber each other. |
| 471 | cc_dir := '${bin_file}.v3cc' |
| 472 | os.mkdir(cc_dir) or {} |
| 473 | cc_src := os.join_path_single(cc_dir, 'src.c') |
| 474 | cc_out := os.join_path_single(cc_dir, 'out') |
| 475 | if !write_text_file_raw(cc_src, c_code) { |
| 476 | eprintln('error writing ${cc_src}') |
| 477 | exit(1) |
| 478 | } |
| 479 | mut cc_cmd := '' |
| 480 | mut exec_cmd := '' |
| 481 | mut result := os.Result{} |
| 482 | if !is_prod { |
| 483 | tcc_dir := os.join_path_single(os.join_path_single(prefs.vroot, 'thirdparty'), 'tcc') |
| 484 | tcc_path := os.join_path_single(tcc_dir, 'tcc.exe') |
| 485 | tcc_lib_dir := os.join_path_single(tcc_dir, 'lib') |
| 486 | tcc_includes := '-I${os.join_path_single(tcc_lib_dir, 'include')}' |
| 487 | tcc_lib := '-L${tcc_lib_dir}' |
| 488 | cc_cmd = '${tcc_path} ${c_standard} ${tcc_includes} ${tcc_lib} ${warn_flags} -o ${bin_file} ${output_file} ${c_flags} -lm' |
| 489 | exec_cmd = 'cd ${cc_dir} && ${tcc_path} ${c_standard} ${tcc_includes} ${tcc_lib} ${warn_flags} -o out src.c ${c_flags} -lm' |
| 490 | println(' > ${cc_cmd}') |
| 491 | result = run_compile_command(exec_cmd) |
| 492 | } |
| 493 | if is_prod || result.exit_code != 0 { |
| 494 | if result.exit_code != 0 && result.output.len > 0 { |
| 495 | eprintln(' tcc error: ${result.output.trim_space()}') |
| 496 | } |
| 497 | stack_flag := if prefs.normalized_target_os() == 'macos' { |
| 498 | ' -Wl,-stack_size,0x4000000' |
| 499 | } else { |
| 500 | '' |
| 501 | } |
| 502 | cc_cmd = 'cc ${c_standard} ${opt_flag}${warn_flags} -Wno-int-conversion${stack_flag} -o ${bin_file} ${output_file} ${c_flags} -lm' |
| 503 | exec_cmd = 'cd ${cc_dir} && cc ${c_standard} ${opt_flag}${warn_flags} -Wno-int-conversion${stack_flag} -o out src.c ${c_flags} -lm' |
| 504 | println(' > ${cc_cmd}') |
| 505 | result = run_compile_command(exec_cmd) |
| 506 | if result.exit_code != 0 { |
| 507 | eprintln('C compilation failed:') |
| 508 | eprintln(result.output) |
| 509 | exit(1) |
| 510 | } |
| 511 | } |
| 512 | os.mv(cc_out, bin_file) or { |
| 513 | eprintln('failed to finalize ${bin_file}: ${err}') |
| 514 | exit(1) |
| 515 | } |
| 516 | os.rm(cc_src) or {} |
| 517 | os.rmdir(cc_dir) or {} |
| 518 | b.step('cc') |
| 519 | } |
| 520 | |
| 521 | b.print_report() |
| 522 | } |
| 523 | |
| 524 | fn vmod_subdirs(dir string) []string { |
| 525 | vmod_path := os.join_path_single(dir, 'v.mod') |
| 526 | content := os.read_file(vmod_path) or { return []string{} } |
| 527 | subdirs_pos := content.index('subdirs:') or { return []string{} } |
| 528 | after_subdirs := content[subdirs_pos..] |
| 529 | lb_rel := after_subdirs.index_u8(`[`) |
| 530 | if lb_rel < 0 { |
| 531 | return []string{} |
| 532 | } |
| 533 | after_lb := after_subdirs[lb_rel + 1..] |
| 534 | rb_rel := after_lb.index_u8(`]`) |
| 535 | if rb_rel < 0 { |
| 536 | return []string{} |
| 537 | } |
| 538 | raw_items := after_lb[..rb_rel].split(',') |
| 539 | mut subdirs := []string{} |
| 540 | for raw in raw_items { |
| 541 | item := raw.trim_space().trim('\'"') |
| 542 | if item.len > 0 { |
| 543 | subdirs << item |
| 544 | } |
| 545 | } |
| 546 | return subdirs |
| 547 | } |
| 548 | |
| 549 | fn expand_single_test_file_inputs(user_files []string, prefs &pref.Preferences) []string { |
| 550 | mut expanded := []string{} |
| 551 | mut seen := map[string]bool{} |
| 552 | for file in user_files { |
| 553 | if pref.is_test_file_for_backend(file, prefs.backend) { |
| 554 | module_name := declared_module_in_file(file) |
| 555 | if module_name.len > 0 && module_name != 'main' && module_name != 'builtin' { |
| 556 | for module_file in same_dir_module_source_files(file, module_name, prefs) { |
| 557 | append_unique_file(mut expanded, mut seen, module_file) |
| 558 | } |
| 559 | } |
| 560 | } |
| 561 | append_unique_file(mut expanded, mut seen, file) |
| 562 | } |
| 563 | return expanded |
| 564 | } |
| 565 | |
| 566 | fn same_dir_module_source_files(test_file string, module_name string, prefs &pref.Preferences) []string { |
| 567 | dir := os.dir(test_file) |
| 568 | mut files := []string{} |
| 569 | for file in pref.get_v_files_from_dir(dir, prefs.user_defines, prefs.target_os) { |
| 570 | if declared_module_in_file(file) == module_name { |
| 571 | files << file |
| 572 | } |
| 573 | } |
| 574 | return files |
| 575 | } |
| 576 | |
| 577 | fn append_unique_file(mut files []string, mut seen map[string]bool, file string) { |
| 578 | key := os.real_path(file) |
| 579 | if seen[key] { |
| 580 | return |
| 581 | } |
| 582 | seen[key] = true |
| 583 | files << file |
| 584 | } |
| 585 | |
| 586 | fn declared_module_in_file(path string) string { |
| 587 | content := os.read_file(path) or { return '' } |
| 588 | mut in_block_comment := false |
| 589 | mut in_attr := false |
| 590 | for raw_line in content.split_into_lines() { |
| 591 | mut line := raw_line.trim_space() |
| 592 | if in_block_comment { |
| 593 | if end := line.index('*/') { |
| 594 | line = line[end + 2..].trim_space() |
| 595 | in_block_comment = false |
| 596 | } else { |
| 597 | continue |
| 598 | } |
| 599 | } |
| 600 | if in_attr { |
| 601 | if line.contains(']') { |
| 602 | in_attr = false |
| 603 | } |
| 604 | continue |
| 605 | } |
| 606 | for line.starts_with('/*') { |
| 607 | if end := line.index('*/') { |
| 608 | line = line[end + 2..].trim_space() |
| 609 | } else { |
| 610 | in_block_comment = true |
| 611 | line = '' |
| 612 | break |
| 613 | } |
| 614 | } |
| 615 | if line.len == 0 || line.starts_with('//') { |
| 616 | continue |
| 617 | } |
| 618 | if line.starts_with('@[') || line.starts_with('[') { |
| 619 | if !line.contains(']') { |
| 620 | in_attr = true |
| 621 | } |
| 622 | continue |
| 623 | } |
| 624 | if line.starts_with('module ') { |
| 625 | mut module_name := line[7..] |
| 626 | if comment := module_name.index('//') { |
| 627 | module_name = module_name[..comment] |
| 628 | } |
| 629 | if comment := module_name.index('/*') { |
| 630 | module_name = module_name[..comment] |
| 631 | } |
| 632 | return module_name.trim_space() |
| 633 | } |
| 634 | return '' |
| 635 | } |
| 636 | return '' |
| 637 | } |
| 638 | |
| 639 | fn project_root_for_files(files []string) string { |
| 640 | for file in files { |
| 641 | root := nearest_vmod_root_for_file(file) |
| 642 | if root.len > 0 { |
| 643 | return root |
| 644 | } |
| 645 | } |
| 646 | if files.len > 0 { |
| 647 | return os.dir(files[0]) |
| 648 | } |
| 649 | return os.getwd() |
| 650 | } |
| 651 | |
| 652 | fn nearest_vmod_root_for_file(path string) string { |
| 653 | mut dir := if os.is_dir(path) { path } else { os.dir(path) } |
| 654 | for _ in 0 .. 32 { |
| 655 | if os.exists(os.join_path_single(dir, 'v.mod')) { |
| 656 | return dir |
| 657 | } |
| 658 | parent := os.dir(dir) |
| 659 | if parent == dir { |
| 660 | break |
| 661 | } |
| 662 | dir = parent |
| 663 | } |
| 664 | return '' |
| 665 | } |
| 666 | |
| 667 | // resolve_vroot resolves resolve vroot information for v3 entry point. |
| 668 | fn resolve_vroot(initial string) string { |
| 669 | if is_valid_vroot(initial) { |
| 670 | return initial |
| 671 | } |
| 672 | mut dir := os.getwd() |
| 673 | for _ in 0 .. 8 { |
| 674 | if is_valid_vroot(dir) { |
| 675 | return dir |
| 676 | } |
| 677 | parent := os.dir(dir) |
| 678 | if parent == dir { |
| 679 | break |
| 680 | } |
| 681 | dir = parent |
| 682 | } |
| 683 | return initial |
| 684 | } |
| 685 | |
| 686 | // is_valid_vroot reports whether is valid vroot applies in v3 entry point. |
| 687 | fn is_valid_vroot(root string) bool { |
| 688 | return root.len > 0 && os.is_dir(builtin_dir_for_vroot(root)) |
| 689 | } |
| 690 | |
| 691 | // builtin_dir_for_vroot supports builtin dir for vroot handling for v3 entry point. |
| 692 | fn builtin_dir_for_vroot(root string) string { |
| 693 | return os.join_path_single(os.join_path_single(root, 'vlib'), 'builtin') |
| 694 | } |
| 695 | |
| 696 | // write_text_file_raw writes text file raw output for v3 entry point. |
| 697 | fn write_text_file_raw(path string, data string) bool { |
| 698 | // Delegate to the stdlib writer so the open flags (O_CREAT/O_TRUNC, binary mode) |
| 699 | // are correct on every platform, instead of hardcoding per-OS bit values. |
| 700 | os.write_file(path, data) or { return false } |
| 701 | return true |
| 702 | } |
| 703 | |
| 704 | // print_type_errors updates print type errors state for v3 entry point. |
| 705 | fn print_type_errors(errors []types.TypeError) { |
| 706 | eprintln('type checker found ${errors.len} error(s):') |
| 707 | max_errors := if errors.len < 20 { errors.len } else { 20 } |
| 708 | for ei in 0 .. max_errors { |
| 709 | eprintln(' ${errors[ei].msg}') |
| 710 | } |
| 711 | if errors.len > 20 { |
| 712 | eprintln(' ... and ${errors.len - 20} more') |
| 713 | } |
| 714 | } |
| 715 | |
| 716 | fn diagnostic_root_for_input(input_file string, user_files []string) string { |
| 717 | if input_file.len > 0 && os.is_dir(input_file) { |
| 718 | return os.real_path(input_file) |
| 719 | } |
| 720 | if user_files.len > 0 { |
| 721 | return os.real_path(os.dir(user_files[0])) |
| 722 | } |
| 723 | return os.real_path(os.getwd()) |
| 724 | } |
| 725 | |
| 726 | fn test_input_files(user_files []string, backend string) []string { |
| 727 | mut files := []string{} |
| 728 | for file in user_files { |
| 729 | if pref.is_test_file_for_backend(file, backend) { |
| 730 | files << file |
| 731 | } |
| 732 | } |
| 733 | return files |
| 734 | } |
| 735 | |
| 736 | fn validate_test_file_harness_inputs(a &flat.FlatAst, tc &types.TypeChecker, test_files []string) []string { |
| 737 | if test_files.len == 0 { |
| 738 | return [] |
| 739 | } |
| 740 | mut selected_files := map[string]bool{} |
| 741 | for file in test_files { |
| 742 | selected_files[file] = true |
| 743 | } |
| 744 | mut errors := []string{} |
| 745 | for file_idx, file_node in a.nodes { |
| 746 | if !is_user_test_file_node(a, file_idx, file_node, selected_files) { |
| 747 | continue |
| 748 | } |
| 749 | if test_file_has_executable_top_level_stmt(a, file_node) { |
| 750 | errors << 'invalid test file ${file_node.value}: executable top-level statements are not supported in test files' |
| 751 | continue |
| 752 | } |
| 753 | mut runnable_tests := 0 |
| 754 | mut invalid_items := 0 |
| 755 | mut decl_ids := []flat.NodeId{} |
| 756 | collect_test_harness_decl_ids(a, file_node, mut decl_ids) |
| 757 | for child_id in decl_ids { |
| 758 | child := a.node(child_id) |
| 759 | if child.value.starts_with('test_') { |
| 760 | if is_supported_test_harness_fn(a, tc, child) { |
| 761 | runnable_tests++ |
| 762 | } else { |
| 763 | invalid_items++ |
| 764 | errors << 'invalid test signature: ${child.value} must be zero-arg and return void, ?, or !' |
| 765 | } |
| 766 | } else if is_test_harness_hook_name(child.value) { |
| 767 | if !is_supported_test_harness_hook(a, tc, child) { |
| 768 | invalid_items++ |
| 769 | errors << 'invalid test hook signature: ${child.value} must be zero-arg void' |
| 770 | } |
| 771 | } |
| 772 | } |
| 773 | if runnable_tests == 0 && invalid_items == 0 { |
| 774 | errors << 'no runnable tests in ${file_node.value}' |
| 775 | } |
| 776 | } |
| 777 | return errors |
| 778 | } |
| 779 | |
| 780 | fn test_file_has_executable_top_level_stmt(a &flat.FlatAst, node flat.Node) bool { |
| 781 | if node.kind != .file && node.kind != .block { |
| 782 | return false |
| 783 | } |
| 784 | for i in 0 .. node.children_count { |
| 785 | child_id := a.child(&node, i) |
| 786 | if int(child_id) < a.user_code_start { |
| 787 | continue |
| 788 | } |
| 789 | child := a.node(child_id) |
| 790 | if child.kind == .block { |
| 791 | if test_file_has_executable_top_level_stmt(a, child) { |
| 792 | return true |
| 793 | } |
| 794 | } else if test_file_is_executable_top_level_stmt(child) { |
| 795 | return true |
| 796 | } |
| 797 | } |
| 798 | return false |
| 799 | } |
| 800 | |
| 801 | fn test_file_is_executable_top_level_stmt(node flat.Node) bool { |
| 802 | return match node.kind { |
| 803 | .expr_stmt, .assign, .decl_assign, .selector_assign, .index_assign, .for_stmt, |
| 804 | .for_in_stmt, .if_expr, .match_stmt, .assert_stmt, .defer_stmt { |
| 805 | true |
| 806 | } |
| 807 | else { |
| 808 | false |
| 809 | } |
| 810 | } |
| 811 | } |
| 812 | |
| 813 | fn collect_test_harness_decl_ids(a &flat.FlatAst, node flat.Node, mut ids []flat.NodeId) { |
| 814 | if node.kind != .file && node.kind != .block { |
| 815 | return |
| 816 | } |
| 817 | for i in 0 .. node.children_count { |
| 818 | child_id := a.child(&node, i) |
| 819 | if int(child_id) < a.user_code_start { |
| 820 | continue |
| 821 | } |
| 822 | child := a.node(child_id) |
| 823 | if child.kind == .fn_decl { |
| 824 | ids << child_id |
| 825 | } else if child.kind == .block { |
| 826 | collect_test_harness_decl_ids(a, child, mut ids) |
| 827 | } |
| 828 | } |
| 829 | } |
| 830 | |
| 831 | fn is_user_test_file_node(a &flat.FlatAst, file_idx int, file_node flat.Node, test_files map[string]bool) bool { |
| 832 | if file_idx < a.user_code_start || file_node.kind != .file || file_node.children_count == 0 { |
| 833 | return false |
| 834 | } |
| 835 | return test_files[file_node.value] |
| 836 | } |
| 837 | |
| 838 | fn test_file_module_name(a &flat.FlatAst, file_node flat.Node) string { |
| 839 | for i in 0 .. file_node.children_count { |
| 840 | child := a.child_node(&file_node, i) |
| 841 | if child.kind == .module_decl { |
| 842 | return child.value |
| 843 | } |
| 844 | } |
| 845 | return '' |
| 846 | } |
| 847 | |
| 848 | fn is_supported_test_harness_fn(a &flat.FlatAst, tc &types.TypeChecker, node &flat.Node) bool { |
| 849 | if node.generic_params.len > 0 { |
| 850 | return false |
| 851 | } |
| 852 | if test_harness_fn_param_count(a, node) != 0 { |
| 853 | return false |
| 854 | } |
| 855 | return test_harness_fn_return_supported(tc.parse_type(node.typ)) |
| 856 | } |
| 857 | |
| 858 | fn is_supported_test_harness_hook(a &flat.FlatAst, tc &types.TypeChecker, node &flat.Node) bool { |
| 859 | if node.generic_params.len > 0 { |
| 860 | return false |
| 861 | } |
| 862 | return test_harness_fn_param_count(a, node) == 0 && tc.parse_type(node.typ) is types.Void |
| 863 | } |
| 864 | |
| 865 | fn test_harness_fn_param_count(a &flat.FlatAst, node &flat.Node) int { |
| 866 | mut count := 0 |
| 867 | for i in 0 .. node.children_count { |
| 868 | child := a.child_node(node, i) |
| 869 | if child.kind == .param { |
| 870 | count++ |
| 871 | } |
| 872 | } |
| 873 | return count |
| 874 | } |
| 875 | |
| 876 | fn test_harness_fn_return_supported(ret types.Type) bool { |
| 877 | return ret is types.Void || ret is types.OptionType || ret is types.ResultType |
| 878 | } |
| 879 | |
| 880 | fn is_test_harness_hook_name(name string) bool { |
| 881 | return name in ['testsuite_begin', 'testsuite_end', 'before_each', 'after_each'] |
| 882 | } |
| 883 | |
| 884 | fn set_diagnostic_files(mut tc types.TypeChecker, user_files []string) { |
| 885 | for uf in user_files { |
| 886 | tc.diagnostic_files[uf] = true |
| 887 | } |
| 888 | } |
| 889 | |
| 890 | fn set_unsupported_generic_files(mut tc types.TypeChecker, a &flat.FlatAst, include_imports bool, diagnostic_root string) { |
| 891 | if !include_imports { |
| 892 | return |
| 893 | } |
| 894 | for i, node in a.nodes { |
| 895 | if i < a.user_code_start || node.kind != .file || node.value.len == 0 { |
| 896 | continue |
| 897 | } |
| 898 | if path_is_in_dir(node.value, diagnostic_root) { |
| 899 | tc.diagnostic_files['generic:' + node.value] = true |
| 900 | } |
| 901 | } |
| 902 | } |
| 903 | |
| 904 | fn path_is_in_dir(path string, dir string) bool { |
| 905 | real_path := os.real_path(path) |
| 906 | real_dir := os.real_path(dir) |
| 907 | return real_path == real_dir || real_path.starts_with(real_dir + os.path_separator) |
| 908 | } |
| 909 | |
| 910 | // skipped_backend_modules lists the importable backend module names that the current |
| 911 | // configuration excludes (driven by the same `skip_*` defines that gate the dispatch in |
| 912 | // main()). The arm64 backend is the only consumer of the SSA pipeline, so skipping it also |
| 913 | // skips v3.ssa and v3.ssa.optimize. |
| 914 | fn skipped_backend_modules(prefs &pref.Preferences) []string { |
| 915 | mut skipped := []string{} |
| 916 | if 'skip_arm64' in prefs.user_defines { |
| 917 | skipped << 'v3.gen.arm64' |
| 918 | skipped << 'v3.ssa' |
| 919 | skipped << 'v3.ssa.optimize' |
| 920 | } |
| 921 | if 'skip_wasm' in prefs.user_defines { |
| 922 | skipped << 'v3.gen.wasm' |
| 923 | } |
| 924 | if 'skip_eval' in prefs.user_defines { |
| 925 | skipped << 'v3.eval' |
| 926 | } |
| 927 | return skipped |
| 928 | } |
| 929 | |
| 930 | // resolve_imports resolves resolve imports information for v3 entry point. |
| 931 | fn resolve_imports(mut a flat.FlatAst, mut p parser.Parser, prefs &pref.Preferences, initial_files []string) { |
| 932 | mut parsed_modules := map[string]bool{} |
| 933 | parsed_modules['builtin'] = true |
| 934 | parsed_modules['main'] = true |
| 935 | seed_initial_modules(a, initial_files, mut parsed_modules) |
| 936 | |
| 937 | // Backend modules excluded by the active configuration are never parsed: their |
| 938 | // dispatch in main() is gated out by the matching `$if !skip_* ?`, so nothing |
| 939 | // references their symbols. Pre-seeding parsed_modules makes the loop below treat |
| 940 | // them as already handled, so neither v3.v's top-level imports nor any transitive |
| 941 | // import pulls them in. Skipping the arm64 group (v3.gen.arm64 + the v3.ssa SSA |
| 942 | // pipeline) and the wasm/eval backends avoids ~30k lines of work when self-hosting. |
| 943 | for skipped in skipped_backend_modules(prefs) { |
| 944 | parsed_modules[skipped] = true |
| 945 | } |
| 946 | |
| 947 | mut first_file := '' |
| 948 | if initial_files.len > 0 { |
| 949 | first_file = initial_files[0] |
| 950 | } |
| 951 | project_root := project_root_for_files(initial_files) |
| 952 | |
| 953 | mut changed := true |
| 954 | for changed { |
| 955 | changed = false |
| 956 | mut cur_file := first_file |
| 957 | scan_len := a.nodes.len |
| 958 | for node_idx in 0 .. scan_len { |
| 959 | node := a.nodes[node_idx] |
| 960 | if node.kind == .file && node.value.len > 0 { |
| 961 | cur_file = node.value |
| 962 | continue |
| 963 | } |
| 964 | if node.kind != .import_decl { |
| 965 | continue |
| 966 | } |
| 967 | mod_name := node.value |
| 968 | if mod_name in parsed_modules { |
| 969 | continue |
| 970 | } |
| 971 | parsed_modules[mod_name] = true |
| 972 | changed = true |
| 973 | |
| 974 | importing_file := if cur_file.len > 0 { cur_file } else { first_file } |
| 975 | mod_dir := resolve_project_or_pref_module_path(prefs, mod_name, importing_file, |
| 976 | project_root) |
| 977 | if mod_dir == '' || !os.is_dir(mod_dir) { |
| 978 | continue |
| 979 | } |
| 980 | module_identity := import_module_identity(prefs, mod_name, importing_file, |
| 981 | project_root, mod_dir) |
| 982 | mod_files := pref.get_v_files_from_dir(mod_dir, prefs.user_defines, prefs.target_os) |
| 983 | for mf in mod_files { |
| 984 | first_node := a.nodes.len |
| 985 | p.parse_into(mf) |
| 986 | if module_identity == mod_name { |
| 987 | canonicalize_imported_module_name(mut a, first_node, mod_name) |
| 988 | } |
| 989 | } |
| 990 | } |
| 991 | } |
| 992 | normalize_import_module_identities(mut a, prefs, first_file, project_root) |
| 993 | } |
| 994 | |
| 995 | fn seed_initial_modules(a &flat.FlatAst, initial_files []string, mut parsed_modules map[string]bool) { |
| 996 | mut selected_files := map[string]bool{} |
| 997 | for file in initial_files { |
| 998 | selected_files[file] = true |
| 999 | selected_files[os.real_path(file)] = true |
| 1000 | } |
| 1001 | for file_idx, file_node in a.nodes { |
| 1002 | if file_idx < a.user_code_start || file_node.kind != .file || file_node.value.len == 0 { |
| 1003 | continue |
| 1004 | } |
| 1005 | if !selected_files[file_node.value] && !selected_files[os.real_path(file_node.value)] { |
| 1006 | continue |
| 1007 | } |
| 1008 | module_name := test_file_module_name(a, file_node) |
| 1009 | if module_name.len > 0 { |
| 1010 | parsed_modules[module_name] = true |
| 1011 | } |
| 1012 | } |
| 1013 | } |
| 1014 | |
| 1015 | fn canonicalize_imported_module_name(mut a flat.FlatAst, first_node int, import_path string) { |
| 1016 | if import_path.len == 0 { |
| 1017 | return |
| 1018 | } |
| 1019 | short_name := import_path.all_after_last('.') |
| 1020 | for i in first_node .. a.nodes.len { |
| 1021 | if a.nodes[i].kind == .module_decl && a.nodes[i].value == short_name { |
| 1022 | a.nodes[i].value = import_path |
| 1023 | } |
| 1024 | } |
| 1025 | } |
| 1026 | |
| 1027 | fn normalize_import_module_identities(mut a flat.FlatAst, prefs &pref.Preferences, first_file string, project_root string) { |
| 1028 | mut cur_file := first_file |
| 1029 | for i in 0 .. a.nodes.len { |
| 1030 | node := a.nodes[i] |
| 1031 | if node.kind == .file && node.value.len > 0 { |
| 1032 | cur_file = node.value |
| 1033 | continue |
| 1034 | } |
| 1035 | if node.kind != .import_decl { |
| 1036 | continue |
| 1037 | } |
| 1038 | importing_file := if cur_file.len > 0 { cur_file } else { first_file } |
| 1039 | mod_dir := resolve_project_or_pref_module_path(prefs, node.value, importing_file, |
| 1040 | project_root) |
| 1041 | a.nodes[i].value = import_module_identity(prefs, node.value, importing_file, project_root, |
| 1042 | mod_dir) |
| 1043 | } |
| 1044 | } |
| 1045 | |
| 1046 | fn import_module_identity(prefs &pref.Preferences, import_path string, importing_file string, project_root string, import_dir string) string { |
| 1047 | if !import_path.contains('.') { |
| 1048 | return import_path |
| 1049 | } |
| 1050 | short_name := import_path.all_after_last('.') |
| 1051 | short_dir := resolve_project_or_pref_module_path(prefs, short_name, importing_file, |
| 1052 | project_root) |
| 1053 | if short_dir.len > 0 && import_dir.len > 0 && os.is_dir(short_dir) |
| 1054 | && os.real_path(short_dir) != os.real_path(import_dir) { |
| 1055 | return import_path |
| 1056 | } |
| 1057 | return short_name |
| 1058 | } |
| 1059 | |
| 1060 | fn resolve_project_or_pref_module_path(prefs &pref.Preferences, mod_name string, importing_file string, project_root string) string { |
| 1061 | if project_root.len > 0 { |
| 1062 | project_path := os.join_path_single(project_root, mod_name.replace('.', os.path_separator)) |
| 1063 | if os.is_dir(project_path) { |
| 1064 | return project_path |
| 1065 | } |
| 1066 | } |
| 1067 | return prefs.get_module_path(mod_name, importing_file) |
| 1068 | } |
| 1069 | |