From 064d83671c55060bd779fb1601a32d84f9f2e826 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 28 Oct 2026 02:11:29 +0300 Subject: [PATCH] compiler: compile vox2 with ~14% fewer instructions; add -show-instructions (#31274) * compiler: add -show-instructions to report retired instructions per stage -show-timings only prints wall time, which is noisy on a loaded machine. -show-instructions implies -show-timings and appends the retired CPU instructions of each stage (summed over all threads) and of the whole process, read from proc_pid_rusage on macOS. * transform: cache bare struct candidates and use interned names for registry lookups Receiver method resolution scanned every checker struct for each bare type name. Build a short-name -> qualified-struct index once per struct registry and invalidate it when generic templates are erased. Generic name checks rebuilt name-id indexes of the enum and transform registries whenever they grew; look the interned name up in the registry map directly instead. * markused: build interface dispatch keys once per scan The used-interface dispatch pass rebuilt every implementer method key on each worklist round. Precompute the groups and keys once, skip the pass when no new functions were marked, and enqueue IError keys once. --- cmd/tools/vcomplete.v | 1 + vlib/v/README.md | 6 +- vlib/v/bench/bench.v | 65 +++++++- vlib/v/bench/bench_test.v | 30 ++++ vlib/v/bench/instructions_darwin.c.v | 17 +++ vlib/v/bench/instructions_default.c.v | 7 + vlib/v/bench/limit_memory_darwin.c.v | 26 ++++ vlib/v/driver/driver.v | 13 +- vlib/v/help/build/build.txt | 7 + vlib/v/markused/markused.v | 138 ++++++++++++----- .../markused/reachable_runtime_helpers_test.v | 30 ++-- vlib/v/transform/comptime.v | 2 +- vlib/v/transform/fn.v | 47 +++++- .../transform/generic_type_name_index_test.v | 4 - vlib/v/transform/monomorphize.v | 139 ++++-------------- vlib/v/transform/transform.v | 6 + 16 files changed, 356 insertions(+), 182 deletions(-) create mode 100644 vlib/v/bench/instructions_darwin.c.v create mode 100644 vlib/v/bench/instructions_default.c.v diff --git a/cmd/tools/vcomplete.v b/cmd/tools/vcomplete.v index b9622b5b99..38f1a3ac58 100644 --- a/cmd/tools/vcomplete.v +++ b/cmd/tools/vcomplete.v @@ -159,6 +159,7 @@ const auto_complete_flags = [ '-arch', '-assert', '-show-timings', + '-show-instructions', '-show-asserts', '-check-syntax', '-check', diff --git a/vlib/v/README.md b/vlib/v/README.md index 161d13f3e6..a1729cc6c0 100644 --- a/vlib/v/README.md +++ b/vlib/v/README.md @@ -587,8 +587,10 @@ the V temporary directory by default; set `V3CACHE` to select another root, or p object cache. An explicit `-b c` binary build also retains the complete generated translation unit at `.c` for codegen inspection. Use `-show-timings` to print elapsed time for each compiler stage and the total, including memory usage and cache metrics. It enables the same benchmark -summary as `-v` without verbose compiler traces. For self-host builds, `-v` also reports -time spent preparing markused, marking reachable symbols, and waiting for transform preparation. +summary as `-v` without verbose compiler traces. `-show-instructions` also prints the retired +CPU instructions of each stage and of the whole process (macOS only). For self-host builds, `-v` +also reports time spent preparing markused, marking reachable symbols, and waiting for +transform preparation. `-silent` and generated C output to stdout (`-o -`) suppress the summary. The benchmark output prints counts for parsed `.vh` and `.v` files and their total diff --git a/vlib/v/bench/bench.v b/vlib/v/bench/bench.v index 287a1c746c..f8fb75cb84 100644 --- a/vlib/v/bench/bench.v +++ b/vlib/v/bench/bench.v @@ -82,6 +82,8 @@ mut: memory_monitor_interval i64 memory_monitor_thread thread memory_monitor_started bool + show_instructions bool + last_instructions u64 } // new creates a new value for bench. @@ -129,6 +131,13 @@ pub fn (mut b Bench) set_quiet() { b.quiet = true } +// enable_instruction_counts adds the retired CPU instructions of every step +// (summed over all threads) to the report, where the platform exposes them. +pub fn (mut b Bench) enable_instruction_counts() { + b.show_instructions = true + b.last_instructions = current_instruction_count() or { 0 } +} + // start_memory_monitor starts the compiler memory safety watchdog. pub fn (mut b Bench) start_memory_monitor() { if b.memory_limit_kb > 0 || !b.quiet { @@ -283,7 +292,7 @@ pub fn (b &Bench) current_step_time_us() i64 { // account for their full wall time. pub fn (mut b Bench) step_measured(name string, elapsed_us i64) { allocations := current_allocation_stats() - b.report_step(name, false, elapsed_us, 0, 0, allocations.enabled, false) + b.report_step(name, false, elapsed_us, 0, 0, allocations.enabled, false, '') } // step_parallel records a pipeline step, appending "(parallel)" to its name @@ -291,9 +300,11 @@ pub fn (mut b Bench) step_measured(name string, elapsed_us i64) { pub fn (mut b Bench) step_parallel(name string, parallel bool) { elapsed_us := b.step_sw.elapsed().microseconds() allocations := current_allocation_stats() + instructions := b.step_instructions() b.report_step(name, parallel, elapsed_us, allocations.allocation_count - b.last_allocation_count, - allocations.allocated_bytes - b.last_allocated_bytes, allocations.enabled, true) + allocations.allocated_bytes - b.last_allocated_bytes, allocations.enabled, true, + instructions) b.finish_step_report() } @@ -303,6 +314,9 @@ pub fn (mut b Bench) step_parts(parts []StepPart) { return } allocations := current_allocation_stats() + // Instructions are counted per process, so they cannot be split between the + // parts; the whole group's count is shown on its last part. + instructions := b.step_instructions() total_allocation_count := allocations.allocation_count - b.last_allocation_count total_allocated_bytes := allocations.allocated_bytes - b.last_allocated_bytes mut total_us := i64(0) @@ -326,15 +340,20 @@ pub fn (mut b Bench) step_parts(parts []StepPart) { } else { u64(i64(total_allocated_bytes) * part.time_us / total_us) } + part_instructions := if is_last && instructions.len > 0 { + '${instructions} (${parts.len} parts)' + } else { + '' + } b.report_step(part.name, part.parallel, part.time_us, allocation_count, allocated_bytes, - allocations.enabled, false) + allocations.enabled, false, part_instructions) reported_allocation_count += allocation_count reported_allocated_bytes += allocated_bytes } b.finish_step_report() } -fn (mut b Bench) report_step(name string, parallel bool, elapsed_us i64, allocation_count u64, allocated_bytes u64, allocations_enabled bool, has_stage_peak bool) { +fn (mut b Bench) report_step(name string, parallel bool, elapsed_us i64, allocation_count u64, allocated_bytes u64, allocations_enabled bool, has_stage_peak bool, instructions string) { label := if parallel { '${name} (parallel)' } else { name } ram_kb := current_rss_kb() stage_peak_ram_kb := if has_stage_peak { b.finish_stage_memory(ram_kb) } else { i64(0) } @@ -364,7 +383,7 @@ fn (mut b Bench) report_step(name string, parallel bool, elapsed_us i64, allocat '' } if !b.quiet { - println(' ${label:-20s} ${ms:8.2f} ms ${ram_mb:6.0f} MB RSS${footprint_suffix}${stage_peak_suffix} ${peak_ram_mb:6.0f} MB peak${allocation_suffix}') + println(' ${label:-20s} ${ms:8.2f} ms ${ram_mb:6.0f} MB RSS${footprint_suffix}${stage_peak_suffix} ${peak_ram_mb:6.0f} MB peak${allocation_suffix}${instruction_suffix(instructions)}') } b.steps << Step{ name: label @@ -382,9 +401,33 @@ fn (mut b Bench) finish_step_report() { after_report := current_allocation_stats() b.last_allocation_count = after_report.allocation_count b.last_allocated_bytes = after_report.allocated_bytes + if b.show_instructions { + b.last_instructions = current_instruction_count() or { 0 } + } b.step_sw.restart() } +// step_instructions returns the formatted instruction count of the current +// step, or an empty string when instruction counts are off or unavailable. +fn (b &Bench) step_instructions() string { + if !b.show_instructions { + return '' + } + count := current_instruction_count() or { return '' } + return format_instructions(count - b.last_instructions) +} + +fn format_instructions(count u64) string { + return '${f64(count) / 1e9:8.3f} G instructions' +} + +fn instruction_suffix(instructions string) string { + if instructions.len == 0 { + return '' + } + return ' ${instructions}' +} + fn physical_footprint_suffix(memory LimitMemory) string { if memory.metric != 'physical footprint' { return '' @@ -443,7 +486,17 @@ pub fn (b &Bench) print_report() { return } total_ms := f64(b.total_sw.elapsed().microseconds()) / 1000.0 - println(' ${'total':-20s} ${total_ms:8.2f} ms') + mut total_instructions := '' + if b.show_instructions { + // The process total includes startup and everything between the steps, + // so it matches what an external counter such as `/usr/bin/time -l` reports. + total_instructions = if count := current_instruction_count() { + ' ${format_instructions(count)} (process)' + } else { + ' instruction counts are unavailable on this platform' + } + } + println(' ${'total':-20s} ${total_ms:8.2f} ms${total_instructions}') if b.metrics.len > 0 { println(' metrics:') for metric in b.metrics { diff --git a/vlib/v/bench/bench_test.v b/vlib/v/bench/bench_test.v index 51bacf68b0..2d6169ae17 100644 --- a/vlib/v/bench/bench_test.v +++ b/vlib/v/bench/bench_test.v @@ -148,3 +148,33 @@ fn test_shorten_home_path() { assert shorten_home_path('${home}_other/code/project/main.v') == '${home}_other/code/project/main.v' assert shorten_home_path('/tmp/project/main.v') == '/tmp/project/main.v' } + +fn test_instruction_count_formatting() { + assert format_instructions(1_234_567_890) == ' 1.235 G instructions' + assert instruction_suffix('') == '' + assert instruction_suffix('1 G instructions') == ' 1 G instructions' +} + +fn test_instruction_counts_are_off_by_default() { + b := new() + assert !b.show_instructions + assert b.step_instructions() == '' +} + +fn test_enable_instruction_counts() { + mut b := new() + b.set_quiet() + b.enable_instruction_counts() + assert b.show_instructions + $if macos { + start := current_instruction_count() or { 0 } + assert start > 0 + assert b.last_instructions > 0 + assert b.last_instructions <= start + assert b.step_instructions().ends_with(' G instructions') + b.step('work') + assert b.last_instructions >= start + } $else { + assert b.step_instructions() == '' + } +} diff --git a/vlib/v/bench/instructions_darwin.c.v b/vlib/v/bench/instructions_darwin.c.v new file mode 100644 index 0000000000..6f1ab7edff --- /dev/null +++ b/vlib/v/bench/instructions_darwin.c.v @@ -0,0 +1,17 @@ +module bench + +import os + +const rusage_info_v4 = 4 + +// current_instruction_count returns the retired CPU instructions of the whole +// process (all threads) since it started. +fn current_instruction_count() ?u64 { + $if macos || ios { + mut usage := V3RusageInfo{} + if C.proc_pid_rusage(os.getpid(), rusage_info_v4, &usage) == 0 && usage.ri_instructions > 0 { + return usage.ri_instructions + } + } + return none +} diff --git a/vlib/v/bench/instructions_default.c.v b/vlib/v/bench/instructions_default.c.v new file mode 100644 index 0000000000..9b0709e077 --- /dev/null +++ b/vlib/v/bench/instructions_default.c.v @@ -0,0 +1,7 @@ +module bench + +// current_instruction_count returns none: retired instruction counters are +// only read on macOS. +fn current_instruction_count() ?u64 { + return none +} diff --git a/vlib/v/bench/limit_memory_darwin.c.v b/vlib/v/bench/limit_memory_darwin.c.v index d98a75b005..c4451f95bc 100644 --- a/vlib/v/bench/limit_memory_darwin.c.v +++ b/vlib/v/bench/limit_memory_darwin.c.v @@ -14,6 +14,32 @@ struct V3RusageInfo { ri_phys_footprint u64 ri_proc_start_abstime u64 ri_proc_exit_abstime u64 + // The fields below are only filled by RUSAGE_INFO_V4 and later. + ri_child_user_time u64 + ri_child_system_time u64 + ri_child_pkg_idle_wkups u64 + ri_child_interrupt_wkups u64 + ri_child_pageins u64 + ri_child_elapsed_abstime u64 + ri_diskio_bytesread u64 + ri_diskio_byteswritten u64 + ri_cpu_time_qos_default u64 + ri_cpu_time_qos_maintenance u64 + ri_cpu_time_qos_background u64 + ri_cpu_time_qos_utility u64 + ri_cpu_time_qos_legacy u64 + ri_cpu_time_qos_user_initiated u64 + ri_cpu_time_qos_user_interactive u64 + ri_billed_system_time u64 + ri_serviced_system_time u64 + ri_logical_writes u64 + ri_lifetime_max_phys_footprint u64 + ri_instructions u64 + ri_cycles u64 + ri_billed_energy u64 + ri_serviced_energy u64 + ri_interval_max_phys_footprint u64 + ri_runnable_time u64 } @[c_extern] diff --git a/vlib/v/driver/driver.v b/vlib/v/driver/driver.v index 124feefc5a..32994ff130 100644 --- a/vlib/v/driver/driver.v +++ b/vlib/v/driver/driver.v @@ -4319,7 +4319,7 @@ fn v3_crun_build_identity(state &V3ModuleCacheState, prefs &pref.Preferences, us } fn cli_usage() string { - return 'usage: v3 [run|crun|test] [options]\n' + ' -o output binary or C file\n' + ' -b backend\n' + ' -os -arch target platform\n' + ' -cc C compiler executable\n' + ' -cflags extra C compiler options\n' + ' -ldflags extra options appended to the link command\n' + ' -thread-stack-size spawned-thread stack size\n' + ' -prod -c99 -shared -strict C build modes\n' + ' -v verbose stage profiling\n' + ' -show-timings compiler stage timings without verbose traces\n' + ' -silent suppress benchmark output\n' + ' -showcc print C compiler commands\n' + ' -trace-calls trace function entries to stderr\n' + ' -trace-fns restrict tracing to functions or modules\n' + ' -race detect data races at runtime (ThreadSanitizer)\n' + ' -profile [file] write V1-compatible function profile data\n' + ' -profile-fns profile only named functions and their callees\n' + ' -profile-no-inline omit @[inline] functions from the profile\n' + ' -no-memory-limit disable the 10176 MiB user-build memory safety limit\n' + ' -memory-limit set a custom user-build memory safety limit\n to specify unit append K|M|G (or k|m|g) (default: M)\n' + ' -d compile-time define' + return 'usage: v3 [run|crun|test] [options]\n' + ' -o output binary or C file\n' + ' -b backend\n' + ' -os -arch target platform\n' + ' -cc C compiler executable\n' + ' -cflags extra C compiler options\n' + ' -ldflags extra options appended to the link command\n' + ' -thread-stack-size spawned-thread stack size\n' + ' -prod -c99 -shared -strict C build modes\n' + ' -v verbose stage profiling\n' + ' -show-timings compiler stage timings without verbose traces\n' + ' -show-instructions -show-timings plus retired CPU instructions per stage (macOS)\n' + ' -silent suppress benchmark output\n' + ' -showcc print C compiler commands\n' + ' -trace-calls trace function entries to stderr\n' + ' -trace-fns restrict tracing to functions or modules\n' + ' -race detect data races at runtime (ThreadSanitizer)\n' + ' -profile [file] write V1-compatible function profile data\n' + ' -profile-fns profile only named functions and their callees\n' + ' -profile-no-inline omit @[inline] functions from the profile\n' + ' -no-memory-limit disable the 10176 MiB user-build memory safety limit\n' + ' -memory-limit set a custom user-build memory safety limit\n to specify unit append K|M|G (or k|m|g) (default: M)\n' + ' -d compile-time define' } fn shared_library_postfix(target_os string) string { @@ -4762,8 +4762,8 @@ fn v3_fast_build_tree_signature_into(root string, mut entries strings.Builder) { fn v3_unchanged_build_key(prefs &pref.Preferences, c_compiler string, bin_file string) string { mut output_args := []string{} for arg in os.args[1..] { - if arg in ['-nocache', '--no-cache', '-v', '-show-timings', '-silent', '-showcc', - '-show-c-output', '-no-memory-limit'] || arg.starts_with('-v3-internal-') { + if arg in ['-nocache', '--no-cache', '-v', '-show-timings', '-show-instructions', '-silent', + '-showcc', '-show-c-output', '-no-memory-limit'] || arg.starts_with('-v3-internal-') { continue } output_args << arg @@ -11215,6 +11215,7 @@ pub fn run(args []string) { mut ownership_mode := false mut verbose := false mut show_timings := false + mut show_instructions := false mut silent := false mut deferred_implicit_tcc_warning := '' mut skip_notices := false @@ -11805,6 +11806,10 @@ pub fn run(args []string) { } else if args[i] == '-show-timings' { show_timings = true i++ + } else if args[i] == '-show-instructions' { + show_timings = true + show_instructions = true + i++ } else if args[i] in ['-new-generic-solver', '-progress', '-use-os-system-to-run'] { // v3 uses its current generic solver without a legacy selection switch. // `-progress` selects a reporter in the test runner, not in the compiler. @@ -12430,6 +12435,8 @@ pub fn run(args []string) { driver_sw := time.new_stopwatch() if !(verbose || show_timings) || silent || c_to_stdout { b.set_quiet() + } else if show_instructions { + b.enable_instruction_counts() } if no_memory_limit { b.disable_memory_limit() diff --git a/vlib/v/help/build/build.txt b/vlib/v/help/build/build.txt index 881b93ec4d..46506ee6e5 100644 --- a/vlib/v/help/build/build.txt +++ b/vlib/v/help/build/build.txt @@ -342,6 +342,13 @@ NB: the build flags are shared with the run command too: without verbose compiler traces. -silent suppresses this summary, as does writing generated C to stdout with -o -. + -show-instructions + Like -show-timings, and also prints the retired CPU instructions of each compiler + stage, summed over all threads, plus the total for the whole process. Instruction + counts are less sensitive to machine load than elapsed time, which makes them useful + when comparing compiler changes. They are read from the kernel on macOS; on other + systems, only the timings are printed. + Related to -show-timings, is the ability to compile a special instrumented v compiler with this command: `v -d time_parsing -d time_checking -d time_cgening -d time_v self` diff --git a/vlib/v/markused/markused.v b/vlib/v/markused/markused.v index 20aa30ca6e..5afe439235 100644 --- a/vlib/v/markused/markused.v +++ b/vlib/v/markused/markused.v @@ -945,7 +945,7 @@ fn mark_used_with_test_files(a &flat.FlatAst, tc &types.TypeChecker, test_files // Interface method values and other roots can enter the queue as abstract // dispatch keys that have no function declaration of their own. Seed their // concrete implementations before the declaration-driven BFS skips those keys. - enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut reachable_helper_scan.interface_dispatch_methods) + enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut reachable_helper_scan.interface_dispatch) // Interface dispatch reachability: calling an interface method `Foo.m` may // dispatch to any concrete `T.m` for a type `T` that implements `Foo`. Those // concrete methods are only referenced from the generated dispatch switch, so @@ -1394,52 +1394,125 @@ fn mark_used_with_test_files(a &flat.FlatAst, tc &types.TypeChecker, test_files return used, uses_generics } -// Each used dispatch method expands a fixed declaration graph during markused. -// Remember completed methods while still discovering methods reached by later bodies. -fn enqueue_used_interface_dispatch_implementers(tc &types.TypeChecker, mut used map[string]bool, mut queue []string, mut processed map[string]bool) bool { - mut added := false - for iface_name_source_id in tc.interface_names.keys() { - iface_name := flat.source_type_name(iface_name_source_id) - iface_name_id := iface_name_source_id +// InterfaceDispatchScan remembers the dispatch keys of every abstract interface method, +// and which methods already had their implementers enqueued. The checker is fixed during +// markused and `used` only grows, so the keys are built once per scan. +struct InterfaceDispatchScan { +mut: + processed map[string]bool + groups []InterfaceDispatchGroup + ierror_keys []string + built bool + ierror_keys_enqueued bool + // used.len after the last call that found nothing new; an unchanged len means an unchanged set. + settled_used_len int = -1 +} + +struct InterfaceDispatchGroup { + name_id u32 + is_ierror bool +mut: + methods []InterfaceDispatchMethod + pending int +} + +struct InterfaceDispatchMethod { + name string + key string + c_key string + short_key string +mut: + done bool +} + +fn (mut scan InterfaceDispatchScan) build(tc &types.TypeChecker) { + scan.built = true + for iface_name_id in tc.interface_names.keys() { + iface_name := flat.source_type_name(iface_name_id) + is_ierror := markused_is_ierror_interface_name(iface_name_id) + if is_ierror { + for method in ['msg', 'code'] { + dispatch_key := '${iface_name}.${method}' + scan.ierror_keys << dispatch_key + scan.ierror_keys << markused_c_name(dispatch_key) + } + } methods := tc.interface_abstract_method_names(iface_name_id) if methods.len == 0 { continue } - mut used_methods := []string{cap: methods.len} + short_name := iface_name.all_after_last('.') + mut group := InterfaceDispatchGroup{ + name_id: iface_name_id + is_ierror: is_ierror + methods: []InterfaceDispatchMethod{cap: methods.len} + pending: methods.len + } for method in methods { dispatch_key := '${iface_name}.${method}' - if processed[dispatch_key] { + group.methods << InterfaceDispatchMethod{ + name: method + key: dispatch_key + c_key: markused_c_name(dispatch_key) + short_key: '${short_name}.${method}' + } + } + scan.groups << group + } +} + +// Each used dispatch method expands a fixed declaration graph during markused. +// Remember completed methods while still discovering methods reached by later bodies. +fn enqueue_used_interface_dispatch_implementers(tc &types.TypeChecker, mut used map[string]bool, mut queue []string, mut scan InterfaceDispatchScan) bool { + if !scan.built { + scan.build(tc) + } + start_len := used.len + if start_len == scan.settled_used_len { + return false + } + mut added := false + for mut group in scan.groups { + if group.pending == 0 { + continue + } + mut used_methods := []int{} + for i, method in group.methods { + if method.done || scan.processed[method.key] { continue } - dispatch_c_key := markused_c_name(dispatch_key) - short_dispatch_key := '${iface_name.all_after_last('.')}.${method}' - if dispatch_key in used || dispatch_c_key in used || short_dispatch_key in used { - used_methods << method + if method.key in used || method.c_key in used || method.short_key in used { + used_methods << i } } if used_methods.len == 0 { continue } - impls := if markused_is_ierror_interface_name(iface_name_id) { + impls := if group.is_ierror { tc.ierror_impl_names() } else { - markused_interface_impl_names(iface_name_id, tc) + markused_interface_impl_names(group.name_id, tc) } if impls.len == 0 { continue } - for method in used_methods { + for i in used_methods { + method_name := group.methods[i].name for impl in impls { - for alias in interface_implementer_method_aliases(impl, method, tc) { + for alias in interface_implementer_method_aliases(impl, method_name, tc) { if enqueue(alias, mut used, mut queue) { added = true } } - enqueue_implicit_interface_str_helpers_for_impl(iface_name_id, method, impl, tc, mut used, mut queue) + enqueue_implicit_interface_str_helpers_for_impl(group.name_id, method_name, + impl, tc, mut used, mut queue) } - processed['${iface_name}.${method}'] = true + scan.processed[group.methods[i].key] = true + group.methods[i].done = true + group.pending-- } } + scan.settled_used_len = if used.len == start_len { start_len } else { -1 } return added } @@ -2930,7 +3003,7 @@ mut: channel_stringify_cache map[string]&MarkusedTypeResults ierror_equality_cache map[u32]int - interface_dispatch_methods map[string]bool + interface_dispatch InterfaceDispatchScan map_type_cache map[u32]int unknown_map_type_cache map[string]int auto_str_skipped_fields map[u32]&MarkusedSkippedFields @@ -3483,7 +3556,7 @@ fn (mut scan RuntimeHelpersScan) enqueue_nodes(a &flat.FlatAst, tc &types.TypeCh enqueue('f32__eq_epsilon', mut used, mut queue) } if scan.needs_ierror_equality_dispatch { - enqueue_ierror_equality_dispatch_helpers(tc, mut used, mut queue, mut scan.interface_dispatch_methods) + enqueue_ierror_equality_dispatch_helpers(tc, mut used, mut queue, mut scan.interface_dispatch) } if scan.needs_shared_runtime && !scan.enqueued_groups.has(.shared) { scan.enqueued_groups.set(.shared) @@ -3562,19 +3635,18 @@ fn markused_is_builtin_closure_global(a &flat.FlatAst, tc &types.TypeChecker, no && os.dir(file.name).replace('\\', '/').ends_with('vlib/builtin/closure') } -fn enqueue_ierror_equality_dispatch_helpers(tc &types.TypeChecker, mut used map[string]bool, mut queue []string, mut processed_dispatches map[string]bool) { - for iface_name_source_id in tc.interface_names.keys() { - iface_name := flat.source_type_name(iface_name_source_id) - if !markused_is_ierror_interface_name(iface_name_source_id) { - continue - } - for method in ['msg', 'code'] { - dispatch_key := '${iface_name}.${method}' - enqueue(dispatch_key, mut used, mut queue) - enqueue(markused_c_name(dispatch_key), mut used, mut queue) +fn enqueue_ierror_equality_dispatch_helpers(tc &types.TypeChecker, mut used map[string]bool, mut queue []string, mut scan InterfaceDispatchScan) { + if !scan.built { + scan.build(tc) + } + // `used` only grows, so the IError dispatch keys stay enqueued once added. + if !scan.ierror_keys_enqueued { + scan.ierror_keys_enqueued = true + for key in scan.ierror_keys { + enqueue(key, mut used, mut queue) } } - enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut processed_dispatches) + enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut scan) } fn markused_type_equality_uses_ierror(typ types.Type, tc &types.TypeChecker, mut cache map[u32]int) bool { diff --git a/vlib/v/markused/reachable_runtime_helpers_test.v b/vlib/v/markused/reachable_runtime_helpers_test.v index 16c629e9e4..0d463b1cb4 100644 --- a/vlib/v/markused/reachable_runtime_helpers_test.v +++ b/vlib/v/markused/reachable_runtime_helpers_test.v @@ -669,23 +669,23 @@ fn main() {} ') mut used := map[string]bool{} mut queue := []string{} - mut processed := map[string]bool{} - assert !enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut processed) - assert processed.len == 0 + mut scan := InterfaceDispatchScan{} + assert !enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut scan) + assert scan.processed.len == 0 used['Reader.first'] = true - assert enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut processed) + assert enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut scan) assert used['Item.first'] assert !used['Item.second'] - assert processed['Reader.first'] - assert !processed['Reader.second'] + assert scan.processed['Reader.first'] + assert !scan.processed['Reader.second'] first_len := queue.len - assert !enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut processed) + assert !enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut scan) assert queue.len == first_len // A later body can introduce a second abstract dispatch after the first one was cached. used['Reader.second'] = true - enqueue_ierror_equality_dispatch_helpers(tc, mut used, mut queue, mut processed) + enqueue_ierror_equality_dispatch_helpers(tc, mut used, mut queue, mut scan) assert used['Item.second'] - assert processed['Reader.second'] + assert scan.processed['Reader.second'] } fn test_interface_dispatch_cache_keeps_implicit_stringification_dependencies() { @@ -705,16 +705,16 @@ fn main() { ') mut used := map[string]bool{} mut queue := []string{} - mut processed := map[string]bool{} - enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut processed) - assert !processed['Printable.str'] + mut scan := InterfaceDispatchScan{} + enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut scan) + assert !scan.processed['Printable.str'] used['Printable.str'] = true - enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut processed) - assert processed['Printable.str'] + enqueue_used_interface_dispatch_implementers(tc, mut used, mut queue, mut scan) + assert scan.processed['Printable.str'] assert used['f64.str'] assert used['strconv__f64_to_str_l'] first_len := queue.len - enqueue_ierror_equality_dispatch_helpers(tc, mut used, mut queue, mut processed) + enqueue_ierror_equality_dispatch_helpers(tc, mut used, mut queue, mut scan) assert queue.len == first_len } diff --git a/vlib/v/transform/comptime.v b/vlib/v/transform/comptime.v index 2464105e9a..1a11c50562 100644 --- a/vlib/v/transform/comptime.v +++ b/vlib/v/transform/comptime.v @@ -1578,7 +1578,7 @@ fn (t &Transformer) generic_checker_struct_name_known(name_id u32) bool { if t.generic_type_indexes_ready && t.generic_checker_struct_index_count == t.tc.structs.len { return t.generic_checker_struct_names[name_id] } - return t.refresh_checker_struct_type_name_index().struct_names[name_id] + return name_id in t.tc.structs } fn (t &Transformer) comptime_method_requested_module(raw_id u32) string { diff --git a/vlib/v/transform/fn.v b/vlib/v/transform/fn.v index d124f2a12c..465bd3690e 100644 --- a/vlib/v/transform/fn.v +++ b/vlib/v/transform/fn.v @@ -540,6 +540,43 @@ fn (mut cache ReceiverMethodCache) set_front(slot int, receiver_type_id u32, met cache.front_states[slot] = if found { i8(1) } else { i8(-1) } } +// bare_struct_candidates returns the module-qualified, non-generic structs whose +// short name is `short_id`. Entries can be stale after template erasure, so +// callers check that each name is still in `t.tc.structs`. +fn (t &Transformer) bare_struct_candidates(short_id u32) []u32 { + if isnil(t.receiver_method_cache) { + return t.collect_bare_struct_candidates(short_id) + } + mut cache := t.receiver_method_cache + tc_ptr := voidptr(t.tc) + if cache.bare_structs_tc != tc_ptr || cache.bare_structs_len != t.tc.structs.len { + cache.bare_structs.clear() + for sname_id, _ in t.tc.structs { + sname_shape := flat.source_type_shape(sname_id) + if !sname_shape.has(.dotted) || sname_shape.has(.bracketed) { + continue + } + short := flat.source_type_syntax(sname_id).unqualified() + cache.bare_structs[short] << sname_id + } + cache.bare_structs_tc = tc_ptr + cache.bare_structs_len = t.tc.structs.len + } + return cache.bare_structs[short_id] or { []u32{} } +} + +fn (t &Transformer) collect_bare_struct_candidates(short_id u32) []u32 { + mut ids := []u32{} + for sname_id, _ in t.tc.structs { + sname_shape := flat.source_type_shape(sname_id) + if sname_shape.has(.dotted) && !sname_shape.has(.bracketed) + && flat.source_type_syntax(sname_id).unqualified() == short_id { + ids << sname_id + } + } + return ids +} + fn (t &Transformer) resolve_receiver_method_for_type_uncached(receiver_type_id u32, method string) ?string { clean_type_id := t.trim_pointer_type(receiver_type_id) clean_type := flat.source_type_name(clean_type_id) @@ -649,15 +686,11 @@ fn (t &Transformer) resolve_receiver_method_for_type_uncached(receiver_type_id u if !clean_shape.has(.dotted) && !clean_shape.has(.bracketed) && !isnil(t.tc) && !t.tc.is_locally_declared_bare_type(clean_type_id) { mut matched := '' - for sname_source_id, _ in t.tc.structs { - sname := flat.source_type_name(sname_source_id) - sname_id := sname_source_id - sname_shape := flat.source_type_shape(sname_id) - if !sname_shape.has(.dotted) || sname_shape.has(.bracketed) - || flat.source_type_syntax(sname_id).unqualified() != clean_type_id { + for sname_id in t.bare_struct_candidates(clean_type_id) { + if sname_id !in t.tc.structs { continue } - qmethod := '${sname}.${method}' + qmethod := '${flat.source_type_name(sname_id)}.${method}' if t.is_known_fn_name(qmethod) { if matched != '' && matched != qmethod { matched = '' diff --git a/vlib/v/transform/generic_type_name_index_test.v b/vlib/v/transform/generic_type_name_index_test.v index 216f612d7d..2ab841b254 100644 --- a/vlib/v/transform/generic_type_name_index_test.v +++ b/vlib/v/transform/generic_type_name_index_test.v @@ -19,7 +19,6 @@ fn test_generic_type_name_indexes_refresh_after_late_declarations() { assert t.generic_concrete_name_known(flat.source_type_id(name)) assert t.type_name_is_declared_id(flat.source_type_id(name)) } - assert t.generic_type_name_index_cache.checker_count == t.generic_checker_concrete_name_count() assert t.generic_checker_sum_name_known(flat.source_type_id('late.Choice')) assert t.generic_checker_interface_name_known(flat.source_type_id('late.Reader')) assert !t.generic_checker_sum_name_known(flat.source_type_id('late.MoreChoice')) @@ -47,7 +46,6 @@ fn test_generic_type_name_indexes_refresh_after_late_declarations() { assert t.type_name_is_declared_id(flat.source_type_id(name)) } assert !t.type_name_is_declared_id(flat.source_type_id('late.Missing')) - assert t.generic_type_name_index_cache.transform_count == t.structs.len + t.sum_types.ids.len + t.enum_types.len } fn test_generic_type_name_index_refresh_is_private_to_each_worker() { @@ -67,8 +65,6 @@ fn test_generic_type_name_index_refresh_is_private_to_each_worker() { assert !worker.generic_concrete_name_known(flat.source_type_id('RootRecord')) assert t.generic_concrete_name_known(flat.source_type_id('RootRecord')) assert !t.generic_concrete_name_known(flat.source_type_id('WorkerRecord')) - assert worker.generic_type_name_index_cache.checker_count == 1 - assert t.generic_type_name_index_cache.checker_count == 2 } fn test_generic_type_name_indexes_refresh_struct_enum_and_parameter_views() { diff --git a/vlib/v/transform/monomorphize.v b/vlib/v/transform/monomorphize.v index 331bb66a23..a86df22362 100644 --- a/vlib/v/transform/monomorphize.v +++ b/vlib/v/transform/monomorphize.v @@ -3260,6 +3260,9 @@ fn (mut t Transformer) materialize_generic_structs(erase_templates bool) { t.tc.params_structs.delete(decl_id) t.tc.unregister_short_type_name(decl_id) } + if !isnil(t.receiver_method_cache) { + t.receiver_method_cache.bare_structs_len = -1 + } // Refresh once after both registries have finished removing templates. t.erase_generic_sum_templates(sum_decls) } @@ -10371,28 +10374,9 @@ fn (t &Transformer) generic_enum_type_name_known(name_id u32) bool { if t.generic_type_indexes_ready && t.generic_enum_index_count == count { return t.generic_enum_type_names[name_id] } - return t.refresh_enum_type_name_index().enum_names[name_id] -} - -fn (t &Transformer) refresh_enum_type_name_index() &GenericTypeNameIndexCache { - mut cache := t.generic_type_name_index_cache - count := t.enum_types.len + if isnil(t.tc) { 0 } else { t.tc.enum_names.len } - if cache.enum_count != count { - mut names := map[u32]bool{} - mut checker_names := map[u32]bool{} - for name_id, _ in t.enum_types { names[name_id] = true } - if !isnil(t.tc) { - for name_source_id, _ in t.tc.enum_names { - name_id := name_source_id - names[name_id] = true - checker_names[name_id] = true - } - } - cache.enum_names = names.move() - cache.checker_enum_names = checker_names.move() - cache.enum_count = count - } - return cache + // Late registrations make the prepared index stale; the registries are + // keyed by interned ids, so look the id up directly. + return name_id in t.enum_types || (!isnil(t.tc) && name_id in t.tc.enum_names) } fn (t &Transformer) refresh_checker_struct_type_name_index() &GenericTypeNameIndexCache { @@ -10425,14 +10409,7 @@ fn (t &Transformer) generic_checker_sum_name_known(name_id u32) bool { if t.generic_type_indexes_ready && t.generic_checker_sum_index_count == count { return t.generic_checker_sum_names[name_id] } - mut cache := t.generic_type_name_index_cache - if cache.sum_count != count { - mut names := map[u32]bool{} - for name_source_id, _ in t.tc.sum_types.ids { names[name_source_id] = true } - cache.sum_names = names.move() - cache.sum_count = count - } - return cache.sum_names[name_id] + return name_id in t.tc.sum_types.ids } fn (t &Transformer) generic_checker_interface_name_known(name_id u32) bool { @@ -10441,14 +10418,7 @@ fn (t &Transformer) generic_checker_interface_name_known(name_id u32) bool { if t.generic_type_indexes_ready && t.generic_checker_interface_index_count == count { return t.generic_checker_interface_names[name_id] } - mut cache := t.generic_type_name_index_cache - if cache.interface_count != count { - mut names := map[u32]bool{} - for name_source_id, _ in t.tc.interface_names { names[name_source_id] = true } - cache.interface_names = names.move() - cache.interface_count = count - } - return cache.interface_names[name_id] + return name_id in t.tc.interface_names } fn (t &Transformer) generic_exact_alias_name_known(name_id u32) bool { @@ -10457,59 +10427,18 @@ fn (t &Transformer) generic_exact_alias_name_known(name_id u32) bool { if t.generic_type_indexes_ready && t.generic_exact_alias_names.len == count { return t.generic_exact_alias_names[name_id] } - mut cache := t.generic_type_name_index_cache - if cache.alias_count != count { - mut names := map[u32]bool{} - for name_source_id, _ in t.tc.type_aliases.ids { names[name_source_id] = true } - cache.alias_names = names.move() - cache.alias_count = count - } - return cache.alias_names[name_id] + return name_id in t.tc.type_aliases.ids } -// GenericTypeNameIndexCache belongs to one transformer or worker. Late -// declarations invalidate the prepared indexes; rebuild their numeric views -// once per registry size change instead of re-interning every key per lookup. +// GenericTypeNameIndexCache belongs to one transformer or worker. Late struct +// declarations invalidate the prepared indexes; rebuild the short-name view +// once per registry size change. Exact-name checks look the interned spelling +// up in the registry directly instead. struct GenericTypeNameIndexCache { mut: - checker_count int = -1 - checker_names map[u32]bool - sum_count int = -1 - sum_names map[u32]bool - interface_count int = -1 - interface_names map[u32]bool - alias_count int = -1 - alias_names map[u32]bool - enum_count int = -1 - enum_names map[u32]bool - checker_enum_names map[u32]bool struct_count int = -1 struct_names map[u32]bool struct_short_names map[u32]bool - parameter_count int = -1 - parameter_names map[u32]bool - transform_count int = -1 - transform_names map[u32]bool - main_structs map[u32]bool -} - -fn (t &Transformer) refresh_transform_type_name_index() &GenericTypeNameIndexCache { - mut cache := t.generic_type_name_index_cache - count := t.structs.len + t.sum_types.ids.len + t.enum_types.len - if cache.transform_count != count { - mut names := map[u32]bool{} - mut main_structs := map[u32]bool{} - for name_id, info in t.structs { - names[name_id] = true - if info.module in ['', 'main'] { main_structs[name_id] = true } - } - for name_id, _ in t.sum_types.ids { names[name_id] = true } - for name_id, _ in t.enum_types { names[name_id] = true } - cache.transform_names = names.move() - cache.main_structs = main_structs.move() - cache.transform_count = count - } - return cache } fn (t &Transformer) generic_transform_concrete_name_known(name_id u32) bool { @@ -10517,7 +10446,7 @@ fn (t &Transformer) generic_transform_concrete_name_known(name_id u32) bool { && t.generic_transform_concrete_index_count == t.structs.len + t.sum_types.ids.len + t.enum_types.len { return t.generic_transform_concrete_type_names[name_id] } - return t.refresh_transform_type_name_index().transform_names[name_id] + return name_id in t.structs || name_id in t.sum_types.ids || name_id in t.enum_types } fn (t &Transformer) generic_resolved_concrete_name_known(name_id u32) bool { @@ -10526,21 +10455,12 @@ fn (t &Transformer) generic_resolved_concrete_name_known(name_id u32) bool { fn (t &Transformer) generic_concrete_name_known(name_id u32) bool { if isnil(t.tc) { return false } - count := t.generic_checker_concrete_name_count() - if t.generic_type_indexes_ready && t.generic_alias_index_concrete_count == count { + if t.generic_type_indexes_ready + && t.generic_alias_index_concrete_count == t.generic_checker_concrete_name_count() { return t.generic_concrete_type_names[name_id] } - mut cache := t.generic_type_name_index_cache - if cache.checker_count != count { - mut names := map[u32]bool{} - for name_source_id, _ in t.tc.structs { names[name_source_id] = true } - for name_source_id, _ in t.tc.sum_types.ids { names[name_source_id] = true } - for name_source_id, _ in t.tc.enum_names { names[name_source_id] = true } - for name_source_id, _ in t.tc.interface_names { names[name_source_id] = true } - cache.checker_names = names.move() - cache.checker_count = count - } - return cache.checker_names[name_id] + return name_id in t.tc.structs || name_id in t.tc.sum_types.ids || name_id in t.tc.enum_names + || name_id in t.tc.interface_names } fn (t &Transformer) generic_arg_is_alias_name(arg string, module_name string) bool { @@ -10699,8 +10619,7 @@ fn (t &Transformer) qualify_generic_arg_for_decl_module(arg_id u32, module_name fn (t &Transformer) generic_main_arg_checker_declaration_known(name_id u32) bool { if t.generic_exact_alias_name_known(name_id) { return true } if isnil(t.tc) { return false } - return t.refresh_enum_type_name_index().checker_enum_names[name_id] - || t.generic_checker_interface_name_known(name_id) + return name_id in t.tc.enum_names || t.generic_checker_interface_name_known(name_id) } fn (t &Transformer) generic_arg_module_owns_type(name string, module_name string) bool { @@ -16630,16 +16549,13 @@ fn (t &Transformer) generic_type_base_requires_args(typ_id u32) bool { if t.generic_type_indexes_ready && t.generic_parameter_type_index_count == count { return t.generic_parameter_type_names[clean_id] || t.generic_parameter_type_names[short_id] } - mut cache := t.generic_type_name_index_cache - if cache.parameter_count != count { - mut names := map[u32]bool{} - for name_source_id, _ in t.tc.struct_generic_params { names[name_source_id] = true } - for name_source_id, _ in t.tc.sum_generic_params { names[name_source_id] = true } - for name_source_id, _ in t.tc.type_alias_generic_params { names[name_source_id] = true } - cache.parameter_names = names.move() - cache.parameter_count = count - } - return cache.parameter_names[clean_id] || cache.parameter_names[short_id] + return t.generic_parameter_type_name_known(clean_id) + || t.generic_parameter_type_name_known(short_id) +} + +fn (t &Transformer) generic_parameter_type_name_known(name_id u32) bool { + return name_id in t.tc.struct_generic_params || name_id in t.tc.sum_generic_params + || name_id in t.tc.type_alias_generic_params } fn strip_main_type_locks(typ string) string { @@ -17081,7 +16997,8 @@ fn (t &Transformer) is_known_main_struct_type_name_id(name_id u32) bool { && t.generic_alias_index_authority_count == t.generic_type_authority_name_count() { return t.generic_main_struct_names[bare_id] } - return t.refresh_transform_type_name_index().main_structs[bare_id] + info := t.structs[bare_id] or { return false } + return info.module in ['', 'main'] } fn (t &Transformer) type_name_is_declared(name string) bool { diff --git a/vlib/v/transform/transform.v b/vlib/v/transform/transform.v index 21ba3b7077..59487b50b0 100644 --- a/vlib/v/transform/transform.v +++ b/vlib/v/transform/transform.v @@ -1205,6 +1205,12 @@ mut: front_results [256]string front_states [256]i8 front_generations [256]u32 + // Short source type id -> source type ids of the module-qualified, + // non-generic structs with that short name. Rebuilt when the struct table + // changes owner or size; template erasure resets bare_structs_len. + bare_structs_tc voidptr + bare_structs_len int = -1 + bare_structs map[u32][]u32 } // PromoteTextCache short-circuits repeated promote_scoped_result_text calls on