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
+1 -1
View File
@@ -209,6 +209,7 @@ exactly once. Bare functions named `memcopy` or `memset` remain ordinary user fu
- later comptime value parameters may depend on earlier type parameters, as in `factory func($T type, $default T) type`
- 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 bare function identities; equal structural values and aliases of one function declaration share specializations, while distinct declarations remain distinct and 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
- direct bodyful value calls with no runtime parameters, including parameterless and all-`$` functions, are evaluated at comptime when their resolved result can materialize; otherwise they retain their zero-argument runtime specialization, while a reached `compile_error!` remains a diagnostic
- 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`, exact type `==`/`!=`, 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
- comptime type factories such as `Box func($T type) type { return struct { value T } }`; calls like `Box(i32)` are concrete nominal types and may appear anywhere a type is expected
@@ -259,7 +260,6 @@ exactly once. Bare functions named `memcopy` or `memset` remain ordinary user fu
## PLANNED / DEFERRED
- native Brolang variadic functions
- exporting Brolang functions to C and broader target-specific C ABI lowering
- non-plain C record layouts such as bitfields, packed records, flexible arrays, qualified fields, and C variadic record arguments
- arenas, pools, build-mode heap policy, and escaping-allocation diagnostics
+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
+100 -17
View File
@@ -3370,7 +3370,8 @@ milestone_39_stable_values_and_richer_formatting_compile_and_run :: proc(t: ^tes
}
testing.expect(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, score_count, 2)
// score has only comptime parameters, so both calls materialize without HIR functions.
testing.expect_value(t, score_count, 0)
testing.expect_value(t, count_substring_occurrences(llvm_text, "define internal fastcc i32 @bro__p0__read_carrier__"), 1)
testing.expect(t, !strings.contains(llvm_text, "FormatToken"))
testing.expect(t, !strings.contains(llvm_text, "parse_format"))
@@ -4407,8 +4408,8 @@ main func() void {}
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 2)
testing.expect(t, strings.contains(llvm_text, "@bro__p0__make("))
testing.expect_value(t, len(hir_module.functions), 1)
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__make("))
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__unused_native("))
testing.expect(t, !strings.contains(llvm_text, "@unused_foreign("))
}
@@ -4511,6 +4512,91 @@ main func() i32 {
testing.expect(t, types.equal(hir_module.globals[1].type, types.I8))
}
@(test)
zero_runtime_calls_fold_with_runtime_fallback :: proc(t: ^testing.T) {
text := `runtime_value i32 = 41
folded func() i32 { return 42 }
by_value func($N usize) usize { return N }
backed func() []i32 {
values [2]mut i32 = [3, 4]
return values[..]
}
fallback func() i32 { return runtime_value }
undefined_result func() i32 {
value i32 = undefined
return value
}
main func() i32 {
if folded() != 42 { return 1 }
if by_value(7) != 7 { return 2 }
view :: backed()
if view[0] != 3 or view[1] != 4 { return 3 }
_ = undefined_result()
return fallback()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
folded_found := false
by_value_found := false
backed_found := false
fallback_found := false
undefined_found := false
for function in hir_module.functions {
name := symbol.resolve(&symbols, function.name)
folded_found = folded_found || name == "folded"
by_value_found = by_value_found || name == "by_value"
backed_found = backed_found || name == "backed"
fallback_found = fallback_found || name == "fallback"
undefined_found = undefined_found || name == "undefined_result"
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, !folded_found)
testing.expect(t, !by_value_found)
testing.expect(t, !backed_found)
testing.expect(t, fallback_found)
testing.expect(t, undefined_found)
}
@(test)
zero_runtime_fold_preserves_reached_compile_error :: proc(t: ^testing.T) {
text := `fail func() i32 {
compile_error!("folded failure")
return 0
}
main func() i32 { return fail() }
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "folded failure")
}
testing.expect(t, found)
}
@(test)
comptime_value_params_specialize_by_value_and_omit_runtime_args :: proc(t: ^testing.T) {
text := `make_array func($N usize) [N]u8 {
@@ -4990,10 +5076,6 @@ missing func() i32 {
if true {
}
}
escape_slice func() []i32 {
values [2]mut i32 = [1, 2]
return values[..]
}
GLOBAL :: 1
main func() void {
runtime i32 = 1
@@ -5027,7 +5109,6 @@ main func() void {
view []i32 = values[1..]
yield view
}
_ = $escape_slice()
_ = $spin()
_ = $missing()
_ = ${
@@ -5051,7 +5132,6 @@ main func() void {
runtime_only_count := 0
pointer_errors := 0
slice_errors := 0
found_expired := false
found_quota := false
found_missing := false
found_yield := false
@@ -5061,7 +5141,6 @@ main func() void {
runtime_only_count += 1 if strings.contains(message, "runtime-only") else 0
pointer_errors += 1 if strings.contains(message, "only immutable pointers to whole comptime arrays can materialize as runtime memory") else 0
slice_errors += 1 if strings.contains(message, "only immutable full-array comptime slices can materialize as runtime memory") else 0
found_expired = found_expired || strings.contains(message, "expired storage")
found_quota = found_quota || strings.contains(message, "comptime evaluation exceeded the step quota")
found_missing = found_missing || strings.contains(message, "did not return a value")
found_yield = found_yield || strings.contains(message, "a value block must end with an explicit 'yield'")
@@ -5070,7 +5149,6 @@ main func() void {
testing.expect(t, runtime_only_count >= 2)
testing.expect(t, pointer_errors >= 4)
testing.expect(t, slice_errors >= 2)
testing.expect(t, found_expired)
testing.expect(t, found_quota)
testing.expect(t, found_missing)
testing.expect(t, found_yield)
@@ -11419,7 +11497,8 @@ main func() i32 {
}
}
}
testing.expect_value(t, call_count, 2)
// make_array() is a zero-runtime value call and is materialized directly.
testing.expect_value(t, call_count, 1)
testing.expect_value(t, extract_count, 3)
testing.expect_value(t, select_count, 2)
testing.expect(t, pointer_add_count >= 1)
@@ -12026,8 +12105,9 @@ compound_assignment_evaluates_lvalue_once :: proc(t: ^testing.T) {
// A compound assignment to an indexed lvalue must compute the element address
// once and reuse it for the load and the store, rather than re-lowering the
// lvalue (which would re-evaluate any side-effecting index subexpression).
text := `bump func() usize {
return 1
text := `index usize = 1
bump func() usize {
return index
}
main func() i32 {
values [3]mut i32 = [10, 20, 30]
@@ -12076,11 +12156,13 @@ compound_assignment_evaluates_nested_locations_once :: proc(t: ^testing.T) {
text := `Box :: struct {
value i32
}
row_index usize = 0
column_index usize = 1
row func() usize {
return 0
return row_index
}
column func() usize {
return 1
return column_index
}
pointer_for func(value @mut i32) @mut i32 {
return value
@@ -13690,6 +13772,7 @@ contextual_inference_rejects_local_constant_that_does_not_fit :: proc(t: ^testin
contextual_inference_flows_through_numeric_arithmetic :: proc(t: ^testing.T) {
text := `take_u16 func(_ u16) void {}
take_f32 func(_ f32) void {}
runtime_seed f32 = 0
G :: 10
H u16 :: G + 2
GF :: 1.5
@@ -13700,7 +13783,7 @@ get func() f32 {
seed f32 :: 2.0
c :: seed + 3.0
d :: 4.0 + seed
return c + d
return c + d + runtime_seed
}
main func() void {
a :: 10
@@ -62,6 +62,13 @@ append func($T type, list @mut ArrayList(T), value T) void ! mem.AllocError {
return
}
pop func($T type, list @mut ArrayList(T)) ?T {
if (list.items.len == 0) return null
value :: list.items[list.items.len - 1]
list.items = list.items.ptr[..list.items.len - 1]
return value
}
clear func($T type, list @mut ArrayList(T)) void {
list.items = list.items.ptr[..0]
}
@@ -40,8 +40,8 @@ init func(
}
}
# free the entries in the hash map.
# note: this operation invalidates the map.
#! free the entries in the hash map.
#! note: this operation invalidates the map.
deinit func(
$K, $V type,
$hash_key func(key K) usize,
View File
View File
+27 -47
View File
@@ -28,29 +28,20 @@ raw_free func(allocator Allocator, memory ?*mut u8, size usize, alignment usize)
}
eql func($T type, left, right []T) bool {
if left.len != right.len {
return false
}
i usize = 0
while i < left.len : i += 1 {
if left[i] != right[i] {
return false
}
if (left.len != right.len) return false
for (0..left.len) |i| if (left[i] != right[i]) {
return false
}
return true
}
# allocate memory for a slice of type `T` with `count` elements.
#! allocate memory for a slice of type `T` with `count` elements.
alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError {
if count == 0 {
return empty_slice(T, 0)
}
if (count == 0) return empty_slice(T, 0)
element_size usize :: sizeof!(T)
if element_size == 0 {
return empty_slice(T, count)
}
if (element_size == 0) return empty_slice(T, count)
if count > divtrunc!(maxval!(usize), element_size) {
return .out_of_memory
}
@@ -63,9 +54,9 @@ alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError {
return .out_of_memory
}
# reallocate memory for a slice of type `T` with `new_count` elements.
# reallocating with `new_count == 0` will free the memory and return an empty slice.
# note: memory must be reallocated with the same allocator that was used to allocate it.
#! reallocate memory for a slice of type `T` with `new_count` elements.
#! reallocating with `new_count == 0` will free the memory and return an empty slice.
#! note: memory must be reallocated with the same allocator that was used to allocate it.
realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mut T ! AllocError {
if new_count == memory.len {
return memory
@@ -105,22 +96,25 @@ realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mu
return .out_of_memory
}
# free memory allocated for a slice of type `T`.
# note: memory must be freed with the same allocator that was used to allocate it.
#! free memory allocated for a slice of type `T`.
#! note: memory must be freed with the same allocator that was used to allocate it.
free func($T type, allocator Allocator, memory []T) void {
if memory.len != 0 and sizeof!(T) != 0 {
mutable_memory []mut T :: constcast!(memory)
raw_free(allocator, ptrcast!(u8, mutable_memory.ptr), memory.len * sizeof!(T), alignof!(T))
}
if (memory.len == 0 or sizeof!(T) == 0) return
raw_free(allocator, ptrcast!(
u8,
constcast!(memory).ptr),
memory.len * sizeof!(T),
alignof!(T),
)
}
# get an empty slice of type `T` with `count` elements.
#! get an empty slice of type `T` with `count` elements.
empty_slice func($T type, count usize) []mut T {
pointer *mut T :: ptrcast!(T, (&empty_storage).ptr)
return pointer[..count]
}
# get an empty slice of type `T` with 0 elements.
#! get an empty slice of type `T` with 0 elements.
empty func($T type) []mut T {
return empty_slice(T, 0)
}
@@ -130,26 +124,18 @@ hide empty_storage [1]mut u64 = [0]
hide malloc_alignment usize :: 16 # ponytail: aarch64-macos libc malloc alignment assumption.
hide power_of_two func(value usize) bool {
if value == 0 {
return false
}
if (value == 0) return false
current usize = value
while current > 1 {
half usize = divtrunc!(current, 2)
if half * 2 != current {
return false
}
if (half * 2 != current) return false
current = half
}
return true
}
hide c_alloc func(_ ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
if power_of_two(alignment) == false {
return null
}
if (power_of_two(alignment) == false) return null
if alignment <= malloc_alignment {
return ptrcast!(u8, c.malloc(c_ulong(size)))
@@ -157,17 +143,13 @@ hide c_alloc func(_ ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
memory [1]mut ?*mut anyopaque = [null]
status c_int = c.posix_memalign((&memory).ptr, c_ulong(alignment), c_ulong(size))
if status != 0 {
return null
}
if (status != 0) return null
return ptrcast!(u8, memory[0])
}
hide c_realloc func(_ ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 {
if power_of_two(alignment) == false {
return null
}
if (power_of_two(alignment) == false) return null
if new_size == 0 {
c.free(memory)
@@ -182,9 +164,7 @@ hide c_realloc func(_ ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size
new_memory ?*mut u8 = c_alloc(null, new_size, alignment)
if new_memory |new_bytes| {
copy_size usize = old_size
if new_size < copy_size {
copy_size = new_size
}
if (new_size < copy_size) copy_size = new_size
memcopy!(new_bytes[..copy_size], old_memory[..copy_size])
c.free(old_memory)
}
+4 -4
View File
@@ -1,4 +1,4 @@
Layout :: enum { auto c }
Layout :: enum { auto, c }
ArrayInfo :: struct {
child type
@@ -46,9 +46,9 @@ TypeInfo :: union(enum) {
EnumFieldStruct func($E, $Field type, $default ?Field) type {
match typeinfo!(E) {
.enum |info|: {
names [info.fields.len]mut []u8 = undefined
field_types [info.fields.len]mut type = undefined
defaults [info.fields.len]mut ?Field = undefined
names [info.fields.len]mut []u8 = undefined
field_types [info.fields.len]mut type = undefined
defaults [info.fields.len]mut ?Field = undefined
expand for info.fields |field, index| {
names[index] = field.name
field_types[index] = Field
-106
View File
@@ -1,106 +0,0 @@
import "@std/mem"
StaticStringMap func($V type) type {
return struct {
keys [][]u8
values []V
len_indexes []u32
min_len u32
max_len u32
}
}
hide Pair func($V type) type {
return struct { []u8, V }
}
init func($V type, $N usize, $entries [N]Pair(V)) StaticStringMap(V) {
return ${
if N > usize(maxval!(u32)) {
compile_error!("static string map has too many entries")
}
keys [N]mut []u8 = undefined
values [N]mut V = undefined
for entries |entry, i| {
if entry.0.len > usize(maxval!(u32)) {
compile_error!("static string map key is too long")
}
for (usize(0))..i |prior| {
if mem.eql(u8, entry.0, entries[prior].0) {
compile_error!("duplicate static string map key")
}
}
keys[i] = entry.0
values[i] = entry.1
}
result :: done: {
if N == 0 {
len_indexes [0]mut u32 = undefined
yield :done StaticStringMap(V) {
keys = keys[..],
values = values[..],
len_indexes = len_indexes[..],
min_len = 0,
max_len = 0,
}
}
# ponytail: insertion sort is compile-time O(N²); replace if large maps affect builds.
i usize = 1
while i < N : i += 1 {
key :: keys[i]
value :: values[i]
j usize = i
while j > 0 and keys[j - 1].len > key.len : j -= 1 {
keys[j] = keys[j - 1]
values[j] = values[j - 1]
}
keys[j] = key
values[j] = value
}
min_len u32 :: u32(keys[0].len)
max_len u32 :: u32(keys[N - 1].len)
len_indexes [usize(max_len) + 1]mut u32 = undefined
entry_index usize = 0
length usize = 0
while length <= usize(max_len) : length += 1 {
while entry_index < N and keys[entry_index].len < length : entry_index += 1 {}
len_indexes[length] = u32(entry_index)
}
yield :done StaticStringMap(V) {
keys = keys[..],
values = values[..],
len_indexes = len_indexes[..],
min_len = min_len,
max_len = max_len,
}
}
yield result
}
}
get func($V type, map @StaticStringMap(V), key []u8) ?V {
if map.keys.len == 0 or key.len > usize(maxval!(u32)) {
return null
}
length u32 :: u32(key.len)
if length < map.min_len or length > map.max_len {
return null
}
index usize = usize(map.len_indexes[usize(length)])
while index < map.keys.len {
candidate :: map.keys[index]
if candidate.len != key.len {
return null
}
if mem.eql(u8, candidate, key) {
return map.values[index]
}
index += 1
}
return null
}
@@ -0,0 +1,93 @@
import "@std/mem"
StaticStringMap func($V type) type {
return struct {
keys [][]u8
values []V
len_indexes []u32
min_len u32
max_len u32
}
}
hide Pair func($V type) type {
return struct { []u8, V }
}
init func($V type, $N usize, $entries [N]Pair(V)) StaticStringMap(V) {
if N > usize(maxval!(u32)) {
compile_error!("static string map has too many entries")
}
keys [N]mut []u8 = undefined
values [N]mut V = undefined
# assert no duplicate keys
for entries |entry, i| {
if entry.0.len > usize(maxval!(u32)) {
compile_error!("static string map key is too long")
}
for (0..i) |prior| if mem.eql(u8, entry.0, entries[prior].0) {
compile_error!("duplicate static string map key")
}
keys[i] = entry.0
values[i] = entry.1
}
if N == 0 {
len_indexes [0]u32 = undefined
return StaticStringMap(V){
keys = keys[..],
values = values[..],
len_indexes = len_indexes[..],
min_len = 0,
max_len = 0,
}
}
# fixme: insertion sort is compile-time O(N^2); replace if large maps affect builds
for 1..N |i| {
key :: keys[i]
value :: values[i]
j usize = i
while j > 0 and keys[j - 1].len > key.len : j -= 1 {
keys[j] = keys[j - 1]
values[j] = values[j - 1]
}
keys[j] = key
values[j] = value
}
min_len u32 :: u32(keys[0].len)
max_len u32 :: u32(keys[N - 1].len)
len_indexes [usize(max_len) + 1]mut u32 = undefined
entry_index usize = 0
for 0..=(usize(max_len)) |length| {
while entry_index < N and keys[entry_index].len < length : entry_index += 1 {}
len_indexes[length] = u32(entry_index)
}
return StaticStringMap(V) {
keys = keys[..],
values = values[..],
len_indexes = len_indexes[..],
min_len = min_len,
max_len = max_len,
}
}
get func($V type, map @StaticStringMap(V), key []u8) ?V {
if (map.keys.len == 0 or key.len > maxval!(u32)) return null
length u32 = u32(key.len)
if (length < map.min_len or length > map.max_len) return null
idx usize = usize(map.len_indexes[usize(length)])
while idx < map.keys.len : idx += 1 {
candidate :: map.keys[idx]
if (candidate.len != key.len) return null
if mem.eql(u8, candidate, key) return map.values[idx]
}
}
@@ -1,36 +0,0 @@
import "@std/testing"
TokenKind :: enum {
keyword_if
keyword_else
keyword_for
keyword_return
}
keywords StaticStringMap(TokenKind) = init([
{"return", .keyword_return},
{"if", .keyword_if},
{"for", .keyword_for},
{"else", .keyword_else},
])
handles_length_bucket_lookups test {
try testing.expect(keywords.keys.len == 4)
try testing.expect(keywords.values.len == keywords.keys.len)
try testing.expect(keywords.len_indexes.len == 7)
try testing.expect_equal(some!(TokenKind.keyword_if), get(&keywords, "if"))
try testing.expect_equal(some!(TokenKind.keyword_else), get(&keywords, "else"))
try testing.expect_equal(some!(TokenKind.keyword_for), get(&keywords, "for"))
try testing.expect_equal(some!(TokenKind.keyword_return), get(&keywords, "return"))
try testing.expect_equal(null, get(&keywords, "no"))
try testing.expect_equal(null, get(&keywords, "four"))
try testing.expect_equal(null, get(&keywords, "longer-than-any-key"))
}
handles_empty_maps test {
empty StaticStringMap(TokenKind) = init([])
try testing.expect(empty.keys.len == 0)
try testing.expect(empty.values.len == 0)
try testing.expect(empty.len_indexes.len == 0)
try testing.expect_equal(null, get(&empty, "if"))
}
+3 -1
View File
@@ -1,9 +1,11 @@
import "io"
import "enums"
import "hashmap"
import "arraylist"
import "static_string_map"
Io :: alias io.Io
ArrayList :: alias arraylist.ArrayList
EnumMap :: alias enums.EnumMap
ArrayList :: alias arraylist.ArrayList
StringHashMap :: alias hashmap.StringHashMap
StaticStringMap :: alias static_string_map.StaticStringMap
@@ -6,14 +6,18 @@ Error :: enum {
}
SourceLocation :: struct {
file []u8
line usize
file []u8
line usize
column usize
}
expect func(condition bool, location SourceLocation) void ! Error {
if !condition {
debug.print("{s}:{d}:{d}: expectation failed\n", {location.file, location.line, location.column})
debug.print("{s}:{d}:{d}: expectation failed\n", {
location.file,
location.line,
location.column,
})
return .expectation_failed
}
}
@@ -26,23 +30,39 @@ expect_equal func($T type, expected, actual T, location SourceLocation) void ! E
try expect_equal(expected_value, actual_value, location)
return
}
debug.print("{s}:{d}:{d}: expected an optional value, found null\n", {location.file, location.line, location.column})
debug.print("{s}:{d}:{d}: expected an optional value, found null\n", {
location.file,
location.line,
location.column,
})
return .expectation_failed
}
if actual |_| {
debug.print("{s}:{d}:{d}: expected null, found an optional value\n", {location.file, location.line, location.column})
debug.print("{s}:{d}:{d}: expected null, found an optional value\n", {
location.file,
location.line,
location.column,
})
return .expectation_failed
}
}
.slice: if !mem.eql(expected, actual) {
debug.print("{s}:{d}:{d}: expected and actual slices differ\n", {location.file, location.line, location.column})
debug.print("{s}:{d}:{d}: expected and actual slices differ\n", {
location.file,
location.line,
location.column,
})
return .expectation_failed
}
else: {
if expected != actual {
debug.print("{s}:{d}:{d}: expected {}, found {}\n", {location.file, location.line, location.column, expected, actual})
return .expectation_failed
}
else: if expected != actual {
debug.print("{s}:{d}:{d}: expected {}, found {}\n", {
location.file,
location.line,
location.column,
expected,
actual,
})
return .expectation_failed
}
}
}
@@ -53,10 +73,10 @@ expect_type func($Expected, $Actual type, _ Actual, location SourceLocation) voi
run func(name []u8, callback *func() void ! Error) bool {
callback() catch |_| {
debug.print("{s} [failed]\n", {name,})
debug.print("{s}...[failed]\n", {name,})
return false
}
debug.print("{s} [ok]\n", {name,})
debug.print("{s}...[ok]\n", {name,})
return true
}