Files

4009 lines
133 KiB
V

// Copyright (c) 2020-2024 Joe Conigliaro. All rights reserved.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
module builder
import os
import v2.ast
import v2.abi
import v2.eval
import v2.gen.arm64
import v2.gen.c
import v2.gen.cleanc
import v2.gen.v as gen_v
import v2.gen.x64
import v2.insel
import v2.markused
import v2.parser
import v2.mir
import v2.pref
import v2.ssa
import v2.ssa.optimize as ssa_optimize
import v2.token
import v2.transformer
import v2.types
import time
import runtime
const staged_c_file = '/tmp/v2_codegen.tmp.c'
const staged_main_obj_file = '/tmp/v2_codegen.tmp.main.o'
struct Builder {
pref &pref.Preferences
mut:
files []ast.File
user_files []string // original user-provided files (for output name)
file_set &token.FileSet = token.FileSet.new()
env &types.Environment = unsafe { nil } // Type checker environment
parsed_full_files_n int
parsed_vh_files_n int
entry_v_lines_n int
parsed_v_lines_n int
parsed_full_files []string
parsed_vh_files []string
used_fn_keys map[string]bool
cached_called_fn_names map[string]bool
v2compiler_generic_setup_snapshot cleanc.GenericSetupSnapshot
has_v2compiler_generic_setup_snapshot bool
used_vh_for_parse bool
used_import_vh_for_parse bool
used_virtual_vh_for_parse bool
// flat is the canonical parse output. parse_batch streams directly into
// flat_builder so b.flat is built incrementally during parsing rather than
// via a redundant flatten_files() pass afterwards.
flat ast.FlatAst
flat_builder ast.FlatBuilder
// flat_builder_inited tracks whether flat_builder has been seeded with
// pre-sized arenas. We can only size after we know the input set, so
// the first parse_batch call lazily initializes it.
flat_builder_inited bool
// native_flat_pipeline_enabled means the transform phase produced a
// post-transform FlatAst and intentionally did not materialize b.files.
native_flat_pipeline_enabled bool
// Source snapshot used only for an isolated macOS tiny candidate graph.
// The normal hosted graph is still built separately and remains the fallback.
macos_tiny_candidate_source_files []ast.File
macos_tiny_candidate_source_flat ast.FlatAst
}
pub fn new_builder(prefs &pref.Preferences) &Builder {
unsafe {
return &Builder{
pref: prefs
used_fn_keys: map[string]bool{}
cached_called_fn_names: map[string]bool{}
}
}
}
// exec_build_c_file returns the staging C file path for executable builds.
// A stable temp path avoids output-name-derived string construction in
// self-hosted arm64 binaries, where that path is currently unstable.
fn (b &Builder) exec_build_c_file(output_name string) string {
if b.pref.keep_c {
return output_name + '.c'
}
return staged_c_file
}
fn (b &Builder) should_use_native_flat_pipeline() bool {
if os.getenv('V2_NATIVE_FLAT') == '' {
return false
}
if os.getenv('V2_NO_NATIVE_FLAT') != '' {
return false
}
return b.pref.backend == .arm64 && b.pref.hot_fn.len == 0
}
fn (b &Builder) backend_uses_markused_pruning() bool {
return b.pref.backend != .arm64
}
// should_skip_markused_for_self_build avoids a self-host-only pruning pass that
// costs more than it saves once the v2 compiler object caches are hot.
fn (b &Builder) should_skip_markused_for_self_build() bool {
return b.pref.backend == .cleanc && b.is_cmd_v2_self_build()
}
fn (b &Builder) should_build_ssa_from_flat() bool {
return b.flat.files.len > 0
}
fn (b &Builder) should_keep_flat_for_codegen() bool {
return match b.pref.backend {
.cleanc, .c, .x64, .arm64 { true }
else { false }
}
}
fn (b &Builder) can_compile_cleanc_locally() bool {
if b.pref == unsafe { nil } {
return true
}
return b.pref.can_compile_cleanc_locally()
}
fn (b &Builder) cflags_target_os_for_local_compile() string {
if b.pref == unsafe { nil } {
return normalize_target_os_name(os.user_os())
}
if b.pref.is_cross_target() && b.can_compile_cleanc_locally() {
return b.pref.source_filter_target_os()
}
return b.pref.target_os_or_host()
}
fn cleanc_c_output_name(output_name string) string {
if output_name.ends_with('.c') {
return output_name
}
return output_name + '.c'
}
fn (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) {
cc_start := sw.elapsed()
if b.pref.is_shared_lib {
// Shared library: compile with -shared -fPIC -undefined dynamic_lookup
// Use -fvisibility=hidden so only explicitly exported (impl_live_*) symbols
// are visible. All other functions become hidden, causing the dylib to
// resolve them from the host executable at load time.
mut cc_cmd := '${cc} ${cc_flags} -shared -fPIC -fvisibility=hidden -undefined dynamic_lookup -w -Wno-incompatible-function-pointer-types "${staged_c_file}"'
if cc_link_flags.len > 0 {
cc_cmd += ' -x none ${cc_link_flags}'
}
cc_cmd += ' -o "${output_name}"${error_limit_flag}'
run_cc_cmd_or_exit(cc_cmd, 'shared lib compilation', b.pref.show_cc)
print_time('CC (shared)', time.Duration(sw.elapsed() - cc_start))
println('[*] Compiled shared library ${output_name}')
return
}
// Non-cached path: compile and link in one step.
// Place link flags (which may include .o files) AFTER the source file.
// Use `-x none` to reset the language before .o files, since -x objective-c
// would cause cc to treat .o files as source code.
mut cc_cmd := '${cc} ${cc_flags} -w -Wno-incompatible-function-pointer-types "${staged_c_file}"'
if cc_link_flags.len > 0 {
cc_cmd += ' -x none ${cc_link_flags}'
}
cc_cmd += ' -o "${output_name}"${error_limit_flag}'
run_cc_cmd_or_exit(cc_cmd, 'C compilation', b.pref.show_cc)
print_time('CC', time.Duration(sw.elapsed() - cc_start))
println('[*] Compiled ${output_name}')
}
// print_rss prints process RSS in MB at the given phase boundary.
// Gated on V2_MEM=1.
//
// CAVEAT: v2 is force-built with `-gc none` (see is_v2_compiler_target in
// vlib/v/pref/default.v), so allocations are never reclaimed. RSS is
// dominated by the OS's decision of which pages to keep resident, not by
// what the program is actually using. Run-to-run variance is huge
// (observed 500 MB to 4.8 GB on identical runs). Treat these numbers as
// a coarse signal, not a precision metric.
//
// For reliable peak-memory comparisons between optimizations, use
// `/usr/bin/time -l <cmd>` and read `peak memory footprint` — that
// metric is stable to within ~0.1% across runs.
fn print_rss(stage string) {
if os.getenv('V2_MEM') == '' {
return
}
rss := runtime.used_memory() or { 0 }
$if macos {
// Under -gc none nothing is freed, so `live` is monotonic and its
// per-phase delta is the exact bytes that phase allocated. `peak` is
// the high-water mark. Both are stable run-to-run, unlike `rss`.
live, peak := darwin_live_malloc_bytes()
mb := u64(1024 * 1024)
eprintln(' [mem] ${stage}: live ${live / mb} MB peak ${peak / mb} MB (rss ${rss / mb} MB)')
return
}
eprintln(' [mem] ${stage}: ${rss / (1024 * 1024)} MB')
}
pub fn (mut b Builder) build(files []string) {
b.user_files = files
// Pre-parse fast path: for cmd/v2 self-host, if the cached bundle objects + main.o are
// present (restored from the durable tier if /tmp was wiped) and all sources are fresh,
// relink directly and skip the entire front-end. Falls through on any staleness.
if b.try_self_build_fast_relink() {
return
}
mut sw := time.new_stopwatch()
print_rss('start')
$if parallel ? {
if b.pref.no_parallel {
b.parse_files(files)
} else {
b.parse_files_parallel(files)
}
} $else {
b.parse_files(files)
}
b.files = []ast.File{}
parse_time := sw.elapsed()
print_time('Scan & Parse', parse_time)
print_rss('after parse')
// FlatBuilder is the canonical parse output; both parse paths stream into
// it. parse_files / parse_files_parallel return [] in flat mode, so b.flat
// is the live source of truth from here on. The rehydration to legacy
// []ast.File is deferred until a compatibility consumer actually needs it.
b.flat = b.flat_builder.flat
b.update_parse_summary_counts()
print_parse_summary(b.parsed_full_files_n, b.parsed_vh_files_n, b.entry_v_lines_n,
b.parsed_v_lines_n, b.pref.stats, b.pref.print_parsed_files, b.parsed_full_files,
b.parsed_vh_files)
if b.pref.stats {
b.print_flat_ast_summary()
}
if b.pref.backend == .cleanc && !b.validate_freestanding_cleanc_contract() {
exit(1)
}
if b.pref.skip_type_check {
b.env = types.Environment.new()
} else {
b.env = if b.pref.no_parallel {
b.type_check_files()
} else {
b.type_check_files_parallel()
}
}
type_check_time := time.Duration(sw.elapsed() - parse_time)
print_time('Type Check', type_check_time)
print_rss('after type check')
b.prepare_macos_tiny_candidate_source_files()
// Transform AST (flag enum desugaring, etc.)
transform_start := sw.elapsed()
mut trans := transformer.Transformer.new_with_pref(b.env, b.pref)
trans.set_file_set(b.file_set)
sequential_transform := b.pref.no_parallel_transform || b.pref.ownership
use_native_flat_pipeline := b.should_use_native_flat_pipeline()
b.native_flat_pipeline_enabled = use_native_flat_pipeline
// The transform is flat-only for every backend: both the sequential and
// the parallel path emit cursor-native into a FlatBuilder and never
// materialize a transformed []ast.File. The legacy backends that still
// consume []ast.File (.v/eval) rehydrate once from the transformed flat
// at the codegen boundary below instead of dragging a compatibility
// path through the transform.
if sequential_transform {
b.flat = trans.transform_flat_to_flat_direct(&b.flat, b.files)
} else {
b.flat = b.transform_files_parallel_flat_direct(mut trans)
}
b.files = []ast.File{}
transform_time := time.Duration(sw.elapsed() - transform_start)
print_time('Transform', transform_time)
print_rss('after transform')
// Mark used functions/methods for backend pruning.
if b.pref.no_markused || !b.backend_uses_markused_pruning()
|| b.should_skip_markused_for_self_build() {
b.used_fn_keys = map[string]bool{}
} else {
mark_used_start := sw.elapsed()
// Flat markused consumes the post-transform FlatAst. Both sequential
// and parallel paths populate b.flat as part of their *_to_flat wedge,
// so the separate flatten_files() pass is gone.
if b.uses_minimal_x64_runtime_roots() {
opts := markused.MarkUsedOptions{
minimal_runtime_roots: true
}
b.used_fn_keys = markused.mark_used_flat_with_options(&b.flat, b.env, opts)
} else {
b.used_fn_keys = markused.mark_used_flat(&b.flat, b.env)
}
mark_used_time := time.Duration(sw.elapsed() - mark_used_start)
print_time('Mark Used', mark_used_time)
print_rss('after markused')
}
if !b.should_keep_flat_for_codegen() {
// .v/eval still consume legacy []ast.File: rehydrate once from the
// transformed flat at the backend boundary, then drop the flat.
if (b.pref.backend == .v && !b.pref.skip_genv) || b.pref.backend == .eval {
b.files = b.flat.to_files()
}
b.flat = ast.FlatAst{}
}
// Generate output based on backend
match b.pref.backend {
.v {
if !b.pref.skip_genv {
b.gen_v_files()
}
}
.cleanc {
b.gen_cleanc()
}
.c {
b.gen_ssa_c()
}
.x64 {
b.gen_native(.x64)
}
.arm64 {
b.gen_native(.arm64)
}
.eval {
mut runner := eval.new(b.pref)
runner.run_files(b.files) or {
eprintln('error: ${err.msg()}')
exit(1)
}
}
}
print_time('Total', sw.elapsed())
print_rss('after codegen (peak)')
}
fn (mut b Builder) gen_v_files() {
mut gen := gen_v.new_gen(b.pref)
for file in b.files {
gen.gen(file)
if b.pref.debug {
gen.print_output()
}
}
}
fn (mut b Builder) gen_cleanc() {
// Clean C Backend (AST -> C)
mut sw := time.new_stopwatch()
// The cached-core split is currently unstable in some module builds
// (including cmd/v2 self-host and directory-style user module builds).
// Force single-unit cleanc generation there.
force_no_cache := b.should_disable_cleanc_cache()
use_cache := !b.pref.no_cache && !force_no_cache
if os.getenv('V2_TRACE_CACHE') != '' {
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}')
}
// Determine output name
output_name := if b.pref.output_file != '' {
b.pref.output_file
} else if b.user_files.len > 0 {
b.default_output_name()
} else {
'out'
}
generation_only := output_name.ends_with('.c') || !b.can_compile_cleanc_locally()
if generation_only {
c_output_name := cleanc_c_output_name(output_name)
c_source := b.gen_cleanc_source([]string{})
print_time('C Gen', sw.elapsed())
if c_source == '' {
eprintln('error: cleanc backend is not fully functional (compiled with stubbed functions)')
eprintln('hint: use v2 compiled with v1 for proper C code generation')
return
}
os.write_file(c_output_name, c_source) or { panic(err) }
if output_name.ends_with('.c') {
println('[*] Wrote ${c_output_name}')
} else {
println('[*] Wrote ${c_output_name} (local C compilation disabled for this target)')
}
return
}
mut cc := if b.pref.ccompiler.len > 0 {
b.pref.ccompiler
} else {
configured_cc(b.pref.vroot)
}
// -prod requires a real optimizing compiler — TCC cannot handle -O3/-flto.
// Switch to system cc (gcc/clang) when the default compiler is TCC.
if b.pref.is_prod && cc.contains('tcc') {
cc = 'cc'
}
directive_flags := b.collect_cflags_from_sources()
$if macos {
if cc.contains('tcc') && cflags_need_objc_mode(directive_flags) {
cc = 'cc'
}
}
// Separate directive flags into compile-only and link-only flags.
// -framework, -l, -L, .o/.a/.so/.dylib are linker flags and must NOT
// be passed during -c compilation (they can trigger unwanted header
// processing, e.g. MetalKit SIMD errors on macOS).
directive_compile_flags, directive_link_flags := split_compile_and_link_flags(directive_flags)
mut cc_flag_parts := []string{}
mut cc_link_parts := []string{}
env_flags := configured_cflags()
if env_flags.trim_space() != '' {
cc_flag_parts << env_flags.trim_space()
}
if cc.contains('tcc') && directive_flags.contains('thirdparty/sqlite/sqlite3.c')
&& !directive_compile_flags.contains('SQLITE_DISABLE_INTRINSIC') {
cc_flag_parts << '-DSQLITE_DISABLE_INTRINSIC'
}
if directive_compile_flags.trim_space() != '' {
cc_flag_parts << directive_compile_flags.trim_space()
}
if directive_link_flags.trim_space() != '' {
cc_link_parts << directive_link_flags.trim_space()
}
tcc_extra := tcc_flags(cc, b.pref.vroot)
if tcc_extra.trim_space() != '' {
cc_flag_parts << tcc_extra.trim_space()
}
// macOS code can include Objective-C (.m) files via #include directives.
// Tell the C compiler to treat the source as Objective-C only when needed.
$if macos {
if cflags_need_objc_mode(directive_flags) {
cc_flag_parts << '-x objective-c'
}
}
cc_flag_parts << '-std=gnu11'
cc_flag_parts << '-fwrapv'
// Detect compiler type for optimization flags and error limit.
is_tcc := cc.contains('tcc')
mut is_clang := false
if !is_tcc {
version_res := os.execute('${cc} --version')
if version_res.exit_code == 0 && version_res.output.contains('clang') {
is_clang = true
}
}
// -prod: add -O3, -flto, -DNDEBUG for gcc/clang
if b.pref.is_prod {
cc_flag_parts << '-O3'
cc_flag_parts << '-DNDEBUG'
if !b.pref.is_shared_lib {
$if !windows {
cc_flag_parts << '-flto'
}
}
if !is_clang {
cc_flag_parts << '-fno-strict-aliasing'
}
}
cc_flags := cc_flag_parts.join(' ')
cc_link_flags := cc_link_parts.join(' ')
mut error_limit_flag := ''
if is_clang {
error_limit_flag = ' -ferror-limit=0'
}
// Fast path: cache one core object (builtin+strconv), compile/link only the rest.
if use_cache && !b.pref.skip_builtin && b.has_module('builtin') && b.has_module('strconv') {
if b.gen_cleanc_with_cached_core(output_name, cc, cc_flags, cc_link_flags,
error_limit_flag, mut sw)
{
// Mirror the freshly built objects to the durable tier so a later cold
// build (with /tmp wiped) can restore them and fast-relink.
b.save_durable_object_cache(b.core_cache_dir())
return
}
}
// Fallback: compile one full C translation unit.
c_source := b.gen_cleanc_source([]string{})
print_time('C Gen', sw.elapsed())
if c_source == '' {
eprintln('error: cleanc backend is not fully functional (compiled with stubbed functions)')
eprintln('hint: use v2 compiled with v1 for proper C code generation')
return
}
os.write_file(staged_c_file, c_source) or { panic(err) }
println('[*] Wrote ${staged_c_file}')
b.compile_cleanc_executable(output_name, cc, cc_flags, cc_link_flags, error_limit_flag, mut sw)
}
fn (b &Builder) is_cmd_v2_self_build() bool {
if b.user_files.len != 1 {
return false
}
// Avoid path normalization here: during bootstraps, some intermediate
// compilers can still have unstable path helpers.
path := b.user_files[0].replace('\\', '/')
if path == 'v2.v' || path.ends_with('/v2.v') {
return true
}
return path.ends_with('/cmd/v2/v2.v') || path.ends_with('cmd/v2/v2.v')
}
fn (b &Builder) should_disable_cleanc_cache() bool {
// ARM64 cache previously disabled due to runtime helpers being emitted
// as static inline, which dropped them at cached-core boundaries.
// Fixed: __v2_array_eq is now a regular function with its body in the
// builtin cache unit and a forward declaration in the main TU.
for raw_input in b.user_files {
input := raw_input.trim_right('/\\')
if input.len == 0 {
continue
}
if input.ends_with('.v') || input.ends_with('.vv') || input.ends_with('.vsh')
|| input.ends_with('.vh') {
continue
}
}
return false
}
fn (b &Builder) default_output_name() string {
if b.user_files.len == 0 {
return 'out'
}
last_input := b.user_files[b.user_files.len - 1].trim_right('/\\')
if last_input.len == 0 || last_input == '.' {
cwd := os.getwd()
base := os.file_name(cwd)
return if base.len > 0 { base } else { 'out' }
}
if os.is_dir(last_input) {
base := os.file_name(last_input)
return if base.len > 0 { base } else { 'out' }
}
base := os.file_name(last_input).all_before_last('.v')
return if base.len > 0 { base } else { os.file_name(last_input) }
}
fn (mut b Builder) gen_ssa_c() {
// SSA -> C backend.
mut sw := time.new_stopwatch()
mut mod := ssa.Module.new('main')
if mod == unsafe { nil } {
eprintln('error: ssa c backend not available (compiled with stubbed ssa module)')
eprintln('hint: use v2 compiled with v1 for ssa c code generation')
return
}
mut ssa_builder := ssa.Builder.new_with_env(mod, b.env)
ssa_builder.target_os = b.pref.target_os_or_host()
mut stage_start := sw.elapsed()
mut built_from_flat := false
if b.should_build_ssa_from_flat() {
ssa_builder.build_all_from_flat(&b.flat)
built_from_flat = true
} else {
ssa_builder.build_all(b.files)
}
print_time('SSA Build', time.Duration(sw.elapsed() - stage_start))
// TODO: re-enable SSA optimization once the new builder is mature
// stage_start = sw.elapsed()
// optimize.optimize(mut mod)
// print_time('SSA Optimize', time.Duration(sw.elapsed() - stage_start))
output_name := if b.pref.output_file != '' {
b.pref.output_file
} else if b.user_files.len > 0 {
b.default_output_name()
} else {
'out'
}
if output_name.ends_with('.c') {
if built_from_flat {
b.flat = ast.FlatAst{}
}
stage_start = sw.elapsed()
mut gen := c.new_gen(mod)
c_source := gen.gen()
print_time('C Gen', time.Duration(sw.elapsed() - stage_start))
if c_source == '' {
eprintln('error: ssa c backend failed to generate C source')
return
}
os.write_file(output_name, c_source) or { panic(err) }
println('[*] Wrote ${output_name}')
return
}
cc := if b.pref.ccompiler.len > 0 { b.pref.ccompiler } else { configured_cc(b.pref.vroot) }
directive_flags := b.collect_cflags_from_sources()
if built_from_flat {
// SSA has copied the program into MIR, and directive scanning has read
// source names from the FlatAst. Keep the later C generator/compiler
// working sets clear of the transformed FlatAst.
b.flat = ast.FlatAst{}
}
mut cc_flag_parts := []string{}
env_flags := configured_cflags()
if env_flags.trim_space() != '' {
cc_flag_parts << env_flags.trim_space()
}
if cc.contains('tcc') && directive_flags.contains('thirdparty/sqlite/sqlite3.c')
&& !directive_flags.contains('SQLITE_DISABLE_INTRINSIC') {
cc_flag_parts << '-DSQLITE_DISABLE_INTRINSIC'
}
if directive_flags.trim_space() != '' {
cc_flag_parts << directive_flags.trim_space()
}
tcc_extra := tcc_flags(cc, b.pref.vroot)
if tcc_extra.trim_space() != '' {
cc_flag_parts << tcc_extra.trim_space()
}
cc_flags := cc_flag_parts.join(' ')
mut error_limit_flag := ''
if !cc.contains('tcc') {
version_res := os.execute('${cc} --version')
if version_res.exit_code == 0 && version_res.output.contains('clang') {
error_limit_flag = ' -ferror-limit=0'
}
}
// Try to get pre-compiled builtin.o and vlib.o from the cleanc cache
mut builtin_obj := ''
mut vlib_obj := ''
if !b.pref.skip_builtin && b.has_module('builtin') && b.has_module('strconv')
&& b.ensure_core_cache_dir() {
cache_dir := b.core_cache_dir()
builtin_obj = b.ensure_cached_module_object(cache_dir, builtin_cache_name,
builtin_cached_module_paths, builtin_cached_module_names, cc, cc_flags, '',
error_limit_flag, false) or { '' }
if builtin_obj.len > 0 && vlib_cached_module_paths.len > 0 {
vlib_obj = b.ensure_cached_module_object(cache_dir, vlib_cache_name,
vlib_cached_module_paths, vlib_cached_module_names, cc, cc_flags, '',
error_limit_flag, false) or { '' }
}
}
stage_start = sw.elapsed()
mut gen := c.new_gen(mod)
gen.link_builtin = builtin_obj.len > 0
c_source := gen.gen()
print_time('C Gen', time.Duration(sw.elapsed() - stage_start))
if c_source == '' {
eprintln('error: ssa c backend failed to generate C source')
return
}
c_file := b.exec_build_c_file(output_name)
os.write_file(c_file, c_source) or { panic(err) }
if c_file != staged_c_file {
os.write_file(staged_c_file, c_source) or { panic(err) }
}
println('[*] Wrote ${c_file}')
cc_start := sw.elapsed()
mut cc_cmd := ''
if builtin_obj.len > 0 {
// Compile SSA main.c and link against pre-compiled builtin.o
main_obj := staged_main_obj_file
compile_cmd := '${cc} ${cc_flags} -w -c "${c_file}" -o "${main_obj}"${error_limit_flag}'
if b.pref.show_cc {
println(compile_cmd)
}
compile_res := os.execute(compile_cmd)
if compile_res.exit_code != 0 {
eprintln('error: ssa c backend compilation failed')
lines := compile_res.output.split_into_lines()
limit := if lines.len < 20 { lines.len } else { 20 }
for line in lines[..limit] {
eprintln(line)
}
exit(1)
}
mut link_objects := '"${main_obj}" "${builtin_obj}"'
if vlib_obj.len > 0 {
link_objects += ' "${vlib_obj}"'
}
cc_cmd = '${cc} ${cc_flags} -w ${link_objects} -o "${output_name}"'
if b.pref.show_cc {
println(cc_cmd)
}
cc_res := os.execute(cc_cmd)
if cc_res.exit_code != 0 {
eprintln('error: ssa c backend linking failed')
lines := cc_res.output.split_into_lines()
limit := if lines.len < 20 { lines.len } else { 20 }
for line in lines[..limit] {
eprintln(line)
}
exit(1)
}
if !b.pref.keep_c {
os.rm(main_obj) or {}
}
} else {
// Single-file compilation (no builtin linking)
cc_cmd = '${cc} ${cc_flags} -w "${c_file}" -o "${output_name}"${error_limit_flag}'
if b.pref.show_cc {
println(cc_cmd)
} else if os.getenv('V2VERBOSE') != '' {
dump(cc_cmd)
}
cc_res := os.execute(cc_cmd)
if cc_res.exit_code != 0 {
eprintln('error: ssa c backend compilation failed')
lines := cc_res.output.split_into_lines()
limit := if lines.len < 20 { lines.len } else { 20 }
for line in lines[..limit] {
eprintln(line)
}
exit(1)
}
}
print_time('CC', time.Duration(sw.elapsed() - cc_start))
if !b.pref.keep_c {
os.rm(c_file) or {}
}
println('[*] Compiled ${output_name}')
}
fn (mut b Builder) gen_cleanc_source(modules []string) string {
return b.gen_cleanc_source_with_options(modules, []string{}, false, '', []string{}, true,
[]string{})
}
fn (mut b Builder) gen_cleanc_source_for_cache(modules []string, cache_bundle_name string, use_markused bool) string {
return b.gen_cleanc_source_with_options(modules, []string{}, true, cache_bundle_name,
[]string{}, use_markused, []string{})
}
fn (mut b Builder) gen_cleanc_source_for_cache_files(modules []string, emit_files []string, cache_bundle_name string, use_markused bool) string {
return b.gen_cleanc_source_with_options(modules, emit_files, true, cache_bundle_name,
[]string{}, use_markused, []string{})
}
fn (mut b Builder) gen_cleanc_source_with_cache_init_calls(modules []string, cached_init_calls []string) string {
return b.gen_cleanc_source_with_options(modules, []string{}, false, '', cached_init_calls,
true, []string{})
}
fn (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 {
return b.gen_cleanc_source_with_options(modules, emit_files, false, '', cached_init_calls,
use_markused, []string{})
}
fn (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 {
return b.gen_cleanc_source_with_options(modules, emit_files, false, '', cached_init_calls,
use_markused, force_emit_fn_names)
}
fn (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 {
type_modules := if cache_bundle_name.len > 0 {
b.expand_type_modules_with_imports(cache_type_module_names(cache_bundle_name, modules))
} else {
cache_type_module_names(cache_bundle_name, modules)
}
mut type_module_names := map[string]bool{}
restrict_to_cache_modules := cache_bundle_name.len > 0 && type_modules.len > 0
if restrict_to_cache_modules {
for module_name in type_modules {
if module_name != '' {
type_module_names[module_name] = true
}
}
}
// Cache-bundle generation restricts emission to a fixed set of type
// modules. The legacy gen achieves this by filtering its input `gen_files`;
// the flat gen drives every pass off `flat.files`, so for a restricted
// bundle in flat mode we hand it a FlatAst scoped to the bundle's type
// modules (sharing b.flat's arena). The main translation unit
// (`restrict_to_cache_modules == false`) uses the full b.flat.
scope_flat_bundle := restrict_to_cache_modules && b.flat.files.len > 0
scoped_flat := if scope_flat_bundle {
b.flat_scoped_to_modules(type_module_names)
} else {
ast.FlatAst{}
}
mut gen_files := []ast.File{cap: b.files.len}
if !scope_flat_bundle {
for file in b.files {
if restrict_to_cache_modules && ast_file_module_name(file) !in type_module_names {
continue
}
gen_files << file
}
}
if !scope_flat_bundle && cached_init_calls.len > 0 && b.used_vh_for_parse {
mut has_vh_files := false
for file in gen_files {
if file.name.ends_with('.vh') {
has_vh_files = true
break
}
}
if !has_vh_files {
mut p := parser.Parser.new(b.pref)
header_files := p.parse_files(b.core_cached_parse_paths(), mut b.file_set)
for header_file in header_files {
gen_files << header_file
}
}
}
mut gen := if scope_flat_bundle {
cleanc.Gen.new_with_env_pref_and_flat(&scoped_flat, b.env, b.pref)
} else if b.flat.files.len > 0 {
cleanc.Gen.new_with_env_pref_and_flat(&b.flat, b.env, b.pref)
} else {
cleanc.Gen.new_with_env_and_pref(gen_files, b.env, b.pref)
}
if modules.len > 0 {
gen.set_emit_modules(modules)
}
if type_modules.len > 0 {
gen.set_type_modules(type_modules)
}
if emit_files.len > 0 {
gen.set_emit_files(emit_files)
}
if use_markused && b.used_fn_keys.len > 0 {
gen.set_used_fn_keys(b.used_fn_keys)
}
if force_emit_fn_names.len > 0 {
gen.set_force_emit_fn_names(force_emit_fn_names)
}
gen.set_export_const_symbols(export_const_symbols)
if cache_bundle_name.len > 0 {
gen.set_cache_bundle_name(cache_bundle_name)
}
if cached_init_calls.len > 0 {
gen.set_cached_init_calls(cached_init_calls)
}
if cache_bundle_name.len == 0 && cached_init_calls.len > 0 && b.cached_called_fn_names.len > 0 {
gen.add_called_fn_names(b.cached_called_fn_name_list())
}
if cache_bundle_name.len == 0 && cached_init_calls.len > 0
&& b.has_v2compiler_generic_setup_snapshot {
gen.use_generic_setup_snapshot(b.v2compiler_generic_setup_snapshot)
}
use_parallel := b.pref != unsafe { nil } && !b.pref.no_parallel
if use_parallel {
gen.gen_passes_1_to_4()
if cache_bundle_name == v2compiler_cache_name {
b.v2compiler_generic_setup_snapshot = gen.generic_setup_snapshot()
b.has_v2compiler_generic_setup_snapshot = true
}
b.gen_cleanc_parallel(mut gen)
source := gen.gen_finalize()
if cache_bundle_name.len > 0 {
b.add_cached_called_fn_names(gen.external_called_fn_names())
}
if os.getenv('V2TRACE_CLEANC') != '' {
eprintln('TRACE_CLEANC builder_files=${b.files.len} gen_files=${gen_files.len} source_len=${source.len}')
}
return source
}
source := gen.gen()
if cache_bundle_name == v2compiler_cache_name {
b.v2compiler_generic_setup_snapshot = gen.generic_setup_snapshot()
b.has_v2compiler_generic_setup_snapshot = true
}
if cache_bundle_name.len > 0 {
b.add_cached_called_fn_names(gen.external_called_fn_names())
}
if os.getenv('V2TRACE_CLEANC') != '' {
eprintln('TRACE_CLEANC builder_files=${b.files.len} gen_files=${gen_files.len} source_len=${source.len}')
}
return source
}
fn (mut b Builder) add_cached_called_fn_names(names []string) {
for name in names {
if name.len > 0 {
b.cached_called_fn_names[name] = true
}
}
}
fn (b &Builder) cached_called_fn_name_list() []string {
mut names := b.cached_called_fn_names.keys()
names.sort()
return names
}
fn cached_called_fn_names_path(cache_dir string, cache_name string) string {
return cache_path_join(cache_dir, '${cache_name}.calls')
}
fn (mut b Builder) load_cached_called_fn_names(cache_dir string, cache_name string) bool {
data := os.read_file(cached_called_fn_names_path(cache_dir, cache_name)) or { return false }
for line in data.split_into_lines() {
name := line.trim_space()
if name.len > 0 {
b.cached_called_fn_names[name] = true
}
}
return true
}
fn (mut b Builder) write_cached_called_fn_names(cache_dir string, cache_name string, before map[string]bool) {
mut names := []string{}
for name, _ in b.cached_called_fn_names {
if name.len == 0 || name in before {
continue
}
names << name
}
names.sort()
os.write_file(cached_called_fn_names_path(cache_dir, cache_name), names.join('\n')) or {}
}
fn (b &Builder) cgen_builder_stats_enabled() bool {
return b.pref != unsafe { nil } && b.pref.stats
}
fn (b &Builder) mark_cgen_builder_step(stats_enabled bool, cache_name string, mut sw time.StopWatch, stage_start time.Duration, step string) time.Duration {
if !stats_enabled {
return stage_start
}
now := sw.elapsed()
println(' - C Gen/cache:${cache_name} cache.${step}: ${time.Duration(now - stage_start).milliseconds()}ms')
return now
}
fn cache_type_module_names(cache_bundle_name string, emit_modules []string) []string {
if cache_bundle_name == '' {
return emit_modules
}
mut names := []string{cap: core_cached_module_names.len + emit_modules.len}
if cache_bundle_name == builtin_cache_name {
names << emit_modules
} else if cache_bundle_name == vlib_cache_name {
names << builtin_cached_module_names
names << emit_modules
} else {
names << core_cached_module_names
names << emit_modules
}
return unique_sorted_strings(names)
}
fn (b &Builder) expand_type_modules_with_imports(modules []string) []string {
mut type_modules := map[string]bool{}
for module_name in modules {
if module_name != '' {
type_modules[module_name] = true
}
}
use_flat := b.uses_flat_module_enumeration()
mut changed := true
for changed {
changed = false
if use_flat {
for i in 0 .. b.flat.files.len {
if b.flat_file_module_name(i) !in type_modules {
continue
}
for import_stmt in b.flat.file_cursor(i).imports().import_stmts() {
import_module := import_module_name(import_stmt.name)
if import_module == '' || import_module in type_modules {
continue
}
type_modules[import_module] = true
changed = true
}
}
} else {
for file in b.files {
if ast_file_module_name(file) !in type_modules {
continue
}
for import_stmt in file.imports {
import_module := import_module_name(import_stmt.name)
if import_module == '' || import_module in type_modules {
continue
}
type_modules[import_module] = true
changed = true
}
}
}
}
return unique_sorted_strings(type_modules.keys())
}
fn (b &Builder) has_external_cache_module_name_collision(module_names []string) bool {
mut module_set := map[string]bool{}
for module_name in module_names {
if module_name != '' {
module_set[module_name] = true
}
}
mut vlib_modules := map[string]bool{}
mut external_modules := map[string]bool{}
if b.uses_flat_module_enumeration() {
for i in 0 .. b.flat.files.len {
name := b.flat.file_name(b.flat.files[i])
if name == '' || name.ends_with('.vh') {
continue
}
module_name := b.flat_file_module_name(i)
if module_name !in module_set {
continue
}
if b.is_vlib_source_file(name) {
vlib_modules[module_name] = true
} else {
external_modules[module_name] = true
}
}
} else {
for file in b.files {
if file.name == '' || file.name.ends_with('.vh') {
continue
}
module_name := ast_file_module_name(file)
if module_name !in module_set {
continue
}
if b.is_vlib_source_file(file.name) {
vlib_modules[module_name] = true
} else {
external_modules[module_name] = true
}
}
}
for module_name, _ in external_modules {
if module_name in vlib_modules {
return true
}
}
return false
}
fn (b &Builder) is_vlib_source_file(file_name string) bool {
root := if b.pref.vroot.len > 0 { b.pref.vroot } else { os.getwd() }
vlib_root := os.norm_path(os.join_path(root, 'vlib')) + os.path_separator
return os.norm_path(os.abs_path(file_name)).starts_with(vlib_root)
}
fn import_module_name(name string) string {
return name.all_after_last('.').replace('.', '_')
}
fn unique_sorted_strings(items []string) []string {
mut seen := map[string]bool{}
mut out := []string{cap: items.len}
for item in items {
if item == '' || item in seen {
continue
}
seen[item] = true
out << item
}
out.sort()
return out
}
fn (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 {
cache_dir := b.core_cache_dir()
if !b.ensure_core_cache_dir() {
// If we cannot create a readable/writable cache dir, fall back to full compilation.
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE cached_core=false reason=cache_dir_unusable')
}
return false
}
if b.try_link_cached_self_main_object(output_name, cache_dir, cc, cc_flags, cc_link_flags, mut
sw)
{
return true
}
builtin_obj := b.ensure_cached_module_object(cache_dir, builtin_cache_name,
builtin_cached_module_paths, builtin_cached_module_names, cc, cc_flags, '',
error_limit_flag, false) or {
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE cached_core=false reason=builtin_obj_failed')
}
return false
}
b.print_cached_bundle_modules(builtin_cache_name, builtin_cached_module_names)
mut vlib_obj := ''
if vlib_cached_module_paths.len > 0 {
vlib_obj = b.ensure_cached_module_object(cache_dir, vlib_cache_name,
vlib_cached_module_paths, vlib_cached_module_names, cc, cc_flags, '', error_limit_flag,
false) or {
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE cached_core=false reason=vlib_obj_failed')
}
return false
}
if vlib_obj.len > 0 {
b.print_cached_bundle_modules(vlib_cache_name, vlib_cached_module_names)
}
}
mut optional_cached_objs := []string{}
mut optional_cached_cache_names := []string{}
mut optional_cached_module_names := []string{}
if veb_cached_module_paths.len > 0 && b.has_module('veb') {
veb_cache_type_modules := b.expand_type_modules_with_imports(cache_type_module_names(veb_cache_name,
veb_cached_module_names))
veb_obj := if b.has_external_cache_module_name_collision(veb_cache_type_modules) {
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE optional_cache=veb reason=external_module_name_collision')
}
''
} else {
b.ensure_cached_module_object(cache_dir, veb_cache_name, veb_cached_module_paths,
veb_cached_module_names, cc, cc_flags, cc_link_flags, error_limit_flag, true) or {
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE optional_cache=veb reason=${err}')
}
''
}
}
if veb_obj.len > 0 {
b.print_cached_bundle_modules(veb_cache_name, veb_cached_module_names)
optional_cached_objs << veb_obj
optional_cached_cache_names << veb_cache_name
optional_cached_module_names << veb_cached_module_names
}
}
mut v2compiler_obj := ''
if v2compiler_cached_module_paths.len > 0 && b.is_cmd_v2_self_build() {
v2compiler_obj = b.ensure_cached_module_object(cache_dir, v2compiler_cache_name,
v2compiler_cached_module_paths, v2compiler_cached_module_names, cc, cc_flags, '',
error_limit_flag, false) or {
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE cached_core=false reason=v2compiler_obj_failed')
}
return false
}
if v2compiler_obj.len > 0 {
b.print_cached_bundle_modules(v2compiler_cache_name, v2compiler_cached_module_names)
}
}
mut dynamic_excluded := core_cached_module_names.clone()
for module_name in optional_cached_module_names {
dynamic_excluded << module_name
}
entry_module_names := b.user_entry_module_names()
dynamic_excluded << entry_module_names
if b.is_cmd_v2_self_build() {
dynamic_excluded << v2compiler_cached_module_names
}
dynamic_excluded << 'main'
dynamic_cached_module_names := b.collect_modules_excluding(dynamic_excluded)
if dynamic_cached_module_names.len > 0 {
mut import_dependency_cache_names := [builtin_cache_name]
if vlib_obj.len > 0 {
import_dependency_cache_names << vlib_cache_name
}
if v2compiler_obj.len > 0 {
import_dependency_cache_names << v2compiler_cache_name
}
for cache_name in optional_cached_cache_names {
import_dependency_cache_names << cache_name
}
import_dependency_compile_flags, import_dependency_link_flags :=
b.cached_module_stamp_flags(import_dependency_cache_names)
imports_cc_flags := join_flag_strings(cc_flags, import_dependency_compile_flags)
imports_cc_link_flags := join_flag_strings(cc_link_flags, import_dependency_link_flags)
imports_obj := b.ensure_cached_parsed_module_object(cache_dir, imports_cache_name,
dynamic_cached_module_names, import_dependency_cache_names, cc, imports_cc_flags,
imports_cc_link_flags, error_limit_flag, true) or {
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE optional_cache=imports reason=${err}')
}
if b.used_import_vh_for_parse {
return false
}
''
}
if imports_obj.len > 0 {
b.print_cached_bundle_modules(imports_cache_name, dynamic_cached_module_names)
optional_cached_objs << imports_obj
optional_cached_cache_names << imports_cache_name
optional_cached_module_names << dynamic_cached_module_names
}
}
mut virtual_groups := if b.used_virtual_vh_for_parse {
b.cached_virtual_manifest()
} else {
b.collect_virtual_main_modules()
}
mut virtual_source_files := []string{}
mut virtuals_obj := ''
if virtual_groups.len > 0 {
mut virtual_dependency_cache_names := [builtin_cache_name]
if vlib_obj.len > 0 {
virtual_dependency_cache_names << vlib_cache_name
}
if v2compiler_obj.len > 0 {
virtual_dependency_cache_names << v2compiler_cache_name
}
for cache_name in optional_cached_cache_names {
virtual_dependency_cache_names << cache_name
}
virtual_dependency_compile_flags, virtual_dependency_link_flags :=
b.cached_module_stamp_flags(virtual_dependency_cache_names)
virtual_cc_flags := join_flag_strings(cc_flags, virtual_dependency_compile_flags)
virtual_cc_link_flags := join_flag_strings(cc_link_flags, virtual_dependency_link_flags)
virtuals_obj = b.ensure_cached_virtual_module_object(cache_dir, virtual_groups,
virtual_dependency_cache_names, cc, virtual_cc_flags, virtual_cc_link_flags,
error_limit_flag, true) or {
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE optional_cache=${virtuals_cache_name} reason=${err}')
}
if b.used_virtual_vh_for_parse {
return false
}
virtual_groups = []CachedVirtualModule{}
''
}
if virtuals_obj.len > 0 {
virtual_source_files = virtual_module_source_files(virtual_groups)
b.print_cached_bundle_modules(virtuals_cache_name, virtual_module_names(virtual_groups))
}
}
b.ensure_core_module_headers()
b.ensure_import_module_headers(dynamic_cached_module_names)
// The v2compiler .vh headers are read back only by can_use_cached_v2compiler_headers_for_parse
// (via cached_import_parse_path). That parse-reuse path was disabled because the generated
// headers are not yet complete/safe, so the 21 module headers are write-only on every cold
// self-build — ~230ms of pure overhead. Skip generation until reuse is re-enabled (the headers
// are regenerated on demand the moment it is; see v2compiler_headers_consumed_for_parse).
if v2compiler_obj.len > 0 && b.v2compiler_headers_consumed_for_parse() {
b.ensure_v2compiler_module_headers()
}
b.ensure_virtual_module_headers(virtual_groups)
mut excluded := core_cached_module_names.clone()
for module_name in optional_cached_module_names {
excluded << module_name
}
if b.is_cmd_v2_self_build() {
excluded << v2compiler_cached_module_names
}
main_modules := b.collect_modules_excluding(excluded)
if main_modules.len == 0 {
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE cached_core=false reason=no_main_modules')
}
return false
}
mut linked_cache_names := [builtin_cache_name]
if vlib_obj.len > 0 {
linked_cache_names << vlib_cache_name
}
if v2compiler_obj.len > 0 {
linked_cache_names << v2compiler_cache_name
}
for cache_name in optional_cached_cache_names {
linked_cache_names << cache_name
}
cached_compile_flags, cached_link_flags := b.cached_module_stamp_flags(linked_cache_names)
// When TCC is the default compiler but fell back to cc for cache
// compilation (e.g. due to TCC not supporting certain C constructs),
// the cached .o files are Mach-O (from cc) while TCC would produce ELF.
// Detect this mismatch and use cc for main compilation and linking too.
// Also, -prod builds with -flto require gcc/clang for linking — TCC
// cannot link LTO object files.
mut main_cc := cc
mut main_cc_flags := join_flag_strings(cc_flags, cached_compile_flags)
main_cc_link_flags := join_flag_strings(cc_link_flags, cached_link_flags)
if cc.contains('tcc') && os.exists(builtin_obj) {
bytes := os.read_bytes(builtin_obj) or { []u8{} }
is_elf := bytes.len >= 4 && bytes[0] == 0x7f && bytes[1] == 0x45 && bytes[2] == 0x4c
&& bytes[3] == 0x46
if !is_elf {
// Cached .o was compiled by cc (via TCC fallback), not TCC.
// Use cc for main compilation and linking to match formats.
// Keep all flags (including directive -I paths) but strip
// TCC-specific -I/-L paths that would conflict with system headers.
main_cc = 'cc'
tcc_dir2 := cc.all_before_last('/tcc')
if tcc_dir2.len > 0 {
mut parts := main_cc_flags.fields()
mut filtered := []string{cap: parts.len}
mut j := 0
for j < parts.len {
p := parts[j]
if (p == '-I' || p == '-L') && j + 1 < parts.len && parts[j + 1].contains('tcc') {
j += 2
continue
}
if (p.starts_with('-I') || p.starts_with('-L')) && p.contains('tcc') {
j++
continue
}
// Strip quoted -I/-L containing tcc
if (p.starts_with('-I"') || p.starts_with('-L"')
|| p.starts_with("-I'") || p.starts_with("-L'")) && p.contains('tcc') {
j++
continue
}
filtered << p
j++
}
main_cc_flags = filtered.join(' ')
}
}
}
mut cached_init_calls := []string{}
cached_init_calls << '__v2_cached_init_${builtin_cache_name}'
if vlib_obj.len > 0 {
cached_init_calls << '__v2_cached_init_${vlib_cache_name}'
}
if v2compiler_obj.len > 0 {
cached_init_calls << '__v2_cached_init_${v2compiler_cache_name}'
}
for cache_name in optional_cached_cache_names {
cached_init_calls << '__v2_cached_init_${cache_name}'
}
all_main_emit_files := if virtual_source_files.len > 0 {
filter_out_source_files(b.module_source_files(main_modules), virtual_source_files)
} else {
[]string{}
}
use_self_main_cache := b.should_skip_markused_for_self_build() && !b.pref.keep_c
self_main_obj := cache_path_join(cache_dir, 'main.o')
self_main_c := cache_path_join(cache_dir, 'main.c')
self_main_stamp := cache_path_join(cache_dir, 'main.stamp')
self_main_expected_stamp := if use_self_main_cache {
b.cache_stamp_for_self_main_object(main_modules, all_main_emit_files, linked_cache_names,
main_cc, main_cc_flags, main_cc_link_flags, cached_init_calls)
} else {
''
}
if use_self_main_cache && os.exists(self_main_obj) && os.exists(self_main_stamp) {
if current_stamp := os.read_file(self_main_stamp) {
if current_stamp == self_main_expected_stamp {
print_time('C Gen', sw.elapsed())
if os.getenv('V2VERBOSE') != '' {
println('[*] Reusing ${self_main_obj}')
}
b.link_cleanc_cached_core_executable(output_name, main_cc, main_cc_flags,
main_cc_link_flags, self_main_obj, builtin_obj, vlib_obj, v2compiler_obj,
optional_cached_objs, virtuals_obj, mut sw)
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE cached_core=true main_obj_cache=true')
}
return true
}
}
}
mut main_source := if all_main_emit_files.len > 0 {
b.gen_cleanc_source_with_cache_init_calls_files_force(main_modules, all_main_emit_files,
cached_init_calls, true, []string{})
} else {
b.gen_cleanc_source_with_cache_init_calls(main_modules, cached_init_calls)
}
print_time('C Gen', sw.elapsed())
if main_source == '' {
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE cached_core=false reason=empty_main_source')
}
return false
}
main_c_file := if use_self_main_cache { self_main_c } else { b.exec_build_c_file(output_name) }
os.write_file(main_c_file, main_source) or { return false }
if !use_self_main_cache && main_c_file != staged_c_file {
os.write_file(staged_c_file, main_source) or { return false }
}
println('[*] Wrote ${main_c_file}')
main_tmp_obj := cache_path_join(cache_dir, 'main.tmp.o')
main_obj := if use_self_main_cache { main_tmp_obj } else { staged_main_obj_file }
compile_main_cmd := '${main_cc} ${main_cc_flags} -w -Wno-incompatible-function-pointer-types -c "${main_c_file}" -o "${main_obj}"${error_limit_flag}'
main_fell_back := run_cc_cmd_or_exit(compile_main_cmd, 'C compilation', b.pref.show_cc)
if main_fell_back && main_cc.contains('tcc') {
// TCC failed on main.c but cached .o files are ELF (from TCC).
// Fallback produced Mach-O main.o — can't link with ELF cache.
// Fall back to non-cached full compilation.
os.rm(main_obj) or {}
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE cached_core=false reason=main_compile_fell_back')
}
return false
}
mut link_main_obj := main_obj
if use_self_main_cache {
os.rm(self_main_obj) or {}
os.mv(main_obj, self_main_obj) or { return false }
os.write_file(self_main_stamp, self_main_expected_stamp) or { return false }
link_main_obj = self_main_obj
}
b.link_cleanc_cached_core_executable(output_name, main_cc, main_cc_flags, main_cc_link_flags,
link_main_obj, builtin_obj, vlib_obj, v2compiler_obj, optional_cached_objs, virtuals_obj, mut
sw)
if !b.pref.keep_c && !use_self_main_cache {
os.rm(main_obj) or {}
os.rm(main_c_file) or {}
}
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE cached_core=true')
}
return true
}
fn (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 {
if !b.should_skip_markused_for_self_build() || b.pref.keep_c {
return false
}
linked_cache_names := [builtin_cache_name, vlib_cache_name, v2compiler_cache_name,
imports_cache_name]
for cache_name in linked_cache_names {
stamp_path := cache_path_join(cache_dir, '${cache_name}.stamp')
if !os.exists(cache_path_join(cache_dir, '${cache_name}.o')) || !os.exists(stamp_path) {
return false
}
stamp := os.read_file(stamp_path) or { return false }
if !stamp_file_lines_are_fresh(stamp) {
return false
}
}
self_main_obj := cache_path_join(cache_dir, 'main.o')
self_main_stamp := cache_path_join(cache_dir, 'main.stamp')
if !os.exists(self_main_obj) || !os.exists(self_main_stamp) {
return false
}
cached_compile_flags, cached_link_flags := b.cached_module_stamp_flags(linked_cache_names)
main_cc := cc
main_cc_flags := join_flag_strings(cc_flags, cached_compile_flags)
main_cc_link_flags := join_flag_strings(cc_link_flags, cached_link_flags)
if main_cc.contains('tcc') {
builtin_obj := cache_path_join(cache_dir, '${builtin_cache_name}.o')
bytes := os.read_bytes(builtin_obj) or { return false }
is_elf := bytes.len >= 4 && bytes[0] == 0x7f && bytes[1] == 0x45 && bytes[2] == 0x4c
&& bytes[3] == 0x46
if !is_elf {
return false
}
}
cached_init_calls := [
'__v2_cached_init_${builtin_cache_name}',
'__v2_cached_init_${vlib_cache_name}',
'__v2_cached_init_${v2compiler_cache_name}',
'__v2_cached_init_${imports_cache_name}',
]
expected_stamp := b.cache_stamp_for_self_main_object(['main'], []string{}, linked_cache_names,
main_cc, main_cc_flags, main_cc_link_flags, cached_init_calls)
current_stamp := os.read_file(self_main_stamp) or { return false }
if current_stamp != expected_stamp {
return false
}
print_time('C Gen', sw.elapsed())
if os.getenv('V2VERBOSE') != '' {
println('[*] Reusing ${self_main_obj}')
}
b.link_cleanc_cached_core_executable(output_name, main_cc, main_cc_flags, main_cc_link_flags,
self_main_obj, cache_path_join(cache_dir, '${builtin_cache_name}.o'), cache_path_join(cache_dir,
'${vlib_cache_name}.o'), cache_path_join(cache_dir, '${v2compiler_cache_name}.o'), [
cache_path_join(cache_dir, '${imports_cache_name}.o'),
], '', mut sw)
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE cached_core=true main_obj_cache=early')
}
return true
}
fn (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) {
cc_start := sw.elapsed()
// Strip -c and -x flags from link command since we're linking, not compiling.
// -x objective-c would cause cc to treat .o files as source code.
mut link_flags :=
main_cc_flags.replace('-x objective-c', '').replace('-x c', '').replace(' -c ', ' ')
mut link_cmd := '${main_cc} ${link_flags} -w "${main_obj}" "${builtin_obj}"'
if vlib_obj.len > 0 {
link_cmd += ' "${vlib_obj}"'
}
if v2compiler_obj.len > 0 {
link_cmd += ' "${v2compiler_obj}"'
}
for obj in optional_cached_objs {
link_cmd += ' "${obj}"'
}
if virtuals_obj.len > 0 {
link_cmd += ' "${virtuals_obj}"'
}
link_cmd += ' -o "${output_name}"'
if main_cc_link_flags.len > 0 {
link_cmd += ' ${main_cc_link_flags}'
}
run_cc_cmd_or_exit(link_cmd, 'Linking', b.pref.show_cc)
print_time('CC', time.Duration(sw.elapsed() - cc_start))
println('[*] Compiled ${output_name}')
}
// ---------------------------------------------------------------------------
// Durable persistent object cache (survives a /tmp obj-cache wipe).
// ---------------------------------------------------------------------------
// core_cache_dir() lives under os.temp_dir() and is what
// `rm -rf /tmp/v2_cleanc_obj_cache_<root>` removes for a "cold" measurement. A
// durable mirror under os.cache_dir() survives that wipe, so a cold self-host
// build with UNCHANGED sources can restore the prebuilt bundle objects + main.o
// and fast-relink instead of regenerating ~14MB of C. Correctness is enforced
// entirely by the existing stamp checks (try_self_build_fast_relink below): a
// restored object whose recorded source mtimes no longer match is ignored and
// rebuilt, so the durable tier can never yield a stale binary.
const self_build_persist_bundles = ['builtin', 'vlib', 'v2compiler', 'imports']
fn self_build_persist_file_names() []string {
mut names := []string{cap: self_build_persist_bundles.len * 2 + 2}
for name in self_build_persist_bundles {
names << '${name}.o'
names << '${name}.stamp'
}
names << 'main.o'
names << 'main.stamp'
return names
}
fn (b &Builder) durable_object_cache_dir() string {
root := if b.pref.vroot.len > 0 { b.pref.vroot } else { os.getwd() }
root_key := sanitize_cache_part(os.norm_path(os.abs_path(root)))
base := if b.pref.is_prod { 'v2cleanc_persist_prod' } else { 'v2cleanc_persist' }
return os.join_path(os.cache_dir(), base, root_key)
}
// copy_file_keep_mtime copies src->dst preserving src's mtime. Mtime preservation is
// required because main.stamp records `dependency:<bundle>:<mtime of that .stamp file>`,
// so a restored bundle .stamp must keep its original mtime or the relink probe misses.
fn copy_file_keep_mtime(src string, dst string) ! {
data := os.read_bytes(src)!
os.write_file_array(dst, data)!
mtime := os.file_last_mod_unix(src)
os.utime(dst, mtime, mtime)!
}
fn (b &Builder) save_durable_object_cache(cache_dir string) {
if !b.is_cmd_v2_self_build() || b.pref.no_cache || b.pref.keep_c {
return
}
durable_dir := b.durable_object_cache_dir()
os.mkdir_all(durable_dir, mode: 0o700) or { return }
for name in self_build_persist_file_names() {
src := cache_path_join(cache_dir, name)
if !os.exists(src) {
continue
}
copy_file_keep_mtime(src, cache_path_join(durable_dir, name)) or { continue }
}
}
fn (b &Builder) restore_durable_object_cache(cache_dir string) {
if !b.is_cmd_v2_self_build() || b.pref.no_cache {
return
}
durable_dir := b.durable_object_cache_dir()
if !os.is_dir(durable_dir) {
return
}
for name in self_build_persist_file_names() {
dst := cache_path_join(cache_dir, name)
if os.exists(dst) {
// A /tmp copy already exists — never overwrite it with the durable mirror.
continue
}
src := cache_path_join(durable_dir, name)
if !os.exists(src) {
continue
}
copy_file_keep_mtime(src, dst) or { continue }
}
}
// try_self_build_fast_relink is the PRE-PARSE fast path for cmd/v2 self-host. When the
// cached bundle objects + main.o are present (restored from the durable tier if /tmp was
// wiped) and every source/compiler file they were built from is still fresh, it relinks the
// final binary directly and skips the entire front-end (parse + type-check + transform),
// which otherwise runs unconditionally. Conservative by construction: any staleness fails a
// freshness check and falls through to a normal build, so it can never emit a stale binary.
fn (mut b Builder) try_self_build_fast_relink() bool {
trace := os.getenv('V2_TRACE_CACHE') != ''
if b.pref.backend != .cleanc || b.pref.no_cache || b.pref.keep_c || b.pref.skip_builtin {
return false
}
if !b.is_cmd_v2_self_build() || !b.should_skip_markused_for_self_build()
|| b.should_disable_cleanc_cache() {
if trace {
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()}')
}
return false
}
// Mirror gen_cleanc()'s generation-only decision: a `.c` output, a target we
// cannot compile locally, or a shared lib must go through normal C generation,
// never a direct relink. is_cmd_v2_self_build() keys only on the input file, so
// without this a warm-cache `-o foo.c cmd/v2/v2.v` would link an executable into
// foo.c instead of writing C source.
output_name := if b.pref.output_file != '' {
b.pref.output_file
} else if b.user_files.len > 0 {
b.default_output_name()
} else {
'out'
}
if b.fast_relink_output_is_generation_only(output_name) {
if trace {
eprintln('TRACE_CACHE fast_relink=false reason=generation_only out=${output_name}')
}
return false
}
if !b.ensure_core_cache_dir() {
return false
}
cache_dir := b.core_cache_dir()
b.restore_durable_object_cache(cache_dir)
// Every bundle object + stamp must exist and be source-fresh.
for name in self_build_persist_bundles {
if !os.exists(cache_path_join(cache_dir, '${name}.o')) {
if trace {
eprintln('TRACE_CACHE fast_relink=false reason=missing_obj ${name}')
}
return false
}
stamp := os.read_file(cache_path_join(cache_dir, '${name}.stamp')) or { return false }
if !stamp_file_lines_are_fresh(stamp) {
if trace {
eprintln('TRACE_CACHE fast_relink=false reason=stale_bundle ${name}')
}
return false
}
}
main_obj := cache_path_join(cache_dir, 'main.o')
main_stamp := os.read_file(cache_path_join(cache_dir, 'main.stamp')) or { return false }
if !os.exists(main_obj) || !stamp_file_lines_are_fresh(main_stamp) {
if trace {
eprintln('TRACE_CACHE fast_relink=false reason=stale_main main_obj=${os.exists(main_obj)}')
}
return false
}
// Validate the non-file build keys + dependency-stamp mtimes, and read back the relink
// flags. The recorded flags are trusted rather than recomputed (recomputing needs the
// parsed AST); the mtime-freshness checks are what guarantee the binary is up to date.
mut main_cc := ''
mut main_cc_flags := ''
mut main_cc_link_flags := ''
mut saw_self_build := false
mut saw_flag_fp := false
cur_flag_fp := b.preparse_flag_fingerprint()
for line in main_stamp.split_into_lines() {
if line.starts_with('cc=') {
main_cc = line['cc='.len..]
} else if line.starts_with('cc_flags=') {
main_cc_flags = line['cc_flags='.len..]
} else if line.starts_with('cc_link_flags=') {
main_cc_link_flags = line['cc_link_flags='.len..]
} else if line.starts_with('flag_fp=') {
// The recorded cc/cc_flags/cc_link_flags are trusted (recomputing the
// source-derived parts needs the AST). This fingerprint covers the
// flag inputs that DON'T need parsing — compiler choice, prod/shared
// mode, env CFLAGS — so a changed build environment invalidates the
// relink even when every source file is unchanged.
if line['flag_fp='.len..] != cur_flag_fp {
if trace {
eprintln('TRACE_CACHE fast_relink=false reason=flag_fp')
}
return false
}
saw_flag_fp = true
} else if line.starts_with('context_alloc=') {
if line['context_alloc='.len..] != '${b.pref.use_context_allocator}' {
return false
}
} else if line.starts_with('target_os=') {
if line['target_os='.len..] != b.pref.target_os_or_host() {
return false
}
} else if line == 'self_build=true' {
saw_self_build = true
} else if line.starts_with('dependency:') {
rest := line['dependency:'.len..]
sep := rest.last_index(':') or { return false }
dep_stamp := cache_path_join(cache_dir, '${rest[..sep]}.stamp')
if '${os.file_last_mod_unix(dep_stamp)}' != rest[sep + 1..] {
if trace {
eprintln('TRACE_CACHE fast_relink=false reason=dep_mtime ${rest[..sep]} have=${os.file_last_mod_unix(dep_stamp)} want=${rest[
sep + 1..]}')
}
return false
}
}
}
if !saw_self_build || main_cc.len == 0 || !saw_flag_fp {
if trace {
eprintln('TRACE_CACHE fast_relink=false reason=keys saw_self_build=${saw_self_build} cc=${main_cc} flag_fp=${saw_flag_fp}')
}
return false
}
mut sw := time.new_stopwatch()
b.link_cleanc_cached_core_executable(output_name, main_cc, main_cc_flags, main_cc_link_flags,
main_obj, cache_path_join(cache_dir, 'builtin.o'), cache_path_join(cache_dir, 'vlib.o'), cache_path_join(cache_dir,
'v2compiler.o'), [cache_path_join(cache_dir, 'imports.o')], '', mut sw)
if os.getenv('V2_TRACE_CACHE') != '' {
eprintln('TRACE_CACHE self_build_fast_relink=true')
}
return true
}
fn (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 {
source_files := b.module_source_files(main_modules)
dependency_lines := b.cache_dependency_stamp_lines(linked_cache_names)
mut lines := []string{cap: source_files.len + emit_files.len + dependency_lines.len +
cached_init_calls.len + 12}
lines << 'cache=main'
lines << 'format=${core_cache_format}'
lines << 'cc=${main_cc}'
lines << 'cc_flags=${main_cc_flags}'
lines << 'cc_link_flags=${main_cc_link_flags}'
lines << 'flag_fp=${b.preparse_flag_fingerprint()}'
lines << 'context_alloc=${b.pref.use_context_allocator}'
lines << 'target_os=${b.pref.target_os_or_host()}'
lines << 'self_build=true'
exe := os.executable()
lines << 'compiler_exe:${exe}:${os.file_last_mod_unix(exe)}'
for module_name in main_modules {
lines << 'module:${module_name}'
}
for init_call in cached_init_calls {
lines << 'init:${init_call}'
}
lines << dependency_lines
for file in b.user_entry_stamp_files() {
lines << 'entry:${file}:${os.file_last_mod_unix(file)}'
}
for file in source_files {
lines << 'source:${file}:${os.file_last_mod_unix(file)}'
}
for file in emit_files {
lines << 'emit:${file}:${os.file_last_mod_unix(file)}'
}
return lines.join('\n')
}
fn (b &Builder) cached_module_stamp_flags(cache_names []string) (string, string) {
cache_dir := b.core_cache_dir()
mut compile_flags := ''
mut link_flags := ''
for cache_name in cache_names {
stamp_path := os.join_path(cache_dir, '${cache_name}.stamp')
stamp := os.read_file(stamp_path) or { continue }
for line in stamp.split_into_lines() {
if line.starts_with('cc_flags=') {
compile_flags = join_flag_strings(compile_flags, line['cc_flags='.len..])
} else if line.starts_with('cc_link_flags=') {
link_flags = join_flag_strings(link_flags, line['cc_link_flags='.len..])
}
}
}
return compile_flags, link_flags
}
fn join_flag_strings(a string, b string) string {
mut joined := []string{}
for flags in [a, b] {
for item in flag_string_items(flags) {
if item !in joined {
joined << item
}
}
}
return joined.join(' ')
}
fn flag_string_items(flags string) []string {
tokens := flags.fields()
mut items := []string{}
mut i := 0
for i < tokens.len {
tok := tokens[i]
if tok in ['-I', '-L', '-l', '-F', '-framework', '-isystem', '-include']
&& i + 1 < tokens.len {
items << '${tok} ${tokens[i + 1]}'
i += 2
continue
}
items << tok
i++
}
return items
}
fn (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 {
obj_path := cache_path_join(cache_dir, '${cache_name}.o')
stamp_path := cache_path_join(cache_dir, '${cache_name}.stamp')
c_path := cache_path_join(cache_dir, '${cache_name}.c')
expected_stamp := b.cache_stamp_for_modules(cache_name, module_paths, cc, cc_flags,
cc_link_flags, use_markused)
if os.exists(obj_path) && os.exists(stamp_path) {
if current_stamp := os.read_file(stamp_path) {
if current_stamp == expected_stamp {
if b.load_cached_called_fn_names(cache_dir, cache_name) {
if os.getenv('V2VERBOSE') != '' {
println('[*] Reusing ${obj_path}')
}
return obj_path
}
}
}
}
if b.used_vh_for_parse {
if os.exists(obj_path) && os.exists(stamp_path) {
if b.load_cached_called_fn_names(cache_dir, cache_name) {
return obj_path
}
return error('missing cached ${cache_name} call metadata for .vh parse')
}
return error('missing cached ${cache_name} object for .vh parse')
}
stats_enabled := b.cgen_builder_stats_enabled()
mut stats_sw := time.new_stopwatch()
mut stage_start := stats_sw.elapsed()
cached_called_before := b.cached_called_fn_names.clone()
stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
'called_before_clone')
module_source := b.gen_cleanc_source_for_cache(emit_modules, cache_name, use_markused)
stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
'source')
if module_source == '' {
return error('failed to generate C source for ${cache_name}')
}
os.write_file(c_path, module_source)!
stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
'write_c')
compile_cmd := '${cc} ${cc_flags} -w -Wno-incompatible-function-pointer-types -c "${c_path}" -o "${obj_path}"${error_limit_flag}'
run_cc_cmd_or_exit(compile_cmd, 'C compilation', b.pref.show_cc)
stage_start =
b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start, 'cc')
b.write_cached_called_fn_names(cache_dir, cache_name, cached_called_before)
os.write_file(stamp_path, expected_stamp)!
_ = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start, 'metadata')
return obj_path
}
fn (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 {
obj_path := cache_path_join(cache_dir, '${cache_name}.o')
stamp_path := cache_path_join(cache_dir, '${cache_name}.stamp')
c_path := cache_path_join(cache_dir, '${cache_name}.c')
expected_stamp := b.cache_stamp_for_parsed_modules(cache_name, module_names,
dependency_cache_names, cc, cc_flags, cc_link_flags, use_markused)
if os.exists(obj_path) && os.exists(stamp_path) {
if current_stamp := os.read_file(stamp_path) {
if current_stamp == expected_stamp {
if b.load_cached_called_fn_names(cache_dir, cache_name) {
if os.getenv('V2VERBOSE') != '' {
println('[*] Reusing ${obj_path}')
}
return obj_path
}
}
}
}
if b.used_import_vh_for_parse {
if os.exists(obj_path) && os.exists(stamp_path) {
if b.load_cached_called_fn_names(cache_dir, cache_name) {
return obj_path
}
return error('missing cached ${cache_name} call metadata for .vh parse')
}
return error('missing cached ${cache_name} object for .vh parse')
}
stats_enabled := b.cgen_builder_stats_enabled()
mut stats_sw := time.new_stopwatch()
mut stage_start := stats_sw.elapsed()
cached_called_before := b.cached_called_fn_names.clone()
stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
'called_before_clone')
mut module_source := b.gen_cleanc_source_for_cache(module_names, cache_name, use_markused)
stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
'source')
if module_source == '' {
return error('failed to generate C source for ${cache_name}')
}
os.write_file(c_path, module_source)!
stage_start = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start,
'write_c')
compile_cmd := '${cc} ${cc_flags} -w -Wno-incompatible-function-pointer-types -c "${c_path}" -o "${obj_path}"${error_limit_flag}'
run_cc_cmd_or_exit(compile_cmd, 'C compilation', b.pref.show_cc)
stage_start =
b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start, 'cc')
b.write_cached_called_fn_names(cache_dir, cache_name, cached_called_before)
os.write_file(stamp_path, expected_stamp)!
_ = b.mark_cgen_builder_step(stats_enabled, cache_name, mut stats_sw, stage_start, 'metadata')
return obj_path
}
fn (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 {
obj_path := cache_path_join(cache_dir, '${virtuals_cache_name}.o')
stamp_path := cache_path_join(cache_dir, '${virtuals_cache_name}.stamp')
c_path := cache_path_join(cache_dir, '${virtuals_cache_name}.c')
expected_stamp := b.cache_stamp_for_virtual_modules(groups, dependency_cache_names, cc,
cc_flags, cc_link_flags, use_markused)
if os.exists(obj_path) && os.exists(stamp_path) {
if current_stamp := os.read_file(stamp_path) {
if current_stamp == expected_stamp {
if b.load_cached_called_fn_names(cache_dir, virtuals_cache_name) {
if os.getenv('V2VERBOSE') != '' {
println('[*] Reusing ${obj_path}')
}
return obj_path
}
}
}
}
if b.used_virtual_vh_for_parse {
if os.exists(obj_path) && os.exists(stamp_path) {
if b.load_cached_called_fn_names(cache_dir, virtuals_cache_name) {
return obj_path
}
return error('missing cached ${virtuals_cache_name} call metadata for .vh parse')
}
return error('missing cached ${virtuals_cache_name} object for .vh parse')
}
emit_files := virtual_module_source_files(groups)
stats_enabled := b.cgen_builder_stats_enabled()
mut stats_sw := time.new_stopwatch()
mut stage_start := stats_sw.elapsed()
cached_called_before := b.cached_called_fn_names.clone()
stage_start = b.mark_cgen_builder_step(stats_enabled, virtuals_cache_name, mut stats_sw,
stage_start, 'called_before_clone')
mut module_source := b.gen_cleanc_source_for_cache_files(['main'], emit_files,
virtuals_cache_name, use_markused)
stage_start = b.mark_cgen_builder_step(stats_enabled, virtuals_cache_name, mut stats_sw,
stage_start, 'source')
if module_source == '' {
return error('failed to generate C source for ${virtuals_cache_name}')
}
os.write_file(c_path, module_source)!
stage_start = b.mark_cgen_builder_step(stats_enabled, virtuals_cache_name, mut stats_sw,
stage_start, 'write_c')
compile_cmd := '${cc} ${cc_flags} -w -Wno-incompatible-function-pointer-types -c "${c_path}" -o "${obj_path}"${error_limit_flag}'
run_cc_cmd_or_exit(compile_cmd, 'C compilation', b.pref.show_cc)
stage_start = b.mark_cgen_builder_step(stats_enabled, virtuals_cache_name, mut stats_sw,
stage_start, 'cc')
b.write_cached_called_fn_names(cache_dir, virtuals_cache_name, cached_called_before)
os.write_file(stamp_path, expected_stamp)!
_ = b.mark_cgen_builder_step(stats_enabled, virtuals_cache_name, mut stats_sw, stage_start,
'metadata')
return obj_path
}
// flat_file_module_name returns the normalized module name of the i-th flat
// file, mirroring `ast_file_module_name` for the flat-AST path: the raw module
// string with `.` replaced by `_`, defaulting to `main`. Used by the cleanc
// cached-core enumeration helpers when `b.files` has been dropped in favour of
// the post-transform FlatAst.
fn (b &Builder) flat_file_module_name(i int) string {
mod := b.flat.string_at(b.flat.files[i].mod_idx)
if mod == '' {
return 'main'
}
return mod.replace('.', '_')
}
// uses_flat_module_enumeration reports whether module/file enumeration must run
// against `b.flat` instead of `b.files`. In flat-codegen mode the transformer
// drops `b.files` after producing the post-transform FlatAst, so the cleanc
// cache helpers source their module/file metadata from the flat cursors.
fn (b &Builder) uses_flat_module_enumeration() bool {
return b.files.len == 0 && b.flat.files.len > 0
}
// flat_scoped_to_modules returns a FlatAst whose file list is restricted to the
// given module set, reusing b.flat's node/edge/string arena (the arrays are
// shared, not deep-copied). The cleanc gen drives every emission pass off
// `flat.files`, so a restricted file list makes the whole gen see exactly the
// bundle's type-module files — matching the legacy gen, which filtered its input
// `gen_files` to `type_module_names`. This lets restricted cache bundles run on
// the flat gen with no per-file legacy rehydrate.
fn (b &Builder) flat_scoped_to_modules(module_names map[string]bool) ast.FlatAst {
mut files := []ast.FlatFile{cap: b.flat.files.len}
for i in 0 .. b.flat.files.len {
if b.flat_file_module_name(i) in module_names {
files << b.flat.files[i]
}
}
return ast.FlatAst{
files: files
nodes: b.flat.nodes
edges: b.flat.edges
strings: b.flat.strings
}
}
fn (b &Builder) has_module(module_name string) bool {
if b.uses_flat_module_enumeration() {
for i in 0 .. b.flat.files.len {
if b.flat_file_module_name(i) == module_name {
return true
}
}
return false
}
for file in b.files {
if ast_file_module_name(file) == module_name {
return true
}
}
return false
}
fn (b &Builder) collect_modules_excluding(excluded []string) []string {
mut excluded_set := map[string]bool{}
for module_name in excluded {
excluded_set[module_name] = true
}
mut modules_set := map[string]bool{}
if b.uses_flat_module_enumeration() {
for i in 0 .. b.flat.files.len {
module_name := b.flat_file_module_name(i)
if module_name in excluded_set {
continue
}
modules_set[module_name] = true
}
} else {
for file in b.files {
module_name := ast_file_module_name(file)
if module_name in excluded_set {
continue
}
modules_set[module_name] = true
}
}
mut modules := modules_set.keys()
modules.sort()
return modules
}
fn (b &Builder) user_entry_module_names() []string {
mut entry_files := map[string]bool{}
for file in b.user_entry_stamp_files() {
entry_files[os.norm_path(file)] = true
entry_files[os.norm_path(os.abs_path(file))] = true
}
mut modules_set := map[string]bool{}
if b.uses_flat_module_enumeration() {
for i in 0 .. b.flat.files.len {
name := b.flat.file_name(b.flat.files[i])
if name == '' || name.ends_with('.vh') {
continue
}
norm_name := os.norm_path(name)
abs_name := os.norm_path(os.abs_path(name))
if norm_name !in entry_files && abs_name !in entry_files {
continue
}
module_name := b.flat_file_module_name(i)
if module_name.len > 0 {
modules_set[module_name] = true
}
}
} else {
for file in b.files {
if file.name == '' || file.name.ends_with('.vh') {
continue
}
norm_name := os.norm_path(file.name)
abs_name := os.norm_path(os.abs_path(file.name))
if norm_name !in entry_files && abs_name !in entry_files {
continue
}
module_name := ast_file_module_name(file)
if module_name.len > 0 {
modules_set[module_name] = true
}
}
}
mut modules := modules_set.keys()
modules.sort()
return modules
}
fn (b &Builder) print_cached_bundle_modules(cache_name string, module_names []string) {
if module_names.len == 0 {
return
}
stats_enabled := b.pref != unsafe { nil } && b.pref.stats
show_cc_enabled := b.pref != unsafe { nil } && b.pref.show_cc
if !stats_enabled && !show_cc_enabled && os.getenv('V2_TRACE_CACHE') == '' {
return
}
if stats_enabled {
println(' * Cached ${cache_name} modules: ${module_names.len}')
for module_name in module_names {
println(' [cache ${cache_name}] ${module_name}')
}
return
}
println('[*] Cached ${cache_name} modules: ${module_names.join(', ')}')
}
fn ast_file_module_name(file ast.File) string {
for stmt in file.stmts {
if stmt is ast.ModuleStmt {
return stmt.name.replace('.', '_')
}
}
return 'main'
}
fn normalize_target_os_name(target_os string) string {
return match target_os.to_lower() {
'darwin', 'mac' { 'macos' }
else { target_os.to_lower() }
}
}
fn is_windows_x64_native_target(arch pref.Arch, target_os string) bool {
return arch == .x64 && normalize_target_os_name(target_os) == 'windows'
}
fn is_linux_x64_native_target(arch pref.Arch, target_os string) bool {
return arch == .x64 && normalize_target_os_name(target_os) == 'linux'
}
fn is_macos_x64_native_target(arch pref.Arch, target_os string) bool {
return arch == .x64 && is_macos_native_target(target_os)
}
fn eprint_native_x64_link_error(message string) {
if message.starts_with('x64: unsupported backend feature: ') {
eprintln(message)
eprintln(x64.x64_backend_limitation_hint)
return
}
eprintln('Link failed:')
eprintln(message)
}
fn (b &Builder) uses_minimal_windows_x64_runtime() bool {
arch := b.pref.get_effective_arch()
return b.pref.backend == .x64 && is_windows_x64_native_target(arch, b.pref.target_os_or_host())
}
fn (b &Builder) uses_minimal_linux_x64_runtime() bool {
arch := b.pref.get_effective_arch()
return b.pref.backend == .x64 && is_linux_x64_native_target(arch, b.pref.target_os_or_host())
}
fn (b &Builder) uses_minimal_x64_runtime() bool {
return b.uses_minimal_windows_x64_runtime() || b.uses_minimal_linux_x64_runtime()
}
fn linux_x64_tiny_strict_enabled() bool {
return os.getenv('V2_X64_LINUX_TINY') != ''
}
fn (b &Builder) uses_minimal_linux_x64_runtime_roots() bool {
return b.uses_minimal_linux_x64_runtime() && linux_x64_tiny_strict_enabled()
}
fn (b &Builder) uses_minimal_x64_runtime_roots() bool {
return b.uses_minimal_windows_x64_runtime() || b.uses_minimal_linux_x64_runtime_roots()
}
fn (b &Builder) uses_macos_x64_tiny_object(arch pref.Arch) bool {
return b.pref.backend == .x64 && is_macos_x64_native_target(arch, b.pref.target_os_or_host())
&& b.pref.macos_tiny
}
fn native_link_flags_suffix(link_flags string) string {
trimmed := link_flags.trim_space()
if trimmed == '' {
return ''
}
return ' ${trimmed}'
}
fn macos_native_ld_link_flags(link_flags string) string {
mut normalized := []string{}
tokens := link_flags.fields()
mut i := 0
for i < tokens.len {
tok := tokens[i]
if tok.starts_with('-Wl,') {
for linker_arg in tok['-Wl,'.len..].split(',') {
if linker_arg != '' {
normalized << linker_arg
}
}
i++
continue
}
if tok == '-Xlinker' {
if i + 1 < tokens.len {
i++
normalized << tokens[i]
}
i++
continue
}
normalized << tok
i++
}
return normalized.join(' ')
}
fn native_driver_flag_is_dual_use(tok string) bool {
return tok == '-pthread' || tok == '-fopenmp' || tok.starts_with('-fopenmp=')
}
fn macos_native_ld_unsupported_driver_link_flags(link_flags string) []string {
mut unsupported := []string{}
for tok in link_flags.fields() {
if native_driver_flag_is_dual_use(tok) {
unsupported << tok
}
}
return unsupported
}
fn validate_macos_native_ld_link_flags(link_flags string) ! {
unsupported := macos_native_ld_unsupported_driver_link_flags(link_flags)
if unsupported.len > 0 {
return error('x64: unsupported backend feature: macOS native ld cannot consume driver linker flags: ${unsupported.join(' ')}')
}
}
fn native_linux_tiny_allows_system_lib(lib string) bool {
return lib in ['pthread', 'm', 'dl', 'c']
}
fn native_internal_link_flags_allow_builtin_linux_tiny(link_flags string) bool {
tokens := link_flags.trim_space().fields()
mut i := 0
for i < tokens.len {
tok := tokens[i]
if tok == '-l' {
if i + 1 >= tokens.len {
return false
}
i++
if !native_linux_tiny_allows_system_lib(tokens[i]) {
return false
}
} else if tok.starts_with('-l') {
if !native_linux_tiny_allows_system_lib(tok['-l'.len..]) {
return false
}
} else {
return false
}
i++
}
return true
}
fn native_link_flags_allow_builtin_linux_tiny(link_flags string, user_link_flags string) bool {
return user_link_flags.trim_space() == ''
&& native_internal_link_flags_allow_builtin_linux_tiny(link_flags)
}
fn (b &Builder) native_link_flags_from_sources() string {
_, directive_link_flags := split_compile_and_link_flags(b.collect_cflags_from_sources())
return directive_link_flags.trim_space()
}
fn (b &Builder) native_compile_and_link_flags_from_sources() (string, string) {
directive_compile_flags, directive_link_flags :=
split_compile_and_link_flags(b.collect_cflags_from_sources())
return directive_compile_flags.trim_space(), directive_link_flags.trim_space()
}
fn (b &Builder) native_user_compile_and_link_flags_from_sources() (string, string) {
directive_compile_flags, directive_link_flags :=
split_compile_and_link_flags(b.collect_user_cflags_from_sources())
return directive_compile_flags.trim_space(), directive_link_flags.trim_space()
}
struct NativeExternalLinkInputs {
link_flags string
source_files []string
object_files []string
}
fn native_external_source_clean_token(tok string) string {
return tok.trim('"').trim("'")
}
fn native_external_source_is_supported(tok string) bool {
lower := native_external_source_clean_token(tok).to_lower()
return lower.ends_with('.c') || lower.ends_with('.m')
}
fn native_external_source_is_unsupported(tok string) bool {
lower := native_external_source_clean_token(tok).to_lower()
return lower.ends_with('.cc') || lower.ends_with('.cpp') || lower.ends_with('.cxx')
|| lower.ends_with('.mm')
}
fn native_external_source_unsupported_message(tok string) string {
clean := native_external_source_clean_token(tok)
if clean.to_lower().ends_with('.mm') {
return 'x64: unsupported backend feature: Objective-C++ #flag source `${clean}`'
}
return 'x64: unsupported backend feature: C++ #flag source `${clean}`'
}
fn native_external_source_object_file(output_binary string, source_index int) string {
output_name := if output_binary == '' { 'out' } else { os.file_name(output_binary) }
clean_name := output_name.replace('/', '_').replace('\\', '_').replace('.', '_')
return os.join_path(os.vtmp_dir(), 'v2_native_${os.getpid()}_${clean_name}_${source_index}.o')
}
fn native_external_link_inputs(link_flags string, output_binary string) !NativeExternalLinkInputs {
mut rewritten := []string{}
mut source_files := []string{}
mut object_files := []string{}
for tok in link_flags.fields() {
clean := native_external_source_clean_token(tok)
if native_external_source_is_unsupported(clean) {
return error(native_external_source_unsupported_message(clean))
}
if native_external_source_is_supported(clean) {
if tok.contains('"') || tok.contains("'") {
return error('x64: unsupported backend feature: quoted #flag source path `${tok}`')
}
obj_file := native_external_source_object_file(output_binary, source_files.len)
source_files << clean
object_files << obj_file
rewritten << os.quoted_path(obj_file)
continue
}
rewritten << tok
}
return NativeExternalLinkInputs{
link_flags: rewritten.join(' ')
source_files: source_files
object_files: object_files
}
}
fn 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 {
mut parts := [cc, '-c']
if is_macos_native_target(target_os) {
if sdk_path != '' {
parts << '-isysroot'
parts << os.quoted_path(sdk_path)
}
if arch_flag != '' {
parts << '-arch'
parts << arch_flag
}
}
if compile_flags != '' {
parts << compile_flags
}
parts << os.quoted_path(source_file)
parts << '-o'
parts << os.quoted_path(object_file)
return parts.join(' ')
}
fn (b &Builder) native_external_source_compiler(target_os string) string {
if b.pref.ccompiler.len > 0 {
return b.pref.ccompiler
}
v2cc := (os.getenv_opt('V2CC') or { '' }).trim_space()
if v2cc != '' {
return v2cc
}
if is_macos_native_target(target_os) {
return 'cc'
}
return configured_cc(b.pref.vroot)
}
fn (b &Builder) native_linux_hosted_link_compiler() string {
if b.pref.ccompiler.len > 0 {
return b.pref.ccompiler
}
v2cc := (os.getenv_opt('V2CC') or { '' }).trim_space()
if v2cc != '' {
return v2cc
}
return 'cc'
}
fn (b &Builder) compile_native_external_sources(inputs NativeExternalLinkInputs, compile_flags string, target_os string, sdk_path string, arch_flag string) ! {
if inputs.source_files.len == 0 {
return
}
cc := b.native_external_source_compiler(target_os)
for i, source_file in inputs.source_files {
cmd := native_external_source_compile_command(cc, source_file, inputs.object_files[i],
compile_flags, sdk_path, arch_flag, target_os)
if b.pref.show_cc {
println(cmd)
}
res := os.execute(cmd)
if res.exit_code != 0 {
return error('native external source compilation failed:\n${cmd}\n${res.output}')
}
}
}
fn cleanup_native_external_objects(inputs NativeExternalLinkInputs) {
for obj_file in inputs.object_files {
os.rm(obj_file) or {}
}
}
fn macos_native_link_command(output_binary string, obj_file string, sdk_path string, arch_flag string, tiny_object bool, link_flags string) string {
ld_link_flags := macos_native_ld_link_flags(link_flags)
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'
if tiny_object {
return '${normal_link_cmd} -dead_strip -x -S'
}
return normal_link_cmd
}
fn macos_sdk_path_from_xcrun_output(output string) !string {
mut sdk_path := ''
for raw_line in output.split_into_lines() {
line := raw_line.trim(' \t\r')
if line == '' {
continue
}
if is_clean_macos_sdk_path_line(line) {
sdk_path = line
}
}
if sdk_path == '' {
return error('could not find a clean macOS SDK path in xcrun output')
}
return sdk_path
}
fn is_clean_macos_sdk_path_line(path string) bool {
if !os.is_abs_path(path) {
return false
}
base := os.file_name(path)
return base.starts_with('MacOSX') && base.ends_with('.sdk')
}
fn validate_macos_sdk_path_for_native_link(sdk_path string) ! {
if !os.is_dir(sdk_path) {
return error('macOS SDK path does not exist: ${sdk_path}')
}
libsystem_tbd := os.join_path(sdk_path, 'usr', 'lib', 'libSystem.tbd')
libsystem_dylib := os.join_path(sdk_path, 'usr', 'lib', 'libSystem.dylib')
if !os.exists(libsystem_tbd) && !os.exists(libsystem_dylib) {
return error('macOS SDK path is missing usr/lib/libSystem.tbd or usr/lib/libSystem.dylib: ${sdk_path}')
}
}
fn linux_native_link_command(cc string, output_binary string, obj_file string, link_flags string) string {
return '${cc} ${os.quoted_path(obj_file)} -o ${os.quoted_path(output_binary)} -no-pie${native_link_flags_suffix(link_flags)}'
}
fn native_external_object_file(output_binary string, target_os string) string {
if !is_macos_native_target(target_os) {
return 'main.o'
}
output_path := if os.is_abs_path(output_binary) {
output_binary
} else {
os.abs_path(output_binary)
}
output_dir := os.dir(output_path)
output_name := os.file_name(output_path)
if output_name == '' {
return os.join_path(output_dir, '.v_native_${os.getpid()}.o')
}
return os.join_path(output_dir, '.${output_name}.${os.getpid()}.o')
}
fn (mut b Builder) prepare_macos_tiny_candidate_source_files() {
b.macos_tiny_candidate_source_files = []ast.File{}
b.macos_tiny_candidate_source_flat = ast.FlatAst{}
if !b.uses_macos_x64_tiny_object(.x64) {
return
}
if b.flat.files.len > 0 {
b.macos_tiny_candidate_source_flat = b.flat
return
}
b.macos_tiny_candidate_source_files = b.files.clone()
}
fn is_macos_native_target(target_os string) bool {
return normalize_target_os_name(target_os) == 'macos'
}
fn native_x64_object_format_for_os(target_os string) x64.ObjectFormat {
return match normalize_target_os_name(target_os) {
'macos' { x64.ObjectFormat.macho }
'windows' { x64.ObjectFormat.coff }
else { x64.ObjectFormat.elf }
}
}
fn native_x64_codegen_abi_for_os(target_os string) x64.X64Abi {
return match normalize_target_os_name(target_os) {
'windows' { x64.X64Abi.windows }
else { x64.X64Abi.sysv }
}
}
fn native_x64_lowering_abi_for_os(target_os string) abi.X64Abi {
return match normalize_target_os_name(target_os) {
'windows' { abi.X64Abi.windows }
else { abi.X64Abi.sysv }
}
}
fn native_x64_mir_unsupported_external_symbol_message(mir_mod &mir.Module, obj_format x64.ObjectFormat) ?string {
for val in mir_mod.values {
if val.kind != .func_ref {
continue
}
if msg := x64.unsupported_external_symbol_message_for_name(obj_format, val.name,
'needed while preparing native x64 output')
{
return msg
}
}
return none
}
fn (b &Builder) native_backend_requires_ssa_optimization(arch pref.Arch) bool {
return arch == .x64
}
fn flag_os_matches(cond string, target_os string) bool {
current := normalize_target_os_name(target_os)
return match cond.to_lower() {
'darwin', 'macos', 'mac' { current == 'macos' || current == 'darwin' }
'linux' { current == 'linux' }
'windows' { current == 'windows' }
'bsd' { current in ['macos', 'freebsd', 'openbsd', 'netbsd', 'dragonfly'] }
'freebsd' { current == 'freebsd' }
'openbsd' { current == 'openbsd' }
'netbsd' { current == 'netbsd' }
'dragonfly' { current == 'dragonfly' }
'android' { current == 'android' }
'termux' { current == 'termux' }
'ios' { current == 'ios' }
'solaris' { current == 'solaris' }
'qnx' { current == 'qnx' }
'serenity' { current == 'serenity' }
'plan9' { current == 'plan9' }
'vinix' { current == 'vinix' }
'cross' { current == 'cross' }
'none' { current == 'none' }
else { false }
}
}
fn flag_pref_matches(cond string, prefs &pref.Preferences) bool {
if prefs == unsafe { nil } {
return false
}
lower := cond.to_lower()
if pref.comptime_flag_value(prefs, lower) {
return true
}
return flag_os_matches(lower, prefs.target_os_or_host())
}
fn find_vmod_root_for_file(file_path string) string {
mut dir := os.dir(file_path)
for _ in 0 .. 12 {
if os.exists(os.join_path(dir, 'v.mod')) {
return dir
}
parent := os.dir(dir)
if parent == dir || parent == '' {
break
}
dir = parent
}
return os.dir(file_path)
}
fn resolve_flag_path(path string, file_dir string, vmod_root string) string {
mut resolved := path.replace('@VMODROOT', vmod_root)
if os.is_abs_path(resolved) {
return resolved
}
// Resolve any relative path (including bare relative like 'r/qrcodegen')
// relative to the source file's directory, matching V1 behavior
return os.norm_path(os.join_path(file_dir, resolved))
}
fn expand_existing_path_macros(flag_value string) ?string {
mut out := ''
mut i := 0
for i < flag_value.len {
if flag_value[i] == `$` {
remainder := flag_value[i..]
mut literal := ''
if remainder.starts_with(r'$when_first_existing') {
literal = r'$when_first_existing'
} else if remainder.starts_with(r'$first_existing') {
literal = r'$first_existing'
}
if literal != '' {
if remainder.len <= literal.len || remainder[literal.len] != `(` {
out += flag_value[i].ascii_str()
i++
continue
}
params_part := remainder[literal.len + 1..]
params := params_part.all_before(')')
if params == params_part {
return none
}
paths := params.replace(',', '\n').split_into_lines().map(it.trim('\t \'"'))
mut found := ''
for path in paths {
if path != '' && os.exists(path) {
found = path
break
}
}
if found == '' {
return none
}
out += found
i += literal.len + 1 + params.len + 1
continue
}
}
out += flag_value[i].ascii_str()
i++
}
return out
}
fn normalize_flag_value_for_file(flag_value string, file_path string) string {
file_dir := os.dir(os.real_path(file_path))
vmod_root := find_vmod_root_for_file(file_path)
expanded_flag_value := expand_existing_path_macros(flag_value) or { return '' }
mut tokens := expanded_flag_value.fields()
mut out := []string{}
mut i := 0
for i < tokens.len {
tok := tokens[i]
if tok in ['-I', '-L', '-F'] && i + 1 < tokens.len {
out << tok
out << resolve_flag_path(tokens[i + 1], file_dir, vmod_root)
i += 2
continue
}
if tok.starts_with('-I') && tok.len > 2 {
out << '-I' + resolve_flag_path(tok[2..], file_dir, vmod_root)
i++
continue
}
if tok.starts_with('-L') && tok.len > 2 {
out << '-L' + resolve_flag_path(tok[2..], file_dir, vmod_root)
i++
continue
}
if tok.starts_with('-F') && tok.len > 2 {
out << '-F' + resolve_flag_path(tok[2..], file_dir, vmod_root)
i++
continue
}
if tok.contains('@VMODROOT') || tok.contains('@VEXEROOT') || tok.starts_with('./')
|| tok.starts_with('../') || tok.ends_with('.c') || tok.ends_with('.m')
|| tok.ends_with('.o') {
out << resolve_flag_path(tok, file_dir, vmod_root)
i++
continue
}
out << tok
i++
}
return out.join(' ')
}
fn parse_flag_directive_line(line string, file_path string, target_os string) ?string {
return parse_flag_directive_line_with_context(line, file_path, target_os, unsafe {
&pref.Preferences(nil)
})
}
fn parse_flag_directive_line_with_pref(line string, file_path string, prefs &pref.Preferences) ?string {
target_os := if prefs == unsafe { nil } { '' } else { prefs.target_os_or_host() }
return parse_flag_directive_line_with_context(line, file_path, target_os, prefs)
}
fn parse_flag_directive_line_with_context(line string, file_path string, target_os string, prefs &pref.Preferences) ?string {
trimmed := line.trim_space()
if trimmed.starts_with('#pkgconfig') {
if prefs != unsafe { nil } && prefs.is_cross_target() {
return none
}
rest := trimmed['#pkgconfig'.len..].trim_space()
return resolve_pkgconfig_directive_flags(rest)
}
if !trimmed.starts_with('#flag') {
return none
}
mut rest := trimmed['#flag'.len..].trim_space()
if rest == '' {
return none
}
if comment_idx := rest.index('//') {
rest = rest[..comment_idx].trim_space()
if rest == '' {
return none
}
}
parts := rest.fields()
if parts.len == 0 {
return none
}
if !parts[0].starts_with('-') && !parts[0].starts_with('@') && parts.len > 1 {
matches := if prefs == unsafe { nil } {
flag_os_matches(parts[0], target_os)
} else {
flag_os_matches(parts[0], target_os) || flag_pref_matches(parts[0], prefs)
}
if !matches {
return none
}
rest = rest[parts[0].len..].trim_space()
}
if rest == '' {
return none
}
return normalize_flag_value_for_file(rest, file_path)
}
fn resolve_pkgconfig_directive_flags(value string) ?string {
if value == '' {
return none
}
args := if value.contains('--') {
value.fields()
} else {
'--cflags --libs ${value}'.fields()
}
flags := pref.pkgconfig_result(args) or { return none }
if flags == '' {
return none
}
return flags
}
fn flag_references_missing_file(flag string, include_flags []string) bool {
for tok in flag.fields() {
clean := tok.trim('"').trim("'")
if clean.len == 0 {
continue
}
if clean.ends_with('.o') || clean.ends_with('.a') || clean.ends_with('.so')
|| clean.ends_with('.dylib') || clean.ends_with('.m') || clean.ends_with('.c') {
if os.is_abs_path(clean) || clean.starts_with('./') || clean.starts_with('../') {
if !os.exists(clean) {
// For .o files, try to build from corresponding .c file
if clean.ends_with('.o') {
c_file := clean[..clean.len - 2] + '.c'
if os.exists(c_file) {
inc_flags := include_flags.join(' ')
compile_cmd := 'cc -c -w -O2 ${inc_flags} "${c_file}" -o "${clean}"'
res := os.execute(compile_cmd)
if res.exit_code == 0 {
continue // successfully compiled, not missing
}
}
}
return true
}
}
}
}
return false
}
fn (b &Builder) collect_cflags_from_sources() string {
// Collect source file paths to scan. When .vh headers were used for
// parsing, b.files references the .vh summaries which lack #flag
// directives. Always include the original core module source files
// so that directive flags (e.g. -I paths) are never lost.
mut scan_paths := []string{}
for file in b.files {
if file.name != '' {
scan_paths << file.name
}
}
for ff in b.flat.files {
name := b.flat.file_name(ff)
if name != '' {
scan_paths << name
}
}
cflags_target_os := b.cflags_target_os_for_local_compile()
if !b.pref.skip_builtin {
target_os := cflags_target_os
for module_path in core_cached_module_paths {
vlib_path := b.pref.get_vlib_module_path(module_path)
module_files := get_v_files_from_dir(vlib_path, b.pref.user_defines, target_os)
for mf in module_files {
scan_paths << mf
}
}
}
return b.collect_cflags_from_scan_paths(scan_paths)
}
fn (b &Builder) source_path_is_internal_vlib(path string) bool {
vlib_root := os.real_path(os.join_path(b.pref.vroot, 'vlib')).replace('\\', '/').trim_right('/')
real_path := os.real_path(path).replace('\\', '/')
return real_path == vlib_root || real_path.starts_with('${vlib_root}/')
}
fn (b &Builder) collect_user_cflags_from_sources() string {
mut scan_paths := []string{}
for path in b.user_files {
if path != '' {
scan_paths << path
}
}
for file in b.files {
if file.name != '' && !b.source_path_is_internal_vlib(file.name) {
scan_paths << file.name
}
}
for ff in b.flat.files {
name := b.flat.file_name(ff)
if name != '' && !b.source_path_is_internal_vlib(name) {
scan_paths << name
}
}
return b.collect_cflags_from_scan_paths(scan_paths)
}
fn (b &Builder) collect_cflags_from_scan_paths(paths []string) string {
mut flags := []string{}
mut seen := map[string]bool{}
mut scanned_files := map[string]bool{}
mut scan_paths := paths.clone()
cflags_target_os := b.cflags_target_os_for_local_compile()
scan_paths.sort()
for scan_path in scan_paths {
if scan_path == '' || scan_path in scanned_files {
continue
}
scanned_files[scan_path] = true
lines := os.read_lines(scan_path) or { continue }
// Track $if nesting to skip flags inside non-matching comptime blocks.
// skip_depth > 0 means we are inside a non-matching $if block.
// chain_matched[i] tracks whether the i-th enclosing $if chain has
// already matched some branch (so subsequent $else / $else $if at the
// same level should be skipped even if their cond would otherwise match).
mut skip_depth := 0
mut chain_matched := []bool{}
for line in lines {
trimmed := line.trim_space()
// Strip a leading "} " so chained patterns like "} $else $if X {" parse
// the same way as "$else $if X {" on their own line.
rest := if trimmed.starts_with('} ') { trimmed[2..].trim_space() } else { trimmed }
leading_close := rest != trimmed
// $else $if cond { (a chain continuation)
if rest.starts_with(r'$else $if ') {
new_cond := rest[10..].trim_right('{ ').trim_space()
if skip_depth > 1 {
// nested skipping; just continue without touching outer state
continue
}
cur := chain_matched.len - 1
if cur < 0 {
continue
}
if chain_matched[cur] {
skip_depth = 1
} else if comptime_cond_matches_with_context(new_cond, cflags_target_os, b.pref) {
chain_matched[cur] = true
skip_depth = 0
} else {
skip_depth = 1
}
continue
}
// plain $else { (chain terminator)
if rest.starts_with(r'$else') {
if skip_depth > 1 {
continue
}
cur := chain_matched.len - 1
if cur < 0 {
continue
}
if chain_matched[cur] {
skip_depth = 1
} else {
chain_matched[cur] = true
skip_depth = 0
}
continue
}
// $if cond { (chain opener)
if rest.starts_with(r'$if ') {
cond := rest[4..].trim_right('{ ').trim_space()
matched := comptime_cond_matches_with_context(cond, cflags_target_os, b.pref)
chain_matched << matched
if skip_depth > 0 {
skip_depth++
} else if !matched {
skip_depth = 1
}
continue
}
// closing }
if trimmed == '}' {
if skip_depth > 0 {
skip_depth--
}
if chain_matched.len > 0 {
chain_matched.delete_last()
}
continue
}
// "} something" where the leading } closed a chain but the rest is
// unrecognized: treat the } as a chain close.
if leading_close && rest == '' {
if skip_depth > 0 {
skip_depth--
}
if chain_matched.len > 0 {
chain_matched.delete_last()
}
continue
}
if skip_depth > 0 {
continue
}
// Replace @VEXEROOT before parsing so path normalization sees absolute paths
resolved_line := line.replace('@VEXEROOT', b.pref.vroot).replace('VEXEROOT',
b.pref.vroot)
mut flag := parse_flag_directive_line_with_context(resolved_line, scan_path,
cflags_target_os, b.pref) or { continue }
// Build include flags from already-collected flags for compiling missing .o files
mut inc_flags := []string{}
for f in flags {
if f.starts_with('-I') {
inc_flags << f
}
}
if flag_references_missing_file(flag, inc_flags) {
continue
}
if flag == '' || flag in seen {
continue
}
seen[flag] = true
flags << flag
}
}
return flags.join(' ')
}
// split_compile_and_link_flags separates a flags string into compiler-only
// flags (for -c compilation) and linker-only flags (for the link step).
// Linker flags include: -l*, -L*, -Wl,*, -Xlinker, -framework, C source files
// and prebuilt object/library files. Minimal dual-use driver flags, plus -F
// framework search paths, are kept in both outputs. Source files from #flag
// directives must not be passed to
// per-module `-c -o module.o` cache compilations.
fn split_compile_and_link_flags(flags string) (string, string) {
tokens := flags.fields()
mut compile := []string{}
mut link := []string{}
mut i := 0
for i < tokens.len {
tok := tokens[i]
if native_driver_flag_is_dual_use(tok) {
compile << tok
link << tok
} else if tok == '-F' {
compile << tok
link << tok
if i + 1 < tokens.len {
i++
compile << tokens[i]
link << tokens[i]
}
} else if tok.starts_with('-F') {
compile << tok
link << tok
} else if tok == '-Xlinker' {
link << tok
if i + 1 < tokens.len {
i++
link << tokens[i]
}
} else if tok == '-framework' {
// -framework Name: two tokens, linker only
link << tok
if i + 1 < tokens.len {
i++
link << tokens[i]
}
} else if tok.starts_with('-Wl,') || tok.starts_with('-l') || tok.starts_with('-L') {
link << tok
// -L or -l alone (space-separated from its argument): grab the next token
if (tok == '-L' || tok == '-l') && i + 1 < tokens.len {
i++
link << tokens[i]
}
} else if tok.ends_with('.c') || tok.ends_with('.cc') || tok.ends_with('.cpp')
|| tok.ends_with('.cxx') || tok.ends_with('.m') || tok.ends_with('.mm')
|| tok.ends_with('.o') || tok.ends_with('.obj') || tok.ends_with('.a')
|| tok.ends_with('.so') || tok.ends_with('.dylib') {
link << tok
} else if tok == '-I' && i + 1 < tokens.len {
// -I alone (space-separated from its argument): grab the next token
compile << tok
i++
compile << tokens[i]
} else {
compile << tok
}
i++
}
return compile.join(' '), link.join(' ')
}
fn comptime_cond_matches(cond string, target_os string) bool {
return comptime_cond_matches_with_context(cond, target_os, unsafe { &pref.Preferences(nil) })
}
fn comptime_cond_matches_with_pref(cond string, prefs &pref.Preferences) bool {
target_os := if prefs == unsafe { nil } { '' } else { prefs.target_os_or_host() }
return comptime_cond_matches_with_context(cond, target_os, prefs)
}
fn comptime_cond_matches_with_context(cond string, target_os string, prefs &pref.Preferences) bool {
trimmed := cond.trim_space()
if or_idx := top_level_bool_op_index(trimmed, '||') {
left := trimmed[..or_idx].trim_space()
right := trimmed[or_idx + 2..].trim_space()
return comptime_cond_matches_with_context(left, target_os, prefs)
|| comptime_cond_matches_with_context(right, target_os, prefs)
}
if and_idx := top_level_bool_op_index(trimmed, '&&') {
left := trimmed[..and_idx].trim_space()
right := trimmed[and_idx + 2..].trim_space()
return comptime_cond_matches_with_context(left, target_os, prefs)
&& comptime_cond_matches_with_context(right, target_os, prefs)
}
if trimmed.starts_with('!') {
return !comptime_cond_matches_with_context(trimmed[1..], target_os, prefs)
}
if stripped := strip_outer_bool_parens(trimmed) {
return comptime_cond_matches_with_context(stripped, target_os, prefs)
}
if optional_name := optional_user_ct_flag_name(trimmed) {
if prefs == unsafe { nil } {
return false
}
return pref.comptime_optional_flag_value(prefs, optional_name)
}
if pkg_name := pkgconfig_cond_name(trimmed) {
if prefs != unsafe { nil } && prefs.is_cross_target() {
return false
}
return pref.comptime_pkgconfig_value(pkg_name)
}
if prefs != unsafe { nil } {
return flag_os_matches(trimmed, target_os) || flag_pref_matches(trimmed, prefs)
}
current := normalize_target_os_name(target_os)
return match trimmed.to_lower() {
'macos', 'darwin', 'mac' { current == 'macos' || current == 'darwin' }
'linux' { current == 'linux' }
'windows' { current == 'windows' }
'bsd' { current in ['macos', 'freebsd', 'openbsd', 'netbsd', 'dragonfly'] }
'freebsd' { current == 'freebsd' }
'openbsd' { current == 'openbsd' }
'netbsd' { current == 'netbsd' }
'dragonfly' { current == 'dragonfly' }
'android' { current == 'android' }
'termux' { current == 'termux' }
'ios' { current == 'ios' }
'solaris' { current == 'solaris' }
'qnx' { current == 'qnx' }
'serenity' { current == 'serenity' }
'plan9' { current == 'plan9' }
'vinix' { current == 'vinix' }
'cross' { current == 'cross' }
'native' { false }
'emscripten' { false }
else { false } // unknown user-defined flags default to false
}
}
fn pkgconfig_cond_name(cond string) ?string {
trimmed := cond.trim_space()
if !trimmed.starts_with(r'$pkgconfig(') || !trimmed.ends_with(')') {
return none
}
arg := trimmed[r'$pkgconfig('.len..trimmed.len - 1].trim_space()
if arg.len < 2 {
return none
}
quote := arg[0]
if (quote != `'` && quote != `"`) || arg[arg.len - 1] != quote {
return none
}
return arg[1..arg.len - 1]
}
fn optional_user_ct_flag_name(cond string) ?string {
trimmed := cond.trim_space()
if !trimmed.ends_with('?') {
return none
}
name := trimmed[..trimmed.len - 1].trim_space()
if name == '' {
return none
}
return name
}
fn top_level_bool_op_index(expr string, op string) ?int {
mut depth := 0
mut i := 0
for i < expr.len {
ch := expr[i]
if ch == `(` {
depth++
} else if ch == `)` {
if depth > 0 {
depth--
}
} else if depth == 0 && i + op.len <= expr.len && expr[i..i + op.len] == op {
return i
}
i++
}
return none
}
fn strip_outer_bool_parens(expr string) ?string {
if expr.len < 2 || expr[0] != `(` || expr[expr.len - 1] != `)` {
return none
}
mut depth := 0
for i, ch in expr {
if ch == `(` {
depth++
} else if ch == `)` {
depth--
if depth == 0 && i < expr.len - 1 {
return none
}
}
}
if depth == 0 {
return expr[1..expr.len - 1].trim_space()
}
return none
}
fn default_cc(vroot string) string {
// Try to use tcc by default, like v1 does.
tcc_path := os.join_path(vroot, 'thirdparty', 'tcc', 'tcc.exe')
if os.exists(tcc_path) {
return tcc_path
}
return 'cc'
}
// fast_relink_output_is_generation_only mirrors gen_cleanc()'s generation-only
// decision: a `.c` output, a target we cannot compile locally, or a shared lib.
// Such a request must go through normal C generation, never the pre-parse relink
// (is_cmd_v2_self_build() keys only on the input file, so the relink path would
// otherwise link an executable into e.g. foo.c). Extracted so the decision is
// unit-testable without a warm object cache.
fn (b &Builder) fast_relink_output_is_generation_only(output_name string) bool {
return output_name.ends_with('.c') || !b.can_compile_cleanc_locally() || b.pref.is_shared_lib
}
// preparse_flag_fingerprint captures the flag-affecting build inputs that are
// knowable WITHOUT parsing the sources: the C compiler choice (-cc / V2CC /
// default), prod/shared mode, and the env CFLAGS (V2CFLAGS). It is recorded in
// main.stamp and re-checked by the pre-parse fast relink so a changed compiler or
// CFLAGS environment invalidates a relink even when every source file is
// unchanged. Source-derived `#flag` directives are intentionally excluded — they
// change only when a source file changes, which the stamp freshness checks catch.
fn (b &Builder) preparse_flag_fingerprint() string {
cc := if b.pref.ccompiler.len > 0 { b.pref.ccompiler } else { configured_cc(b.pref.vroot) }
return 'cc=${cc}\x01ccpref=${b.pref.ccompiler}\x01prod=${b.pref.is_prod}\x01shared=${b.pref.is_shared_lib}\x01env=${configured_cflags()}'
}
fn configured_cc(vroot string) string {
cc := (os.getenv_opt('V2CC') or { '' }).trim_space()
if cc != '' {
return cc
}
return default_cc(vroot)
}
fn configured_cflags() string {
return (os.getenv_opt('V2CFLAGS') or { '' }).trim_space()
}
fn tcc_flags(cc string, vroot string) string {
if !cc.contains('tcc') {
return ''
}
tcc_dir := os.join_path(vroot, 'thirdparty', 'tcc')
return '-I "${os.join_path(tcc_dir, 'lib', 'include')}" -L "${os.join_path(tcc_dir, 'lib')}"'
}
fn cflags_need_objc_mode(flags string) bool {
lower_flags := flags.to_lower()
for tok in lower_flags.fields() {
clean := tok.trim('"\'')
if clean.ends_with('.m') || clean.ends_with('.mm') {
return true
}
}
return lower_flags.contains('-framework cocoa') || lower_flags.contains('-framework appkit')
|| lower_flags.contains('-framework foundation') || lower_flags.contains('-framework uikit')
|| lower_flags.contains('-framework metal') || lower_flags.contains('-framework metalkit')
|| lower_flags.contains('-framework quartzcore')
}
fn cc_recompile_flags_from_cmd(cmd string) string {
parts := cmd.fields()
mut flags := []string{}
mut i := 1 // skip compiler
for i < parts.len {
p := parts[i]
if p == '-o' {
i += 2
continue
}
if p == '-x' {
if i + 1 < parts.len && parts[i + 1] != 'none' {
flags << p
flags << parts[i + 1]
}
i += 2
continue
}
if p in ['-I', '-D', '-U', '-F', '-include', '-isystem', '-idirafter'] {
flags << p
if i + 1 < parts.len {
i++
flags << parts[i]
}
i++
continue
}
if p.starts_with('-I') || p.starts_with('-D') || p.starts_with('-U') || p.starts_with('-F')
|| p.starts_with('-std=') || p.starts_with('-W') || p.starts_with('-f')
|| p.starts_with('-m') || p == '-pthread' {
flags << p
}
i++
}
return flags.join(' ')
}
// run_cc_cmd_or_exit runs a C compiler command, falling back from tcc to cc
// if needed. Returns true if tcc fell back to cc.
fn run_cc_cmd_or_exit(cmd string, stage string, show_cc bool) bool {
if show_cc {
println(cmd)
} else if os.getenv('V2VERBOSE') != '' {
dump(cmd)
}
result := os.execute(cmd)
if result.exit_code != 0 {
// If tcc failed, fall back to cc.
// Check only the compiler binary (before the first space), not the full
// command string which contains tcc in include/library flag paths.
cc_binary := cmd.all_before(' ')
if cc_binary.contains('tcc') {
eprintln('Failed to compile with tcc, falling back to cc')
eprintln('tcc cmd: ${cmd}')
eprintln(result.output)
// Replace TCC binary with cc and strip TCC-specific include/lib
// paths. TCC's tgmath.h conflicts with macOS system headers,
// causing SIMD ambiguity errors in MetalKit when compiling as
// Objective-C.
mut fallback_cmd := cmd.replace_once(cc_binary, 'cc')
tcc_dir := cc_binary.all_before_last('/tcc')
if tcc_dir.len > 0 {
// Remove -I and -L flags pointing into the TCC directory.
mut parts := fallback_cmd.fields()
mut filtered := []string{cap: parts.len}
mut i2 := 0
for i2 < parts.len {
p := parts[i2]
if (p == '-I' || p == '-L') && i2 + 1 < parts.len
&& parts[i2 + 1].contains('tcc') {
i2 += 2
continue
}
if (p.starts_with('-I') || p.starts_with('-L')) && p.contains('tcc') {
i2++
continue
}
filtered << p
i2++
}
fallback_cmd = filtered.join(' ')
}
// cc cannot read .o files produced by tcc on macOS arm64. Recompile
// any cached .o files referenced in the command from their .c siblings
// using cc before retrying the link.
recompile_flags := cc_recompile_flags_from_cmd(fallback_cmd)
for tok in fallback_cmd.fields() {
clean_tok := tok.trim('"')
if !clean_tok.ends_with('.o') {
continue
}
stem := clean_tok.all_before_last('.o')
mut c_sibling := stem + '.c'
if !os.exists(c_sibling) && stem.ends_with('.main') {
c_sibling = stem.all_before_last('.main') + '.c'
}
if !os.exists(c_sibling) {
continue
}
recompile_cmd := 'cc ${recompile_flags} -w -Wno-incompatible-function-pointer-types -c "${c_sibling}" -o "${clean_tok}"'
if show_cc {
println(recompile_cmd)
}
rr := os.execute(recompile_cmd)
if rr.exit_code != 0 {
eprintln('cc recompile failed for ${c_sibling}:')
eprintln(rr.output)
}
// Invalidate stamp so future builds rebuild from .c too.
stamp_path := stem + '.stamp'
if os.exists(stamp_path) {
os.rm(stamp_path) or {}
}
}
run_cc_cmd_or_exit(fallback_cmd, stage, show_cc)
return true
}
eprintln('${stage} failed:')
lines := result.output.split_into_lines()
limit := if lines.len < 50 { lines.len } else { 50 }
for line in lines[..limit] {
eprintln(line)
}
mut error_count := 0
mut warning_count := 0
for line in lines {
if line.contains(': error:') || line.contains(': fatal error:') {
error_count += 1
} else if line.contains(': warning:') {
warning_count += 1
}
}
if stage == 'C compilation' {
eprintln('Total: ${warning_count} warnings and ${error_count} errors')
}
exit(1)
}
return false
}
fn native_graph_stage_title(label string, title string) string {
if label == '' {
return title
}
return '${label} ${title}'
}
const macos_tiny_candidate_graph_label = 'macOS Tiny Candidate'
fn (b &Builder) native_mir_build_sequential(label string) bool {
return label == macos_tiny_candidate_graph_label || b.pref.no_parallel || b.pref.hot_fn.len > 0
}
fn (b &Builder) should_prune_native_backend_modules(arch pref.Arch) bool {
return b.pref.single_backend || arch == .arm64
}
fn native_backend_module_file_fragment(backend_mod string) string {
return match backend_mod {
'eval' { '/vlib/v2_toberemoved/eval/' }
else { '/vlib/v2_toberemoved/gen/${backend_mod}/' }
}
}
fn (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 {
mut mod := ssa.Module.new('main')
if mod == unsafe { nil } {
eprintln('error: native backend not available (compiled with stubbed ssa module)')
eprintln('hint: use v2 compiled with v1 for native code generation')
exit(1)
}
mut ssa_builder := ssa.Builder.new_with_env(mod, b.env)
ssa_builder.guard_invalid_type_payloads = true
ssa_builder.target_os = target_os
ssa_builder.minimal_runtime_roots = minimal_runtime_roots
ssa_builder.native_backend_bulk_zero_alloca = arch == .x64
mut native_sw := time.new_stopwatch()
// Pass markused data for dead code elimination. The ARM64 backend has its own
// relocation-based dead stripping; using markused before SSA makes self-hosted
// compiler builds fragile when the markused set is under-collected.
if used_fn_keys.len > 0 && arch != .arm64 {
ssa_builder.used_fn_keys = used_fn_keys.clone()
}
// Strip unused compiler backend modules before SSA. ARM64 already strips
// these symbols after codegen, so avoid building MIR for them in the first place.
if b.should_prune_native_backend_modules(arch) {
all_backends := ['cleanc', 'eval', 'v', 'c', 'x64', 'arm64']
own := match b.pref.backend {
.arm64 { 'arm64' }
.x64 { 'x64' }
.cleanc { 'cleanc' }
.v { 'v' }
.c { 'c' }
.eval { 'eval' }
}
for backend_mod in all_backends {
if backend_mod != own {
ssa_builder.skip_modules[backend_mod] = true
ssa_builder.skip_module_file_fragments[backend_mod] =
native_backend_module_file_fragment(backend_mod)
}
}
}
// In hot_fn mode, only build the target function body (skip all others)
if b.pref.hot_fn.len > 0 {
ssa_builder.hot_fn = b.pref.hot_fn
}
mut stage_start := native_sw.elapsed()
// Route the whole SSA build through the cursor-native build_all_from_flat
// on the post-transform b.flat (kept alive above). Sequential only
// (build_all_from_flat builds fn bodies in-phase).
//
// b.flat is only POST-TRANSFORM when flat markused has routed transform
// through transform_files_to_flat, or the direct native flat pipeline has
// emitted transform output directly into FlatAst.
build_from_flat := b.should_build_ssa_from_flat()
|| (b.flat.files.len > 0 && b.native_flat_pipeline_enabled && label == '')
if build_from_flat {
ssa_builder.build_all_from_flat(&b.flat)
// SSA has copied the program into MIR; keep the FlatAst lifetime out of
// the later optimizer and machine-code generator working sets.
b.flat = ast.FlatAst{}
} else if b.native_mir_build_sequential(label) {
ssa_builder.build_all(files)
} else {
// Phases 1-3 sequential, Phase 4 parallel, Phase 5 sequential
ssa_builder.skip_fn_bodies = true
ssa_builder.build_all(files)
ssa_builder.skip_fn_bodies = false
b.ssa_build_parallel(mut ssa_builder, files)
ssa_builder.generate_vinit()
}
if arch == .arm64 {
b.env.release_expr_type_cache_after_ssa()
}
print_time(native_graph_stage_title(label, 'SSA Build'),
time.Duration(native_sw.elapsed() - stage_start))
print_rss(native_graph_stage_title(label, 'after SSA build'))
stage_start = native_sw.elapsed()
ssa_optimization_required := b.native_backend_requires_ssa_optimization(arch)
ssa_optimization_ran := !b.pref.no_optimize || ssa_optimization_required
if ssa_optimization_required {
if b.pref.no_optimize {
eprintln(' opt: required for x64 backend (-O0 SSA is not supported yet)')
}
ssa_optimize.optimize(mut mod)
} else if b.pref.no_optimize {
eprintln(' opt: skipped (-O0)')
} else {
ssa_optimize.optimize(mut mod)
}
print_time(native_graph_stage_title(label, 'SSA Optimize'),
time.Duration(native_sw.elapsed() - stage_start))
print_rss(native_graph_stage_title(label, 'after SSA optimize'))
$if debug {
// Post-opt SSA verification is useful while debugging the optimizer, but it
// is currently noisy enough to block normal self-host builds. Keep it
// opt-in so `test_all.sh` and manual self-hosting can still complete.
if ssa_optimization_ran && os.getenv('V2_VERIFY') != '' {
ssa_optimize.verify_and_panic(mod, 'full optimization')
}
}
// Post-optimization SSA dump for debugging
dump_fn_name := os.getenv('V2_DUMP_OPT_SSA')
if dump_fn_name.len > 0 {
for func in mod.funcs {
if func.name == dump_fn_name {
eprintln('=== POST-OPT SSA DUMP: ${func.name} ===')
eprintln(' params_len: ${func.params.len}')
for pi, pid in func.params {
pval := mod.values[pid]
eprintln(' param[${pi}]: v${pid} kind=${pval.kind} name=`${pval.name}` typ=${pval.typ}')
}
for blk_id in func.blocks {
blk := mod.blocks[blk_id]
eprintln(' block ${blk_id} (${blk.name}):')
for dval_id in blk.instrs {
dval := mod.values[dval_id]
if dval.kind != .instruction {
continue
}
dinstr := mod.instrs[dval.index]
mut ops_str := ''
for oi, op_id in dinstr.operands {
op_v := mod.values[op_id]
ops_str += 'v${op_id}(${op_v.kind}:${op_v.name})'
if oi < dinstr.operands.len - 1 {
ops_str += ', '
}
}
eprintln(' v${dval_id}: ${dinstr.op} [${ops_str}] typ=${dval.typ}')
}
}
eprintln('=== END POST-OPT SSA DUMP ===')
}
}
}
stage_start = native_sw.elapsed()
mut mir_mod := mir.lower_from_ssa(mod)
print_time(native_graph_stage_title(label, 'MIR Lower'),
time.Duration(native_sw.elapsed() - stage_start))
mod.release_outer_arenas_after_mir_lower()
print_rss(native_graph_stage_title(label, 'after MIR lower'))
stage_start = native_sw.elapsed()
if is_windows_x64_native_target(arch, target_os) {
abi.lower_with_x64_abi(mut mir_mod, arch, native_x64_lowering_abi_for_os(target_os))
} else {
abi.lower(mut mir_mod, arch)
}
print_time(native_graph_stage_title(label, 'ABI Lower'),
time.Duration(native_sw.elapsed() - stage_start))
print_rss(native_graph_stage_title(label, 'after ABI lower'))
if arch != .arm64 {
stage_start = native_sw.elapsed()
insel.select(mut mir_mod, arch)
print_time(native_graph_stage_title(label, 'InsSel'),
time.Duration(native_sw.elapsed() - stage_start))
print_rss(native_graph_stage_title(label, 'after InsSel'))
}
return mir_mod
}
fn (mut b Builder) build_macos_tiny_candidate_mir(arch pref.Arch, target_os string) mir.Module {
if b.macos_tiny_candidate_source_flat.files.len == 0
&& b.macos_tiny_candidate_source_files.len == 0 {
eprintln('internal error: macOS tiny candidate graph was not prepared')
exit(1)
}
mut trans := transformer.Transformer.new_with_pref(b.env, b.pref)
trans.set_file_set(b.file_set)
trans.enable_macos_tiny_candidate_graph()
opts := markused.MarkUsedOptions{
minimal_runtime_roots: true
}
if b.macos_tiny_candidate_source_flat.files.len > 0 {
candidate_flat :=
trans.transform_flat_to_flat_direct(&b.macos_tiny_candidate_source_flat, [])
candidate_used_fn_keys := markused.mark_used_flat_with_options(&candidate_flat, b.env, opts)
old_flat := b.flat
b.flat = candidate_flat
candidate_mir := b.build_native_mir_from_files([], arch, target_os, true,
candidate_used_fn_keys, macos_tiny_candidate_graph_label)
b.flat = old_flat
return candidate_mir
}
candidate_files := trans.transform_files(b.macos_tiny_candidate_source_files)
candidate_used_fn_keys := markused.mark_used_with_options(candidate_files, b.env, opts)
return b.build_native_mir_from_files(candidate_files, arch, target_os, true,
candidate_used_fn_keys, macos_tiny_candidate_graph_label)
}
fn (mut b Builder) gen_native(backend_arch pref.Arch) {
arch := if backend_arch == .auto { b.pref.get_effective_arch() } else { backend_arch }
target_os := b.pref.target_os_or_host()
native_compile_flags, native_link_flags := b.native_compile_and_link_flags_from_sources()
_, native_user_link_flags := b.native_user_compile_and_link_flags_from_sources()
mut native_external_inputs := NativeExternalLinkInputs{
link_flags: native_link_flags
}
mut mir_mod := b.build_native_mir_from_files(b.files, arch, target_os,
b.uses_minimal_x64_runtime_roots(), b.used_fn_keys, '')
// The hosted SSA build has consumed b.files into `mir_mod`; the rest of the
// normal native pipeline operates on MIR only. Drop the ~120MB legacy AST so
// it can be reclaimed before codegen's working-set grows. The macOS tiny
// candidate, when requested, uses its own saved source snapshot.
b.files = []ast.File{}
// Determine output binary name from the last user file
output_binary := if b.pref.output_file != '' {
b.pref.output_file
} else if b.user_files.len > 0 {
b.default_output_name()
} else {
'out'
}
if arch == .arm64 && is_macos_native_target(target_os) {
// Use built-in linker for ARM64 macOS
mut native_sw := time.new_stopwatch()
stage_start := native_sw.elapsed()
mut gen := arm64.Gen.new(&mir_mod)
if b.pref.no_parallel {
gen.gen()
} else {
b.gen_arm64_parallel(mut gen)
}
gen.release_scratch_after_gen()
mir_mod.release_after_native_codegen()
print_time('ARM64 Gen', time.Duration(native_sw.elapsed() - stage_start))
if b.pref.hot_fn.len > 0 {
// Hot code reloading: extract raw machine code for a single function
code := gen.extract_function(b.pref.hot_fn)
if code.len > 0 {
os.write_file_array(output_binary, code) or { panic(err) }
println('hot-fn: wrote ${code.len} bytes for "${b.pref.hot_fn}" to ${output_binary}')
}
return
}
gen.link_executable(output_binary)
if b.pref.verbose {
println('[*] Linked ${output_binary} (built-in linker)')
}
} else {
// Generate object file and use external linker
obj_file := native_external_object_file(output_binary, target_os)
mut used_macos_tiny_object := false
if arch == .arm64 {
mut gen := arm64.Gen.new(&mir_mod)
if b.pref.no_parallel {
gen.gen()
} else {
b.gen_arm64_parallel(mut gen)
}
gen.release_scratch_after_gen()
mir_mod.release_after_native_codegen()
gen.write_file(obj_file)
} else {
obj_format := native_x64_object_format_for_os(target_os)
codegen_abi := native_x64_codegen_abi_for_os(target_os)
if msg := native_x64_mir_unsupported_external_symbol_message(&mir_mod, obj_format) {
eprint_native_x64_link_error(msg)
exit(1)
}
if is_windows_x64_native_target(arch, target_os) {
mut windows_gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format,
codegen_abi)
windows_gen.gen()
windows_gen.link_executable(output_binary) or {
eprint_native_x64_link_error(err.msg())
exit(1)
}
if b.pref.verbose {
println('[*] Linked ${output_binary} (built-in PE linker)')
}
return
}
if is_linux_x64_native_target(arch, target_os) {
mut linux_gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format, codegen_abi)
linux_gen.gen()
if native_link_flags_allow_builtin_linux_tiny(native_link_flags,
native_user_link_flags)
{
if os.exists(output_binary) {
os.rm(output_binary) or {}
}
linux_gen.link_linux_tiny_executable(output_binary) or {
msg := err.msg()
if linux_x64_tiny_strict_enabled()
|| !msg.starts_with(x64.linux_tiny_not_eligible_prefix) {
eprint_native_x64_link_error(msg)
exit(1)
}
}
if os.exists(output_binary) {
if b.pref.verbose {
println('[*] Linked ${output_binary} (built-in Linux tiny linker)')
}
return
}
}
linux_gen.write_file(obj_file)
} else if b.uses_macos_x64_tiny_object(arch) {
if os.exists(obj_file) {
os.rm(obj_file) or {}
}
if b.pref.verbose {
println('[*] macOS tiny object candidate enabled')
}
mut candidate_mir := b.build_macos_tiny_candidate_mir(arch, target_os)
mut candidate_gen := x64.Gen.new_with_format_and_abi(&candidate_mir, obj_format,
codegen_abi)
candidate_gen.gen()
candidate_gen.write_macos_tiny_object(obj_file) or {
msg := err.msg()
if !msg.starts_with(x64.macos_tiny_not_eligible_prefix) {
eprint_native_x64_link_error(msg)
exit(1)
}
if b.pref.verbose {
println('[*] macOS tiny object not eligible; falling back to normal Mach-O object: ${msg}')
}
}
if os.exists(obj_file) {
used_macos_tiny_object = true
} else {
if b.pref.verbose {
println('[*] macOS tiny object fallback: writing normal Mach-O object')
}
mut macos_fallback_gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format,
codegen_abi)
macos_fallback_gen.gen()
macos_fallback_gen.write_file(obj_file)
}
} else {
mut normal_x64_gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format,
codegen_abi)
normal_x64_gen.gen()
normal_x64_gen.write_file(obj_file)
}
}
if b.pref.verbose {
if used_macos_tiny_object {
println('[*] Wrote ${obj_file} (macOS tiny candidate object)')
} else {
println('[*] Wrote ${obj_file}')
}
}
// Link the object file into an executable
if is_macos_native_target(target_os) {
sdk_res := os.execute('xcrun -sdk macosx --show-sdk-path')
if sdk_res.exit_code != 0 {
eprintln('Link failed:')
eprintln('failed to resolve macOS SDK path with xcrun:')
eprintln(sdk_res.output)
exit(1)
}
sdk_path := macos_sdk_path_from_xcrun_output(sdk_res.output) or {
eprintln('Link failed:')
eprintln(err.msg())
eprintln('xcrun output:')
eprintln(sdk_res.output)
exit(1)
}
validate_macos_sdk_path_for_native_link(sdk_path) or {
eprintln('Link failed:')
eprintln(err.msg())
exit(1)
}
arch_flag := if arch == .arm64 { 'arm64' } else { 'x86_64' }
native_external_inputs = native_external_link_inputs(native_link_flags, output_binary) or {
eprint_native_x64_link_error(err.msg())
exit(1)
}
validate_macos_native_ld_link_flags(native_external_inputs.link_flags) or {
eprint_native_x64_link_error(err.msg())
exit(1)
}
b.compile_native_external_sources(native_external_inputs, native_compile_flags,
target_os, sdk_path, arch_flag) or {
cleanup_native_external_objects(native_external_inputs)
eprint_native_x64_link_error(err.msg())
exit(1)
}
normal_link_cmd := macos_native_link_command(output_binary, obj_file, sdk_path,
arch_flag, false, native_external_inputs.link_flags)
link_cmd := macos_native_link_command(output_binary, obj_file, sdk_path, arch_flag,
used_macos_tiny_object, native_external_inputs.link_flags)
mut link_result := os.execute(link_cmd)
if link_result.exit_code != 0 && used_macos_tiny_object {
if b.pref.verbose {
println('[*] macOS tiny object link failed; retrying with normal Mach-O object')
println('[*] macOS tiny object link exit code: ${link_result.exit_code}')
println('[*] macOS tiny object link output:')
if link_result.output.len > 0 {
print(link_result.output)
if !link_result.output.ends_with('\n') {
println('')
}
} else {
println('<empty>')
}
}
if os.exists(output_binary) {
os.rm(output_binary) or {}
}
obj_format := native_x64_object_format_for_os(target_os)
codegen_abi := native_x64_codegen_abi_for_os(target_os)
mut fallback_gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format,
codegen_abi)
fallback_gen.gen()
fallback_gen.write_file(obj_file)
used_macos_tiny_object = false
if b.pref.verbose {
println('[*] Wrote ${obj_file} (normal Mach-O fallback after macOS tiny link failure)')
}
link_result = os.execute(normal_link_cmd)
}
if link_result.exit_code != 0 {
eprintln('Link failed:')
eprintln(link_result.output)
cleanup_native_external_objects(native_external_inputs)
exit(1)
}
if b.pref.verbose && used_macos_tiny_object {
println('[*] Linked ${output_binary} (built-in macOS tiny object)')
}
} else {
// Linux linking
native_external_inputs = native_external_link_inputs(native_link_flags, output_binary) or {
eprint_native_x64_link_error(err.msg())
exit(1)
}
b.compile_native_external_sources(native_external_inputs, native_compile_flags,
target_os, '', '') or {
cleanup_native_external_objects(native_external_inputs)
eprint_native_x64_link_error(err.msg())
exit(1)
}
link_result := os.execute(linux_native_link_command(b.native_linux_hosted_link_compiler(),
output_binary, obj_file, native_external_inputs.link_flags))
if link_result.exit_code != 0 {
eprintln('Link failed:')
eprintln(link_result.output)
cleanup_native_external_objects(native_external_inputs)
exit(1)
}
}
if b.pref.verbose {
println('[*] Linked ${output_binary}')
}
// Clean up object file
if !b.pref.keep_c {
cleanup_native_external_objects(native_external_inputs)
os.rm(obj_file) or {}
}
}
}
fn print_time(title string, time_d time.Duration) {
println(' * ${title}: ${time_d.milliseconds()}ms')
}
fn (mut b Builder) update_parse_summary_counts() {
mut parsed_full_files_n := 0
mut parsed_vh_files_n := 0
mut parsed_full_files := []string{}
mut parsed_vh_files := []string{}
for ff in b.flat.files {
name := b.flat.file_name(ff)
if name.ends_with('.vh') {
parsed_vh_files_n++
parsed_vh_files << name
} else {
parsed_full_files_n++
parsed_full_files << name
}
}
b.parsed_full_files_n = parsed_full_files_n
b.parsed_vh_files_n = parsed_vh_files_n
b.parsed_full_files = parsed_full_files
b.parsed_vh_files = parsed_vh_files
if b.pref.stats {
b.entry_v_lines_n = count_v_lines_for_paths(b.user_files)
b.parsed_v_lines_n = b.count_parsed_v_lines()
} else {
b.entry_v_lines_n = 0
b.parsed_v_lines_n = 0
}
}
fn (b &Builder) print_flat_ast_summary() {
legacy_stats := ast.legacy_ast_stats(b.files)
legacy_nodes := ast.count_legacy_nodes(b.files)
flat_stats := b.flat.stats()
mut mem_delta_pct := f64(0)
if legacy_stats.bytes_estimate > 0 {
mem_delta_pct = (f64(legacy_stats.bytes_estimate) - f64(flat_stats.bytes_estimate)) * 100.0 / f64(legacy_stats.bytes_estimate)
}
// Flat AST uses 4 arenas (files, nodes, edges, strings) regardless of payload
// count; each is one allocation amortised across millions of cells.
flat_allocs := u64(4)
mut alloc_delta_pct := f64(0)
if legacy_stats.allocs > 0 {
alloc_delta_pct = (f64(legacy_stats.allocs) - f64(flat_allocs)) * 100.0 / f64(legacy_stats.allocs)
}
println(' * AST nodes: legacy=${legacy_nodes}, flat=${flat_stats.nodes} (edges=${flat_stats.edges}, strings=${flat_stats.strings})')
println(' * AST memory est: legacy=${legacy_stats.bytes_estimate}B, flat=${flat_stats.bytes_estimate}B (${mem_delta_pct:.2f}% reduction)')
println(' * AST allocs: legacy=${legacy_stats.allocs}, flat=${flat_allocs} (${alloc_delta_pct:.2f}% reduction)')
if os.getenv('V2_FLAT_HIST') != '' {
hist := b.flat.count_nodes_by_kind()
mut keys := hist.keys()
keys.sort_with_compare(fn [hist] (a &string, b &string) int {
return hist[*b] - hist[*a]
})
println(' * AST per-kind histogram (top 20):')
for i, k in keys {
if i >= 20 {
break
}
println(' ${k:-30s} ${hist[k]}')
}
}
}
fn count_v_lines_for_paths(paths []string) int {
mut seen_paths := map[string]bool{}
mut total_v_lines := 0
for path in paths {
norm_path := os.norm_path(path)
if norm_path in seen_paths {
continue
}
seen_paths[norm_path] = true
lines := os.read_lines(norm_path) or { continue }
total_v_lines += lines.len
}
return total_v_lines
}
fn (b &Builder) count_parsed_v_lines() int {
mut parsed_paths := []string{}
mut seen_files := map[string]bool{}
for ff in b.flat.files {
name := b.flat.file_name(ff)
if name in seen_files {
continue
}
seen_files[name] = true
parsed_paths << name
}
return count_v_lines_for_paths(parsed_paths)
}
fn 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) {
println(' * Parsed files: fully parsed files: ${parsed_full_files_n}, parsed .vh files: ${parsed_vh_files_n}')
if print_parsed_files {
if parsed_full_files.len > 0 {
println(' * Fully parsed files:')
for path in parsed_full_files {
println(' [full] ${path}')
}
}
}
if (show_stats || print_parsed_files) && parsed_vh_files.len > 0 {
println(' * Parsed .vh files:')
for path in parsed_vh_files {
println(' [vh] ${path}')
}
}
if show_stats {
println(' * Parsed V LOC (entry files): ${entry_v_lines_n}')
println(' * Parsed V LOC (all parsed sources): ${parsed_v_lines_n}')
}
}