whole-function comptime folding for zero-runtime value calls
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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,10 +5560,26 @@ 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,
|
||||
)
|
||||
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
|
||||
checker.current_comptime_values = comptime_values
|
||||
for source_index in 0..<len(expr.args) {
|
||||
@@ -5539,12 +5604,13 @@ infer_expr :: proc(
|
||||
if spec != INVALID_SPEC {
|
||||
last = checker.specs[spec].result
|
||||
} else {
|
||||
previous_comptime := checker.current_comptime_values
|
||||
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)
|
||||
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
|
||||
}
|
||||
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))
|
||||
|
||||
@@ -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)
|
||||
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
@@ -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,
|
||||
@@ -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 {
|
||||
if (left.len != right.len) return false
|
||||
for (0..left.len) |i| if (left[i] != right[i]) {
|
||||
return false
|
||||
}
|
||||
|
||||
i usize = 0
|
||||
while i < left.len : i += 1 {
|
||||
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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
Layout :: enum { auto c }
|
||||
Layout :: enum { auto, c }
|
||||
|
||||
ArrayInfo :: struct {
|
||||
child type
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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
|
||||
@@ -13,7 +13,11 @@ SourceLocation :: struct {
|
||||
|
||||
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,26 +30,42 @@ 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})
|
||||
else: if expected != actual {
|
||||
debug.print("{s}:{d}:{d}: expected {}, found {}\n", {
|
||||
location.file,
|
||||
location.line,
|
||||
location.column,
|
||||
expected,
|
||||
actual,
|
||||
})
|
||||
return .expectation_failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect_type func($Expected, $Actual type, _ Actual, location SourceLocation) void ! Error {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user