From e8810a671233f1d2072d70ab8e17c3b1dad5e1e5 Mon Sep 17 00:00:00 2001 From: GGRei Date: Mon, 1 Jun 2026 18:11:20 +0200 Subject: [PATCH] v2: implement native x64 backend, part 1b (#27314) --- cmd/v/vvm_test.v | 30 + vlib/v/help/build/build.txt | 2 + vlib/v/pref/pref.v | 1 + vlib/v2/builder/builder.v | 253 +++- vlib/v2/builder/native_test.v | 146 ++- vlib/v2/gen/x64/elf.v | 7 + vlib/v2/gen/x64/elf_tiny.v | 832 ++++++++++++ vlib/v2/gen/x64/macho_tiny.v | 302 +++++ vlib/v2/gen/x64/pe.v | 76 +- vlib/v2/gen/x64/x64.v | 68 +- vlib/v2/gen/x64/x64_object_format_test.v | 747 +++++++++++ vlib/v2/gen/x64/x64_pe_linker_test.v | 113 +- vlib/v2/gen/x64/x64_runtime_smoke_test.v | 1114 +++++++++++++++++ vlib/v2/pref/pref.v | 16 +- vlib/v2/pref/pref_test.v | 41 + .../transformer/minimal_runtime_init_test.v | 150 +++ vlib/v2/transformer/transformer.v | 49 +- 17 files changed, 3852 insertions(+), 95 deletions(-) create mode 100644 vlib/v2/gen/x64/elf_tiny.v create mode 100644 vlib/v2/gen/x64/macho_tiny.v create mode 100644 vlib/v2/transformer/minimal_runtime_init_test.v diff --git a/cmd/v/vvm_test.v b/cmd/v/vvm_test.v index 3f52f6f1a..74c68fffd 100644 --- a/cmd/v/vvm_test.v +++ b/cmd/v/vvm_test.v @@ -199,6 +199,36 @@ fn test_v2_delegation_forwards_target_specific_flags() { assert marker.contains('main.v'), marker assert !marker.contains('-v2'), marker + res_macos := run_v_command_with_exe(wrapper_exe, project_dir, [ + '-v2', + '-b', + 'x64', + '-os', + 'macos', + '-no-mos-tiny', + '-o', + 'out', + 'main.v', + ], envs) + assert res_macos.exit_code == 0, res_macos.output + macos_marker := os.read_file(marker_file) or { panic(err) } + assert macos_marker.contains('-b x64'), macos_marker + assert macos_marker.contains('-os macos'), macos_marker + assert macos_marker.contains('-no-mos-tiny'), macos_marker + assert macos_marker.contains('-o out'), macos_marker + assert macos_marker.contains('main.v'), macos_marker + assert !macos_marker.contains('-v2'), macos_marker + + os.rm(marker_file) or {} + legacy_macos_tiny_res := run_v_command_with_exe(wrapper_exe, project_dir, [ + '-v2', + '-mos-tiny', + 'main.v', + ], envs) + assert legacy_macos_tiny_res.exit_code == 1, legacy_macos_tiny_res.output + assert legacy_macos_tiny_res.output.contains('Unknown argument `-mos-tiny`'), legacy_macos_tiny_res.output + assert !os.exists(marker_file), 'legacy -mos-tiny was forwarded to V2' + for blocked_args in [ ['--skip-builtin', '--', '-v2', 'main.v'], ['--skip-builtin', 'run', 'main.v', '-v2'], diff --git a/vlib/v/help/build/build.txt b/vlib/v/help/build/build.txt index e7f622302..c67c11020 100644 --- a/vlib/v/help/build/build.txt +++ b/vlib/v/help/build/build.txt @@ -44,6 +44,8 @@ NB: the build flags are shared with the run command too: Example: `v -v2 hello.v` or `v -v2 -b arm64 hello.v`. V2 defaults to the `cleanc` backend unless `-b`/`-backend` selects another one. Use `v -v2 -eval file.v` to run the V2 eval backend. + With `v -v2 -b x64` on macOS x64, eligible programs use the tiny object + path automatically. `-no-mos-tiny` disables it for comparison/debugging. -b , -backend Specifies the backend that will be used for building the executable. diff --git a/vlib/v/pref/pref.v b/vlib/v/pref/pref.v index 946ebc4c1..a6e3eb579 100644 --- a/vlib/v/pref/pref.v +++ b/vlib/v/pref/pref.v @@ -382,6 +382,7 @@ fn is_v2_passthrough_bool_flag(arg string) bool { '--single-backend', '-single-backend', '--freestanding', + '-no-mos-tiny', ] } diff --git a/vlib/v2/builder/builder.v b/vlib/v2/builder/builder.v index 1131bbe51..68f9dd037 100644 --- a/vlib/v2/builder/builder.v +++ b/vlib/v2/builder/builder.v @@ -59,6 +59,9 @@ mut: // 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 + // Source AST snapshot used only for an isolated macOS tiny candidate graph. + // The normal hosted graph is still built from b.files and remains the fallback. + macos_tiny_candidate_source_files []ast.File } pub fn new_builder(prefs &pref.Preferences) &Builder { @@ -235,6 +238,8 @@ pub fn (mut b Builder) build(files []string) { print_rss('after type check') print_heap('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) @@ -297,7 +302,7 @@ pub fn (mut b Builder) build(files []string) { if use_flat_markused && !flat_populated_by_transform { b.flat = ast.flatten_files(b.files) } - if b.uses_minimal_windows_x64_runtime() { + if b.uses_minimal_x64_runtime_roots() { opts := markused.MarkUsedOptions{ minimal_runtime_roots: true } @@ -1473,6 +1478,14 @@ 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) @@ -1487,6 +1500,52 @@ fn (b &Builder) uses_minimal_windows_x64_runtime() bool { 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 macos_native_link_command(output_binary string, obj_file string, sdk_path string, arch_flag string, tiny_object bool) string { + normal_link_cmd := 'ld -o ${output_binary} ${obj_file} -lSystem -syslibroot "${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 (mut b Builder) prepare_macos_tiny_candidate_source_files() { + b.macos_tiny_candidate_source_files = []ast.File{} + if !b.uses_macos_x64_tiny_object(.x64) { + return + } + b.macos_tiny_candidate_source_files = if b.flat_check_enabled { + b.flat.to_files() + } else { + b.files.clone() + } +} + fn is_macos_native_target(target_os string) bool { return normalize_target_os_name(target_os) == 'macos' } @@ -2231,29 +2290,38 @@ fn run_cc_cmd_or_exit(cmd string, stage string, show_cc bool) bool { return false } -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() +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 +} - // Build all files into a single SSA module +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') - return + 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 = b.uses_minimal_windows_x64_runtime() + 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 b.used_fn_keys.len > 0 && arch != .arm64 { - ssa_builder.used_fn_keys = b.used_fn_keys.clone() + if used_fn_keys.len > 0 && arch != .arm64 { + ssa_builder.used_fn_keys = used_fn_keys.clone() } // --single-backend: strip unused backend modules from the binary @@ -2280,25 +2348,19 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { ssa_builder.hot_fn = b.pref.hot_fn } - // Build all files together with proper multi-file ordering mut stage_start := native_sw.elapsed() - if b.pref.no_parallel || b.pref.hot_fn.len > 0 { - ssa_builder.build_all(b.files) + 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(b.files) + ssa_builder.build_all(files) ssa_builder.skip_fn_bodies = false - b.ssa_build_parallel(mut ssa_builder, b.files) + b.ssa_build_parallel(mut ssa_builder, files) ssa_builder.generate_vinit() } - print_time('SSA Build', time.Duration(native_sw.elapsed() - stage_start)) - - // SSA build has consumed b.files into `mod`; the rest of the native - // pipeline (optimize, MIR lower, ABI lower, insel, codegen, link) - // operates on `mod`/`mir_mod` only. Drop the ~120MB legacy AST so it - // can be reclaimed before codegen's working-set grows. - b.files = []ast.File{} + print_time(native_graph_stage_title(label, 'SSA Build'), + time.Duration(native_sw.elapsed() - stage_start)) stage_start = native_sw.elapsed() ssa_optimization_required := b.native_backend_requires_ssa_optimization(arch) @@ -2313,7 +2375,8 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { } else { ssa_optimize.optimize(mut mod) } - print_time('SSA Optimize', time.Duration(native_sw.elapsed() - stage_start)) + print_time(native_graph_stage_title(label, 'SSA Optimize'), + time.Duration(native_sw.elapsed() - stage_start)) $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 @@ -2361,7 +2424,8 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { stage_start = native_sw.elapsed() mut mir_mod := mir.lower_from_ssa(mod) - print_time('MIR Lower', time.Duration(native_sw.elapsed() - stage_start)) + print_time(native_graph_stage_title(label, 'MIR Lower'), + time.Duration(native_sw.elapsed() - stage_start)) stage_start = native_sw.elapsed() if is_windows_x64_native_target(arch, target_os) { @@ -2369,11 +2433,44 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { } else { abi.lower(mut mir_mod, arch) } - print_time('ABI Lower', time.Duration(native_sw.elapsed() - stage_start)) + print_time(native_graph_stage_title(label, 'ABI Lower'), + time.Duration(native_sw.elapsed() - stage_start)) stage_start = native_sw.elapsed() insel.select(mut mir_mod, arch) - print_time('InsSel', time.Duration(native_sw.elapsed() - stage_start)) + print_time(native_graph_stage_title(label, 'InsSel'), + time.Duration(native_sw.elapsed() - stage_start)) + 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_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() + candidate_files := trans.transform_files(b.macos_tiny_candidate_source_files) + opts := markused.MarkUsedOptions{ + minimal_runtime_roots: true + } + 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() + + 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 != '' { @@ -2386,7 +2483,8 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { if arch == .arm64 && is_macos_native_target(target_os) { // Use built-in linker for ARM64 macOS - stage_start = native_sw.elapsed() + 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() @@ -2413,6 +2511,7 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { } else { // Generate object file and use external linker obj_file := 'main.o' + mut used_macos_tiny_object := false if arch == .arm64 { mut gen := arm64.Gen.new(&mir_mod) @@ -2425,10 +2524,11 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { } else { obj_format := native_x64_object_format_for_os(target_os) codegen_abi := native_x64_codegen_abi_for_os(target_os) - mut gen := x64.Gen.new_with_format_and_abi(&mir_mod, obj_format, codegen_abi) - gen.gen() if is_windows_x64_native_target(arch, target_os) { - gen.link_executable(output_binary) or { + 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) } @@ -2437,11 +2537,73 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { } return } - gen.write_file(obj_file) + 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 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 { - println('[*] Wrote ${obj_file}') + 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 @@ -2449,13 +2611,38 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { sdk_res := os.execute('xcrun -sdk macosx --show-sdk-path') sdk_path := sdk_res.output.trim_space() arch_flag := if arch == .arm64 { 'arm64' } else { 'x86_64' } - link_cmd := 'ld -o ${output_binary} ${obj_file} -lSystem -syslibroot "${sdk_path}" -e _main -arch ${arch_flag} -platform_version macos 11.0.0 11.0.0' - link_result := os.execute(link_cmd) + normal_link_cmd := macos_native_link_command(output_binary, obj_file, sdk_path, + arch_flag, false) + link_cmd := macos_native_link_command(output_binary, obj_file, sdk_path, arch_flag, + used_macos_tiny_object) + 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') + } + 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) exit(1) } + if b.pref.verbose && used_macos_tiny_object { + println('[*] Linked ${output_binary} (built-in macOS tiny object)') + } } else { // Linux linking link_result := os.execute('cc ${obj_file} -o ${output_binary} -no-pie') diff --git a/vlib/v2/builder/native_test.v b/vlib/v2/builder/native_test.v index 667a1cda3..7f0713344 100644 --- a/vlib/v2/builder/native_test.v +++ b/vlib/v2/builder/native_test.v @@ -1,6 +1,8 @@ module builder +import os import v2.abi +import v2.ast import v2.gen.x64 import v2.pref @@ -28,7 +30,17 @@ fn test_native_x64_non_windows_keeps_existing_object_formats_and_sysv() { assert native_x64_lowering_abi_for_os('darwin') == abi.X64Abi.sysv } -fn test_minimal_windows_runtime_is_not_enabled_for_linux_or_macos() { +fn test_minimal_x64_runtime_and_macos_tiny_object_targets() { + old_linux_tiny := os.getenv_opt('V2_X64_LINUX_TINY') + os.unsetenv('V2_X64_LINUX_TINY') + defer { + if old := old_linux_tiny { + os.setenv('V2_X64_LINUX_TINY', old, true) + } else { + os.unsetenv('V2_X64_LINUX_TINY') + } + } + mut prefs := pref.new_preferences() prefs.backend = .x64 prefs.arch = .x64 @@ -36,18 +48,150 @@ fn test_minimal_windows_runtime_is_not_enabled_for_linux_or_macos() { prefs.target_os = 'linux' linux_builder := new_builder(&prefs) assert !linux_builder.uses_minimal_windows_x64_runtime() + assert linux_builder.uses_minimal_linux_x64_runtime() + assert linux_builder.uses_minimal_x64_runtime() + assert !linux_builder.uses_minimal_linux_x64_runtime_roots() + assert !linux_builder.uses_minimal_x64_runtime_roots() + + os.setenv('V2_X64_LINUX_TINY', '1', true) + linux_tiny_builder := new_builder(&prefs) + assert linux_tiny_builder.uses_minimal_linux_x64_runtime() + assert linux_tiny_builder.uses_minimal_linux_x64_runtime_roots() + assert linux_tiny_builder.uses_minimal_x64_runtime_roots() + os.unsetenv('V2_X64_LINUX_TINY') prefs.target_os = 'macos' macos_builder := new_builder(&prefs) assert !macos_builder.uses_minimal_windows_x64_runtime() + assert !macos_builder.uses_minimal_linux_x64_runtime() + assert !macos_builder.uses_minimal_x64_runtime() + assert !macos_builder.uses_minimal_x64_runtime_roots() + assert macos_builder.uses_macos_x64_tiny_object(.x64) + assert !macos_builder.uses_macos_x64_tiny_object(.arm64) + + macos_tiny_builder := new_builder(&prefs) + assert !macos_tiny_builder.uses_minimal_x64_runtime() + assert !macos_tiny_builder.uses_minimal_x64_runtime_roots() + assert macos_tiny_builder.uses_macos_x64_tiny_object(.x64) + assert !macos_tiny_builder.uses_macos_x64_tiny_object(.arm64) + + prefs.macos_tiny = false + macos_no_tiny_builder := new_builder(&prefs) + assert !macos_no_tiny_builder.uses_macos_x64_tiny_object(.x64) + + prefs.macos_tiny = true + prefs.arch = .auto + macos_auto_arch_builder := new_builder(&prefs) + assert !macos_auto_arch_builder.uses_minimal_x64_runtime_roots() + assert macos_auto_arch_builder.uses_macos_x64_tiny_object(.x64) + assert !macos_auto_arch_builder.uses_macos_x64_tiny_object(.arm64) + prefs.arch = .x64 prefs.target_os = 'darwin' darwin_builder := new_builder(&prefs) assert !darwin_builder.uses_minimal_windows_x64_runtime() + assert !darwin_builder.uses_minimal_linux_x64_runtime() + assert !darwin_builder.uses_minimal_x64_runtime() + assert !darwin_builder.uses_minimal_x64_runtime_roots() + assert darwin_builder.uses_macos_x64_tiny_object(.x64) prefs.target_os = 'windows' windows_builder := new_builder(&prefs) assert windows_builder.uses_minimal_windows_x64_runtime() + assert !windows_builder.uses_minimal_linux_x64_runtime() + assert windows_builder.uses_minimal_x64_runtime() + assert windows_builder.uses_minimal_x64_runtime_roots() + assert !windows_builder.uses_macos_x64_tiny_object(.x64) +} + +fn test_macos_tiny_candidate_source_snapshot_is_auto_by_default_with_opt_out() { + mut prefs := pref.new_preferences() + prefs.backend = .x64 + prefs.arch = .auto + prefs.target_os = 'macos' + + mut hosted_builder := new_builder(&prefs) + hosted_builder.files = [ + ast.File{ + mod: 'main' + name: 'main.v' + }, + ] + hosted_builder.prepare_macos_tiny_candidate_source_files() + assert hosted_builder.macos_tiny_candidate_source_files.len == 1 + assert hosted_builder.macos_tiny_candidate_source_files[0].name == 'main.v' + + prefs.macos_tiny = false + mut opt_out_builder := new_builder(&prefs) + opt_out_builder.files = [ + ast.File{ + mod: 'main' + name: 'main.v' + }, + ] + opt_out_builder.prepare_macos_tiny_candidate_source_files() + assert opt_out_builder.macos_tiny_candidate_source_files.len == 0 + assert !opt_out_builder.uses_minimal_x64_runtime_roots() +} + +fn test_macos_tiny_candidate_source_snapshot_uses_flat_when_enabled() { + mut prefs := pref.new_preferences() + prefs.backend = .x64 + prefs.arch = .auto + prefs.target_os = 'macos' + + source_files := [ + ast.File{ + mod: 'main' + name: 'flat_main.v' + stmts: [ + ast.Stmt(ast.FnDecl{ + name: 'main' + }), + ] + }, + ] + mut tiny_builder := new_builder(&prefs) + tiny_builder.flat_check_enabled = true + tiny_builder.files = [ + ast.File{ + mod: 'stale' + name: 'stale.v' + }, + ] + tiny_builder.flat = ast.flatten_files(source_files) + tiny_builder.prepare_macos_tiny_candidate_source_files() + assert tiny_builder.macos_tiny_candidate_source_files.len == source_files.len + assert tiny_builder.macos_tiny_candidate_source_files[0].name == 'flat_main.v' + assert tiny_builder.macos_tiny_candidate_source_files[0].mod == 'main' +} + +fn test_macos_tiny_candidate_native_mir_build_policy_is_sequential_only_for_candidate() { + mut prefs := pref.new_preferences() + prefs.backend = .x64 + prefs.arch = .x64 + prefs.target_os = 'macos' + prefs.no_parallel = false + prefs.hot_fn = '' + builder := new_builder(&prefs) + + assert !builder.native_mir_build_sequential('') + assert builder.native_mir_build_sequential(macos_tiny_candidate_graph_label) + + prefs.no_parallel = true + no_parallel_builder := new_builder(&prefs) + assert no_parallel_builder.native_mir_build_sequential('') +} + +fn test_macos_tiny_link_command_adds_hygiene_only_for_tiny_object() { + normal := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', false) + tiny := macos_native_link_command('/tmp/out', 'main.o', '/SDK Path', 'x86_64', true) + + assert normal == 'ld -o /tmp/out main.o -lSystem -syslibroot "/SDK Path" -e _main -arch x86_64 -platform_version macos 11.0.0 11.0.0' + assert tiny == '${normal} -dead_strip -x -S' + assert !normal.contains('-dead_strip') + assert !normal.contains(' -x') + assert !normal.contains(' -S') } fn test_native_x64_requires_ssa_optimization() { diff --git a/vlib/v2/gen/x64/elf.v b/vlib/v2/gen/x64/elf.v index 3b6dad646..e90b5463e 100644 --- a/vlib/v2/gen/x64/elf.v +++ b/vlib/v2/gen/x64/elf.v @@ -16,8 +16,15 @@ const elfclass64 = 2 const elfdata2lsb = 1 const ev_current = 1 const et_rel = 1 // Relocatable file +const et_exec = 2 // Executable file const em_x86_64 = 62 +const pt_load = 1 + +const pf_x = 0x1 +const pf_w = 0x2 +const pf_r = 0x4 + const sht_progbits = 1 const sht_symtab = 2 const sht_strtab = 3 diff --git a/vlib/v2/gen/x64/elf_tiny.v b/vlib/v2/gen/x64/elf_tiny.v new file mode 100644 index 000000000..deef5c560 --- /dev/null +++ b/vlib/v2/gen/x64/elf_tiny.v @@ -0,0 +1,832 @@ +// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. + +module x64 + +import encoding.binary +import os + +const linux_tiny_base_vaddr = u64(0x400000) +const linux_tiny_page_align = 0x1000 +const linux_sys_mmap = u32(9) +const linux_sys_write = u32(1) +const linux_sys_exit_group = u32(231) +const linux_mmap_prot_read_write = u32(0x3) +const linux_mmap_private_anonymous = u32(0x22) +const linux_tiny_int_str_arena_bytes = u32(4096) +const linux_tiny_int_str_slot_bytes = 32 +const linux_tiny_int_str_arena_metadata_bytes = 16 + +pub const linux_tiny_not_eligible_prefix = 'Linux tiny executable is not eligible: ' + +struct ElfTextRange { + name string + start u64 + end u64 +} + +struct ElfDataRange { + section ObjectSection + start u64 + end u64 +} + +struct ElfTinyRuntime { +mut: + text []u8 + symbols map[string]u64 + int_str_arena_patches []int +} + +struct ElfTinyReachable { + names []string + data []ElfDataRange + runtime_symbols map[string]bool +} + +struct ElfTinyLinker { + elf &ElfObject +} + +pub fn (mut g Gen) link_linux_tiny_executable(path string) ! { + if g.obj_format != .elf { + return error('Linux tiny executable linking requires ELF object output') + } + if g.abi != .sysv { + return error('Linux tiny executable linking requires SysV x64 code generation') + } + mut linker := ElfTinyLinker{ + elf: g.elf + } + linker.write(path)! +} + +fn (mut l ElfTinyLinker) write(path string) ! { + reachable := l.collect_reachable()! + if stdout := l.ultra_constant_stdout(reachable) { + l.write_ultra_executable(path, stdout)! + return + } + selected_names := reachable.names + selected_data := reachable.data + mut text := []u8{} + entry_call_field := elf_tiny_emit_start(mut text) + mut func_offsets := map[string]u64{} + for name in selected_names { + range := l.text_range(name)! + func_offsets[name] = u64(text.len) + text << l.elf.text_data[int(range.start)..int(range.end)] + } + mut runtime := l.build_runtime(reachable.runtime_symbols) + runtime_base := u64(text.len) + text << runtime.text + + mut rodata := []u8{} + mut data := []u8{} + mut data_offsets := map[string]u64{} + l.copy_data_ranges(selected_data, .rodata, mut rodata, mut data_offsets)! + l.copy_data_ranges(selected_data, .data, mut data, mut data_offsets)! + bss_bytes := if 'builtin__int__str' in reachable.runtime_symbols { + linux_tiny_int_str_arena_metadata_bytes + } else { + 0 + } + if rodata.len > 0 { + for text.len % int(l.data_section_alignment(.rodata)) != 0 { + text << u8(0) + } + } + + phnum := if data.len > 0 || bss_bytes > 0 { 2 } else { 1 } + text_off := elf_tiny_text_file_offset(phnum) + data_off := elf_tiny_data_file_offset(text_off, text.len, rodata.len) + text_vaddr := linux_tiny_base_vaddr + u64(text_off) + rodata_vaddr := text_vaddr + u64(text.len) + data_vaddr := linux_tiny_base_vaddr + u64(data_off) + bss_vaddr := if data.len > 0 { data_vaddr + u64(data.len) } else { data_vaddr } + if bss_bytes > 0 { + for field_off in runtime.int_str_arena_patches { + elf_tiny_patch_rel32(mut text, int(runtime_base) + field_off, text_vaddr, 0, bss_vaddr) + } + } + mut symbols := map[string]u64{} + for name, off in func_offsets { + symbols[name] = text_vaddr + off + } + for name, off in runtime.symbols { + symbols[name] = text_vaddr + runtime_base + off + } + for name, off in data_offsets { + symbols[name] = if name in l.rodata_symbol_names() { + rodata_vaddr + off + } else { + data_vaddr + off + } + } + main_vaddr := symbols['main'] or { + return error('Linux tiny executable requires a defined main symbol') + } + elf_tiny_patch_rel32(mut text, entry_call_field, text_vaddr, 0, main_vaddr) + l.apply_relocations(mut text, selected_names, func_offsets, symbols, text_vaddr)! + l.write_executable(path, text, rodata, data, bss_bytes, phnum, text_off, data_off)! +} + +fn (l ElfTinyLinker) ultra_constant_stdout(reachable ElfTinyReachable) ?[]u8 { + expected_names := ['main', 'builtin__println', 'builtin___writeln_to_fd', + 'builtin___write_buf_to_fd'] + if !elf_tiny_string_arrays_equal(reachable.names, expected_names) { + return none + } + if reachable.data.len != 1 { + return none + } + if reachable.runtime_symbols.len != 2 || 'write' !in reachable.runtime_symbols + || 'fflush' !in reachable.runtime_symbols { + return none + } + data_range := reachable.data[0] + if data_range.section != .rodata { + return none + } + literal := 'Hello, World!'.bytes() + if data_range.end - data_range.start != u64(literal.len + 1) { + return none + } + for i, b in literal { + if l.elf.rodata[int(data_range.start) + i] != b { + return none + } + } + if l.elf.rodata[int(data_range.start) + literal.len] != 0 { + return none + } + if !l.ultra_hello_world_main_shape_matches(data_range) { + return none + } + mut stdout := literal.clone() + stdout << u8(`\n`) + return stdout +} + +fn (l ElfTinyLinker) ultra_hello_world_main_shape_matches(data_range ElfDataRange) bool { + range := l.text_range('main') or { return false } + main_text := l.elf.text_data[int(range.start)..int(range.end)] + expected_main_text := [ + u8(0xf3), + 0x0f, + 0x1e, + 0xfa, + 0x55, + 0x48, + 0x89, + 0xe5, + 0x53, + 0x48, + 0x83, + 0xec, + 0x38, + 0x48, + 0x8d, + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x48, + 0x89, + 0x45, + 0xe0, + 0xb8, + 0x0d, + 0x00, + 0x00, + 0x00, + 0x89, + 0x45, + 0xe8, + 0xb8, + 0x01, + 0x00, + 0x00, + 0x00, + 0x89, + 0x45, + 0xec, + 0x48, + 0x8d, + 0x45, + 0xe0, + 0x49, + 0x89, + 0xc2, + 0x49, + 0x8b, + 0x3a, + 0x49, + 0x8b, + 0x72, + 0x08, + 0x31, + 0xc0, + 0xe8, + 0x00, + 0x00, + 0x00, + 0x00, + 0x31, + 0xc0, + 0x48, + 0x83, + 0xc4, + 0x38, + 0x5b, + 0x5d, + 0xc3, + ] + if main_text.len != expected_main_text.len { + return false + } + for i, b in expected_main_text { + if main_text[i] != b { + return false + } + } + mut relocs := []ElfRela{} + for reloc in l.elf.text_relocs { + if reloc.offset >= range.start && reloc.offset < range.end { + relocs << reloc + } + } + relocs.sort(a.offset < b.offset) + if relocs.len != 2 { + return false + } + rodata_reloc := relocs[0] + if rodata_reloc.offset - range.start != 16 + || rodata_reloc.info & 0xffff_ffff != u64(r_x86_64_pc32) || rodata_reloc.addend != -4 { + return false + } + rodata_sym := l.reloc_symbol(rodata_reloc) or { return false } + if rodata_sym.shndx != u16(l.elf_section_index(.rodata)) || rodata_sym.value != data_range.start { + return false + } + println_reloc := relocs[1] + if println_reloc.offset - range.start != 57 + || println_reloc.info & 0xffff_ffff != u64(r_x86_64_plt32) || println_reloc.addend != -4 { + return false + } + println_sym := l.reloc_symbol(println_reloc) or { return false } + return println_sym.name == 'builtin__println' + && println_sym.shndx == u16(l.elf_section_index(.text)) +} + +fn elf_tiny_string_arrays_equal(left []string, right []string) bool { + if left.len != right.len { + return false + } + for i, value in left { + if value != right[i] { + return false + } + } + return true +} + +fn (mut l ElfTinyLinker) collect_reachable() !ElfTinyReachable { + if _ := l.text_range('main') { + } else { + return linux_tiny_not_eligible('missing main symbol') + } + mut selected := []string{} + mut seen := map[string]bool{} + mut queue := ['main'] + mut selected_data := []ElfDataRange{} + mut runtime_symbols := map[string]bool{} + for queue.len > 0 { + name := queue[0] + queue.delete(0) + if seen[name] { + continue + } + if l.tiny_runtime_symbol_name(name) { + continue + } + if l.tiny_unsupported_defined_symbol_name(name) { + return linux_tiny_not_eligible('`${name}` is not covered by the tiny runtime yet') + } + range := l.text_range(name)! + seen[name] = true + selected << name + for reloc in l.elf.text_relocs { + if reloc.offset < range.start || reloc.offset >= range.end { + continue + } + sym := l.reloc_symbol(reloc)! + if l.tiny_runtime_symbol_name(sym.name) { + runtime_symbols[sym.name] = true + continue + } + if sym.shndx == u16(l.elf_section_index(.text)) { + if l.tiny_unsupported_defined_symbol_name(sym.name) { + return linux_tiny_not_eligible('`${sym.name}` is not covered by the tiny runtime yet') + } + if !seen[sym.name] { + queue << sym.name + } + } else if sym.shndx == u16(l.elf_section_index(.rodata)) { + l.add_data_range(mut selected_data, .rodata, sym.value)! + } else if sym.shndx == u16(l.elf_section_index(.data)) { + l.add_data_range(mut selected_data, .data, sym.value)! + } else if sym.shndx == 0 { + if !l.tiny_runtime_symbol_name(sym.name) { + return linux_tiny_not_eligible('external symbol `${sym.name}` referenced from `${name}` requires the hosted linker') + } + runtime_symbols[sym.name] = true + } + } + } + return ElfTinyReachable{ + names: selected + data: selected_data + runtime_symbols: runtime_symbols + } +} + +fn (l ElfTinyLinker) apply_relocations(mut text []u8, selected_names []string, func_offsets map[string]u64, symbols map[string]u64, text_vaddr u64) ! { + for name in selected_names { + range := l.text_range(name)! + new_base := func_offsets[name] + for reloc in l.elf.text_relocs { + if reloc.offset < range.start || reloc.offset >= range.end { + continue + } + reloc_type := reloc.info & 0xffff_ffff + if reloc_type !in [u64(r_x86_64_pc32), u64(r_x86_64_plt32)] { + return linux_tiny_not_eligible('ELF relocation type ${reloc_type} is not supported') + } + sym := l.reloc_symbol(reloc)! + target := symbols[sym.name] or { + return linux_tiny_not_eligible('symbol `${sym.name}` is not resolved by the tiny runtime') + } + field_off := int(new_base + reloc.offset - range.start) + field_vaddr := text_vaddr + u64(field_off) + disp := i64(target) + reloc.addend - i64(field_vaddr) + if disp < i64(-2147483648) || disp > i64(2147483647) { + return error('Linux tiny executable relocation for `${sym.name}` is out of range') + } + binary.little_endian_put_u32(mut text[field_off..field_off + 4], u32(i32(disp))) + } + } +} + +fn (mut l ElfTinyLinker) build_runtime(runtime_symbols map[string]bool) ElfTinyRuntime { + mut rt := ElfTinyRuntime{ + symbols: map[string]u64{} + } + if 'fflush' in runtime_symbols { + rt.symbols['fflush'] = u64(rt.text.len) + rt.text << [u8(0x31), 0xc0, 0xc3] // xor eax, eax; ret + } + if 'write' in runtime_symbols { + rt.symbols['write'] = u64(rt.text.len) + elf_tiny_emit_write(mut rt) + } + if 'exit' in runtime_symbols { + rt.symbols['exit'] = u64(rt.text.len) + rt.text << [u8(0xb8)] + write_u32_le(mut rt.text, linux_sys_exit_group) + rt.text << [u8(0x0f), 0x05, 0x0f, 0x0b] // syscall; ud2 + } + if 'builtin__int__str' in runtime_symbols { + rt.symbols['builtin__int__str'] = u64(rt.text.len) + elf_tiny_emit_int_str(mut rt) + } + return rt +} + +fn elf_tiny_emit_start(mut text []u8) int { + text << [u8(0x31), 0xed] // xor ebp, ebp + text << u8(0xe8) + call_field := text.len + text << [u8(0), 0, 0, 0] + text << [u8(0x89), 0xc7] // mov edi, eax + text << u8(0xb8) + write_u32_le(mut text, linux_sys_exit_group) + text << [u8(0x0f), 0x05, 0x0f, 0x0b] // syscall; ud2 + return call_field +} + +fn elf_tiny_emit_write(mut rt ElfTinyRuntime) { + rt.text << [u8(0x49), 0x89, 0xd0] // mov r8, rdx + rt.text << [u8(0x49), 0x89, 0xf1] // mov r9, rsi + rt.text << [u8(0x45), 0x31, 0xd2] // xor r10d, r10d + rt.text << [u8(0x48), 0x85, 0xd2] // test rdx, rdx + rt.text << [u8(0x7f), 0x03] // jg loop + rt.text << [u8(0x31), 0xc0] // xor eax, eax + rt.text << u8(0xc3) // ret + loop_start := rt.text.len + rt.text << u8(0xb8) + write_u32_le(mut rt.text, linux_sys_write) + rt.text << [u8(0x4c), 0x89, 0xce] // mov rsi, r9 + rt.text << [u8(0x4c), 0x89, 0xc2] // mov rdx, r8 + rt.text << [u8(0x0f), 0x05] // syscall + rt.text << [u8(0x48), 0x85, 0xc0] // test rax, rax + progress_field := elf_tiny_emit_rel8_placeholder(mut rt.text, 0x7f) // jg progress + rt.text << [u8(0xbf), 0x01, 0, 0, 0] // mov edi, 1 + rt.text << u8(0xb8) + write_u32_le(mut rt.text, linux_sys_exit_group) + rt.text << [u8(0x0f), 0x05, 0x0f, 0x0b] // syscall; ud2 + progress_off := rt.text.len + rt.text << [u8(0x49), 0x01, 0xc1] // add r9, rax + rt.text << [u8(0x49), 0x29, 0xc0] // sub r8, rax + rt.text << [u8(0x49), 0x01, 0xc2] // add r10, rax + rt.text << [u8(0x4d), 0x85, 0xc0] // test r8, r8 + loop_field := elf_tiny_emit_rel8_placeholder(mut rt.text, 0x75) // jne loop + rt.text << [u8(0x4c), 0x89, 0xd0] // mov rax, r10 + rt.text << u8(0xc3) // ret + elf_tiny_patch_rel8(mut rt.text, progress_field, progress_off) + elf_tiny_patch_rel8(mut rt.text, loop_field, loop_start) +} + +fn elf_tiny_emit_ultra_stdout(mut text []u8, stdout_len int) int { + text << [u8(0x31), 0xed] // xor ebp, ebp + text << [u8(0x49), 0xc7, 0xc0] + write_u32_le(mut text, u32(stdout_len)) // mov r8, stdout_len + text << [u8(0x4c), 0x8d, 0x0d] // lea r9, [rip+stdout] + stdout_field := text.len + text << [u8(0), 0, 0, 0] + text << [u8(0x4d), 0x85, 0xc0] // test r8, r8 + done_field := elf_tiny_emit_rel8_placeholder(mut text, 0x74) // je done + loop_start := text.len + text << u8(0xb8) + write_u32_le(mut text, linux_sys_write) + text << [u8(0xbf), 0x01, 0, 0, 0] // mov edi, 1 + text << [u8(0x4c), 0x89, 0xce] // mov rsi, r9 + text << [u8(0x4c), 0x89, 0xc2] // mov rdx, r8 + text << [u8(0x0f), 0x05] // syscall + text << [u8(0x48), 0x85, 0xc0] // test rax, rax + fail_field := elf_tiny_emit_rel8_placeholder(mut text, 0x7e) // jle fail + text << [u8(0x49), 0x01, 0xc1] // add r9, rax + text << [u8(0x49), 0x29, 0xc0] // sub r8, rax + loop_field := elf_tiny_emit_rel8_placeholder(mut text, 0x75) // jne loop + done_off := text.len + text << [u8(0x31), 0xff] // xor edi, edi + text << u8(0xb8) + write_u32_le(mut text, linux_sys_exit_group) + text << [u8(0x0f), 0x05, 0x0f, 0x0b] // syscall; ud2 + fail_off := text.len + text << [u8(0xbf), 0x01, 0, 0, 0] // mov edi, 1 + text << u8(0xb8) + write_u32_le(mut text, linux_sys_exit_group) + text << [u8(0x0f), 0x05, 0x0f, 0x0b] // syscall; ud2 + elf_tiny_patch_rel8(mut text, done_field, done_off) + elf_tiny_patch_rel8(mut text, fail_field, fail_off) + elf_tiny_patch_rel8(mut text, loop_field, loop_start) + return stdout_field +} + +fn elf_tiny_emit_int_str(mut rt ElfTinyRuntime) { + rt.text << u8(0x57) // push rdi + rt.text << [u8(0x4c), 0x8d, 0x1d] // lea r11, [arena] + rt.int_str_arena_patches << rt.text.len + rt.text << [u8(0), 0, 0, 0] + rt.text << [u8(0x4d), 0x8b, 0x03] // mov r8, [r11] + rt.text << [u8(0x4d), 0x8b, 0x4b, 0x08] // mov r9, [r11+8] + rt.text << [u8(0x4d), 0x85, 0xc0] // test r8, r8 + need_mmap_field := elf_tiny_emit_rel8_placeholder(mut rt.text, 0x74) // je need_mmap + rt.text << [u8(0x49), 0x8d, 0x40, u8(linux_tiny_int_str_slot_bytes)] // lea rax, [r8+32] + rt.text << [u8(0x4c), 0x39, 0xc8] // cmp rax, r9 + have_block_field := elf_tiny_emit_rel8_placeholder(mut rt.text, 0x76) // jbe have_block + need_mmap_off := rt.text.len + rt.text << [u8(0x31), 0xff] // xor edi, edi + rt.text << u8(0xbe) + write_u32_le(mut rt.text, linux_tiny_int_str_arena_bytes) + rt.text << u8(0xba) + write_u32_le(mut rt.text, linux_mmap_prot_read_write) + rt.text << [u8(0x41), 0xba] + write_u32_le(mut rt.text, linux_mmap_private_anonymous) + rt.text << [u8(0x49), 0xc7, 0xc0] + write_u32_le(mut rt.text, u32(0xffff_ffff)) + rt.text << [u8(0x45), 0x31, 0xc9] // xor r9d, r9d + rt.text << u8(0xb8) + write_u32_le(mut rt.text, linux_sys_mmap) + rt.text << [u8(0x0f), 0x05] // syscall + rt.text << [u8(0x48), 0x85, 0xc0] // test rax, rax + mmap_ok_field := elf_tiny_emit_rel8_placeholder(mut rt.text, 0x79) // jns mmap_ok + rt.text << u8(0xbf) + write_u32_le(mut rt.text, 1) + rt.text << u8(0xb8) + write_u32_le(mut rt.text, linux_sys_exit_group) + rt.text << [u8(0x0f), 0x05, 0x0f, 0x0b] // syscall; ud2 + mmap_ok_off := rt.text.len + rt.text << [u8(0x4c), 0x8d, 0x40, u8(linux_tiny_int_str_slot_bytes)] // lea r8, [rax+32] + rt.text << [u8(0x49), 0x89, 0xc1] // mov r9, rax + rt.text << [u8(0x49), 0x81, 0xc1] + write_u32_le(mut rt.text, linux_tiny_int_str_arena_bytes) // add r9, 4096 + rt.text << [u8(0x4c), 0x8d, 0x1d] // lea r11, [arena] + rt.int_str_arena_patches << rt.text.len + rt.text << [u8(0), 0, 0, 0] + rt.text << [u8(0x4d), 0x89, 0x03] // mov [r11], r8 + rt.text << [u8(0x4d), 0x89, 0x4b, 0x08] // mov [r11+8], r9 + allocated_field := elf_tiny_emit_rel8_placeholder(mut rt.text, 0xeb) // jmp allocated + have_block_off := rt.text.len + rt.text << [u8(0x49), 0x89, 0x03] // mov [r11], rax + rt.text << [u8(0x49), 0x89, 0xc0] // mov r8, rax + allocated_off := rt.text.len + rt.text << u8(0x5f) // pop rdi + rt.text << [u8(0x89), 0xf8] // mov eax, edi + rt.text << [u8(0x45), 0x31, 0xc9] // xor r9d, r9d + rt.text << [u8(0x45), 0x31, 0xd2] // xor r10d, r10d + rt.text << [u8(0x85), 0xc0] // test eax, eax + non_negative_field := elf_tiny_emit_rel8_placeholder(mut rt.text, 0x79) // jns non_negative + rt.text << [u8(0x41), 0xb2, 0x01] // mov r10b, 1 + rt.text << [u8(0xf7), 0xd8] // neg eax + non_negative_off := rt.text.len + rt.text << [u8(0x85), 0xc0] // test eax, eax + non_zero_field := elf_tiny_emit_rel8_placeholder(mut rt.text, 0x75) // jne loop + rt.text << [u8(0x49), 0xff, 0xc8] // dec r8 + rt.text << [u8(0x41), 0xc6, 0x00, 0x30] // mov byte ptr [r8], '0' + rt.text << [u8(0x41), 0xb9, 0x01, 0, 0, 0] // mov r9d, 1 + maybe_sign_field := elf_tiny_emit_rel8_placeholder(mut rt.text, 0xeb) // jmp maybe_sign + loop_start := rt.text.len + rt.text << [u8(0x31), 0xd2] // xor edx, edx + rt.text << [u8(0xb9), 0x0a, 0, 0, 0] // mov ecx, 10 + rt.text << [u8(0xf7), 0xf1] // div ecx + rt.text << [u8(0x80), 0xc2, 0x30] // add dl, '0' + rt.text << [u8(0x49), 0xff, 0xc8] // dec r8 + rt.text << [u8(0x41), 0x88, 0x10] // mov [r8], dl + rt.text << [u8(0x49), 0xff, 0xc1] // inc r9 + rt.text << [u8(0x85), 0xc0] // test eax, eax + loop_field := elf_tiny_emit_rel8_placeholder(mut rt.text, 0x75) // jne loop + maybe_sign_off := rt.text.len + rt.text << [u8(0x45), 0x84, 0xd2] // test r10b, r10b + done_field := elf_tiny_emit_rel8_placeholder(mut rt.text, 0x74) // je done + rt.text << [u8(0x49), 0xff, 0xc8] // dec r8 + rt.text << [u8(0x41), 0xc6, 0x00, 0x2d] // mov byte ptr [r8], '-' + rt.text << [u8(0x49), 0xff, 0xc1] // inc r9 + done_off := rt.text.len + rt.text << [u8(0x4c), 0x89, 0xc0] // mov rax, r8 + rt.text << [u8(0x4c), 0x89, 0xca] // mov rdx, r9 + rt.text << u8(0xc3) + elf_tiny_patch_rel8(mut rt.text, non_negative_field, non_negative_off) + elf_tiny_patch_rel8(mut rt.text, non_zero_field, loop_start) + elf_tiny_patch_rel8(mut rt.text, maybe_sign_field, maybe_sign_off) + elf_tiny_patch_rel8(mut rt.text, loop_field, loop_start) + elf_tiny_patch_rel8(mut rt.text, done_field, done_off) + elf_tiny_patch_rel8(mut rt.text, mmap_ok_field, mmap_ok_off) + elf_tiny_patch_rel8(mut rt.text, need_mmap_field, need_mmap_off) + elf_tiny_patch_rel8(mut rt.text, have_block_field, have_block_off) + elf_tiny_patch_rel8(mut rt.text, allocated_field, allocated_off) +} + +fn (mut l ElfTinyLinker) write_executable(path string, text []u8, rodata []u8, data []u8, bss_bytes int, phnum int, text_off int, data_off int) ! { + rodata_off := text_off + text.len + text_filesz := rodata_off + rodata.len + mut buf := []u8{} + buf << [u8(ei_mag0), ei_mag1, ei_mag2, ei_mag3] + buf << [u8(elfclass64), elfdata2lsb, ev_current, 0] + for _ in 0 .. 8 { + buf << u8(0) + } + write_u16_le(mut buf, et_exec) + write_u16_le(mut buf, em_x86_64) + write_u32_le(mut buf, ev_current) + write_u64_le(mut buf, linux_tiny_base_vaddr + u64(text_off)) + write_u64_le(mut buf, 64) + write_u64_le(mut buf, 0) + write_u32_le(mut buf, 0) + write_u16_le(mut buf, 64) + write_u16_le(mut buf, 56) + write_u16_le(mut buf, u16(phnum)) + write_u16_le(mut buf, 0) + write_u16_le(mut buf, 0) + write_u16_le(mut buf, 0) + elf_tiny_write_phdr(mut buf, pt_load, pf_r | pf_x, 0, linux_tiny_base_vaddr, u64(text_filesz), + u64(text_filesz), linux_tiny_page_align) + if data.len > 0 || bss_bytes > 0 { + rw_filesz := u64(data.len) + rw_memsz := u64(data.len + bss_bytes) + rw_offset := if data.len > 0 { u64(data_off) } else { u64(0) } + elf_tiny_write_phdr(mut buf, pt_load, pf_r | pf_w, rw_offset, linux_tiny_base_vaddr + + u64(data_off), rw_filesz, rw_memsz, linux_tiny_page_align) + } + for buf.len < text_off { + buf << u8(0) + } + buf << text + buf << rodata + if data.len > 0 { + for buf.len < data_off { + buf << u8(0) + } + buf << data + } + os.write_file_array(path, buf)! + os.chmod(path, 0o755)! +} + +fn (mut l ElfTinyLinker) write_ultra_executable(path string, stdout []u8) ! { + phnum := 1 + text_off := elf_tiny_text_file_offset(phnum) + text_vaddr := linux_tiny_base_vaddr + u64(text_off) + mut text := []u8{} + stdout_field := elf_tiny_emit_ultra_stdout(mut text, stdout.len) + stdout_vaddr := text_vaddr + u64(text.len) + elf_tiny_patch_rel32(mut text, stdout_field, text_vaddr, 0, stdout_vaddr) + l.write_executable(path, text, stdout, []u8{}, 0, phnum, text_off, 0)! +} + +fn elf_tiny_emit_rel8_placeholder(mut text []u8, opcode u8) int { + text << opcode + field_off := text.len + text << u8(0) + return field_off +} + +fn elf_tiny_patch_rel8(mut text []u8, field_off int, target_off int) { + disp := target_off - (field_off + 1) + if disp < -128 || disp > 127 { + panic('Linux tiny executable short branch is out of range') + } + text[field_off] = if disp < 0 { u8(256 + disp) } else { u8(disp) } +} + +fn elf_tiny_write_phdr(mut b []u8, type_ u32, flags u32, off u64, vaddr u64, filesz u64, memsz u64, align u64) { + write_u32_le(mut b, type_) + write_u32_le(mut b, flags) + write_u64_le(mut b, off) + write_u64_le(mut b, vaddr) + write_u64_le(mut b, vaddr) + write_u64_le(mut b, filesz) + write_u64_le(mut b, memsz) + write_u64_le(mut b, align) +} + +fn (l ElfTinyLinker) text_range(name string) !ElfTextRange { + ranges := l.text_symbol_ranges() + for range in ranges { + if range.name == name { + return range + } + } + return error('missing text symbol `${name}`') +} + +fn (l ElfTinyLinker) text_symbol_ranges() []ElfTextRange { + mut syms := []ElfSymbol{} + for sym in l.elf.symbols { + if sym.name != '' && sym.shndx == u16(l.elf_section_index(.text)) { + syms << sym + } + } + syms.sort(a.value < b.value) + mut ranges := []ElfTextRange{} + for i, sym in syms { + mut end := u64(l.elf.text_data.len) + for j := i + 1; j < syms.len; j++ { + if syms[j].value > sym.value { + end = syms[j].value + break + } + } + if end > sym.value { + ranges << ElfTextRange{ + name: sym.name + start: sym.value + end: end + } + } + } + return ranges +} + +fn (l ElfTinyLinker) reloc_symbol(reloc ElfRela) !ElfSymbol { + sym_idx := int(reloc.info >> 32) + if sym_idx < 0 || sym_idx >= l.elf.symbols.len { + return error('Linux tiny executable relocation references invalid symbol index ${sym_idx}') + } + return l.elf.symbols[sym_idx] +} + +fn (l ElfTinyLinker) elf_section_index(section ObjectSection) int { + return match section { + .text { 1 } + .data { 2 } + .rodata { 3 } + } +} + +fn (l ElfTinyLinker) tiny_runtime_symbol_name(name string) bool { + return name in ['write', 'exit', 'fflush', 'builtin__int__str'] +} + +fn (l ElfTinyLinker) tiny_unsupported_defined_symbol_name(name string) bool { + return name in ['builtin__print', 'builtin__int__str_l'] +} + +fn (mut l ElfTinyLinker) add_data_range(mut ranges []ElfDataRange, section ObjectSection, start u64) ! { + end := l.data_symbol_range_end(section, start)! + for range in ranges { + if range.section == section && range.start == start && range.end == end { + return + } + } + ranges << ElfDataRange{ + section: section + start: start + end: end + } +} + +fn (l ElfTinyLinker) data_symbol_range_end(section ObjectSection, start u64) !u64 { + section_idx := u16(l.elf_section_index(section)) + section_len := match section { + .rodata { u64(l.elf.rodata.len) } + .data { u64(l.elf.data_data.len) } + else { u64(0) } + } + + mut end := section_len + for sym in l.elf.symbols { + if sym.shndx == section_idx && sym.value > start && sym.value < end { + end = sym.value + } + } + if end < start { + return error('invalid data symbol range') + } + return end +} + +fn (l ElfTinyLinker) rodata_symbol_names() map[string]bool { + mut out := map[string]bool{} + for sym in l.elf.symbols { + if sym.shndx == u16(l.elf_section_index(.rodata)) { + out[sym.name] = true + } + } + return out +} + +fn (l ElfTinyLinker) copy_data_ranges(ranges []ElfDataRange, section ObjectSection, mut out []u8, mut offsets map[string]u64) ! { + for dr in ranges { + if dr.section != section { + continue + } + align := int(l.data_section_alignment(section)) + for align > 1 && out.len % align != int(dr.start % u64(align)) { + out << u8(0) + } + off := u64(out.len) + for sym in l.elf.symbols { + if sym.shndx == u16(l.elf_section_index(section)) && sym.value == dr.start { + offsets[sym.name] = off + } + } + match section { + .rodata { + out << l.elf.rodata[int(dr.start)..int(dr.end)] + } + .data { + out << l.elf.data_data[int(dr.start)..int(dr.end)] + } + else { + return error('Linux tiny executable cannot copy ${section} as data') + } + } + } +} + +fn (l ElfTinyLinker) data_section_alignment(section ObjectSection) u64 { + return match section { + .data { u64(8) } + .rodata { u64(4) } + else { u64(1) } + } +} + +fn elf_tiny_text_file_offset(phnum int) int { + return 64 + phnum * 56 +} + +fn elf_tiny_data_file_offset(text_off int, text_len int, rodata_len int) int { + return elf_tiny_align(text_off + text_len + rodata_len, linux_tiny_page_align) +} + +fn elf_tiny_align(v int, align int) int { + if align <= 1 || v % align == 0 { + return v + } + return v + (align - (v % align)) +} + +fn elf_tiny_patch_rel32(mut text []u8, field_off int, source_base_vaddr u64, source_off u64, target_vaddr u64) { + field_vaddr := source_base_vaddr + source_off + u64(field_off) + disp := i64(target_vaddr) - i64(field_vaddr + 4) + binary.little_endian_put_u32(mut text[field_off..field_off + 4], u32(i32(disp))) +} + +fn linux_tiny_not_eligible(message string) IError { + return error(linux_tiny_not_eligible_prefix + message) +} diff --git a/vlib/v2/gen/x64/macho_tiny.v b/vlib/v2/gen/x64/macho_tiny.v new file mode 100644 index 000000000..e0ecfe279 --- /dev/null +++ b/vlib/v2/gen/x64/macho_tiny.v @@ -0,0 +1,302 @@ +// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. + +module x64 + +pub const macos_tiny_not_eligible_prefix = 'macOS tiny object is not eligible: ' + +struct MachOTextRange { + name string + start u64 + end u64 +} + +struct MachODataRange { + section ObjectSection + start u64 + end u64 +} + +struct MachOTinyReachable { + names []string + data []MachODataRange + undefined map[string]bool +} + +struct MachOTinyObjectWriter { + macho &MachOObject +} + +pub fn (mut g Gen) write_macos_tiny_object(path string) ! { + if g.obj_format != .macho { + return error('macOS tiny object writing requires Mach-O object output') + } + if g.abi != .sysv { + return error('macOS tiny object writing requires SysV x64 code generation') + } + mut writer := MachOTinyObjectWriter{ + macho: g.macho + } + writer.write(path)! +} + +fn (mut w MachOTinyObjectWriter) write(path string) ! { + reachable := w.collect_reachable()! + mut out := MachOObject.new() + mut symbol_indices := map[string]int{} + mut func_offsets := map[string]u64{} + + for name in reachable.names { + range := w.text_range(name)! + func_offsets[name] = u64(out.text_data.len) + out.text_data << w.macho.text_data[int(range.start)..int(range.end)] + } + + mut data_offsets := map[string]u64{} + w.copy_data_ranges(reachable.data, .rodata, mut out.rodata, mut data_offsets)! + w.copy_data_ranges(reachable.data, .data, mut out.data_data, mut data_offsets)! + + for name in reachable.names { + symbol_indices[name] = out.add_symbol(name, func_offsets[name], + macho_tiny_defined_symbol_is_external(name), ObjectFormat.macho.section_index(.text)) + } + mut data_names := data_offsets.keys() + data_names.sort() + for name in data_names { + orig := w.symbol_by_name(name)! + section := w.symbol_object_section(orig)! + symbol_indices[name] = out.add_symbol(name, data_offsets[name], + macho_tiny_defined_symbol_is_external(name), ObjectFormat.macho.section_index(section)) + } + mut undefined_names := reachable.undefined.keys() + undefined_names.sort() + for name in undefined_names { + symbol_indices[name] = out.add_undefined(name) + } + + for name in reachable.names { + range := w.text_range(name)! + new_base := func_offsets[name] + for reloc in w.macho.relocs { + if reloc.addr < int(range.start) || reloc.addr >= int(range.end) { + continue + } + sym := w.reloc_symbol(reloc)! + sym_idx := symbol_indices[sym.name] or { + return macos_tiny_not_eligible('symbol `${sym.name}` is not resolved by the tiny object') + } + out.add_reloc(int(new_base + u64(reloc.addr - int(range.start))), sym_idx, reloc.type_, + reloc.pcrel, reloc.length) + } + } + + out.write(path) +} + +fn (mut w MachOTinyObjectWriter) collect_reachable() !MachOTinyReachable { + _ = w.text_range('_main') or { return macos_tiny_not_eligible('missing _main symbol') } + mut selected := []string{} + mut seen := map[string]bool{} + mut queue := ['_main'] + mut selected_data := []MachODataRange{} + mut undefined := map[string]bool{} + for queue.len > 0 { + name := queue[0] + queue.delete(0) + if seen[name] { + continue + } + if macho_tiny_is_module_init_symbol(name) { + return macos_tiny_not_eligible('module init symbol `${name}` is not supported') + } + range := w.text_range(name)! + seen[name] = true + selected << name + for reloc in w.macho.relocs { + if reloc.addr < int(range.start) || reloc.addr >= int(range.end) { + continue + } + validate_macho_tiny_relocation(reloc)! + sym := w.reloc_symbol(reloc)! + if sym.sect == ObjectFormat.macho.section_index(.text) { + if !seen[sym.name] { + queue << sym.name + } + } else if sym.sect == ObjectFormat.macho.section_index(.rodata) { + w.add_data_range(mut selected_data, .rodata, sym.value)! + } else if sym.sect == ObjectFormat.macho.section_index(.data) { + w.add_data_range(mut selected_data, .data, sym.value)! + } else if sym.sect == 0 && sym.type_ == 0x01 { + undefined[sym.name] = true + } else { + return macos_tiny_not_eligible('relocation to symbol `${sym.name}` in Mach-O section ${sym.sect} is not supported') + } + } + } + return MachOTinyReachable{ + names: selected + data: selected_data + undefined: undefined + } +} + +fn validate_macho_tiny_relocation(reloc MachORelocationInfo) ! { + if !reloc.pcrel || reloc.length != 2 { + return macos_tiny_not_eligible('non-pcrel or non-32-bit relocation at __text+${reloc.addr} is not supported') + } + if reloc.type_ !in [x86_64_reloc_signed, x86_64_reloc_branch, x86_64_reloc_got_load, + x86_64_reloc_got] { + return macos_tiny_not_eligible('Mach-O relocation type ${reloc.type_} is not supported') + } +} + +fn (w MachOTinyObjectWriter) text_range(name string) !MachOTextRange { + ranges := w.text_symbol_ranges() + for range in ranges { + if range.name == name { + return range + } + } + return error('missing text symbol `${name}`') +} + +fn (w MachOTinyObjectWriter) text_symbol_ranges() []MachOTextRange { + mut syms := []MachOSymbol{} + for sym in w.macho.symbols { + if sym.name != '' && sym.sect == ObjectFormat.macho.section_index(.text) { + syms << sym + } + } + syms.sort(a.value < b.value) + mut ranges := []MachOTextRange{} + for i, sym in syms { + mut end := u64(w.macho.text_data.len) + for j := i + 1; j < syms.len; j++ { + if syms[j].value > sym.value { + end = syms[j].value + break + } + } + if end > sym.value { + ranges << MachOTextRange{ + name: sym.name + start: sym.value + end: end + } + } + } + return ranges +} + +fn (w MachOTinyObjectWriter) reloc_symbol(reloc MachORelocationInfo) !MachOSymbol { + if reloc.sym_idx < 0 || reloc.sym_idx >= w.macho.symbols.len { + return error('macOS tiny object relocation references invalid symbol index ${reloc.sym_idx}') + } + return w.macho.symbols[reloc.sym_idx] +} + +fn (w MachOTinyObjectWriter) symbol_by_name(name string) !MachOSymbol { + idx := w.macho.sym_by_name[name] or { return error('missing symbol `${name}`') } + if idx < 0 || idx >= w.macho.symbols.len { + return error('invalid symbol index ${idx} for `${name}`') + } + return w.macho.symbols[idx] +} + +fn (w MachOTinyObjectWriter) symbol_object_section(sym MachOSymbol) !ObjectSection { + if sym.sect == ObjectFormat.macho.section_index(.rodata) { + return .rodata + } + if sym.sect == ObjectFormat.macho.section_index(.data) { + return .data + } + if sym.sect == ObjectFormat.macho.section_index(.text) { + return .text + } + return error('symbol `${sym.name}` is not in a known Mach-O section') +} + +fn (mut w MachOTinyObjectWriter) add_data_range(mut ranges []MachODataRange, section ObjectSection, start u64) ! { + end := w.data_symbol_range_end(section, start)! + for range in ranges { + if range.section == section && range.start == start && range.end == end { + return + } + } + ranges << MachODataRange{ + section: section + start: start + end: end + } +} + +fn (w MachOTinyObjectWriter) data_symbol_range_end(section ObjectSection, start u64) !u64 { + section_idx := ObjectFormat.macho.section_index(section) + section_len := match section { + .rodata { u64(w.macho.rodata.len) } + .data { u64(w.macho.data_data.len) } + else { u64(0) } + } + + mut end := section_len + for sym in w.macho.symbols { + if sym.sect == section_idx && sym.value > start && sym.value < end { + end = sym.value + } + } + if end < start { + return error('invalid Mach-O data symbol range') + } + return end +} + +fn (w MachOTinyObjectWriter) copy_data_ranges(ranges []MachODataRange, section ObjectSection, mut out []u8, mut offsets map[string]u64) ! { + for dr in ranges { + if dr.section != section { + continue + } + align := int(w.data_section_alignment(section)) + for align > 1 && out.len % align != int(dr.start % u64(align)) { + out << u8(0) + } + off := u64(out.len) + section_idx := ObjectFormat.macho.section_index(section) + for sym in w.macho.symbols { + if sym.sect == section_idx && sym.value == dr.start { + offsets[sym.name] = off + } + } + match section { + .rodata { + out << w.macho.rodata[int(dr.start)..int(dr.end)] + } + .data { + out << w.macho.data_data[int(dr.start)..int(dr.end)] + } + else { + return error('macOS tiny object cannot copy ${section} as data') + } + } + } +} + +fn (w MachOTinyObjectWriter) data_section_alignment(section ObjectSection) u64 { + return match section { + .data { u64(8) } + .rodata { u64(4) } + else { u64(1) } + } +} + +fn macho_tiny_defined_symbol_is_external(name string) bool { + return name == '_main' +} + +fn macho_tiny_is_module_init_symbol(name string) bool { + return name.ends_with('__init') && name != '_main' +} + +fn macos_tiny_not_eligible(message string) IError { + return error(macos_tiny_not_eligible_prefix + message) +} diff --git a/vlib/v2/gen/x64/pe.v b/vlib/v2/gen/x64/pe.v index e36f5f052..8a6a2ceb1 100644 --- a/vlib/v2/gen/x64/pe.v +++ b/vlib/v2/gen/x64/pe.v @@ -102,18 +102,11 @@ mut: } pub fn PeLinker.new(coff &CoffObject) &PeLinker { - mut linker := unsafe { + return unsafe { &PeLinker{ coff: coff } } - for name in pe_kernel32_imports { - linker.imports << PeImport{ - dll: 'kernel32.dll' - name: name - } - } - return linker } pub fn (mut l PeLinker) write(path string) ! { @@ -123,7 +116,9 @@ pub fn (mut l PeLinker) write(path string) ! { } pub fn (mut l PeLinker) image() ![]u8 { - runtime_text := l.build_runtime_text() + mut runtime_text := l.build_runtime_text() + l.imports = l.required_kernel32_imports(runtime_text)! + l.patch_runtime_import_calls(mut runtime_text)! mut text := l.build_text_section(runtime_text)! mut data_data := l.coff.data_data.clone() runtime_data_base := if runtime_text.data.len > 0 { @@ -267,7 +262,10 @@ fn (l PeLinker) build_text_section(runtime_text PeRuntimeText) ![]u8 { pe_patch_rel32(mut text, cursor + 1, u32(entry_stub_len) + main_rva, 0) cursor += 5 cursor += 2 - exit_thunk_off := u32(l.import_thunk_base(runtime_text.bytes.len)) + import_offsets := l.import_thunk_offsets(runtime_text.bytes.len) + exit_thunk_off := import_offsets['ExitProcess'] or { + return error('PE linker internal error: missing ExitProcess import thunk') + } pe_patch_rel32(mut text, cursor + 1, exit_thunk_off, 0) return text @@ -349,14 +347,68 @@ fn (l PeLinker) build_runtime_text() PeRuntimeText { rt.symbols['builtin__new_array_from_c_array_noscan'] = runtime_base + u32(rt.bytes.len) pe_emit_runtime_new_array_from_c_array_noscan(mut rt) } + return rt +} + +fn (l PeLinker) required_kernel32_imports(runtime_text PeRuntimeText) ![]PeImport { + mut required := map[string]bool{} + pe_require_kernel32_import(mut required, 'ExitProcess')! + for patch in runtime_text.import_patches { + pe_require_kernel32_import(mut required, patch.import_name)! + } + for reloc in l.coff.text_relocs { + if reloc.sym_idx < 0 || reloc.sym_idx >= l.coff.symbols.len { + continue + } + sym := l.coff.symbols[reloc.sym_idx] + if sym.section != 0 { + continue + } + if _ := runtime_text.symbols[sym.name] { + continue + } + if pe_kernel32_import_is_known(sym.name) { + required[sym.name] = true + } + } + + mut imports := []PeImport{cap: required.len} + for name in pe_kernel32_imports { + if required[name] { + imports << PeImport{ + dll: 'kernel32.dll' + name: name + } + } + } + return imports +} + +fn (l PeLinker) patch_runtime_import_calls(mut rt PeRuntimeText) ! { + runtime_base := u32(l.entry_stub_size() + l.coff.text_data.len) import_offsets := l.import_thunk_offsets(rt.bytes.len) for patch in rt.import_patches { target := import_offsets[patch.import_name] or { - panic('PE linker internal error: missing import thunk for `${patch.import_name}`') + return error('PE linker internal error: missing import thunk for `${patch.import_name}`') } pe_patch_rel32(mut rt.bytes, patch.field_off, target, runtime_base) } - return rt +} + +fn pe_require_kernel32_import(mut required map[string]bool, name string) ! { + if !pe_kernel32_import_is_known(name) { + return error('PE linker internal error: unknown Kernel32 import `${name}`') + } + required[name] = true +} + +fn pe_kernel32_import_is_known(name string) bool { + for known in pe_kernel32_imports { + if known == name { + return true + } + } + return false } fn (rt PeRuntimeText) symbol_rvas(text_rva u32) map[string]u32 { diff --git a/vlib/v2/gen/x64/x64.v b/vlib/v2/gen/x64/x64.v index 94f632091..7cf700352 100644 --- a/vlib/v2/gen/x64/x64.v +++ b/vlib/v2/gen/x64/x64.v @@ -46,6 +46,11 @@ mut: has_call bool } +struct ReferencedWindowsCoffGlobals { + complete bool + indexes map[int]bool +} + pub fn Gen.new(mod &mir.Module) &Gen { return &Gen{ mod: mod @@ -84,10 +89,14 @@ pub fn (mut g Gen) gen() { } // Generate Globals in .data - for gvar in g.mod.globals { + referenced_windows_globals := g.referenced_windows_coff_globals() + for global_index, gvar in g.mod.globals { if gvar.linkage == .external { continue } + if g.omit_unreferenced_windows_coff_global(global_index, referenced_windows_globals) { + continue + } for g.data_len() % 8 != 0 { g.add_data_byte(0) } @@ -116,6 +125,63 @@ pub fn (mut g Gen) gen() { } } +fn (g Gen) referenced_windows_coff_globals() ReferencedWindowsCoffGlobals { + mut referenced := map[int]bool{} + if g.obj_format != .coff || g.abi != .windows { + return ReferencedWindowsCoffGlobals{} + } + for func in g.mod.funcs { + if func.is_c_extern || func.blocks.len == 0 { + continue + } + for block_id in func.blocks { + if block_id < 0 || block_id >= g.mod.blocks.len { + return ReferencedWindowsCoffGlobals{} + } + block := g.mod.blocks[block_id] + for val_id in block.instrs { + if val_id < 0 || val_id >= g.mod.values.len { + return ReferencedWindowsCoffGlobals{} + } + val := g.mod.values[val_id] + if val.kind != .instruction || val.index < 0 || val.index >= g.mod.instrs.len { + return ReferencedWindowsCoffGlobals{} + } + instr := g.mod.instrs[val.index] + for operand_id in instr.operands { + if operand_id < 0 || operand_id >= g.mod.values.len { + return ReferencedWindowsCoffGlobals{} + } + operand := g.mod.values[operand_id] + if operand.kind != .global { + continue + } + if operand.index < 0 || operand.index >= g.mod.globals.len { + return ReferencedWindowsCoffGlobals{} + } + if g.mod.globals[operand.index].linkage != .external { + referenced[operand.index] = true + } + } + } + } + } + return ReferencedWindowsCoffGlobals{ + complete: true + indexes: referenced + } +} + +fn (g Gen) omit_unreferenced_windows_coff_global(index int, referenced ReferencedWindowsCoffGlobals) bool { + if g.obj_format != .coff || g.abi != .windows { + return false + } + if !referenced.complete { + return false + } + return !referenced.indexes[index] +} + fn (g Gen) scalar_global_initial_value_supported(typ_id ssa.TypeID) bool { if typ_id <= 0 || typ_id >= g.mod.type_store.types.len { return false diff --git a/vlib/v2/gen/x64/x64_object_format_test.v b/vlib/v2/gen/x64/x64_object_format_test.v index daabebf24..cbed706d4 100644 --- a/vlib/v2/gen/x64/x64_object_format_test.v +++ b/vlib/v2/gen/x64/x64_object_format_test.v @@ -59,6 +59,85 @@ fn new_macho_global_codegen_test_module(name string, linkage ssa.Linkage) mir.Mo } } +fn new_windows_coff_global_codegen_test_module() mir.Module { + mut ts := ssa.TypeStore.new() + i64_t := ts.get_int(64) + ptr_i64_t := ts.get_ptr(i64_t) + return mir.Module{ + type_store: unsafe { *ts } + values: [ + mir.Value{ + id: 0 + typ: ptr_i64_t + index: 0 + kind: .global + name: 'keep_global' + }, + mir.Value{ + id: 1 + typ: ptr_i64_t + index: 1 + kind: .global + name: 'drop_global' + }, + mir.Value{ + id: 2 + typ: ptr_i64_t + index: 0 + kind: .instruction + }, + ] + instrs: [ + mir.Instruction{ + op: .ret + operands: [0] + typ: ptr_i64_t + block: 0 + }, + ] + blocks: [ + mir.BasicBlock{ + id: 0 + val_id: 2 + name: 'entry' + parent: 0 + instrs: [2] + }, + ] + funcs: [ + mir.Function{ + id: 0 + name: 'main' + typ: ptr_i64_t + blocks: [0] + }, + ] + globals: [ + ssa.GlobalVar{ + name: 'keep_global' + typ: i64_t + linkage: .private + initial_data: [u8(1), 2, 3, 4, 5, 6, 7, 8] + }, + ssa.GlobalVar{ + name: 'drop_global' + typ: i64_t + linkage: .private + initial_data: [u8(9), 10, 11, 12, 13, 14, 15, 16] + }, + ] + } +} + +fn coff_test_symbol(obj &CoffObject, name string) ?CoffSymbol { + for sym in obj.symbols { + if sym.name == name { + return sym + } + } + return none +} + struct TestByteRange { label string start u64 @@ -152,6 +231,23 @@ struct MachOTestSection { flags u32 } +struct MachOTestSymbol { + name string + type_ u8 + sect u8 + desc u16 + value u64 +} + +struct MachOTestRelocation { + addr u32 + sym_idx int + pcrel bool + length int + extern bool + type_ int +} + fn macho_test_load_commands(data []u8) []int { ncmds := int(read_u32_le(data, 16)) mut off := 32 @@ -196,6 +292,65 @@ fn macho_test_section_ranges(sections []MachOTestSection) []TestByteRange { return ranges } +fn macho_test_symbols(data []u8) []MachOTestSymbol { + load_cmds := macho_test_load_commands(data) + symtab_cmd_off := load_cmds[1] + sym_off := int(read_u32_le(data, symtab_cmd_off + 8)) + nsyms := int(read_u32_le(data, symtab_cmd_off + 12)) + str_off := int(read_u32_le(data, symtab_cmd_off + 16)) + mut symbols := []MachOTestSymbol{cap: nsyms} + for i in 0 .. nsyms { + off := sym_off + i * 16 + name_off := int(read_u32_le(data, off)) + symbols << MachOTestSymbol{ + name: read_string(data, str_off + name_off) + type_: data[off + 4] + sect: data[off + 5] + desc: read_u16_le(data, off + 6) + value: read_u64_le(data, off + 8) + } + } + return symbols +} + +fn macho_test_symbol_names(data []u8) []string { + symbols := macho_test_symbols(data) + mut names := []string{cap: symbols.len} + for symbol in symbols { + names << symbol.name + } + return names +} + +fn macho_test_symbol_by_name(symbols []MachOTestSymbol, name string) ?MachOTestSymbol { + for symbol in symbols { + if symbol.name == name { + return symbol + } + } + return none +} + +fn macho_test_text_relocations(data []u8) []MachOTestRelocation { + load_cmds := macho_test_load_commands(data) + sections := macho_test_sections(data, load_cmds[0]) + text_section := sections[0] + mut relocs := []MachOTestRelocation{cap: int(text_section.nreloc)} + for i in 0 .. int(text_section.nreloc) { + off := int(text_section.reloff) + i * 8 + info := read_u32_le(data, off + 4) + relocs << MachOTestRelocation{ + addr: read_u32_le(data, off) + sym_idx: int(info & 0x00ff_ffff) + pcrel: ((info >> 24) & 1) == 1 + length: int((info >> 25) & 3) + extern: ((info >> 27) & 1) == 1 + type_: int(info >> 28) + } + } + return relocs +} + struct CoffTestSection { name string virtual_size u32 @@ -551,6 +706,120 @@ fn test_elf_writer_emits_x64_relocatable_object() { 58)) * u64(read_u16_le(data, 60)))) } +fn test_linux_tiny_elf_writer_emits_exec_program_headers_without_sections() { + $if linux { + path := os.join_path(os.vtmp_dir(), 'v2_x64_tiny_elf_${os.getpid()}') + defer { + os.rm(path) or {} + } + + mut obj := ElfObject.new() + obj.text_data << [u8(0x31), 0xc0, 0xc3] // xor eax, eax; ret + obj.add_symbol('main', 0, true, 1) + mut linker := ElfTinyLinker{ + elf: obj + } + linker.write(path) or { panic(err) } + + data := os.read_bytes(path) or { panic(err) } + assert data.len > elf_tiny_text_file_offset(1) + assert data[0] == 0x7f + assert data[1] == `E` + assert data[2] == `L` + assert data[3] == `F` + assert read_u16_le(data, 16) == et_exec + assert read_u16_le(data, 18) == em_x86_64 + assert read_u64_le(data, 24) == linux_tiny_base_vaddr + u64(elf_tiny_text_file_offset(1)) + assert read_u64_le(data, 32) == 64 + assert read_u64_le(data, 40) == 0 + assert read_u16_le(data, 54) == 56 + assert read_u16_le(data, 56) == 1 + assert read_u16_le(data, 58) == 0 + assert read_u16_le(data, 60) == 0 + assert read_u32_le(data, 64) == pt_load + assert read_u32_le(data, 68) == pf_r | pf_x + } +} + +fn test_linux_tiny_elf_writer_keeps_runtime_metadata_wx_clean() { + $if linux { + path := os.join_path(os.vtmp_dir(), 'v2_x64_tiny_elf_runtime_${os.getpid()}') + defer { + os.rm(path) or {} + } + + mut obj := ElfObject.new() + obj.text_data << [u8(0xbf), 23, 0, 0, 0, 0xe8, 0, 0, 0, 0, 0xc3] + int_str_sym := obj.add_undefined('builtin__int__str') + obj.add_symbol('main', 0, true, 1) + obj.add_text_reloc(6, int_str_sym, r_x86_64_plt32, -4) + mut linker := ElfTinyLinker{ + elf: obj + } + linker.write(path) or { panic(err) } + + data := os.read_bytes(path) or { panic(err) } + assert data.len > elf_tiny_text_file_offset(2) + assert data.len < linux_tiny_page_align + assert read_u16_le(data, 16) == et_exec + assert read_u16_le(data, 56) == 2 + assert read_u16_le(data, 58) == 0 + assert read_u16_le(data, 60) == 0 + + text_ph := 64 + assert read_u32_le(data, text_ph) == pt_load + assert read_u32_le(data, text_ph + 4) == pf_r | pf_x + assert read_u64_le(data, text_ph + 8) == 0 + assert read_u64_le(data, text_ph + 16) == linux_tiny_base_vaddr + text_filesz := read_u64_le(data, text_ph + 32) + assert text_filesz == read_u64_le(data, text_ph + 40) + assert read_u64_le(data, text_ph + 48) == u64(linux_tiny_page_align) + + rw_ph := text_ph + 56 + assert read_u32_le(data, rw_ph) == pt_load + assert read_u32_le(data, rw_ph + 4) == pf_r | pf_w + rw_offset := read_u64_le(data, rw_ph + 8) + rw_vaddr := read_u64_le(data, rw_ph + 16) + rw_align := read_u64_le(data, rw_ph + 48) + assert read_u64_le(data, rw_ph + 32) == 0 + assert read_u64_le(data, rw_ph + 40) == linux_tiny_int_str_arena_metadata_bytes + assert rw_align == u64(linux_tiny_page_align) + assert rw_vaddr % rw_align == rw_offset % rw_align + assert (read_u32_le(data, text_ph + 4) & u32(pf_w | pf_x)) != u32(pf_w | pf_x) + assert (read_u32_le(data, rw_ph + 4) & u32(pf_w | pf_x)) != u32(pf_w | pf_x) + } +} + +fn test_linux_tiny_write_runtime_exits_on_non_positive_syscall_result() { + $if linux { + mut obj := ElfObject.new() + mut linker := ElfTinyLinker{ + elf: obj + } + rt := linker.build_runtime({ + 'write': true + }) + write_off := int(rt.symbols['write'] or { panic('missing write runtime symbol') }) + write_bytes := rt.text[write_off..] + mut has_exit_group_failure := false + mut has_old_partial_success_return := false + for i in 0 .. write_bytes.len { + if i + 10 <= write_bytes.len && write_bytes[i] == 0xbf && write_bytes[i + 1] == 0x01 + && write_bytes[i + 2] == 0 && write_bytes[i + 3] == 0 && write_bytes[i + 4] == 0 + && write_bytes[i + 5] == 0xb8 + && read_u32_le(write_bytes, i + 6) == linux_sys_exit_group { + has_exit_group_failure = true + } + if i + 4 <= write_bytes.len && write_bytes[i] == 0x49 && write_bytes[i + 1] == 0x0f + && write_bytes[i + 2] == 0x45 && write_bytes[i + 3] == 0xc2 { + has_old_partial_success_return = true + } + } + assert has_exit_group_failure + assert !has_old_partial_success_return + } +} + fn test_elf_writer_serializes_sections_symbols_and_relocations() { path := temp_object_path('elf_relocs') defer { @@ -975,6 +1244,87 @@ fn test_macho_codegen_keeps_private_global_references_signed() { assert gen.macho.symbols[gen.macho.relocs[0].sym_idx].name == '_app_global' } +fn test_windows_coff_codegen_omits_unused_global_data() { + mut mod := new_windows_coff_global_codegen_test_module() + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + gen.gen() + + assert gen.coff.data_data == [u8(1), 2, 3, 4, 5, 6, 7, 8] + keep_sym := coff_test_symbol(gen.coff, 'keep_global') or { + panic('missing referenced global symbol') + } + assert keep_sym.section == 3 + assert keep_sym.value == 0 + assert coff_test_symbol(gen.coff, 'drop_global') == none + assert gen.coff.text_relocs.len == 1 + reloc := gen.coff.text_relocs[0] + assert gen.coff.symbols[reloc.sym_idx].name == 'keep_global' + assert gen.coff.symbols[reloc.sym_idx].section == 3 +} + +fn test_windows_coff_codegen_keeps_globals_when_reference_scan_is_ambiguous() { + mut mod := new_windows_coff_global_codegen_test_module() + ghost_id := mod.values.len + mod.values << mir.Value{ + id: ghost_id + typ: mod.values[0].typ + index: 99 + kind: .global + name: 'ghost_global' + } + mod.instrs[0].operands = [ghost_id] + mut gen := Gen.new_with_format_and_abi(&mod, .coff, .windows) + gen.gen() + + assert gen.coff.data_data == [ + u8(1), + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + ] + assert coff_test_symbol(gen.coff, 'keep_global') != none + assert coff_test_symbol(gen.coff, 'drop_global') != none +} + +fn test_non_windows_coff_codegen_preserves_existing_global_emission() { + mut mod := new_windows_coff_global_codegen_test_module() + mut gen := Gen.new_with_format(&mod, .coff) + gen.gen() + + assert gen.coff.data_data == [ + u8(1), + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + ] + assert coff_test_symbol(gen.coff, 'keep_global') != none + assert coff_test_symbol(gen.coff, 'drop_global') != none +} + fn test_macho_codegen_keeps_local_rodata_references_signed() { mut ts := ssa.TypeStore.new() i64_t := ts.get_int(64) @@ -1148,6 +1498,403 @@ fn test_macho_writer_serializes_text_relocations_and_symbols() { assert data[sym_off + 16 + 5] == 2 } +fn test_macho_tiny_object_writer_prunes_unreachable_text_and_data() { + path := temp_object_path('macho_tiny_pruned') + defer { + os.rm(path) or {} + } + + mut obj := MachOObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0x48, 0x8d, 0x05, 0, 0, 0, 0, 0xc3] + main_sym := obj.add_symbol('_main', 0, true, 1) + helper_start := obj.text_data.len + obj.text_data << u8(0xc3) + helper_sym := obj.add_symbol('_helper', u64(helper_start), true, 1) + unused_start := obj.text_data.len + obj.text_data << u8(0xc3) + obj.add_symbol('_unused', u64(unused_start), true, 1) + keep_sym := obj.add_symbol('L_keep', 0, false, 2) + obj.rodata << 'ok'.bytes() + obj.rodata << 0 + drop_start := obj.rodata.len + obj.add_symbol('L_drop', u64(drop_start), false, 2) + obj.rodata << 'drop'.bytes() + obj.rodata << 0 + obj.add_reloc(1, helper_sym, x86_64_reloc_branch, true, 2) + obj.add_reloc(8, keep_sym, x86_64_reloc_signed, true, 2) + + mut writer := MachOTinyObjectWriter{ + macho: obj + } + writer.write(path) or { panic(err) } + + data := os.read_bytes(path) or { panic(err) } + load_cmds := macho_test_load_commands(data) + sections := macho_test_sections(data, load_cmds[0]) + assert sections[0].size == u64(helper_start + 1) + assert sections[1].size == u64(3) + assert data[int(sections[0].offset)..int(sections[0].offset + sections[0].size)] == obj.text_data[.. + helper_start + 1] + assert data[int(sections[1].offset)..int(sections[1].offset + sections[1].size)] == 'ok\0'.bytes() + assert sections[0].nreloc == 2 + + symbols := macho_test_symbols(data) + names := symbols.map(it.name) + assert '_main' in names + assert '_helper' in names + assert 'L_keep' in names + assert '_unused' !in names + assert 'L_drop' !in names + main_out := macho_test_symbol_by_name(symbols, '_main') or { + assert false, 'missing _main in Mach-O tiny output' + return + } + helper_out := macho_test_symbol_by_name(symbols, '_helper') or { + assert false, 'missing _helper in Mach-O tiny output' + return + } + keep_out := macho_test_symbol_by_name(symbols, 'L_keep') or { + assert false, 'missing L_keep in Mach-O tiny output' + return + } + assert main_out.type_ == 0x0f + assert main_out.sect == 1 + assert helper_out.type_ == 0x0e + assert helper_out.sect == 1 + assert keep_out.type_ == 0x0e + assert keep_out.sect == 2 + assert main_sym == 0 +} + +fn test_macho_tiny_object_writer_preserves_undefined_externals_and_remaps_relocations() { + path := temp_object_path('macho_tiny_undefined_remap') + defer { + os.rm(path) or {} + } + + mut obj := MachOObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0x48, 0x8b, 0x05, 0, 0, 0, 0, 0x48, 0x8d, 0x05, 0, + 0, 0, 0, 0xc3] + unused_start := obj.text_data.len + obj.text_data << u8(0xc3) + obj.add_symbol('_unused', u64(unused_start), true, 1) + obj.add_undefined('_unused_external') + call_sym := obj.add_undefined('_calloc') + obj.add_symbol('_main', 0, true, 1) + got_load_sym := obj.add_undefined('___stderrp') + got_sym := obj.add_undefined('___error') + obj.add_reloc(1, call_sym, x86_64_reloc_branch, true, 2) + obj.add_reloc(8, got_load_sym, x86_64_reloc_got_load, true, 2) + obj.add_reloc(15, got_sym, x86_64_reloc_got, true, 2) + + mut writer := MachOTinyObjectWriter{ + macho: obj + } + writer.write(path) or { panic(err) } + + data := os.read_bytes(path) or { panic(err) } + symbols := macho_test_symbols(data) + names := symbols.map(it.name) + assert '_main' in names + assert '_calloc' in names + assert '___stderrp' in names + assert '___error' in names + assert '_unused' !in names + assert '_unused_external' !in names + for name in ['_calloc', '___stderrp', '___error'] { + symbol := macho_test_symbol_by_name(symbols, name) or { + assert false, 'missing ${name} in Mach-O tiny output' + return + } + assert symbol.type_ == 0x01 + assert symbol.sect == 0 + assert symbol.value == 0 + } + + relocs := macho_test_text_relocations(data) + assert relocs.len == 3 + assert relocs[0].addr == 1 + assert relocs[0].type_ == x86_64_reloc_branch + assert symbols[relocs[0].sym_idx].name == '_calloc' + assert relocs[0].sym_idx != call_sym + assert relocs[1].addr == 8 + assert relocs[1].type_ == x86_64_reloc_got_load + assert symbols[relocs[1].sym_idx].name == '___stderrp' + assert relocs[1].sym_idx != got_load_sym + assert relocs[2].addr == 15 + assert relocs[2].type_ == x86_64_reloc_got + assert symbols[relocs[2].sym_idx].name == '___error' + assert relocs[2].sym_idx != got_sym + for reloc in relocs { + assert reloc.pcrel + assert reloc.length == 2 + assert reloc.extern + } +} + +fn test_macho_tiny_object_writer_keeps_referenced_rodata_and_data() { + path := temp_object_path('macho_tiny_rodata_data') + defer { + os.rm(path) or {} + } + + mut obj := MachOObject.new() + obj.text_data << [u8(0x48), 0x8d, 0x05, 0, 0, 0, 0, 0x48, 0x8d, 0x05, 0, 0, 0, 0, 0xc3] + obj.add_symbol('_main', 0, true, 1) + ro_keep_sym := obj.add_symbol('L_ro_keep', 0, false, 2) + obj.rodata << 'ro'.bytes() + obj.rodata << 0 + ro_drop_start := obj.rodata.len + obj.add_symbol('L_ro_drop', u64(ro_drop_start), false, 2) + obj.rodata << 'drop'.bytes() + obj.rodata << 0 + data_keep_sym := obj.add_symbol('_data_keep', 0, true, 3) + obj.data_data << [u8(1), 2, 3, 4, 5, 6, 7, 8] + data_drop_start := obj.data_data.len + obj.add_symbol('_data_drop', u64(data_drop_start), true, 3) + obj.data_data << [u8(9), 10, 11, 12] + obj.add_reloc(3, ro_keep_sym, x86_64_reloc_signed, true, 2) + obj.add_reloc(10, data_keep_sym, x86_64_reloc_signed, true, 2) + + mut writer := MachOTinyObjectWriter{ + macho: obj + } + writer.write(path) or { panic(err) } + + data := os.read_bytes(path) or { panic(err) } + load_cmds := macho_test_load_commands(data) + sections := macho_test_sections(data, load_cmds[0]) + assert sections[1].size == u64(3) + assert sections[2].size == u64(8) + assert data[int(sections[1].offset)..int(sections[1].offset + sections[1].size)] == 'ro\0'.bytes() + assert data[int(sections[2].offset)..int(sections[2].offset + sections[2].size)] == [ + u8(1), + 2, + 3, + 4, + 5, + 6, + 7, + 8, + ] + + names := macho_test_symbol_names(data) + assert 'L_ro_keep' in names + assert '_data_keep' in names + assert 'L_ro_drop' !in names + assert '_data_drop' !in names + relocs := macho_test_text_relocations(data) + symbols := macho_test_symbols(data) + ro_keep_out := macho_test_symbol_by_name(symbols, 'L_ro_keep') or { + assert false, 'missing L_ro_keep in Mach-O tiny output' + return + } + data_keep_out := macho_test_symbol_by_name(symbols, '_data_keep') or { + assert false, 'missing _data_keep in Mach-O tiny output' + return + } + assert ro_keep_out.type_ == 0x0e + assert ro_keep_out.sect == 2 + assert data_keep_out.type_ == 0x0e + assert data_keep_out.sect == 3 + assert relocs.len == 2 + assert symbols[relocs[0].sym_idx].name == 'L_ro_keep' + assert symbols[relocs[1].sym_idx].name == '_data_keep' +} + +fn test_macho_tiny_object_writer_remaps_multiple_aligned_data_ranges() { + path := temp_object_path('macho_tiny_aligned_data_ranges') + defer { + os.rm(path) or {} + } + + ro_a := [u8(0x01), 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + ro_b := [u8(0x11), 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18] + data_a := [u8(0x21), 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28] + data_b := [u8(0x31), 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38] + + mut obj := MachOObject.new() + obj.text_data << [u8(0x48), 0x8d, 0x05, 0, 0, 0, 0, 0x48, 0x8d, 0x05, 0, 0, 0, 0, 0x48, 0x8d, + 0x05, 0, 0, 0, 0, 0x48, 0x8d, 0x05, 0, 0, 0, 0, 0xc3] + obj.add_symbol('_main', 0, true, 1) + ro_a_sym := obj.add_symbol('L_ro64_a', 0, false, 2) + obj.rodata << ro_a + ro_b_sym := obj.add_symbol('L_ro64_b', u64(obj.rodata.len), false, 2) + obj.rodata << ro_b + obj.add_symbol('L_ro64_unused', u64(obj.rodata.len), false, 2) + obj.rodata << [u8(0xf1), 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8] + data_a_sym := obj.add_symbol('_data64_a', 0, true, 3) + obj.data_data << data_a + data_b_sym := obj.add_symbol('_data64_b', u64(obj.data_data.len), true, 3) + obj.data_data << data_b + obj.add_symbol('_data64_unused', u64(obj.data_data.len), true, 3) + obj.data_data << [u8(0xe1), 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8] + obj.add_reloc(3, ro_b_sym, x86_64_reloc_signed, true, 2) + obj.add_reloc(10, ro_a_sym, x86_64_reloc_signed, true, 2) + obj.add_reloc(17, data_b_sym, x86_64_reloc_signed, true, 2) + obj.add_reloc(24, data_a_sym, x86_64_reloc_signed, true, 2) + + mut writer := MachOTinyObjectWriter{ + macho: obj + } + writer.write(path) or { panic(err) } + + data := os.read_bytes(path) or { panic(err) } + load_cmds := macho_test_load_commands(data) + sections := macho_test_sections(data, load_cmds[0]) + rodata_payload := data[int(sections[1].offset)..int(sections[1].offset + sections[1].size)] + data_payload := data[int(sections[2].offset)..int(sections[2].offset + sections[2].size)] + mut expected_rodata := []u8{} + expected_rodata << ro_b + expected_rodata << ro_a + mut expected_data := []u8{} + expected_data << data_b + expected_data << data_a + assert rodata_payload == expected_rodata + assert data_payload == expected_data + + symbols := macho_test_symbols(data) + ro_b_out := macho_test_symbol_by_name(symbols, 'L_ro64_b') or { + assert false, 'missing L_ro64_b' + return + } + ro_a_out := macho_test_symbol_by_name(symbols, 'L_ro64_a') or { + assert false, 'missing L_ro64_a' + return + } + data_b_out := macho_test_symbol_by_name(symbols, '_data64_b') or { + assert false, 'missing _data64_b' + return + } + data_a_out := macho_test_symbol_by_name(symbols, '_data64_a') or { + assert false, 'missing _data64_a' + return + } + assert ro_b_out.value == sections[1].addr + assert ro_a_out.value == sections[1].addr + 8 + assert data_b_out.value == sections[2].addr + assert data_a_out.value == sections[2].addr + 8 + + names := symbols.map(it.name) + assert 'L_ro64_unused' !in names + assert '_data64_unused' !in names + relocs := macho_test_text_relocations(data) + assert relocs.len == 4 + assert symbols[relocs[0].sym_idx].name == 'L_ro64_b' + assert symbols[relocs[1].sym_idx].name == 'L_ro64_a' + assert symbols[relocs[2].sym_idx].name == '_data64_b' + assert symbols[relocs[3].sym_idx].name == '_data64_a' +} + +fn test_macho_tiny_object_writer_rejects_unknown_relocation_type() { + path := temp_object_path('macho_tiny_bad_reloc') + defer { + os.rm(path) or {} + } + + mut obj := MachOObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0xc3] + call_sym := obj.add_undefined('_calloc') + obj.add_symbol('_main', 0, true, 1) + obj.add_reloc(1, call_sym, 99, true, 2) + mut writer := MachOTinyObjectWriter{ + macho: obj + } + writer.write(path) or { + assert err.msg().starts_with(macos_tiny_not_eligible_prefix) + return + } + assert false, 'Mach-O tiny writer accepted an unsupported relocation type' +} + +fn test_macho_tiny_object_writer_rejects_non_pcrel_relocation() { + path := temp_object_path('macho_tiny_non_pcrel_reloc') + defer { + os.rm(path) or {} + } + + mut obj := MachOObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0xc3] + call_sym := obj.add_undefined('_calloc') + obj.add_symbol('_main', 0, true, 1) + obj.add_reloc(1, call_sym, x86_64_reloc_branch, false, 2) + mut writer := MachOTinyObjectWriter{ + macho: obj + } + writer.write(path) or { + assert err.msg().starts_with(macos_tiny_not_eligible_prefix) + assert err.msg().contains('non-pcrel') + return + } + assert false, 'Mach-O tiny writer accepted a non-pcrel relocation' +} + +fn test_macho_tiny_object_writer_rejects_non_32_bit_relocation() { + path := temp_object_path('macho_tiny_non_32_bit_reloc') + defer { + os.rm(path) or {} + } + + mut obj := MachOObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0xc3] + call_sym := obj.add_undefined('_calloc') + obj.add_symbol('_main', 0, true, 1) + obj.add_reloc(1, call_sym, x86_64_reloc_branch, true, 3) + mut writer := MachOTinyObjectWriter{ + macho: obj + } + writer.write(path) or { + assert err.msg().starts_with(macos_tiny_not_eligible_prefix) + assert err.msg().contains('non-32-bit') + return + } + assert false, 'Mach-O tiny writer accepted a non-32-bit relocation' +} + +fn test_macho_tiny_object_writer_rejects_missing_main_symbol() { + path := temp_object_path('macho_tiny_missing_main') + defer { + os.rm(path) or {} + } + + mut obj := MachOObject.new() + obj.text_data << u8(0xc3) + obj.add_symbol('_not_main', 0, true, 1) + mut writer := MachOTinyObjectWriter{ + macho: obj + } + writer.write(path) or { + assert err.msg().starts_with(macos_tiny_not_eligible_prefix) + assert err.msg().contains('missing _main symbol') + return + } + assert false, 'Mach-O tiny writer accepted an object without _main' +} + +fn test_macho_tiny_object_writer_rejects_module_init_symbol() { + path := temp_object_path('macho_tiny_module_init') + defer { + os.rm(path) or {} + } + + mut obj := MachOObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0xc3] + init_start := obj.text_data.len + obj.text_data << u8(0xc3) + init_sym := obj.add_symbol('_dep__init', u64(init_start), true, 1) + obj.add_symbol('_main', 0, true, 1) + obj.add_reloc(1, init_sym, x86_64_reloc_branch, true, 2) + mut writer := MachOTinyObjectWriter{ + macho: obj + } + writer.write(path) or { + assert err.msg().starts_with(macos_tiny_not_eligible_prefix) + assert err.msg().contains('module init symbol') + return + } + assert false, 'Mach-O tiny writer accepted a module init symbol' +} + fn test_macho_object_is_accepted_by_external_tools_when_available() { tool := find_first_available_external_tool(macho_external_tool_candidates()) or { println('skipping ${@FN}: llvm-readobj, llvm-objdump, and otool variants are not available') diff --git a/vlib/v2/gen/x64/x64_pe_linker_test.v b/vlib/v2/gen/x64/x64_pe_linker_test.v index 4ea25bf13..b5bdba945 100644 --- a/vlib/v2/gen/x64/x64_pe_linker_test.v +++ b/vlib/v2/gen/x64/x64_pe_linker_test.v @@ -188,6 +188,40 @@ fn pe_test_rva_to_file_off(image []u8, rva u32) int { return -1 } +fn pe_test_import_names(image []u8) []string { + pe_off := int(pe_test_u32(image, 0x3c)) + opt_off := pe_off + 4 + 20 + import_rva := pe_test_u32(image, opt_off + 112 + pe_import_directory_index * 8) + if import_rva == 0 { + return [] + } + import_off := pe_test_rva_to_file_off(image, import_rva) + assert import_off > 0 + ilt_rva := pe_test_u32(image, import_off) + ilt_off := pe_test_rva_to_file_off(image, ilt_rva) + assert ilt_off > 0 + mut names := []string{} + for i := 0; i < 128; i++ { + hint_name_rva := u32(pe_test_u64(image, ilt_off + i * 8)) + if hint_name_rva == 0 { + return names + } + hint_name_off := pe_test_rva_to_file_off(image, hint_name_rva) + assert hint_name_off > 0 + assert pe_test_u16(image, hint_name_off) == 0 + assert hint_name_off % 2 == 0 + names << pe_test_string(image, hint_name_off + 2) + } + assert false + return names +} + +fn pe_test_iat_size(image []u8) u32 { + pe_off := int(pe_test_u32(image, 0x3c)) + opt_off := pe_off + 4 + 20 + return pe_test_u32(image, opt_off + 116 + pe_iat_directory_index * 8) +} + fn sample_pe_coff_object() &CoffObject { mut obj := CoffObject.new() obj.text_data << [u8(0xc3), 0xc3] @@ -311,10 +345,11 @@ fn test_pe_linker_emits_pe32_plus_headers_and_sections() { assert pe_test_u32(image, opt_off + 56) == idata_end } -fn test_pe_linker_emits_kernel32_import_table() { +fn test_pe_linker_emits_demand_driven_kernel32_import_table() { obj := sample_pe_coff_object() mut linker := PeLinker.new(obj) image := linker.image() or { panic(err) } + expected_imports := ['ExitProcess'] pe_off := int(pe_test_u32(image, 0x3c)) opt_off := pe_off + 4 + 20 @@ -325,7 +360,7 @@ fn test_pe_linker_emits_kernel32_import_table() { assert import_rva != 0 assert import_size == 40 assert iat_rva != 0 - assert iat_size == u32((pe_kernel32_imports.len + 1) * 8) + assert iat_size == u32((expected_imports.len + 1) * 8) import_off := pe_test_rva_to_file_off(image, import_rva) assert import_off > 0 @@ -342,7 +377,7 @@ fn test_pe_linker_emits_kernel32_import_table() { mut names := []string{} ilt_off := pe_test_rva_to_file_off(image, ilt_rva) iat_off := pe_test_rva_to_file_off(image, iat_rva) - for i in 0 .. pe_kernel32_imports.len { + for i in 0 .. expected_imports.len { hint_name_rva := u32(pe_test_u64(image, ilt_off + i * 8)) hint_name_off := pe_test_rva_to_file_off(image, hint_name_rva) assert pe_test_u16(image, hint_name_off) == 0 @@ -350,9 +385,47 @@ fn test_pe_linker_emits_kernel32_import_table() { names << pe_test_string(image, hint_name_off + 2) assert pe_test_u64(image, iat_off + i * 8) == pe_test_u64(image, ilt_off + i * 8) } - assert pe_test_u64(image, ilt_off + pe_kernel32_imports.len * 8) == 0 - assert pe_test_u64(image, iat_off + pe_kernel32_imports.len * 8) == 0 - assert names == pe_kernel32_imports + assert pe_test_u64(image, ilt_off + expected_imports.len * 8) == 0 + assert pe_test_u64(image, iat_off + expected_imports.len * 8) == 0 + assert names == expected_imports + assert names == pe_test_import_names(image) + assert pe_test_iat_size(image) == u32((expected_imports.len + 1) * 8) + assert 'HeapAlloc' !in names + assert 'WriteFile' !in names +} + +fn test_pe_linker_adds_direct_kernel32_imports_in_stable_order() { + mut obj := CoffObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0xe8, 0, 0, 0, 0, 0xc3] + obj.add_symbol('main', 10, true, 1) + write_file_sym := obj.add_undefined('WriteFile') + get_std_handle_sym := obj.add_undefined('GetStdHandle') + obj.add_text_reloc(1, write_file_sym, coff_image_rel_amd64_rel32) + obj.add_text_reloc(6, get_std_handle_sym, coff_image_rel_amd64_rel32) + + mut linker := PeLinker.new(obj) + image := linker.image() or { panic(err) } + names := pe_test_import_names(image) + + assert names == ['ExitProcess', 'GetStdHandle', 'WriteFile'] + assert pe_test_iat_size(image) == u32((names.len + 1) * 8) +} + +fn test_pe_linker_adds_runtime_kernel32_import_patches_only_when_needed() { + mut obj := CoffObject.new() + obj.text_data << [u8(0xe8), 0, 0, 0, 0, 0xc3] + obj.add_symbol('main', 5, true, 1) + calloc_sym := obj.add_undefined('calloc') + obj.add_text_reloc(1, calloc_sym, coff_image_rel_amd64_rel32) + + mut linker := PeLinker.new(obj) + image := linker.image() or { panic(err) } + names := pe_test_import_names(image) + + assert names == ['ExitProcess', 'GetProcessHeap', 'HeapAlloc'] + assert pe_test_iat_size(image) == u32((names.len + 1) * 8) + assert 'HeapFree' !in names + assert 'WriteFile' !in names } fn test_pe_linker_zeroes_unemitted_data_directories() { @@ -913,26 +986,17 @@ fn test_pe_linker_resolves_wgetenv_with_kernel32_runtime_thunk() { runtime_text_size := first_import_thunk_file_off - (text_raw + entry_stub_len + obj.text_data.len) import_offsets := linker.import_thunk_offsets(runtime_text_size) + names := pe_test_import_names(image) + assert names == ['ExitProcess', 'GetEnvironmentVariableW'] getenv_thunk := import_offsets['GetEnvironmentVariableW'] or { panic('missing GetEnvironmentVariableW import thunk') } - get_process_heap_thunk := import_offsets['GetProcessHeap'] or { - panic('missing GetProcessHeap import thunk') - } - heap_alloc_thunk := import_offsets['HeapAlloc'] or { panic('missing HeapAlloc import thunk') } - heap_free_thunk := import_offsets['HeapFree'] or { panic('missing HeapFree import thunk') } getenv_thunk_rva := text_rva + getenv_thunk - get_process_heap_thunk_rva := text_rva + get_process_heap_thunk - heap_alloc_thunk_rva := text_rva + heap_alloc_thunk - heap_free_thunk_rva := text_rva + heap_free_thunk mut has_buffer_arg_lea := false mut has_buffer_return_lea := false mut has_wgetenv_buffer_size := false mut calls_getenv := false - mut calls_get_process_heap := false - mut calls_heap_alloc := false - mut calls_heap_free := false runtime_scan_end := first_import_thunk_file_off - 8 for off in wgetenv_off .. runtime_scan_end { if image[off..off + 3] == [u8(0x48), 0x8d, 0x15] { @@ -958,24 +1022,15 @@ fn test_pe_linker_resolves_wgetenv_with_kernel32_runtime_thunk() { if target == getenv_thunk_rva { calls_getenv = true } - if target == get_process_heap_thunk_rva { - calls_get_process_heap = true - } - if target == heap_alloc_thunk_rva { - calls_heap_alloc = true - } - if target == heap_free_thunk_rva { - calls_heap_free = true - } } } assert has_buffer_arg_lea assert has_buffer_return_lea assert has_wgetenv_buffer_size assert calls_getenv - assert !calls_get_process_heap - assert !calls_heap_alloc - assert !calls_heap_free + assert 'GetProcessHeap' !in names + assert 'HeapAlloc' !in names + assert 'HeapFree' !in names } fn test_pe_linker_resolves_aligned_realloc_with_internal_heap_thunk() { diff --git a/vlib/v2/gen/x64/x64_runtime_smoke_test.v b/vlib/v2/gen/x64/x64_runtime_smoke_test.v index 7176607c0..f281c13f1 100644 --- a/vlib/v2/gen/x64/x64_runtime_smoke_test.v +++ b/vlib/v2/gen/x64/x64_runtime_smoke_test.v @@ -1,5 +1,6 @@ module x64 +import encoding.binary import os struct X64LinuxRunResult { @@ -36,6 +37,15 @@ struct X64HostRunExitResult { exit_code int } +struct X64ElfLoadSegment { + offset u64 + vaddr u64 + flags u32 + filesz u64 + memsz u64 + align u64 +} + fn run_x64_linux_program_redirected(name string, source string) X64LinuxRunResult { tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') os.mkdir_all(tmp_dir) or { panic(err) } @@ -207,6 +217,79 @@ fn x64_host_build_failure_message(name string, tmp_dir string, source_path strin return '${source_context}\nbuild exit code: ${build_exit_code}\nbuild output:\n${build_output}' } +fn x64_build_v2_delegate_for_tmp(vexe string, tmp_dir string) string { + v2_source := os.join_path(@VMODROOT, 'cmd', 'v2', 'v2.v') + v2_delegate := x64_host_bin_path(tmp_dir, 'v2_delegate') + build := + os.execute('${os.quoted_path(vexe)} -o ${os.quoted_path(v2_delegate)} ${os.quoted_path(v2_source)}') + if build.exit_code != 0 { + assert false, 'failed to build v2 delegate for tiny x64 test:\n${build.output}' + } + return v2_delegate +} + +fn x64_pinned_v2_build_environment(vexe string, tmp_dir string) map[string]string { + mut env := os.environ() + env.delete('V2_X64_LINUX_TINY') + env['V_V2_EXE'] = x64_build_v2_delegate_for_tmp(vexe, tmp_dir) + return env +} + +fn x64_strict_tiny_build_environment(vexe string, tmp_dir string) map[string]string { + mut env := x64_pinned_v2_build_environment(vexe, tmp_dir) + env['V2_X64_LINUX_TINY'] = '1' + return env +} + +fn x64_no_tiny_build_environment(vexe string, tmp_dir string) map[string]string { + return x64_pinned_v2_build_environment(vexe, tmp_dir) +} + +fn x64_macos_auto_tiny_build_environment(vexe string, tmp_dir string) map[string]string { + return x64_pinned_v2_build_environment(vexe, tmp_dir) +} + +fn x64_optional_tool_output(caller string, tool string, args string) ?string { + path := os.find_abs_path_of_executable(tool) or { + println('skipping ${caller} ${tool} assertions: ${tool} is not available') + return none + } + return os.execute('${os.quoted_path(path)} ${args}').output +} + +fn x64_elf_load_segments(path string) []X64ElfLoadSegment { + data := os.read_bytes(path) or { panic(err) } + if data.len < 64 || data[0] != 0x7f || data[1] != `E` || data[2] != `L` || data[3] != `F` + || data[4] != 2 || data[5] != 1 { + panic('${path} is not an ELF64 little-endian file') + } + phoff := int(binary.little_endian_u64_at(data, 32)) + phentsize := int(binary.little_endian_u16_at(data, 54)) + phnum := int(binary.little_endian_u16_at(data, 56)) + mut segments := []X64ElfLoadSegment{} + for i in 0 .. phnum { + off := phoff + i * phentsize + if off + 56 > data.len { + panic('${path} has a truncated ELF program header table') + } + if binary.little_endian_u32_at(data, off) == u32(pt_load) { + segments << X64ElfLoadSegment{ + offset: binary.little_endian_u64_at(data, off + 8) + vaddr: binary.little_endian_u64_at(data, off + 16) + flags: binary.little_endian_u32_at(data, off + 4) + filesz: binary.little_endian_u64_at(data, off + 32) + memsz: binary.little_endian_u64_at(data, off + 40) + align: binary.little_endian_u64_at(data, off + 48) + } + } + } + return segments +} + +fn x64_elf_load_segment_count(path string) int { + return x64_elf_load_segments(path).len +} + fn x64_host_run_failure_message(name string, tmp_dir string, source_path string, source_text string, bin_path string, build_output string, run_exit_code int, stdout []u8, stderr []u8) string { context := x64_host_context_with_source(name, tmp_dir, source_path, source_text, bin_path) stdout_report := x64_runtime_bytes_report('stdout', stdout) @@ -285,6 +368,98 @@ fn run_x64_host_program_redirected(name string, source string) X64HostRunResult } } +fn run_x64_host_program_redirected_tiny(name string, source string) X64HostRunResult { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + source_path := os.join_path(tmp_dir, '${name}.v') + bin_path := x64_host_bin_path(tmp_dir, name) + os.write_file(source_path, source) or { panic(err) } + vexe := os.getenv_opt('VEXE') or { @VEXE } + mut build := os.new_process(vexe) + defer { + build.close() + } + build.set_environment(x64_strict_tiny_build_environment(vexe, tmp_dir)) + build.set_args(['-v2', '-b', 'x64', source_path, '-o', bin_path]) + build.set_redirect_stdio() + build.run() + build.wait() + build_output := 'stdout:\n${build.stdout_slurp()}\nstderr:\n${build.stderr_slurp()}' + if build.code != 0 { + assert false, x64_host_build_failure_message(name, tmp_dir, source_path, source, bin_path, + build.code, build_output) + } + mut run := os.new_process(bin_path) + defer { + run.close() + } + run.set_redirect_stdio() + run.run() + run.wait() + stdout := run.stdout_slurp().bytes() + stderr := run.stderr_slurp().bytes() + if run.code != 0 { + assert false, x64_host_run_failure_message(name, tmp_dir, source_path, source, bin_path, + build_output, run.code, stdout, stderr) + } + return X64HostRunResult{ + name: name + tmp_dir: tmp_dir + source_path: source_path + source_text: source + bin_path: bin_path + build_output: build_output + stdout: stdout + stderr: stderr + } +} + +fn run_x64_host_program_redirected_auto(name string, source string) X64HostRunResult { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + source_path := os.join_path(tmp_dir, '${name}.v') + bin_path := x64_host_bin_path(tmp_dir, name) + os.write_file(source_path, source) or { panic(err) } + vexe := os.getenv_opt('VEXE') or { @VEXE } + mut build := os.new_process(vexe) + defer { + build.close() + } + build.set_environment(x64_pinned_v2_build_environment(vexe, tmp_dir)) + build.set_args(['-v2', '-no-parallel', '-b', 'x64', source_path, '-o', bin_path]) + build.set_redirect_stdio() + build.run() + build.wait() + build_output := 'stdout:\n${build.stdout_slurp()}\nstderr:\n${build.stderr_slurp()}' + if build.code != 0 { + assert false, x64_host_build_failure_message(name, tmp_dir, source_path, source, bin_path, + build.code, build_output) + } + mut run := os.new_process(bin_path) + defer { + run.close() + } + run.set_redirect_stdio() + run.run() + run.wait() + stdout := run.stdout_slurp().bytes() + stderr := run.stderr_slurp().bytes() + if run.code != 0 { + assert false, x64_host_run_failure_message(name, tmp_dir, source_path, source, bin_path, + build_output, run.code, stdout, stderr) + } + return X64HostRunResult{ + name: name + tmp_dir: tmp_dir + source_path: source_path + source_text: source + bin_path: bin_path + build_output: build_output + stdout: stdout + stderr: stderr + } +} + fn run_x64_host_program_redirected_with_exit(name string, source string) X64HostRunExitResult { tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') os.mkdir_all(tmp_dir) or { panic(err) } @@ -320,6 +495,20 @@ fn run_x64_host_program_redirected_with_exit(name string, source string) X64Host } } +fn x64_write_project_sources(root string, sources map[string]string) string { + mut source_text := '' + mut rel_paths := sources.keys() + rel_paths.sort() + for rel_path in rel_paths { + source := sources[rel_path] + source_path := os.join_path(root, rel_path) + os.mkdir_all(os.dir(source_path)) or { panic(err) } + os.write_file(source_path, source) or { panic(err) } + source_text += '--- ${rel_path} ---\n${source}\n' + } + return source_text +} + fn run_x64_host_project_redirected(name string, sources map[string]string) X64HostRunResult { tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') os.mkdir_all(tmp_dir) or { panic(err) } @@ -364,6 +553,98 @@ fn run_x64_host_project_redirected(name string, sources map[string]string) X64Ho } } +fn run_x64_host_project_redirected_auto(name string, sources map[string]string) X64HostRunResult { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + bin_path := x64_host_bin_path(tmp_dir, name) + source_root := tmp_dir + source_text := x64_write_project_sources(tmp_dir, sources) + vexe := x64_vexe_command_path() + mut build := os.new_process(vexe) + defer { + build.close() + } + build.set_environment(x64_pinned_v2_build_environment(vexe, tmp_dir)) + build.set_args(['-v2', '-no-parallel', '-b', 'x64', tmp_dir, '-o', bin_path]) + build.set_redirect_stdio() + build.run() + build.wait() + build_output := 'stdout:\n${build.stdout_slurp()}\nstderr:\n${build.stderr_slurp()}' + if build.code != 0 { + assert false, x64_host_build_failure_message(name, tmp_dir, source_root, source_text, + bin_path, build.code, build_output) + } + mut run := os.new_process(bin_path) + defer { + run.close() + } + run.set_redirect_stdio() + run.run() + run.wait() + stdout := run.stdout_slurp().bytes() + stderr := run.stderr_slurp().bytes() + if run.code != 0 { + assert false, x64_host_run_failure_message(name, tmp_dir, source_root, source_text, + bin_path, build_output, run.code, stdout, stderr) + } + return X64HostRunResult{ + name: name + tmp_dir: tmp_dir + source_path: source_root + source_text: source_text + bin_path: bin_path + build_output: build_output + stdout: stdout + stderr: stderr + } +} + +fn run_x64_host_project_redirected_macos_auto_tiny_verbose(name string, sources map[string]string) X64HostRunResult { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + bin_path := x64_host_bin_path(tmp_dir, name) + source_root := tmp_dir + source_text := x64_write_project_sources(tmp_dir, sources) + vexe := x64_vexe_command_path() + mut build := os.new_process(vexe) + defer { + build.close() + } + build.set_environment(x64_macos_auto_tiny_build_environment(vexe, tmp_dir)) + build.set_args(['-v2', '-v', '-no-parallel', '-b', 'x64', tmp_dir, '-o', bin_path]) + build.set_redirect_stdio() + build.run() + build.wait() + build_output := 'stdout:\n${build.stdout_slurp()}\nstderr:\n${build.stderr_slurp()}' + if build.code != 0 { + assert false, x64_host_build_failure_message(name, tmp_dir, source_root, source_text, + bin_path, build.code, build_output) + } + mut run := os.new_process(bin_path) + defer { + run.close() + } + run.set_redirect_stdio() + run.run() + run.wait() + stdout := run.stdout_slurp().bytes() + stderr := run.stderr_slurp().bytes() + if run.code != 0 { + assert false, x64_host_run_failure_message(name, tmp_dir, source_root, source_text, + bin_path, build_output, run.code, stdout, stderr) + } + return X64HostRunResult{ + name: name + tmp_dir: tmp_dir + source_path: source_root + source_text: source_text + bin_path: bin_path + build_output: build_output + stdout: stdout + stderr: stderr + } +} + fn run_x64_host_file_redirected(name string, source_dir string, source_file string) X64HostRunResult { tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') os.mkdir_all(tmp_dir) or { panic(err) } @@ -401,6 +682,147 @@ fn run_x64_host_file_redirected(name string, source_dir string, source_file stri } } +fn run_x64_host_file_redirected_auto(name string, source_dir string, source_file string) X64HostRunResult { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + source_path := os.join_path(source_dir, source_file) + source_text := 'source file: ${source_path}' + bin_path := x64_host_bin_path(tmp_dir, name) + vexe := x64_vexe_command_path() + mut build := os.new_process(vexe) + defer { + build.close() + } + build.set_environment(x64_pinned_v2_build_environment(vexe, tmp_dir)) + build.set_work_folder(source_dir) + build.set_args(['-v2', '-no-parallel', '-b', 'x64', source_file, '-o', bin_path]) + build.set_redirect_stdio() + build.run() + build.wait() + build_output := 'stdout:\n${build.stdout_slurp()}\nstderr:\n${build.stderr_slurp()}' + if build.code != 0 { + assert false, x64_host_build_failure_message(name, tmp_dir, source_path, source_text, + bin_path, build.code, build_output) + } + mut run := os.new_process(bin_path) + defer { + run.close() + } + run.set_redirect_stdio() + run.run() + run.wait() + stdout := run.stdout_slurp().bytes() + stderr := run.stderr_slurp().bytes() + if run.code != 0 { + assert false, x64_host_run_failure_message(name, tmp_dir, source_path, source_text, + bin_path, build_output, run.code, stdout, stderr) + } + return X64HostRunResult{ + name: name + tmp_dir: tmp_dir + source_path: source_path + source_text: source_text + bin_path: bin_path + build_output: build_output + stdout: stdout + stderr: stderr + } +} + +fn run_x64_host_file_redirected_macos_auto_tiny(name string, source_dir string, source_file string) X64HostRunResult { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + source_path := os.join_path(source_dir, source_file) + source_text := 'source file: ${source_path}' + bin_path := x64_host_bin_path(tmp_dir, name) + vexe := x64_vexe_command_path() + mut build := os.new_process(vexe) + defer { + build.close() + } + build.set_environment(x64_macos_auto_tiny_build_environment(vexe, tmp_dir)) + build.set_work_folder(source_dir) + build.set_args(['-v2', '-v', '-no-parallel', '-b', 'x64', source_file, '-o', bin_path]) + build.set_redirect_stdio() + build.run() + build.wait() + build_output := 'stdout:\n${build.stdout_slurp()}\nstderr:\n${build.stderr_slurp()}' + if build.code != 0 { + assert false, x64_host_build_failure_message(name, tmp_dir, source_path, source_text, + bin_path, build.code, build_output) + } + mut run := os.new_process(bin_path) + defer { + run.close() + } + run.set_redirect_stdio() + run.run() + run.wait() + stdout := run.stdout_slurp().bytes() + stderr := run.stderr_slurp().bytes() + if run.code != 0 { + assert false, x64_host_run_failure_message(name, tmp_dir, source_path, source_text, + bin_path, build_output, run.code, stdout, stderr) + } + return X64HostRunResult{ + name: name + tmp_dir: tmp_dir + source_path: source_path + source_text: source_text + bin_path: bin_path + build_output: build_output + stdout: stdout + stderr: stderr + } +} + +fn run_x64_host_file_redirected_no_tiny(name string, source_dir string, source_file string) X64HostRunResult { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + source_path := os.join_path(source_dir, source_file) + source_text := 'source file: ${source_path}' + bin_path := x64_host_bin_path(tmp_dir, name) + vexe := x64_vexe_command_path() + mut build := os.new_process(vexe) + defer { + build.close() + } + build.set_environment(x64_no_tiny_build_environment(vexe, tmp_dir)) + build.set_work_folder(source_dir) + build.set_args(['-v2', '-no-parallel', '-b', 'x64', '-no-mos-tiny', source_file, '-o', bin_path]) + build.set_redirect_stdio() + build.run() + build.wait() + build_output := 'stdout:\n${build.stdout_slurp()}\nstderr:\n${build.stderr_slurp()}' + if build.code != 0 { + assert false, x64_host_build_failure_message(name, tmp_dir, source_path, source_text, + bin_path, build.code, build_output) + } + mut run := os.new_process(bin_path) + defer { + run.close() + } + run.set_redirect_stdio() + run.run() + run.wait() + stdout := run.stdout_slurp().bytes() + stderr := run.stderr_slurp().bytes() + if run.code != 0 { + assert false, x64_host_run_failure_message(name, tmp_dir, source_path, source_text, + bin_path, build_output, run.code, stdout, stderr) + } + return X64HostRunResult{ + name: name + tmp_dir: tmp_dir + source_path: source_path + source_text: source_text + bin_path: bin_path + build_output: build_output + stdout: stdout + stderr: stderr + } +} + fn assert_x64_linux_stdout_bytes(name string, source string, expected_stdout []u8) { $if linux { result := run_x64_linux_program_redirected(name, source) @@ -417,6 +839,149 @@ fn assert_x64_linux_file_stdout_bytes(name string, source_dir string, source_fil } } +fn assert_x64_linux_no_libc_binary(result X64HostRunResult, expected_stdout []u8) { + $if linux { + context := x64_host_result_context(result) + assert result.stdout == expected_stdout, x64_stdout_mismatch_message(result.name, 'linux', + expected_stdout, result.stdout, context) + + assert result.stderr == []u8{}, x64_stderr_mismatch_message(result.name, 'linux', []u8{}, + result.stderr, context) + + assert os.file_size(result.bin_path) < 16 * 1024, '${context}\nbinary is not tiny: ${os.file_size(result.bin_path)} bytes' + if readelf_dynamic := x64_optional_tool_output(@FN, 'readelf', + '-d ${os.quoted_path(result.bin_path)}') + { + assert readelf_dynamic.contains('There is no dynamic section in this file.'), '${context}\nreadelf -d output:\n${readelf_dynamic}' + } + + if readelf_headers := x64_optional_tool_output(@FN, 'readelf', + '-h -l ${os.quoted_path(result.bin_path)}') + { + assert readelf_headers.contains('Type: EXEC (Executable file)'), readelf_headers + + assert readelf_headers.contains('LOAD'), readelf_headers + } + if ldd := x64_optional_tool_output(@FN, 'ldd', os.quoted_path(result.bin_path)) { + assert ldd.contains('not a dynamic executable'), '${context}\nldd output:\n${ldd}' + } + if nm := x64_optional_tool_output(@FN, 'nm', '-u ${os.quoted_path(result.bin_path)}') { + assert nm.contains('no symbols') || nm.trim_space() == '', '${context}\nnm -u output:\n${nm}' + } + } +} + +fn assert_x64_linux_hosted_libc_binary(result X64HostRunResult, expected_stdout []u8) { + $if linux { + context := x64_host_result_context(result) + assert result.stdout == expected_stdout, x64_stdout_mismatch_message(result.name, 'linux', + expected_stdout, result.stdout, context) + + assert result.stderr == []u8{}, x64_stderr_mismatch_message(result.name, 'linux', []u8{}, + result.stderr, context) + + if readelf_dynamic := x64_optional_tool_output(@FN, 'readelf', + '-d ${os.quoted_path(result.bin_path)}') + { + assert readelf_dynamic.contains('Dynamic section at offset'), '${context}\nreadelf -d output:\n${readelf_dynamic}' + } + if ldd := x64_optional_tool_output(@FN, 'ldd', os.quoted_path(result.bin_path)) { + assert ldd.contains('libc.so'), '${context}\nldd output:\n${ldd}' + } + if nm := x64_optional_tool_output(@FN, 'nm', '-u ${os.quoted_path(result.bin_path)}') { + assert nm.contains('__libc_start_main') || nm.contains('calloc'), '${context}\nnm -u output:\n${nm}' + } + } +} + +fn assert_x64_linux_load_segment_count(result X64HostRunResult, expected_count int) { + $if linux { + context := x64_host_result_context(result) + actual_count := x64_elf_load_segment_count(result.bin_path) + assert actual_count == expected_count, '${context}\nELF PT_LOAD count: expected ${expected_count}, got ${actual_count}' + } +} + +fn assert_x64_linux_tiny_load_segment_layout(result X64HostRunResult, expected_rw_bss_bytes u64) { + $if linux { + context := x64_host_result_context(result) + segments := x64_elf_load_segments(result.bin_path) + for i, segment in segments { + wx_flags := segment.flags & u32(pf_w | pf_x) + assert wx_flags != u32(pf_w | pf_x), '${context}\nELF PT_LOAD ${i} is writable and executable: flags=0x${segment.flags.hex()}' + assert segment.memsz >= segment.filesz, '${context}\nELF PT_LOAD ${i} memsz ${segment.memsz} is smaller than filesz ${segment.filesz}' + if segment.align > 1 { + assert segment.vaddr % segment.align == segment.offset % segment.align, '${context}\nELF PT_LOAD ${i} offset/vaddr are not congruent modulo align: offset=0x${segment.offset.hex()} vaddr=0x${segment.vaddr.hex()} align=0x${segment.align.hex()}' + } + } + text_segment := segments[0] + assert text_segment.flags == u32(pf_r | pf_x), '${context}\nELF text PT_LOAD flags: expected 0x${u32(pf_r | pf_x).hex()}, got 0x${text_segment.flags.hex()}' + assert text_segment.memsz == text_segment.filesz, '${context}\nELF text PT_LOAD should not contain writable zero-fill: filesz=${text_segment.filesz} memsz=${text_segment.memsz}' + if expected_rw_bss_bytes == 0 { + assert segments.len == 1, '${context}\nELF PT_LOAD count: expected 1, got ${segments.len}' + return + } + assert segments.len == 2, '${context}\nELF PT_LOAD count: expected 2, got ${segments.len}' + rw_segment := segments[1] + assert rw_segment.flags == u32(pf_r | pf_w), '${context}\nELF RW PT_LOAD flags: expected 0x${u32(pf_r | pf_w).hex()}, got 0x${rw_segment.flags.hex()}' + assert rw_segment.filesz == 0, '${context}\nELF RW PT_LOAD for tiny runtime metadata should be zero-fill only: filesz=${rw_segment.filesz}' + assert rw_segment.memsz == expected_rw_bss_bytes, '${context}\nELF RW PT_LOAD bss bytes: expected ${expected_rw_bss_bytes}, got ${rw_segment.memsz}' + } +} + +fn assert_x64_linux_tiny_build_rejected(name string, source string, expected_message string) { + $if linux { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + source_path := os.join_path(tmp_dir, '${name}.v') + bin_path := x64_host_bin_path(tmp_dir, name) + os.write_file(source_path, source) or { panic(err) } + vexe := x64_vexe_command_path() + mut build := os.new_process(vexe) + defer { + build.close() + } + build.set_environment(x64_strict_tiny_build_environment(vexe, tmp_dir)) + build.set_args(['-v2', '-b', 'x64', source_path, '-o', bin_path]) + build.set_redirect_stdio() + build.run() + build.wait() + build_output := 'stdout:\n${build.stdout_slurp()}\nstderr:\n${build.stderr_slurp()}' + assert build.code != 0, '${name} unexpectedly built with Linux tiny:\n${build_output}' + assert build_output.contains(expected_message), '${name} rejection did not contain `${expected_message}`:\n${build_output}' + assert !os.exists(bin_path), '${name} produced a binary despite Linux tiny rejection: ${bin_path}' + } +} + +fn assert_x64_linux_tiny_project_build_rejected(name string, sources map[string]string, expected_message string) { + $if linux { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + bin_path := x64_host_bin_path(tmp_dir, name) + x64_write_project_sources(tmp_dir, sources) + vexe := x64_vexe_command_path() + mut build := os.new_process(vexe) + defer { + build.close() + } + build.set_environment(x64_strict_tiny_build_environment(vexe, tmp_dir)) + build.set_args(['-v2', '-no-parallel', '-b', 'x64', tmp_dir, '-o', bin_path]) + build.set_redirect_stdio() + build.run() + build.wait() + build_output := 'stdout:\n${build.stdout_slurp()}\nstderr:\n${build.stderr_slurp()}' + assert build.code != 0, '${name} unexpectedly built with Linux tiny:\n${build_output}' + assert build_output.contains(expected_message), '${name} rejection did not contain `${expected_message}`:\n${build_output}' + assert !os.exists(bin_path), '${name} produced a binary despite Linux tiny rejection: ${bin_path}' + } +} + fn assert_x64_macos_windows_stdout_bytes(name string, source string, expected_stdout []u8) { $if macos { result := run_x64_host_program_redirected('macos_${name}', source) @@ -1812,6 +2377,49 @@ fn x64_module_init_once_stdout() []u8 { '.bytes() } +fn x64_auto_tiny_rejected_imported_runtime_init_sources() map[string]string { + return { + 'main.v': "module main + +import dep + +fn main() { + marker := 'H'.clone() + print(marker) + println(dep.score()) +} +" + 'dep/dep.v': 'module dep + +const runtime_offset = make_offset() + +pub __global mut init_hits = 0 +pub __global runtime_state = make_state() + +fn make_offset() int { + return 7 +} + +fn make_state() int { + return 23 +} + +fn init() { + init_hits += runtime_state + runtime_offset +} + +pub fn score() int { + return init_hits + runtime_state + runtime_offset +} +' + } +} + +fn x64_auto_tiny_rejected_imported_runtime_init_stdout() []u8 { + return 'H60 +'.bytes() +} + fn x64_module_storage_sources() map[string]string { return { 'main.v': "module main @@ -2202,6 +2810,512 @@ fn test_x64_linux_fizz_buzz_example_top_level_stdout_exact_bytes() { 'fizz_buzz.v', x64_fizz_buzz_example_stdout()) } +fn test_x64_linux_fizz_buzz_example_auto_tiny_no_libc() { + $if linux { + result := run_x64_host_file_redirected_auto('fizz_buzz_example_auto_tiny_no_libc', + x64_examples_dir(), 'fizz_buzz.v') + assert_x64_linux_no_libc_binary(result, x64_fizz_buzz_example_stdout()) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_hello_world_example_auto_tiny_no_libc() { + $if linux { + result := run_x64_host_file_redirected_auto('hello_world_example_auto_tiny_no_libc', + x64_examples_dir(), 'hello_world.v') + assert_x64_linux_no_libc_binary(result, 'Hello, World!\n'.bytes()) + assert_x64_linux_tiny_load_segment_layout(result, 0) + context := x64_host_result_context(result) + assert os.file_size(result.bin_path) < 512, '${context}\nbinary is not ultra tiny: ${os.file_size(result.bin_path)} bytes' + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn assert_x64_macos_tiny_object_used(result X64HostRunResult, context string) { + assert result.build_output.contains('macOS tiny object candidate enabled'), context + assert result.build_output.contains('built-in macOS tiny object'), context + assert !result.build_output.contains('falling back to normal Mach-O object'), context + assert !result.build_output.contains('retrying with normal Mach-O object'), context +} + +fn test_x64_macos_hello_world_example_auto_tiny_uses_tiny_object() { + $if macos { + tiny := run_x64_host_file_redirected_macos_auto_tiny('macos_hello_world_example_auto_tiny', + x64_examples_dir(), 'hello_world.v') + normal := run_x64_host_file_redirected_no_tiny('macos_hello_world_example_normal', + x64_examples_dir(), 'hello_world.v') + context := '${x64_host_result_context(tiny)}\nnormal build:\n${x64_host_result_context(normal)}' + assert tiny.stdout == 'Hello, World!\n'.bytes(), context + assert tiny.stderr == []u8{}, context + assert normal.stdout == tiny.stdout, context + assert normal.stderr == []u8{}, context + assert_x64_macos_tiny_object_used(tiny, context) + tiny_size := os.file_size(tiny.bin_path) + normal_size := os.file_size(normal.bin_path) + assert tiny_size > 0, '${context}\ntiny size: ${tiny_size}' + assert normal_size > 0, '${context}\nnormal size: ${normal_size}' + println('macOS x64 tiny hello_world: tiny size=${tiny_size}; normal size=${normal_size}') + x64_host_cleanup_tmp(tiny.tmp_dir) + x64_host_cleanup_tmp(normal.tmp_dir) + } +} + +fn test_x64_macos_fizz_buzz_example_auto_tiny_uses_tiny_object() { + $if macos { + tiny := run_x64_host_file_redirected_macos_auto_tiny('macos_fizz_buzz_example_auto_tiny', + x64_examples_dir(), 'fizz_buzz.v') + normal := run_x64_host_file_redirected_no_tiny('macos_fizz_buzz_example_normal', + x64_examples_dir(), 'fizz_buzz.v') + context := '${x64_host_result_context(tiny)}\nnormal build:\n${x64_host_result_context(normal)}' + assert tiny.stdout == x64_fizz_buzz_example_stdout(), context + assert tiny.stderr == []u8{}, context + assert normal.stdout == tiny.stdout, context + assert normal.stderr == []u8{}, context + assert_x64_macos_tiny_object_used(tiny, context) + tiny_size := os.file_size(tiny.bin_path) + normal_size := os.file_size(normal.bin_path) + assert tiny_size > 0, '${context}\ntiny size: ${tiny_size}' + assert normal_size > 0, '${context}\nnormal size: ${normal_size}' + println('macOS x64 tiny fizz_buzz: tiny size=${tiny_size}; normal size=${normal_size}') + x64_host_cleanup_tmp(tiny.tmp_dir) + x64_host_cleanup_tmp(normal.tmp_dir) + } +} + +fn test_x64_macos_auto_tiny_ineligible_project_falls_back_to_normal_macho() { + $if macos { + result := run_x64_host_project_redirected_macos_auto_tiny_verbose('macos_auto_tiny_module_init_fallback', + x64_auto_tiny_rejected_imported_runtime_init_sources()) + context := x64_host_result_context(result) + assert result.stdout == x64_auto_tiny_rejected_imported_runtime_init_stdout(), context + assert result.stderr == []u8{}, context + assert result.build_output.contains('macOS tiny object candidate enabled'), context + + assert result.build_output.contains('macOS tiny object not eligible; falling back to normal Mach-O object'), context + + assert result.build_output.contains('module init symbol'), context + assert !result.build_output.contains('built-in macOS tiny object'), context + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_non_hello_literal_falls_back_to_tiny_runtime() { + $if linux { + result := run_x64_host_program_redirected_auto('non_hello_literal_tiny_runtime', "module main + +fn main() { + println('x') +} +") + assert_x64_linux_no_libc_binary(result, 'x\n'.bytes()) + assert_x64_linux_tiny_load_segment_layout(result, 0) + context := x64_host_result_context(result) + assert os.file_size(result.bin_path) >= 512, '${context}\nnon-hello literal unexpectedly used ultra tiny path: ${os.file_size(result.bin_path)} bytes' + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_hello_print_without_newline_does_not_use_ultra_tiny() { + $if linux { + result := run_x64_host_program_redirected_auto('hello_print_without_newline_hosted_runtime', "module main + +fn main() { + print('Hello, World!') +} +") + assert_x64_linux_hosted_libc_binary(result, 'Hello, World!'.bytes()) + context := x64_host_result_context(result) + assert os.file_size(result.bin_path) >= 512, '${context}\nprint without newline unexpectedly used ultra tiny path: ${os.file_size(result.bin_path)} bytes' + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_two_hello_println_calls_do_not_use_ultra_tiny() { + $if linux { + result := run_x64_host_program_redirected_auto('two_hello_println_tiny_runtime', "module main + +fn main() { + println('Hello, World!') + println('Hello, World!') +} +") + assert_x64_linux_no_libc_binary(result, 'Hello, World!\nHello, World!\n'.bytes()) + context := x64_host_result_context(result) + assert os.file_size(result.bin_path) >= 512, '${context}\ntwo println calls unexpectedly used ultra tiny path: ${os.file_size(result.bin_path)} bytes' + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_conditional_hello_println_does_not_use_ultra_tiny() { + $if linux { + result := run_x64_host_program_redirected_auto('conditional_hello_println_tiny_runtime', "module main + +fn main() { + if 1 == 1 { + println('Hello, World!') + } +} +") + assert_x64_linux_no_libc_binary(result, 'Hello, World!\n'.bytes()) + context := x64_host_result_context(result) + assert os.file_size(result.bin_path) >= 512, '${context}\nconditional println unexpectedly used ultra tiny path: ${os.file_size(result.bin_path)} bytes' + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_tiny_casted_large_int_literal_uses_int_str_runtime() { + $if linux { + result := run_x64_host_program_redirected_tiny('tiny_casted_large_int_literal_int_str_runtime', 'module main + +fn main() { + println(int(2147483648)) +} +') + assert_x64_linux_no_libc_binary(result, '-2147483648\n'.bytes()) + assert_x64_linux_tiny_load_segment_layout(result, linux_tiny_int_str_arena_metadata_bytes) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_tiny_int_str_negative_stdout_exact_bytes() { + $if linux { + result := run_x64_host_program_redirected_tiny('tiny_negative_int_str', 'module main + +fn main() { + n := int(-23) + println(n.str()) +} +') + assert_x64_linux_no_libc_binary(result, '-23\n'.bytes()) + assert_x64_linux_tiny_load_segment_layout(result, linux_tiny_int_str_arena_metadata_bytes) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_tiny_int_str_i32_min_stdout_exact_bytes() { + $if linux { + result := run_x64_host_program_redirected_tiny('tiny_i32_min_int_str', 'module main + +fn main() { + x := int(-2147483648) + println(x.str()) +} +') + assert_x64_linux_no_libc_binary(result, '-2147483648\n'.bytes()) + assert_x64_linux_tiny_load_segment_layout(result, linux_tiny_int_str_arena_metadata_bytes) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_auto_tiny_stored_int_str_values_fall_back_to_hosted_libc() { + $if linux { + result := run_x64_host_program_redirected_auto('auto_tiny_stored_int_str_values_hosted', 'module main + +fn main() { + a := 12.str() + b := 34.str() + println(a) + println(b) + println(a) +} +') + assert_x64_linux_hosted_libc_binary(result, '12\n34\n12\n'.bytes()) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_strict_tiny_stored_int_str_values_are_rejected() { + $if linux { + assert_x64_linux_tiny_build_rejected('strict_tiny_stored_int_str_values_rejected', 'module main + +fn main() { + a := 12.str() + b := 34.str() + println(a) + println(b) + println(a) +} +', + linux_tiny_not_eligible_prefix) + } +} + +fn test_x64_linux_auto_tiny_passed_int_str_value_falls_back_to_hosted_libc() { + $if linux { + result := run_x64_host_program_redirected_auto('auto_tiny_passed_int_str_value_hosted', 'module main + +fn emit_twice(s string) { + println(s) + println(s) +} + +fn main() { + emit_twice(42.str()) +} +') + assert_x64_linux_hosted_libc_binary(result, '42\n42\n'.bytes()) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_strict_tiny_passed_int_str_value_is_rejected() { + $if linux { + assert_x64_linux_tiny_build_rejected('strict_tiny_passed_int_str_value_rejected', 'module main + +fn emit_twice(s string) { + println(s) + println(s) +} + +fn main() { + emit_twice(42.str()) +} +', + linux_tiny_not_eligible_prefix) + } +} + +fn test_x64_linux_auto_tiny_returned_int_str_value_uses_tiny_runtime() { + $if linux { + result := run_x64_host_program_redirected_auto('auto_tiny_returned_int_str_value_runtime', 'module main + +fn render(n int) string { + return n.str() +} + +fn main() { + s := render(43) + println(s) + println(s) +} +') + assert_x64_linux_no_libc_binary(result, '43\n43\n'.bytes()) + assert_x64_linux_tiny_load_segment_layout(result, linux_tiny_int_str_arena_metadata_bytes) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_strict_tiny_returned_int_str_value_uses_tiny_runtime() { + $if linux { + result := run_x64_host_program_redirected_tiny('strict_tiny_returned_int_str_value_runtime', 'module main + +fn render(n int) string { + return n.str() +} + +fn main() { + s := render(43) + println(s) + println(s) +} +') + assert_x64_linux_no_libc_binary(result, '43\n43\n'.bytes()) + assert_x64_linux_tiny_load_segment_layout(result, linux_tiny_int_str_arena_metadata_bytes) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_auto_tiny_cloned_int_str_value_falls_back_to_hosted_libc() { + $if linux { + result := run_x64_host_program_redirected_auto('auto_tiny_cloned_int_str_value_hosted', 'module main + +fn main() { + s := 44.str().clone() + println(s) + println(s) +} +') + assert_x64_linux_hosted_libc_binary(result, '44\n44\n'.bytes()) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_strict_tiny_cloned_int_str_value_is_rejected() { + $if linux { + assert_x64_linux_tiny_build_rejected('strict_tiny_cloned_int_str_value_rejected', 'module main + +fn main() { + s := 44.str().clone() + println(s) + println(s) +} +', + linux_tiny_not_eligible_prefix) + } +} + +fn test_x64_linux_tiny_int_str_runtime_many_values_stdout_exact_bytes() { + $if linux { + result := run_x64_host_program_redirected_tiny('tiny_int_str_runtime_many_values', 'module main + +fn main() { + x := int(-2147483648) + y := int(-23) + z := int(0) + max := int(2147483647) + println(x) + println(y.str()) + println(z) + for i in 0 .. 130 { + println(i.str()) + } + println(max) +} +') + mut expected_stdout := []u8{} + expected_stdout << '-2147483648\n-23\n0\n'.bytes() + for i in 0 .. 130 { + expected_stdout << '${i}\n'.bytes() + } + expected_stdout << '2147483647\n'.bytes() + assert_x64_linux_no_libc_binary(result, expected_stdout) + assert_x64_linux_tiny_load_segment_layout(result, linux_tiny_int_str_arena_metadata_bytes) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_tiny_untyped_int_literal_uses_int_str_runtime() { + $if linux { + result := run_x64_host_program_redirected_tiny('tiny_untyped_int_literal_int_str_runtime', 'module main + +fn main() { + println(0) +} +') + assert_x64_linux_no_libc_binary(result, '0\n'.bytes()) + assert_x64_linux_tiny_load_segment_layout(result, linux_tiny_int_str_arena_metadata_bytes) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_strict_tiny_ineligible_string_clone_is_rejected() { + $if linux { + assert_x64_linux_tiny_build_rejected('tiny_ineligible_string_clone_rejected', "module main + +fn main() { + s := 'X'.clone() + println(s) +} +", + linux_tiny_not_eligible_prefix) + } +} + +fn test_x64_linux_auto_tiny_ineligible_string_clone_falls_back_to_hosted_libc() { + $if linux { + result := run_x64_host_program_redirected_auto('auto_tiny_ineligible_string_clone_hosted_libc', "module main + +fn main() { + s := 'X'.clone() + println(s) +} +") + assert_x64_linux_hosted_libc_binary(result, 'X\n'.bytes()) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_auto_tiny_rejected_project_falls_back_to_hosted_with_runtime_inits() { + $if linux { + sources := x64_auto_tiny_rejected_imported_runtime_init_sources() + assert_x64_linux_tiny_project_build_rejected('strict_tiny_imported_runtime_init_project_rejected', + sources, linux_tiny_not_eligible_prefix) + result := run_x64_host_project_redirected_auto('auto_tiny_imported_runtime_init_project_hosted', + sources) + assert_x64_linux_hosted_libc_binary(result, + x64_auto_tiny_rejected_imported_runtime_init_stdout()) + x64_host_cleanup_tmp(result.tmp_dir) + } +} + +fn test_x64_linux_auto_tiny_rejection_replaces_existing_tiny_output_with_hosted_link() { + $if linux { + name := 'auto_tiny_stale_output_replaced_by_hosted' + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_x64_runtime_${name}_${os.getpid()}') + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + bin_path := x64_host_bin_path(tmp_dir, name) + vexe := x64_vexe_command_path() + env := x64_pinned_v2_build_environment(vexe, tmp_dir) + + stale_source := "module main + +fn main() { + println('stale') +} +" + stale_source_path := os.join_path(tmp_dir, 'stale.v') + os.write_file(stale_source_path, stale_source) or { panic(err) } + mut stale_build := os.new_process(vexe) + defer { + stale_build.close() + } + stale_build.set_environment(env) + stale_build.set_args(['-v2', '-no-parallel', '-b', 'x64', stale_source_path, '-o', bin_path]) + stale_build.set_redirect_stdio() + stale_build.run() + stale_build.wait() + stale_build_output := 'stdout:\n${stale_build.stdout_slurp()}\nstderr:\n${stale_build.stderr_slurp()}' + assert stale_build.code == 0, x64_host_build_failure_message(name, tmp_dir, + stale_source_path, stale_source, bin_path, stale_build.code, stale_build_output) + + assert os.exists(bin_path), '${name} did not create the initial output binary' + stale_run := os.execute(os.quoted_path(bin_path)) + assert stale_run.exit_code == 0, '${name} initial stale binary failed:\n${stale_run.output}' + assert stale_run.output == 'stale\n', '${name} initial stale binary output mismatch:\n${stale_run.output}' + + hosted_source := "module main + +fn main() { + s := 'hosted'.clone() + println(s) +} +" + hosted_source_path := os.join_path(tmp_dir, 'hosted.v') + os.write_file(hosted_source_path, hosted_source) or { panic(err) } + mut hosted_build := os.new_process(vexe) + defer { + hosted_build.close() + } + hosted_build.set_environment(env) + hosted_build.set_args(['-v2', '-no-parallel', '-b', 'x64', hosted_source_path, '-o', bin_path]) + hosted_build.set_redirect_stdio() + hosted_build.run() + hosted_build.wait() + hosted_build_output := 'stdout:\n${hosted_build.stdout_slurp()}\nstderr:\n${hosted_build.stderr_slurp()}' + if hosted_build.code != 0 { + assert false, x64_host_build_failure_message(name, tmp_dir, hosted_source_path, + hosted_source, bin_path, hosted_build.code, hosted_build_output) + } + mut hosted_run := os.new_process(bin_path) + defer { + hosted_run.close() + } + hosted_run.set_redirect_stdio() + hosted_run.run() + hosted_run.wait() + stdout := hosted_run.stdout_slurp().bytes() + stderr := hosted_run.stderr_slurp().bytes() + if hosted_run.code != 0 { + assert false, x64_host_run_failure_message(name, tmp_dir, hosted_source_path, + hosted_source, bin_path, hosted_build_output, hosted_run.code, stdout, stderr) + } + result := X64HostRunResult{ + name: name + tmp_dir: tmp_dir + source_path: hosted_source_path + source_text: hosted_source + bin_path: bin_path + build_output: hosted_build_output + stdout: stdout + stderr: stderr + } + assert_x64_linux_hosted_libc_binary(result, 'hosted\n'.bytes()) + } +} + fn test_x64_macos_windows_getwd_clone_stdout_exact_bytes() { assert_x64_macos_windows_stdout_bytes('getwd_clone_exact', x64_getwd_clone_source(), x64_getwd_clone_stdout()) diff --git a/vlib/v2/pref/pref.v b/vlib/v2/pref/pref.v index a32fe3802..2d9b3dee5 100644 --- a/vlib/v2/pref/pref.v +++ b/vlib/v2/pref/pref.v @@ -59,6 +59,7 @@ pub mut: output_cross_c bool // -os cross: keep generated C portable freestanding bool // -freestanding: target a platform contract without an OS runtime freestanding_hooks []string // -fhooks: explicit freestanding platform capabilities + macos_tiny bool = true // -no-mos-tiny: disable the automatic macOS x64 tiny object path output_file string printfn_list []string // List of function names whose generated C source should be printed user_defines []string // All active comptime flags, including compiler-synthesized flags. @@ -274,6 +275,12 @@ fn validate_target_contract(target_os string, freestanding bool) { } } +fn validate_macos_tiny_flag_contract(options []string, target_os string) ! { + if '-no-mos-tiny' in options && target_os != 'macos' { + return error('-no-mos-tiny requires a macOS target; use -os macos or run on a macOS host') + } +} + fn normalize_freestanding_hook_name(name string) ?string { normalized := name.trim_space().to_lower() return match normalized { @@ -497,6 +504,11 @@ pub fn new_preferences_from_args(args []string) Preferences { add_freestanding_hook_defines(mut all_defines, freestanding_hooks) options := cmdline.only_options(args) + validate_macos_tiny_flag_contract(options, normalized_target_os) or { + eprintln('error: ${err.msg()}') + exit(1) + } + macos_tiny := '-no-mos-tiny' !in options // Parse -ownership flag (must be after options is declared) mut ownership := false @@ -515,7 +527,7 @@ pub fn new_preferences_from_args(args []string) Preferences { '--nocache', '-nomarkused', '--nomarkused', '-showcc', '--showcc', '-stats', '--stats', '-print-parsed-files', '--print-parsed-files', '-keepc', '--profile-alloc', '-profile-alloc', '-enable-globals', '--enable-globals', '-shared', '--shared', '-O0', '--single-backend', - '-single-backend', '-prod', '-prealloc', '-freestanding', '--freestanding'] + '-single-backend', '-prod', '-prealloc', '-freestanding', '--freestanding', '-no-mos-tiny'] $if ownership ? { known_boolean_flags << '-ownership' } @@ -541,6 +553,7 @@ pub fn new_preferences_from_args(args []string) Preferences { eprintln(' -freestanding Generate for a freestanding platform contract') eprintln(' -fhooks Advanced freestanding hooks for --skip-builtin --skip-type-check stubs') eprintln(' Values: output, panic, alloc, minimal') + eprintln(' -no-mos-tiny Disable macOS x64 tiny object output') eprintln(' -O0 Skip SSA optimization (default; use -prod to enable)') $if ownership ? { eprintln(' -ownership Enable ownership checking for strings') @@ -586,6 +599,7 @@ pub fn new_preferences_from_args(args []string) Preferences { output_cross_c: output_cross_c freestanding: freestanding freestanding_hooks: freestanding_hooks + macos_tiny: macos_tiny output_file: output_file printfn_list: printfn_list user_defines: all_defines diff --git a/vlib/v2/pref/pref_test.v b/vlib/v2/pref/pref_test.v index b956a1702..0dae1da90 100644 --- a/vlib/v2/pref/pref_test.v +++ b/vlib/v2/pref/pref_test.v @@ -24,6 +24,47 @@ fn test_new_preferences_from_args_defaults_to_host_target() { assert prefs.explicit_user_defines.len == 0 } +fn test_new_preferences_from_args_parses_macos_tiny_opt_out() { + default_prefs := new_preferences_from_args(['-b', 'x64', '-os', 'macos', 'main.v']) + assert default_prefs.macos_tiny + + opt_out_prefs := + new_preferences_from_args(['-b', 'x64', '-no-mos-tiny', '-os', 'macos', 'main.v']) + assert !opt_out_prefs.macos_tiny +} + +fn test_macos_tiny_opt_out_requires_macos_target() { + validate_macos_tiny_flag_contract(['-no-mos-tiny'], 'macos')! + + for target in ['linux', 'windows', 'cross', 'none'] { + mut got_error := false + validate_macos_tiny_flag_contract(['-no-mos-tiny'], target) or { + got_error = true + assert err.msg().contains('-no-mos-tiny requires a macOS target') + } + assert got_error, '-no-mos-tiny was accepted for target ${target}' + } +} + +fn test_macos_tiny_opt_out_rejection_has_cli_error_prefix() { + tmp_dir := os.join_path(os.vtmp_dir(), 'v2_pref_macos_tiny_error_${os.getpid()}') + os.rmdir_all(tmp_dir) or {} + os.mkdir_all(tmp_dir) or { panic(err) } + defer { + os.rmdir_all(tmp_dir) or {} + } + source_path := os.join_path(tmp_dir, 'main.v') + os.write_file(source_path, + "import v2.pref\n\nfn main() {\n\tpref.new_preferences_from_args(['-no-mos-tiny', '-os', 'linux', 'main.v'])\n}\n") or { + panic(err) + } + vlib_path := os.join_path(os.dir(@VEXE), 'vlib') + res := + os.execute('${os.quoted_path(@VEXE)} -path "${vlib_path}|@vlib|@vmodules" run ${os.quoted_path(source_path)}') + assert res.exit_code == 1, res.output + assert res.output.contains('error: -no-mos-tiny requires a macOS target'), res.output +} + fn test_new_preferences_from_args_parses_target_os() { linux_prefs := new_preferences_from_args(['-os', 'linux', 'main.v']) assert linux_prefs.target_os == 'linux' diff --git a/vlib/v2/transformer/minimal_runtime_init_test.v b/vlib/v2/transformer/minimal_runtime_init_test.v new file mode 100644 index 000000000..11b46a4e8 --- /dev/null +++ b/vlib/v2/transformer/minimal_runtime_init_test.v @@ -0,0 +1,150 @@ +module transformer + +import v2.ast +import v2.pref as vpref +import v2.types + +fn transformer_with_x64_target(target_os string) &Transformer { + env := &types.Environment{} + prefs := &vpref.Preferences{ + backend: .x64 + arch: .x64 + target_os: target_os + } + return Transformer.new_with_pref(env, prefs) +} + +fn transformer_with_macos_tiny_candidate_graph() &Transformer { + mut t := transformer_with_x64_target('macos') + t.enable_macos_tiny_candidate_graph() + return t +} + +fn runtime_init_call_names(stmts []ast.Stmt) []string { + mut names := []string{} + for stmt in stmts { + if stmt is ast.ExprStmt { + if stmt.expr is ast.CallExpr { + if stmt.expr.lhs is ast.Ident { + names << stmt.expr.lhs.name + } + } + } + } + return names +} + +fn runtime_init_call_names_from_flat(mut t Transformer, files []ast.File) []string { + flat := ast.flatten_files(files) + return runtime_init_call_names(t.runtime_const_init_main_calls_parts_from_flat(&flat)) +} + +fn runtime_init_test_files(main_imports []ast.ImportStmt) []ast.File { + return [ + ast.File{ + mod: 'main' + imports: main_imports + stmts: [ + ast.Stmt(ast.FnDecl{ + name: 'main' + }), + ] + }, + ast.File{ + mod: 'os' + stmts: [ + ast.Stmt(ast.FnDecl{ + name: 'init' + }), + ] + }, + ] +} + +fn seed_runtime_const_init_names(mut t Transformer) { + t.runtime_const_modules = ['os', 'main'] + t.runtime_const_init_fn_name['os'] = '__v_init_consts_os' + t.runtime_const_init_fn_name['main'] = '__v_init_consts_main' +} + +fn test_linux_minimal_runtime_filters_unimported_runtime_init_calls() { + mut t := transformer_with_x64_target('linux') + seed_runtime_const_init_names(mut t) + names := + runtime_init_call_names(t.runtime_const_init_main_calls_parts(runtime_init_test_files([]ast.ImportStmt{}))) + assert names == ['__v_init_consts_main'] +} + +fn test_linux_minimal_runtime_keeps_imported_runtime_init_calls() { + mut t := transformer_with_x64_target('linux') + seed_runtime_const_init_names(mut t) + names := runtime_init_call_names(t.runtime_const_init_main_calls_parts(runtime_init_test_files([ + ast.ImportStmt{ + name: 'os' + }, + ]))) + assert names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main'] +} + +fn test_linux_minimal_runtime_filters_unimported_runtime_init_calls_from_flat() { + mut t := transformer_with_x64_target('linux') + seed_runtime_const_init_names(mut t) + names := runtime_init_call_names_from_flat(mut t, runtime_init_test_files([]ast.ImportStmt{})) + assert names == ['__v_init_consts_main'] +} + +fn test_linux_minimal_runtime_keeps_imported_runtime_init_calls_from_flat() { + mut t := transformer_with_x64_target('linux') + seed_runtime_const_init_names(mut t) + names := runtime_init_call_names_from_flat(mut t, runtime_init_test_files([ + ast.ImportStmt{ + name: 'os' + }, + ])) + assert names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main'] +} + +fn test_macos_tiny_candidate_filters_unimported_runtime_init_calls() { + mut t := transformer_with_macos_tiny_candidate_graph() + seed_runtime_const_init_names(mut t) + names := + runtime_init_call_names(t.runtime_const_init_main_calls_parts(runtime_init_test_files([]ast.ImportStmt{}))) + assert names == ['__v_init_consts_main'] +} + +fn test_macos_tiny_candidate_keeps_imported_runtime_init_calls() { + mut t := transformer_with_macos_tiny_candidate_graph() + seed_runtime_const_init_names(mut t) + names := runtime_init_call_names(t.runtime_const_init_main_calls_parts(runtime_init_test_files([ + ast.ImportStmt{ + name: 'os' + }, + ]))) + assert names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main'] +} + +fn test_macos_tiny_candidate_filters_unimported_runtime_init_calls_from_flat() { + mut t := transformer_with_macos_tiny_candidate_graph() + seed_runtime_const_init_names(mut t) + names := runtime_init_call_names_from_flat(mut t, runtime_init_test_files([]ast.ImportStmt{})) + assert names == ['__v_init_consts_main'] +} + +fn test_macos_tiny_candidate_keeps_imported_runtime_init_calls_from_flat() { + mut t := transformer_with_macos_tiny_candidate_graph() + seed_runtime_const_init_names(mut t) + names := runtime_init_call_names_from_flat(mut t, runtime_init_test_files([ + ast.ImportStmt{ + name: 'os' + }, + ])) + assert names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main'] +} + +fn test_non_minimal_x64_runtime_keeps_existing_init_behavior() { + mut t := transformer_with_x64_target('macos') + seed_runtime_const_init_names(mut t) + names := + runtime_init_call_names(t.runtime_const_init_main_calls_parts(runtime_init_test_files([]ast.ImportStmt{}))) + assert names == ['os____v_init_consts_os', 'os__init', '__v_init_consts_main'] +} diff --git a/vlib/v2/transformer/transformer.v b/vlib/v2/transformer/transformer.v index 62f6774ac..5864d2d8e 100644 --- a/vlib/v2/transformer/transformer.v +++ b/vlib/v2/transformer/transformer.v @@ -130,7 +130,8 @@ mut: monomorphized_specs map[string]bool // Cached at construction: avoids per-block t.pref nil/enum re-check in // hot loops (transform_stmts, IfGuardExpr handling, OrExpr expansion, etc). - is_native_be bool + is_native_be bool + macos_tiny_candidate_graph bool } fn escape_c_keyword(name string) string { @@ -294,6 +295,10 @@ pub fn (mut t Transformer) set_file_set(fs &token.FileSet) { t.file_set = unsafe { fs } } +pub fn (mut t Transformer) enable_macos_tiny_candidate_graph() { + t.macos_tiny_candidate_graph = true +} + // new_worker_clone creates a lightweight Transformer that shares read-only state // (env, pref, elided_fns, comptime_vmodroot, file_set) but has its own // accumulator maps for thread-safe per-file transformation. @@ -328,6 +333,7 @@ pub fn (t &Transformer) new_worker_clone(worker_idx int) &Transformer { interface_concrete_types: map[string]string{} smartcast_expr_counts: map[string]int{} is_native_be: t.is_native_be + macos_tiny_candidate_graph: t.macos_tiny_candidate_graph } } @@ -2358,6 +2364,16 @@ fn (t &Transformer) uses_minimal_windows_x64_runtime() bool { && t.pref.target_os_or_host().to_lower() == 'windows' } +fn (t &Transformer) uses_minimal_linux_x64_runtime() bool { + return t.pref != unsafe { nil } && t.pref.backend == .x64 && t.pref.get_effective_arch() == .x64 + && t.pref.target_os_or_host().to_lower() == 'linux' +} + +fn (t &Transformer) uses_minimal_x64_runtime() bool { + return t.uses_minimal_windows_x64_runtime() || t.uses_minimal_linux_x64_runtime() + || t.macos_tiny_candidate_graph +} + fn (t &Transformer) collect_runtime_init_imports_from_if_expr(node ast.IfExpr, mut imports []ast.ImportStmt) { if t.eval_comptime_cond(node.cond) { t.collect_runtime_init_imports_from_stmts(node.stmts, mut imports) @@ -3472,10 +3488,10 @@ fn (mut t Transformer) inject_test_main(mut files []ast.File) { // `__v_init_consts_()` + `__init()` call ExprStmts that should be // prepended to main(): first the runtime-const init calls (one per // `t.runtime_const_modules` entry that has a registered fn_name; gated by -// `uses_minimal_windows_x64_runtime` + `imported_runtime_init_modules` on -// minimal-windows-x64), then the module-level `init()` calls (one per -// module seen in files that declares a non-method `init` fn; gated by the -// same minimal-windows filter; de-duped per module). +// `uses_minimal_x64_runtime` + `imported_runtime_init_modules` on minimal x64 +// runtimes), then the module-level `init()` calls (one per module seen in +// files that declares a non-method `init` fn; gated by the same minimal +// runtime filter; de-duped per module). // // Does NOT mutate `files` or any caller state. Caller decides whether to // splice the result into a legacy `[]ast.File` (via @@ -3489,15 +3505,15 @@ pub fn (mut t Transformer) runtime_const_init_main_calls_parts(files []ast.File) mut init_calls := []ast.Stmt{} mut main_const_calls := []ast.Stmt{} mut seen_init_mods := map[string]bool{} - minimal_windows_x64_runtime := t.uses_minimal_windows_x64_runtime() - init_modules := if minimal_windows_x64_runtime { + minimal_x64_runtime := t.uses_minimal_x64_runtime() + init_modules := if minimal_x64_runtime { t.imported_runtime_init_modules(files) } else { map[string]bool{} } main_mod := main_module_name_from_files(files) for mod in t.runtime_const_modules { - if minimal_windows_x64_runtime && mod !in init_modules { + if minimal_x64_runtime && mod !in init_modules { continue } fn_name := t.runtime_const_init_fn_name[mod] or { continue } @@ -3519,7 +3535,7 @@ pub fn (mut t Transformer) runtime_const_init_main_calls_parts(files []ast.File) if file.mod in seen_init_mods { continue } - if minimal_windows_x64_runtime && file.mod !in init_modules { + if minimal_x64_runtime && file.mod !in init_modules { continue } for stmt in file.stmts { @@ -3629,13 +3645,10 @@ pub fn (mut t Transformer) runtime_const_init_main_calls_parts_from_flat(flat &a mut init_calls := []ast.Stmt{} mut main_const_calls := []ast.Stmt{} mut seen_init_mods := map[string]bool{} - minimal_windows_x64_runtime := t.uses_minimal_windows_x64_runtime() - init_modules := if minimal_windows_x64_runtime { - // Dormant branch (only on minimal_windows_x64 backend); route through - // the legacy file-walker via a one-shot rehydrate. Direct flat port - // would need `active_runtime_init_imports` (comptime-active import - // collector) lifted to cursors, which is non-trivial and not yet - // exercised by any test target. + minimal_x64_runtime := t.uses_minimal_x64_runtime() + init_modules := if minimal_x64_runtime { + // Minimal x64 paths are uncommon and reuse the legacy import collector + // for comptime-active imports until that logic is ported to cursors. rehydrated := flat.to_files_range(0, flat.files.len) t.imported_runtime_init_modules(rehydrated) } else { @@ -3643,7 +3656,7 @@ pub fn (mut t Transformer) runtime_const_init_main_calls_parts_from_flat(flat &a } main_mod := main_module_name_from_flat(flat) for mod in t.runtime_const_modules { - if minimal_windows_x64_runtime && mod !in init_modules { + if minimal_x64_runtime && mod !in init_modules { continue } fn_name := t.runtime_const_init_fn_name[mod] or { continue } @@ -3667,7 +3680,7 @@ pub fn (mut t Transformer) runtime_const_init_main_calls_parts_from_flat(flat &a if mod in seen_init_mods { continue } - if minimal_windows_x64_runtime && mod !in init_modules { + if minimal_x64_runtime && mod !in init_modules { continue } stmts := fc.stmts() -- 2.39.5