vxx2 / vlib / v3 / v3.v
1068 lines · 991 sloc · 31.6 KB · 2b81918e62db9ab38787518603a7f93bee80868c
Raw
1module main
2
3import os
4import v3.bench
5import v3.flat
6import v3.gen.c as cgen
7import v3.markused
8import v3.parser
9import v3.pref
10import v3.transform
11import 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.
26fn 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
33fn 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
47fn 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
64fn 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
88fn 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
99fn 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
109fn 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
118fn c_flag_is_object_file(flag string) bool {
119 return flag.ends_with('.o') || flag.ends_with('.obj')
120}
121
122fn 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
126fn c_standard_flag(c99 bool) string {
127 return if c99 { '-std=c99' } else { '-std=gnu11' }
128}
129
130// main runs the v3 entry point.
131fn 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
524fn 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
549fn 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
566fn 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
577fn 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
586fn 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
639fn 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
652fn 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.
668fn 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.
687fn 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.
692fn 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.
697fn 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.
705fn 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
716fn 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
726fn 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
736fn 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
780fn 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
801fn 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
813fn 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
831fn 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
838fn 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
848fn 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
858fn 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
865fn 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
876fn 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
880fn is_test_harness_hook_name(name string) bool {
881 return name in ['testsuite_begin', 'testsuite_end', 'before_each', 'after_each']
882}
883
884fn 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
890fn 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
904fn 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.
914fn 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.
931fn 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
995fn 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
1015fn 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
1027fn 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
1046fn 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
1060fn 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