v / vlib / v2 / builder / builder.v
4008 lines · 3785 sloc · 132.58 KB
Raw
1// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module builder
5
6import os
7import v2.ast
8import v2.abi
9import v2.eval
10import v2.gen.arm64
11import v2.gen.c
12import v2.gen.cleanc
13import v2.gen.v as gen_v
14import v2.gen.x64
15import v2.insel
16import v2.markused
17import v2.parser
18import v2.mir
19import v2.pref
20import v2.ssa
21import v2.ssa.optimize as ssa_optimize
22import v2.token
23import v2.transformer
24import v2.types
25import time
26import runtime
27
28const staged_c_file = '/tmp/v2_codegen.tmp.c'
29const staged_main_obj_file = '/tmp/v2_codegen.tmp.main.o'
30
31struct Builder {
32 pref &pref.Preferences
33mut:
34 files []ast.File
35 user_files []string // original user-provided files (for output name)
36 file_set &token.FileSet = token.FileSet.new()
37 env &types.Environment = unsafe { nil } // Type checker environment
38 parsed_full_files_n int
39 parsed_vh_files_n int
40 entry_v_lines_n int
41 parsed_v_lines_n int
42 parsed_full_files []string
43 parsed_vh_files []string
44 used_fn_keys map[string]bool
45 cached_called_fn_names map[string]bool
46 v2compiler_generic_setup_snapshot cleanc.GenericSetupSnapshot
47 has_v2compiler_generic_setup_snapshot bool
48 used_vh_for_parse bool
49 used_import_vh_for_parse bool
50 used_virtual_vh_for_parse bool
51 // flat is the canonical parse output. parse_batch streams directly into
52 // flat_builder so b.flat is built incrementally during parsing rather than
53 // via a redundant flatten_files() pass afterwards.
54 flat ast.FlatAst
55 flat_builder ast.FlatBuilder
56 // flat_builder_inited tracks whether flat_builder has been seeded with
57 // pre-sized arenas. We can only size after we know the input set, so
58 // the first parse_batch call lazily initializes it.
59 flat_builder_inited bool
60 // native_flat_pipeline_enabled means the transform phase produced a
61 // post-transform FlatAst and intentionally did not materialize b.files.
62 native_flat_pipeline_enabled bool
63 // Source snapshot used only for an isolated macOS tiny candidate graph.
64 // The normal hosted graph is still built separately and remains the fallback.
65 macos_tiny_candidate_source_files []ast.File
66 macos_tiny_candidate_source_flat ast.FlatAst
67}
68
69pub fn new_builder(prefs &pref.Preferences) &Builder {
70 unsafe {
71 return &Builder{
72 pref: prefs
73 used_fn_keys: map[string]bool{}
74 cached_called_fn_names: map[string]bool{}
75 }
76 }
77}
78
79// exec_build_c_file returns the staging C file path for executable builds.
80// A stable temp path avoids output-name-derived string construction in
81// self-hosted arm64 binaries, where that path is currently unstable.
82fn (b &Builder) exec_build_c_file(output_name string) string {
83 if b.pref.keep_c {
84 return output_name + '.c'
85 }
86 return staged_c_file
87}
88
89fn (b &Builder) should_use_native_flat_pipeline() bool {
90 if os.getenv('V2_NATIVE_FLAT') == '' {
91 return false
92 }
93 if os.getenv('V2_NO_NATIVE_FLAT') != '' {
94 return false
95 }
96 return b.pref.backend == .arm64 && b.pref.hot_fn.len == 0
97}
98
99fn (b &Builder) backend_uses_markused_pruning() bool {
100 return b.pref.backend != .arm64
101}
102
103// should_skip_markused_for_self_build avoids a self-host-only pruning pass that
104// costs more than it saves once the v2 compiler object caches are hot.
105fn (b &Builder) should_skip_markused_for_self_build() bool {
106 return b.pref.backend == .cleanc && b.is_cmd_v2_self_build()
107}
108
109fn (b &Builder) should_build_ssa_from_flat() bool {
110 return b.flat.files.len > 0
111}
112
113fn (b &Builder) should_keep_flat_for_codegen() bool {
114 return match b.pref.backend {
115 .cleanc, .c, .x64, .arm64 { true }
116 else { false }
117 }
118}
119
120fn (b &Builder) can_compile_cleanc_locally() bool {
121 if b.pref == unsafe { nil } {
122 return true
123 }
124 return b.pref.can_compile_cleanc_locally()
125}
126
127fn (b &Builder) cflags_target_os_for_local_compile() string {
128 if b.pref == unsafe { nil } {
129 return normalize_target_os_name(os.user_os())
130 }
131 if b.pref.is_cross_target() && b.can_compile_cleanc_locally() {
132 return b.pref.source_filter_target_os()
133 }
134 return b.pref.target_os_or_host()
135}
136
137fn cleanc_c_output_name(output_name string) string {
138 if output_name.ends_with('.c') {
139 return output_name
140 }
141 return output_name + '.c'
142}
143
144fn (mut b Builder) compile_cleanc_executable(output_name string, cc string, cc_flags string, cc_link_flags string, error_limit_flag string, mut sw time.StopWatch) {
145 cc_start := sw.elapsed()
146 if b.pref.is_shared_lib {
147 // Shared library: compile with -shared -fPIC -undefined dynamic_lookup
148 // Use -fvisibility=hidden so only explicitly exported (impl_live_*) symbols
149 // are visible. All other functions become hidden, causing the dylib to
150 // resolve them from the host executable at load time.
151 mut cc_cmd := '${cc} ${cc_flags} -shared -fPIC -fvisibility=hidden -undefined dynamic_lookup -w -Wno-incompatible-function-pointer-types "${staged_c_file}"'
152 if cc_link_flags.len > 0 {
153 cc_cmd += ' -x none ${cc_link_flags}'
154 }
155 cc_cmd += ' -o "${output_name}"${error_limit_flag}'
156 run_cc_cmd_or_exit(cc_cmd, 'shared lib compilation', b.pref.show_cc)
157 print_time('CC (shared)', time.Duration(sw.elapsed() - cc_start))
158 println('[*] Compiled shared library ${output_name}')
159 return
160 }
161 // Non-cached path: compile and link in one step.
162 // Place link flags (which may include .o files) AFTER the source file.
163 // Use `-x none` to reset the language before .o files, since -x objective-c
164 // would cause cc to treat .o files as source code.
165 mut cc_cmd := '${cc} ${cc_flags} -w -Wno-incompatible-function-pointer-types "${staged_c_file}"'
166 if cc_link_flags.len > 0 {
167 cc_cmd += ' -x none ${cc_link_flags}'
168 }
169 cc_cmd += ' -o "${output_name}"${error_limit_flag}'
170 run_cc_cmd_or_exit(cc_cmd, 'C compilation', b.pref.show_cc)
171 print_time('CC', time.Duration(sw.elapsed() - cc_start))
172
173 println('[*] Compiled ${output_name}')
174}
175
176// print_rss prints process RSS in MB at the given phase boundary.
177// Gated on V2_MEM=1.
178//
179// CAVEAT: v2 is force-built with `-gc none` (see is_v2_compiler_target in
180// vlib/v/pref/default.v), so allocations are never reclaimed. RSS is
181// dominated by the OS's decision of which pages to keep resident, not by
182// what the program is actually using. Run-to-run variance is huge
183// (observed 500 MB to 4.8 GB on identical runs). Treat these numbers as
184// a coarse signal, not a precision metric.
185//
186// For reliable peak-memory comparisons between optimizations, use
187// `/usr/bin/time -l <cmd>` and read `peak memory footprint` — that
188// metric is stable to within ~0.1% across runs.
189fn print_rss(stage string) {
190 if os.getenv('V2_MEM') == '' {
191 return
192 }
193 rss := runtime.used_memory() or { 0 }
194 $if macos {
195 // Under -gc none nothing is freed, so `live` is monotonic and its
196 // per-phase delta is the exact bytes that phase allocated. `peak` is
197 // the high-water mark. Both are stable run-to-run, unlike `rss`.
198 live, peak := darwin_live_malloc_bytes()
199 mb := u64(1024 * 1024)
200 eprintln(' [mem] ${stage}: live ${live / mb} MB peak ${peak / mb} MB (rss ${rss / mb} MB)')
201 return
202 }
203 eprintln(' [mem] ${stage}: ${rss / (1024 * 1024)} MB')
204}
205
206pub fn (mut b Builder) build(files []string) {
207 b.user_files = files
208 // Pre-parse fast path: for cmd/v2 self-host, if the cached bundle objects + main.o are
209 // present (restored from the durable tier if /tmp was wiped) and all sources are fresh,
210 // relink directly and skip the entire front-end. Falls through on any staleness.
211 if b.try_self_build_fast_relink() {
212 return
213 }
214 mut sw := time.new_stopwatch()
215 print_rss('start')
216 $if parallel ? {
217 if b.pref.no_parallel {
218 b.parse_files(files)
219 } else {
220 b.parse_files_parallel(files)
221 }
222 } $else {
223 b.parse_files(files)
224 }
225 b.files = []ast.File{}
226 parse_time := sw.elapsed()
227 print_time('Scan & Parse', parse_time)
228 print_rss('after parse')
229 // FlatBuilder is the canonical parse output; both parse paths stream into
230 // it. parse_files / parse_files_parallel return [] in flat mode, so b.flat
231 // is the live source of truth from here on. The rehydration to legacy
232 // []ast.File is deferred until a compatibility consumer actually needs it.
233 b.flat = b.flat_builder.flat
234 b.update_parse_summary_counts()
235 print_parse_summary(b.parsed_full_files_n, b.parsed_vh_files_n, b.entry_v_lines_n,
236 b.parsed_v_lines_n, b.pref.stats, b.pref.print_parsed_files, b.parsed_full_files,
237 b.parsed_vh_files)
238 if b.pref.stats {
239 b.print_flat_ast_summary()
240 }
241
242 if b.pref.backend == .cleanc && !b.validate_freestanding_cleanc_contract() {
243 exit(1)
244 }
245
246 if b.pref.skip_type_check {
247 b.env = types.Environment.new()
248 } else {
249 b.env = if b.pref.no_parallel {
250 b.type_check_files()
251 } else {
252 b.type_check_files_parallel()
253 }
254 }
255 type_check_time := time.Duration(sw.elapsed() - parse_time)
256 print_time('Type Check', type_check_time)
257 print_rss('after type check')
258
259 b.prepare_macos_tiny_candidate_source_files()
260
261 // Transform AST (flag enum desugaring, etc.)
262 transform_start := sw.elapsed()
263 mut trans := transformer.Transformer.new_with_pref(b.env, b.pref)
264 trans.set_file_set(b.file_set)
265 sequential_transform := b.pref.no_parallel_transform || b.pref.ownership
266 use_native_flat_pipeline := b.should_use_native_flat_pipeline()
267 b.native_flat_pipeline_enabled = use_native_flat_pipeline
268 // The transform is flat-only for every backend: both the sequential and
269 // the parallel path emit cursor-native into a FlatBuilder and never
270 // materialize a transformed []ast.File. The legacy backends that still
271 // consume []ast.File (.v/eval) rehydrate once from the transformed flat
272 // at the codegen boundary below instead of dragging a compatibility
273 // path through the transform.
274 if sequential_transform {
275 b.flat = trans.transform_flat_to_flat_direct(&b.flat, b.files)
276 } else {
277 b.flat = b.transform_files_parallel_flat_direct(mut trans)
278 }
279 b.files = []ast.File{}
280 transform_time := time.Duration(sw.elapsed() - transform_start)
281 print_time('Transform', transform_time)
282 print_rss('after transform')
283
284 // Mark used functions/methods for backend pruning.
285 if b.pref.no_markused || !b.backend_uses_markused_pruning()
286 || b.should_skip_markused_for_self_build() {
287 b.used_fn_keys = map[string]bool{}
288 } else {
289 mark_used_start := sw.elapsed()
290 // Flat markused consumes the post-transform FlatAst. Both sequential
291 // and parallel paths populate b.flat as part of their *_to_flat wedge,
292 // so the separate flatten_files() pass is gone.
293 if b.uses_minimal_x64_runtime_roots() {
294 opts := markused.MarkUsedOptions{
295 minimal_runtime_roots: true
296 }
297 b.used_fn_keys = markused.mark_used_flat_with_options(&b.flat, b.env, opts)
298 } else {
299 b.used_fn_keys = markused.mark_used_flat(&b.flat, b.env)
300 }
301 mark_used_time := time.Duration(sw.elapsed() - mark_used_start)
302 print_time('Mark Used', mark_used_time)
303 print_rss('after markused')
304 }
305 if !b.should_keep_flat_for_codegen() {
306 // .v/eval still consume legacy []ast.File: rehydrate once from the
307 // transformed flat at the backend boundary, then drop the flat.
308 if (b.pref.backend == .v && !b.pref.skip_genv) || b.pref.backend == .eval {
309 b.files = b.flat.to_files()
310 }
311 b.flat = ast.FlatAst{}
312 }
313
314 // Generate output based on backend
315 match b.pref.backend {
316 .v {
317 if !b.pref.skip_genv {
318 b.gen_v_files()
319 }
320 }
321 .cleanc {
322 b.gen_cleanc()
323 }
324 .c {
325 b.gen_ssa_c()
326 }
327 .x64 {
328 b.gen_native(.x64)
329 }
330 .arm64 {
331 b.gen_native(.arm64)
332 }
333 .eval {
334 mut runner := eval.new(b.pref)
335 runner.run_files(b.files) or {
336 eprintln('error: ${err.msg()}')
337 exit(1)
338 }
339 }
340 }
341
342 print_time('Total', sw.elapsed())
343 print_rss('after codegen (peak)')
344}
345
346fn (mut b Builder) gen_v_files() {
347 mut gen := gen_v.new_gen(b.pref)
348 for file in b.files {
349 gen.gen(file)
350 if b.pref.debug {
351 gen.print_output()
352 }
353 }
354}
355
356fn (mut b Builder) gen_cleanc() {
357 // Clean C Backend (AST -> C)
358 mut sw := time.new_stopwatch()
359
360 // The cached-core split is currently unstable in some module builds
361 // (including cmd/v2 self-host and directory-style user module builds).
362 // Force single-unit cleanc generation there.
363 force_no_cache := b.should_disable_cleanc_cache()
364 use_cache := !b.pref.no_cache && !force_no_cache
365 if os.getenv('V2_TRACE_CACHE') != '' {
366 eprintln('TRACE_CACHE use_cache=${use_cache} no_cache=${b.pref.no_cache} force_no_cache=${force_no_cache} self_build=${b.is_cmd_v2_self_build()} files=${b.user_files}')
367 }
368
369 // Determine output name
370 output_name := if b.pref.output_file != '' {
371 b.pref.output_file
372 } else if b.user_files.len > 0 {
373 b.default_output_name()
374 } else {
375 'out'
376 }
377
378 generation_only := output_name.ends_with('.c') || !b.can_compile_cleanc_locally()
379 if generation_only {
380 c_output_name := cleanc_c_output_name(output_name)
381 c_source := b.gen_cleanc_source([]string{})
382 print_time('C Gen', sw.elapsed())
383 if c_source == '' {
384 eprintln('error: cleanc backend is not fully functional (compiled with stubbed functions)')
385 eprintln('hint: use v2 compiled with v1 for proper C code generation')
386 return
387 }
388 os.write_file(c_output_name, c_source) or { panic(err) }
389 if output_name.ends_with('.c') {
390 println('[*] Wrote ${c_output_name}')
391 } else {
392 println('[*] Wrote ${c_output_name} (local C compilation disabled for this target)')
393 }
394 return
395 }
396
397 mut cc := if b.pref.ccompiler.len > 0 {
398 b.pref.ccompiler
399 } else {
400 configured_cc(b.pref.vroot)
401 }
402 // -prod requires a real optimizing compiler — TCC cannot handle -O3/-flto.
403 // Switch to system cc (gcc/clang) when the default compiler is TCC.
404 if b.pref.is_prod && cc.contains('tcc') {
405 cc = 'cc'
406 }
407 directive_flags := b.collect_cflags_from_sources()
408 $if macos {
409 if cc.contains('tcc') && cflags_need_objc_mode(directive_flags) {
410 cc = 'cc'
411 }
412 }
413 // Separate directive flags into compile-only and link-only flags.
414 // -framework, -l, -L, .o/.a/.so/.dylib are linker flags and must NOT
415 // be passed during -c compilation (they can trigger unwanted header
416 // processing, e.g. MetalKit SIMD errors on macOS).
417 directive_compile_flags, directive_link_flags := split_compile_and_link_flags(directive_flags)
418 mut cc_flag_parts := []string{}
419 mut cc_link_parts := []string{}
420 env_flags := configured_cflags()
421 if env_flags.trim_space() != '' {
422 cc_flag_parts << env_flags.trim_space()
423 }
424 if cc.contains('tcc') && directive_flags.contains('thirdparty/sqlite/sqlite3.c')
425 && !directive_compile_flags.contains('SQLITE_DISABLE_INTRINSIC') {
426 cc_flag_parts << '-DSQLITE_DISABLE_INTRINSIC'
427 }
428 if directive_compile_flags.trim_space() != '' {
429 cc_flag_parts << directive_compile_flags.trim_space()
430 }
431 if directive_link_flags.trim_space() != '' {
432 cc_link_parts << directive_link_flags.trim_space()
433 }
434 tcc_extra := tcc_flags(cc, b.pref.vroot)
435 if tcc_extra.trim_space() != '' {
436 cc_flag_parts << tcc_extra.trim_space()
437 }
438 // macOS code can include Objective-C (.m) files via #include directives.
439 // Tell the C compiler to treat the source as Objective-C only when needed.
440 $if macos {
441 if cflags_need_objc_mode(directive_flags) {
442 cc_flag_parts << '-x objective-c'
443 }
444 }
445 cc_flag_parts << '-std=gnu11'
446 cc_flag_parts << '-fwrapv'
447
448 // Detect compiler type for optimization flags and error limit.
449 is_tcc := cc.contains('tcc')
450 mut is_clang := false
451 if !is_tcc {
452 version_res := os.execute('${cc} --version')
453 if version_res.exit_code == 0 && version_res.output.contains('clang') {
454 is_clang = true
455 }
456 }
457
458 // -prod: add -O3, -flto, -DNDEBUG for gcc/clang
459 if b.pref.is_prod {
460 cc_flag_parts << '-O3'
461 cc_flag_parts << '-DNDEBUG'
462 if !b.pref.is_shared_lib {
463 $if !windows {
464 cc_flag_parts << '-flto'
465 }
466 }
467 if !is_clang {
468 cc_flag_parts << '-fno-strict-aliasing'
469 }
470 }
471
472 cc_flags := cc_flag_parts.join(' ')
473 cc_link_flags := cc_link_parts.join(' ')
474 mut error_limit_flag := ''
475 if is_clang {
476 error_limit_flag = ' -ferror-limit=0'
477 }
478
479 // Fast path: cache one core object (builtin+strconv), compile/link only the rest.
480 if use_cache && !b.pref.skip_builtin && b.has_module('builtin') && b.has_module('strconv') {
481 if b.gen_cleanc_with_cached_core(output_name, cc, cc_flags, cc_link_flags,
482 error_limit_flag, mut sw)
483 {
484 // Mirror the freshly built objects to the durable tier so a later cold
485 // build (with /tmp wiped) can restore them and fast-relink.
486 b.save_durable_object_cache(b.core_cache_dir())
487 return
488 }
489 }
490
491 // Fallback: compile one full C translation unit.
492 c_source := b.gen_cleanc_source([]string{})
493 print_time('C Gen', sw.elapsed())
494 if c_source == '' {
495 eprintln('error: cleanc backend is not fully functional (compiled with stubbed functions)')
496 eprintln('hint: use v2 compiled with v1 for proper C code generation')
497 return
498 }
499 os.write_file(staged_c_file, c_source) or { panic(err) }
500 println('[*] Wrote ${staged_c_file}')
501 b.compile_cleanc_executable(output_name, cc, cc_flags, cc_link_flags, error_limit_flag, mut sw)
502}
503
504fn (b &Builder) is_cmd_v2_self_build() bool {
505 if b.user_files.len != 1 {
506 return false
507 }
508 // Avoid path normalization here: during bootstraps, some intermediate
509 // compilers can still have unstable path helpers.
510 path := b.user_files[0].replace('\\', '/')
511 if path == 'v2.v' || path.ends_with('/v2.v') {
512 return true
513 }
514 return path.ends_with('/cmd/v2/v2.v') || path.ends_with('cmd/v2/v2.v')
515}
516
517fn (b &Builder) should_disable_cleanc_cache() bool {
518 // ARM64 cache previously disabled due to runtime helpers being emitted
519 // as static inline, which dropped them at cached-core boundaries.
520 // Fixed: __v2_array_eq is now a regular function with its body in the
521 // builtin cache unit and a forward declaration in the main TU.
522 for raw_input in b.user_files {
523 input := raw_input.trim_right('/\\')
524 if input.len == 0 {
525 continue
526 }
527 if input.ends_with('.v') || input.ends_with('.vv') || input.ends_with('.vsh')
528 || input.ends_with('.vh') {
529 continue
530 }
531 }
532 return false
533}
534
535fn (b &Builder) default_output_name() string {
536 if b.user_files.len == 0 {
537 return 'out'
538 }
539 last_input := b.user_files[b.user_files.len - 1].trim_right('/\\')
540 if last_input.len == 0 || last_input == '.' {
541 cwd := os.getwd()
542 base := os.file_name(cwd)
543 return if base.len > 0 { base } else { 'out' }
544 }
545 if os.is_dir(last_input) {
546 base := os.file_name(last_input)
547 return if base.len > 0 { base } else { 'out' }
548 }
549 base := os.file_name(last_input).all_before_last('.v')
550 return if base.len > 0 { base } else { os.file_name(last_input) }
551}
552
553fn (mut b Builder) gen_ssa_c() {
554 // SSA -> C backend.
555 mut sw := time.new_stopwatch()
556
557 mut mod := ssa.Module.new('main')
558 if mod == unsafe { nil } {
559 eprintln('error: ssa c backend not available (compiled with stubbed ssa module)')
560 eprintln('hint: use v2 compiled with v1 for ssa c code generation')
561 return
562 }
563 mut ssa_builder := ssa.Builder.new_with_env(mod, b.env)
564 ssa_builder.target_os = b.pref.target_os_or_host()
565
566 mut stage_start := sw.elapsed()
567 mut built_from_flat := false
568 if b.should_build_ssa_from_flat() {
569 ssa_builder.build_all_from_flat(&b.flat)
570 built_from_flat = true
571 } else {
572 ssa_builder.build_all(b.files)
573 }
574 print_time('SSA Build', time.Duration(sw.elapsed() - stage_start))
575
576 // TODO: re-enable SSA optimization once the new builder is mature
577 // stage_start = sw.elapsed()
578 // optimize.optimize(mut mod)
579 // print_time('SSA Optimize', time.Duration(sw.elapsed() - stage_start))
580
581 output_name := if b.pref.output_file != '' {
582 b.pref.output_file
583 } else if b.user_files.len > 0 {
584 b.default_output_name()
585 } else {
586 'out'
587 }
588
589 if output_name.ends_with('.c') {
590 if built_from_flat {
591 b.flat = ast.FlatAst{}
592 }
593 stage_start = sw.elapsed()
594 mut gen := c.new_gen(mod)
595 c_source := gen.gen()
596 print_time('C Gen', time.Duration(sw.elapsed() - stage_start))
597 if c_source == '' {
598 eprintln('error: ssa c backend failed to generate C source')
599 return
600 }
601 os.write_file(output_name, c_source) or { panic(err) }
602 println('[*] Wrote ${output_name}')
603 return
604 }
605
606 cc := if b.pref.ccompiler.len > 0 { b.pref.ccompiler } else { configured_cc(b.pref.vroot) }
607 directive_flags := b.collect_cflags_from_sources()
608 if built_from_flat {
609 // SSA has copied the program into MIR, and directive scanning has read
610 // source names from the FlatAst. Keep the later C generator/compiler
611 // working sets clear of the transformed FlatAst.
612 b.flat = ast.FlatAst{}
613 }
614 mut cc_flag_parts := []string{}
615 env_flags := configured_cflags()
616 if env_flags.trim_space() != '' {
617 cc_flag_parts << env_flags.trim_space()
618 }
619 if cc.contains('tcc') && directive_flags.contains('thirdparty/sqlite/sqlite3.c')
620 && !directive_flags.contains('SQLITE_DISABLE_INTRINSIC') {
621 cc_flag_parts << '-DSQLITE_DISABLE_INTRINSIC'
622 }
623 if directive_flags.trim_space() != '' {
624 cc_flag_parts << directive_flags.trim_space()
625 }
626 tcc_extra := tcc_flags(cc, b.pref.vroot)
627 if tcc_extra.trim_space() != '' {
628 cc_flag_parts << tcc_extra.trim_space()
629 }
630 cc_flags := cc_flag_parts.join(' ')
631 mut error_limit_flag := ''
632 if !cc.contains('tcc') {
633 version_res := os.execute('${cc} --version')
634 if version_res.exit_code == 0 && version_res.output.contains('clang') {
635 error_limit_flag = ' -ferror-limit=0'
636 }
637 }
638
639 // Try to get pre-compiled builtin.o and vlib.o from the cleanc cache
640 mut builtin_obj := ''
641 mut vlib_obj := ''
642 if !b.pref.skip_builtin && b.has_module('builtin') && b.has_module('strconv')
643 && b.ensure_core_cache_dir() {
644 cache_dir := b.core_cache_dir()
645 builtin_obj = b.ensure_cached_module_object(cache_dir, builtin_cache_name,
646 builtin_cached_module_paths, builtin_cached_module_names, cc, cc_flags, '',
647 error_limit_flag, false) or { '' }
648 if builtin_obj.len > 0 && vlib_cached_module_paths.len > 0 {
649 vlib_obj = b.ensure_cached_module_object(cache_dir, vlib_cache_name,
650 vlib_cached_module_paths, vlib_cached_module_names, cc, cc_flags, '',
651 error_limit_flag, false) or { '' }
652 }
653 }
654 stage_start = sw.elapsed()
655 mut gen := c.new_gen(mod)
656 gen.link_builtin = builtin_obj.len > 0
657 c_source := gen.gen()
658 print_time('C Gen', time.Duration(sw.elapsed() - stage_start))
659 if c_source == '' {
660 eprintln('error: ssa c backend failed to generate C source')
661 return
662 }
663
664 c_file := b.exec_build_c_file(output_name)
665 os.write_file(c_file, c_source) or { panic(err) }
666 if c_file != staged_c_file {
667 os.write_file(staged_c_file, c_source) or { panic(err) }
668 }
669 println('[*] Wrote ${c_file}')
670
671 cc_start := sw.elapsed()
672 mut cc_cmd := ''
673 if builtin_obj.len > 0 {
674 // Compile SSA main.c and link against pre-compiled builtin.o
675 main_obj := staged_main_obj_file
676 compile_cmd := '${cc} ${cc_flags} -w -c "${c_file}" -o "${main_obj}"${error_limit_flag}'
677 if b.pref.show_cc {
678 println(compile_cmd)
679 }
680 compile_res := os.execute(compile_cmd)
681 if compile_res.exit_code != 0 {
682 eprintln('error: ssa c backend compilation failed')
683 lines := compile_res.output.split_into_lines()
684 limit := if lines.len < 20 { lines.len } else { 20 }
685 for line in lines[..limit] {
686 eprintln(line)
687 }
688 exit(1)
689 }
690 mut link_objects := '"${main_obj}" "${builtin_obj}"'
691 if vlib_obj.len > 0 {
692 link_objects += ' "${vlib_obj}"'
693 }
694 cc_cmd = '${cc} ${cc_flags} -w ${link_objects} -o "${output_name}"'
695 if b.pref.show_cc {
696 println(cc_cmd)
697 }
698 cc_res := os.execute(cc_cmd)
699 if cc_res.exit_code != 0 {
700 eprintln('error: ssa c backend linking failed')
701 lines := cc_res.output.split_into_lines()
702 limit := if lines.len < 20 { lines.len } else { 20 }
703 for line in lines[..limit] {
704 eprintln(line)
705 }
706 exit(1)
707 }
708 if !b.pref.keep_c {
709 os.rm(main_obj) or {}
710 }
711 } else {
712 // Single-file compilation (no builtin linking)
713 cc_cmd = '${cc} ${cc_flags} -w "${c_file}" -o "${output_name}"${error_limit_flag}'
714 if b.pref.show_cc {
715 println(cc_cmd)
716 } else if os.getenv('V2VERBOSE') != '' {
717 dump(cc_cmd)
718 }
719 cc_res := os.execute(cc_cmd)
720 if cc_res.exit_code != 0 {
721 eprintln('error: ssa c backend compilation failed')
722 lines := cc_res.output.split_into_lines()
723 limit := if lines.len < 20 { lines.len } else { 20 }
724 for line in lines[..limit] {
725 eprintln(line)
726 }
727 exit(1)
728 }
729 }
730 print_time('CC', time.Duration(sw.elapsed() - cc_start))
731
732 if !b.pref.keep_c {
733 os.rm(c_file) or {}
734 }
735 println('[*] Compiled ${output_name}')
736}
737
738fn (mut b Builder) gen_cleanc_source(modules []string) string {
739 return b.gen_cleanc_source_with_options(modules, []string{}, false, '', []string{}, true,
740 []string{})
741}
742
743fn (mut b Builder) gen_cleanc_source_for_cache(modules []string, cache_bundle_name string, use_markused bool) string {
744 return b.gen_cleanc_source_with_options(modules, []string{}, true, cache_bundle_name,
745 []string{}, use_markused, []string{})
746}
747
748fn (mut b Builder) gen_cleanc_source_for_cache_files(modules []string, emit_files []string, cache_bundle_name string, use_markused bool) string {
749 return b.gen_cleanc_source_with_options(modules, emit_files, true, cache_bundle_name,
750 []string{}, use_markused, []string{})
751}
752
753fn (mut b Builder) gen_cleanc_source_with_cache_init_calls(modules []string, cached_init_calls []string) string {
754 return b.gen_cleanc_source_with_options(modules, []string{}, false, '', cached_init_calls,
755 true, []string{})
756}
757
758fn (mut b Builder) gen_cleanc_source_with_cache_init_calls_and_files(modules []string, emit_files []string, cached_init_calls []string, use_markused bool) string {
759 return b.gen_cleanc_source_with_options(modules, emit_files, false, '', cached_init_calls,
760 use_markused, []string{})
761}
762
763fn (mut b Builder) gen_cleanc_source_with_cache_init_calls_files_force(modules []string, emit_files []string, cached_init_calls []string, use_markused bool, force_emit_fn_names []string) string {
764 return b.gen_cleanc_source_with_options(modules, emit_files, false, '', cached_init_calls,
765 use_markused, force_emit_fn_names)
766}
767
768fn (mut b Builder) gen_cleanc_source_with_options(modules []string, emit_files []string, export_const_symbols bool, cache_bundle_name string, cached_init_calls []string, use_markused bool, force_emit_fn_names []string) string {
769 type_modules := if cache_bundle_name.len > 0 {
770 b.expand_type_modules_with_imports(cache_type_module_names(cache_bundle_name, modules))
771 } else {
772 cache_type_module_names(cache_bundle_name, modules)
773 }
774 mut type_module_names := map[string]bool{}
775 restrict_to_cache_modules := cache_bundle_name.len > 0 && type_modules.len > 0
776 if restrict_to_cache_modules {
777 for module_name in type_modules {
778 if module_name != '' {
779 type_module_names[module_name] = true
780 }
781 }
782 }
783 // Cache-bundle generation restricts emission to a fixed set of type
784 // modules. The legacy gen achieves this by filtering its input `gen_files`;
785 // the flat gen drives every pass off `flat.files`, so for a restricted
786 // bundle in flat mode we hand it a FlatAst scoped to the bundle's type
787 // modules (sharing b.flat's arena). The main translation unit
788 // (`restrict_to_cache_modules == false`) uses the full b.flat.
789 scope_flat_bundle := restrict_to_cache_modules && b.flat.files.len > 0
790 scoped_flat := if scope_flat_bundle {
791 b.flat_scoped_to_modules(type_module_names)
792 } else {
793 ast.FlatAst{}
794 }
795 mut gen_files := []ast.File{cap: b.files.len}
796 if !scope_flat_bundle {
797 for file in b.files {
798 if restrict_to_cache_modules && ast_file_module_name(file) !in type_module_names {
799 continue
800 }
801 gen_files << file
802 }
803 }
804 if !scope_flat_bundle && cached_init_calls.len > 0 && b.used_vh_for_parse {
805 mut has_vh_files := false
806 for file in gen_files {
807 if file.name.ends_with('.vh') {
808 has_vh_files = true
809 break
810 }
811 }
812 if !has_vh_files {
813 mut p := parser.Parser.new(b.pref)
814 header_files := p.parse_files(b.core_cached_parse_paths(), mut b.file_set)
815 for header_file in header_files {
816 gen_files << header_file
817 }
818 }
819 }
820 mut gen := if scope_flat_bundle {
821 cleanc.Gen.new_with_env_pref_and_flat(&scoped_flat, b.env, b.pref)
822 } else if b.flat.files.len > 0 {
823 cleanc.Gen.new_with_env_pref_and_flat(&b.flat, b.env, b.pref)
824 } else {
825 cleanc.Gen.new_with_env_and_pref(gen_files, b.env, b.pref)
826 }
827 if modules.len > 0 {
828 gen.set_emit_modules(modules)
829 }
830 if type_modules.len > 0 {
831 gen.set_type_modules(type_modules)
832 }
833 if emit_files.len > 0 {
834 gen.set_emit_files(emit_files)
835 }
836 if use_markused && b.used_fn_keys.len > 0 {
837 gen.set_used_fn_keys(b.used_fn_keys)
838 }
839 if force_emit_fn_names.len > 0 {
840 gen.set_force_emit_fn_names(force_emit_fn_names)
841 }
842 gen.set_export_const_symbols(export_const_symbols)
843 if cache_bundle_name.len > 0 {
844 gen.set_cache_bundle_name(cache_bundle_name)
845 }
846 if cached_init_calls.len > 0 {
847 gen.set_cached_init_calls(cached_init_calls)
848 }
849 if cache_bundle_name.len == 0 && cached_init_calls.len > 0 && b.cached_called_fn_names.len > 0 {
850 gen.add_called_fn_names(b.cached_called_fn_name_list())
851 }
852 if cache_bundle_name.len == 0 && cached_init_calls.len > 0
853 && b.has_v2compiler_generic_setup_snapshot {
854 gen.use_generic_setup_snapshot(b.v2compiler_generic_setup_snapshot)
855 }
856 use_parallel := b.pref != unsafe { nil } && !b.pref.no_parallel
857 if use_parallel {
858 gen.gen_passes_1_to_4()
859 if cache_bundle_name == v2compiler_cache_name {
860 b.v2compiler_generic_setup_snapshot = gen.generic_setup_snapshot()
861 b.has_v2compiler_generic_setup_snapshot = true
862 }
863 b.gen_cleanc_parallel(mut gen)
864 source := gen.gen_finalize()
865 if cache_bundle_name.len > 0 {
866 b.add_cached_called_fn_names(gen.external_called_fn_names())
867 }
868 if os.getenv('V2TRACE_CLEANC') != '' {
869 eprintln('TRACE_CLEANC builder_files=${b.files.len} gen_files=${gen_files.len} source_len=${source.len}')
870 }
871 return source
872 }
873 source := gen.gen()
874 if cache_bundle_name == v2compiler_cache_name {
875 b.v2compiler_generic_setup_snapshot = gen.generic_setup_snapshot()
876 b.has_v2compiler_generic_setup_snapshot = true
877 }
878 if cache_bundle_name.len > 0 {
879 b.add_cached_called_fn_names(gen.external_called_fn_names())
880 }
881 if os.getenv('V2TRACE_CLEANC') != '' {
882 eprintln('TRACE_CLEANC builder_files=${b.files.len} gen_files=${gen_files.len} source_len=${source.len}')
883 }
884 return source
885}
886
887fn (mut b Builder) add_cached_called_fn_names(names []string) {
888 for name in names {
889 if name.len > 0 {
890 b.cached_called_fn_names[name] = true
891 }
892 }
893}
894
895fn (b &Builder) cached_called_fn_name_list() []string {
896 mut names := b.cached_called_fn_names.keys()
897 names.sort()
898 return names
899}
900
901fn cached_called_fn_names_path(cache_dir string, cache_name string) string {
902 return cache_path_join(cache_dir, '${cache_name}.calls')
903}
904
905fn (mut b Builder) load_cached_called_fn_names(cache_dir string, cache_name string) bool {
906 data := os.read_file(cached_called_fn_names_path(cache_dir, cache_name)) or { return false }
907 for line in data.split_into_lines() {
908 name := line.trim_space()
909 if name.len > 0 {
910 b.cached_called_fn_names[name] = true
911 }
912 }
913 return true
914}
915
916fn (mut b Builder) write_cached_called_fn_names(cache_dir string, cache_name string, before map[string]bool) {
917 mut names := []string{}
918 for name, _ in b.cached_called_fn_names {
919 if name.len == 0 || name in before {
920 continue
921 }
922 names << name
923 }
924 names.sort()
925 os.write_file(cached_called_fn_names_path(cache_dir, cache_name), names.join('\n')) or {}
926}
927
928fn (b &Builder) cgen_builder_stats_enabled() bool {
929 return b.pref != unsafe { nil } && b.pref.stats
930}
931
932fn (b &Builder) mark_cgen_builder_step(stats_enabled bool, cache_name string, mut sw time.StopWatch, stage_start time.Duration, step string) time.Duration {
933 if !stats_enabled {
934 return stage_start
935 }
936 now := sw.elapsed()
937 println(' - C Gen/cache:${cache_name} cache.${step}: ${time.Duration(now - stage_start).milliseconds()}ms')
938 return now
939}
940
941fn cache_type_module_names(cache_bundle_name string, emit_modules []string) []string {
942 if cache_bundle_name == '' {
943 return emit_modules
944 }
945 mut names := []string{cap: core_cached_module_names.len + emit_modules.len}
946 if cache_bundle_name == builtin_cache_name {
947 names << emit_modules
948 } else if cache_bundle_name == vlib_cache_name {
949 names << builtin_cached_module_names
950 names << emit_modules
951 } else {
952 names << core_cached_module_names
953 names << emit_modules
954 }
955 return unique_sorted_strings(names)
956}
957
958fn (b &Builder) expand_type_modules_with_imports(modules []string) []string {
959 mut type_modules := map[string]bool{}
960 for module_name in modules {
961 if module_name != '' {
962 type_modules[module_name] = true
963 }
964 }
965 use_flat := b.uses_flat_module_enumeration()
966 mut changed := true
967 for changed {
968 changed = false
969 if use_flat {
970 for i in 0 .. b.flat.files.len {
971 if b.flat_file_module_name(i) !in type_modules {
972 continue
973 }
974 for import_stmt in b.flat.file_cursor(i).imports().import_stmts() {
975 import_module := import_module_name(import_stmt.name)
976 if import_module == '' || import_module in type_modules {
977 continue
978 }
979 type_modules[import_module] = true
980 changed = true
981 }
982 }
983 } else {
984 for file in b.files {
985 if ast_file_module_name(file) !in type_modules {
986 continue
987 }
988 for import_stmt in file.imports {
989 import_module := import_module_name(import_stmt.name)
990 if import_module == '' || import_module in type_modules {
991 continue
992 }
993 type_modules[import_module] = true
994 changed = true
995 }
996 }
997 }
998 }
999 return unique_sorted_strings(type_modules.keys())
1000}
1001
1002fn (b &Builder) has_external_cache_module_name_collision(module_names []string) bool {
1003 mut module_set := map[string]bool{}
1004 for module_name in module_names {
1005 if module_name != '' {
1006 module_set[module_name] = true
1007 }
1008 }
1009 mut vlib_modules := map[string]bool{}
1010 mut external_modules := map[string]bool{}
1011 if b.uses_flat_module_enumeration() {
1012 for i in 0 .. b.flat.files.len {
1013 name := b.flat.file_name(b.flat.files[i])
1014 if name == '' || name.ends_with('.vh') {
1015 continue
1016 }
1017 module_name := b.flat_file_module_name(i)
1018 if module_name !in module_set {
1019 continue
1020 }
1021 if b.is_vlib_source_file(name) {
1022 vlib_modules[module_name] = true
1023 } else {
1024 external_modules[module_name] = true
1025 }
1026 }
1027 } else {
1028 for file in b.files {
1029 if file.name == '' || file.name.ends_with('.vh') {
1030 continue
1031 }
1032 module_name := ast_file_module_name(file)
1033 if module_name !in module_set {
1034 continue
1035 }
1036 if b.is_vlib_source_file(file.name) {
1037 vlib_modules[module_name] = true
1038 } else {
1039 external_modules[module_name] = true
1040 }
1041 }
1042 }
1043 for module_name, _ in external_modules {
1044 if module_name in vlib_modules {
1045 return true
1046 }
1047 }
1048 return false
1049}
1050
1051fn (b &Builder) is_vlib_source_file(file_name string) bool {
1052 root := if b.pref.vroot.len > 0 { b.pref.vroot } else { os.getwd() }
1053 vlib_root := os.norm_path(os.join_path(root, 'vlib')) + os.path_separator
1054 return os.norm_path(os.abs_path(file_name)).starts_with(vlib_root)
1055}
1056
1057fn import_module_name(name string) string {
1058 return name.all_after_last('.').replace('.', '_')
1059}
1060
1061fn unique_sorted_strings(items []string) []string {
1062 mut seen := map[string]bool{}
1063 mut out := []string{cap: items.len}
1064 for item in items {
1065 if item == '' || item in seen {
1066 continue
1067 }
1068 seen[item] = true
1069 out << item
1070 }
1071 out.sort()
1072 return out
1073}
1074
1075fn (mut b Builder) gen_cleanc_with_cached_core(output_name string, cc string, cc_flags string, cc_link_flags string, error_limit_flag string, mut sw time.StopWatch) bool {
1076 cache_dir := b.core_cache_dir()
1077 if !b.ensure_core_cache_dir() {
1078 // If we cannot create a readable/writable cache dir, fall back to full compilation.
1079 if os.getenv('V2_TRACE_CACHE') != '' {
1080 eprintln('TRACE_CACHE cached_core=false reason=cache_dir_unusable')
1081 }
1082 return false
1083 }
1084 if b.try_link_cached_self_main_object(output_name, cache_dir, cc, cc_flags, cc_link_flags, mut
1085 sw)
1086 {
1087 return true
1088 }
1089
1090 builtin_obj := b.ensure_cached_module_object(cache_dir, builtin_cache_name,
1091 builtin_cached_module_paths, builtin_cached_module_names, cc, cc_flags, '',
1092 error_limit_flag, false) or {
1093 if os.getenv('V2_TRACE_CACHE') != '' {
1094 eprintln('TRACE_CACHE cached_core=false reason=builtin_obj_failed')
1095 }
1096 return false
1097 }
1098 b.print_cached_bundle_modules(builtin_cache_name, builtin_cached_module_names)
1099 mut vlib_obj := ''
1100 if vlib_cached_module_paths.len > 0 {
1101 vlib_obj = b.ensure_cached_module_object(cache_dir, vlib_cache_name,
1102 vlib_cached_module_paths, vlib_cached_module_names, cc, cc_flags, '', error_limit_flag,
1103 false) or {
1104 if os.getenv('V2_TRACE_CACHE') != '' {
1105 eprintln('TRACE_CACHE cached_core=false reason=vlib_obj_failed')
1106 }
1107 return false
1108 }
1109 if vlib_obj.len > 0 {
1110 b.print_cached_bundle_modules(vlib_cache_name, vlib_cached_module_names)
1111 }
1112 }
1113 mut optional_cached_objs := []string{}
1114 mut optional_cached_cache_names := []string{}
1115 mut optional_cached_module_names := []string{}
1116 if veb_cached_module_paths.len > 0 && b.has_module('veb') {
1117 veb_cache_type_modules := b.expand_type_modules_with_imports(cache_type_module_names(veb_cache_name,
1118 veb_cached_module_names))
1119 veb_obj := if b.has_external_cache_module_name_collision(veb_cache_type_modules) {
1120 if os.getenv('V2_TRACE_CACHE') != '' {
1121 eprintln('TRACE_CACHE optional_cache=veb reason=external_module_name_collision')
1122 }
1123 ''
1124 } else {
1125 b.ensure_cached_module_object(cache_dir, veb_cache_name, veb_cached_module_paths,
1126 veb_cached_module_names, cc, cc_flags, cc_link_flags, error_limit_flag, true) or {
1127 if os.getenv('V2_TRACE_CACHE') != '' {
1128 eprintln('TRACE_CACHE optional_cache=veb reason=${err}')
1129 }
1130 ''
1131 }
1132 }
1133 if veb_obj.len > 0 {
1134 b.print_cached_bundle_modules(veb_cache_name, veb_cached_module_names)
1135 optional_cached_objs << veb_obj
1136 optional_cached_cache_names << veb_cache_name
1137 optional_cached_module_names << veb_cached_module_names
1138 }
1139 }
1140 mut v2compiler_obj := ''
1141 if v2compiler_cached_module_paths.len > 0 && b.is_cmd_v2_self_build() {
1142 v2compiler_obj = b.ensure_cached_module_object(cache_dir, v2compiler_cache_name,
1143 v2compiler_cached_module_paths, v2compiler_cached_module_names, cc, cc_flags, '',
1144 error_limit_flag, false) or {
1145 if os.getenv('V2_TRACE_CACHE') != '' {
1146 eprintln('TRACE_CACHE cached_core=false reason=v2compiler_obj_failed')
1147 }
1148 return false
1149 }
1150 if v2compiler_obj.len > 0 {
1151 b.print_cached_bundle_modules(v2compiler_cache_name, v2compiler_cached_module_names)
1152 }
1153 }
1154 mut dynamic_excluded := core_cached_module_names.clone()
1155 for module_name in optional_cached_module_names {
1156 dynamic_excluded << module_name
1157 }
1158 entry_module_names := b.user_entry_module_names()
1159 dynamic_excluded << entry_module_names
1160 if b.is_cmd_v2_self_build() {
1161 dynamic_excluded << v2compiler_cached_module_names
1162 }
1163 dynamic_excluded << 'main'
1164 dynamic_cached_module_names := b.collect_modules_excluding(dynamic_excluded)
1165 if dynamic_cached_module_names.len > 0 {
1166 mut import_dependency_cache_names := [builtin_cache_name]
1167 if vlib_obj.len > 0 {
1168 import_dependency_cache_names << vlib_cache_name
1169 }
1170 if v2compiler_obj.len > 0 {
1171 import_dependency_cache_names << v2compiler_cache_name
1172 }
1173 for cache_name in optional_cached_cache_names {
1174 import_dependency_cache_names << cache_name
1175 }
1176 import_dependency_compile_flags, import_dependency_link_flags :=
1177 b.cached_module_stamp_flags(import_dependency_cache_names)
1178 imports_cc_flags := join_flag_strings(cc_flags, import_dependency_compile_flags)
1179 imports_cc_link_flags := join_flag_strings(cc_link_flags, import_dependency_link_flags)
1180 imports_obj := b.ensure_cached_parsed_module_object(cache_dir, imports_cache_name,
1181 dynamic_cached_module_names, import_dependency_cache_names, cc, imports_cc_flags,
1182 imports_cc_link_flags, error_limit_flag, true) or {
1183 if os.getenv('V2_TRACE_CACHE') != '' {
1184 eprintln('TRACE_CACHE optional_cache=imports reason=${err}')
1185 }
1186 if b.used_import_vh_for_parse {
1187 return false
1188 }
1189 ''
1190 }
1191 if imports_obj.len > 0 {
1192 b.print_cached_bundle_modules(imports_cache_name, dynamic_cached_module_names)
1193 optional_cached_objs << imports_obj
1194 optional_cached_cache_names << imports_cache_name
1195 optional_cached_module_names << dynamic_cached_module_names
1196 }
1197 }
1198 mut virtual_groups := if b.used_virtual_vh_for_parse {
1199 b.cached_virtual_manifest()
1200 } else {
1201 b.collect_virtual_main_modules()
1202 }
1203 mut virtual_source_files := []string{}
1204 mut virtuals_obj := ''
1205 if virtual_groups.len > 0 {
1206 mut virtual_dependency_cache_names := [builtin_cache_name]
1207 if vlib_obj.len > 0 {
1208 virtual_dependency_cache_names << vlib_cache_name
1209 }
1210 if v2compiler_obj.len > 0 {
1211 virtual_dependency_cache_names << v2compiler_cache_name
1212 }
1213 for cache_name in optional_cached_cache_names {
1214 virtual_dependency_cache_names << cache_name
1215 }
1216 virtual_dependency_compile_flags, virtual_dependency_link_flags :=
1217 b.cached_module_stamp_flags(virtual_dependency_cache_names)
1218 virtual_cc_flags := join_flag_strings(cc_flags, virtual_dependency_compile_flags)
1219 virtual_cc_link_flags := join_flag_strings(cc_link_flags, virtual_dependency_link_flags)
1220 virtuals_obj = b.ensure_cached_virtual_module_object(cache_dir, virtual_groups,
1221 virtual_dependency_cache_names, cc, virtual_cc_flags, virtual_cc_link_flags,
1222 error_limit_flag, true) or {
1223 if os.getenv('V2_TRACE_CACHE') != '' {
1224 eprintln('TRACE_CACHE optional_cache=${virtuals_cache_name} reason=${err}')
1225 }
1226 if b.used_virtual_vh_for_parse {
1227 return false
1228 }
1229 virtual_groups = []CachedVirtualModule{}
1230 ''
1231 }
1232 if virtuals_obj.len > 0 {
1233 virtual_source_files = virtual_module_source_files(virtual_groups)
1234 b.print_cached_bundle_modules(virtuals_cache_name, virtual_module_names(virtual_groups))
1235 }
1236 }
1237 b.ensure_core_module_headers()
1238 b.ensure_import_module_headers(dynamic_cached_module_names)
1239 // The v2compiler .vh headers are read back only by can_use_cached_v2compiler_headers_for_parse
1240 // (via cached_import_parse_path). That parse-reuse path was disabled because the generated
1241 // headers are not yet complete/safe, so the 21 module headers are write-only on every cold
1242 // self-build — ~230ms of pure overhead. Skip generation until reuse is re-enabled (the headers
1243 // are regenerated on demand the moment it is; see v2compiler_headers_consumed_for_parse).
1244 if v2compiler_obj.len > 0 && b.v2compiler_headers_consumed_for_parse() {
1245 b.ensure_v2compiler_module_headers()
1246 }
1247 b.ensure_virtual_module_headers(virtual_groups)
1248 mut excluded := core_cached_module_names.clone()
1249 for module_name in optional_cached_module_names {
1250 excluded << module_name
1251 }
1252 if b.is_cmd_v2_self_build() {
1253 excluded << v2compiler_cached_module_names
1254 }
1255 main_modules := b.collect_modules_excluding(excluded)
1256 if main_modules.len == 0 {
1257 if os.getenv('V2_TRACE_CACHE') != '' {
1258 eprintln('TRACE_CACHE cached_core=false reason=no_main_modules')
1259 }
1260 return false
1261 }
1262 mut linked_cache_names := [builtin_cache_name]
1263 if vlib_obj.len > 0 {
1264 linked_cache_names << vlib_cache_name
1265 }
1266 if v2compiler_obj.len > 0 {
1267 linked_cache_names << v2compiler_cache_name
1268 }
1269 for cache_name in optional_cached_cache_names {
1270 linked_cache_names << cache_name
1271 }
1272 cached_compile_flags, cached_link_flags := b.cached_module_stamp_flags(linked_cache_names)
1273
1274 // When TCC is the default compiler but fell back to cc for cache
1275 // compilation (e.g. due to TCC not supporting certain C constructs),
1276 // the cached .o files are Mach-O (from cc) while TCC would produce ELF.
1277 // Detect this mismatch and use cc for main compilation and linking too.
1278 // Also, -prod builds with -flto require gcc/clang for linking — TCC
1279 // cannot link LTO object files.
1280 mut main_cc := cc
1281 mut main_cc_flags := join_flag_strings(cc_flags, cached_compile_flags)
1282 main_cc_link_flags := join_flag_strings(cc_link_flags, cached_link_flags)
1283 if cc.contains('tcc') && os.exists(builtin_obj) {
1284 bytes := os.read_bytes(builtin_obj) or { []u8{} }
1285 is_elf := bytes.len >= 4 && bytes[0] == 0x7f && bytes[1] == 0x45 && bytes[2] == 0x4c
1286 && bytes[3] == 0x46
1287 if !is_elf {
1288 // Cached .o was compiled by cc (via TCC fallback), not TCC.
1289 // Use cc for main compilation and linking to match formats.
1290 // Keep all flags (including directive -I paths) but strip
1291 // TCC-specific -I/-L paths that would conflict with system headers.
1292 main_cc = 'cc'
1293 tcc_dir2 := cc.all_before_last('/tcc')
1294 if tcc_dir2.len > 0 {
1295 mut parts := main_cc_flags.fields()
1296 mut filtered := []string{cap: parts.len}
1297 mut j := 0
1298 for j < parts.len {
1299 p := parts[j]
1300 if (p == '-I' || p == '-L') && j + 1 < parts.len && parts[j + 1].contains('tcc') {
1301 j += 2
1302 continue
1303 }
1304 if (p.starts_with('-I') || p.starts_with('-L')) && p.contains('tcc') {
1305 j++
1306 continue
1307 }
1308 // Strip quoted -I/-L containing tcc
1309 if (p.starts_with('-I"') || p.starts_with('-L"')
1310 || p.starts_with("-I'") || p.starts_with("-L'")) && p.contains('tcc') {
1311 j++
1312 continue
1313 }
1314 filtered << p
1315 j++
1316 }
1317 main_cc_flags = filtered.join(' ')
1318 }
1319 }
1320 }
1321
1322 mut cached_init_calls := []string{}
1323 cached_init_calls << '__v2_cached_init_${builtin_cache_name}'
1324 if vlib_obj.len > 0 {
1325 cached_init_calls << '__v2_cached_init_${vlib_cache_name}'
1326 }
1327 if v2compiler_obj.len > 0 {
1328 cached_init_calls << '__v2_cached_init_${v2compiler_cache_name}'
1329 }
1330 for cache_name in optional_cached_cache_names {
1331 cached_init_calls << '__v2_cached_init_${cache_name}'
1332 }
1333 all_main_emit_files := if virtual_source_files.len > 0 {
1334 filter_out_source_files(b.module_source_files(main_modules), virtual_source_files)
1335 } else {
1336 []string{}
1337 }
1338 use_self_main_cache := b.should_skip_markused_for_self_build() && !b.pref.keep_c
1339 self_main_obj := cache_path_join(cache_dir, 'main.o')
1340 self_main_c := cache_path_join(cache_dir, 'main.c')
1341 self_main_stamp := cache_path_join(cache_dir, 'main.stamp')
1342 self_main_expected_stamp := if use_self_main_cache {
1343 b.cache_stamp_for_self_main_object(main_modules, all_main_emit_files, linked_cache_names,
1344 main_cc, main_cc_flags, main_cc_link_flags, cached_init_calls)
1345 } else {
1346 ''
1347 }
1348 if use_self_main_cache && os.exists(self_main_obj) && os.exists(self_main_stamp) {
1349 if current_stamp := os.read_file(self_main_stamp) {
1350 if current_stamp == self_main_expected_stamp {
1351 print_time('C Gen', sw.elapsed())
1352 if os.getenv('V2VERBOSE') != '' {
1353 println('[*] Reusing ${self_main_obj}')
1354 }
1355 b.link_cleanc_cached_core_executable(output_name, main_cc, main_cc_flags,
1356 main_cc_link_flags, self_main_obj, builtin_obj, vlib_obj, v2compiler_obj,
1357 optional_cached_objs, virtuals_obj, mut sw)
1358 if os.getenv('V2_TRACE_CACHE') != '' {
1359 eprintln('TRACE_CACHE cached_core=true main_obj_cache=true')
1360 }
1361 return true
1362 }
1363 }
1364 }
1365 mut main_source := if all_main_emit_files.len > 0 {
1366 b.gen_cleanc_source_with_cache_init_calls_files_force(main_modules, all_main_emit_files,
1367 cached_init_calls, true, []string{})
1368 } else {
1369 b.gen_cleanc_source_with_cache_init_calls(main_modules, cached_init_calls)
1370 }
1371 print_time('C Gen', sw.elapsed())
1372 if main_source == '' {
1373 if os.getenv('V2_TRACE_CACHE') != '' {
1374 eprintln('TRACE_CACHE cached_core=false reason=empty_main_source')
1375 }
1376 return false
1377 }
1378
1379 main_c_file := if use_self_main_cache { self_main_c } else { b.exec_build_c_file(output_name) }
1380 os.write_file(main_c_file, main_source) or { return false }
1381 if !use_self_main_cache && main_c_file != staged_c_file {
1382 os.write_file(staged_c_file, main_source) or { return false }
1383 }
1384 println('[*] Wrote ${main_c_file}')
1385
1386 main_tmp_obj := cache_path_join(cache_dir, 'main.tmp.o')
1387 main_obj := if use_self_main_cache { main_tmp_obj } else { staged_main_obj_file }
1388 compile_main_cmd := '${main_cc} ${main_cc_flags} -w -Wno-incompatible-function-pointer-types -c "${main_c_file}" -o "${main_obj}"${error_limit_flag}'
1389 main_fell_back := run_cc_cmd_or_exit(compile_main_cmd, 'C compilation', b.pref.show_cc)
1390 if main_fell_back && main_cc.contains('tcc') {
1391 // TCC failed on main.c but cached .o files are ELF (from TCC).
1392 // Fallback produced Mach-O main.o — can't link with ELF cache.
1393 // Fall back to non-cached full compilation.
1394 os.rm(main_obj) or {}
1395 if os.getenv('V2_TRACE_CACHE') != '' {
1396 eprintln('TRACE_CACHE cached_core=false reason=main_compile_fell_back')
1397 }
1398 return false
1399 }
1400 mut link_main_obj := main_obj
1401 if use_self_main_cache {
1402 os.rm(self_main_obj) or {}
1403 os.mv(main_obj, self_main_obj) or { return false }
1404 os.write_file(self_main_stamp, self_main_expected_stamp) or { return false }
1405 link_main_obj = self_main_obj
1406 }
1407 b.link_cleanc_cached_core_executable(output_name, main_cc, main_cc_flags, main_cc_link_flags,
1408 link_main_obj, builtin_obj, vlib_obj, v2compiler_obj, optional_cached_objs, virtuals_obj, mut
1409 sw)
1410
1411 if !b.pref.keep_c && !use_self_main_cache {
1412 os.rm(main_obj) or {}
1413 os.rm(main_c_file) or {}
1414 }
1415 if os.getenv('V2_TRACE_CACHE') != '' {
1416 eprintln('TRACE_CACHE cached_core=true')
1417 }
1418 return true
1419}
1420
1421fn (mut b Builder) try_link_cached_self_main_object(output_name string, cache_dir string, cc string, cc_flags string, cc_link_flags string, mut sw time.StopWatch) bool {
1422 if !b.should_skip_markused_for_self_build() || b.pref.keep_c {
1423 return false
1424 }
1425 linked_cache_names := [builtin_cache_name, vlib_cache_name, v2compiler_cache_name,
1426 imports_cache_name]
1427 for cache_name in linked_cache_names {
1428 stamp_path := cache_path_join(cache_dir, '${cache_name}.stamp')
1429 if !os.exists(cache_path_join(cache_dir, '${cache_name}.o')) || !os.exists(stamp_path) {
1430 return false
1431 }
1432 stamp := os.read_file(stamp_path) or { return false }
1433 if !stamp_file_lines_are_fresh(stamp) {
1434 return false
1435 }
1436 }
1437 self_main_obj := cache_path_join(cache_dir, 'main.o')
1438 self_main_stamp := cache_path_join(cache_dir, 'main.stamp')
1439 if !os.exists(self_main_obj) || !os.exists(self_main_stamp) {
1440 return false
1441 }
1442 cached_compile_flags, cached_link_flags := b.cached_module_stamp_flags(linked_cache_names)
1443 main_cc := cc
1444 main_cc_flags := join_flag_strings(cc_flags, cached_compile_flags)
1445 main_cc_link_flags := join_flag_strings(cc_link_flags, cached_link_flags)
1446 if main_cc.contains('tcc') {
1447 builtin_obj := cache_path_join(cache_dir, '${builtin_cache_name}.o')
1448 bytes := os.read_bytes(builtin_obj) or { return false }
1449 is_elf := bytes.len >= 4 && bytes[0] == 0x7f && bytes[1] == 0x45 && bytes[2] == 0x4c
1450 && bytes[3] == 0x46
1451 if !is_elf {
1452 return false
1453 }
1454 }
1455 cached_init_calls := [
1456 '__v2_cached_init_${builtin_cache_name}',
1457 '__v2_cached_init_${vlib_cache_name}',
1458 '__v2_cached_init_${v2compiler_cache_name}',
1459 '__v2_cached_init_${imports_cache_name}',
1460 ]
1461 expected_stamp := b.cache_stamp_for_self_main_object(['main'], []string{}, linked_cache_names,
1462 main_cc, main_cc_flags, main_cc_link_flags, cached_init_calls)
1463 current_stamp := os.read_file(self_main_stamp) or { return false }
1464 if current_stamp != expected_stamp {
1465 return false
1466 }
1467 print_time('C Gen', sw.elapsed())
1468 if os.getenv('V2VERBOSE') != '' {
1469 println('[*] Reusing ${self_main_obj}')
1470 }
1471 b.link_cleanc_cached_core_executable(output_name, main_cc, main_cc_flags, main_cc_link_flags,
1472 self_main_obj, cache_path_join(cache_dir, '${builtin_cache_name}.o'), cache_path_join(cache_dir,
1473 '${vlib_cache_name}.o'), cache_path_join(cache_dir, '${v2compiler_cache_name}.o'), [
1474 cache_path_join(cache_dir, '${imports_cache_name}.o'),
1475 ], '', mut sw)
1476 if os.getenv('V2_TRACE_CACHE') != '' {
1477 eprintln('TRACE_CACHE cached_core=true main_obj_cache=early')
1478 }
1479 return true
1480}
1481
1482fn (mut b Builder) link_cleanc_cached_core_executable(output_name string, main_cc string, main_cc_flags string, main_cc_link_flags string, main_obj string, builtin_obj string, vlib_obj string, v2compiler_obj string, optional_cached_objs []string, virtuals_obj string, mut sw time.StopWatch) {
1483 cc_start := sw.elapsed()
1484 // Strip -c and -x flags from link command since we're linking, not compiling.
1485 // -x objective-c would cause cc to treat .o files as source code.
1486 mut link_flags :=
1487 main_cc_flags.replace('-x objective-c', '').replace('-x c', '').replace(' -c ', ' ')
1488 mut link_cmd := '${main_cc} ${link_flags} -w "${main_obj}" "${builtin_obj}"'
1489 if vlib_obj.len > 0 {
1490 link_cmd += ' "${vlib_obj}"'
1491 }
1492 if v2compiler_obj.len > 0 {
1493 link_cmd += ' "${v2compiler_obj}"'
1494 }
1495 for obj in optional_cached_objs {
1496 link_cmd += ' "${obj}"'
1497 }
1498 if virtuals_obj.len > 0 {
1499 link_cmd += ' "${virtuals_obj}"'
1500 }
1501 link_cmd += ' -o "${output_name}"'
1502 if main_cc_link_flags.len > 0 {
1503 link_cmd += ' ${main_cc_link_flags}'
1504 }
1505 run_cc_cmd_or_exit(link_cmd, 'Linking', b.pref.show_cc)
1506 print_time('CC', time.Duration(sw.elapsed() - cc_start))
1507 println('[*] Compiled ${output_name}')
1508}
1509
1510// ---------------------------------------------------------------------------
1511// Durable persistent object cache (survives a /tmp obj-cache wipe).
1512// ---------------------------------------------------------------------------
1513// core_cache_dir() lives under os.temp_dir() and is what
1514// `rm -rf /tmp/v2_cleanc_obj_cache_<root>` removes for a "cold" measurement. A
1515// durable mirror under os.cache_dir() survives that wipe, so a cold self-host
1516// build with UNCHANGED sources can restore the prebuilt bundle objects + main.o
1517// and fast-relink instead of regenerating ~14MB of C. Correctness is enforced
1518// entirely by the existing stamp checks (try_self_build_fast_relink below): a
1519// restored object whose recorded source mtimes no longer match is ignored and
1520// rebuilt, so the durable tier can never yield a stale binary.
1521
1522const self_build_persist_bundles = ['builtin', 'vlib', 'v2compiler', 'imports']
1523
1524fn self_build_persist_file_names() []string {
1525 mut names := []string{cap: self_build_persist_bundles.len * 2 + 2}
1526 for name in self_build_persist_bundles {
1527 names << '${name}.o'
1528 names << '${name}.stamp'
1529 }
1530 names << 'main.o'
1531 names << 'main.stamp'
1532 return names
1533}
1534
1535fn (b &Builder) durable_object_cache_dir() string {
1536 root := if b.pref.vroot.len > 0 { b.pref.vroot } else { os.getwd() }
1537 root_key := sanitize_cache_part(os.norm_path(os.abs_path(root)))
1538 base := if b.pref.is_prod { 'v2cleanc_persist_prod' } else { 'v2cleanc_persist' }
1539 return os.join_path(os.cache_dir(), base, root_key)
1540}
1541
1542// copy_file_keep_mtime copies src->dst preserving src's mtime. Mtime preservation is
1543// required because main.stamp records `dependency:<bundle>:<mtime of that .stamp file>`,
1544// so a restored bundle .stamp must keep its original mtime or the relink probe misses.
1545fn copy_file_keep_mtime(src string, dst string) ! {
1546 data := os.read_bytes(src)!
1547 os.write_file_array(dst, data)!
1548 mtime := os.file_last_mod_unix(src)
1549 os.utime(dst, mtime, mtime)!
1550}
1551
1552fn (b &Builder) save_durable_object_cache(cache_dir string) {
1553 if !b.is_cmd_v2_self_build() || b.pref.no_cache || b.pref.keep_c {
1554 return
1555 }
1556 durable_dir := b.durable_object_cache_dir()
1557 os.mkdir_all(durable_dir, mode: 0o700) or { return }
1558 for name in self_build_persist_file_names() {
1559 src := cache_path_join(cache_dir, name)
1560 if !os.exists(src) {
1561 continue
1562 }
1563 copy_file_keep_mtime(src, cache_path_join(durable_dir, name)) or { continue }
1564 }
1565}
1566
1567fn (b &Builder) restore_durable_object_cache(cache_dir string) {
1568 if !b.is_cmd_v2_self_build() || b.pref.no_cache {
1569 return
1570 }
1571 durable_dir := b.durable_object_cache_dir()
1572 if !os.is_dir(durable_dir) {
1573 return
1574 }
1575 for name in self_build_persist_file_names() {
1576 dst := cache_path_join(cache_dir, name)
1577 if os.exists(dst) {
1578 // A /tmp copy already exists — never overwrite it with the durable mirror.
1579 continue
1580 }
1581 src := cache_path_join(durable_dir, name)
1582 if !os.exists(src) {
1583 continue
1584 }
1585 copy_file_keep_mtime(src, dst) or { continue }
1586 }
1587}
1588
1589// try_self_build_fast_relink is the PRE-PARSE fast path for cmd/v2 self-host. When the
1590// cached bundle objects + main.o are present (restored from the durable tier if /tmp was
1591// wiped) and every source/compiler file they were built from is still fresh, it relinks the
1592// final binary directly and skips the entire front-end (parse + type-check + transform),
1593// which otherwise runs unconditionally. Conservative by construction: any staleness fails a
1594// freshness check and falls through to a normal build, so it can never emit a stale binary.
1595fn (mut b Builder) try_self_build_fast_relink() bool {
1596 trace := os.getenv('V2_TRACE_CACHE') != ''
1597 if b.pref.backend != .cleanc || b.pref.no_cache || b.pref.keep_c || b.pref.skip_builtin {
1598 return false
1599 }
1600 if !b.is_cmd_v2_self_build() || !b.should_skip_markused_for_self_build()
1601 || b.should_disable_cleanc_cache() {
1602 if trace {
1603 eprintln('TRACE_CACHE fast_relink=false reason=guard self_build=${b.is_cmd_v2_self_build()} skip_mu=${b.should_skip_markused_for_self_build()} disable=${b.should_disable_cleanc_cache()}')
1604 }
1605 return false
1606 }
1607 // Mirror gen_cleanc()'s generation-only decision: a `.c` output, a target we
1608 // cannot compile locally, or a shared lib must go through normal C generation,
1609 // never a direct relink. is_cmd_v2_self_build() keys only on the input file, so
1610 // without this a warm-cache `-o foo.c cmd/v2/v2.v` would link an executable into
1611 // foo.c instead of writing C source.
1612 output_name := if b.pref.output_file != '' {
1613 b.pref.output_file
1614 } else if b.user_files.len > 0 {
1615 b.default_output_name()
1616 } else {
1617 'out'
1618 }
1619 if b.fast_relink_output_is_generation_only(output_name) {
1620 if trace {
1621 eprintln('TRACE_CACHE fast_relink=false reason=generation_only out=${output_name}')
1622 }
1623 return false
1624 }
1625 if !b.ensure_core_cache_dir() {
1626 return false
1627 }
1628 cache_dir := b.core_cache_dir()
1629 b.restore_durable_object_cache(cache_dir)
1630
1631 // Every bundle object + stamp must exist and be source-fresh.
1632 for name in self_build_persist_bundles {
1633 if !os.exists(cache_path_join(cache_dir, '${name}.o')) {
1634 if trace {
1635 eprintln('TRACE_CACHE fast_relink=false reason=missing_obj ${name}')
1636 }
1637 return false
1638 }
1639 stamp := os.read_file(cache_path_join(cache_dir, '${name}.stamp')) or { return false }
1640 if !stamp_file_lines_are_fresh(stamp) {
1641 if trace {
1642 eprintln('TRACE_CACHE fast_relink=false reason=stale_bundle ${name}')
1643 }
1644 return false
1645 }
1646 }
1647 main_obj := cache_path_join(cache_dir, 'main.o')
1648 main_stamp := os.read_file(cache_path_join(cache_dir, 'main.stamp')) or { return false }
1649 if !os.exists(main_obj) || !stamp_file_lines_are_fresh(main_stamp) {
1650 if trace {
1651 eprintln('TRACE_CACHE fast_relink=false reason=stale_main main_obj=${os.exists(main_obj)}')
1652 }
1653 return false
1654 }
1655 // Validate the non-file build keys + dependency-stamp mtimes, and read back the relink
1656 // flags. The recorded flags are trusted rather than recomputed (recomputing needs the
1657 // parsed AST); the mtime-freshness checks are what guarantee the binary is up to date.
1658 mut main_cc := ''
1659 mut main_cc_flags := ''
1660 mut main_cc_link_flags := ''
1661 mut saw_self_build := false
1662 mut saw_flag_fp := false
1663 cur_flag_fp := b.preparse_flag_fingerprint()
1664 for line in main_stamp.split_into_lines() {
1665 if line.starts_with('cc=') {
1666 main_cc = line['cc='.len..]
1667 } else if line.starts_with('cc_flags=') {
1668 main_cc_flags = line['cc_flags='.len..]
1669 } else if line.starts_with('cc_link_flags=') {
1670 main_cc_link_flags = line['cc_link_flags='.len..]
1671 } else if line.starts_with('flag_fp=') {
1672 // The recorded cc/cc_flags/cc_link_flags are trusted (recomputing the
1673 // source-derived parts needs the AST). This fingerprint covers the
1674 // flag inputs that DON'T need parsing — compiler choice, prod/shared
1675 // mode, env CFLAGS — so a changed build environment invalidates the
1676 // relink even when every source file is unchanged.
1677 if line['flag_fp='.len..] != cur_flag_fp {
1678 if trace {
1679 eprintln('TRACE_CACHE fast_relink=false reason=flag_fp')
1680 }
1681 return false
1682 }
1683 saw_flag_fp = true
1684 } else if line.starts_with('context_alloc=') {
1685 if line['context_alloc='.len..] != '${b.pref.use_context_allocator}' {
1686 return false
1687 }
1688 } else if line.starts_with('target_os=') {
1689 if line['target_os='.len..] != b.pref.target_os_or_host() {
1690 return false
1691 }
1692 } else if line == 'self_build=true' {
1693 saw_self_build = true
1694 } else if line.starts_with('dependency:') {
1695 rest := line['dependency:'.len..]
1696 sep := rest.last_index(':') or { return false }
1697 dep_stamp := cache_path_join(cache_dir, '${rest[..sep]}.stamp')
1698 if '${os.file_last_mod_unix(dep_stamp)}' != rest[sep + 1..] {
1699 if trace {
1700 eprintln('TRACE_CACHE fast_relink=false reason=dep_mtime ${rest[..sep]} have=${os.file_last_mod_unix(dep_stamp)} want=${rest[
1701 sep + 1..]}')
1702 }
1703 return false
1704 }
1705 }
1706 }
1707 if !saw_self_build || main_cc.len == 0 || !saw_flag_fp {
1708 if trace {
1709 eprintln('TRACE_CACHE fast_relink=false reason=keys saw_self_build=${saw_self_build} cc=${main_cc} flag_fp=${saw_flag_fp}')
1710 }
1711 return false
1712 }
1713 mut sw := time.new_stopwatch()
1714 b.link_cleanc_cached_core_executable(output_name, main_cc, main_cc_flags, main_cc_link_flags,
1715 main_obj, cache_path_join(cache_dir, 'builtin.o'), cache_path_join(cache_dir, 'vlib.o'), cache_path_join(cache_dir,
1716 'v2compiler.o'), [cache_path_join(cache_dir, 'imports.o')], '', mut sw)
1717 if os.getenv('V2_TRACE_CACHE') != '' {
1718 eprintln('TRACE_CACHE self_build_fast_relink=true')
1719 }
1720 return true
1721}
1722
1723fn (b &Builder) cache_stamp_for_self_main_object(main_modules []string, emit_files []string, linked_cache_names []string, main_cc string, main_cc_flags string, main_cc_link_flags string, cached_init_calls []string) string {
1724 source_files := b.module_source_files(main_modules)
1725 dependency_lines := b.cache_dependency_stamp_lines(linked_cache_names)
1726 mut lines := []string{cap: source_files.len + emit_files.len + dependency_lines.len +
1727 cached_init_calls.len + 12}
1728 lines << 'cache=main'
1729 lines << 'format=${core_cache_format}'
1730 lines << 'cc=${main_cc}'
1731 lines << 'cc_flags=${main_cc_flags}'
1732 lines << 'cc_link_flags=${main_cc_link_flags}'
1733 lines << 'flag_fp=${b.preparse_flag_fingerprint()}'
1734 lines << 'context_alloc=${b.pref.use_context_allocator}'
1735 lines << 'target_os=${b.pref.target_os_or_host()}'
1736 lines << 'self_build=true'
1737 exe := os.executable()
1738 lines << 'compiler_exe:${exe}:${os.file_last_mod_unix(exe)}'
1739 for module_name in main_modules {
1740 lines << 'module:${module_name}'
1741 }
1742 for init_call in cached_init_calls {
1743 lines << 'init:${init_call}'
1744 }
1745 lines << dependency_lines
1746 for file in b.user_entry_stamp_files() {
1747 lines << 'entry:${file}:${os.file_last_mod_unix(file)}'
1748 }
1749 for file in source_files {
1750 lines << 'source:${file}:${os.file_last_mod_unix(file)}'
1751 }
1752 for file in emit_files {
1753 lines << 'emit:${file}:${os.file_last_mod_unix(file)}'
1754 }
1755 return lines.join('\n')
1756}
1757
1758fn (b &Builder) cached_module_stamp_flags(cache_names []string) (string, string) {
1759 cache_dir := b.core_cache_dir()
1760 mut compile_flags := ''
1761 mut link_flags := ''
1762 for cache_name in cache_names {
1763 stamp_path := os.join_path(cache_dir, '${cache_name}.stamp')
1764 stamp := os.read_file(stamp_path) or { continue }
1765 for line in stamp.split_into_lines() {
1766 if line.starts_with('cc_flags=') {
1767 compile_flags = join_flag_strings(compile_flags, line['cc_flags='.len..])
1768 } else if line.starts_with('cc_link_flags=') {
1769 link_flags = join_flag_strings(link_flags, line['cc_link_flags='.len..])
1770 }
1771 }
1772 }
1773 return compile_flags, link_flags
1774}
1775
1776fn join_flag_strings(a string, b string) string {
1777 mut joined := []string{}
1778 for flags in [a, b] {
1779 for item in flag_string_items(flags) {
1780 if item !in joined {
1781 joined << item
1782 }
1783 }
1784 }
1785 return joined.join(' ')
1786}
1787
1788fn flag_string_items(flags string) []string {
1789 tokens := flags.fields()
1790 mut items := []string{}
1791 mut i := 0
1792 for i < tokens.len {
1793 tok := tokens[i]
1794 if tok in ['-I', '-L', '-l', '-F', '-framework', '-isystem', '-include']
1795 && i + 1 < tokens.len {
1796 items << '${tok} ${tokens[i + 1]}'
1797 i += 2
1798 continue
1799 }
1800 items << tok
1801 i++
1802 }
1803 return items
1804}
1805
1806fn (mut b Builder) ensure_cached_module_object(cache_dir string, cache_name string, module_paths []string, emit_modules []string, cc string, cc_flags string, cc_link_flags string, error_limit_flag string, use_markused bool) !string {
1807 obj_path := cache_path_join(cache_dir, '${cache_name}.o')
1808 stamp_path := cache_path_join(cache_dir, '${cache_name}.stamp')
1809 c_path := cache_path_join(cache_dir, '${cache_name}.c')
1810 expected_stamp := b.cache_stamp_for_modules(cache_name, module_paths, cc, cc_flags,
1811 cc_link_flags, use_markused)
1812 if os.exists(obj_path) && os.exists(stamp_path) {
1813 if current_stamp := os.read_file(stamp_path) {
1814 if current_stamp == expected_stamp {
1815 if b.load_cached_called_fn_names(cache_dir, cache_name) {
1816 if os.getenv('V2VERBOSE') != '' {
1817 println('[*] Reusing ${obj_path}')
1818 }
1819 return obj_path
1820 }
1821 }
1822 }
1823 }
1824 if b.used_vh_for_parse {
1825 if os.exists(obj_path) && os.exists(stamp_path) {
1826 if b.load_cached_called_fn_names(cache_dir, cache_name) {
1827 return obj_path
1828 }
1829 return error('missing cached ${cache_name} call metadata for .vh parse')
1830 }
1831 return error('missing cached ${cache_name} object for .vh parse')
1832 }
1833
1834 stats_enabled := b.cgen_builder_stats_enabled()
1835 mut stats_sw := time.new_stopwatch()
1836 mut stage_start := stats_sw.elapsed()
1837 cached_called_before := b.cached_called_fn_names.clone()
1838 stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
1839 'called_before_clone')
1840 module_source := b.gen_cleanc_source_for_cache(emit_modules, cache_name, use_markused)
1841 stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
1842 'source')
1843 if module_source == '' {
1844 return error('failed to generate C source for ${cache_name}')
1845 }
1846 os.write_file(c_path, module_source)!
1847 stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
1848 'write_c')
1849
1850 compile_cmd := '${cc} ${cc_flags} -w -Wno-incompatible-function-pointer-types -c "${c_path}" -o "${obj_path}"${error_limit_flag}'
1851 run_cc_cmd_or_exit(compile_cmd, 'C compilation', b.pref.show_cc)
1852 stage_start =
1853 b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start, 'cc')
1854 b.write_cached_called_fn_names(cache_dir, cache_name, cached_called_before)
1855 os.write_file(stamp_path, expected_stamp)!
1856 _ = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start, 'metadata')
1857 return obj_path
1858}
1859
1860fn (mut b Builder) ensure_cached_parsed_module_object(cache_dir string, cache_name string, module_names []string, dependency_cache_names []string, cc string, cc_flags string, cc_link_flags string, error_limit_flag string, use_markused bool) !string {
1861 obj_path := cache_path_join(cache_dir, '${cache_name}.o')
1862 stamp_path := cache_path_join(cache_dir, '${cache_name}.stamp')
1863 c_path := cache_path_join(cache_dir, '${cache_name}.c')
1864 expected_stamp := b.cache_stamp_for_parsed_modules(cache_name, module_names,
1865 dependency_cache_names, cc, cc_flags, cc_link_flags, use_markused)
1866 if os.exists(obj_path) && os.exists(stamp_path) {
1867 if current_stamp := os.read_file(stamp_path) {
1868 if current_stamp == expected_stamp {
1869 if b.load_cached_called_fn_names(cache_dir, cache_name) {
1870 if os.getenv('V2VERBOSE') != '' {
1871 println('[*] Reusing ${obj_path}')
1872 }
1873 return obj_path
1874 }
1875 }
1876 }
1877 }
1878 if b.used_import_vh_for_parse {
1879 if os.exists(obj_path) && os.exists(stamp_path) {
1880 if b.load_cached_called_fn_names(cache_dir, cache_name) {
1881 return obj_path
1882 }
1883 return error('missing cached ${cache_name} call metadata for .vh parse')
1884 }
1885 return error('missing cached ${cache_name} object for .vh parse')
1886 }
1887
1888 stats_enabled := b.cgen_builder_stats_enabled()
1889 mut stats_sw := time.new_stopwatch()
1890 mut stage_start := stats_sw.elapsed()
1891 cached_called_before := b.cached_called_fn_names.clone()
1892 stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
1893 'called_before_clone')
1894 mut module_source := b.gen_cleanc_source_for_cache(module_names, cache_name, use_markused)
1895 stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
1896 'source')
1897 if module_source == '' {
1898 return error('failed to generate C source for ${cache_name}')
1899 }
1900 os.write_file(c_path, module_source)!
1901 stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
1902 'write_c')
1903
1904 compile_cmd := '${cc} ${cc_flags} -w -Wno-incompatible-function-pointer-types -c "${c_path}" -o "${obj_path}"${error_limit_flag}'
1905 run_cc_cmd_or_exit(compile_cmd, 'C compilation', b.pref.show_cc)
1906 stage_start =
1907 b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start, 'cc')
1908 b.write_cached_called_fn_names(cache_dir, cache_name, cached_called_before)
1909 os.write_file(stamp_path, expected_stamp)!
1910 _ = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start, 'metadata')
1911 return obj_path
1912}
1913
1914fn (mut b Builder) ensure_cached_virtual_module_object(cache_dir string, groups []CachedVirtualModule, dependency_cache_names []string, cc string, cc_flags string, cc_link_flags string, error_limit_flag string, use_markused bool) !string {
1915 obj_path := cache_path_join(cache_dir, '${virtuals_cache_name}.o')
1916 stamp_path := cache_path_join(cache_dir, '${virtuals_cache_name}.stamp')
1917 c_path := cache_path_join(cache_dir, '${virtuals_cache_name}.c')
1918 expected_stamp := b.cache_stamp_for_virtual_modules(groups, dependency_cache_names, cc,
1919 cc_flags, cc_link_flags, use_markused)
1920 if os.exists(obj_path) && os.exists(stamp_path) {
1921 if current_stamp := os.read_file(stamp_path) {
1922 if current_stamp == expected_stamp {
1923 if b.load_cached_called_fn_names(cache_dir, virtuals_cache_name) {
1924 if os.getenv('V2VERBOSE') != '' {
1925 println('[*] Reusing ${obj_path}')
1926 }
1927 return obj_path
1928 }
1929 }
1930 }
1931 }
1932 if b.used_virtual_vh_for_parse {
1933 if os.exists(obj_path) && os.exists(stamp_path) {
1934 if b.load_cached_called_fn_names(cache_dir, virtuals_cache_name) {
1935 return obj_path
1936 }
1937 return error('missing cached ${virtuals_cache_name} call metadata for .vh parse')
1938 }
1939 return error('missing cached ${virtuals_cache_name} object for .vh parse')
1940 }
1941
1942 emit_files := virtual_module_source_files(groups)
1943 stats_enabled := b.cgen_builder_stats_enabled()
1944 mut stats_sw := time.new_stopwatch()
1945 mut stage_start := stats_sw.elapsed()
1946 cached_called_before := b.cached_called_fn_names.clone()
1947 stage_start = b.mark_cgen_builder_step(stats_enabled, virtuals_cache_name, mut stats_sw,
1948 stage_start, 'called_before_clone')
1949 mut module_source := b.gen_cleanc_source_for_cache_files(['main'], emit_files,
1950 virtuals_cache_name, use_markused)
1951 stage_start = b.mark_cgen_builder_step(stats_enabled, virtuals_cache_name, mut stats_sw,
1952 stage_start, 'source')
1953 if module_source == '' {
1954 return error('failed to generate C source for ${virtuals_cache_name}')
1955 }
1956 os.write_file(c_path, module_source)!
1957 stage_start = b.mark_cgen_builder_step(stats_enabled, virtuals_cache_name, mut stats_sw,
1958 stage_start, 'write_c')
1959
1960 compile_cmd := '${cc} ${cc_flags} -w -Wno-incompatible-function-pointer-types -c "${c_path}" -o "${obj_path}"${error_limit_flag}'
1961 run_cc_cmd_or_exit(compile_cmd, 'C compilation', b.pref.show_cc)
1962 stage_start = b.mark_cgen_builder_step(stats_enabled, virtuals_cache_name, mut stats_sw,
1963 stage_start, 'cc')
1964 b.write_cached_called_fn_names(cache_dir, virtuals_cache_name, cached_called_before)
1965 os.write_file(stamp_path, expected_stamp)!
1966 _ = b.mark_cgen_builder_step(stats_enabled, virtuals_cache_name, mut stats_sw, stage_start,
1967 'metadata')
1968 return obj_path
1969}
1970
1971// flat_file_module_name returns the normalized module name of the i-th flat
1972// file, mirroring `ast_file_module_name` for the flat-AST path: the raw module
1973// string with `.` replaced by `_`, defaulting to `main`. Used by the cleanc
1974// cached-core enumeration helpers when `b.files` has been dropped in favour of
1975// the post-transform FlatAst.
1976fn (b &Builder) flat_file_module_name(i int) string {
1977 mod := b.flat.string_at(b.flat.files[i].mod_idx)
1978 if mod == '' {
1979 return 'main'
1980 }
1981 return mod.replace('.', '_')
1982}
1983
1984// uses_flat_module_enumeration reports whether module/file enumeration must run
1985// against `b.flat` instead of `b.files`. In flat-codegen mode the transformer
1986// drops `b.files` after producing the post-transform FlatAst, so the cleanc
1987// cache helpers source their module/file metadata from the flat cursors.
1988fn (b &Builder) uses_flat_module_enumeration() bool {
1989 return b.files.len == 0 && b.flat.files.len > 0
1990}
1991
1992// flat_scoped_to_modules returns a FlatAst whose file list is restricted to the
1993// given module set, reusing b.flat's node/edge/string arena (the arrays are
1994// shared, not deep-copied). The cleanc gen drives every emission pass off
1995// `flat.files`, so a restricted file list makes the whole gen see exactly the
1996// bundle's type-module files — matching the legacy gen, which filtered its input
1997// `gen_files` to `type_module_names`. This lets restricted cache bundles run on
1998// the flat gen with no per-file legacy rehydrate.
1999fn (b &Builder) flat_scoped_to_modules(module_names map[string]bool) ast.FlatAst {
2000 mut files := []ast.FlatFile{cap: b.flat.files.len}
2001 for i in 0 .. b.flat.files.len {
2002 if b.flat_file_module_name(i) in module_names {
2003 files << b.flat.files[i]
2004 }
2005 }
2006 return ast.FlatAst{
2007 files: files
2008 nodes: b.flat.nodes
2009 edges: b.flat.edges
2010 strings: b.flat.strings
2011 }
2012}
2013
2014fn (b &Builder) has_module(module_name string) bool {
2015 if b.uses_flat_module_enumeration() {
2016 for i in 0 .. b.flat.files.len {
2017 if b.flat_file_module_name(i) == module_name {
2018 return true
2019 }
2020 }
2021 return false
2022 }
2023 for file in b.files {
2024 if ast_file_module_name(file) == module_name {
2025 return true
2026 }
2027 }
2028 return false
2029}
2030
2031fn (b &Builder) collect_modules_excluding(excluded []string) []string {
2032 mut excluded_set := map[string]bool{}
2033 for module_name in excluded {
2034 excluded_set[module_name] = true
2035 }
2036 mut modules_set := map[string]bool{}
2037 if b.uses_flat_module_enumeration() {
2038 for i in 0 .. b.flat.files.len {
2039 module_name := b.flat_file_module_name(i)
2040 if module_name in excluded_set {
2041 continue
2042 }
2043 modules_set[module_name] = true
2044 }
2045 } else {
2046 for file in b.files {
2047 module_name := ast_file_module_name(file)
2048 if module_name in excluded_set {
2049 continue
2050 }
2051 modules_set[module_name] = true
2052 }
2053 }
2054 mut modules := modules_set.keys()
2055 modules.sort()
2056 return modules
2057}
2058
2059fn (b &Builder) user_entry_module_names() []string {
2060 mut entry_files := map[string]bool{}
2061 for file in b.user_entry_stamp_files() {
2062 entry_files[os.norm_path(file)] = true
2063 entry_files[os.norm_path(os.abs_path(file))] = true
2064 }
2065 mut modules_set := map[string]bool{}
2066 if b.uses_flat_module_enumeration() {
2067 for i in 0 .. b.flat.files.len {
2068 name := b.flat.file_name(b.flat.files[i])
2069 if name == '' || name.ends_with('.vh') {
2070 continue
2071 }
2072 norm_name := os.norm_path(name)
2073 abs_name := os.norm_path(os.abs_path(name))
2074 if norm_name !in entry_files && abs_name !in entry_files {
2075 continue
2076 }
2077 module_name := b.flat_file_module_name(i)
2078 if module_name.len > 0 {
2079 modules_set[module_name] = true
2080 }
2081 }
2082 } else {
2083 for file in b.files {
2084 if file.name == '' || file.name.ends_with('.vh') {
2085 continue
2086 }
2087 norm_name := os.norm_path(file.name)
2088 abs_name := os.norm_path(os.abs_path(file.name))
2089 if norm_name !in entry_files && abs_name !in entry_files {
2090 continue
2091 }
2092 module_name := ast_file_module_name(file)
2093 if module_name.len > 0 {
2094 modules_set[module_name] = true
2095 }
2096 }
2097 }
2098 mut modules := modules_set.keys()
2099 modules.sort()
2100 return modules
2101}
2102
2103fn (b &Builder) print_cached_bundle_modules(cache_name string, module_names []string) {
2104 if module_names.len == 0 {
2105 return
2106 }
2107 stats_enabled := b.pref != unsafe { nil } && b.pref.stats
2108 show_cc_enabled := b.pref != unsafe { nil } && b.pref.show_cc
2109 if !stats_enabled && !show_cc_enabled && os.getenv('V2_TRACE_CACHE') == '' {
2110 return
2111 }
2112 if stats_enabled {
2113 println(' * Cached ${cache_name} modules: ${module_names.len}')
2114 for module_name in module_names {
2115 println(' [cache ${cache_name}] ${module_name}')
2116 }
2117 return
2118 }
2119 println('[*] Cached ${cache_name} modules: ${module_names.join(', ')}')
2120}
2121
2122fn ast_file_module_name(file ast.File) string {
2123 for stmt in file.stmts {
2124 if stmt is ast.ModuleStmt {
2125 return stmt.name.replace('.', '_')
2126 }
2127 }
2128 return 'main'
2129}
2130
2131fn normalize_target_os_name(target_os string) string {
2132 return match target_os.to_lower() {
2133 'darwin', 'mac' { 'macos' }
2134 else { target_os.to_lower() }
2135 }
2136}
2137
2138fn is_windows_x64_native_target(arch pref.Arch, target_os string) bool {
2139 return arch == .x64 && normalize_target_os_name(target_os) == 'windows'
2140}
2141
2142fn is_linux_x64_native_target(arch pref.Arch, target_os string) bool {
2143 return arch == .x64 && normalize_target_os_name(target_os) == 'linux'
2144}
2145
2146fn is_macos_x64_native_target(arch pref.Arch, target_os string) bool {
2147 return arch == .x64 && is_macos_native_target(target_os)
2148}
2149
2150fn eprint_native_x64_link_error(message string) {
2151 if message.starts_with('x64: unsupported backend feature: ') {
2152 eprintln(message)
2153 eprintln(x64.x64_backend_limitation_hint)
2154 return
2155 }
2156 eprintln('Link failed:')
2157 eprintln(message)
2158}
2159
2160fn (b &Builder) uses_minimal_windows_x64_runtime() bool {
2161 arch := b.pref.get_effective_arch()
2162 return b.pref.backend == .x64 && is_windows_x64_native_target(arch, b.pref.target_os_or_host())
2163}
2164
2165fn (b &Builder) uses_minimal_linux_x64_runtime() bool {
2166 arch := b.pref.get_effective_arch()
2167 return b.pref.backend == .x64 && is_linux_x64_native_target(arch, b.pref.target_os_or_host())
2168}
2169
2170fn (b &Builder) uses_minimal_x64_runtime() bool {
2171 return b.uses_minimal_windows_x64_runtime() || b.uses_minimal_linux_x64_runtime()
2172}
2173
2174fn linux_x64_tiny_strict_enabled() bool {
2175 return os.getenv('V2_X64_LINUX_TINY') != ''
2176}
2177
2178fn (b &Builder) uses_minimal_linux_x64_runtime_roots() bool {
2179 return b.uses_minimal_linux_x64_runtime() && linux_x64_tiny_strict_enabled()
2180}
2181
2182fn (b &Builder) uses_minimal_x64_runtime_roots() bool {
2183 return b.uses_minimal_windows_x64_runtime() || b.uses_minimal_linux_x64_runtime_roots()
2184}
2185
2186fn (b &Builder) uses_macos_x64_tiny_object(arch pref.Arch) bool {
2187 return b.pref.backend == .x64 && is_macos_x64_native_target(arch, b.pref.target_os_or_host())
2188 && b.pref.macos_tiny
2189}
2190
2191fn native_link_flags_suffix(link_flags string) string {
2192 trimmed := link_flags.trim_space()
2193 if trimmed == '' {
2194 return ''
2195 }
2196 return ' ${trimmed}'
2197}
2198
2199fn macos_native_ld_link_flags(link_flags string) string {
2200 mut normalized := []string{}
2201 tokens := link_flags.fields()
2202 mut i := 0
2203 for i < tokens.len {
2204 tok := tokens[i]
2205 if tok.starts_with('-Wl,') {
2206 for linker_arg in tok['-Wl,'.len..].split(',') {
2207 if linker_arg != '' {
2208 normalized << linker_arg
2209 }
2210 }
2211 i++
2212 continue
2213 }
2214 if tok == '-Xlinker' {
2215 if i + 1 < tokens.len {
2216 i++
2217 normalized << tokens[i]
2218 }
2219 i++
2220 continue
2221 }
2222 normalized << tok
2223 i++
2224 }
2225 return normalized.join(' ')
2226}
2227
2228fn native_driver_flag_is_dual_use(tok string) bool {
2229 return tok == '-pthread' || tok == '-fopenmp' || tok.starts_with('-fopenmp=')
2230}
2231
2232fn macos_native_ld_unsupported_driver_link_flags(link_flags string) []string {
2233 mut unsupported := []string{}
2234 for tok in link_flags.fields() {
2235 if native_driver_flag_is_dual_use(tok) {
2236 unsupported << tok
2237 }
2238 }
2239 return unsupported
2240}
2241
2242fn validate_macos_native_ld_link_flags(link_flags string) ! {
2243 unsupported := macos_native_ld_unsupported_driver_link_flags(link_flags)
2244 if unsupported.len > 0 {
2245 return error('x64: unsupported backend feature: macOS native ld cannot consume driver linker flags: ${unsupported.join(' ')}')
2246 }
2247}
2248
2249fn native_linux_tiny_allows_system_lib(lib string) bool {
2250 return lib in ['pthread', 'm', 'dl', 'c']
2251}
2252
2253fn native_internal_link_flags_allow_builtin_linux_tiny(link_flags string) bool {
2254 tokens := link_flags.trim_space().fields()
2255 mut i := 0
2256 for i < tokens.len {
2257 tok := tokens[i]
2258 if tok == '-l' {
2259 if i + 1 >= tokens.len {
2260 return false
2261 }
2262 i++
2263 if !native_linux_tiny_allows_system_lib(tokens[i]) {
2264 return false
2265 }
2266 } else if tok.starts_with('-l') {
2267 if !native_linux_tiny_allows_system_lib(tok['-l'.len..]) {
2268 return false
2269 }
2270 } else {
2271 return false
2272 }
2273 i++
2274 }
2275 return true
2276}
2277
2278fn native_link_flags_allow_builtin_linux_tiny(link_flags string, user_link_flags string) bool {
2279 return user_link_flags.trim_space() == ''
2280 && native_internal_link_flags_allow_builtin_linux_tiny(link_flags)
2281}
2282
2283fn (b &Builder) native_link_flags_from_sources() string {
2284 _, directive_link_flags := split_compile_and_link_flags(b.collect_cflags_from_sources())
2285 return directive_link_flags.trim_space()
2286}
2287
2288fn (b &Builder) native_compile_and_link_flags_from_sources() (string, string) {
2289 directive_compile_flags, directive_link_flags :=
2290 split_compile_and_link_flags(b.collect_cflags_from_sources())
2291 return directive_compile_flags.trim_space(), directive_link_flags.trim_space()
2292}
2293
2294fn (b &Builder) native_user_compile_and_link_flags_from_sources() (string, string) {
2295 directive_compile_flags, directive_link_flags :=
2296 split_compile_and_link_flags(b.collect_user_cflags_from_sources())
2297 return directive_compile_flags.trim_space(), directive_link_flags.trim_space()
2298}
2299
2300struct NativeExternalLinkInputs {
2301 link_flags string
2302 source_files []string
2303 object_files []string
2304}
2305
2306fn native_external_source_clean_token(tok string) string {
2307 return tok.trim('"').trim("'")
2308}
2309
2310fn native_external_source_is_supported(tok string) bool {
2311 lower := native_external_source_clean_token(tok).to_lower()
2312 return lower.ends_with('.c') || lower.ends_with('.m')
2313}
2314
2315fn native_external_source_is_unsupported(tok string) bool {
2316 lower := native_external_source_clean_token(tok).to_lower()
2317 return lower.ends_with('.cc') || lower.ends_with('.cpp') || lower.ends_with('.cxx')
2318 || lower.ends_with('.mm')
2319}
2320
2321fn native_external_source_unsupported_message(tok string) string {
2322 clean := native_external_source_clean_token(tok)
2323 if clean.to_lower().ends_with('.mm') {
2324 return 'x64: unsupported backend feature: Objective-C++ #flag source `${clean}`'
2325 }
2326 return 'x64: unsupported backend feature: C++ #flag source `${clean}`'
2327}
2328
2329fn native_external_source_object_file(output_binary string, source_index int) string {
2330 output_name := if output_binary == '' { 'out' } else { os.file_name(output_binary) }
2331 clean_name := output_name.replace('/', '_').replace('\\', '_').replace('.', '_')
2332 return os.join_path(os.vtmp_dir(), 'v2_native_${os.getpid()}_${clean_name}_${source_index}.o')
2333}
2334
2335fn native_external_link_inputs(link_flags string, output_binary string) !NativeExternalLinkInputs {
2336 mut rewritten := []string{}
2337 mut source_files := []string{}
2338 mut object_files := []string{}
2339 for tok in link_flags.fields() {
2340 clean := native_external_source_clean_token(tok)
2341 if native_external_source_is_unsupported(clean) {
2342 return error(native_external_source_unsupported_message(clean))
2343 }
2344 if native_external_source_is_supported(clean) {
2345 if tok.contains('"') || tok.contains("'") {
2346 return error('x64: unsupported backend feature: quoted #flag source path `${tok}`')
2347 }
2348 obj_file := native_external_source_object_file(output_binary, source_files.len)
2349 source_files << clean
2350 object_files << obj_file
2351 rewritten << os.quoted_path(obj_file)
2352 continue
2353 }
2354 rewritten << tok
2355 }
2356 return NativeExternalLinkInputs{
2357 link_flags: rewritten.join(' ')
2358 source_files: source_files
2359 object_files: object_files
2360 }
2361}
2362
2363fn native_external_source_compile_command(cc string, source_file string, object_file string, compile_flags string, sdk_path string, arch_flag string, target_os string) string {
2364 mut parts := [cc, '-c']
2365 if is_macos_native_target(target_os) {
2366 if sdk_path != '' {
2367 parts << '-isysroot'
2368 parts << os.quoted_path(sdk_path)
2369 }
2370 if arch_flag != '' {
2371 parts << '-arch'
2372 parts << arch_flag
2373 }
2374 }
2375 if compile_flags != '' {
2376 parts << compile_flags
2377 }
2378 parts << os.quoted_path(source_file)
2379 parts << '-o'
2380 parts << os.quoted_path(object_file)
2381 return parts.join(' ')
2382}
2383
2384fn (b &Builder) native_external_source_compiler(target_os string) string {
2385 if b.pref.ccompiler.len > 0 {
2386 return b.pref.ccompiler
2387 }
2388 v2cc := (os.getenv_opt('V2CC') or { '' }).trim_space()
2389 if v2cc != '' {
2390 return v2cc
2391 }
2392 if is_macos_native_target(target_os) {
2393 return 'cc'
2394 }
2395 return configured_cc(b.pref.vroot)
2396}
2397
2398fn (b &Builder) native_linux_hosted_link_compiler() string {
2399 if b.pref.ccompiler.len > 0 {
2400 return b.pref.ccompiler
2401 }
2402 v2cc := (os.getenv_opt('V2CC') or { '' }).trim_space()
2403 if v2cc != '' {
2404 return v2cc
2405 }
2406 return 'cc'
2407}
2408
2409fn (b &Builder) compile_native_external_sources(inputs NativeExternalLinkInputs, compile_flags string, target_os string, sdk_path string, arch_flag string) ! {
2410 if inputs.source_files.len == 0 {
2411 return
2412 }
2413 cc := b.native_external_source_compiler(target_os)
2414 for i, source_file in inputs.source_files {
2415 cmd := native_external_source_compile_command(cc, source_file, inputs.object_files[i],
2416 compile_flags, sdk_path, arch_flag, target_os)
2417 if b.pref.show_cc {
2418 println(cmd)
2419 }
2420 res := os.execute(cmd)
2421 if res.exit_code != 0 {
2422 return error('native external source compilation failed:\n${cmd}\n${res.output}')
2423 }
2424 }
2425}
2426
2427fn cleanup_native_external_objects(inputs NativeExternalLinkInputs) {
2428 for obj_file in inputs.object_files {
2429 os.rm(obj_file) or {}
2430 }
2431}
2432
2433fn macos_native_link_command(output_binary string, obj_file string, sdk_path string, arch_flag string, tiny_object bool, link_flags string) string {
2434 ld_link_flags := macos_native_ld_link_flags(link_flags)
2435 normal_link_cmd := 'ld -o ${os.quoted_path(output_binary)} ${os.quoted_path(obj_file)}${native_link_flags_suffix(ld_link_flags)} -lSystem -syslibroot ${os.quoted_path(sdk_path)} -e _main -arch ${arch_flag} -platform_version macos 11.0.0 11.0.0'
2436 if tiny_object {
2437 return '${normal_link_cmd} -dead_strip -x -S'
2438 }
2439 return normal_link_cmd
2440}
2441
2442fn macos_sdk_path_from_xcrun_output(output string) !string {
2443 mut sdk_path := ''
2444 for raw_line in output.split_into_lines() {
2445 line := raw_line.trim(' \t\r')
2446 if line == '' {
2447 continue
2448 }
2449 if is_clean_macos_sdk_path_line(line) {
2450 sdk_path = line
2451 }
2452 }
2453 if sdk_path == '' {
2454 return error('could not find a clean macOS SDK path in xcrun output')
2455 }
2456 return sdk_path
2457}
2458
2459fn is_clean_macos_sdk_path_line(path string) bool {
2460 if !os.is_abs_path(path) {
2461 return false
2462 }
2463 base := os.file_name(path)
2464 return base.starts_with('MacOSX') && base.ends_with('.sdk')
2465}
2466
2467fn validate_macos_sdk_path_for_native_link(sdk_path string) ! {
2468 if !os.is_dir(sdk_path) {
2469 return error('macOS SDK path does not exist: ${sdk_path}')
2470 }
2471 libsystem_tbd := os.join_path(sdk_path, 'usr', 'lib', 'libSystem.tbd')
2472 libsystem_dylib := os.join_path(sdk_path, 'usr', 'lib', 'libSystem.dylib')
2473 if !os.exists(libsystem_tbd) && !os.exists(libsystem_dylib) {
2474 return error('macOS SDK path is missing usr/lib/libSystem.tbd or usr/lib/libSystem.dylib: ${sdk_path}')
2475 }
2476}
2477
2478fn linux_native_link_command(cc string, output_binary string, obj_file string, link_flags string) string {
2479 return '${cc} ${os.quoted_path(obj_file)} -o ${os.quoted_path(output_binary)} -no-pie${native_link_flags_suffix(link_flags)}'
2480}
2481
2482fn native_external_object_file(output_binary string, target_os string) string {
2483 if !is_macos_native_target(target_os) {
2484 return 'main.o'
2485 }
2486 output_path := if os.is_abs_path(output_binary) {
2487 output_binary
2488 } else {
2489 os.abs_path(output_binary)
2490 }
2491 output_dir := os.dir(output_path)
2492 output_name := os.file_name(output_path)
2493 if output_name == '' {
2494 return os.join_path(output_dir, '.v_native_${os.getpid()}.o')
2495 }
2496 return os.join_path(output_dir, '.${output_name}.${os.getpid()}.o')
2497}
2498
2499fn (mut b Builder) prepare_macos_tiny_candidate_source_files() {
2500 b.macos_tiny_candidate_source_files = []ast.File{}
2501 b.macos_tiny_candidate_source_flat = ast.FlatAst{}
2502 if !b.uses_macos_x64_tiny_object(.x64) {
2503 return
2504 }
2505 if b.flat.files.len > 0 {
2506 b.macos_tiny_candidate_source_flat = b.flat
2507 return
2508 }
2509 b.macos_tiny_candidate_source_files = b.files.clone()
2510}
2511
2512fn is_macos_native_target(target_os string) bool {
2513 return normalize_target_os_name(target_os) == 'macos'
2514}
2515
2516fn native_x64_object_format_for_os(target_os string) x64.ObjectFormat {
2517 return match normalize_target_os_name(target_os) {
2518 'macos' { x64.ObjectFormat.macho }
2519 'windows' { x64.ObjectFormat.coff }
2520 else { x64.ObjectFormat.elf }
2521 }
2522}
2523
2524fn native_x64_codegen_abi_for_os(target_os string) x64.X64Abi {
2525 return match normalize_target_os_name(target_os) {
2526 'windows' { x64.X64Abi.windows }
2527 else { x64.X64Abi.sysv }
2528 }
2529}
2530
2531fn native_x64_lowering_abi_for_os(target_os string) abi.X64Abi {
2532 return match normalize_target_os_name(target_os) {
2533 'windows' { abi.X64Abi.windows }
2534 else { abi.X64Abi.sysv }
2535 }
2536}
2537
2538fn native_x64_mir_unsupported_external_symbol_message(mir_mod &mir.Module, obj_format x64.ObjectFormat) ?string {
2539 for val in mir_mod.values {
2540 if val.kind != .func_ref {
2541 continue
2542 }
2543 if msg := x64.unsupported_external_symbol_message_for_name(obj_format, val.name,
2544 'needed while preparing native x64 output')
2545 {
2546 return msg
2547 }
2548 }
2549 return none
2550}
2551
2552fn (b &Builder) native_backend_requires_ssa_optimization(arch pref.Arch) bool {
2553 return arch == .x64
2554}
2555
2556fn flag_os_matches(cond string, target_os string) bool {
2557 current := normalize_target_os_name(target_os)
2558 return match cond.to_lower() {
2559 'darwin', 'macos', 'mac' { current == 'macos' || current == 'darwin' }
2560 'linux' { current == 'linux' }
2561 'windows' { current == 'windows' }
2562 'bsd' { current in ['macos', 'freebsd', 'openbsd', 'netbsd', 'dragonfly'] }
2563 'freebsd' { current == 'freebsd' }
2564 'openbsd' { current == 'openbsd' }
2565 'netbsd' { current == 'netbsd' }
2566 'dragonfly' { current == 'dragonfly' }
2567 'android' { current == 'android' }
2568 'termux' { current == 'termux' }
2569 'ios' { current == 'ios' }
2570 'solaris' { current == 'solaris' }
2571 'qnx' { current == 'qnx' }
2572 'serenity' { current == 'serenity' }
2573 'plan9' { current == 'plan9' }
2574 'vinix' { current == 'vinix' }
2575 'cross' { current == 'cross' }
2576 'none' { current == 'none' }
2577 else { false }
2578 }
2579}
2580
2581fn flag_pref_matches(cond string, prefs &pref.Preferences) bool {
2582 if prefs == unsafe { nil } {
2583 return false
2584 }
2585 lower := cond.to_lower()
2586 if pref.comptime_flag_value(prefs, lower) {
2587 return true
2588 }
2589 return flag_os_matches(lower, prefs.target_os_or_host())
2590}
2591
2592fn find_vmod_root_for_file(file_path string) string {
2593 mut dir := os.dir(file_path)
2594 for _ in 0 .. 12 {
2595 if os.exists(os.join_path(dir, 'v.mod')) {
2596 return dir
2597 }
2598 parent := os.dir(dir)
2599 if parent == dir || parent == '' {
2600 break
2601 }
2602 dir = parent
2603 }
2604 return os.dir(file_path)
2605}
2606
2607fn resolve_flag_path(path string, file_dir string, vmod_root string) string {
2608 mut resolved := path.replace('@VMODROOT', vmod_root)
2609 if os.is_abs_path(resolved) {
2610 return resolved
2611 }
2612 // Resolve any relative path (including bare relative like 'r/qrcodegen')
2613 // relative to the source file's directory, matching V1 behavior
2614 return os.norm_path(os.join_path(file_dir, resolved))
2615}
2616
2617fn expand_existing_path_macros(flag_value string) ?string {
2618 mut out := ''
2619 mut i := 0
2620 for i < flag_value.len {
2621 if flag_value[i] == `$` {
2622 remainder := flag_value[i..]
2623 mut literal := ''
2624 if remainder.starts_with(r'$when_first_existing') {
2625 literal = r'$when_first_existing'
2626 } else if remainder.starts_with(r'$first_existing') {
2627 literal = r'$first_existing'
2628 }
2629 if literal != '' {
2630 if remainder.len <= literal.len || remainder[literal.len] != `(` {
2631 out += flag_value[i].ascii_str()
2632 i++
2633 continue
2634 }
2635 params_part := remainder[literal.len + 1..]
2636 params := params_part.all_before(')')
2637 if params == params_part {
2638 return none
2639 }
2640 paths := params.replace(',', '\n').split_into_lines().map(it.trim('\t \'"'))
2641 mut found := ''
2642 for path in paths {
2643 if path != '' && os.exists(path) {
2644 found = path
2645 break
2646 }
2647 }
2648 if found == '' {
2649 return none
2650 }
2651 out += found
2652 i += literal.len + 1 + params.len + 1
2653 continue
2654 }
2655 }
2656 out += flag_value[i].ascii_str()
2657 i++
2658 }
2659 return out
2660}
2661
2662fn normalize_flag_value_for_file(flag_value string, file_path string) string {
2663 file_dir := os.dir(os.real_path(file_path))
2664 vmod_root := find_vmod_root_for_file(file_path)
2665 expanded_flag_value := expand_existing_path_macros(flag_value) or { return '' }
2666 mut tokens := expanded_flag_value.fields()
2667 mut out := []string{}
2668 mut i := 0
2669 for i < tokens.len {
2670 tok := tokens[i]
2671 if tok in ['-I', '-L', '-F'] && i + 1 < tokens.len {
2672 out << tok
2673 out << resolve_flag_path(tokens[i + 1], file_dir, vmod_root)
2674 i += 2
2675 continue
2676 }
2677 if tok.starts_with('-I') && tok.len > 2 {
2678 out << '-I' + resolve_flag_path(tok[2..], file_dir, vmod_root)
2679 i++
2680 continue
2681 }
2682 if tok.starts_with('-L') && tok.len > 2 {
2683 out << '-L' + resolve_flag_path(tok[2..], file_dir, vmod_root)
2684 i++
2685 continue
2686 }
2687 if tok.starts_with('-F') && tok.len > 2 {
2688 out << '-F' + resolve_flag_path(tok[2..], file_dir, vmod_root)
2689 i++
2690 continue
2691 }
2692 if tok.contains('@VMODROOT') || tok.contains('@VEXEROOT') || tok.starts_with('./')
2693 || tok.starts_with('../') || tok.ends_with('.c') || tok.ends_with('.m')
2694 || tok.ends_with('.o') {
2695 out << resolve_flag_path(tok, file_dir, vmod_root)
2696 i++
2697 continue
2698 }
2699 out << tok
2700 i++
2701 }
2702 return out.join(' ')
2703}
2704
2705fn parse_flag_directive_line(line string, file_path string, target_os string) ?string {
2706 return parse_flag_directive_line_with_context(line, file_path, target_os, unsafe {
2707 &pref.Preferences(nil)
2708 })
2709}
2710
2711fn parse_flag_directive_line_with_pref(line string, file_path string, prefs &pref.Preferences) ?string {
2712 target_os := if prefs == unsafe { nil } { '' } else { prefs.target_os_or_host() }
2713 return parse_flag_directive_line_with_context(line, file_path, target_os, prefs)
2714}
2715
2716fn parse_flag_directive_line_with_context(line string, file_path string, target_os string, prefs &pref.Preferences) ?string {
2717 trimmed := line.trim_space()
2718 if trimmed.starts_with('#pkgconfig') {
2719 if prefs != unsafe { nil } && prefs.is_cross_target() {
2720 return none
2721 }
2722 rest := trimmed['#pkgconfig'.len..].trim_space()
2723 return resolve_pkgconfig_directive_flags(rest)
2724 }
2725 if !trimmed.starts_with('#flag') {
2726 return none
2727 }
2728 mut rest := trimmed['#flag'.len..].trim_space()
2729 if rest == '' {
2730 return none
2731 }
2732 if comment_idx := rest.index('//') {
2733 rest = rest[..comment_idx].trim_space()
2734 if rest == '' {
2735 return none
2736 }
2737 }
2738 parts := rest.fields()
2739 if parts.len == 0 {
2740 return none
2741 }
2742 if !parts[0].starts_with('-') && !parts[0].starts_with('@') && parts.len > 1 {
2743 matches := if prefs == unsafe { nil } {
2744 flag_os_matches(parts[0], target_os)
2745 } else {
2746 flag_os_matches(parts[0], target_os) || flag_pref_matches(parts[0], prefs)
2747 }
2748 if !matches {
2749 return none
2750 }
2751 rest = rest[parts[0].len..].trim_space()
2752 }
2753 if rest == '' {
2754 return none
2755 }
2756 return normalize_flag_value_for_file(rest, file_path)
2757}
2758
2759fn resolve_pkgconfig_directive_flags(value string) ?string {
2760 if value == '' {
2761 return none
2762 }
2763 args := if value.contains('--') {
2764 value.fields()
2765 } else {
2766 '--cflags --libs ${value}'.fields()
2767 }
2768 flags := pref.pkgconfig_result(args) or { return none }
2769 if flags == '' {
2770 return none
2771 }
2772 return flags
2773}
2774
2775fn flag_references_missing_file(flag string, include_flags []string) bool {
2776 for tok in flag.fields() {
2777 clean := tok.trim('"').trim("'")
2778 if clean.len == 0 {
2779 continue
2780 }
2781 if clean.ends_with('.o') || clean.ends_with('.a') || clean.ends_with('.so')
2782 || clean.ends_with('.dylib') || clean.ends_with('.m') || clean.ends_with('.c') {
2783 if os.is_abs_path(clean) || clean.starts_with('./') || clean.starts_with('../') {
2784 if !os.exists(clean) {
2785 // For .o files, try to build from corresponding .c file
2786 if clean.ends_with('.o') {
2787 c_file := clean[..clean.len - 2] + '.c'
2788 if os.exists(c_file) {
2789 inc_flags := include_flags.join(' ')
2790 compile_cmd := 'cc -c -w -O2 ${inc_flags} "${c_file}" -o "${clean}"'
2791 res := os.execute(compile_cmd)
2792 if res.exit_code == 0 {
2793 continue // successfully compiled, not missing
2794 }
2795 }
2796 }
2797 return true
2798 }
2799 }
2800 }
2801 }
2802 return false
2803}
2804
2805fn (b &Builder) collect_cflags_from_sources() string {
2806 // Collect source file paths to scan. When .vh headers were used for
2807 // parsing, b.files references the .vh summaries which lack #flag
2808 // directives. Always include the original core module source files
2809 // so that directive flags (e.g. -I paths) are never lost.
2810 mut scan_paths := []string{}
2811 for file in b.files {
2812 if file.name != '' {
2813 scan_paths << file.name
2814 }
2815 }
2816 for ff in b.flat.files {
2817 name := b.flat.file_name(ff)
2818 if name != '' {
2819 scan_paths << name
2820 }
2821 }
2822 cflags_target_os := b.cflags_target_os_for_local_compile()
2823 if !b.pref.skip_builtin {
2824 target_os := cflags_target_os
2825 for module_path in core_cached_module_paths {
2826 vlib_path := b.pref.get_vlib_module_path(module_path)
2827 module_files := get_v_files_from_dir(vlib_path, b.pref.user_defines, target_os)
2828 for mf in module_files {
2829 scan_paths << mf
2830 }
2831 }
2832 }
2833 return b.collect_cflags_from_scan_paths(scan_paths)
2834}
2835
2836fn (b &Builder) source_path_is_internal_vlib(path string) bool {
2837 vlib_root := os.real_path(os.join_path(b.pref.vroot, 'vlib')).replace('\\', '/').trim_right('/')
2838 real_path := os.real_path(path).replace('\\', '/')
2839 return real_path == vlib_root || real_path.starts_with('${vlib_root}/')
2840}
2841
2842fn (b &Builder) collect_user_cflags_from_sources() string {
2843 mut scan_paths := []string{}
2844 for path in b.user_files {
2845 if path != '' {
2846 scan_paths << path
2847 }
2848 }
2849 for file in b.files {
2850 if file.name != '' && !b.source_path_is_internal_vlib(file.name) {
2851 scan_paths << file.name
2852 }
2853 }
2854 for ff in b.flat.files {
2855 name := b.flat.file_name(ff)
2856 if name != '' && !b.source_path_is_internal_vlib(name) {
2857 scan_paths << name
2858 }
2859 }
2860 return b.collect_cflags_from_scan_paths(scan_paths)
2861}
2862
2863fn (b &Builder) collect_cflags_from_scan_paths(paths []string) string {
2864 mut flags := []string{}
2865 mut seen := map[string]bool{}
2866 mut scanned_files := map[string]bool{}
2867 mut scan_paths := paths.clone()
2868 cflags_target_os := b.cflags_target_os_for_local_compile()
2869 scan_paths.sort()
2870 for scan_path in scan_paths {
2871 if scan_path == '' || scan_path in scanned_files {
2872 continue
2873 }
2874 scanned_files[scan_path] = true
2875 lines := os.read_lines(scan_path) or { continue }
2876 // Track $if nesting to skip flags inside non-matching comptime blocks.
2877 // skip_depth > 0 means we are inside a non-matching $if block.
2878 // chain_matched[i] tracks whether the i-th enclosing $if chain has
2879 // already matched some branch (so subsequent $else / $else $if at the
2880 // same level should be skipped even if their cond would otherwise match).
2881 mut skip_depth := 0
2882 mut chain_matched := []bool{}
2883 for line in lines {
2884 trimmed := line.trim_space()
2885 // Strip a leading "} " so chained patterns like "} $else $if X {" parse
2886 // the same way as "$else $if X {" on their own line.
2887 rest := if trimmed.starts_with('} ') { trimmed[2..].trim_space() } else { trimmed }
2888 leading_close := rest != trimmed
2889 // $else $if cond { (a chain continuation)
2890 if rest.starts_with(r'$else $if ') {
2891 new_cond := rest[10..].trim_right('{ ').trim_space()
2892 if skip_depth > 1 {
2893 // nested skipping; just continue without touching outer state
2894 continue
2895 }
2896 cur := chain_matched.len - 1
2897 if cur < 0 {
2898 continue
2899 }
2900 if chain_matched[cur] {
2901 skip_depth = 1
2902 } else if comptime_cond_matches_with_context(new_cond, cflags_target_os, b.pref) {
2903 chain_matched[cur] = true
2904 skip_depth = 0
2905 } else {
2906 skip_depth = 1
2907 }
2908 continue
2909 }
2910 // plain $else { (chain terminator)
2911 if rest.starts_with(r'$else') {
2912 if skip_depth > 1 {
2913 continue
2914 }
2915 cur := chain_matched.len - 1
2916 if cur < 0 {
2917 continue
2918 }
2919 if chain_matched[cur] {
2920 skip_depth = 1
2921 } else {
2922 chain_matched[cur] = true
2923 skip_depth = 0
2924 }
2925 continue
2926 }
2927 // $if cond { (chain opener)
2928 if rest.starts_with(r'$if ') {
2929 cond := rest[4..].trim_right('{ ').trim_space()
2930 matched := comptime_cond_matches_with_context(cond, cflags_target_os, b.pref)
2931 chain_matched << matched
2932 if skip_depth > 0 {
2933 skip_depth++
2934 } else if !matched {
2935 skip_depth = 1
2936 }
2937 continue
2938 }
2939 // closing }
2940 if trimmed == '}' {
2941 if skip_depth > 0 {
2942 skip_depth--
2943 }
2944 if chain_matched.len > 0 {
2945 chain_matched.delete_last()
2946 }
2947 continue
2948 }
2949 // "} something" where the leading } closed a chain but the rest is
2950 // unrecognized: treat the } as a chain close.
2951 if leading_close && rest == '' {
2952 if skip_depth > 0 {
2953 skip_depth--
2954 }
2955 if chain_matched.len > 0 {
2956 chain_matched.delete_last()
2957 }
2958 continue
2959 }
2960 if skip_depth > 0 {
2961 continue
2962 }
2963 // Replace @VEXEROOT before parsing so path normalization sees absolute paths
2964 resolved_line := line.replace('@VEXEROOT', b.pref.vroot).replace('VEXEROOT',
2965 b.pref.vroot)
2966 mut flag := parse_flag_directive_line_with_context(resolved_line, scan_path,
2967 cflags_target_os, b.pref) or { continue }
2968 // Build include flags from already-collected flags for compiling missing .o files
2969 mut inc_flags := []string{}
2970 for f in flags {
2971 if f.starts_with('-I') {
2972 inc_flags << f
2973 }
2974 }
2975 if flag_references_missing_file(flag, inc_flags) {
2976 continue
2977 }
2978 if flag == '' || flag in seen {
2979 continue
2980 }
2981 seen[flag] = true
2982 flags << flag
2983 }
2984 }
2985 return flags.join(' ')
2986}
2987
2988// split_compile_and_link_flags separates a flags string into compiler-only
2989// flags (for -c compilation) and linker-only flags (for the link step).
2990// Linker flags include: -l*, -L*, -Wl,*, -Xlinker, -framework, C source files
2991// and prebuilt object/library files. Minimal dual-use driver flags, plus -F
2992// framework search paths, are kept in both outputs. Source files from #flag
2993// directives must not be passed to
2994// per-module `-c -o module.o` cache compilations.
2995fn split_compile_and_link_flags(flags string) (string, string) {
2996 tokens := flags.fields()
2997 mut compile := []string{}
2998 mut link := []string{}
2999 mut i := 0
3000 for i < tokens.len {
3001 tok := tokens[i]
3002 if native_driver_flag_is_dual_use(tok) {
3003 compile << tok
3004 link << tok
3005 } else if tok == '-F' {
3006 compile << tok
3007 link << tok
3008 if i + 1 < tokens.len {
3009 i++
3010 compile << tokens[i]
3011 link << tokens[i]
3012 }
3013 } else if tok.starts_with('-F') {
3014 compile << tok
3015 link << tok
3016 } else if tok == '-Xlinker' {
3017 link << tok
3018 if i + 1 < tokens.len {
3019 i++
3020 link << tokens[i]
3021 }
3022 } else if tok == '-framework' {
3023 // -framework Name: two tokens, linker only
3024 link << tok
3025 if i + 1 < tokens.len {
3026 i++
3027 link << tokens[i]
3028 }
3029 } else if tok.starts_with('-Wl,') || tok.starts_with('-l') || tok.starts_with('-L') {
3030 link << tok
3031 // -L or -l alone (space-separated from its argument): grab the next token
3032 if (tok == '-L' || tok == '-l') && i + 1 < tokens.len {
3033 i++
3034 link << tokens[i]
3035 }
3036 } else if tok.ends_with('.c') || tok.ends_with('.cc') || tok.ends_with('.cpp')
3037 || tok.ends_with('.cxx') || tok.ends_with('.m') || tok.ends_with('.mm')
3038 || tok.ends_with('.o') || tok.ends_with('.obj') || tok.ends_with('.a')
3039 || tok.ends_with('.so') || tok.ends_with('.dylib') {
3040 link << tok
3041 } else if tok == '-I' && i + 1 < tokens.len {
3042 // -I alone (space-separated from its argument): grab the next token
3043 compile << tok
3044 i++
3045 compile << tokens[i]
3046 } else {
3047 compile << tok
3048 }
3049 i++
3050 }
3051 return compile.join(' '), link.join(' ')
3052}
3053
3054fn comptime_cond_matches(cond string, target_os string) bool {
3055 return comptime_cond_matches_with_context(cond, target_os, unsafe { &pref.Preferences(nil) })
3056}
3057
3058fn comptime_cond_matches_with_pref(cond string, prefs &pref.Preferences) bool {
3059 target_os := if prefs == unsafe { nil } { '' } else { prefs.target_os_or_host() }
3060 return comptime_cond_matches_with_context(cond, target_os, prefs)
3061}
3062
3063fn comptime_cond_matches_with_context(cond string, target_os string, prefs &pref.Preferences) bool {
3064 trimmed := cond.trim_space()
3065 if or_idx := top_level_bool_op_index(trimmed, '||') {
3066 left := trimmed[..or_idx].trim_space()
3067 right := trimmed[or_idx + 2..].trim_space()
3068 return comptime_cond_matches_with_context(left, target_os, prefs)
3069 || comptime_cond_matches_with_context(right, target_os, prefs)
3070 }
3071 if and_idx := top_level_bool_op_index(trimmed, '&&') {
3072 left := trimmed[..and_idx].trim_space()
3073 right := trimmed[and_idx + 2..].trim_space()
3074 return comptime_cond_matches_with_context(left, target_os, prefs)
3075 && comptime_cond_matches_with_context(right, target_os, prefs)
3076 }
3077 if trimmed.starts_with('!') {
3078 return !comptime_cond_matches_with_context(trimmed[1..], target_os, prefs)
3079 }
3080 if stripped := strip_outer_bool_parens(trimmed) {
3081 return comptime_cond_matches_with_context(stripped, target_os, prefs)
3082 }
3083 if optional_name := optional_user_ct_flag_name(trimmed) {
3084 if prefs == unsafe { nil } {
3085 return false
3086 }
3087 return pref.comptime_optional_flag_value(prefs, optional_name)
3088 }
3089 if pkg_name := pkgconfig_cond_name(trimmed) {
3090 if prefs != unsafe { nil } && prefs.is_cross_target() {
3091 return false
3092 }
3093 return pref.comptime_pkgconfig_value(pkg_name)
3094 }
3095 if prefs != unsafe { nil } {
3096 return flag_os_matches(trimmed, target_os) || flag_pref_matches(trimmed, prefs)
3097 }
3098 current := normalize_target_os_name(target_os)
3099 return match trimmed.to_lower() {
3100 'macos', 'darwin', 'mac' { current == 'macos' || current == 'darwin' }
3101 'linux' { current == 'linux' }
3102 'windows' { current == 'windows' }
3103 'bsd' { current in ['macos', 'freebsd', 'openbsd', 'netbsd', 'dragonfly'] }
3104 'freebsd' { current == 'freebsd' }
3105 'openbsd' { current == 'openbsd' }
3106 'netbsd' { current == 'netbsd' }
3107 'dragonfly' { current == 'dragonfly' }
3108 'android' { current == 'android' }
3109 'termux' { current == 'termux' }
3110 'ios' { current == 'ios' }
3111 'solaris' { current == 'solaris' }
3112 'qnx' { current == 'qnx' }
3113 'serenity' { current == 'serenity' }
3114 'plan9' { current == 'plan9' }
3115 'vinix' { current == 'vinix' }
3116 'cross' { current == 'cross' }
3117 'native' { false }
3118 'emscripten' { false }
3119 else { false } // unknown user-defined flags default to false
3120 }
3121}
3122
3123fn pkgconfig_cond_name(cond string) ?string {
3124 trimmed := cond.trim_space()
3125 if !trimmed.starts_with(r'$pkgconfig(') || !trimmed.ends_with(')') {
3126 return none
3127 }
3128 arg := trimmed[r'$pkgconfig('.len..trimmed.len - 1].trim_space()
3129 if arg.len < 2 {
3130 return none
3131 }
3132 quote := arg[0]
3133 if (quote != `'` && quote != `"`) || arg[arg.len - 1] != quote {
3134 return none
3135 }
3136 return arg[1..arg.len - 1]
3137}
3138
3139fn optional_user_ct_flag_name(cond string) ?string {
3140 trimmed := cond.trim_space()
3141 if !trimmed.ends_with('?') {
3142 return none
3143 }
3144 name := trimmed[..trimmed.len - 1].trim_space()
3145 if name == '' {
3146 return none
3147 }
3148 return name
3149}
3150
3151fn top_level_bool_op_index(expr string, op string) ?int {
3152 mut depth := 0
3153 mut i := 0
3154 for i < expr.len {
3155 ch := expr[i]
3156 if ch == `(` {
3157 depth++
3158 } else if ch == `)` {
3159 if depth > 0 {
3160 depth--
3161 }
3162 } else if depth == 0 && i + op.len <= expr.len && expr[i..i + op.len] == op {
3163 return i
3164 }
3165 i++
3166 }
3167 return none
3168}
3169
3170fn strip_outer_bool_parens(expr string) ?string {
3171 if expr.len < 2 || expr[0] != `(` || expr[expr.len - 1] != `)` {
3172 return none
3173 }
3174 mut depth := 0
3175 for i, ch in expr {
3176 if ch == `(` {
3177 depth++
3178 } else if ch == `)` {
3179 depth--
3180 if depth == 0 && i < expr.len - 1 {
3181 return none
3182 }
3183 }
3184 }
3185 if depth == 0 {
3186 return expr[1..expr.len - 1].trim_space()
3187 }
3188 return none
3189}
3190
3191fn default_cc(vroot string) string {
3192 // Try to use tcc by default, like v1 does.
3193 tcc_path := os.join_path(vroot, 'thirdparty', 'tcc', 'tcc.exe')
3194 if os.exists(tcc_path) {
3195 return tcc_path
3196 }
3197 return 'cc'
3198}
3199
3200// fast_relink_output_is_generation_only mirrors gen_cleanc()'s generation-only
3201// decision: a `.c` output, a target we cannot compile locally, or a shared lib.
3202// Such a request must go through normal C generation, never the pre-parse relink
3203// (is_cmd_v2_self_build() keys only on the input file, so the relink path would
3204// otherwise link an executable into e.g. foo.c). Extracted so the decision is
3205// unit-testable without a warm object cache.
3206fn (b &Builder) fast_relink_output_is_generation_only(output_name string) bool {
3207 return output_name.ends_with('.c') || !b.can_compile_cleanc_locally() || b.pref.is_shared_lib
3208}
3209
3210// preparse_flag_fingerprint captures the flag-affecting build inputs that are
3211// knowable WITHOUT parsing the sources: the C compiler choice (-cc / V2CC /
3212// default), prod/shared mode, and the env CFLAGS (V2CFLAGS). It is recorded in
3213// main.stamp and re-checked by the pre-parse fast relink so a changed compiler or
3214// CFLAGS environment invalidates a relink even when every source file is
3215// unchanged. Source-derived `#flag` directives are intentionally excluded — they
3216// change only when a source file changes, which the stamp freshness checks catch.
3217fn (b &Builder) preparse_flag_fingerprint() string {
3218 cc := if b.pref.ccompiler.len > 0 { b.pref.ccompiler } else { configured_cc(b.pref.vroot) }
3219 return 'cc=${cc}\x01ccpref=${b.pref.ccompiler}\x01prod=${b.pref.is_prod}\x01shared=${b.pref.is_shared_lib}\x01env=${configured_cflags()}'
3220}
3221
3222fn configured_cc(vroot string) string {
3223 cc := (os.getenv_opt('V2CC') or { '' }).trim_space()
3224 if cc != '' {
3225 return cc
3226 }
3227 return default_cc(vroot)
3228}
3229
3230fn configured_cflags() string {
3231 return (os.getenv_opt('V2CFLAGS') or { '' }).trim_space()
3232}
3233
3234fn tcc_flags(cc string, vroot string) string {
3235 if !cc.contains('tcc') {
3236 return ''
3237 }
3238 tcc_dir := os.join_path(vroot, 'thirdparty', 'tcc')
3239 return '-I "${os.join_path(tcc_dir, 'lib', 'include')}" -L "${os.join_path(tcc_dir, 'lib')}"'
3240}
3241
3242fn cflags_need_objc_mode(flags string) bool {
3243 lower_flags := flags.to_lower()
3244 for tok in lower_flags.fields() {
3245 clean := tok.trim('"\'')
3246 if clean.ends_with('.m') || clean.ends_with('.mm') {
3247 return true
3248 }
3249 }
3250 return lower_flags.contains('-framework cocoa') || lower_flags.contains('-framework appkit')
3251 || lower_flags.contains('-framework foundation') || lower_flags.contains('-framework uikit')
3252 || lower_flags.contains('-framework metal') || lower_flags.contains('-framework metalkit')
3253 || lower_flags.contains('-framework quartzcore')
3254}
3255
3256fn cc_recompile_flags_from_cmd(cmd string) string {
3257 parts := cmd.fields()
3258 mut flags := []string{}
3259 mut i := 1 // skip compiler
3260 for i < parts.len {
3261 p := parts[i]
3262 if p == '-o' {
3263 i += 2
3264 continue
3265 }
3266 if p == '-x' {
3267 if i + 1 < parts.len && parts[i + 1] != 'none' {
3268 flags << p
3269 flags << parts[i + 1]
3270 }
3271 i += 2
3272 continue
3273 }
3274 if p in ['-I', '-D', '-U', '-F', '-include', '-isystem', '-idirafter'] {
3275 flags << p
3276 if i + 1 < parts.len {
3277 i++
3278 flags << parts[i]
3279 }
3280 i++
3281 continue
3282 }
3283 if p.starts_with('-I') || p.starts_with('-D') || p.starts_with('-U') || p.starts_with('-F')
3284 || p.starts_with('-std=') || p.starts_with('-W') || p.starts_with('-f')
3285 || p.starts_with('-m') || p == '-pthread' {
3286 flags << p
3287 }
3288 i++
3289 }
3290 return flags.join(' ')
3291}
3292
3293// run_cc_cmd_or_exit runs a C compiler command, falling back from tcc to cc
3294// if needed. Returns true if tcc fell back to cc.
3295fn run_cc_cmd_or_exit(cmd string, stage string, show_cc bool) bool {
3296 if show_cc {
3297 println(cmd)
3298 } else if os.getenv('V2VERBOSE') != '' {
3299 dump(cmd)
3300 }
3301 result := os.execute(cmd)
3302 if result.exit_code != 0 {
3303 // If tcc failed, fall back to cc.
3304 // Check only the compiler binary (before the first space), not the full
3305 // command string which contains tcc in include/library flag paths.
3306 cc_binary := cmd.all_before(' ')
3307 if cc_binary.contains('tcc') {
3308 eprintln('Failed to compile with tcc, falling back to cc')
3309 eprintln('tcc cmd: ${cmd}')
3310 eprintln(result.output)
3311 // Replace TCC binary with cc and strip TCC-specific include/lib
3312 // paths. TCC's tgmath.h conflicts with macOS system headers,
3313 // causing SIMD ambiguity errors in MetalKit when compiling as
3314 // Objective-C.
3315 mut fallback_cmd := cmd.replace_once(cc_binary, 'cc')
3316 tcc_dir := cc_binary.all_before_last('/tcc')
3317 if tcc_dir.len > 0 {
3318 // Remove -I and -L flags pointing into the TCC directory.
3319 mut parts := fallback_cmd.fields()
3320 mut filtered := []string{cap: parts.len}
3321 mut i2 := 0
3322 for i2 < parts.len {
3323 p := parts[i2]
3324 if (p == '-I' || p == '-L') && i2 + 1 < parts.len
3325 && parts[i2 + 1].contains('tcc') {
3326 i2 += 2
3327 continue
3328 }
3329 if (p.starts_with('-I') || p.starts_with('-L')) && p.contains('tcc') {
3330 i2++
3331 continue
3332 }
3333 filtered << p
3334 i2++
3335 }
3336 fallback_cmd = filtered.join(' ')
3337 }
3338 // cc cannot read .o files produced by tcc on macOS arm64. Recompile
3339 // any cached .o files referenced in the command from their .c siblings
3340 // using cc before retrying the link.
3341 recompile_flags := cc_recompile_flags_from_cmd(fallback_cmd)
3342 for tok in fallback_cmd.fields() {
3343 clean_tok := tok.trim('"')
3344 if !clean_tok.ends_with('.o') {
3345 continue
3346 }
3347 stem := clean_tok.all_before_last('.o')
3348 mut c_sibling := stem + '.c'
3349 if !os.exists(c_sibling) && stem.ends_with('.main') {
3350 c_sibling = stem.all_before_last('.main') + '.c'
3351 }
3352 if !os.exists(c_sibling) {
3353 continue
3354 }
3355 recompile_cmd := 'cc ${recompile_flags} -w -Wno-incompatible-function-pointer-types -c "${c_sibling}" -o "${clean_tok}"'
3356 if show_cc {
3357 println(recompile_cmd)
3358 }
3359 rr := os.execute(recompile_cmd)
3360 if rr.exit_code != 0 {
3361 eprintln('cc recompile failed for ${c_sibling}:')
3362 eprintln(rr.output)
3363 }
3364 // Invalidate stamp so future builds rebuild from .c too.
3365 stamp_path := stem + '.stamp'
3366 if os.exists(stamp_path) {
3367 os.rm(stamp_path) or {}
3368 }
3369 }
3370 run_cc_cmd_or_exit(fallback_cmd, stage, show_cc)
3371 return true
3372 }
3373 eprintln('${stage} failed:')
3374 lines := result.output.split_into_lines()
3375 limit := if lines.len < 50 { lines.len } else { 50 }
3376 for line in lines[..limit] {
3377 eprintln(line)
3378 }
3379 mut error_count := 0
3380 mut warning_count := 0
3381 for line in lines {
3382 if line.contains(': error:') || line.contains(': fatal error:') {
3383 error_count += 1
3384 } else if line.contains(': warning:') {
3385 warning_count += 1
3386 }
3387 }
3388 if stage == 'C compilation' {
3389 eprintln('Total: ${warning_count} warnings and ${error_count} errors')
3390 }
3391 exit(1)
3392 }
3393 return false
3394}
3395
3396fn native_graph_stage_title(label string, title string) string {
3397 if label == '' {
3398 return title
3399 }
3400 return '${label} ${title}'
3401}
3402
3403const macos_tiny_candidate_graph_label = 'macOS Tiny Candidate'
3404
3405fn (b &Builder) native_mir_build_sequential(label string) bool {
3406 return label == macos_tiny_candidate_graph_label || b.pref.no_parallel || b.pref.hot_fn.len > 0
3407}
3408
3409fn (b &Builder) should_prune_native_backend_modules(arch pref.Arch) bool {
3410 return b.pref.single_backend || arch == .arm64
3411}
3412
3413fn native_backend_module_file_fragment(backend_mod string) string {
3414 return match backend_mod {
3415 'eval' { '/vlib/v2/eval/' }
3416 else { '/vlib/v2/gen/${backend_mod}/' }
3417 }
3418}
3419
3420fn (mut b Builder) build_native_mir_from_files(files []ast.File, arch pref.Arch, target_os string, minimal_runtime_roots bool, used_fn_keys map[string]bool, label string) mir.Module {
3421 mut mod := ssa.Module.new('main')
3422 if mod == unsafe { nil } {
3423 eprintln('error: native backend not available (compiled with stubbed ssa module)')
3424 eprintln('hint: use v2 compiled with v1 for native code generation')
3425 exit(1)
3426 }
3427 mut ssa_builder := ssa.Builder.new_with_env(mod, b.env)
3428 ssa_builder.guard_invalid_type_payloads = true
3429 ssa_builder.target_os = target_os
3430 ssa_builder.minimal_runtime_roots = minimal_runtime_roots
3431 ssa_builder.native_backend_bulk_zero_alloca = arch == .x64
3432 mut native_sw := time.new_stopwatch()
3433
3434 // Pass markused data for dead code elimination. The ARM64 backend has its own
3435 // relocation-based dead stripping; using markused before SSA makes self-hosted
3436 // compiler builds fragile when the markused set is under-collected.
3437 if used_fn_keys.len > 0 && arch != .arm64 {
3438 ssa_builder.used_fn_keys = used_fn_keys.clone()
3439 }
3440
3441 // Strip unused compiler backend modules before SSA. ARM64 already strips
3442 // these symbols after codegen, so avoid building MIR for them in the first place.
3443 if b.should_prune_native_backend_modules(arch) {
3444 all_backends := ['cleanc', 'eval', 'v', 'c', 'x64', 'arm64']
3445 own := match b.pref.backend {
3446 .arm64 { 'arm64' }
3447 .x64 { 'x64' }
3448 .cleanc { 'cleanc' }
3449 .v { 'v' }
3450 .c { 'c' }
3451 .eval { 'eval' }
3452 }
3453
3454 for backend_mod in all_backends {
3455 if backend_mod != own {
3456 ssa_builder.skip_modules[backend_mod] = true
3457 ssa_builder.skip_module_file_fragments[backend_mod] =
3458 native_backend_module_file_fragment(backend_mod)
3459 }
3460 }
3461 }
3462
3463 // In hot_fn mode, only build the target function body (skip all others)
3464 if b.pref.hot_fn.len > 0 {
3465 ssa_builder.hot_fn = b.pref.hot_fn
3466 }
3467
3468 mut stage_start := native_sw.elapsed()
3469 // Route the whole SSA build through the cursor-native build_all_from_flat
3470 // on the post-transform b.flat (kept alive above). Sequential only
3471 // (build_all_from_flat builds fn bodies in-phase).
3472 //
3473 // b.flat is only POST-TRANSFORM when flat markused has routed transform
3474 // through transform_files_to_flat, or the direct native flat pipeline has
3475 // emitted transform output directly into FlatAst.
3476 build_from_flat := b.should_build_ssa_from_flat()
3477 || (b.flat.files.len > 0 && b.native_flat_pipeline_enabled && label == '')
3478 if build_from_flat {
3479 ssa_builder.build_all_from_flat(&b.flat)
3480 // SSA has copied the program into MIR; keep the FlatAst lifetime out of
3481 // the later optimizer and machine-code generator working sets.
3482 b.flat = ast.FlatAst{}
3483 } else if b.native_mir_build_sequential(label) {
3484 ssa_builder.build_all(files)
3485 } else {
3486 // Phases 1-3 sequential, Phase 4 parallel, Phase 5 sequential
3487 ssa_builder.skip_fn_bodies = true
3488 ssa_builder.build_all(files)
3489 ssa_builder.skip_fn_bodies = false
3490 b.ssa_build_parallel(mut ssa_builder, files)
3491 ssa_builder.generate_vinit()
3492 }
3493 if arch == .arm64 {
3494 b.env.release_expr_type_cache_after_ssa()
3495 }
3496 print_time(native_graph_stage_title(label, 'SSA Build'),
3497 time.Duration(native_sw.elapsed() - stage_start))
3498 print_rss(native_graph_stage_title(label, 'after SSA build'))
3499
3500 stage_start = native_sw.elapsed()
3501 ssa_optimization_required := b.native_backend_requires_ssa_optimization(arch)
3502 ssa_optimization_ran := !b.pref.no_optimize || ssa_optimization_required
3503 if ssa_optimization_required {
3504 if b.pref.no_optimize {
3505 eprintln(' opt: required for x64 backend (-O0 SSA is not supported yet)')
3506 }
3507 ssa_optimize.optimize(mut mod)
3508 } else if b.pref.no_optimize {
3509 eprintln(' opt: skipped (-O0)')
3510 } else {
3511 ssa_optimize.optimize(mut mod)
3512 }
3513 print_time(native_graph_stage_title(label, 'SSA Optimize'),
3514 time.Duration(native_sw.elapsed() - stage_start))
3515 print_rss(native_graph_stage_title(label, 'after SSA optimize'))
3516 $if debug {
3517 // Post-opt SSA verification is useful while debugging the optimizer, but it
3518 // is currently noisy enough to block normal self-host builds. Keep it
3519 // opt-in so `test_all.sh` and manual self-hosting can still complete.
3520 if ssa_optimization_ran && os.getenv('V2_VERIFY') != '' {
3521 ssa_optimize.verify_and_panic(mod, 'full optimization')
3522 }
3523 }
3524
3525 // Post-optimization SSA dump for debugging
3526 dump_fn_name := os.getenv('V2_DUMP_OPT_SSA')
3527 if dump_fn_name.len > 0 {
3528 for func in mod.funcs {
3529 if func.name == dump_fn_name {
3530 eprintln('=== POST-OPT SSA DUMP: ${func.name} ===')
3531 eprintln(' params_len: ${func.params.len}')
3532 for pi, pid in func.params {
3533 pval := mod.values[pid]
3534 eprintln(' param[${pi}]: v${pid} kind=${pval.kind} name=`${pval.name}` typ=${pval.typ}')
3535 }
3536 for blk_id in func.blocks {
3537 blk := mod.blocks[blk_id]
3538 eprintln(' block ${blk_id} (${blk.name}):')
3539 for dval_id in blk.instrs {
3540 dval := mod.values[dval_id]
3541 if dval.kind != .instruction {
3542 continue
3543 }
3544 dinstr := mod.instrs[dval.index]
3545 mut ops_str := ''
3546 for oi, op_id in dinstr.operands {
3547 op_v := mod.values[op_id]
3548 ops_str += 'v${op_id}(${op_v.kind}:${op_v.name})'
3549 if oi < dinstr.operands.len - 1 {
3550 ops_str += ', '
3551 }
3552 }
3553 eprintln(' v${dval_id}: ${dinstr.op} [${ops_str}] typ=${dval.typ}')
3554 }
3555 }
3556 eprintln('=== END POST-OPT SSA DUMP ===')
3557 }
3558 }
3559 }
3560
3561 stage_start = native_sw.elapsed()
3562 mut mir_mod := mir.lower_from_ssa(mod)
3563 print_time(native_graph_stage_title(label, 'MIR Lower'),
3564 time.Duration(native_sw.elapsed() - stage_start))
3565 mod.release_outer_arenas_after_mir_lower()
3566 print_rss(native_graph_stage_title(label, 'after MIR lower'))
3567
3568 stage_start = native_sw.elapsed()
3569 if is_windows_x64_native_target(arch, target_os) {
3570 abi.lower_with_x64_abi(mut mir_mod, arch, native_x64_lowering_abi_for_os(target_os))
3571 } else {
3572 abi.lower(mut mir_mod, arch)
3573 }
3574 print_time(native_graph_stage_title(label, 'ABI Lower'),
3575 time.Duration(native_sw.elapsed() - stage_start))
3576 print_rss(native_graph_stage_title(label, 'after ABI lower'))
3577
3578 if arch != .arm64 {
3579 stage_start = native_sw.elapsed()
3580 insel.select(mut mir_mod, arch)
3581 print_time(native_graph_stage_title(label, 'InsSel'),
3582 time.Duration(native_sw.elapsed() - stage_start))
3583 print_rss(native_graph_stage_title(label, 'after InsSel'))
3584 }
3585 return mir_mod
3586}
3587
3588fn (mut b Builder) build_macos_tiny_candidate_mir(arch pref.Arch, target_os string) mir.Module {
3589 if b.macos_tiny_candidate_source_flat.files.len == 0
3590 && b.macos_tiny_candidate_source_files.len == 0 {
3591 eprintln('internal error: macOS tiny candidate graph was not prepared')
3592 exit(1)
3593 }
3594 mut trans := transformer.Transformer.new_with_pref(b.env, b.pref)
3595 trans.set_file_set(b.file_set)
3596 trans.enable_macos_tiny_candidate_graph()
3597 opts := markused.MarkUsedOptions{
3598 minimal_runtime_roots: true
3599 }
3600 if b.macos_tiny_candidate_source_flat.files.len > 0 {
3601 candidate_flat :=
3602 trans.transform_flat_to_flat_direct(&b.macos_tiny_candidate_source_flat, [])
3603 candidate_used_fn_keys := markused.mark_used_flat_with_options(&candidate_flat, b.env, opts)
3604 old_flat := b.flat
3605 b.flat = candidate_flat
3606 candidate_mir := b.build_native_mir_from_files([], arch, target_os, true,
3607 candidate_used_fn_keys, macos_tiny_candidate_graph_label)
3608 b.flat = old_flat
3609 return candidate_mir
3610 }
3611 candidate_files := trans.transform_files(b.macos_tiny_candidate_source_files)
3612 candidate_used_fn_keys := markused.mark_used_with_options(candidate_files, b.env, opts)
3613 return b.build_native_mir_from_files(candidate_files, arch, target_os, true,
3614 candidate_used_fn_keys, macos_tiny_candidate_graph_label)
3615}
3616
3617fn (mut b Builder) gen_native(backend_arch pref.Arch) {
3618 arch := if backend_arch == .auto { b.pref.get_effective_arch() } else { backend_arch }
3619 target_os := b.pref.target_os_or_host()
3620 native_compile_flags, native_link_flags := b.native_compile_and_link_flags_from_sources()
3621 _, native_user_link_flags := b.native_user_compile_and_link_flags_from_sources()
3622 mut native_external_inputs := NativeExternalLinkInputs{
3623 link_flags: native_link_flags
3624 }
3625
3626 mut mir_mod := b.build_native_mir_from_files(b.files, arch, target_os,
3627 b.uses_minimal_x64_runtime_roots(), b.used_fn_keys, '')
3628 // The hosted SSA build has consumed b.files into `mir_mod`; the rest of the
3629 // normal native pipeline operates on MIR only. Drop the ~120MB legacy AST so
3630 // it can be reclaimed before codegen's working-set grows. The macOS tiny
3631 // candidate, when requested, uses its own saved source snapshot.
3632 b.files = []ast.File{}
3633
3634 // Determine output binary name from the last user file
3635 output_binary := if b.pref.output_file != '' {
3636 b.pref.output_file
3637 } else if b.user_files.len > 0 {
3638 b.default_output_name()
3639 } else {
3640 'out'
3641 }
3642
3643 if arch == .arm64 && is_macos_native_target(target_os) {
3644 // Use built-in linker for ARM64 macOS
3645 mut native_sw := time.new_stopwatch()
3646 stage_start := native_sw.elapsed()
3647 mut gen := arm64.Gen.new(&mir_mod)
3648 if b.pref.no_parallel {
3649 gen.gen()
3650 } else {
3651 b.gen_arm64_parallel(mut gen)
3652 }
3653 gen.release_scratch_after_gen()
3654 mir_mod.release_after_native_codegen()
3655 print_time('ARM64 Gen', time.Duration(native_sw.elapsed() - stage_start))
3656
3657 if b.pref.hot_fn.len > 0 {
3658 // Hot code reloading: extract raw machine code for a single function
3659 code := gen.extract_function(b.pref.hot_fn)
3660 if code.len > 0 {
3661 os.write_file_array(output_binary, code) or { panic(err) }
3662 println('hot-fn: wrote ${code.len} bytes for "${b.pref.hot_fn}" to ${output_binary}')
3663 }
3664 return
3665 }
3666
3667 gen.link_executable(output_binary)
3668
3669 if b.pref.verbose {
3670 println('[*] Linked ${output_binary} (built-in linker)')
3671 }
3672 } else {
3673 // Generate object file and use external linker
3674 obj_file := native_external_object_file(output_binary, target_os)
3675 mut used_macos_tiny_object := false
3676
3677 if arch == .arm64 {
3678 mut gen := arm64.Gen.new(&mir_mod)
3679 if b.pref.no_parallel {
3680 gen.gen()
3681 } else {
3682 b.gen_arm64_parallel(mut gen)
3683 }
3684 gen.release_scratch_after_gen()
3685 mir_mod.release_after_native_codegen()
3686 gen.write_file(obj_file)
3687 } else {
3688 obj_format := native_x64_object_format_for_os(target_os)
3689 codegen_abi := native_x64_codegen_abi_for_os(target_os)
3690 if msg := native_x64_mir_unsupported_external_symbol_message(&mir_mod, obj_format) {
3691 eprint_native_x64_link_error(msg)
3692 exit(1)
3693 }
3694 if is_windows_x64_native_target(arch, target_os) {
3695 mut windows_gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format,
3696 codegen_abi)
3697 windows_gen.gen()
3698 windows_gen.link_executable(output_binary) or {
3699 eprint_native_x64_link_error(err.msg())
3700 exit(1)
3701 }
3702 if b.pref.verbose {
3703 println('[*] Linked ${output_binary} (built-in PE linker)')
3704 }
3705 return
3706 }
3707 if is_linux_x64_native_target(arch, target_os) {
3708 mut linux_gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format, codegen_abi)
3709 linux_gen.gen()
3710 if native_link_flags_allow_builtin_linux_tiny(native_link_flags,
3711 native_user_link_flags)
3712 {
3713 if os.exists(output_binary) {
3714 os.rm(output_binary) or {}
3715 }
3716 linux_gen.link_linux_tiny_executable(output_binary) or {
3717 msg := err.msg()
3718 if linux_x64_tiny_strict_enabled()
3719 || !msg.starts_with(x64.linux_tiny_not_eligible_prefix) {
3720 eprint_native_x64_link_error(msg)
3721 exit(1)
3722 }
3723 }
3724 if os.exists(output_binary) {
3725 if b.pref.verbose {
3726 println('[*] Linked ${output_binary} (built-in Linux tiny linker)')
3727 }
3728 return
3729 }
3730 }
3731 linux_gen.write_file(obj_file)
3732 } else if b.uses_macos_x64_tiny_object(arch) {
3733 if os.exists(obj_file) {
3734 os.rm(obj_file) or {}
3735 }
3736 if b.pref.verbose {
3737 println('[*] macOS tiny object candidate enabled')
3738 }
3739 mut candidate_mir := b.build_macos_tiny_candidate_mir(arch, target_os)
3740 mut candidate_gen := x64.Gen.new_with_format_and_abi(&candidate_mir, obj_format,
3741 codegen_abi)
3742 candidate_gen.gen()
3743 candidate_gen.write_macos_tiny_object(obj_file) or {
3744 msg := err.msg()
3745 if !msg.starts_with(x64.macos_tiny_not_eligible_prefix) {
3746 eprint_native_x64_link_error(msg)
3747 exit(1)
3748 }
3749 if b.pref.verbose {
3750 println('[*] macOS tiny object not eligible; falling back to normal Mach-O object: ${msg}')
3751 }
3752 }
3753 if os.exists(obj_file) {
3754 used_macos_tiny_object = true
3755 } else {
3756 if b.pref.verbose {
3757 println('[*] macOS tiny object fallback: writing normal Mach-O object')
3758 }
3759 mut macos_fallback_gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format,
3760 codegen_abi)
3761 macos_fallback_gen.gen()
3762 macos_fallback_gen.write_file(obj_file)
3763 }
3764 } else {
3765 mut normal_x64_gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format,
3766 codegen_abi)
3767 normal_x64_gen.gen()
3768 normal_x64_gen.write_file(obj_file)
3769 }
3770 }
3771
3772 if b.pref.verbose {
3773 if used_macos_tiny_object {
3774 println('[*] Wrote ${obj_file} (macOS tiny candidate object)')
3775 } else {
3776 println('[*] Wrote ${obj_file}')
3777 }
3778 }
3779
3780 // Link the object file into an executable
3781 if is_macos_native_target(target_os) {
3782 sdk_res := os.execute('xcrun -sdk macosx --show-sdk-path')
3783 if sdk_res.exit_code != 0 {
3784 eprintln('Link failed:')
3785 eprintln('failed to resolve macOS SDK path with xcrun:')
3786 eprintln(sdk_res.output)
3787 exit(1)
3788 }
3789 sdk_path := macos_sdk_path_from_xcrun_output(sdk_res.output) or {
3790 eprintln('Link failed:')
3791 eprintln(err.msg())
3792 eprintln('xcrun output:')
3793 eprintln(sdk_res.output)
3794 exit(1)
3795 }
3796 validate_macos_sdk_path_for_native_link(sdk_path) or {
3797 eprintln('Link failed:')
3798 eprintln(err.msg())
3799 exit(1)
3800 }
3801 arch_flag := if arch == .arm64 { 'arm64' } else { 'x86_64' }
3802 native_external_inputs = native_external_link_inputs(native_link_flags, output_binary) or {
3803 eprint_native_x64_link_error(err.msg())
3804 exit(1)
3805 }
3806 validate_macos_native_ld_link_flags(native_external_inputs.link_flags) or {
3807 eprint_native_x64_link_error(err.msg())
3808 exit(1)
3809 }
3810 b.compile_native_external_sources(native_external_inputs, native_compile_flags,
3811 target_os, sdk_path, arch_flag) or {
3812 cleanup_native_external_objects(native_external_inputs)
3813 eprint_native_x64_link_error(err.msg())
3814 exit(1)
3815 }
3816 normal_link_cmd := macos_native_link_command(output_binary, obj_file, sdk_path,
3817 arch_flag, false, native_external_inputs.link_flags)
3818 link_cmd := macos_native_link_command(output_binary, obj_file, sdk_path, arch_flag,
3819 used_macos_tiny_object, native_external_inputs.link_flags)
3820 mut link_result := os.execute(link_cmd)
3821 if link_result.exit_code != 0 && used_macos_tiny_object {
3822 if b.pref.verbose {
3823 println('[*] macOS tiny object link failed; retrying with normal Mach-O object')
3824 println('[*] macOS tiny object link exit code: ${link_result.exit_code}')
3825 println('[*] macOS tiny object link output:')
3826 if link_result.output.len > 0 {
3827 print(link_result.output)
3828 if !link_result.output.ends_with('\n') {
3829 println('')
3830 }
3831 } else {
3832 println('<empty>')
3833 }
3834 }
3835 if os.exists(output_binary) {
3836 os.rm(output_binary) or {}
3837 }
3838 obj_format := native_x64_object_format_for_os(target_os)
3839 codegen_abi := native_x64_codegen_abi_for_os(target_os)
3840 mut fallback_gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format,
3841 codegen_abi)
3842 fallback_gen.gen()
3843 fallback_gen.write_file(obj_file)
3844 used_macos_tiny_object = false
3845 if b.pref.verbose {
3846 println('[*] Wrote ${obj_file} (normal Mach-O fallback after macOS tiny link failure)')
3847 }
3848 link_result = os.execute(normal_link_cmd)
3849 }
3850 if link_result.exit_code != 0 {
3851 eprintln('Link failed:')
3852 eprintln(link_result.output)
3853 cleanup_native_external_objects(native_external_inputs)
3854 exit(1)
3855 }
3856 if b.pref.verbose && used_macos_tiny_object {
3857 println('[*] Linked ${output_binary} (built-in macOS tiny object)')
3858 }
3859 } else {
3860 // Linux linking
3861 native_external_inputs = native_external_link_inputs(native_link_flags, output_binary) or {
3862 eprint_native_x64_link_error(err.msg())
3863 exit(1)
3864 }
3865 b.compile_native_external_sources(native_external_inputs, native_compile_flags,
3866 target_os, '', '') or {
3867 cleanup_native_external_objects(native_external_inputs)
3868 eprint_native_x64_link_error(err.msg())
3869 exit(1)
3870 }
3871 link_result := os.execute(linux_native_link_command(b.native_linux_hosted_link_compiler(),
3872 output_binary, obj_file, native_external_inputs.link_flags))
3873 if link_result.exit_code != 0 {
3874 eprintln('Link failed:')
3875 eprintln(link_result.output)
3876 cleanup_native_external_objects(native_external_inputs)
3877 exit(1)
3878 }
3879 }
3880
3881 if b.pref.verbose {
3882 println('[*] Linked ${output_binary}')
3883 }
3884
3885 // Clean up object file
3886 if !b.pref.keep_c {
3887 cleanup_native_external_objects(native_external_inputs)
3888 os.rm(obj_file) or {}
3889 }
3890 }
3891}
3892
3893fn print_time(title string, time_d time.Duration) {
3894 println(' * ${title}: ${time_d.milliseconds()}ms')
3895}
3896
3897fn (mut b Builder) update_parse_summary_counts() {
3898 mut parsed_full_files_n := 0
3899 mut parsed_vh_files_n := 0
3900 mut parsed_full_files := []string{}
3901 mut parsed_vh_files := []string{}
3902 for ff in b.flat.files {
3903 name := b.flat.file_name(ff)
3904 if name.ends_with('.vh') {
3905 parsed_vh_files_n++
3906 parsed_vh_files << name
3907 } else {
3908 parsed_full_files_n++
3909 parsed_full_files << name
3910 }
3911 }
3912 b.parsed_full_files_n = parsed_full_files_n
3913 b.parsed_vh_files_n = parsed_vh_files_n
3914 b.parsed_full_files = parsed_full_files
3915 b.parsed_vh_files = parsed_vh_files
3916 if b.pref.stats {
3917 b.entry_v_lines_n = count_v_lines_for_paths(b.user_files)
3918 b.parsed_v_lines_n = b.count_parsed_v_lines()
3919 } else {
3920 b.entry_v_lines_n = 0
3921 b.parsed_v_lines_n = 0
3922 }
3923}
3924
3925fn (b &Builder) print_flat_ast_summary() {
3926 legacy_stats := ast.legacy_ast_stats(b.files)
3927 legacy_nodes := ast.count_legacy_nodes(b.files)
3928 flat_stats := b.flat.stats()
3929 mut mem_delta_pct := f64(0)
3930 if legacy_stats.bytes_estimate > 0 {
3931 mem_delta_pct = (f64(legacy_stats.bytes_estimate) - f64(flat_stats.bytes_estimate)) * 100.0 / f64(legacy_stats.bytes_estimate)
3932 }
3933 // Flat AST uses 4 arenas (files, nodes, edges, strings) regardless of payload
3934 // count; each is one allocation amortised across millions of cells.
3935 flat_allocs := u64(4)
3936 mut alloc_delta_pct := f64(0)
3937 if legacy_stats.allocs > 0 {
3938 alloc_delta_pct = (f64(legacy_stats.allocs) - f64(flat_allocs)) * 100.0 / f64(legacy_stats.allocs)
3939 }
3940 println(' * AST nodes: legacy=${legacy_nodes}, flat=${flat_stats.nodes} (edges=${flat_stats.edges}, strings=${flat_stats.strings})')
3941 println(' * AST memory est: legacy=${legacy_stats.bytes_estimate}B, flat=${flat_stats.bytes_estimate}B (${mem_delta_pct:.2f}% reduction)')
3942 println(' * AST allocs: legacy=${legacy_stats.allocs}, flat=${flat_allocs} (${alloc_delta_pct:.2f}% reduction)')
3943 if os.getenv('V2_FLAT_HIST') != '' {
3944 hist := b.flat.count_nodes_by_kind()
3945 mut keys := hist.keys()
3946 keys.sort_with_compare(fn [hist] (a &string, b &string) int {
3947 return hist[*b] - hist[*a]
3948 })
3949 println(' * AST per-kind histogram (top 20):')
3950 for i, k in keys {
3951 if i >= 20 {
3952 break
3953 }
3954 println(' ${k:-30s} ${hist[k]}')
3955 }
3956 }
3957}
3958
3959fn count_v_lines_for_paths(paths []string) int {
3960 mut seen_paths := map[string]bool{}
3961 mut total_v_lines := 0
3962 for path in paths {
3963 norm_path := os.norm_path(path)
3964 if norm_path in seen_paths {
3965 continue
3966 }
3967 seen_paths[norm_path] = true
3968 lines := os.read_lines(norm_path) or { continue }
3969 total_v_lines += lines.len
3970 }
3971 return total_v_lines
3972}
3973
3974fn (b &Builder) count_parsed_v_lines() int {
3975 mut parsed_paths := []string{}
3976 mut seen_files := map[string]bool{}
3977 for ff in b.flat.files {
3978 name := b.flat.file_name(ff)
3979 if name in seen_files {
3980 continue
3981 }
3982 seen_files[name] = true
3983 parsed_paths << name
3984 }
3985 return count_v_lines_for_paths(parsed_paths)
3986}
3987
3988fn print_parse_summary(parsed_full_files_n int, parsed_vh_files_n int, entry_v_lines_n int, parsed_v_lines_n int, show_stats bool, print_parsed_files bool, parsed_full_files []string, parsed_vh_files []string) {
3989 println(' * Parsed files: fully parsed files: ${parsed_full_files_n}, parsed .vh files: ${parsed_vh_files_n}')
3990 if print_parsed_files {
3991 if parsed_full_files.len > 0 {
3992 println(' * Fully parsed files:')
3993 for path in parsed_full_files {
3994 println(' [full] ${path}')
3995 }
3996 }
3997 }
3998 if (show_stats || print_parsed_files) && parsed_vh_files.len > 0 {
3999 println(' * Parsed .vh files:')
4000 for path in parsed_vh_files {
4001 println(' [vh] ${path}')
4002 }
4003 }
4004 if show_stats {
4005 println(' * Parsed V LOC (entry files): ${entry_v_lines_n}')
4006 println(' * Parsed V LOC (all parsed sources): ${parsed_v_lines_n}')
4007 }
4008}
4009