function values as comptime params

This commit is contained in:
2026-07-18 01:35:39 +02:00
parent 9f433af724
commit 85693e57e1
7 changed files with 254 additions and 12 deletions
+2 -2
View File
@@ -155,7 +155,7 @@ as `math.divfloor(a, b)` resolve to ordinary functions.
- demand-monomorphized Brolang and C-ABI functions
- integer comptime value parameters such as `make_array func($N usize) [N]u8`, specialized by value and omitted from the runtime ABI
- explicit comptime type parameters such as `max func($T type, a, b T) T`, specialized by type and omitted from the runtime ABI
- comptime parameters may appear anywhere, are erased from the runtime ABI, and accept recursively stable booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, and tagged unions; equal structural values share specializations, while pointers, functions, general slices, fallibles, ranges, untagged unions, and undefined values have no stable comptime identity
- comptime parameters may appear anywhere, are erased from the runtime ABI, and accept recursively stable booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, tagged unions, and concrete function values; equal structural values and function declarations share specializations, while pointers, general slices, fallibles, ranges, untagged unions, and undefined values have no stable comptime identity
- comptime parameters may be omitted when uniquely recoverable from runtime arguments, the immediate expected result, or exact type-factory provenance; `_` is an explicit inference hole
- forced typed comptime expressions such as `$sum(1, 2)`, `$Point { x = 1, y = 2 }`, and comptime value blocks such as `${ yield 4 }`
- comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values; `undefined` storage may be initialized at comptime, but remaining poison cannot be observed
@@ -165,7 +165,7 @@ as `math.divfloor(a, b)` resolve to ordinary functions.
- `tag!(value)` reads a tagged union's active discriminant and folds when the value is comptime-known; `tagname!(enum_value)` requires a comptime-known enum value and returns its immutable declaration name
- bodyful `c_func` definitions and bodyless `c_func` declarations with exact external symbol names
- concrete-only C signatures, C variadic declarations/calls, and C default argument promotions
- native function pointer values and types with `@func(...) R`, fallible `@func(...) R ! E`, optional `?@func(...) R`, and non-variadic native indirect calls
- native function pointer values and types with `@func(...) R`, fallible `@func(...) R ! E`, optional `?@func(...) R`, comptime specialization by declaration identity, and non-variadic native indirect calls when the target is not statically known
- Apple Silicon C ABI lowering for scalars, pointers, fixed-signature plain records/unions, small aggregates, homogeneous float aggregates, and indirect aggregate returns
- imported C typedefs, scalar constants, enum constants, fixed arrays, complete plain structs/unions, C `void*` as nullable `anyopaque` pointers, and pointers to opaque records
- imported external C object variables, including mutable variables and immutable object globals
+1 -1
View File
@@ -226,7 +226,7 @@ Current prototype features:
- Plain imported C structs/unions, fixed arrays, and C function pointer typedefs, including keyed literals, field access, callbacks, and Apple Silicon by-value ABI lowering
- Qualified imported globals and functions with package-aware symbol mangling
- Demand-monomorphized Brolang and C-ABI functions
- Recursively stable comptime values—including booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, and tagged unions—may be interleaved with runtime parameters, are erased from the ABI, and specialize from explicit arguments or exact inference provenance
- Recursively stable comptime values—including booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, tagged unions, and concrete function values—may be interleaved with runtime parameters, are erased from the ABI, and specialize from explicit arguments, declaration identity, or exact inference provenance
- Forced typed comptime expressions (`$sum(1, 2)`, `$Point { x = 1, y = 2 }`) and comptime value blocks (`${ yield 4 }`)
- Zig-style comptime type factories returning anonymous native structs (`Box func($T type) type`, used as `Box(i32)`)
- Comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`/`errdefer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values
+9 -1
View File
@@ -878,7 +878,7 @@
- equal structural values reuse specializations and stable emitted names, distinct values
specialize separately, aggregate inference uses exact type-factory provenance, and all
comptime parameters remain erased from the runtime ABI
- undefined values, pointers, functions, general slices, fallibles, ranges, and untagged
- undefined values, pointers, general slices, fallibles, ranges, and untagged
unions diagnose that they have no stable comptime identity
- `@std/meta.TypeInfo.enum` carries declaration-ordered `EnumInfo.fields`, enabling enum
formatting through `field!` without runtime reflection metadata
@@ -913,6 +913,14 @@
- native unsigned scalars and target-classified unsigned C scalars satisfy the constraint
- `isize` and `usize` remain concrete pointer-sized types
43. comptime function parameters (implemented)
- concrete native and C function values, function literals, and aggregates containing them
specialize by declaration identity rather than runtime address
- repeated declarations reuse specializations, distinct declarations specialize separately, and
function-valued parameters remain erased from the runtime ABI
- statically known callback invocations lower to direct calls; runtime-selected function pointers
remain indirect, and bodyless C declarations remain runtime-only during comptime execution
## A word on unchecked casts
For casts that bypass safety checks, Honey provides builtin functions:
+51 -3
View File
@@ -596,6 +596,9 @@ build_static_value :: proc(checker: ^Checker, value: Ct_Value, span: source.Span
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if value.kind == .Function {
return build_function_value(checker, ast.Function_Id(u32(value.index)), span, expected)
}
if value.kind == .Array || value.kind == .Struct || value.kind == .Range {
children := ct_child_slice(&checker.static_state, value)
args := make([]hir.Expr_Id, len(children), checker.allocator)
@@ -4728,7 +4731,7 @@ infer_expr :: proc(
_ = pop(&stack)
continue
}
if callee_type, handled := infer_qualified_value_field_type(checker, expr, locals, pkg, file); handled {
if callee_type, handled := infer_qualified_value_field_type(checker, expr, locals, pkg, file, demanded); handled {
_, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types)
if !ok {
last = types.INVALID
@@ -4757,6 +4760,19 @@ infer_expr :: proc(
callee_type := types.INVALID
if !symbol.is_valid(expr.qualifier) {
callee_type = find_infer_local(locals, expr.name)
if !types.is_valid(callee_type) {
if value, ok := current_comptime_value(checker, expr.name); ok && value.kind == .Static {
callee_type = value.type
if value.static_value != INVALID_CT_VALUE && int(value.static_value) < len(checker.static_state.values) {
function_value := checker.static_state.values[value.static_value]
if function_value.kind == .Function {
_, _, _ = function_pointer_type_for_template(
checker, ast.Function_Id(u32(function_value.index)), demanded,
)
}
}
}
}
}
if !types.is_valid(callee_type) && available {
global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
@@ -6690,12 +6706,24 @@ infer_qualified_value_field_type :: proc(
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
demanded: ^[dynamic]Spec_Id = nil,
) -> (types.Type, bool) {
if !symbol.is_valid(expr.qualifier) ||
find_import(checker, file, expr.qualifier) != ast.INVALID_IMPORT {
return types.INVALID, false
}
base_type := find_infer_local(locals, expr.qualifier)
if !types.is_valid(base_type) {
if value, ok := current_comptime_value(checker, expr.qualifier); ok && value.kind == .Static {
base_type = value.type
if field_value, found := persistent_field_value(checker, value.static_value, expr.name);
found && field_value.kind == .Function {
_, _, _ = function_pointer_type_for_template(
checker, ast.Function_Id(u32(field_value.index)), demanded,
)
}
}
}
if !types.is_valid(base_type) {
if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL {
base_type = checker.global_types[global]
@@ -8235,6 +8263,15 @@ build_expr :: proc(
non_callable = true
}
}
if callee == hir.INVALID_EXPR && !non_callable {
if value, ok := current_comptime_value(checker, expr.name);
ok && value.kind == .Static && value.static_value != INVALID_CT_VALUE &&
int(value.static_value) < len(checker.static_state.values) {
callee = build_static_value(
checker, checker.static_state.values[value.static_value], expr.span, types.INVALID,
)
}
}
}
if callee == hir.INVALID_EXPR && !non_callable {
if global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)); global != ast.INVALID_GLOBAL {
@@ -8757,9 +8794,20 @@ build_expr :: proc(
stack[frame_index].built_args = nil
last = invalid_hir_expr(checker, expr.span, id)
} else {
target := hir.INVALID_REF
callee := frame.left
callee_expr := checker.module.exprs[frame.left]
if callee_expr.kind == .Function {
function := hir.as_function(callee_expr.target)
if function != hir.INVALID_FUNCTION {
target = callee_expr.target
callee = hir.INVALID_EXPR
add_unique_function(calls, function)
}
}
last = add_hir_expr(checker, hir.Expr{
kind=.Call, span=expr.span, type=result, target=hir.INVALID_REF,
left=frame.left, right=hir.INVALID_EXPR, args=stack[frame_index].built_args,
kind=.Call, span=expr.span, type=result, target=target,
left=callee, right=hir.INVALID_EXPR, args=stack[frame_index].built_args,
diagnostic=source.INVALID_DIAGNOSTIC,
})
stack[frame_index].built_args = nil
+57 -1
View File
@@ -623,6 +623,14 @@ ct_coerce_value :: proc(state: ^Ct_State, id: Ct_Value_Id, expected: types.Type,
value.type = expected
return ct_add_value(state, value), true
}
if value.kind == .Function {
_, _, actual_function, actual_ok := types.function_pointer(value.type, store)
_, _, expected_function, expected_ok := types.function_pointer(expected, store)
if actual_ok && expected_ok && types.equal(actual_function, expected_function) {
value.type = expected
return ct_add_value(state, value), true
}
}
if value.kind == .Array {
expected_item, expected_ok := types.node(store, expected)
value_item, value_ok := types.node(store, value.type)
@@ -1040,6 +1048,11 @@ ct_eval_expr :: proc(
}
return ct_add_value(state, Ct_Value{kind=.String, type=value.type, index=string_id}), ct_flow(.Normal), true
}
if value.kind == .Static && value.static_value != INVALID_CT_VALUE &&
int(value.static_value) < len(checker.static_state.values) {
id := ct_clone_graph(state, &checker.static_state, value.static_value)
return ct_observe_value(state, id, expr.span)
}
return ct_add_value(state, Ct_Value{kind=.Type, type=types.INVALID, index=u64(value.type)}), ct_flow(.Normal), true
}
} else if find_import(checker, state.file, expr.qualifier) == ast.INVALID_IMPORT {
@@ -1084,6 +1097,22 @@ ct_eval_expr :: proc(
}
global_expected := type_from_syntax(checker, g.type, g.pkg, g.file)
return ct_eval_expr(state, g.expr, global_expected, depth+1)
case .Function_Literal:
template := ast.Function_Id(u32(expr.integer))
pointer_type, _, ok := function_pointer_type_for_template(
checker,
template,
state.demanded,
state.demanded != nil,
)
if !ok {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(
state, .Not_Comptime, expr.span, "function literal is not comptime-callable as a value",
)
}
return ct_add_value(state, Ct_Value{
kind=.Function, type=pointer_type, index=u64(template),
}), ct_flow(.Normal), true
case .Comptime:
if expr.left != ast.INVALID_EXPR {
return ct_eval_expr(state, expr.left, expected, depth+1)
@@ -2586,6 +2615,20 @@ 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, "unknown intrinsic '%s!'", symbol_text(checker, expr.name))
}
if symbol.is_valid(expr.qualifier) && find_import(checker, state.file, expr.qualifier) == ast.INVALID_IMPORT {
if index, ok := ct_find_binding_index(state, expr.qualifier); ok {
base := ct_binding_value(state, index)
callee, flow, field_ok := ct_eval_field_value(state, base, expr.name, expr.span)
if !field_ok || flow.kind != .Normal {
return INVALID_CT_VALUE, flow, field_ok
}
if callee != INVALID_CT_VALUE && int(callee) < len(state.values) && state.values[callee].kind == .Function {
return ct_eval_template_call(
state, ast.Function_Id(u32(state.values[callee].index)), expr.args, expr.span, expected, depth+1,
)
}
}
}
target_pkg, available := expr_package(checker, expr, state.pkg, state.file, false)
if !available {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "unavailable function package")
@@ -2697,6 +2740,13 @@ ct_eval_template_call :: proc(
state.result = previous_result
}
param_start := len(state.bindings)
for comptime_value in comptime_values {
if comptime_value.kind == .Static && comptime_value.static_value != INVALID_CT_VALUE &&
int(comptime_value.static_value) < len(checker.static_state.values) {
value := ct_clone_graph(state, &checker.static_state, comptime_value.static_value)
ct_bind_value(state, comptime_value.name, comptime_value.type, value, false)
}
}
for value, index in runtime_values {
ct_bind_value(state, runtime_names[index], runtime_types[index], value, false)
}
@@ -2884,6 +2934,12 @@ ct_write_comptime_key :: proc(state: ^Ct_State, id: Ct_Value_Id, builder: ^strin
}
strings.write_byte(builder, ';')
return true
case .Function:
if value.index >= u64(len(state.checker.ast_module.functions)) {
return false
}
fmt.sbprintf(builder, "fn%d;", value.index)
return true
case .Slice:
item, item_ok := types.container(value.type, &state.checker.module.types)
if !item_ok || item.kind != .Slice || item.mutable || item.child != types.U8 {
@@ -2935,7 +2991,7 @@ ct_write_comptime_key :: proc(state: ^Ct_State, id: Ct_Value_Id, builder: ^strin
}
strings.write_string(builder, "o;")
return true
case .Invalid, .Void, .Undefined, .Range, .Pointer, .Function, .Fallible:
case .Invalid, .Void, .Undefined, .Range, .Pointer, .Fallible:
return false
}
return false
+91 -4
View File
@@ -2789,9 +2789,7 @@ milestone_39_rejects_values_without_stable_identity :: proc(t: ^testing.T) {
text := `BadUnion :: union { number i32, flag bool }
Config :: struct { value i32 }
identity func() i32 { return 1 }
reject_pointer func($value @i32) void {}
reject_function func($value @func() i32) void {}
reject_slice func($value []i32) void {}
reject_range func($value range) void {}
reject_union func($value BadUnion) void {}
@@ -2802,7 +2800,6 @@ items [2]i32 :: [1, 2]
main func() void {
reject_pointer(&stored)
reject_function(identity)
reject_slice(items[..])
reject_range(0..3)
reject_union(BadUnion {number = 1})
@@ -2827,7 +2824,97 @@ main func() void {
found += 1
}
}
testing.expect(t, found >= 6)
testing.expect(t, found >= 5)
}
@(test)
comptime_function_parameters_specialize_and_lower_directly :: proc(t: ^testing.T) {
text := `Callback_Config :: struct { call @func(value i32) i32 }
increment func(value i32) i32 { return value + 1 }
decrement func(value i32) i32 { return value - 1 }
external c_func(value i32) i32
apply func($callback @func(value i32) i32, value i32) i32 {
return callback(value)
}
apply_c func($callback *c_func(value i32) i32, value i32) i32 {
return callback(value)
}
apply_config func($config Callback_Config, value i32) i32 {
return config.call(value)
}
main func() i32 {
a i32 :: apply(increment, 1)
b i32 :: apply(increment, 2)
c i32 :: apply(decrement, 3)
d i32 :: apply(func(value i32) i32 { return value + 2 }, 4)
e i32 :: apply_c(external, 5)
f i32 :: apply_config(Callback_Config {call = increment}, 6)
return a + b + c + d + e + f
}
`
stable_names: [dynamic]string
defer {
for name in stable_names {
delete(name)
}
delete(stable_names)
}
for pass := 0; pass < 2; pass += 1 {
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
ir_module := lower.lower(&hir_module)
apply_count := 0
callback_specializations := 0
for function in ir_module.functions {
plain_apply := strings.contains(function.link_name, "bro__p0__apply__")
callback_specialization := plain_apply ||
strings.contains(function.link_name, "bro__p0__apply_c__") ||
strings.contains(function.link_name, "bro__p0__apply_config__")
if !callback_specialization {
continue
}
if plain_apply {
apply_count += 1
}
if pass == 0 {
append(&stable_names, strings.clone(function.link_name))
} else {
testing.expect_value(t, function.link_name, stable_names[callback_specializations])
}
callback_specializations += 1
testing.expect_value(t, len(function.param_types), 1)
found_direct_call := false
for instruction in function.instructions {
testing.expect(t, instruction.op != .Function_Address)
if instruction.op == .Call {
testing.expect(t, ir.as_function(instruction.target) != ir.INVALID_FUNCTION)
found_direct_call = true
}
}
testing.expect(t, found_direct_call)
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, apply_count, 3)
testing.expect_value(t, callback_specializations, 5)
ir.destroy_module(&ir_module)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
+43
View File
@@ -9,6 +9,10 @@ Point :: struct {
y i32
}
Callback_Config :: struct {
call @func(value i32) i32
}
Box :: union(enum) {
point Point
empty void
@@ -78,6 +82,26 @@ increment func(value i32) i32 {
return value + 1
}
double func(value i32) i32 {
return value * 2
}
decrement_c c_func(value i32) i32 {
return value - 1
}
apply_comptime func($callback @func(value i32) i32, value i32) i32 {
return callback(value)
}
apply_comptime_c func($callback *c_func(value i32) i32, value i32) i32 {
return callback(value)
}
apply_comptime_config func($config Callback_Config, value i32) i32 {
return config.call(value)
}
call_native func(callback @func(value i32) i32, value i32) i32 {
return callback(value)
}
@@ -180,6 +204,10 @@ main func() i32 {
recovered i32 :: $recover()
storage i32 :: $storage_mutation()
called i32 :: $call_native(increment, 11)
comptime_callback i32 :: $apply_comptime(increment, 12)
comptime_literal i32 :: $apply_comptime(func(value i32) i32 { return value + 2 }, 12)
comptime_c_callback i32 :: $apply_comptime_c(decrement_c, 15)
comptime_config i32 :: $apply_comptime_config(Callback_Config {call = increment}, 15)
fallible_ok i32 :: $call_fallible(may_fail, false) catch 0
fallible_err i32 :: $call_fallible(may_fail, true) catch |e| {
result i32 :: match e {
@@ -239,5 +267,20 @@ main func() i32 {
if ERRDEFER != 42 {
return 17
}
if comptime_callback != 13 or comptime_literal != 14 or
comptime_c_callback != 14 or comptime_config != 16 {
return 18
}
if apply_comptime(increment, 20) != 21 or apply_comptime(increment, 21) != 22 or
apply_comptime(double, 20) != 40 {
return 19
}
if apply_comptime(func(value i32) i32 { return value + 3 }, 20) != 23 {
return 20
}
if apply_comptime_c(decrement_c, 20) != 19 or
apply_comptime_config(Callback_Config {call = increment}, 20) != 21 {
return 21
}
return 0
}