From 85693e57e1978727678beccb3f27d7ae14152028 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 18 Jul 2026 01:35:39 +0200 Subject: [PATCH] function values as comptime params --- LANGUAGE.md | 4 +- README.md | 2 +- TODO.md | 10 ++- compiler/checker/checker.odin | 54 ++++++++++++++- compiler/checker/comptime.odin | 58 +++++++++++++++- compiler_tests.odin | 95 ++++++++++++++++++++++++-- examples/programs/comptime_v1/main.bro | 43 ++++++++++++ 7 files changed, 254 insertions(+), 12 deletions(-) diff --git a/LANGUAGE.md b/LANGUAGE.md index 1939810..b76510b 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -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 diff --git a/README.md b/README.md index b3c614d..d7ac793 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/TODO.md b/TODO.md index 5f96b6d..c2e8b4c 100644 --- a/TODO.md +++ b/TODO.md @@ -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: diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index d518559..32fd1e1 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -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 diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 3d4774f..a20a6ad 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -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 diff --git a/compiler_tests.odin b/compiler_tests.odin index faaee67..d2154b4 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -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) diff --git a/examples/programs/comptime_v1/main.bro b/examples/programs/comptime_v1/main.bro index 89b5651..f784e07 100644 --- a/examples/programs/comptime_v1/main.bro +++ b/examples/programs/comptime_v1/main.bro @@ -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 }