v4 / vlib / v3 / v3.v
954 lines · 883 sloc · 28.82 KB · 1498e02bf64c6e33255fc0d3055b6e72900af9a5
Raw
1module main
2
3import os
4import v3.bench
5import v3.eval
6import v3.flat
7import v3.gen.arm64
8import v3.gen.c as cgen
9import v3.gen.wasm
10import v3.markused
11import v3.parser
12import v3.pref
13import v3.ssa
14import v3.ssa.optimize
15import v3.transform
16import v3.types
17
18// run_compile_command supports run compile command handling for v3 entry point.
19fn 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
26fn 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
40fn 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
57fn 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
81fn 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
92fn 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
102fn 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
111fn c_flag_is_object_file(flag string) bool {
112 return flag.ends_with('.o') || flag.ends_with('.obj')
113}
114
115fn 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
119fn c_standard_flag(c99 bool) string {
120 return if c99 { '-std=c99' } else { '-std=gnu11' }
121}
122
123// main runs the v3 entry point.
124fn 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
516fn 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
541fn 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
554fn 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.
570fn 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.
589fn 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.
594fn 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.
599fn 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.
607fn 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
618fn 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
628fn 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
638fn 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
687fn 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
708fn 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
720fn 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
738fn 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
745fn 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
755fn 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
765fn 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
772fn 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
783fn 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
787fn is_test_harness_hook_name(name string) bool {
788 return name in ['testsuite_begin', 'testsuite_end', 'before_each', 'after_each']
789}
790
791fn 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
797fn 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
811fn 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.
821fn 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.
838fn 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
901fn 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
913fn 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
932fn 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
946fn 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