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