whole-function comptime folding for zero-runtime value calls

This commit is contained in:
2026-07-22 23:11:48 +02:00
parent 21ff291788
commit 8b50eb7606
24 changed files with 564 additions and 260 deletions
+116 -26
View File
@@ -189,6 +189,13 @@ Type_Factory_Origin :: struct {
values: []Comptime_Value,
}
Call_Fold :: enum u8 {
Unknown,
Runtime,
Value,
Compile_Error,
}
Call_Resolution :: struct {
expr: ast.Expr_Id,
ctx: []Comptime_Value,
@@ -196,6 +203,9 @@ Call_Resolution :: struct {
mapping: []int,
comptime_values: []Comptime_Value,
runtime_types: []types.Type,
fold: Call_Fold,
folded_value: Ct_Value_Id,
diagnostic: source.Diagnostic_Id,
}
Checker :: struct {
@@ -257,6 +267,7 @@ Checker :: struct {
generated_types: [dynamic]Generated_Type_Entry,
type_factory_origins: [dynamic]Type_Factory_Origin,
call_resolutions: [dynamic]Call_Resolution,
building_hir: bool,
target: target.Target,
allocator: mem.Allocator,
}
@@ -1106,6 +1117,13 @@ runtime_param_count :: proc(function: ast.Function) -> int {
return count
}
can_fold_zero_runtime_call :: proc(checker: ^Checker, function: ast.Function) -> bool {
return function.has_body && !function.c_abi && runtime_param_count(function) == 0 &&
!types.is_void(function.result) &&
!types.is_constraint(function.result) &&
!is_type_metatype_syntax(checker, function.result)
}
comptime_param_count :: proc(function: ast.Function) -> int {
count := 0
for param in function.params {
@@ -3066,13 +3084,41 @@ find_call_resolution :: proc(
return -1, false
}
call_resolution_matches :: proc(
entry: Call_Resolution,
mapping: []int,
comptime_values: []Comptime_Value,
runtime_types: []types.Type,
) -> bool {
if len(entry.mapping) != len(mapping) || len(entry.runtime_types) != len(runtime_types) ||
!comptime_values_equal(entry.comptime_values, comptime_values) {
return false
}
for value, index in mapping {
if entry.mapping[index] != value {
return false
}
}
for value, index in runtime_types {
if !types.equal(entry.runtime_types[index], value) {
return false
}
}
return true
}
store_call_resolution :: proc(
checker: ^Checker,
expr: ast.Expr_Id,
mapping: []int,
comptime_values: []Comptime_Value,
runtime_types: []types.Type,
) {
) -> int {
if index, ok := find_call_resolution(checker, expr); ok && call_resolution_matches(
checker.call_resolutions[index], mapping, comptime_values, runtime_types,
) {
return index
}
entry := Call_Resolution{
expr=expr,
ctx=clone_comptime_values(checker.current_comptime_values, checker.allocator),
@@ -3080,6 +3126,8 @@ store_call_resolution :: proc(
mapping=slice.clone(mapping, checker.allocator),
comptime_values=clone_comptime_values(comptime_values, checker.allocator),
runtime_types=slice.clone(runtime_types, checker.allocator),
folded_value=INVALID_CT_VALUE,
diagnostic=source.INVALID_DIAGNOSTIC,
}
if index, ok := find_call_resolution(checker, expr); ok {
previous := checker.call_resolutions[index]
@@ -3089,9 +3137,10 @@ store_call_resolution :: proc(
delete(previous.comptime_values, checker.allocator)
delete(previous.runtime_types, checker.allocator)
checker.call_resolutions[index] = entry
return
return index
}
append(&checker.call_resolutions, entry)
return len(checker.call_resolutions)-1
}
resolved_call_arg_expected :: proc(
@@ -5511,39 +5560,56 @@ infer_expr :: proc(
defer delete(comptime_values, checker.allocator)
if comptime_ok &&
can_specialize(checker, function, stack[frame_index].args, comptime_values) {
store_call_resolution(
resolution := store_call_resolution(
checker, frame.expr, frame.mapping,
comptime_values, stack[frame_index].args,
)
previous_comptime := checker.current_comptime_values
checker.current_comptime_values = comptime_values
for source_index in 0..<len(expr.args) {
param_index := call_param_index(frame.mapping, source_index)
if param_index >= len(function.params) || function.params[param_index].comptime_value {
continue
}
demand := call_arg_expected(checker, function, param_index)
record_demand(checker, expr.args[source_index], demand, locals, local_types, pkg, file)
fold := Call_Fold.Runtime
if can_fold_zero_runtime_call(checker, function) {
fold = cache_zero_runtime_call(checker, resolution, frame.expr, pkg, file)
}
checker.current_comptime_values = previous_comptime
spec := INVALID_SPEC
if demanded == nil {
spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values)
} else {
spec = find_spec(checker, frame.template, stack[frame_index].args, comptime_values)
if spec == INVALID_SPEC {
spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values)
if fold == .Compile_Error {
last = types.INVALID
} else if fold == .Value {
// Folded specializations are still inferred to validate their bodies and
// participate in the type-demand fixpoint. Leaving them undemanded keeps
// prune_specs from emitting an otherwise unused runtime function.
if !checker.building_hir && demanded == nil && !function_has_comptime_params(function) {
_ = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values)
}
mark_spec_demanded(checker, spec, demanded)
}
if spec != INVALID_SPEC {
last = checker.specs[spec].result
value := checker.call_resolutions[resolution].folded_value
last = checker.static_state.values[value].type
} else {
previous_comptime := checker.current_comptime_values
checker.current_comptime_values = comptime_values
declared := function_channel_type(checker, function)
for source_index in 0..<len(expr.args) {
param_index := call_param_index(frame.mapping, source_index)
if param_index >= len(function.params) || function.params[param_index].comptime_value {
continue
}
demand := call_arg_expected(checker, function, param_index)
record_demand(checker, expr.args[source_index], demand, locals, local_types, pkg, file)
}
checker.current_comptime_values = previous_comptime
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) || types.is_noreturn(declared) else types.INVALID
spec := INVALID_SPEC
if demanded == nil {
spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values)
} else {
spec = find_spec(checker, frame.template, stack[frame_index].args, comptime_values)
if spec == INVALID_SPEC {
spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values)
}
mark_spec_demanded(checker, spec, demanded)
}
if spec != INVALID_SPEC {
last = checker.specs[spec].result
} else {
previous_comptime = checker.current_comptime_values
checker.current_comptime_values = comptime_values
declared := function_channel_type(checker, function)
checker.current_comptime_values = previous_comptime
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) || types.is_noreturn(declared) else types.INVALID
}
}
} else {
declared := function_channel_type(checker, function)
@@ -9619,6 +9685,29 @@ build_expr :: proc(
}
checker.current_comptime_values = previous_comptime
}
fold := Call_Fold.Runtime
if comptime_ok && can_fold_zero_runtime_call(checker, function) &&
frame.resolution >= 0 && frame.resolution < len(checker.call_resolutions) {
fold = cache_zero_runtime_call(checker, frame.resolution, frame.expr, pkg, file)
}
if comptime_ok && arg_violation == source.INVALID_DIAGNOSTIC &&
(fold == .Value || fold == .Compile_Error) {
if fold == .Value {
value := checker.call_resolutions[frame.resolution].folded_value
last = ct_materialize_value(&checker.static_state, value, expr.span, frame.expected)
} else {
diagnostic := checker.call_resolutions[frame.resolution].diagnostic
last = invalid_hir_expr(checker, expr.span, diagnostic, frame.expected)
}
delete(stack[frame_index].arg_types, checker.allocator)
stack[frame_index].arg_types = nil
delete(stack[frame_index].built_args, checker.allocator)
stack[frame_index].built_args = nil
delete(stack[frame_index].mapping, checker.allocator)
stack[frame_index].mapping = nil
_ = pop(&stack)
continue
}
spec := INVALID_SPEC
if comptime_ok {
if frame.resolution >= 0 && frame.resolution < len(checker.call_resolutions) {
@@ -14274,6 +14363,7 @@ check :: proc(
finalize_record_field_inference(&checker)
validate_external_globals(&checker)
prune_specs(&checker)
checker.building_hir = true
build_globals(&checker)
for index := 0; index < len(checker.specs); index += 1 {
build_function(&checker, spec_id(index))