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))
+178 -7
View File
@@ -236,6 +236,7 @@ Ct_Value_Kind :: enum u8 {
Ct_Error_Kind :: enum u8 {
None,
Not_Comptime,
Compile_Error,
Overflow,
Div_By_Zero,
Non_Exact,
@@ -325,6 +326,7 @@ Ct_State :: struct {
error: Ct_Error_Kind,
diagnostic: source.Diagnostic_Id,
silent: bool,
foldable: bool,
demanded: ^[dynamic]Spec_Id,
promoted_cells: [dynamic]Ct_Cell_Id,
promoted_globals: [dynamic]hir.Global_Id,
@@ -353,6 +355,7 @@ ct_state_make :: proc(
state.error = .None
state.diagnostic = source.INVALID_DIAGNOSTIC
state.silent = !diagnose
state.foldable = true
state.demanded = demanded
state.values.allocator = checker.allocator
state.children.allocator = checker.allocator
@@ -479,7 +482,15 @@ ct_pop_bindings :: proc(state: ^Ct_State, start: int) {
resize(&state.bindings, start)
}
ct_value_has_children :: proc(kind: Ct_Value_Kind) -> bool {
return kind == .Range || kind == .Array || kind == .Struct ||
kind == .Optional_Some || kind == .Fallible
}
ct_child_slice :: proc(state: ^Ct_State, value: Ct_Value) -> []Ct_Value_Id {
if !ct_value_has_children(value.kind) {
return nil
}
start := int(value.start)
end := start+int(value.count)
if start < 0 || end > len(state.children) {
@@ -850,6 +861,88 @@ ct_value_references_dead_storage :: proc(state: ^Ct_State, id: Ct_Value_Id) -> b
return false
}
ct_retain_value_storage :: proc(state: ^Ct_State, id: Ct_Value_Id, depth := 0) {
if depth > 64 || id == INVALID_CT_VALUE || int(id) >= len(state.values) {
return
}
value := state.values[id]
if value.kind == .Pointer || value.kind == .Slice {
place_id := Ct_Place_Id(value.index)
if place_id != INVALID_CT_PLACE && int(place_id) < len(state.places) {
cell := state.places[place_id].cell
if cell != INVALID_CT_CELL && int(cell) < len(state.cells) {
state.cells[cell].live = true
}
}
}
for child in ct_child_slice(state, value) {
ct_retain_value_storage(state, child, depth+1)
}
}
ct_value_can_materialize :: proc(state: ^Ct_State, id: Ct_Value_Id, depth := 0) -> bool {
if depth > 64 || id == INVALID_CT_VALUE || int(id) >= len(state.values) {
return false
}
value := state.values[id]
#partial switch value.kind {
case .Invalid, .Undefined, .Function, .Type:
return false
case .Void, .Integer, .Float, .Bool, .String, .Null:
return true
case .Pointer:
store := &state.checker.module.types
pointer, pointer_ok := types.node(store, value.type)
place_id := Ct_Place_Id(value.index)
if !pointer_ok || pointer.kind != .Pointer || pointer.mutable ||
place_id == INVALID_CT_PLACE || int(place_id) >= len(state.places) {
return false
}
place := state.places[place_id]
if place.cell == INVALID_CT_CELL || int(place.cell) >= len(state.cells) || len(ct_place_path(state, place)) != 0 {
return false
}
root_id := state.cells[place.cell].value
if root_id == INVALID_CT_VALUE || int(root_id) >= len(state.values) {
return false
}
root := state.values[root_id]
array, array_ok := types.node(store, root.type)
return array_ok && array.kind == .Array &&
((pointer.many && value.active == 0 && types.equal(pointer.child, array.child)) ||
(!pointer.many && value.active == -1 && types.equal(pointer.child, root.type))) &&
ct_value_can_materialize(state, root_id, depth+1)
case .Slice:
store := &state.checker.module.types
slice, slice_ok := types.node(store, value.type)
place_id := Ct_Place_Id(value.index)
if !slice_ok || slice.kind != .Slice || slice.mutable || value.start != 0 ||
place_id == INVALID_CT_PLACE || int(place_id) >= len(state.places) {
return false
}
place := state.places[place_id]
if place.cell == INVALID_CT_CELL || int(place.cell) >= len(state.cells) || len(ct_place_path(state, place)) != 0 {
return false
}
root_id := state.cells[place.cell].value
if root_id == INVALID_CT_VALUE || int(root_id) >= len(state.values) {
return false
}
root := state.values[root_id]
array, array_ok := types.node(store, root.type)
return array_ok && array.kind == .Array && u64(value.count) == array.count &&
types.equal(slice.child, array.child) && ct_value_can_materialize(state, root_id, depth+1)
case .Range, .Array, .Struct, .Optional_Some, .Fallible:
for child in ct_child_slice(state, value) {
if child != INVALID_CT_VALUE && !ct_value_can_materialize(state, child, depth+1) {
return false
}
}
return true
}
return false
}
ct_materialize_array_pointer :: proc(
state: ^Ct_State,
value: Ct_Value,
@@ -976,6 +1069,12 @@ ct_materialize_value :: proc(
}
value := state.values[materialized]
#partial switch value.kind {
case .Void:
return add_hir_expr(checker, hir.Expr{
kind=.Void, span=span, type=types.VOID,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Undefined:
if state.diagnostic == source.INVALID_DIAGNOSTIC {
state.diagnostic = source.add(checker.diagnostics, span, "cannot materialize an undefined comptime value")
@@ -3234,6 +3333,12 @@ ct_struct_type :: proc(
ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type, depth: int, expr_id := ast.INVALID_EXPR) -> (Ct_Value_Id, Ct_Flow, bool) {
checker := state.checker
resolution := -1
if expr_id != ast.INVALID_EXPR {
if index, ok := find_call_resolution(checker, expr_id); ok {
resolution = index
}
}
if expr.left != ast.INVALID_EXPR {
callee, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1)
if !ok || flow.kind != .Normal {
@@ -3254,7 +3359,7 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type
}
}
}
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, message)
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Compile_Error, expr.span, message)
}
if is_intrinsic_call(checker, expr, "some") {
if len(expr.args) != 1 {
@@ -3447,7 +3552,10 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type
}
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unresolved function '%s'", symbol_text(checker, expr.name))
}
return ct_eval_template_call(state, template, expr.args, expr.span, expected, depth+1)
if runtime_param_count(checker.ast_module.functions[template]) != 0 {
resolution = -1
}
return ct_eval_template_call(state, template, expr.args, expr.span, expected, depth+1, resolution)
}
ct_eval_template_call :: proc(
@@ -3457,6 +3565,7 @@ ct_eval_template_call :: proc(
span: source.Span,
expected: types.Type,
depth: int,
resolution := -1,
) -> (Ct_Value_Id, Ct_Flow, bool) {
checker := state.checker
if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) {
@@ -3466,10 +3575,22 @@ ct_eval_template_call :: proc(
if !function.has_body || len(function.unsupported_reason) > 0 {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "function '%s' is runtime-only", symbol_text(checker, function.name))
}
if !valid_call_arity(function, len(args)) {
resolved := resolution >= 0 && resolution < len(checker.call_resolutions)
if !resolved && !valid_call_arity(function, len(args)) {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "function '%s' arity mismatch", symbol_text(checker, function.name))
}
comptime_values, comptime_ok := collect_comptime_values(checker, function, args, state.pkg, state.file, false, checker.current_comptime_values)
comptime_values: []Comptime_Value
comptime_ok := false
if resolved {
comptime_values = clone_comptime_values(
checker.call_resolutions[resolution].comptime_values, checker.allocator,
)
comptime_ok = true
} else {
comptime_values, comptime_ok = collect_comptime_values(
checker, function, args, state.pkg, state.file, false, checker.current_comptime_values,
)
}
defer delete(comptime_values, checker.allocator)
if !comptime_ok {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "invalid comptime argument for '%s'", symbol_text(checker, function.name))
@@ -3479,6 +3600,9 @@ ct_eval_template_call :: proc(
defer checker.current_comptime_values = previous_comptime
result_type := function_channel_type(checker, function)
if types.is_void(result_type) || types.kind(result_type, &checker.module.types) == .Fallible {
state.foldable = false
}
runtime_values: [dynamic]Ct_Value_Id
runtime_values.allocator = checker.allocator
runtime_types: [dynamic]types.Type
@@ -3570,6 +3694,9 @@ ct_eval_template_call :: proc(
state, .Not_Comptime, span, "comptime function returned an undefined value",
)
}
if runtime_param_count(function) == 0 {
ct_retain_value_storage(state, result)
}
if ct_value_references_dead_storage(state, result) {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime function returned a pointer to expired storage")
}
@@ -3616,8 +3743,10 @@ ct_clone_graph_value :: proc(ctx: ^Ct_Clone_Context, id: Ct_Value_Id) -> Ct_Valu
}
value := ctx.src.values[id]
children := ct_child_slice(ctx.src, value)
value.start = 0
value.count = 0
if ct_value_has_children(value.kind) {
value.start = 0
value.count = 0
}
dst_id := ct_add_value(ctx.dst, value)
ctx.values[id] = dst_id
if len(children) > 0 {
@@ -3632,7 +3761,8 @@ ct_clone_graph_value :: proc(ctx: ^Ct_Clone_Context, id: Ct_Value_Id) -> Ct_Valu
ctx.dst.values[dst_id].count = u32(len(children))
}
if value.kind == .Pointer || value.kind == .Slice {
ctx.dst.values[dst_id].index = u64(ct_clone_graph_place(ctx, Ct_Place_Id(value.index)))
cloned_place := ct_clone_graph_place(ctx, Ct_Place_Id(value.index))
ctx.dst.values[dst_id].index = u64(cloned_place)
}
return dst_id
}
@@ -3692,6 +3822,47 @@ ct_clone_graph :: proc(dst, src: ^Ct_State, id: Ct_Value_Id) -> Ct_Value_Id {
return ct_clone_graph_value(&ctx, id)
}
cache_zero_runtime_call :: proc(
checker: ^Checker,
resolution: int,
expr: ast.Expr_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> Call_Fold {
if resolution < 0 || resolution >= len(checker.call_resolutions) {
return .Runtime
}
if checker.call_resolutions[resolution].fold != .Unknown {
return checker.call_resolutions[resolution].fold
}
state := ct_state_make(
checker, pkg, file, values=checker.current_comptime_values, diagnose=false,
)
value, flow, ok := ct_eval_expr(&state, expr)
if ok && flow.kind == .Normal && state.foldable && ct_value_can_materialize(&state, value) {
checker.call_resolutions[resolution].fold = .Value
checker.call_resolutions[resolution].folded_value = ct_clone_graph(
&checker.static_state, &state, value,
)
ct_state_destroy(&state)
return .Value
}
error := state.error
ct_state_destroy(&state)
if error == .Compile_Error {
diagnosed := ct_state_make(
checker, pkg, file, values=checker.current_comptime_values,
)
_, _, _ = ct_eval_expr(&diagnosed, expr)
checker.call_resolutions[resolution].fold = .Compile_Error
checker.call_resolutions[resolution].diagnostic = diagnosed.diagnostic
ct_state_destroy(&diagnosed)
return .Compile_Error
}
checker.call_resolutions[resolution].fold = .Runtime
return .Runtime
}
store_static_binding :: proc(checker: ^Checker, source: ^Ct_State, id: Ct_Value_Id, name: symbol.Id) -> Static_Binding {
value := ct_clone_graph(&checker.static_state, source, id)
value_type := types.INVALID