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.
This commit is contained in:
Alexander Medvednikov authored and GitHub committed 2026-10-28 02:11:29 +03:00
1 parent 727f3cde79
commit 064d83671c
16 files changed
+356 -182

No files matched your search

+1
View File
@@ -159,6 +159,7 @@ const auto_complete_flags = [
'-arch',
'-assert',
'-show-timings',
'-show-instructions',
'-show-asserts',
'-check-syntax',
'-check',
+4 -2
View File
@@ -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 `<output>.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
+59 -6
View File
@@ -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 {
+30
View File
@@ -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() == ''
}
}
+17
View File
@@ -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
}
+7
View File
@@ -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
}
+26
View File
@@ -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]
+10 -3
View File
@@ -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] <file.v|directory> [options]\n' + ' -o <output> output binary or C file\n' + ' -b <c|fastc|arm64|wasm|eval> backend\n' + ' -os <name> -arch <name> target platform\n' + ' -cc <compiler> C compiler executable\n' + ' -cflags <flags> extra C compiler options\n' + ' -ldflags <flags> extra options appended to the link command\n' + ' -thread-stack-size <bytes> 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 <patterns> 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 <names> 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 <size> set a custom user-build memory safety limit\n to specify unit append K|M|G (or k|m|g) (default: M)\n' + ' -d <name> compile-time define'
return 'usage: v3 [run|crun|test] <file.v|directory> [options]\n' + ' -o <output> output binary or C file\n' + ' -b <c|fastc|arm64|wasm|eval> backend\n' + ' -os <name> -arch <name> target platform\n' + ' -cc <compiler> C compiler executable\n' + ' -cflags <flags> extra C compiler options\n' + ' -ldflags <flags> extra options appended to the link command\n' + ' -thread-stack-size <bytes> 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 <patterns> 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 <names> 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 <size> set a custom user-build memory safety limit\n to specify unit append K|M|G (or k|m|g) (default: M)\n' + ' -d <name> 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()
+7
View File
@@ -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`
+105 -33
View File
@@ -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 {
@@ -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
}
+1 -1
View File
@@ -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 {
+40 -7
View File
@@ -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 = ''
@@ -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() {
+28 -111
View File
@@ -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 {
+6
View File
@@ -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