vxx2 / vlib / v / builder / builder.v
1322 lines · 1243 sloc · 40.16 KB · ddc9c99fda9b46d4a1e3d8fe674a4395bb311d96
Raw
1module builder
2
3import os
4import v.token
5import v.pref
6import v.errors
7import v.util
8import v.ast
9import v.ast.walker
10import v.vmod
11import v.checker
12import v.transformer
13import v.comptime
14import v.generics
15import v.parser
16import v.markused
17import v.depgraph
18import v.callgraph
19import v.dotgraph
20// import x.json2
21
22fn append_map_array(mut items map[string][]string, key string, value string) {
23 if key !in items {
24 items[key] = []string{}
25 }
26 items[key] << value
27}
28
29pub struct Builder {
30pub:
31 compiled_dir string // contains os.real_path() of the dir of the final file being compiled, or the dir itself when doing `v .`
32 module_path string
33pub mut:
34 checker &checker.Checker = unsafe { nil }
35 transformer &transformer.Transformer = unsafe { nil }
36 comptime &comptime.Comptime = unsafe { nil }
37 generics &generics.Generics = unsafe { nil }
38 out_name_c string
39 out_name_js string
40 stats_lines int // size of backend generated source code in lines
41 stats_bytes int // size of backend generated source code in bytes
42 nr_errors int // accumulated error count of scanner, parser, checker, and builder
43 nr_warnings int // accumulated warning count of scanner, parser, checker, and builder
44 nr_notices int // accumulated notice count of scanner, parser, checker, and builder
45 pref &pref.Preferences = unsafe { nil }
46 module_search_paths []string
47 parsed_files []&ast.File
48 //$if windows {
49 cached_msvc MsvcResult
50 //}
51 table &ast.Table = unsafe { nil }
52 ccoptions CcompilerOptions
53 // Note: changes in mod `builtin` force invalidation of every other .v file
54 mod_invalidates_paths map[string][]string // changes in mod `os`, invalidate only .v files, that do `import os`
55 mod_invalidates_mods map[string][]string // changes in mod `os`, force invalidation of mods, that do `import os`
56 path_invalidates_mods map[string][]string // changes in a .v file from `os`, invalidates `os`
57 crun_cache_keys []string // target executable + top level source files; filled in by Builder.should_rebuild
58 executable_exists bool // if the executable already exists, don't remove new executable after `v run`
59 str_args string // for parallel_cc mode only, to know which cc args to use (like -I etc)
60 last_cc_cmd string // the most recently executed C compiler command; reused to regenerate a #line annotated report
61 disable_flto bool
62 thirdparty_header_mtimes map[string]i64
63}
64
65struct CFunctionCallCollector {
66mut:
67 names map[string]bool
68}
69
70fn c_function_call_collector_visit(node &ast.Node, data voidptr) bool {
71 mut c := unsafe { &CFunctionCallCollector(data) }
72 if node is ast.Expr && node is ast.CallExpr {
73 call := node as ast.CallExpr
74 if call.name.starts_with('C.') {
75 c.names[call.name] = true
76 }
77 }
78 return true
79}
80
81pub fn new_builder(pref_ &pref.Preferences) Builder {
82 rdir := os.real_path(pref_.path)
83 compiled_dir := if os.is_dir(rdir) { rdir } else { os.dir(rdir) }
84 mut table := ast.new_table()
85 table.is_fmt = false
86 if pref_.use_color == .always {
87 util.emanager.set_support_color(true)
88 }
89 if pref_.use_color == .never {
90 util.emanager.set_support_color(false)
91 }
92 table.pointer_size = if pref_.m64 && pref_.backend != .wasm { 8 } else { 4 }
93 mut msvc := MsvcResult{}
94 if pref_.ccompiler_type == .msvc || pref.cc_from_string(pref_.ccompiler) == .msvc {
95 $if windows {
96 msvc = find_msvc(pref_.m64) or {
97 MsvcResult{
98 valid: false
99 }
100 }
101 }
102 }
103 util.timing_set_should_print(pref_.show_timings || pref_.is_verbose)
104 if pref_.show_callgraph || pref_.show_depgraph {
105 dotgraph.start_digraph()
106 }
107 mut executable_name := pref_.out_name
108 $if windows {
109 executable_name += '.exe'
110 }
111 return Builder{
112 pref: pref_
113 table: table
114 checker: checker.new_checker(table, pref_)
115 transformer: transformer.new_transformer_with_table(table, pref_)
116 comptime: comptime.new_comptime_with_table(table, pref_)
117 generics: generics.new_generics_with_table(table, pref_)
118 compiled_dir: compiled_dir
119 cached_msvc: msvc
120 executable_exists: os.is_file(executable_name)
121 }
122}
123
124fn (v &Builder) msvc_object_path(path string) string {
125 path_without_obj_postfix := if path.ends_with('.obj') {
126 path[..path.len - 4]
127 } else if path.ends_with('.o') {
128 path[..path.len - 2]
129 } else {
130 path
131 }
132 return os.real_path(if v.pref.is_debug {
133 // MSVC debug builds use /MDd, so they need their own thirdparty object files.
134 '${path_without_obj_postfix}.debug.obj'
135 } else {
136 '${path_without_obj_postfix}.obj'
137 })
138}
139
140fn (mut v Builder) msvc_thirdparty_obj_path(mod string, path string, cached_path string) string {
141 base_path := if cached_path != '' {
142 cached_path
143 } else {
144 // Reuse the cache-derived .o path so different targets/options do not share one .obj.
145 v.pref.cache_manager.mod_postfix_with_key2cpath(mod, '.o', os.real_path(path))
146 }
147 return v.msvc_object_path(base_path)
148}
149
150pub fn (mut b Builder) interpret_text(code string, v_files []string) ! {
151 b.parsed_files = parser.parse_files(v_files, mut b.table, b.pref)
152 b.parsed_files << parser.parse_text(code, '', mut b.table, .skip_comments, b.pref)
153 if b.should_stop_after_frontend_error() && b.has_frontend_errors() {
154 exit(1)
155 }
156 b.parse_imports()
157 b.check_unused_imports()
158 b.print_frontend_builder_errors()
159 if b.should_stop_after_frontend_error() && b.has_frontend_errors() {
160 exit(1)
161 }
162
163 if b.pref.only_check_syntax {
164 return error_with_code('stop_after_parser', 7001)
165 }
166
167 b.middle_stages()!
168}
169
170pub fn (mut b Builder) front_stages(v_files []string) ! {
171 mut timers := util.get_timers()
172 util.timing_start('ALL_FRONT_STAGES')
173 defer {
174 timers.show('ALL_FRONT_STAGES')
175 }
176 util.timing_start('PARSE')
177
178 util.timing_start('Builder.front_stages.parse_files')
179 b.parsed_files = parser.parse_files(v_files, mut b.table, b.pref)
180 timers.show('Builder.front_stages.parse_files')
181 if b.should_stop_after_frontend_error() && b.has_frontend_errors() {
182 exit(1)
183 }
184
185 b.parse_imports()
186 b.check_unused_imports()
187 b.print_frontend_builder_errors()
188 if b.should_stop_after_frontend_error() && b.has_frontend_errors() {
189 exit(1)
190 }
191
192 timers.show('SCAN')
193 timers.show('PARSE')
194 timers.show_if_exists('PARSE stmt')
195 if b.pref.only_check_syntax {
196 return error_with_code('stop_after_parser', 7001)
197 }
198}
199
200pub fn (mut b Builder) middle_stages() ! {
201 util.timing_start('CHECK')
202
203 util.timing_start('Checker.generic_insts_to_concrete')
204 b.table.generic_insts_to_concrete()
205 util.timing_measure('Checker.generic_insts_to_concrete')
206
207 b.checker.check_files(b.parsed_files)
208 util.timing_start('Checker.generic_insts_to_concrete.after_check')
209 b.table.generic_insts_to_concrete()
210 util.timing_measure('Checker.generic_insts_to_concrete.after_check')
211 util.timing_measure('CHECK')
212 $if trace_type_symbols_after_checker ? {
213 for t, s in b.table.type_symbols {
214 println('> t: ${t:10} | s.mod: ${s.mod:-40} | s.name: ${'${s.name#[..30]}':-30} | s.is_builtin: ${s.is_builtin:6} | s.is_pub: ${s.is_pub}')
215 }
216 }
217
218 if b.pref.new_generic_solver {
219 util.timing_start('GENERICS')
220 b.generics.solve_files(b.parsed_files)
221 util.timing_start('Checker.generic_insts_to_concrete.after_generics')
222 b.table.generic_insts_to_concrete()
223 util.timing_measure('Checker.generic_insts_to_concrete.after_generics')
224 util.timing_measure('GENERICS')
225 }
226
227 util.timing_start('COMPTIME')
228 b.comptime.solve_files(b.parsed_files)
229 util.timing_measure('COMPTIME')
230
231 if b.pref.dump_defines != '' {
232 b.dump_defines()
233 }
234 mut mcache := vmod.get_cache()
235 mcache.debug()
236 b.print_warnings_and_errors()
237 if b.checker.should_abort {
238 return error('too many errors/warnings/notices')
239 }
240 if b.checker.unresolved_fixed_sizes.len > 0 {
241 util.timing_start('Checker.update_unresolved_fixed_sizes')
242 b.checker.update_unresolved_fixed_sizes()
243 util.timing_measure('Checker.update_unresolved_fixed_sizes')
244 }
245 if b.pref.check_only {
246 return error_with_code('stop_after_checker', 8001)
247 }
248 util.timing_start('TRANSFORM')
249 b.transformer.transform_files(b.parsed_files)
250 util.timing_measure('TRANSFORM')
251
252 b.table.complete_interface_check()
253 if b.pref.skip_unused {
254 markused.mark_used(mut b.table, mut b.pref, b.parsed_files)
255 }
256 if b.pref.show_callgraph {
257 callgraph.show(mut b.table, b.pref, b.parsed_files)
258 }
259}
260
261pub fn (mut b Builder) front_and_middle_stages(v_files []string) ! {
262 b.front_stages(v_files)!
263 b.middle_stages()!
264}
265
266@[inline]
267fn (b &Builder) should_stop_after_frontend_error() bool {
268 return b.pref.fatal_errors
269 || (b.pref.output_mode == .stdout && !b.pref.check_only && !b.pref.is_vls)
270}
271
272@[inline]
273fn (b &Builder) has_frontend_errors() bool {
274 return b.parsed_files.any(it.errors.len > 0)
275}
276
277// parse all deps from already parsed files
278pub fn (mut b Builder) parse_imports() {
279 util.timing_start(@METHOD)
280 defer {
281 util.timing_measure(@METHOD)
282 }
283 mut done_imports := []string{}
284 mut done_import_path_modules := map[string][]string{}
285 mut done_import_path_files := map[string][]string{}
286 if b.pref.is_vsh {
287 done_imports << 'os'
288 }
289 // TODO: (joe): decide if this is correct solution.
290 // in the case of building a module, the actual module files
291 // are passed via cmd line, so they have already been parsed
292 // by this stage. note that if one files from a module was
293 // parsed (but not all of them), then this will cause a problem.
294 // we could add a list of parsed files instead, but I think
295 // there is a better solution all around, I will revisit this.
296 // NOTE: there is a very similar occurrence with the way
297 // internal module test's work, and this was the reason there
298 // were issues with duplicate declarations, so we should sort
299 // that out in a similar way.
300 for file in b.parsed_files {
301 if file.mod.name != 'main' && file.mod.name !in done_imports {
302 done_imports << file.mod.name
303 }
304 }
305 // Note: b.parsed_files is appended in the loop,
306 // so we can not use the shorter `for in` form.
307 for i := 0; i < b.parsed_files.len; i++ {
308 ast_file := b.parsed_files[i]
309 append_map_array(mut b.path_invalidates_mods, ast_file.path, ast_file.mod.name)
310 if ast_file.mod.name != 'builtin' {
311 append_map_array(mut b.mod_invalidates_paths, 'builtin', ast_file.path)
312 append_map_array(mut b.mod_invalidates_mods, 'builtin', ast_file.mod.name)
313 }
314 for imp in ast_file.imports {
315 mod := imp.mod
316 append_map_array(mut b.mod_invalidates_paths, mod, ast_file.path)
317 append_map_array(mut b.mod_invalidates_mods, mod, ast_file.mod.name)
318 if mod == 'builtin' {
319 b.parsed_files[i].errors << b.error_with_pos('cannot import module "builtin"',
320 ast_file.path, imp.pos)
321 break
322 }
323 if mod in done_imports {
324 continue
325 }
326 import_path := b.find_module_path(mod, ast_file.path) or {
327 // v.parsers[i].error_with_token_index('cannot import module "${mod}" (not found)', v.parsers[i].import_ast.get_import_tok_idx(mod))
328 // break
329 b.parsed_files[i].errors << b.error_with_pos('cannot import module "${mod}" (not found)',
330 ast_file.path, imp.pos)
331 break
332 }
333 import_path_key := comparable_real_path(import_path)
334 if import_path_key in done_import_path_modules {
335 for module_idx, name in done_import_path_modules[import_path_key] {
336 b.validate_imported_module_name(i, ast_file.path, mod,
337 done_import_path_files[import_path_key][module_idx], name, imp.pos)
338 }
339 done_imports << mod
340 continue
341 }
342 v_files := b.v_files_from_dir(import_path)
343 if v_files.len == 0 {
344 // v.parsers[i].error_with_token_index('cannot import module "${mod}" (no .v files in "${import_path}")', v.parsers[i].import_ast.get_import_tok_idx(mod))
345 b.parsed_files[i].errors << b.error_with_pos('cannot import module "${mod}" (no .v files in "${import_path}")',
346 ast_file.path, imp.pos)
347 continue
348 }
349 // eprintln('>> ast_file.path: ${ast_file.path} , done: ${done_imports}, `import ${mod}` => ${v_files}')
350 // Add all imports referenced by these libs
351 parsed_files := parser.parse_files(v_files, mut b.table, b.pref)
352 mut imported_module_names := []string{}
353 mut imported_module_paths := []string{}
354 for file in parsed_files {
355 name := imported_file_module_name(file)
356 b.validate_imported_module_name(i, ast_file.path, mod, file.path, name, imp.pos)
357 imported_module_names << name
358 imported_module_paths << file.path
359 }
360 b.parsed_files << parsed_files
361 if b.should_stop_after_frontend_error() && parsed_files.any(it.errors.len > 0) {
362 return
363 }
364 done_imports << mod
365 done_import_path_modules[import_path_key] = imported_module_names
366 done_import_path_files[import_path_key] = imported_module_paths
367 }
368 }
369 b.resolve_deps()
370 $if trace_parsed_files ? {
371 b.show_parsed_files()
372 }
373 if b.pref.print_v_files {
374 b.show_parsed_files()
375 exit(0)
376 }
377 if b.pref.print_watched_files {
378 for p in b.parsed_files {
379 if p.is_parse_text {
380 // a generated snippet, `v watch` does not care about those, since they are duplicates for other files
381 continue
382 }
383 println(p.path)
384 for tp in p.template_paths {
385 println(tp)
386 }
387 }
388 exit(0)
389 }
390 if b.pref.dump_files != '' {
391 b.dump_files(b.parsed_files.map(it.path))
392 }
393 b.rebuild_modules()
394}
395
396pub fn (mut b Builder) resolve_deps() {
397 util.timing_start(@METHOD)
398 defer {
399 util.timing_measure(@METHOD)
400 }
401 graph := b.import_graph()
402 deps_resolved := graph.resolve()
403 if b.pref.is_verbose {
404 eprintln('------ resolved dependencies graph: ------')
405 eprintln(deps_resolved.display())
406 eprintln('------------------------------------------')
407 }
408 if b.pref.show_depgraph {
409 depgraph.show(deps_resolved, b.pref.path)
410 }
411 cycles := deps_resolved.display_cycles()
412 if cycles.len > 1 {
413 verror('error: import cycle detected between the following modules: \n' + cycles)
414 }
415 mut mods := []string{}
416 for node in deps_resolved.nodes {
417 mods << node.name
418 }
419 b.dump_modules(mods)
420 if b.pref.is_verbose {
421 eprintln('------ imported modules: ------')
422 eprintln(mods.str())
423 eprintln('-------------------------------')
424 }
425 unsafe {
426 mut reordered_parsed_files := []&ast.File{}
427 for m in mods {
428 for pf in b.parsed_files {
429 if m == pf.mod.name {
430 reordered_parsed_files << pf
431 // eprintln('pf.mod.name: ${pf.mod.name} | pf.path: ${pf.path}')
432 }
433 }
434 }
435 b.table.modules = mods
436 b.parsed_files = reordered_parsed_files
437 }
438}
439
440fn imported_file_module_name(file ast.File) string {
441 if file.mod.name != '' {
442 return file.mod.name
443 }
444 return file.mod.short_name
445}
446
447fn (mut b Builder) validate_imported_module_name(importer_idx int, importer_path string, mod string, imported_path string, name string, import_pos token.Pos) {
448 sname := name.all_after_last('.')
449 smod := mod.all_after_last('.')
450 if sname != smod {
451 msg := 'bad module definition: ${importer_path} imports module "${mod}" but ${imported_path} is defined as module `${name}`'
452 b.parsed_files[importer_idx].errors << b.error_with_pos(msg, importer_path, import_pos)
453 }
454}
455
456fn import_alias_for_mod(file &ast.File, mod string) ?string {
457 for import_m in file.imports {
458 if import_m.mod == mod {
459 return import_m.alias
460 }
461 }
462 return none
463}
464
465fn register_used_import(mut file ast.File, alias string) {
466 if alias !in file.used_imports {
467 file.used_imports << alias
468 }
469}
470
471fn file_needs_c_function_call_import_scan(file &ast.File) bool {
472 for import_m in file.imports {
473 alias := import_m.alias
474 if (alias.len == 1 && alias[0] == `_`) || alias in file.used_imports
475 || alias in file.auto_imports {
476 continue
477 }
478 return true
479 }
480 return false
481}
482
483fn (mut b Builder) collect_used_c_function_calls(file &ast.File) map[string]bool {
484 mut collector := CFunctionCallCollector{
485 names: map[string]bool{}
486 }
487 walker.inspect(file, &collector, c_function_call_collector_visit)
488 return collector.names
489}
490
491fn (mut b Builder) mark_imports_used_by_c_function_calls() {
492 for mut file in b.parsed_files {
493 if !file_needs_c_function_call_import_scan(file) {
494 continue
495 }
496 used_c_calls := b.collect_used_c_function_calls(file)
497 if used_c_calls.len == 0 {
498 continue
499 }
500 for c_name, _ in used_c_calls {
501 if c_fn := b.table.find_fn(c_name) {
502 alias := import_alias_for_mod(file, c_fn.mod) or { continue }
503 register_used_import(mut file, alias)
504 continue
505 }
506 c_typ := b.table.find_type(c_name)
507 if c_typ == 0 {
508 continue
509 }
510 c_sym := b.table.sym(c_typ)
511 if c_sym.kind == .placeholder {
512 continue
513 }
514 alias := import_alias_for_mod(file, c_sym.mod) or { continue }
515 register_used_import(mut file, alias)
516 }
517 }
518}
519
520fn (mut b Builder) add_unused_import_message(mut file ast.File, message string, pos token.Pos) {
521 file_path := if pos.file_idx < 0 { file.path } else { b.table.filelist[pos.file_idx] }
522 if b.pref.warns_are_errors {
523 err := errors.Error{
524 file_path: file_path
525 pos: pos
526 reporter: .parser
527 message: message
528 }
529 file.errors << err
530 if b.pref.output_mode == .stdout && !b.pref.check_only {
531 util.show_compiler_message('error:', err.CompilerMessage)
532 }
533 return
534 }
535 if b.pref.skip_warnings {
536 return
537 }
538 wrn := errors.Warning{
539 file_path: file_path
540 pos: pos
541 reporter: .parser
542 message: message
543 }
544 file.warnings << wrn
545 if b.pref.output_mode == .stdout && !b.pref.check_only {
546 util.show_compiler_message('warning:', wrn.CompilerMessage)
547 }
548}
549
550fn (mut b Builder) check_unused_imports() {
551 if b.pref.is_repl || b.pref.is_fmt {
552 return
553 }
554 b.mark_imports_used_by_c_function_calls()
555 for mut file in b.parsed_files {
556 for import_m in file.imports {
557 alias := import_m.alias
558 mod := import_m.mod
559 if (alias.len == 1 && alias[0] == `_`) || alias in file.used_imports
560 || alias in file.auto_imports {
561 continue
562 }
563 mod_alias := if alias == mod { alias } else { '${alias} (${mod})' }
564 b.add_unused_import_message(mut file,
565 "module '${mod_alias}' is imported but never used. Use `import ${mod_alias} as _`, to silence this warning, or just remove the unused import line",
566 import_m.mod_pos)
567 }
568 }
569}
570
571// graph of all imported modules
572pub fn (b &Builder) import_graph() &depgraph.DepGraph {
573 builtins := util.builtin_module_parts.clone()
574 mut graph := depgraph.new_dep_graph()
575 for p in b.parsed_files {
576 // eprintln('p.path: ${p.path}')
577 mut deps := []string{}
578 if p.mod.name !in builtins {
579 deps << 'builtin'
580 if b.pref.backend == .c {
581 // TODO: JavaScript backend doesn't handle os for now
582 // os import libraries so we exclude anything which could cause a loop
583 if p.path.ends_with('.vsh') {
584 deps << 'os'
585 }
586 }
587 }
588 for m in p.imports {
589 if m.mod == p.mod.name {
590 continue
591 }
592 deps << m.mod
593 }
594 graph.add(p.mod.name, deps)
595 }
596 $if trace_import_graph ? {
597 eprintln(graph.display())
598 }
599 return graph
600}
601
602pub fn (b &Builder) v_files_from_dir(dir string) []string {
603 if !os.exists(dir) {
604 if dir == 'compiler' && os.is_dir('vlib') {
605 println('looks like you are trying to build V with an old command')
606 println('use `v -o v cmd/v` instead of `v -o v compiler`')
607 }
608 verror("${dir} doesn't exist")
609 } else if !os.is_dir(dir) {
610 verror("${dir} isn't a directory!")
611 }
612 mut files := os.ls(dir) or { panic(err) }
613 mut source_dir := os.real_path(dir)
614 if b.pref.is_verbose {
615 println('v_files_from_dir ("${dir}")')
616 }
617 mut res := b.pref.should_compile_filtered_files(dir, files)
618 if res.len == 0 {
619 // An explicit `base_url` in v.mod still re-homes the source folder.
620 if source_root := source_root_from_vmod_root(dir) {
621 if source_root != dir && os.is_dir(source_root) {
622 if b.pref.is_verbose {
623 println('v_files_from_dir ("${source_root}") (v.mod base_url)')
624 }
625 files = os.ls(source_root) or { panic(err) }
626 source_dir = os.real_path(source_root)
627 res = b.pref.should_compile_filtered_files(source_root, files)
628 }
629 }
630 if res.len == 0 {
631 report_removed_src_layout_if_any(dir)
632 }
633 }
634 return b.with_same_module_subdir_files(source_dir, res)
635}
636
637// report_removed_src_layout_if_any prints a clear error if `dir` still relies on
638// the removed virtual `src/` module layout (sources under `dir/src/` instead of
639// at the module root). The explicit `base_url` opt-in in v.mod is unaffected.
640fn report_removed_src_layout_if_any(dir string) {
641 src_dir := os.join_path(dir, 'src')
642 if !os.is_dir(src_dir) {
643 return
644 }
645 src_files := os.ls(src_dir) or { return }
646 mut has_v_files := false
647 for f in src_files {
648 if f.ends_with('.v') {
649 has_v_files = true
650 break
651 }
652 }
653 if !has_v_files {
654 return
655 }
656 verror('the virtual `src/` module directory is no longer supported.\nV found .v source files under ${src_dir}, but will not treat `src/` as a virtual module root anymore.\nPlease move the sources up from `src/` into ${dir}:\n\tmv ${src_dir}/*.v ${dir}/\n\trmdir ${src_dir}\n\nIf you want to split one module across subdirectories after moving the root files, add `subdirs` to v.mod, for example:\n\tsubdirs: [\'admin\', \'repo\', \'commit\', \'ci\', \'security\', \'ssh\', \'user\']')
657}
658
659fn (b &Builder) with_same_module_subdir_files(source_dir string, v_files []string) []string {
660 mut mcache := vmod.get_cache()
661 vmod_file_location := mcache.get_by_folder(source_dir)
662 if vmod_file_location.vmod_file == '' {
663 return v_files
664 }
665 module_source_root := b.module_source_root(vmod_file_location.vmod_folder)
666 if source_dir != module_source_root {
667 return v_files
668 }
669 manifest := vmod.from_file(vmod_file_location.vmod_file) or { return v_files }
670 subdirs := manifest.unknown['subdirs'] or { return v_files }
671 if subdirs.len == 0 {
672 return v_files
673 }
674 mut res := v_files.clone()
675 mut seen := map[string]bool{}
676 for file in res {
677 seen[os.real_path(file)] = true
678 }
679 for subdir in subdirs {
680 normalized_subdir := normalize_same_module_subdir(subdir, manifest.base_url) or { continue }
681 subdir_path := os.join_path(module_source_root, normalized_subdir)
682 b.collect_same_module_v_files(vmod_file_location.vmod_folder, subdir_path, mut seen, mut
683 res)
684 }
685 return res
686}
687
688fn (b &Builder) module_source_root(module_root string) string {
689 real_module_root := os.real_path(module_root)
690 if source_root := source_root_from_vmod_root(real_module_root) {
691 if source_root != real_module_root && os.is_dir(source_root) {
692 return os.real_path(source_root)
693 }
694 }
695 return real_module_root
696}
697
698fn normalize_same_module_subdir(subdir string, base_url string) !string {
699 mut normalized := os.norm_path(subdir.trim_space().replace('\\', os.path_separator).replace('/',
700 os.path_separator))
701 if normalized == '' || normalized == '.' || os.is_abs_path(normalized) {
702 return error('invalid subdir')
703 }
704 if base_url != '' {
705 base := os.norm_path(base_url).trim_left(os.path_separator).trim_right(os.path_separator)
706 if base != '' {
707 base_prefix := base + os.path_separator
708 if normalized == base {
709 return error('invalid subdir')
710 }
711 if normalized.starts_with(base_prefix) {
712 normalized = normalized.all_after(base_prefix)
713 }
714 }
715 }
716 normalized = normalized.trim_left(os.path_separator).trim_right(os.path_separator)
717 if normalized == '' {
718 return error('invalid subdir')
719 }
720 parts := normalized.split(os.path_separator)
721 if '' in parts || '.' in parts || '..' in parts {
722 return error('invalid subdir')
723 }
724 return normalized
725}
726
727fn (b &Builder) collect_same_module_v_files(module_root string, dir string, mut seen map[string]bool, mut res []string) {
728 if !os.is_dir(dir) {
729 return
730 }
731 real_dir := os.real_path(dir)
732 if real_dir != os.real_path(module_root) && os.is_file(os.join_path(real_dir, 'v.mod')) {
733 return
734 }
735 mut entries := os.ls(real_dir) or { return }
736 entries.sort()
737 for file in b.pref.should_compile_filtered_files(real_dir, entries) {
738 real_file := os.real_path(file)
739 if real_file !in seen {
740 seen[real_file] = true
741 res << file
742 }
743 }
744 for entry in entries {
745 subdir_path := os.join_path(real_dir, entry)
746 if os.is_dir(subdir_path) {
747 b.collect_same_module_v_files(module_root, subdir_path, mut seen, mut res)
748 }
749 }
750}
751
752pub fn (b &Builder) log(s string) {
753 if b.pref.is_verbose {
754 println(s)
755 }
756}
757
758pub fn (b &Builder) info(s string) {
759 if b.pref.is_verbose {
760 println(s)
761 }
762}
763
764@[inline]
765pub fn module_path(mod string) string {
766 // submodule support
767 return mod.replace('.', os.path_separator)
768}
769
770fn manifest_from_vmod_root(vmod_root string) !vmod.Manifest {
771 vmod_path := os.join_path(vmod_root, 'v.mod')
772 if !os.is_file(vmod_path) {
773 return error('module not found')
774 }
775 return vmod.from_file(vmod_path) or { return error('module not found') }
776}
777
778fn source_root_from_vmod_root(vmod_root string) !string {
779 manifest := manifest_from_vmod_root(vmod_root)!
780 if manifest.base_url == '' {
781 return error('module not found')
782 }
783 return manifest.source_root(vmod_root)
784}
785
786fn find_module_path_from_vmod_root(vmod_root string, mod string) !string {
787 manifest := manifest_from_vmod_root(vmod_root)!
788 if manifest.base_url == '' {
789 return error('module not found')
790 }
791 tail_path := mod_tail_after_vmod_name(mod, manifest.name) or {
792 return error('module not found')
793 }
794 if tail_path == '' {
795 return error('module not found')
796 }
797 try_path := os.join_path(manifest.source_root(vmod_root), tail_path)
798 if os.is_dir(try_path) {
799 return try_path
800 }
801 return error('module not found')
802}
803
804fn find_module_path_from_search_root(search_path string, mod string) !string {
805 mod_path := module_path(mod)
806 try_path := os.join_path_single(search_path, mod_path)
807 if os.is_dir(try_path) {
808 return try_path
809 }
810 if src_try_path := find_module_path_from_vmod_root(search_path, mod) {
811 return src_try_path
812 }
813 mod_parts := mod.split('.')
814 for i := mod_parts.len - 1; i > 0; i-- {
815 candidate_root := os.join_path_single(search_path, mod_parts[..i].join(os.path_separator))
816 if !os.is_file(os.join_path(candidate_root, 'v.mod')) {
817 continue
818 }
819 source_root := source_root_from_vmod_root(candidate_root) or { continue }
820 submodule_path := mod_parts[i..].join(os.path_separator)
821 src_try_path := os.join_path(source_root, submodule_path)
822 if os.is_dir(src_try_path) {
823 return src_try_path
824 }
825 }
826 return error('module not found')
827}
828
829fn mod_tail_after_vmod_name(mod string, vmod_name string) !string {
830 if vmod_name == '' {
831 return error('module not found')
832 }
833 mod_parts := mod.split('.')
834 vmod_parts := vmod_name.split('.')
835 for i := 0; i + vmod_parts.len <= mod_parts.len; i++ {
836 if i > 1 {
837 break
838 }
839 if mod_parts[i..i + vmod_parts.len].join('.') == vmod_name {
840 return mod_parts[i + vmod_parts.len..].join(os.path_separator)
841 }
842 }
843 return error('module not found')
844}
845
846fn (b &Builder) module_path_has_v_files(path string) bool {
847 return b.v_files_from_dir(path).len > 0
848}
849
850// candidate_belongs_to_foreign_project returns true when `candidate_path` is
851// neither inside the importer's `v.mod` tree nor inside a configured
852// `lookup_path` entry (vlib / vmodules). Such candidates are someone else's
853// source tree: e.g. when a `vlib/*` file imports `sync`, a sibling
854// `coreutils/src/sync` directory is not a real `sync` module and should be
855// skipped (see #27151). Comparing by path containment (rather than by the
856// nearest enclosing `v.mod`) keeps vendored dependencies like
857// `<project>/modules/<name>/` — which carry their own `v.mod` — resolvable
858// from `<project>`'s own code.
859fn (b &Builder) candidate_belongs_to_foreign_project(candidate_path string, importer_vmod_folder string, mod string) bool {
860 if importer_vmod_folder == '' {
861 return false
862 }
863 abs_candidate := comparable_real_path(candidate_path)
864 abs_importer_vmod := comparable_real_path(importer_vmod_folder)
865 if path_is_at_or_inside(abs_candidate, abs_importer_vmod) {
866 return false
867 }
868 if candidate_vmod_matches_import(abs_candidate, mod) {
869 return false
870 }
871 // A module installed as a symlink inside a lookup path (e.g.
872 // `.vmodules/einar_hjortdal/luuid -> /real/luuid`) resolves via
873 // os.real_path to a location outside the lookup path. Compare the
874 // logical (unresolved) path too, so such modules are still recognized
875 // as belonging to the project (see #27391).
876 candidate_lookup_path := comparable_path(candidate_path)
877 for lookup in b.pref.lookup_path {
878 abs_lookup := comparable_real_path(lookup)
879 if path_is_at_or_inside(abs_candidate, abs_lookup) {
880 return false
881 }
882 lookup_path := comparable_path(lookup)
883 if path_is_at_or_inside(candidate_lookup_path, lookup_path) {
884 return false
885 }
886 }
887 return true
888}
889
890fn (b &Builder) path_belongs_to_lookup_path(path string) bool {
891 abs_path := comparable_real_path(path)
892 for lookup in b.pref.lookup_path {
893 abs_lookup := comparable_real_path(lookup)
894 if path_is_at_or_inside(abs_path, abs_lookup) {
895 return true
896 }
897 }
898 return false
899}
900
901// comparable_real_path normalizes a path for comparison, resolving symlinks
902// via os.real_path. Use when both sides of a comparison should refer to the
903// same physical location on disk.
904fn comparable_real_path(path string) string {
905 return comparable_path_from(os.real_path(path))
906}
907
908// comparable_path normalizes a path for comparison without resolving symlinks.
909// Use when the original (logical) path matters, e.g. for symlinked modules
910// inside `.vmodules` that should match their lookup path as-is.
911fn comparable_path(path string) string {
912 return comparable_path_from(os.abs_path(path))
913}
914
915// comparable_path_from normalizes a path string for consistent comparison:
916// converts backslashes to forward slashes, collapses duplicate separators,
917// and strips a trailing slash.
918fn comparable_path_from(path string) string {
919 mut normalized := path.replace('\\', '/')
920 for normalized.contains('//') {
921 normalized = normalized.replace('//', '/')
922 }
923 if normalized.len > 1 {
924 normalized = normalized.trim_right('/')
925 }
926 return normalized
927}
928
929fn path_is_at_or_inside(candidate string, root string) bool {
930 if root == '' {
931 return false
932 }
933 return candidate == root || candidate.starts_with(root + '/')
934}
935
936fn candidate_vmod_matches_import(candidate_path string, mod string) bool {
937 mut mcache := vmod.get_cache()
938 vmod_file_location := mcache.get_by_folder(candidate_path)
939 if vmod_file_location.vmod_file == '' {
940 return false
941 }
942 manifest := vmod.from_file(vmod_file_location.vmod_file) or { return false }
943 return manifest.name == mod || mod.starts_with(manifest.name + '.')
944}
945
946// TODO: try to merge this & util.module functions to create a
947// reliable multi use function. see comments in util/module.v
948pub fn (b &Builder) find_module_path(mod string, fpath string) !string {
949 // support @VEXEROOT/v.mod relative paths:
950 mut mcache := vmod.get_cache()
951 resolved_fpath := os.real_path(fpath)
952 vmod_file_location := mcache.get_by_file(resolved_fpath)
953 // Anchor the importer to the OUTERMOST enclosing `v.mod`, not the nearest
954 // one. A file at `<project>/modules/<name>/file.v` carries the vendored
955 // package's `v.mod` as its nearest root, but logically belongs to
956 // `<project>` — and must be able to see sibling vendored deps under
957 // `<project>/modules/`.
958 mut importer_vmod_folder := ''
959 if vmod_file_location.vmod_file.len != 0 {
960 importer_vmod_folder = vmod_file_location.vmod_folder
961 for {
962 parent := os.dir(importer_vmod_folder)
963 if parent == importer_vmod_folder {
964 break
965 }
966 parent_loc := mcache.get_by_folder(parent)
967 if parent_loc.vmod_file == '' {
968 break
969 }
970 // On Windows, os.dir('C:\project') returns 'C:' (no trailing slash),
971 // and os.real_path('C:') resolves to the drive's current directory —
972 // which may equal importer_vmod_folder, producing an infinite loop.
973 // Stop climbing whenever the candidate is not a strict path ancestor.
974 if !is_strict_ancestor(parent_loc.vmod_folder, importer_vmod_folder) {
975 break
976 }
977 importer_vmod_folder = parent_loc.vmod_folder
978 }
979 }
980 mod_path := module_path(mod)
981 mut module_lookup_paths := []string{}
982 if b.path_belongs_to_lookup_path(resolved_fpath) {
983 module_lookup_paths << b.pref.lookup_path
984 }
985 if vmod_file_location.vmod_file.len != 0
986 && vmod_file_location.vmod_folder !in b.module_search_paths {
987 module_lookup_paths << vmod_file_location.vmod_folder
988 }
989 module_lookup_paths << b.module_search_paths
990 // go up through parents looking for modules a folder.
991 // we need a proper solution that works most of the time. look at vdoc.get_parent_mod
992 if resolved_fpath.contains(os.path_separator + 'modules' + os.path_separator) {
993 parts := resolved_fpath.split(os.path_separator)
994 for i := parts.len - 2; i >= 0; i-- {
995 if parts[i] == 'modules' {
996 module_lookup_paths << parts[0..i + 1].join(os.path_separator)
997 break
998 }
999 }
1000 }
1001 mut empty_module_path := ''
1002 for search_path in module_lookup_paths {
1003 try_path := os.join_path(search_path, mod_path)
1004 if b.pref.is_verbose {
1005 println(' >> trying to find ${mod} in ${try_path} ..')
1006 }
1007 if found_path := find_module_path_from_search_root(search_path, mod) {
1008 if b.candidate_belongs_to_foreign_project(found_path, importer_vmod_folder, mod) {
1009 if b.pref.is_verbose {
1010 println(' << skipped ${found_path} (belongs to a different v.mod project) .')
1011 }
1012 continue
1013 }
1014 if b.module_path_has_v_files(found_path) {
1015 if b.pref.is_verbose {
1016 println(' << found ${found_path} .')
1017 }
1018 return found_path
1019 }
1020 if empty_module_path == '' {
1021 empty_module_path = found_path
1022 }
1023 if b.pref.is_verbose {
1024 println(' << skipped ${found_path} (no .v files) .')
1025 }
1026 }
1027 }
1028 // look up through parents
1029 mut current_dir := os.dir(resolved_fpath)
1030 for {
1031 try_path := os.join_path(current_dir, mod_path)
1032 if b.pref.is_verbose {
1033 println(' >> trying to find ${mod} in ${try_path} ..')
1034 }
1035 if found_path := find_module_path_from_search_root(current_dir, mod) {
1036 if b.candidate_belongs_to_foreign_project(found_path, importer_vmod_folder, mod) {
1037 if b.pref.is_verbose {
1038 println(' << skipped ${found_path} (belongs to a different v.mod project) .')
1039 }
1040 } else if b.module_path_has_v_files(found_path) {
1041 if b.pref.is_verbose {
1042 println(' << found ${found_path} .')
1043 }
1044 return found_path
1045 } else {
1046 if empty_module_path == '' {
1047 empty_module_path = found_path
1048 }
1049 if b.pref.is_verbose {
1050 println(' << skipped ${found_path} (no .v files) .')
1051 }
1052 }
1053 }
1054 parent_dir := os.dir(current_dir)
1055 if parent_dir == current_dir {
1056 break
1057 }
1058 current_dir = parent_dir
1059 }
1060 if empty_module_path != '' {
1061 return empty_module_path
1062 }
1063 smodule_lookup_paths := module_lookup_paths.join(', ')
1064 return error('module "${mod}" not found in:\n${smodule_lookup_paths}')
1065}
1066
1067pub fn (b &Builder) show_total_warns_and_errors_stats() {
1068 if b.nr_errors == 0 && b.nr_warnings == 0 && b.nr_notices == 0 {
1069 return
1070 }
1071 if b.pref.is_stats {
1072 mut nr_errors := b.checker.nr_errors
1073 mut nr_warnings := b.checker.nr_warnings
1074 mut nr_notices := b.checker.nr_notices
1075
1076 if b.pref.check_only {
1077 nr_errors = b.nr_errors
1078 nr_warnings = b.nr_warnings
1079 nr_notices = b.nr_notices
1080 }
1081
1082 estring := util.bold(nr_errors.str())
1083 wstring := util.bold(nr_warnings.str())
1084 nstring := util.bold(nr_notices.str())
1085
1086 if b.pref.check_only {
1087 println('summary: ${estring} V errors, ${wstring} V warnings, ${nstring} V notices')
1088 } else {
1089 println('checker summary: ${estring} V errors, ${wstring} V warnings, ${nstring} V notices')
1090 }
1091 }
1092 if !b.pref.is_vls && b.checker.nr_errors > 0 && b.pref.path.ends_with('.v')
1093 && os.is_file(b.pref.path) && !b.pref.path.ends_with('vrepl_temp.v') {
1094 if b.checker.errors.any(it.message.starts_with('unknown ')) {
1095 // Sometimes users try to `v main.v`, when they have several .v files in their project.
1096 // Then, they encounter puzzling errors about missing or unknown types. In this case,
1097 // the intended command may have been `v .` instead, so just suggest that:
1098 old_cmd := util.bold('v ${b.pref.path}')
1099 new_cmd := util.bold('v ${os.dir(b.pref.path)}')
1100 eprintln(util.color('notice',
1101 'If the code of your project is in a folder with multiple .v files, try `${new_cmd}` instead of `${old_cmd}`'))
1102 }
1103 }
1104}
1105
1106pub fn (mut b Builder) print_warnings_and_errors() {
1107 defer {
1108 b.show_total_warns_and_errors_stats()
1109 }
1110
1111 for file in b.parsed_files {
1112 b.nr_errors += file.errors.len
1113 b.nr_warnings += file.warnings.len
1114 b.nr_notices += file.notices.len
1115 }
1116
1117 if b.pref.output_mode == .silent {
1118 if b.nr_errors > 0 {
1119 exit(1)
1120 }
1121 return
1122 }
1123
1124 if b.pref.check_only {
1125 if !b.pref.skip_notes && !b.pref.json_errors {
1126 for file in b.parsed_files {
1127 for err in file.notices {
1128 kind := if b.pref.is_verbose {
1129 '${err.reporter} notice #${b.nr_notices}:'
1130 } else {
1131 'notice:'
1132 }
1133 util.show_compiler_message(kind, err.CompilerMessage)
1134 }
1135 }
1136 }
1137
1138 mut json_errors := []util.JsonError{}
1139 for file in b.parsed_files {
1140 for err in file.errors {
1141 kind := if b.pref.is_verbose {
1142 '${err.reporter} error #${b.nr_errors}:'
1143 } else {
1144 'error:'
1145 }
1146
1147 if b.pref.json_errors {
1148 json_errors << util.JsonError{
1149 message: err.message
1150 path: os.to_slash(err.file_path)
1151 line_nr: err.pos.line_nr + 1
1152 col: err.pos.col + 1
1153 }
1154 // util.print_json_error(kind, err.CompilerMessage)
1155 } else {
1156 util.show_compiler_message(kind, err.CompilerMessage)
1157 }
1158 }
1159 }
1160 if b.pref.json_errors {
1161 if !b.pref.is_vls
1162 || b.pref.linfo.method !in [.definition, .completion, .signature_help, .hover] {
1163 util.print_json_errors(json_errors)
1164 }
1165 // eprintln(json2.encode_pretty(json_errors))
1166 }
1167 if !b.pref.skip_warnings {
1168 for file in b.parsed_files {
1169 for err in file.warnings {
1170 kind := if b.pref.is_verbose {
1171 '${err.reporter} warning #${b.nr_warnings}:'
1172 } else {
1173 'warning:'
1174 }
1175 util.show_compiler_message(kind, err.CompilerMessage)
1176 }
1177 }
1178 }
1179
1180 b.show_total_warns_and_errors_stats()
1181 if b.nr_errors > 0 {
1182 exit(1)
1183 }
1184 }
1185
1186 if b.pref.is_verbose && b.checker.nr_warnings > 1 {
1187 println('${b.checker.nr_warnings} warnings')
1188 }
1189 if b.pref.is_verbose && b.checker.nr_notices > 1 {
1190 println('${b.checker.nr_notices} notices')
1191 }
1192 if b.checker.nr_notices > 0 && !b.pref.skip_notes {
1193 for err in b.checker.notices {
1194 kind := if b.pref.is_verbose {
1195 '${err.reporter} notice #${b.checker.nr_notices}:'
1196 } else {
1197 'notice:'
1198 }
1199 util.show_compiler_message(kind, err.CompilerMessage)
1200 }
1201 }
1202 if b.checker.nr_warnings > 0 && !b.pref.skip_warnings {
1203 for err in b.checker.warnings {
1204 kind := if b.pref.is_verbose {
1205 '${err.reporter} warning #${b.checker.nr_warnings}:'
1206 } else {
1207 'warning:'
1208 }
1209 util.show_compiler_message(kind, err.CompilerMessage)
1210 }
1211 }
1212
1213 if b.pref.is_verbose && b.checker.nr_errors > 1 {
1214 println('${b.checker.nr_errors} errors')
1215 }
1216 if b.checker.nr_errors > 0 {
1217 for err in b.checker.errors {
1218 kind := if b.pref.is_verbose {
1219 '${err.reporter} error #${b.checker.nr_errors}:'
1220 } else {
1221 'error:'
1222 }
1223 util.show_compiler_message(kind, err.CompilerMessage)
1224 }
1225 b.show_total_warns_and_errors_stats()
1226 exit(1)
1227 }
1228 if b.table.redefined_fns.len > 0 {
1229 mut total_conflicts := 0
1230 for fn_name in b.table.redefined_fns {
1231 // Find where this function was already declared
1232 mut redefines := []FunctionRedefinition{}
1233 mut redefine_conflicts := map[string]int{}
1234 for file in b.parsed_files {
1235 for stmt in file.stmts {
1236 if stmt is ast.FnDecl {
1237 if stmt.name == fn_name {
1238 fheader := b.table.stringify_fn_decl(&stmt, 'main',
1239 map[string]string{}, false)
1240 redefines << FunctionRedefinition{
1241 fpath: file.path
1242 fline: stmt.pos.line_nr
1243 f: stmt
1244 fheader: fheader
1245 }
1246 redefine_conflicts[fheader]++
1247 }
1248 }
1249 }
1250 }
1251 if redefines.len > 0 {
1252 util.show_compiler_message('builder error:',
1253 message: 'redefinition of function `${fn_name}`'
1254 )
1255 for redefine in redefines {
1256 util.show_compiler_message('conflicting declaration:',
1257 message: redefine.fheader
1258 file_path: redefine.fpath
1259 pos: redefine.f.pos
1260 )
1261 }
1262 total_conflicts++
1263 }
1264 }
1265 if total_conflicts > 0 {
1266 b.show_total_warns_and_errors_stats()
1267 exit(1)
1268 }
1269 }
1270}
1271
1272struct FunctionRedefinition {
1273 fpath string
1274 fline int
1275 fheader string
1276 f ast.FnDecl
1277}
1278
1279pub fn (b &Builder) error_with_pos(s string, fpath string, pos token.Pos) errors.Error {
1280 return errors.Error{
1281 file_path: fpath
1282 pos: pos
1283 reporter: .builder
1284 message: s
1285 }
1286}
1287
1288fn (b &Builder) print_frontend_builder_errors() {
1289 if b.pref.check_only || b.pref.output_mode != .stdout {
1290 return
1291 }
1292 for file in b.parsed_files {
1293 for err in file.errors {
1294 if err.reporter == .builder {
1295 util.show_compiler_message('builder error:', err.CompilerMessage)
1296 }
1297 }
1298 }
1299}
1300
1301@[noreturn]
1302pub fn verror(s string) {
1303 util.verror('builder error', s)
1304}
1305
1306pub fn (mut b Builder) show_parsed_files() {
1307 for p in b.parsed_files {
1308 if p.is_parse_text {
1309 println(p.path + ':parse_text')
1310 }
1311 println(p.path)
1312 }
1313}
1314
1315fn is_strict_ancestor(ancestor string, descendant string) bool {
1316 if ancestor == '' || descendant == '' {
1317 return false
1318 }
1319 a := comparable_real_path(ancestor)
1320 d := comparable_real_path(descendant)
1321 return a != d && path_is_at_or_inside(d, a)
1322}
1323