From a7153322629091f3f2d79f0bf318d0fb2c13c425 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 6 Jun 2026 16:07:07 +0300 Subject: [PATCH] v2: reduce self-host memory (#27366) --- vlib/v2/abi/abi.v | 69 +++++--- vlib/v2/builder/builder.v | 136 +++++++++------ vlib/v2/gen/arm64/arm64.v | 155 +++++++++++------- .../v2/gen/cleanc/array_append_codegen_test.v | 30 ++++ vlib/v2/gen/cleanc/fn.v | 41 +++++ .../gen/cleanc/result_option_codegen_test.v | 33 ++++ vlib/v2/gen/cleanc/types.v | 13 +- vlib/v2/gen/x64/x64.v | 32 +--- vlib/v2/insel/insel.v | 46 +----- vlib/v2/mir/mir.v | 52 +++--- vlib/v2/ssa/builder.v | 43 +++-- vlib/v2/ssa/module.v | 21 ++- vlib/v2/ssa/skip_modules_test.v | 36 ++++ vlib/v2/transformer/flat_write.v | 18 +- .../propagate_types_from_flat_test.v | 20 +-- vlib/v2/transformer/transformer.v | 88 +++++++++- .../transformer/transformer_flat_diff_test.v | 48 ++++++ .../transformer/transformer_v2_darwin_test.v | 18 +- vlib/v2/transformer/type_propagation.v | 24 +-- vlib/v2/types/checker.v | 140 ++++++++-------- vlib/v2/types/checker_test.v | 65 ++++++-- vlib/v2/types/types.v | 137 ++++++++++++++++ 22 files changed, 875 insertions(+), 390 deletions(-) create mode 100644 vlib/v2/ssa/skip_modules_test.v diff --git a/vlib/v2/abi/abi.v b/vlib/v2/abi/abi.v index 196ff4624..28f5b6b1a 100644 --- a/vlib/v2/abi/abi.v +++ b/vlib/v2/abi/abi.v @@ -31,15 +31,22 @@ pub fn lower(mut m mir.Module, arch pref.Arch) { } pub fn lower_with_x64_abi(mut m mir.Module, arch pref.Arch, x64_abi X64Abi) { + is_x64 := arch == .x64 mut fn_by_name := map[string]int{} for i := 0; i < m.funcs.len; i++ { mut f := &m.funcs[i] fn_by_name[f.name] = i - f.abi_ret_class = abi_value_class(m, f.typ, arch, x64_abi) - f.abi_ret_indirect = abi_class_is_indirect(f.abi_ret_class, m, f.typ, arch, x64_abi) + if is_x64 { + f.abi_ret_class = abi_value_class(m, f.typ, arch, x64_abi) + f.abi_ret_indirect = abi_class_is_indirect(f.abi_ret_class, m, f.typ, arch, x64_abi) + } else { + f.abi_ret_indirect = needs_indirect(m, f.typ, arch, x64_abi) + } f.abi_param_class = []mir.AbiArgClass{len: f.params.len, init: .in_reg} - f.abi_param_classes = []mir.AbiValueClass{len: f.params.len} - f.abi_param_layouts = []mir.AbiValueLayout{len: f.params.len} + if is_x64 { + f.abi_param_classes = []mir.AbiValueClass{len: f.params.len} + f.abi_param_layouts = []mir.AbiValueLayout{len: f.params.len} + } mut param_loc_state := SysVLocationState{} if arch == .x64 && x64_abi == .sysv && f.abi_ret_indirect { param_loc_state.int_regs = 1 @@ -49,12 +56,17 @@ pub fn lower_with_x64_abi(mut m mir.Module, arch pref.Arch, x64_abi X64Abi) { continue } param_typ := m.values[param_id].typ - param_class := abi_value_class(m, param_typ, arch, x64_abi) - f.abi_param_classes[pi] = param_class - if arch == .x64 && x64_abi == .sysv { - f.abi_param_layouts[pi] = sysv_assign_value_layout(param_class, mut param_loc_state) - } - if abi_class_is_indirect(param_class, m, param_typ, arch, x64_abi) { + if is_x64 { + param_class := abi_value_class(m, param_typ, arch, x64_abi) + f.abi_param_classes[pi] = param_class + if x64_abi == .sysv { + f.abi_param_layouts[pi] = sysv_assign_value_layout(param_class, mut + param_loc_state) + } + if abi_class_is_indirect(param_class, m, param_typ, arch, x64_abi) { + f.abi_param_class[pi] = .indirect + } + } else if needs_indirect(m, param_typ, arch, x64_abi) { f.abi_param_class[pi] = .indirect } } @@ -505,6 +517,7 @@ fn lower_calls(mut m mir.Module, arch pref.Arch, x64_abi X64Abi, fn_by_name map[ if arch !in [.arm64, .x64] { return } + is_x64 := arch == .x64 for i := 0; i < m.instrs.len; i++ { mut instr := &m.instrs[i] @@ -512,12 +525,22 @@ fn lower_calls(mut m mir.Module, arch pref.Arch, x64_abi X64Abi, fn_by_name map[ continue } ret_typ, sig_param_types := call_signature(m, instr, fn_by_name) - ret_class := abi_value_class(m, ret_typ, arch, x64_abi) - ret_indirect := abi_class_is_indirect(ret_class, m, ret_typ, arch, x64_abi) + ret_class := if is_x64 { + abi_value_class(m, ret_typ, arch, x64_abi) + } else { + mir.AbiValueClass{} + } + ret_indirect := if is_x64 { + abi_class_is_indirect(ret_class, m, ret_typ, arch, x64_abi) + } else { + needs_indirect(m, ret_typ, arch, x64_abi) + } num_args := instr.operands.len - 1 instr.abi_arg_class = []mir.AbiArgClass{len: num_args, init: .in_reg} - instr.abi_arg_classes = []mir.AbiValueClass{len: num_args} - instr.abi_arg_layouts = []mir.AbiValueLayout{len: num_args} + if is_x64 { + instr.abi_arg_classes = []mir.AbiValueClass{len: num_args} + instr.abi_arg_layouts = []mir.AbiValueLayout{len: num_args} + } mut arg_loc_state := SysVLocationState{} if arch == .x64 && x64_abi == .sysv && ret_indirect { arg_loc_state.int_regs = 1 @@ -530,13 +553,17 @@ fn lower_calls(mut m mir.Module, arch pref.Arch, x64_abi X64Abi, fn_by_name map[ arg_id := instr.operands[arg_idx + 1] arg_typ = fallback_arg_type(m, arg_id) } - arg_class := abi_value_class(m, arg_typ, arch, x64_abi) - instr.abi_arg_classes[arg_idx] = arg_class - if arch == .x64 && x64_abi == .sysv { - instr.abi_arg_layouts[arg_idx] = sysv_assign_value_layout(arg_class, mut - arg_loc_state) - } - if abi_class_is_indirect(arg_class, m, arg_typ, arch, x64_abi) { + if is_x64 { + arg_class := abi_value_class(m, arg_typ, arch, x64_abi) + instr.abi_arg_classes[arg_idx] = arg_class + if x64_abi == .sysv { + instr.abi_arg_layouts[arg_idx] = sysv_assign_value_layout(arg_class, mut + arg_loc_state) + } + if abi_class_is_indirect(arg_class, m, arg_typ, arch, x64_abi) { + instr.abi_arg_class[arg_idx] = .indirect + } + } else if needs_indirect(m, arg_typ, arch, x64_abi) { instr.abi_arg_class[arg_idx] = .indirect } } diff --git a/vlib/v2/builder/builder.v b/vlib/v2/builder/builder.v index 22344f93d..ffb714e4d 100644 --- a/vlib/v2/builder/builder.v +++ b/vlib/v2/builder/builder.v @@ -60,6 +60,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 + // native_flat_pipeline_enabled means the transform phase produced a + // post-transform FlatAst and intentionally did not materialize b.files. + native_flat_pipeline_enabled bool // Source 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 @@ -89,6 +92,20 @@ fn (b &Builder) exec_build_c_file(output_name string) string { return staged_c_file } +fn (b &Builder) should_use_native_flat_pipeline() bool { + if os.getenv('V2_NATIVE_FLAT') == '' { + return false + } + if os.getenv('V2_NO_NATIVE_FLAT') != '' { + return false + } + return b.pref.backend == .arm64 && b.pref.hot_fn.len == 0 +} + +fn (b &Builder) backend_uses_markused_pruning() bool { + return b.pref.backend != .arm64 +} + fn (b &Builder) can_compile_cleanc_locally() bool { if b.pref == unsafe { nil } { return true @@ -175,28 +192,10 @@ fn print_rss(stage string) { eprintln(' [mem] ${stage}: ${rss / (1024 * 1024)} MB') } -// print_heap reports retained heap size after a forced GC, in MB. Unlike -// print_rss, this would give a stable measurement of memory the program -// is actually holding alive — but ONLY when built with a real GC. -// -// CURRENTLY A NO-OP FOR v2 SELF-HOST: v2 is forced to `-gc none`, where -// `gc_collect()` is a NOP and `gc_memory_use()` always returns 0. If -// you build v2 with an explicit GC (bypassing is_v2_compiler_target) this -// becomes useful. Until then, use `/usr/bin/time -l` for peak readings. -fn print_heap(stage string) { - if os.getenv('V2_HEAP') == '' { - return - } - gc_collect() - bytes := gc_memory_use() - eprintln(' [heap] ${stage}: ${bytes / (1024 * 1024)} MB') -} - pub fn (mut b Builder) build(files []string) { b.user_files = files mut sw := time.new_stopwatch() print_rss('start') - print_heap('start') $if parallel ? { if b.flat_roundtrip_enabled && !b.pref.no_parallel { eprintln('warning: V2_FLAT_ROUNDTRIP=1 only routes through the serial parser; pass --no-parallel to exercise it') @@ -212,7 +211,6 @@ pub fn (mut b Builder) build(files []string) { parse_time := sw.elapsed() print_time('Scan & Parse', parse_time) print_rss('after parse') - print_heap('after parse') if b.flat_check_enabled { // FlatBuilder is the canonical parse output; both parse paths stream // into it. parse_files / parse_files_parallel return [] in flat @@ -247,7 +245,6 @@ pub fn (mut b Builder) build(files []string) { type_check_time := time.Duration(sw.elapsed() - parse_time) print_time('Type Check', type_check_time) print_rss('after type check') - print_heap('after type check') b.prepare_macos_tiny_candidate_source_files() @@ -256,7 +253,10 @@ pub fn (mut b Builder) build(files []string) { mut trans := transformer.Transformer.new_with_pref(b.env, b.pref) trans.set_file_set(b.file_set) sequential_transform := b.pref.no_parallel_transform || b.pref.ownership - use_flat_markused := b.markused_flat_enabled && b.flat_check_enabled + use_native_flat_pipeline := b.should_use_native_flat_pipeline() + b.native_flat_pipeline_enabled = use_native_flat_pipeline + use_flat_markused := (b.markused_flat_enabled && b.flat_check_enabled) + || use_native_flat_pipeline // Both paths can now consume flat directly: sequential streams via // transform_files_from_flat, parallel streams per-worker via // to_files_range. No up-front full rehydration needed in either case. @@ -267,7 +267,11 @@ pub fn (mut b Builder) build(files []string) { // flatten_files() pass before mark_used_flat. mut flat_populated_by_transform := false if sequential_transform { - if use_flat_markused { + if use_native_flat_pipeline && !b.flat_check_enabled { + b.flat = trans.transform_files_to_flat_direct(b.files) + b.files = []ast.File{} + flat_populated_by_transform = true + } else if use_flat_markused { new_flat, files_out := trans.transform_files_to_flat(&b.flat, b.files) b.flat = new_flat b.files = files_out @@ -278,7 +282,11 @@ pub fn (mut b Builder) build(files []string) { b.files = trans.transform_files(b.files) } } else { - if use_flat_markused { + if use_native_flat_pipeline && !b.flat_check_enabled { + b.flat = trans.transform_files_to_flat_direct(b.files) + b.files = []ast.File{} + flat_populated_by_transform = true + } else if use_flat_markused { new_flat, files_out := b.transform_files_parallel_to_flat(mut trans) b.flat = new_flat b.files = files_out @@ -292,10 +300,9 @@ pub fn (mut b Builder) build(files []string) { transform_time := time.Duration(sw.elapsed() - transform_start) print_time('Transform', transform_time) print_rss('after transform') - print_heap('after transform') // Mark used functions/methods for backend pruning. - if b.pref.no_markused { + if b.pref.no_markused || !b.backend_uses_markused_pruning() { b.used_fn_keys = map[string]bool{} } else { mark_used_start := sw.elapsed() @@ -331,16 +338,15 @@ pub fn (mut b Builder) build(files []string) { } mark_used_time := time.Duration(sw.elapsed() - mark_used_start) print_time('Mark Used', mark_used_time) - // b.flat is unused by the legacy codegen path; drop the arenas so a GC - // build can reclaim them. Under -gc none this is a no-op for peak memory, - // but it documents the lifetime correctly for the eventual GC switch. - // When V2_FLAT_SSA is on, the native SSA build consumes b.flat directly - // (build_all_from_flat), so keep it alive through codegen. - if !b.flat_ssa_enabled { + // b.flat is unused by the legacy codegen path. Under -gc none this is a + // no-op for peak memory, but keep the lifetime explicit for readers. + // When V2_FLAT_SSA or the native flat pipeline is on, the native SSA + // build consumes b.flat directly (build_all_from_flat), so keep it alive + // through codegen. + if !b.flat_ssa_enabled && !b.native_flat_pipeline_enabled { b.flat = ast.FlatAst{} } print_rss('after markused') - print_heap('after markused') } // Generate output based on backend @@ -373,7 +379,6 @@ pub fn (mut b Builder) build(files []string) { print_time('Total', sw.elapsed()) print_rss('after codegen (peak)') - print_heap('after codegen') } fn (mut b Builder) gen_v_files() { @@ -2456,6 +2461,17 @@ fn (b &Builder) native_mir_build_sequential(label string) bool { return label == macos_tiny_candidate_graph_label || b.pref.no_parallel || b.pref.hot_fn.len > 0 } +fn (b &Builder) should_prune_native_backend_modules(arch pref.Arch) bool { + return b.pref.single_backend || arch == .arm64 +} + +fn native_backend_module_file_fragment(backend_mod string) string { + return match backend_mod { + 'eval' { '/vlib/v2/eval/' } + else { '/vlib/v2/gen/${backend_mod}/' } + } +} + fn (mut b Builder) build_native_mir_from_files(files []ast.File, arch pref.Arch, target_os string, minimal_runtime_roots bool, used_fn_keys map[string]bool, label string) mir.Module { mut mod := ssa.Module.new('main') if mod == unsafe { nil } { @@ -2477,21 +2493,24 @@ fn (mut b Builder) build_native_mir_from_files(files []ast.File, arch pref.Arch, ssa_builder.used_fn_keys = used_fn_keys.clone() } - // --single-backend: strip unused backend modules from the binary - if b.pref.single_backend { - all_backends := ['cleanc', 'eval', 'c', 'x64', 'arm64'] + // Strip unused compiler backend modules before SSA. ARM64 already strips + // these symbols after codegen, so avoid building MIR for them in the first place. + if b.should_prune_native_backend_modules(arch) { + all_backends := ['cleanc', 'eval', 'v', 'c', 'x64', 'arm64'] own := match b.pref.backend { .arm64 { 'arm64' } .x64 { 'x64' } .cleanc { 'cleanc' } + .v { 'v' } .c { 'c' } .eval { 'eval' } - else { '' } } for backend_mod in all_backends { if backend_mod != own { ssa_builder.skip_modules[backend_mod] = true + ssa_builder.skip_module_file_fragments[backend_mod] = + native_backend_module_file_fragment(backend_mod) } } } @@ -2506,15 +2525,19 @@ fn (mut b Builder) build_native_mir_from_files(files []ast.File, arch pref.Arch, // build_all_from_flat on the post-transform b.flat (kept alive above). // Sequential only (build_all_from_flat builds fn bodies in-phase). Default off. // - // b.flat is only POST-TRANSFORM when V2_MARKUSED_FLAT is also on: that path - // routes transform through transform_files_to_flat, which re-flattens the - // transformed files back into b.flat. With V2_CHECK_FLAT but NOT - // V2_MARKUSED_FLAT, b.flat stays the parse-time (pre-transform) flat while the - // transformer only updates b.files, so feeding it to build_all_from_flat would - // skip every transformer rewrite. Require both flags here; otherwise fall back - // to the legacy build_all(files), which uses the post-transform b.files. - if b.flat_ssa_enabled && b.markused_flat_enabled && b.flat_check_enabled && b.flat.files.len > 0 { + // b.flat is only POST-TRANSFORM when either V2_MARKUSED_FLAT has routed + // transform through transform_files_to_flat, or the native flat pipeline has + // emitted transform output directly into FlatAst. With V2_CHECK_FLAT but NOT + // V2_MARKUSED_FLAT, b.flat stays parse-time, so feeding it here would skip + // transformer rewrites. + build_from_flat := b.flat.files.len > 0 + && ((b.flat_ssa_enabled && b.markused_flat_enabled && b.flat_check_enabled) + || (b.native_flat_pipeline_enabled && label == '')) + if build_from_flat { ssa_builder.build_all_from_flat(&b.flat) + // SSA has copied the program into MIR; keep the FlatAst lifetime out of + // the later optimizer and machine-code generator working sets. + b.flat = ast.FlatAst{} } else if b.native_mir_build_sequential(label) { ssa_builder.build_all(files) } else { @@ -2525,8 +2548,12 @@ fn (mut b Builder) build_native_mir_from_files(files []ast.File, arch pref.Arch, b.ssa_build_parallel(mut ssa_builder, files) ssa_builder.generate_vinit() } + if arch == .arm64 { + b.env.release_expr_type_cache_after_ssa() + } print_time(native_graph_stage_title(label, 'SSA Build'), time.Duration(native_sw.elapsed() - stage_start)) + print_rss(native_graph_stage_title(label, 'after SSA build')) stage_start = native_sw.elapsed() ssa_optimization_required := b.native_backend_requires_ssa_optimization(arch) @@ -2543,6 +2570,7 @@ fn (mut b Builder) build_native_mir_from_files(files []ast.File, arch pref.Arch, } print_time(native_graph_stage_title(label, 'SSA Optimize'), time.Duration(native_sw.elapsed() - stage_start)) + print_rss(native_graph_stage_title(label, 'after SSA optimize')) $if debug { // Post-opt SSA verification is useful while debugging the optimizer, but it // is currently noisy enough to block normal self-host builds. Keep it @@ -2592,6 +2620,8 @@ fn (mut b Builder) build_native_mir_from_files(files []ast.File, arch pref.Arch, mut mir_mod := mir.lower_from_ssa(mod) print_time(native_graph_stage_title(label, 'MIR Lower'), time.Duration(native_sw.elapsed() - stage_start)) + mod.release_outer_arenas_after_mir_lower() + print_rss(native_graph_stage_title(label, 'after MIR lower')) stage_start = native_sw.elapsed() if is_windows_x64_native_target(arch, target_os) { @@ -2601,11 +2631,15 @@ fn (mut b Builder) build_native_mir_from_files(files []ast.File, arch pref.Arch, } print_time(native_graph_stage_title(label, 'ABI Lower'), time.Duration(native_sw.elapsed() - stage_start)) + print_rss(native_graph_stage_title(label, 'after ABI lower')) - stage_start = native_sw.elapsed() - insel.select(mut mir_mod, arch) - print_time(native_graph_stage_title(label, 'InsSel'), - time.Duration(native_sw.elapsed() - stage_start)) + if arch != .arm64 { + stage_start = native_sw.elapsed() + insel.select(mut mir_mod, arch) + print_time(native_graph_stage_title(label, 'InsSel'), + time.Duration(native_sw.elapsed() - stage_start)) + print_rss(native_graph_stage_title(label, 'after InsSel')) + } return mir_mod } @@ -2657,6 +2691,8 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { } else { b.gen_arm64_parallel(mut gen) } + gen.release_scratch_after_gen() + mir_mod.release_after_native_codegen() print_time('ARM64 Gen', time.Duration(native_sw.elapsed() - stage_start)) if b.pref.hot_fn.len > 0 { @@ -2686,6 +2722,8 @@ fn (mut b Builder) gen_native(backend_arch pref.Arch) { } else { b.gen_arm64_parallel(mut gen) } + gen.release_scratch_after_gen() + mir_mod.release_after_native_codegen() gen.write_file(obj_file) } else { obj_format := native_x64_object_format_for_os(target_os) diff --git a/vlib/v2/gen/arm64/arm64.v b/vlib/v2/gen/arm64/arm64.v index 9211b3a28..38b6bf432 100644 --- a/vlib/v2/gen/arm64/arm64.v +++ b/vlib/v2/gen/arm64/arm64.v @@ -124,14 +124,6 @@ pub mut: func_abi_ret_indirect []bool func_abi_param_class [][]mir.AbiArgClass func_ref_to_func_idx []int - instr_ops []ssa.OpCode - instr_operands [][]ssa.ValueID - instr_operand0 []ssa.ValueID - instr_operand1 []ssa.ValueID - instr_selected_ops []string - instr_typs []ssa.TypeID - instr_blocks []int - instr_abi_arg_class [][]mir.AbiArgClass type_kinds []ssa.TypeKind type_elem_types []ssa.TypeID type_lens []int @@ -277,6 +269,86 @@ pub fn (mut g Gen) gen() { } } +// release_scratch_after_gen drops codegen lookup/cache tables after all machine +// code and relocations have been emitted into g.macho. +pub fn (mut g Gen) release_scratch_after_gen() { + unsafe { + g.stack_map.free() + g.alloca_offsets.free() + g.block_offsets.free() + g.pending_label_blks.free() + g.pending_label_offs.free() + g.pending_head.free() + g.pending_next.free() + g.reg_map.free() + g.used_regs.free() + g.string_literal_offsets.free() + g.const_cache.free() + g.string_data_cache.free() + g.sumtype_data_heap_allocas.free() + g.type_size_cache.free() + g.type_align_cache.free() + g.type_size_stack.free() + g.type_align_stack.free() + g.struct_field_offset_cache.free() + g.func_by_name.free() + g.global_by_name.free() + g.alloca_ptr_cache.free() + g.block_instrs.free() + g.func_blocks.free() + g.func_params.free() + g.func_typs.free() + g.func_is_c_extern.free() + g.func_abi_ret_indirect.free() + g.func_abi_param_class.free() + g.func_ref_to_func_idx.free() + g.type_kinds.free() + g.type_elem_types.free() + g.type_lens.free() + g.type_is_unsigned.free() + g.fn_starts.free() + g.fn_ends.free() + g.fn_sym_ids.free() + } + g.stack_map = map[int]int{} + g.alloca_offsets = map[int]int{} + g.block_offsets = []int{} + g.pending_label_blks = []int{} + g.pending_label_offs = []int{} + g.pending_head = []int{} + g.pending_next = []int{} + g.reg_map = map[int]int{} + g.used_regs = []int{} + g.string_literal_offsets = map[int]int{} + g.const_cache = map[int]i64{} + g.string_data_cache = map[string]int{} + g.sumtype_data_heap_allocas = map[int]bool{} + g.type_size_cache = []int{} + g.type_align_cache = []int{} + g.type_size_stack = []bool{} + g.type_align_stack = []bool{} + g.struct_field_offset_cache = map[int]int{} + g.func_by_name = map[string]int{} + g.global_by_name = map[string]int{} + g.alloca_ptr_cache = map[int]u8{} + g.cur_blk_instrs = []int{} + g.block_instrs = [][]int{} + g.func_blocks = [][]int{} + g.func_params = [][]int{} + g.func_typs = []ssa.TypeID{} + g.func_is_c_extern = []bool{} + g.func_abi_ret_indirect = []bool{} + g.func_abi_param_class = [][]mir.AbiArgClass{} + g.func_ref_to_func_idx = []int{} + g.type_kinds = []ssa.TypeKind{} + g.type_elem_types = []ssa.TypeID{} + g.type_lens = []int{} + g.type_is_unsigned = []bool{} + g.fn_starts = []int{} + g.fn_ends = []int{} + g.fn_sym_ids = []int{} +} + // gen_pre_pass registers global symbols and builds lookup caches. // Must be called before any gen_func calls. pub fn (mut g Gen) gen_pre_pass() { @@ -329,29 +401,6 @@ pub fn (mut g Gen) gen_pre_pass() { } } - n_instrs := g.mod.instrs.len - g.instr_ops = []ssa.OpCode{len: n_instrs} - g.instr_operands = [][]ssa.ValueID{len: n_instrs} - g.instr_operand0 = []ssa.ValueID{len: n_instrs} - g.instr_operand1 = []ssa.ValueID{len: n_instrs} - g.instr_selected_ops = []string{len: n_instrs} - g.instr_typs = []ssa.TypeID{len: n_instrs} - g.instr_blocks = []int{len: n_instrs} - g.instr_abi_arg_class = [][]mir.AbiArgClass{len: n_instrs} - for ii := 0; ii < n_instrs; ii++ { - g.instr_ops[ii] = g.mod.instrs[ii].op - g.instr_operands[ii] = g.mod.instrs[ii].operands - if g.mod.instrs[ii].operands.len > 0 { - g.instr_operand0[ii] = g.mod.instrs[ii].operands[0] - } - if g.mod.instrs[ii].operands.len > 1 { - g.instr_operand1[ii] = g.mod.instrs[ii].operands[1] - } - g.instr_selected_ops[ii] = g.mod.instrs[ii].selected_op - g.instr_typs[ii] = g.mod.instrs[ii].typ - g.instr_blocks[ii] = g.mod.instrs[ii].block - g.instr_abi_arg_class[ii] = g.mod.instrs[ii].abi_arg_class - } n_types := g.mod.type_store.types.len g.type_kinds = []ssa.TypeKind{len: n_types} g.type_elem_types = []ssa.TypeID{len: n_types} @@ -730,14 +779,6 @@ pub fn (g &Gen) new_worker_clone() &Gen { func_abi_ret_indirect: g.func_abi_ret_indirect.clone() func_abi_param_class: g.func_abi_param_class.clone() func_ref_to_func_idx: g.func_ref_to_func_idx.clone() - instr_ops: g.instr_ops.clone() - instr_operands: g.instr_operands.clone() - instr_operand0: g.instr_operand0.clone() - instr_operand1: g.instr_operand1.clone() - instr_selected_ops: g.instr_selected_ops.clone() - instr_typs: g.instr_typs.clone() - instr_blocks: g.instr_blocks.clone() - instr_abi_arg_class: g.instr_abi_arg_class.clone() type_kinds: g.type_kinds.clone() type_elem_types: g.type_elem_types.clone() type_lens: g.type_lens.clone() @@ -1531,23 +1572,16 @@ pub fn (mut g Gen) gen_func(func_idx int) { fn (mut g Gen) gen_instr(val_id int) { instr_idx := g.mod.values[val_id].index - if instr_idx < 0 || instr_idx >= g.instr_ops.len { + if instr_idx < 0 || instr_idx >= g.mod.instrs.len { return } - instr_operands := g.instr_operands[instr_idx] - instr := mir.Instruction{ - op: g.instr_ops[instr_idx] - operands: instr_operands - selected_op: g.instr_selected_ops[instr_idx] - abi_arg_class: g.instr_abi_arg_class[instr_idx] - typ: g.instr_typs[instr_idx] - block: g.instr_blocks[instr_idx] - } + instr := g.mod.instrs[instr_idx] + instr_operands := instr.operands op := g.selected_opcode(instr) trace_val := g.env_trace_val.len > 0 && (g.env_trace_val == '*' || g.cur_func_name == g.env_trace_val) if trace_val { - eprintln('ARM64 VAL fn=${g.cur_func_name} val=${val_id} opi=${int(op)} off=${g.macho.text_data.len - g.curr_offset} sel=`${instr.selected_op}` ops=${instr_operands}') + eprintln('ARM64 VAL fn=${g.cur_func_name} val=${val_id} opi=${int(op)} off=${g.macho.text_data.len - g.curr_offset} ops=${instr_operands}') } trace_instr := g.env_trace_instr.len > 0 && (g.env_trace_instr == '*' || g.cur_func_name == g.env_trace_instr) @@ -1562,7 +1596,7 @@ fn (mut g Gen) gen_instr(val_id int) { width = typ.width is_unsigned = typ.is_unsigned } - eprintln('ARM64 INSTR fn=${g.cur_func_name} val=${val_id} op=${op} orig=${instr.op} sel=${instr.selected_op} typ=${typ_id} kind=${kind} width=${width} unsigned=${is_unsigned} ops=${instr_operands}') + eprintln('ARM64 INSTR fn=${g.cur_func_name} val=${val_id} op=${op} orig=${instr.op} typ=${typ_id} kind=${kind} width=${width} unsigned=${is_unsigned} ops=${instr_operands}') } if op == .store && g.try_emit_simple_scalar_store(instr_idx) { return @@ -1882,8 +1916,8 @@ fn (mut g Gen) gen_instr(val_id int) { if instr_operands.len < 2 { return } - src_id := g.instr_operand0[instr_idx] - ptr_id := g.instr_operand1[instr_idx] + src_id := instr_operands[0] + ptr_id := instr_operands[1] trace_store := g.env_trace_store.len > 0 && (g.env_trace_store == '*' || g.cur_func_name == g.env_trace_store) // ValueID 0 is the SSA null/invalid sentinel. @@ -5142,18 +5176,22 @@ fn (mut g Gen) gen_instr(val_id int) { } } else { - eprintln('arm64: unknown instruction ${int(op)} (${instr.selected_op}) in fn ${g.cur_func_name}') + eprintln('arm64: unknown instruction ${int(op)} in fn ${g.cur_func_name}') exit(1) } } } fn (mut g Gen) try_emit_simple_scalar_store(instr_idx int) bool { - if instr_idx < 0 || instr_idx >= g.instr_operand0.len || instr_idx >= g.instr_operand1.len { + if instr_idx < 0 || instr_idx >= g.mod.instrs.len { + return false + } + instr := g.mod.instrs[instr_idx] + if instr.operands.len < 2 { return false } - src_id := g.instr_operand0[instr_idx] - ptr_id := g.instr_operand1[instr_idx] + src_id := instr.operands[0] + ptr_id := instr.operands[1] if src_id <= 0 || src_id >= g.mod.values.len || ptr_id <= 0 || ptr_id >= g.mod.values.len { return false } @@ -5219,9 +5257,6 @@ fn (g &Gen) cached_type_elem_type(typ_id ssa.TypeID) ssa.TypeID { @[inline] fn (g &Gen) selected_opcode(instr &mir.Instruction) ssa.OpCode { - // InsSel's textual selected_op round-trips back to instr.op via an - // inverse-identical mapping. Returning instr.op directly skips the - // per-instruction string contains/all_after/match work. return instr.op } diff --git a/vlib/v2/gen/cleanc/array_append_codegen_test.v b/vlib/v2/gen/cleanc/array_append_codegen_test.v index 4cf7f06f1..964b473ce 100644 --- a/vlib/v2/gen/cleanc/array_append_codegen_test.v +++ b/vlib/v2/gen/cleanc/array_append_codegen_test.v @@ -145,6 +145,36 @@ fn main() { assert !csrc.contains('}) | n0') } +fn test_generate_c_wraps_mut_u8_array_param_bitwise_append_value() { + csrc := generate_array_append_c_for_test(' +fn write_int(mut out []u8, value u64, high_bits u8) { + out << high_bits | u8(value) +} +') + assert csrc.contains('array__push(((array*)(out)), &(u8[1]){') + assert !csrc.contains('array__push(((array*)(out)), (high_bits | ((u8)(value))))') +} + +fn test_generate_c_appends_for_mut_pointer_value_to_array() { + csrc := generate_array_append_c_for_test(' +struct PullRequest { +mut: + repo_name string +} + +fn copy(mut prs []PullRequest) []PullRequest { + mut out := []PullRequest{} + for mut pr in prs { + pr.repo_name = "x" + out << pr + } + return out +} +') + assert csrc.contains('array__push(((array*)(&out)), pr);') + assert !csrc.contains('array__push(((array*)(&out)), &(PullRequest[1]){pr});') +} + fn test_generate_c_indexes_local_array_that_shadows_function_name() { csrc := generate_array_append_c_for_test(' fn bytes() []u8 { diff --git a/vlib/v2/gen/cleanc/fn.v b/vlib/v2/gen/cleanc/fn.v index 108febc51..c850f9e81 100644 --- a/vlib/v2/gen/cleanc/fn.v +++ b/vlib/v2/gen/cleanc/fn.v @@ -11414,6 +11414,47 @@ fn (mut g Gen) call_expr(lhs ast.Expr, args []ast.Expr) { continue } } + if c_name == 'array__push' && i == 1 && call_args.len > 0 { + push_arg := call_args[i] + mut handled_array_push_arg := false + if push_arg is ast.PrefixExpr && push_arg.op == .amp { + g.expr(push_arg) + handled_array_push_arg = true + } else if push_arg is ast.ArrayInitExpr { + elem_type := unmangle_c_ptr_type(g.extract_array_elem_type(push_arg.typ)) + elem_expr := if push_arg.exprs.len == 1 { + push_arg.exprs[0] + } else { + ast.empty_expr + } + mut elem_expr_type := g.get_expr_type(elem_expr).trim_space() + if (elem_expr_type == '' || elem_expr_type == 'int') && elem_expr is ast.Ident { + elem_expr_type = + (g.get_local_var_c_type(elem_expr.name) or { '' }).trim_space() + } + if elem_type != '' && elem_expr_type.ends_with('*') + && elem_expr_type.trim_right('*') == elem_type { + g.expr(elem_expr) + } else { + g.sb.write_string('&') + g.expr(push_arg) + } + handled_array_push_arg = true + } else if push_arg !is ast.ArrayInitExpr { + _, mut elem_type := g.array_append_elem_type(call_args[0], push_arg) + if elem_type == '' { + elem_type = g.get_expr_type(push_arg).trim_space() + } + if elem_type == '' { + elem_type = 'int' + } + g.gen_array_push_elem_arg(push_arg, elem_type) + handled_array_push_arg = true + } + if handled_array_push_arg { + continue + } + } if c_name == 'signal' && i == 1 { g.sb.write_string('((void (*)(int))') g.expr(call_args[i]) diff --git a/vlib/v2/gen/cleanc/result_option_codegen_test.v b/vlib/v2/gen/cleanc/result_option_codegen_test.v index ff8132157..5cee4f593 100644 --- a/vlib/v2/gen/cleanc/result_option_codegen_test.v +++ b/vlib/v2/gen/cleanc/result_option_codegen_test.v @@ -1295,6 +1295,39 @@ fn fail() !int { assert !csrc.contains('int _val = e') } +fn test_generate_c_returns_option_or_error_fallback_from_result_function_as_error() { + csrc := generate_result_option_c_for_test(' +interface IError { + msg() string +} + +struct MessageError {} + +fn (err MessageError) msg() string { + return "missing" +} + +fn error(msg string) IError { + return MessageError{} +} + +struct Payload { + value int +} + +fn maybe_payload() ?Payload { + return none +} + +fn fail() !Payload { + return maybe_payload() or { error("missing") } +} + ') + assert csrc.contains('_option_Payload _or_t') + assert csrc.contains('return (_result_Payload){ .is_error=true, .err=') + assert !csrc.contains(' = main__error(') +} + fn test_generate_c_keeps_concrete_error_literal_when_context_is_concrete() { csrc := generate_result_option_c_for_test(' struct MyError {} diff --git a/vlib/v2/gen/cleanc/types.v b/vlib/v2/gen/cleanc/types.v index 4c2401d8f..b2832fd20 100644 --- a/vlib/v2/gen/cleanc/types.v +++ b/vlib/v2/gen/cleanc/types.v @@ -831,18 +831,7 @@ fn (mut g Gen) collect_runtime_aliases() { } // Also use type-checker output so aliases used only in expressions are captured. if g.env != unsafe { nil } { - for typ in g.env.expr_type_values { - if !type_has_valid_data(typ) || typ is types.Void { - continue - } - // Skip top-level aliases from env cache; declarations are collected - // from the AST path above. - if typ is types.Alias { - continue - } - g.collect_aliases_from_type(typ) - } - for typ in g.env.expr_type_neg_values { + for typ in g.env.all_expr_types() { if !type_has_valid_data(typ) || typ is types.Void { continue } diff --git a/vlib/v2/gen/x64/x64.v b/vlib/v2/gen/x64/x64.v index 7cf700352..eded0a891 100644 --- a/vlib/v2/gen/x64/x64.v +++ b/vlib/v2/gen/x64/x64.v @@ -1028,7 +1028,7 @@ fn (mut g Gen) gen_instr(val_id int) { asm_ud2(mut g) } else { - x64_unsupported('op ${op} (${instr.selected_op}) in value ${val_id}') + x64_unsupported('op ${op} in value ${val_id}') } } } @@ -1575,34 +1575,8 @@ fn (mut g Gen) emit_epilogue() { } fn (g Gen) selected_opcode(instr mir.Instruction) ssa.OpCode { - if instr.selected_op == '' { - return instr.op - } - suffix := if instr.selected_op.contains('.') { - instr.selected_op.all_after('.') - } else { - instr.selected_op - } - return match suffix { - 'add_rr' { .add } - 'sub_rr' { .sub } - 'mul_rr' { .mul } - 'sdiv_rr' { .sdiv } - 'and_rr' { .and_ } - 'or_rr' { .or_ } - 'xor_rr' { .xor } - 'load_mr' { .load } - 'store_rm' { .store } - 'call' { .call } - 'call_indirect' { .call_indirect } - 'call_sret' { .call_sret } - 'ret' { .ret } - 'br' { .br } - 'jmp' { .jmp } - 'switch' { .switch_ } - 'copy' { .assign } - else { instr.op } - } + _ = g + return instr.op } fn (mut g Gen) emit_jmp(target_idx int) { diff --git a/vlib/v2/insel/insel.v b/vlib/v2/insel/insel.v index 4242e4b30..59b834c25 100644 --- a/vlib/v2/insel/insel.v +++ b/vlib/v2/insel/insel.v @@ -6,48 +6,12 @@ module insel import v2.mir import v2.pref -import v2.ssa // select performs target-specific instruction selection on MIR. -// The selected opcode is currently encoded as a stable textual tag so later -// layers can consume it without changing existing backend contracts. +// The current native backends consume MIR opcodes directly, so there is no +// target-specific rewrite to apply yet. Keep the API as the insertion point for +// a future real instruction selector. pub fn select(mut m mir.Module, arch pref.Arch) { - prefix := target_prefix(arch) - for i := 0; i < m.instrs.len; i++ { - mut ins := &m.instrs[i] - ins.selected_op = select_op_name(prefix, ins.op) - } -} - -fn target_prefix(arch pref.Arch) string { - return match arch { - .arm64 { 'arm64' } - .x64 { 'x64' } - else { 'native' } - } -} - -fn select_op_name(prefix string, op ssa.OpCode) string { - suffix := match op { - .add { 'add_rr' } - .sub { 'sub_rr' } - .mul { 'mul_rr' } - .sdiv { 'sdiv_rr' } - .and_ { 'and_rr' } - .or_ { 'or_rr' } - .xor { 'xor_rr' } - .load { 'load_mr' } - .store { 'store_rm' } - .call { 'call' } - .call_indirect { 'call_indirect' } - .call_sret { 'call_sret' } - .ret { 'ret' } - .br { 'br' } - .jmp { 'jmp' } - .switch_ { 'switch' } - .assign { 'copy' } - else { op.str() } - } - - return '${prefix}.${suffix}' + _ = m + _ = arch } diff --git a/vlib/v2/mir/mir.v b/vlib/v2/mir/mir.v index 837348073..d1950f165 100644 --- a/vlib/v2/mir/mir.v +++ b/vlib/v2/mir/mir.v @@ -34,7 +34,7 @@ pub fn (k ValueKind) str() string { } } -pub enum AbiArgClass { +pub enum AbiArgClass as u8 { in_reg indirect } @@ -98,7 +98,6 @@ pub struct Instruction { pub mut: op ssa.OpCode operands []ssa.ValueID - selected_op string abi_ret_indirect bool abi_arg_class []AbiArgClass abi_ret_class AbiValueClass @@ -156,16 +155,12 @@ pub mut: globals []ssa.GlobalVar } -fn clone_value_ids(values []ssa.ValueID) []ssa.ValueID { - return values.clone() +fn share_value_ids(values []ssa.ValueID) []ssa.ValueID { + return values } -fn clone_block_ids(blocks []ssa.BlockID) []ssa.BlockID { - return blocks.clone() -} - -fn opcode_label(op ssa.OpCode) string { - return int(op).str() +fn share_block_ids(blocks []ssa.BlockID) []ssa.BlockID { + return blocks } pub fn lower_from_ssa(ssa_mod &ssa.Module) Module { @@ -179,7 +174,7 @@ pub fn lower_from_ssa(ssa_mod &ssa.Module) Module { instrs: []Instruction{len: ssa_mod.instrs.len} blocks: []BasicBlock{len: ssa_mod.blocks.len} funcs: []Function{len: ssa_mod.funcs.len} - globals: ssa_mod.globals.clone() + globals: ssa_mod.globals } for i, val in ssa_mod.values { @@ -189,15 +184,14 @@ pub fn lower_from_ssa(ssa_mod &ssa.Module) Module { index: val.index kind: value_kind_from_ssa(val.kind) name: val.name - uses: clone_value_ids(val.uses) + uses: share_value_ids(val.uses) } } for i, instr in ssa_mod.instrs { mod.instrs[i] = Instruction{ op: instr.op - operands: clone_value_ids(instr.operands) - selected_op: opcode_label(instr.op) + operands: share_value_ids(instr.operands) abi_ret_indirect: false abi_arg_class: []AbiArgClass{} abi_ret_class: AbiValueClass{} @@ -215,9 +209,9 @@ pub fn lower_from_ssa(ssa_mod &ssa.Module) Module { val_id: blk.val_id name: blk.name parent: blk.parent - instrs: clone_value_ids(blk.instrs) - preds: clone_block_ids(blk.preds) - succs: clone_block_ids(blk.succs) + instrs: share_value_ids(blk.instrs) + preds: share_block_ids(blk.preds) + succs: share_block_ids(blk.succs) } } @@ -229,8 +223,8 @@ pub fn lower_from_ssa(ssa_mod &ssa.Module) Module { linkage: f.linkage call_conv: f.call_conv is_c_extern: f.is_c_extern - blocks: clone_block_ids(f.blocks) - params: clone_value_ids(f.params) + blocks: share_block_ids(f.blocks) + params: share_value_ids(f.params) abi_ret_indirect: false abi_param_class: []AbiArgClass{len: f.params.len, init: .in_reg} abi_ret_class: AbiValueClass{} @@ -242,6 +236,26 @@ pub fn lower_from_ssa(ssa_mod &ssa.Module) Module { return mod } +// release_after_native_codegen releases MIR storage after native codegen has +// copied everything it needs into the object writer/linker. +pub fn (mut m Module) release_after_native_codegen() { + unsafe { + m.values.free() + m.instrs.free() + m.blocks.free() + m.funcs.free() + m.globals.free() + m.type_store.types.free() + m.type_store.cache.free() + } + m.values = []Value{} + m.instrs = []Instruction{} + m.blocks = []BasicBlock{} + m.funcs = []Function{} + m.globals = []ssa.GlobalVar{} + m.type_store = ssa.TypeStore{} +} + pub fn (m &Module) ssa() &ssa.Module { return m.ssa_mod } diff --git a/vlib/v2/ssa/builder.v b/vlib/v2/ssa/builder.v index c046cefc2..ee061a035 100644 --- a/vlib/v2/ssa/builder.v +++ b/vlib/v2/ssa/builder.v @@ -148,6 +148,9 @@ pub mut: used_fn_keys map[string]bool // When set, skip all functions from these modules (dead code elimination for unused backends). skip_modules map[string]bool + // Optional path fragments for skip_modules. When populated for a module, + // the skip only applies to files whose path matches the fragment. + skip_module_file_fragments map[string]string // Native self-hosted builds use the SSA sumtype layout directly; guard // against null large-variant payloads before matching types.Type. guard_invalid_type_payloads bool @@ -245,6 +248,8 @@ pub fn Builder.new_with_env(mod &Module, env &types.Environment) &Builder { fn_param_array_elem_types: map[string][]TypeID{} fn_refs: map[string]ValueID{} global_refs: map[string]ValueID{} + skip_modules: map[string]bool{} + skip_module_file_fragments: map[string]string{} option_wrapper_types: map[string]TypeID{} result_wrapper_types: map[string]TypeID{} array_value_elem_types: map[int]TypeID{} @@ -4562,7 +4567,7 @@ pub fn (mut b Builder) build_fn_bodies_from_flat(file_cursor ast.FileCursor) { // When used_fn_keys is populated (markused ran), only reachable functions are built. pub fn (mut b Builder) should_build_fn(file_name string, decl ast.FnDecl) bool { // Skip entire modules for unused backends (e.g., cleanc/eval/x64 when building arm64-only) - if b.skip_modules.len > 0 && b.cur_module in b.skip_modules { + if b.should_skip_module_file(file_name) { return false } if b.used_fn_keys.len == 0 { @@ -4601,6 +4606,24 @@ pub fn (mut b Builder) should_build_fn(file_name string, decl ast.FnDecl) bool { return key in b.used_fn_keys } +fn (b &Builder) should_skip_module_file(file_name string) bool { + if b.skip_modules.len == 0 || b.cur_module !in b.skip_modules { + return false + } + if b.skip_module_file_fragments.len == 0 { + return true + } + fragment := b.skip_module_file_fragments[b.cur_module] or { return false } + normalized := file_name.replace('\\', '/') + if normalized.contains(fragment) { + return true + } + if fragment.len > 0 && fragment[0] == `/` { + return normalized.starts_with(fragment[1..]) + } + return false +} + pub fn (mut b Builder) build_fn(decl ast.FnDecl) { fn_name := b.mangle_fn_name(decl) // Skip C-language extern functions without bodies @@ -7042,7 +7065,7 @@ fn (mut b Builder) build_if_stmt(node ast.IfExpr) { mut saved_local_smartcasts := b.local_smartcasts.clone() b.apply_local_smartcasts(then_smartcasts) b.build_stmts(node.stmts) - b.local_smartcasts = saved_local_smartcasts.clone() + b.local_smartcasts = saved_local_smartcasts.move() if !b.block_has_terminator(b.cur_block) { b.mod.add_instr(.jmp, b.cur_block, 0, [b.mod.blocks[merge_block].val_id]) b.add_edge(b.cur_block, merge_block) @@ -7069,8 +7092,6 @@ fn (mut b Builder) build_if_stmt(node ast.IfExpr) { b.add_edge(b.cur_block, merge_block) } } - b.local_smartcasts = saved_local_smartcasts.move() - b.cur_block = merge_block // If the merge block has no predecessors (both branches returned/jumped elsewhere), // mark it as unreachable so no implicit return is added @@ -7113,7 +7134,7 @@ fn (mut b Builder) build_if_stmt_from_flat(c ast.Cursor) { for i in 2 .. c.edge_count() { b.build_stmt_from_flat(c.edge(i)) } - b.local_smartcasts = saved_local_smartcasts.clone() + b.local_smartcasts = saved_local_smartcasts.move() if !b.block_has_terminator(b.cur_block) { b.mod.add_instr(.jmp, b.cur_block, 0, [b.mod.blocks[merge_block].val_id]) b.add_edge(b.cur_block, merge_block) @@ -7140,8 +7161,6 @@ fn (mut b Builder) build_if_stmt_from_flat(c ast.Cursor) { b.add_edge(b.cur_block, merge_block) } } - b.local_smartcasts = saved_local_smartcasts.move() - b.cur_block = merge_block if b.mod.blocks[merge_block].preds.len == 0 { b.mod.add_instr(.unreachable, b.cur_block, 0, []ValueID{}) @@ -13259,7 +13278,7 @@ fn (mut b Builder) get_receiver_type_name(expr ast.Expr) string { if expr is ast.Ident { // Try to get the type from the SSA variable's alloca type. // This is more reliable than env.get_expr_type in ARM64-compiled binaries - // where the type checker's expr_type_values may have corrupt entries. + // where the type checker's expression type cache may have corrupt entries. if var_id := b.vars[expr.name] { mut var_type := b.mod.values[var_id].typ // Alloca types are ptr(T), unwrap the pointer to get base type @@ -14683,7 +14702,7 @@ fn (mut b Builder) build_if_expr(node ast.IfExpr) ValueID { b.build_stmt(last) } } - b.local_smartcasts = saved_local_smartcasts.clone() + b.local_smartcasts = saved_local_smartcasts.move() then_end_block := b.cur_block mut then_reaches_merge := false if !b.block_has_terminator(b.cur_block) { @@ -14726,8 +14745,6 @@ fn (mut b Builder) build_if_expr(node ast.IfExpr) ValueID { else_reaches_merge = true } } - b.local_smartcasts = saved_local_smartcasts.move() - // Merge block: use phi to select result (no alloca/store/load) b.cur_block = merge_block if b.mod.blocks[merge_block].preds.len == 0 { @@ -14826,7 +14843,7 @@ fn (mut b Builder) build_if_expr_from_flat(c ast.Cursor) ValueID { b.build_stmt_from_flat(last_c) } } - b.local_smartcasts = saved_local_smartcasts.clone() + b.local_smartcasts = saved_local_smartcasts.move() then_end_block := b.cur_block mut then_reaches_merge := false if !b.block_has_terminator(b.cur_block) { @@ -14870,8 +14887,6 @@ fn (mut b Builder) build_if_expr_from_flat(c ast.Cursor) ValueID { else_reaches_merge = true } } - b.local_smartcasts = saved_local_smartcasts.move() - // Merge block: use phi to select result (no alloca/store/load) b.cur_block = merge_block if b.mod.blocks[merge_block].preds.len == 0 { diff --git a/vlib/v2/ssa/module.v b/vlib/v2/ssa/module.v index 40ebde4dc..9277ccee3 100644 --- a/vlib/v2/ssa/module.v +++ b/vlib/v2/ssa/module.v @@ -65,6 +65,23 @@ pub fn Module.new(name string) &Module { return m } +// release_outer_arenas_after_mir_lower releases SSA arena buffers that MIR has +// copied by value. Nested slices (value uses, instruction operands, block edges, +// function params/blocks) are intentionally not freed because MIR shallow-shares +// them after lowering. +pub fn (mut m Module) release_outer_arenas_after_mir_lower() { + unsafe { + m.values.free() + m.instrs.free() + m.blocks.free() + m.funcs.free() + } + m.values = []Value{} + m.instrs = []Instruction{} + m.blocks = []BasicBlock{} + m.funcs = []Function{} +} + pub fn (mut m Module) new_function(name string, ret TypeID, params []TypeID) int { // Check if function already exists (avoid duplicates from multiple files) for i, f in m.funcs { @@ -203,7 +220,7 @@ pub fn (mut m Module) add_instr(op OpCode, block BlockID, typ TypeID, operands [ m.instrs << instr // 2. Pass instr_idx to Value - val_id := m.add_value_node(.instruction, typ, 'v${m.values.len}', instr_idx) + val_id := m.add_value_node(.instruction, typ, '', instr_idx) // 3. Link Block — read whole struct, modify, write back (chained broken in ARM64) mut blk := m.blocks[block] @@ -291,7 +308,7 @@ pub fn (mut m Module) add_instr_front(op OpCode, block BlockID, typ TypeID, oper operands: operands } m.instrs << instr - val_id := m.add_value_node(.instruction, typ, 'v${m.values.len}', instr_idx) + val_id := m.add_value_node(.instruction, typ, '', instr_idx) // Prepend to block instructions — read whole struct, modify, write back (chained broken in ARM64) mut blk := m.blocks[block] diff --git a/vlib/v2/ssa/skip_modules_test.v b/vlib/v2/ssa/skip_modules_test.v new file mode 100644 index 000000000..7a6f82e0e --- /dev/null +++ b/vlib/v2/ssa/skip_modules_test.v @@ -0,0 +1,36 @@ +// 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 ssa + +import v2.ast + +fn test_skip_modules_with_file_fragment_does_not_drop_user_modules() { + mut mod := Module.new('skip_modules_test') + mut b := Builder.new(mod) + b.cur_module = 'c' + b.skip_modules['c'] = true + b.skip_module_file_fragments['c'] = '/vlib/v2/gen/c/' + + decl := ast.FnDecl{ + name: 'user_fn' + } + assert b.should_build_fn('/tmp/project/c/user.v', decl) + assert !b.should_build_fn('/Users/me/code/v/vlib/v2/gen/c/c.v', decl) + assert !b.should_build_fn('../../vlib/v2/gen/c/c.v', decl) +} + +fn test_skip_modules_with_file_fragment_matches_eval_backend_path() { + mut mod := Module.new('skip_modules_eval_test') + mut b := Builder.new(mod) + b.cur_module = 'eval' + b.skip_modules['eval'] = true + b.skip_module_file_fragments['eval'] = '/vlib/v2/eval/' + + decl := ast.FnDecl{ + name: 'user_fn' + } + assert b.should_build_fn('/tmp/project/eval/user.v', decl) + assert !b.should_build_fn('/Users/me/code/v/vlib/v2/eval/eval.v', decl) + assert !b.should_build_fn('../../vlib/v2/eval/eval.v', decl) +} diff --git a/vlib/v2/transformer/flat_write.v b/vlib/v2/transformer/flat_write.v index dc0f1d1c4..3b76da878 100644 --- a/vlib/v2/transformer/flat_write.v +++ b/vlib/v2/transformer/flat_write.v @@ -1709,7 +1709,14 @@ pub fn (mut t Transformer) transform_file_index_to_flat(input_flat &ast.FlatAst, if src_arr.len == 0 { return ast.invalid_flat_node_id } - file := src_arr[0] + return t.transform_file_to_flat(src_arr[0], mut out) +} + +// transform_file_to_flat transforms one legacy file and appends the transformed +// tree directly to `out`. It is the AST-input counterpart to +// `transform_file_index_to_flat`, used by the native low-memory pipeline after +// whole-program generic preparation has produced the concrete file list. +pub fn (mut t Transformer) transform_file_to_flat(file ast.File, mut out ast.FlatBuilder) ast.FlatNodeId { // Mirror transform_file's per-file prologue. transform_stmt and the // rewrite sites read these fields to resolve cross-stmt references. t.cur_file_name = file.name @@ -4291,7 +4298,14 @@ pub fn (mut t Transformer) transform_stmt_to_flat(stmt ast.Stmt, mut out ast.Fla // helper. lowered_stmt := t.fn_decl_with_implicit_veb_context_param(stmt) attrs, stmt_ids := t.transform_fn_decl_parts_to_flat(lowered_stmt, mut out) - receiver_id := out.emit_parameter(lowered_stmt.receiver) + receiver := if lowered_stmt.is_method { + lowered_stmt.receiver + } else { + ast.Parameter{ + typ: ast.empty_expr + } + } + receiver_id := out.emit_parameter(receiver) typ_id := out.emit_type(ast.Type(lowered_stmt.typ)) attrs_id := out.emit_attribute_list(attrs) stmts_list_id := out.emit_aux_list_from_ids(stmt_ids) diff --git a/vlib/v2/transformer/propagate_types_from_flat_test.v b/vlib/v2/transformer/propagate_types_from_flat_test.v index 814c1fa4b..69e001cca 100644 --- a/vlib/v2/transformer/propagate_types_from_flat_test.v +++ b/vlib/v2/transformer/propagate_types_from_flat_test.v @@ -61,9 +61,9 @@ fn make_propagate_test_files() []ast.File { fn test_propagate_types_from_flat_empty_flat_is_noop() { mut t := create_propagate_test_transformer(.cleanc) flat := ast.FlatAst{} - pre := t.env.expr_type_values.len + pre := t.env.expr_type_count() t.propagate_types_from_flat(&flat) - assert t.env.expr_type_values.len == pre + assert t.env.expr_type_count() == pre } // On a non-empty flat, propagate_types_from_flat visits the same files as @@ -79,8 +79,8 @@ fn test_propagate_types_from_flat_matches_legacy_for_simple_files() { t_flat.propagate_types_from_flat(&flat) // Both transformers should end with identical env state. With no - // expressions to propagate, both expr_type_values arrays stay empty. - assert t_legacy.env.expr_type_values.len == t_flat.env.expr_type_values.len + // expressions to propagate, both expression type maps stay empty. + assert t_legacy.env.expr_type_count() == t_flat.env.expr_type_count() // cur_module is set to the last visited file's mod on both paths. assert t_legacy.cur_module == t_flat.cur_module } @@ -109,10 +109,10 @@ fn test_apply_post_pass_tail_from_flat_matches_legacy_arm64_skip() { t_legacy.apply_post_pass_tail(files) t_flat.apply_post_pass_tail_from_flat(&flat) - // expr_type_values must contain the synth_types[42] entry on both. - assert t_legacy.env.expr_type_values.len > 42 - assert t_flat.env.expr_type_values.len > 42 - assert t_legacy.env.expr_type_values.len == t_flat.env.expr_type_values.len + // Expression type maps must contain the synth_types[42] entry on both. + assert t_legacy.env.has_expr_type(42) + assert t_flat.env.has_expr_type(42) + assert t_legacy.env.expr_type_count() == t_flat.env.expr_type_count() // fn_scopes must have the seeded key on both. lock t_legacy.env.fn_scopes { assert 'Foo__bar' in t_legacy.env.fn_scopes @@ -134,7 +134,7 @@ fn test_apply_post_pass_tail_from_flat_matches_legacy_cleanc() { t_flat.apply_post_pass_tail_from_flat(&flat) // Both paths should end with identical env state. With a stmt-free - // fixture, propagate_types is a no-op and expr_type_values stays empty. - assert t_legacy.env.expr_type_values.len == t_flat.env.expr_type_values.len + // fixture, propagate_types is a no-op and expression type maps stay empty. + assert t_legacy.env.expr_type_count() == t_flat.env.expr_type_count() assert t_legacy.cur_module == t_flat.cur_module } diff --git a/vlib/v2/transformer/transformer.v b/vlib/v2/transformer/transformer.v index db438d961..b87a407b1 100644 --- a/vlib/v2/transformer/transformer.v +++ b/vlib/v2/transformer/transformer.v @@ -229,6 +229,32 @@ fn (t &Transformer) enum_member_ident_for_lookup(lookup_name string, typ types.E return enum_member_ident(t.enum_type_c_name_for_lookup(lookup_name, typ), field_name) } +fn (t &Transformer) skip_native_backend_transform_file(file ast.File) bool { + if t.pref == unsafe { nil } { + return false + } + own := match t.pref.backend { + .arm64 { 'arm64' } + .x64 { 'x64' } + else { return false } + } + + if !t.pref.single_backend && own != 'arm64' { + return false + } + + name := file.name.replace('\\', '/') + if !name.contains('/v2/gen/') { + return false + } + for backend_mod in ['cleanc', 'eval', 'v', 'c', 'x64', 'arm64'] { + if backend_mod != own && name.contains('/v2/gen/${backend_mod}/') { + return true + } + } + return false +} + struct LiveFn { decl_name string // e.g., 'frame' or 'update_model' mangled_name string // e.g., 'frame' or 'Game__update_model' @@ -2539,6 +2565,59 @@ pub fn (mut t Transformer) transform_files_to_flat_via_driver(flat &ast.FlatAst, return builder.flat, result } +// transform_files_to_flat_direct transforms `files` into a FlatAst without +// collecting the transformed legacy []ast.File result. The input still goes +// through the existing whole-program generic preparation, but each prepared file +// is emitted through transform_file_to_flat and is then immediately consumed by +// the FlatBuilder. This is the low-memory path used by native backends that can +// consume post-transform FlatAst directly. +pub fn (mut t Transformer) transform_files_to_flat_direct(files []ast.File) ast.FlatAst { + timing := os.getenv('V2_TTIME') != '' + mut sw := time.new_stopwatch() + t_print_mem('enter') + t.pre_pass(files) + t_print_mem('after pre_pass') + if timing { + eprintln(' [ttime] flat direct pre_pass: ${sw.elapsed().milliseconds()}ms') + sw = time.new_stopwatch() + } + files_to_transform := t.prepare_files_for_transform(files) + t_print_mem('after prepare/monomorphize') + if timing { + eprintln(' [ttime] flat direct prepare: ${sw.elapsed().milliseconds()}ms') + sw = time.new_stopwatch() + } + mut builder := new_transform_output_flat_builder(files_to_transform) + for file in files_to_transform { + t.transform_file_to_flat(file, mut builder) + } + t_print_mem('after per-file flat loop') + if timing { + eprintln(' [ttime] flat direct per-file: ${sw.elapsed().milliseconds()}ms') + sw = time.new_stopwatch() + } + generated_parts := t.generated_fns_parts_from_flat(&builder.flat) + t.post_pass_to_flat(mut builder, generated_parts) + t.apply_post_pass_tail_from_flat(&builder.flat) + t_print_mem('after post_pass') + if timing { + eprintln(' [ttime] flat direct post_pass: ${sw.elapsed().milliseconds()}ms') + } + return builder.flat +} + +fn new_transform_output_flat_builder(files []ast.File) ast.FlatBuilder { + mut total_bytes := i64(0) + for file in files { + if file.name == '' || !os.exists(file.name) { + continue + } + total_bytes += os.file_size(file.name) + } + nodes_cap, edges_cap, strings_cap := ast.arena_caps_for_bytes(total_bytes * 2) + return ast.new_flat_builder_with_capacity(nodes_cap, edges_cap, strings_cap) +} + fn runtime_const_init_base_name(mod string) string { mut suffix := if mod == '' { 'main' } else { mod } suffix = suffix.replace('.', '_').replace('-', '_') @@ -4011,6 +4090,9 @@ pub fn (mut t Transformer) inject_main_runtime_const_init_to_flat(mut out ast.Fl } fn (mut t Transformer) transform_file(file ast.File) ast.File { + if t.skip_native_backend_transform_file(file) { + return file + } t.enter_file_context(file) stmts := t.transform_stmts(file.stmts) return ast.File{ @@ -6137,7 +6219,8 @@ fn (mut t Transformer) expand_direct_or_expr_assign(stmt ast.AssignStmt, or_expr if or_side_effect_stmts.len > 0 { if_stmts << or_side_effect_stmts } - if is_result && t.cur_fn_returns_result && t.or_fallback_is_ierror(or_payload_value) { + if (is_result || is_option) && t.cur_fn_returns_result + && t.or_fallback_is_ierror(or_payload_value) { if_stmts << ast.ReturnStmt{ exprs: [or_payload_value] } @@ -8771,7 +8854,8 @@ fn (mut t Transformer) expand_single_or_expr(or_expr ast.OrExpr, mut prefix_stmt if_stmts << or_side_effect_stmts } // Error/void fallbacks can't be assigned into the success payload. - if is_result && t.cur_fn_returns_result && t.or_fallback_is_ierror(or_payload_value) { + if (is_result || is_option) && t.cur_fn_returns_result + && t.or_fallback_is_ierror(or_payload_value) { if_stmts << ast.ReturnStmt{ exprs: [or_payload_value] } diff --git a/vlib/v2/transformer/transformer_flat_diff_test.v b/vlib/v2/transformer/transformer_flat_diff_test.v index 5b8a19969..456ea4325 100644 --- a/vlib/v2/transformer/transformer_flat_diff_test.v +++ b/vlib/v2/transformer/transformer_flat_diff_test.v @@ -1785,6 +1785,54 @@ fn test_per_file_parity_for_in_map() { run_per_file_parity('per_file_for_in_map', fixture_for_in_map) } +// --- parity: transform_files_to_flat_direct per-file subtree vs legacy --- +// +// `transform_files_to_flat_direct` is the native low-memory path: it keeps the +// existing whole-program prepare/monomorphize step, but emits each transformed +// file directly into a FlatBuilder instead of collecting a transformed +// []ast.File result. Its observable tree must match legacy transform output. +fn run_to_flat_direct_parity(label string, src string) { + p := parse_transformer_fixture(src) + leg := run_legacy_transform(p) + leg_flat := ast.flatten_files(leg) + + mut t := Transformer.new_with_pref(p.env, p.prefs) + new_flat := t.transform_files_to_flat_direct(p.files) + + if leg_flat.files.len != new_flat.files.len { + assert false, '${label}: file count mismatch: legacy=${leg_flat.files.len} direct=${new_flat.files.len}' + return + } + + for i in 0 .. leg_flat.files.len { + leg_stmts := leg_flat.child_at(leg_flat.files[i].file_id, 2) + new_stmts := new_flat.child_at(new_flat.files[i].file_id, 2) + leg_sub_sig := leg_flat.subtree_signature(leg_stmts) + new_sub_sig := new_flat.subtree_signature(new_stmts) + if leg_sub_sig == new_sub_sig { + continue + } + pa, pb := dump_signature_pair('${label}_file${i}', leg_sub_sig, new_sub_sig) + eprintln('[${label}] transform_files_to_flat_direct file ${i} subtree diverged from legacy.') + eprintln(' legacy: ${pa}') + eprintln(' direct: ${pb}') + eprintln(' diff with: diff -u ${pa} ${pb}') + assert false, '${label}: transform_files_to_flat_direct output diverged at file ${i} (see /tmp dumps above)' + } +} + +fn test_to_flat_direct_parity_plain_fn() { + run_to_flat_direct_parity('to_flat_direct_plain_fn', fixture_plain_fn) +} + +fn test_to_flat_direct_parity_if_guard_assign() { + run_to_flat_direct_parity('to_flat_direct_if_guard_assign', fixture_if_guard_assign) +} + +fn test_to_flat_direct_parity_for_in_map() { + run_to_flat_direct_parity('to_flat_direct_for_in_map', fixture_for_in_map) +} + // --- parity: transform_files_to_flat_via_driver per-file subtree vs legacy --- // // `transform_files_to_flat_via_driver` is the s162 wedge that routes through diff --git a/vlib/v2/transformer/transformer_v2_darwin_test.v b/vlib/v2/transformer/transformer_v2_darwin_test.v index a4d0d5ce9..e09395c3c 100644 --- a/vlib/v2/transformer/transformer_v2_darwin_test.v +++ b/vlib/v2/transformer/transformer_v2_darwin_test.v @@ -203,23 +203,7 @@ fn get_v_files_from_dir(dir string) []string { // filtering all Void types, so expressions explicitly typed as void (Void(0)) are // correctly recognized as having a type. fn (c &ExprTypeChecker) has_type(id int) bool { - if id > 0 && id < c.env.expr_type_values.len { - typ := c.env.expr_type_values[id] - if typ is types.Void { - return u8(typ) != 1 - } - return true - } else if id < 0 { - idx := -id - if idx < c.env.expr_type_neg_values.len { - typ := c.env.expr_type_neg_values[idx] - if typ is types.Void { - return u8(typ) != 1 - } - return true - } - } - return false + return c.env.has_expr_type(id) } fn (mut c ExprTypeChecker) check_expr(expr ast.Expr) { diff --git a/vlib/v2/transformer/type_propagation.v b/vlib/v2/transformer/type_propagation.v index 596982e26..d9083af17 100644 --- a/vlib/v2/transformer/type_propagation.v +++ b/vlib/v2/transformer/type_propagation.v @@ -321,29 +321,7 @@ fn (mut t Transformer) prop_expr(expr ast.Expr) { // has_prop_type checks if the environment has a type set for the given expression ID. fn (t &Transformer) has_prop_type(id int) bool { - if usize(t.env.expr_type_values.data) > 0 && usize(t.env.expr_type_values.data) < 4096 { - eprintln('HAS_PROP_TYPE bad env expr_type_values data=${usize(t.env.expr_type_values.data)} len=${t.env.expr_type_values.len} cap=${t.env.expr_type_values.cap} off=${t.env.expr_type_values.offset} flags=${t.env.expr_type_values.flags} esz=${t.env.expr_type_values.element_size} id=${id}') - } - if usize(t.env.expr_type_neg_values.data) > 0 && usize(t.env.expr_type_neg_values.data) < 4096 { - eprintln('HAS_PROP_TYPE bad env expr_type_neg_values data=${usize(t.env.expr_type_neg_values.data)} len=${t.env.expr_type_neg_values.len} cap=${t.env.expr_type_neg_values.cap} off=${t.env.expr_type_neg_values.offset} flags=${t.env.expr_type_neg_values.flags} esz=${t.env.expr_type_neg_values.element_size} id=${id}') - } - if id > 0 && id < t.env.expr_type_values.len { - typ := t.env.expr_type_values[id] - if typ is types.Void { - return u8(typ) != 1 - } - return true - } else if id < 0 { - idx := -id - if idx < t.env.expr_type_neg_values.len { - typ := t.env.expr_type_neg_values[idx] - if typ is types.Void { - return u8(typ) != 1 - } - return true - } - } - return false + return t.env.has_expr_type(id) } // infer_prop_type tries to determine the type of an expression from its content. diff --git a/vlib/v2/types/checker.v b/vlib/v2/types/checker.v index b84d0cd33..7956d901b 100644 --- a/vlib/v2/types/checker.v +++ b/vlib/v2/types/checker.v @@ -26,12 +26,12 @@ pub mut: methods shared map[string][]&Fn = map[string][]&Fn{} generic_types map[string][]map[string]Type cur_generic_types []map[string]Type - // Expression types - indexed directly by pos.id. - // Positive IDs (1-based) come from the parser via token.Pos.id. - // Negative IDs come from transformer's synthesized nodes. - expr_type_values []Type // indexed by pos.id for positive IDs - expr_type_neg_values []Type // indexed by -pos.id for negative IDs (synth nodes) - selector_names map[int]string + // Expression types keyed by token.Pos.id. Positive IDs come from the parser; + // negative IDs come from transformer's synthesized nodes. This must stay sparse: + // parser position IDs are not dense enough to use as direct array indexes in + // large self-host builds. + expr_types map[int]Type + selector_names map[int]string // Drop-codegen handoff: per-fn list of bindings whose `drop(mut self)` // method must be called at the fn's natural exit, in declaration order. // Populated by `ownership_snapshot_drops_at_fn_exit` after each fn body @@ -58,70 +58,59 @@ pub mut: pub fn Environment.new() &Environment { return &Environment{ - expr_type_values: []Type{cap: 100_000} - selector_names: map[int]string{} - c_scope: new_scope(unsafe { nil }) - c_scope_mu: sync.new_mutex() + expr_types: map[int]Type{} + selector_names: map[int]string{} + c_scope: new_scope(unsafe { nil }) + c_scope_mu: sync.new_mutex() } } // set_expr_type stores the computed type for an expression by its unique ID. pub fn (mut e Environment) set_expr_type(id int, typ Type) { - if id >= 0 { - if id >= e.expr_type_values.len { - // Grow with 2x strategy to amortize reallocation cost - mut new_len := if e.expr_type_values.len < 100_000 { - 100_000 - } else { - e.expr_type_values.len * 2 - } - if new_len <= id { - new_len = id + 1 - } - for e.expr_type_values.len < new_len { - // Void(1) is the sentinel for "unset"; Void(0) is valid void type. - e.expr_type_values << Type(Void(1)) - } - } - e.expr_type_values[id] = typ - } else { - idx := -id - if idx >= e.expr_type_neg_values.len { - mut new_len := if e.expr_type_neg_values.len == 0 { - 64 - } else { - e.expr_type_neg_values.len * 2 - } - if new_len <= idx { - new_len = idx + 1 - } - for e.expr_type_neg_values.len < new_len { - e.expr_type_neg_values << Type(Void(1)) - } - } - e.expr_type_neg_values[idx] = typ - } + e.expr_types[id] = typ } // get_expr_type retrieves the computed type for an expression by its unique ID. pub fn (e &Environment) get_expr_type(id int) ?Type { - if id > 0 && id < e.expr_type_values.len { - typ := e.expr_type_values[id] - if typ is Void || type_has_null_data(typ) { - return none - } - return typ - } else if id < 0 { - idx := -id - if idx < e.expr_type_neg_values.len { - typ := e.expr_type_neg_values[idx] - if typ is Void || type_has_null_data(typ) { - return none - } - return typ - } + typ := e.expr_types[id] or { return none } + if typ is Void || type_has_null_data(typ) { + return none } - return none + return typ +} + +// has_expr_type reports whether an expression has a stored type. Unlike +// get_expr_type, it treats an explicit `void` type as present. +pub fn (e &Environment) has_expr_type(id int) bool { + typ := e.expr_types[id] or { return false } + if typ is Void { + return u8(typ) != 1 + } + return !type_has_null_data(typ) +} + +pub fn (e &Environment) expr_type_count() int { + return e.expr_types.len +} + +pub fn (e &Environment) all_expr_types() []Type { + mut out := []Type{cap: e.expr_types.len} + for _, typ in e.expr_types { + out << typ + } + return out +} + +// release_expr_type_cache_after_ssa releases expression-position metadata once +// SSA has consumed it. Native MIR/codegen keeps type IDs in SSA/MIR values and +// does not need these maps on the ARM64 path. +pub fn (mut e Environment) release_expr_type_cache_after_ssa() { + unsafe { + e.expr_types.free() + e.selector_names.free() + } + e.expr_types = map[int]Type{} + e.selector_names = map[int]string{} } // type_has_null_data checks if a Type sumtype has a missing payload. @@ -635,6 +624,9 @@ mut: expr_stack []string // Whether we are inside an unsafe{} block inside_unsafe bool + // Whether the currently checked expression is directly inside a return + // statement. Used for result error propagation in `or {}` fallbacks. + inside_return_stmt bool // Ownership tracking: variables that hold owned values // (from `.to_owned()` for strings, or any non-Copy value for other types). owned_vars map[string]token.Pos // var name -> position where it became owned @@ -2522,11 +2514,11 @@ fn (mut c Checker) check_types(exp_type Type, got_type Type) bool { // Prefer name-based equality first so recursive composite types // (e.g. `map[string]Any` where `Any` contains `map[string]Any`) // do not recurse indefinitely through structural `==`. - exp_name := exp_type.name() - got_name := got_type.name() - if exp_name == got_name { + if same_type_name(exp_type, got_type) { return true } + exp_name := exp_type.name() + got_name := got_type.name() if exp_name == 'int' && (got_name == 'Duration' || got_name == 'time__Duration') { return true } @@ -2797,6 +2789,9 @@ fn (c &Checker) can_or_block_propagate_error(raw_cond_type Type, cond ast.Expr, if raw_cond_type is ResultType { return true } + if expected_is_result && c.inside_return_stmt { + return true + } return expected_is_result && cond is ast.IndexExpr && (cond as ast.IndexExpr).expr is ast.RangeExpr && c.is_string_like(raw_cond_type) } @@ -4571,9 +4566,12 @@ fn (mut c Checker) stmt(stmt ast.Stmt) { // ast.FnDecl - handled by preregister_all_fn_signatures / process_pending_fn_bodies ast.ReturnStmt { c.log('ReturnStmt:') + prev_inside_return_stmt := c.inside_return_stmt + c.inside_return_stmt = true for expr in stmt.exprs { c.expr(expr) } + c.inside_return_stmt = prev_inside_return_stmt $if ownership ? { c.ownership_check_return(stmt) } @@ -5005,8 +5003,8 @@ fn (mut c Checker) check_pending_fn_body(pending PendingFnBody) bool { prev_scope := c.scope prev_module := c.cur_file_module prev_fn_root_scope := c.fn_root_scope - prev_fallback_vars := c.fallback_vars.clone() - prev_generic_params := c.generic_params.clone() + mut prev_fallback_vars := c.fallback_vars.move() + prev_generic_params := c.generic_params c.scope = pending.scope c.cur_file_module = pending.module_name c.fn_root_scope = pending.scope @@ -5026,7 +5024,7 @@ fn (mut c Checker) check_pending_fn_body(pending PendingFnBody) bool { c.fallback_vars[pending.decl.receiver.name] = receiver_type } if has_generic_params { - c.generic_params = decl_generic_params.clone() + c.generic_params = decl_generic_params for gp_name in decl_generic_params { c.scope.insert(gp_name, Type(NamedType(gp_name))) } @@ -5057,7 +5055,7 @@ fn (mut c Checker) check_pending_fn_body(pending PendingFnBody) bool { } c.expected_type = expected_type c.generic_params = prev_generic_params - c.fallback_vars = prev_fallback_vars.clone() + c.fallback_vars = prev_fallback_vars.move() c.fn_root_scope = prev_fn_root_scope c.env.set_fn_scope(pending.module_name, pending.scope_fn_name, pending.scope) c.scope = prev_scope @@ -5405,7 +5403,7 @@ fn (mut c Checker) sql_or_fallback_type(expr ast.Expr) ?Type { // fn (mut c Checker) assignment(lx ast.Expr, typ Type) { fn (mut c Checker) assignment(from_type Type, to_type Type) ! { // same type - if from_type.name() == to_type.name() { + if same_type_name(from_type, to_type) { return } // numbers literals @@ -5448,7 +5446,7 @@ fn (mut c Checker) assignment(from_type Type, to_type Type) ! { return } // return c.assignment(from_type.base_type, to_type.base_type)! - if from_type.base_type.name() == to_type.base_type.name() { + if same_type_name(from_type.base_type, to_type.base_type) { return } } @@ -5727,13 +5725,13 @@ fn (mut c Checker) fn_decl(decl ast.FnDecl) { // c.expr(decl.typ.return_type) mut prev_scope := c.scope c.open_scope() - prev_generic_params := c.generic_params.clone() + prev_generic_params := c.generic_params decl_generic_params := collect_fn_generic_params(decl) for gp_name in decl_generic_params { c.scope.insert(gp_name, Type(NamedType(gp_name))) } if decl_generic_params.len > 0 { - c.generic_params = decl_generic_params.clone() + c.generic_params = decl_generic_params } mut fn_typ := c.fn_type_with_insert_params(decl.typ, FnTypeAttribute.from_ast_attributes(decl.attributes), true) diff --git a/vlib/v2/types/checker_test.v b/vlib/v2/types/checker_test.v index 6f6bd149b..6bfb670cd 100644 --- a/vlib/v2/types/checker_test.v +++ b/vlib/v2/types/checker_test.v @@ -37,7 +37,7 @@ fn check_code_and_files(code string) (&Environment, []ast.File) { // Helper to check if a specific type exists in the environment fn has_type(env &Environment, type_name string) bool { - for typ in env.expr_type_values { + for typ in env.all_expr_types() { if typ is Void { continue } @@ -50,7 +50,7 @@ fn has_type(env &Environment, type_name string) bool { // Helper to check if a type matches a predicate fn has_type_matching(env &Environment, predicate fn (Type) bool) bool { - for typ in env.expr_type_values { + for typ in env.all_expr_types() { if typ is Void { continue } @@ -65,7 +65,7 @@ fn has_type_matching(env &Environment, predicate fn (Type) bool) bool { fn test_basic_literal_int() { env := check_code('fn main() { x := 42 }') - assert env.expr_type_values.len > 0, 'checker should populate expr_types' + assert env.expr_type_count() > 0, 'checker should populate expr_types' // int literals get int_literal type assert has_type_matching(env, fn (t Type) bool { return t is Primitive && t.props.has(.integer) @@ -74,7 +74,7 @@ fn test_basic_literal_int() { fn test_basic_literal_float() { env := check_code('fn main() { x := 3.14 }') - assert env.expr_type_values.len > 0, 'checker should populate expr_types for float' + assert env.expr_type_count() > 0, 'checker should populate expr_types for float' assert has_type_matching(env, fn (t Type) bool { return t is Primitive && t.props.has(.float) }), 'should have float primitive type' @@ -814,7 +814,7 @@ fn main() { fn test_infix_expr_arithmetic() { env := check_code('fn main() { x := 1 + 2 }') - assert env.expr_type_values.len > 0, 'checker should populate expr_types for infix' + assert env.expr_type_count() > 0, 'checker should populate expr_types for infix' } fn test_infix_expr_comparison_lt() { @@ -868,7 +868,7 @@ fn test_fixed_array_slice_returns_array() { fn test_array_element_type() { env := check_code('fn main() { x := [1, 2, 3] }') mut found := false - for typ in env.expr_type_values { + for typ in env.all_expr_types() { if typ is Array { if typ.elem_type is Primitive { found = typ.elem_type.props.has(.integer) @@ -882,7 +882,7 @@ fn test_array_element_type() { fn test_array_init_mixed_float_and_int_literals() { env := check_code('fn main() { x := [50.0, 15, 1] }') mut found := false - for typ in env.expr_type_values { + for typ in env.all_expr_types() { if typ is Array && typ.elem_type.name() == 'float_literal' { found = true break @@ -927,7 +927,7 @@ fn test_map_init() { fn test_map_types() { env := check_code("fn main() { x := {'a': 1} }") mut found_correct_types := false - for typ in env.expr_type_values { + for typ in env.all_expr_types() { if typ is Map { key_is_string := typ.key_type.name() == 'string' value_is_int := typ.value_type is Primitive && typ.value_type.props.has(.integer) @@ -952,7 +952,7 @@ fn test_prefix_address_of() { fn test_pointer_base_type() { env := check_code('fn main() { x := 42; y := &x }') mut found_int_ptr := false - for typ in env.expr_type_values { + for typ in env.all_expr_types() { if typ is Pointer { if typ.base_type is Primitive && typ.base_type.props.has(.integer) { found_int_ptr = true @@ -988,7 +988,7 @@ fn foo() int { return 42 } fn main() { x := foo() } ' env := check_code(code) - assert env.expr_type_values.len > 0, 'call expr should populate types' + assert env.expr_type_count() > 0, 'call expr should populate types' } fn test_fn_literal() { @@ -1009,7 +1009,7 @@ struct Point { x int; y int } fn main() { p := Point{x: 1, y: 2}; z := p.x } ' env := check_code(code) - assert env.expr_type_values.len > 0, 'selector expr should populate types' + assert env.expr_type_count() > 0, 'selector expr should populate types' } fn test_init_expr() { @@ -1134,7 +1134,7 @@ fn main() { fn test_index_expr_array() { env := check_code('fn main() { arr := [1, 2, 3]; x := arr[0] }') - assert env.expr_type_values.len > 0, 'index expr should populate types' + assert env.expr_type_count() > 0, 'index expr should populate types' } fn test_index_expr_returns_element_type() { @@ -1149,7 +1149,7 @@ fn test_index_expr_returns_element_type() { fn test_if_expr() { env := check_code('fn main() { x := if true { 1 } else { 2 } }') - assert env.expr_type_values.len > 0, 'if expr should populate types' + assert env.expr_type_count() > 0, 'if expr should populate types' } fn test_match_expr() { @@ -1163,7 +1163,7 @@ fn main() { } ' env := check_code(code) - assert env.expr_type_values.len > 0, 'match expr should populate types' + assert env.expr_type_count() > 0, 'match expr should populate types' assert has_type(env, 'string'), 'match expr should produce string type' } @@ -1414,14 +1414,14 @@ const labels = { fn test_cast_expr() { env := check_code('fn main() { x := int(3.14) }') - assert env.expr_type_values.len > 0, 'cast expr should populate types' + assert env.expr_type_count() > 0, 'cast expr should populate types' } // === Parenthesized Expression Tests === fn test_paren_expr() { env := check_code('fn main() { x := (1 + 2) * 3 }') - assert env.expr_type_values.len > 0, 'paren expr should populate types' + assert env.expr_type_count() > 0, 'paren expr should populate types' } // === Unsafe Expression Tests === @@ -1433,7 +1433,7 @@ fn main() { } ' env := check_code(code) - assert env.expr_type_values.len > 0, 'unsafe expr should populate types' + assert env.expr_type_count() > 0, 'unsafe expr should populate types' } // === Tuple Tests === @@ -1473,6 +1473,35 @@ fn main() { x := foo() } }), 'result return should produce ResultType' } +fn test_result_return_accepts_option_or_error_fallback() { + code := ' +interface IError { + msg() string +} + +struct MessageError {} + +fn (err MessageError) msg() string { + return "missing" +} + +fn error(msg string) IError { + return MessageError{} +} + +fn maybe() ?int { return none } +fn foo() !int { return maybe() or { error("missing") } } +fn main() {} +' + env := check_code(code) + scope := env.get_scope('main') or { panic('missing main scope') } + foo_obj := scope.lookup_parent('foo', 0) or { panic('missing foo function') } + foo_type := foo_obj.typ() + assert foo_type is FnType + foo_return := (foo_type as FnType).return_type or { panic('missing foo return type') } + assert foo_return is ResultType +} + // === Channel Tests === fn test_channel_type() { @@ -1560,7 +1589,7 @@ fn main() { ' env := check_code(code) mut found_option_int := false - for typ in env.expr_type_values { + for typ in env.all_expr_types() { if typ is OptionType { if typ.base_type is Primitive && typ.base_type.props.has(.integer) { found_option_int = true diff --git a/vlib/v2/types/types.v b/vlib/v2/types/types.v index 6e48fff4c..b8b732bdb 100644 --- a/vlib/v2/types/types.v +++ b/vlib/v2/types/types.v @@ -522,6 +522,143 @@ fn (t Type) is_compatible_with(t2 Type) bool { return t1_unwrapped == t2_unwrapped } +fn same_type_name(a Type, b Type) bool { + if !type_has_valid_payload(a) || !type_has_valid_payload(b) { + return false + } + match a { + Alias { + return b is Alias && a.name == b.name + } + Array { + return b is Array && same_type_name(a.elem_type, b.elem_type) + } + ArrayFixed { + return b is ArrayFixed && a.len == b.len && same_type_name(a.elem_type, b.elem_type) + } + Channel { + if b is Channel { + a_elem := a.elem_type or { + if _ := b.elem_type { + return false + } + return true + } + + b_elem := b.elem_type or { return false } + return same_type_name(a_elem, b_elem) + } + } + Char { + return b is Char + } + Enum { + return b is Enum && a.name == b.name + } + FnType { + if b is FnType { + if a.params.len != b.params.len || a.is_variadic != b.is_variadic + || a.is_mut_receiver != b.is_mut_receiver { + return false + } + for i, param in a.params { + if param.name != b.params[i].name || param.is_mut != b.params[i].is_mut + || !same_type_name(param.typ, b.params[i].typ) { + return false + } + } + a_ret := a.return_type or { + if _ := b.return_type { + return false + } + return true + } + + b_ret := b.return_type or { return false } + return same_type_name(a_ret, b_ret) + } + } + Interface { + return b is Interface && a.name == b.name + } + ISize { + return b is ISize + } + Map { + return b is Map && same_type_name(a.key_type, b.key_type) + && same_type_name(a.value_type, b.value_type) + } + NamedType { + return b is NamedType && string(a) == string(b as NamedType) + } + Nil { + return b is Nil + } + None { + return b is None + } + OptionType { + return b is OptionType && same_type_name(a.base_type, b.base_type) + } + Pointer { + return b is Pointer && a.lifetime == b.lifetime + && same_type_name(a.base_type, b.base_type) + } + Primitive { + return b is Primitive && a == b + } + ResultType { + return b is ResultType && same_type_name(a.base_type, b.base_type) + } + Rune { + return b is Rune + } + String { + return b is String + } + Struct { + return b is Struct && a.name == b.name + } + SumType { + return b is SumType && a.name == b.name + } + Thread { + if b is Thread { + a_elem := a.elem_type or { + if _ := b.elem_type { + return false + } + return true + } + + b_elem := b.elem_type or { return false } + return same_type_name(a_elem, b_elem) + } + } + Tuple { + if b is Tuple { + if a.types.len != b.types.len { + return false + } + for i, typ in a.types { + if !same_type_name(typ, b.types[i]) { + return false + } + } + return true + } + } + USize { + return b is USize + } + Void { + return b is Void + } + } + + return false +} + fn (t Type) is_float() bool { if t is Primitive { return t.is_float() -- 2.39.5