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` - 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 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 - 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 }` - 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 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 - 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 ## PLANNED / DEFERRED
- native Brolang variadic functions
- exporting Brolang functions to C and broader target-specific C ABI lowering - 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 - 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 - arenas, pools, build-mode heap policy, and escaping-allocation diagnostics
+93 -3
View File
@@ -189,6 +189,13 @@ Type_Factory_Origin :: struct {
values: []Comptime_Value, values: []Comptime_Value,
} }
Call_Fold :: enum u8 {
Unknown,
Runtime,
Value,
Compile_Error,
}
Call_Resolution :: struct { Call_Resolution :: struct {
expr: ast.Expr_Id, expr: ast.Expr_Id,
ctx: []Comptime_Value, ctx: []Comptime_Value,
@@ -196,6 +203,9 @@ Call_Resolution :: struct {
mapping: []int, mapping: []int,
comptime_values: []Comptime_Value, comptime_values: []Comptime_Value,
runtime_types: []types.Type, runtime_types: []types.Type,
fold: Call_Fold,
folded_value: Ct_Value_Id,
diagnostic: source.Diagnostic_Id,
} }
Checker :: struct { Checker :: struct {
@@ -257,6 +267,7 @@ Checker :: struct {
generated_types: [dynamic]Generated_Type_Entry, generated_types: [dynamic]Generated_Type_Entry,
type_factory_origins: [dynamic]Type_Factory_Origin, type_factory_origins: [dynamic]Type_Factory_Origin,
call_resolutions: [dynamic]Call_Resolution, call_resolutions: [dynamic]Call_Resolution,
building_hir: bool,
target: target.Target, target: target.Target,
allocator: mem.Allocator, allocator: mem.Allocator,
} }
@@ -1106,6 +1117,13 @@ runtime_param_count :: proc(function: ast.Function) -> int {
return count 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 { comptime_param_count :: proc(function: ast.Function) -> int {
count := 0 count := 0
for param in function.params { for param in function.params {
@@ -3066,13 +3084,41 @@ find_call_resolution :: proc(
return -1, false 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( store_call_resolution :: proc(
checker: ^Checker, checker: ^Checker,
expr: ast.Expr_Id, expr: ast.Expr_Id,
mapping: []int, mapping: []int,
comptime_values: []Comptime_Value, comptime_values: []Comptime_Value,
runtime_types: []types.Type, 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{ entry := Call_Resolution{
expr=expr, expr=expr,
ctx=clone_comptime_values(checker.current_comptime_values, checker.allocator), ctx=clone_comptime_values(checker.current_comptime_values, checker.allocator),
@@ -3080,6 +3126,8 @@ store_call_resolution :: proc(
mapping=slice.clone(mapping, checker.allocator), mapping=slice.clone(mapping, checker.allocator),
comptime_values=clone_comptime_values(comptime_values, checker.allocator), comptime_values=clone_comptime_values(comptime_values, checker.allocator),
runtime_types=slice.clone(runtime_types, 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 { if index, ok := find_call_resolution(checker, expr); ok {
previous := checker.call_resolutions[index] previous := checker.call_resolutions[index]
@@ -3089,9 +3137,10 @@ store_call_resolution :: proc(
delete(previous.comptime_values, checker.allocator) delete(previous.comptime_values, checker.allocator)
delete(previous.runtime_types, checker.allocator) delete(previous.runtime_types, checker.allocator)
checker.call_resolutions[index] = entry checker.call_resolutions[index] = entry
return return index
} }
append(&checker.call_resolutions, entry) append(&checker.call_resolutions, entry)
return len(checker.call_resolutions)-1
} }
resolved_call_arg_expected :: proc( resolved_call_arg_expected :: proc(
@@ -5511,10 +5560,26 @@ infer_expr :: proc(
defer delete(comptime_values, checker.allocator) defer delete(comptime_values, checker.allocator)
if comptime_ok && if comptime_ok &&
can_specialize(checker, function, stack[frame_index].args, comptime_values) { can_specialize(checker, function, stack[frame_index].args, comptime_values) {
store_call_resolution( resolution := store_call_resolution(
checker, frame.expr, frame.mapping, checker, frame.expr, frame.mapping,
comptime_values, stack[frame_index].args, comptime_values, stack[frame_index].args,
) )
fold := Call_Fold.Runtime
if can_fold_zero_runtime_call(checker, function) {
fold = cache_zero_runtime_call(checker, resolution, frame.expr, pkg, file)
}
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)
}
value := checker.call_resolutions[resolution].folded_value
last = checker.static_state.values[value].type
} else {
previous_comptime := checker.current_comptime_values previous_comptime := checker.current_comptime_values
checker.current_comptime_values = comptime_values checker.current_comptime_values = comptime_values
for source_index in 0..<len(expr.args) { for source_index in 0..<len(expr.args) {
@@ -5539,12 +5604,13 @@ infer_expr :: proc(
if spec != INVALID_SPEC { if spec != INVALID_SPEC {
last = checker.specs[spec].result last = checker.specs[spec].result
} else { } else {
previous_comptime := checker.current_comptime_values previous_comptime = checker.current_comptime_values
checker.current_comptime_values = comptime_values checker.current_comptime_values = comptime_values
declared := function_channel_type(checker, function) declared := function_channel_type(checker, function)
checker.current_comptime_values = previous_comptime 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 last = declared if is_runtime_type(checker, declared) || types.is_void(declared) || types.is_noreturn(declared) else types.INVALID
} }
}
} else { } else {
declared := function_channel_type(checker, function) declared := function_channel_type(checker, function)
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT && if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT &&
@@ -9619,6 +9685,29 @@ build_expr :: proc(
} }
checker.current_comptime_values = previous_comptime 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 spec := INVALID_SPEC
if comptime_ok { if comptime_ok {
if frame.resolution >= 0 && frame.resolution < len(checker.call_resolutions) { if frame.resolution >= 0 && frame.resolution < len(checker.call_resolutions) {
@@ -14274,6 +14363,7 @@ check :: proc(
finalize_record_field_inference(&checker) finalize_record_field_inference(&checker)
validate_external_globals(&checker) validate_external_globals(&checker)
prune_specs(&checker) prune_specs(&checker)
checker.building_hir = true
build_globals(&checker) build_globals(&checker)
for index := 0; index < len(checker.specs); index += 1 { for index := 0; index < len(checker.specs); index += 1 {
build_function(&checker, spec_id(index)) build_function(&checker, spec_id(index))
+176 -5
View File
@@ -236,6 +236,7 @@ Ct_Value_Kind :: enum u8 {
Ct_Error_Kind :: enum u8 { Ct_Error_Kind :: enum u8 {
None, None,
Not_Comptime, Not_Comptime,
Compile_Error,
Overflow, Overflow,
Div_By_Zero, Div_By_Zero,
Non_Exact, Non_Exact,
@@ -325,6 +326,7 @@ Ct_State :: struct {
error: Ct_Error_Kind, error: Ct_Error_Kind,
diagnostic: source.Diagnostic_Id, diagnostic: source.Diagnostic_Id,
silent: bool, silent: bool,
foldable: bool,
demanded: ^[dynamic]Spec_Id, demanded: ^[dynamic]Spec_Id,
promoted_cells: [dynamic]Ct_Cell_Id, promoted_cells: [dynamic]Ct_Cell_Id,
promoted_globals: [dynamic]hir.Global_Id, promoted_globals: [dynamic]hir.Global_Id,
@@ -353,6 +355,7 @@ ct_state_make :: proc(
state.error = .None state.error = .None
state.diagnostic = source.INVALID_DIAGNOSTIC state.diagnostic = source.INVALID_DIAGNOSTIC
state.silent = !diagnose state.silent = !diagnose
state.foldable = true
state.demanded = demanded state.demanded = demanded
state.values.allocator = checker.allocator state.values.allocator = checker.allocator
state.children.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) 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 { 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) start := int(value.start)
end := start+int(value.count) end := start+int(value.count)
if start < 0 || end > len(state.children) { 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 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( ct_materialize_array_pointer :: proc(
state: ^Ct_State, state: ^Ct_State,
value: Ct_Value, value: Ct_Value,
@@ -976,6 +1069,12 @@ ct_materialize_value :: proc(
} }
value := state.values[materialized] value := state.values[materialized]
#partial switch value.kind { #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: case .Undefined:
if state.diagnostic == source.INVALID_DIAGNOSTIC { if state.diagnostic == source.INVALID_DIAGNOSTIC {
state.diagnostic = source.add(checker.diagnostics, span, "cannot materialize an undefined comptime value") 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) { 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 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 { if expr.left != ast.INVALID_EXPR {
callee, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) callee, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1)
if !ok || flow.kind != .Normal { 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 is_intrinsic_call(checker, expr, "some") {
if len(expr.args) != 1 { 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 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( ct_eval_template_call :: proc(
@@ -3457,6 +3565,7 @@ ct_eval_template_call :: proc(
span: source.Span, span: source.Span,
expected: types.Type, expected: types.Type,
depth: int, depth: int,
resolution := -1,
) -> (Ct_Value_Id, Ct_Flow, bool) { ) -> (Ct_Value_Id, Ct_Flow, bool) {
checker := state.checker checker := state.checker
if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) { 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 { 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)) 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)) 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) defer delete(comptime_values, checker.allocator)
if !comptime_ok { 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)) 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 defer checker.current_comptime_values = previous_comptime
result_type := function_channel_type(checker, function) 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: [dynamic]Ct_Value_Id
runtime_values.allocator = checker.allocator runtime_values.allocator = checker.allocator
runtime_types: [dynamic]types.Type runtime_types: [dynamic]types.Type
@@ -3570,6 +3694,9 @@ ct_eval_template_call :: proc(
state, .Not_Comptime, span, "comptime function returned an undefined value", 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) { 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") 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] value := ctx.src.values[id]
children := ct_child_slice(ctx.src, value) children := ct_child_slice(ctx.src, value)
if ct_value_has_children(value.kind) {
value.start = 0 value.start = 0
value.count = 0 value.count = 0
}
dst_id := ct_add_value(ctx.dst, value) dst_id := ct_add_value(ctx.dst, value)
ctx.values[id] = dst_id ctx.values[id] = dst_id
if len(children) > 0 { 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)) ctx.dst.values[dst_id].count = u32(len(children))
} }
if value.kind == .Pointer || value.kind == .Slice { 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 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) 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 { 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 := ct_clone_graph(&checker.static_state, source, id)
value_type := types.INVALID 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(t, loaded)
testing.expect_value(t, len(diagnostics.items), 0) 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_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, "FormatToken"))
testing.expect(t, !strings.contains(llvm_text, "parse_format")) testing.expect(t, !strings.contains(llvm_text, "parse_format"))
@@ -4407,8 +4408,8 @@ main func() void {}
defer delete(llvm_text) defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0) testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 2) 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__make("))
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__unused_native(")) testing.expect(t, !strings.contains(llvm_text, "@bro__p0__unused_native("))
testing.expect(t, !strings.contains(llvm_text, "@unused_foreign(")) 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)) 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) @(test)
comptime_value_params_specialize_by_value_and_omit_runtime_args :: proc(t: ^testing.T) { comptime_value_params_specialize_by_value_and_omit_runtime_args :: proc(t: ^testing.T) {
text := `make_array func($N usize) [N]u8 { text := `make_array func($N usize) [N]u8 {
@@ -4990,10 +5076,6 @@ missing func() i32 {
if true { if true {
} }
} }
escape_slice func() []i32 {
values [2]mut i32 = [1, 2]
return values[..]
}
GLOBAL :: 1 GLOBAL :: 1
main func() void { main func() void {
runtime i32 = 1 runtime i32 = 1
@@ -5027,7 +5109,6 @@ main func() void {
view []i32 = values[1..] view []i32 = values[1..]
yield view yield view
} }
_ = $escape_slice()
_ = $spin() _ = $spin()
_ = $missing() _ = $missing()
_ = ${ _ = ${
@@ -5051,7 +5132,6 @@ main func() void {
runtime_only_count := 0 runtime_only_count := 0
pointer_errors := 0 pointer_errors := 0
slice_errors := 0 slice_errors := 0
found_expired := false
found_quota := false found_quota := false
found_missing := false found_missing := false
found_yield := false found_yield := false
@@ -5061,7 +5141,6 @@ main func() void {
runtime_only_count += 1 if strings.contains(message, "runtime-only") else 0 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 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 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_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_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'") 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, runtime_only_count >= 2)
testing.expect(t, pointer_errors >= 4) testing.expect(t, pointer_errors >= 4)
testing.expect(t, slice_errors >= 2) testing.expect(t, slice_errors >= 2)
testing.expect(t, found_expired)
testing.expect(t, found_quota) testing.expect(t, found_quota)
testing.expect(t, found_missing) testing.expect(t, found_missing)
testing.expect(t, found_yield) 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, extract_count, 3)
testing.expect_value(t, select_count, 2) testing.expect_value(t, select_count, 2)
testing.expect(t, pointer_add_count >= 1) 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 // 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 // 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). // lvalue (which would re-evaluate any side-effecting index subexpression).
text := `bump func() usize { text := `index usize = 1
return 1 bump func() usize {
return index
} }
main func() i32 { main func() i32 {
values [3]mut i32 = [10, 20, 30] values [3]mut i32 = [10, 20, 30]
@@ -12076,11 +12156,13 @@ compound_assignment_evaluates_nested_locations_once :: proc(t: ^testing.T) {
text := `Box :: struct { text := `Box :: struct {
value i32 value i32
} }
row_index usize = 0
column_index usize = 1
row func() usize { row func() usize {
return 0 return row_index
} }
column func() usize { column func() usize {
return 1 return column_index
} }
pointer_for func(value @mut i32) @mut i32 { pointer_for func(value @mut i32) @mut i32 {
return value 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) { contextual_inference_flows_through_numeric_arithmetic :: proc(t: ^testing.T) {
text := `take_u16 func(_ u16) void {} text := `take_u16 func(_ u16) void {}
take_f32 func(_ f32) void {} take_f32 func(_ f32) void {}
runtime_seed f32 = 0
G :: 10 G :: 10
H u16 :: G + 2 H u16 :: G + 2
GF :: 1.5 GF :: 1.5
@@ -13700,7 +13783,7 @@ get func() f32 {
seed f32 :: 2.0 seed f32 :: 2.0
c :: seed + 3.0 c :: seed + 3.0
d :: 4.0 + seed d :: 4.0 + seed
return c + d return c + d + runtime_seed
} }
main func() void { main func() void {
a :: 10 a :: 10
@@ -62,6 +62,13 @@ append func($T type, list @mut ArrayList(T), value T) void ! mem.AllocError {
return 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 { clear func($T type, list @mut ArrayList(T)) void {
list.items = list.items.ptr[..0] list.items = list.items.ptr[..0]
} }
@@ -40,8 +40,8 @@ init func(
} }
} }
# free the entries in the hash map. #! free the entries in the hash map.
# note: this operation invalidates the map. #! note: this operation invalidates the map.
deinit func( deinit func(
$K, $V type, $K, $V type,
$hash_key func(key K) usize, $hash_key func(key K) usize,
View File
View File
+26 -46
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 { eql func($T type, left, right []T) bool {
if left.len != right.len { if (left.len != right.len) return false
for (0..left.len) |i| if (left[i] != right[i]) {
return false return false
} }
i usize = 0
while i < left.len : i += 1 {
if left[i] != right[i] {
return false
}
}
return true 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 { alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError {
if count == 0 { if (count == 0) return empty_slice(T, 0)
return empty_slice(T, 0)
}
element_size usize :: sizeof!(T) element_size usize :: sizeof!(T)
if element_size == 0 { if (element_size == 0) return empty_slice(T, count)
return empty_slice(T, count)
}
if count > divtrunc!(maxval!(usize), element_size) { if count > divtrunc!(maxval!(usize), element_size) {
return .out_of_memory return .out_of_memory
} }
@@ -63,9 +54,9 @@ alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError {
return .out_of_memory return .out_of_memory
} }
# reallocate memory for a slice of type `T` with `new_count` elements. #! 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. #! 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. #! 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 { realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mut T ! AllocError {
if new_count == memory.len { if new_count == memory.len {
return memory return memory
@@ -105,22 +96,25 @@ realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mu
return .out_of_memory return .out_of_memory
} }
# free memory allocated for a slice of type `T`. #! free memory allocated for a slice of type `T`.
# note: memory must be freed with the same allocator that was used to allocate it. #! note: memory must be freed with the same allocator that was used to allocate it.
free func($T type, allocator Allocator, memory []T) void { free func($T type, allocator Allocator, memory []T) void {
if memory.len != 0 and sizeof!(T) != 0 { if (memory.len == 0 or sizeof!(T) == 0) return
mutable_memory []mut T :: constcast!(memory) raw_free(allocator, ptrcast!(
raw_free(allocator, ptrcast!(u8, mutable_memory.ptr), memory.len * sizeof!(T), alignof!(T)) 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 { empty_slice func($T type, count usize) []mut T {
pointer *mut T :: ptrcast!(T, (&empty_storage).ptr) pointer *mut T :: ptrcast!(T, (&empty_storage).ptr)
return pointer[..count] 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 { empty func($T type) []mut T {
return empty_slice(T, 0) 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 malloc_alignment usize :: 16 # ponytail: aarch64-macos libc malloc alignment assumption.
hide power_of_two func(value usize) bool { hide power_of_two func(value usize) bool {
if value == 0 { if (value == 0) return false
return false
}
current usize = value current usize = value
while current > 1 { while current > 1 {
half usize = divtrunc!(current, 2) half usize = divtrunc!(current, 2)
if half * 2 != current { if (half * 2 != current) return false
return false
}
current = half current = half
} }
return true return true
} }
hide c_alloc func(_ ?@mut anyopaque, size usize, alignment usize) ?*mut u8 { hide c_alloc func(_ ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
if power_of_two(alignment) == false { if (power_of_two(alignment) == false) return null
return null
}
if alignment <= malloc_alignment { if alignment <= malloc_alignment {
return ptrcast!(u8, c.malloc(c_ulong(size))) 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] memory [1]mut ?*mut anyopaque = [null]
status c_int = c.posix_memalign((&memory).ptr, c_ulong(alignment), c_ulong(size)) status c_int = c.posix_memalign((&memory).ptr, c_ulong(alignment), c_ulong(size))
if status != 0 { if (status != 0) return null
return null
}
return ptrcast!(u8, memory[0]) return ptrcast!(u8, memory[0])
} }
hide c_realloc func(_ ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { 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 { if (power_of_two(alignment) == false) return null
return null
}
if new_size == 0 { if new_size == 0 {
c.free(memory) 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) new_memory ?*mut u8 = c_alloc(null, new_size, alignment)
if new_memory |new_bytes| { if new_memory |new_bytes| {
copy_size usize = old_size copy_size usize = old_size
if new_size < copy_size { if (new_size < copy_size) copy_size = new_size
copy_size = new_size
}
memcopy!(new_bytes[..copy_size], old_memory[..copy_size]) memcopy!(new_bytes[..copy_size], old_memory[..copy_size])
c.free(old_memory) c.free(old_memory)
} }
+1 -1
View File
@@ -1,4 +1,4 @@
Layout :: enum { auto c } Layout :: enum { auto, c }
ArrayInfo :: struct { ArrayInfo :: struct {
child type child type
-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 "io"
import "enums" import "enums"
import "hashmap"
import "arraylist" import "arraylist"
import "static_string_map" import "static_string_map"
Io :: alias io.Io Io :: alias io.Io
ArrayList :: alias arraylist.ArrayList
EnumMap :: alias enums.EnumMap EnumMap :: alias enums.EnumMap
ArrayList :: alias arraylist.ArrayList
StringHashMap :: alias hashmap.StringHashMap
StaticStringMap :: alias static_string_map.StaticStringMap StaticStringMap :: alias static_string_map.StaticStringMap
@@ -13,7 +13,11 @@ SourceLocation :: struct {
expect func(condition bool, location SourceLocation) void ! Error { expect func(condition bool, location SourceLocation) void ! Error {
if !condition { 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 return .expectation_failed
} }
} }
@@ -26,26 +30,42 @@ expect_equal func($T type, expected, actual T, location SourceLocation) void ! E
try expect_equal(expected_value, actual_value, location) try expect_equal(expected_value, actual_value, location)
return 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 return .expectation_failed
} }
if actual |_| { 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 return .expectation_failed
} }
} }
.slice: if !mem.eql(expected, actual) { .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 return .expectation_failed
} }
else: { else: if expected != actual {
if expected != actual { debug.print("{s}:{d}:{d}: expected {}, found {}\n", {
debug.print("{s}:{d}:{d}: expected {}, found {}\n", {location.file, location.line, location.column, expected, actual}) location.file,
location.line,
location.column,
expected,
actual,
})
return .expectation_failed return .expectation_failed
} }
} }
} }
}
expect_type func($Expected, $Actual type, _ Actual, location SourceLocation) void ! Error { expect_type func($Expected, $Actual type, _ Actual, location SourceLocation) void ! Error {
try expect($(Expected == Actual), location) try expect($(Expected == Actual), location)
@@ -53,10 +73,10 @@ expect_type func($Expected, $Actual type, _ Actual, location SourceLocation) voi
run func(name []u8, callback *func() void ! Error) bool { run func(name []u8, callback *func() void ! Error) bool {
callback() catch |_| { callback() catch |_| {
debug.print("{s} [failed]\n", {name,}) debug.print("{s}...[failed]\n", {name,})
return false return false
} }
debug.print("{s} [ok]\n", {name,}) debug.print("{s}...[ok]\n", {name,})
return true return true
} }