From 8b50eb7606f25ddcd886e3856ec46a7b339d3f3a Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Wed, 22 Jul 2026 23:11:48 +0200 Subject: [PATCH] whole-function comptime folding for zero-runtime value calls --- LANGUAGE.md | 2 +- compiler/checker/checker.odin | 142 +++++++++++--- compiler/checker/comptime.odin | 185 +++++++++++++++++- compiler_tests.odin | 117 +++++++++-- .../{arraylist.bro => arraylist.hon} | 7 + ...{arraylist.test.bro => arraylist.test.hon} | 0 std/build/{build.bro => build.hon} | 0 std/debug/{debug.bro => debug.hon} | 0 std/enums/{enums.bro => enums.hon} | 0 std/enums/{enums.test.bro => enums.test.hon} | 0 std/hashmap/{hashmap.bro => hashmap.hon} | 4 +- .../{hashmap.test.bro => hashmap.test.hon} | 0 std/io/{file.bro => file.hon} | 0 std/io/{io.bro => io.hon} | 0 std/mem/{mem.bro => mem.hon} | 74 +++---- std/meta/{meta.bro => meta.hon} | 8 +- std/meta/{meta.test.bro => meta.test.hon} | 0 std/process/{process.bro => process.hon} | 0 std/static_string_map/static_string_map.bro | 106 ---------- std/static_string_map/static_string_map.hon | 93 +++++++++ .../static_string_map.test.bro | 36 ---- .../static_string_map.test.hon | 0 std/{std.bro => std.hon} | 4 +- std/testing/{testing.bro => testing.hon} | 46 +++-- 24 files changed, 564 insertions(+), 260 deletions(-) rename std/arraylist/{arraylist.bro => arraylist.hon} (88%) rename std/arraylist/{arraylist.test.bro => arraylist.test.hon} (100%) rename std/build/{build.bro => build.hon} (100%) rename std/debug/{debug.bro => debug.hon} (100%) rename std/enums/{enums.bro => enums.hon} (100%) rename std/enums/{enums.test.bro => enums.test.hon} (100%) rename std/hashmap/{hashmap.bro => hashmap.hon} (97%) rename std/hashmap/{hashmap.test.bro => hashmap.test.hon} (100%) rename std/io/{file.bro => file.hon} (100%) rename std/io/{io.bro => io.hon} (100%) rename std/mem/{mem.bro => mem.hon} (75%) rename std/meta/{meta.bro => meta.hon} (82%) rename std/meta/{meta.test.bro => meta.test.hon} (100%) rename std/process/{process.bro => process.hon} (100%) delete mode 100644 std/static_string_map/static_string_map.bro create mode 100644 std/static_string_map/static_string_map.hon delete mode 100644 std/static_string_map/static_string_map.test.bro create mode 100644 std/static_string_map/static_string_map.test.hon rename std/{std.bro => std.hon} (78%) rename std/testing/{testing.bro => testing.hon} (64%) diff --git a/LANGUAGE.md b/LANGUAGE.md index eaf2798..d366e01 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -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 diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index ffe359b..44785b5 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -189,6 +189,13 @@ Type_Factory_Origin :: struct { values: []Comptime_Value, } +Call_Fold :: enum u8 { + Unknown, + Runtime, + Value, + Compile_Error, +} + Call_Resolution :: struct { expr: ast.Expr_Id, ctx: []Comptime_Value, @@ -196,6 +203,9 @@ Call_Resolution :: struct { mapping: []int, comptime_values: []Comptime_Value, runtime_types: []types.Type, + fold: Call_Fold, + folded_value: Ct_Value_Id, + diagnostic: source.Diagnostic_Id, } Checker :: struct { @@ -257,6 +267,7 @@ Checker :: struct { generated_types: [dynamic]Generated_Type_Entry, type_factory_origins: [dynamic]Type_Factory_Origin, call_resolutions: [dynamic]Call_Resolution, + building_hir: bool, target: target.Target, allocator: mem.Allocator, } @@ -1106,6 +1117,13 @@ runtime_param_count :: proc(function: ast.Function) -> int { return count } +can_fold_zero_runtime_call :: proc(checker: ^Checker, function: ast.Function) -> bool { + return function.has_body && !function.c_abi && runtime_param_count(function) == 0 && + !types.is_void(function.result) && + !types.is_constraint(function.result) && + !is_type_metatype_syntax(checker, function.result) +} + comptime_param_count :: proc(function: ast.Function) -> int { count := 0 for param in function.params { @@ -3066,13 +3084,41 @@ find_call_resolution :: proc( return -1, false } +call_resolution_matches :: proc( + entry: Call_Resolution, + mapping: []int, + comptime_values: []Comptime_Value, + runtime_types: []types.Type, +) -> bool { + if len(entry.mapping) != len(mapping) || len(entry.runtime_types) != len(runtime_types) || + !comptime_values_equal(entry.comptime_values, comptime_values) { + return false + } + for value, index in mapping { + if entry.mapping[index] != value { + return false + } + } + for value, index in runtime_types { + if !types.equal(entry.runtime_types[index], value) { + return false + } + } + return true +} + store_call_resolution :: proc( checker: ^Checker, expr: ast.Expr_Id, mapping: []int, comptime_values: []Comptime_Value, runtime_types: []types.Type, -) { +) -> int { + if index, ok := find_call_resolution(checker, expr); ok && call_resolution_matches( + checker.call_resolutions[index], mapping, comptime_values, runtime_types, + ) { + return index + } entry := Call_Resolution{ expr=expr, ctx=clone_comptime_values(checker.current_comptime_values, checker.allocator), @@ -3080,6 +3126,8 @@ store_call_resolution :: proc( mapping=slice.clone(mapping, checker.allocator), comptime_values=clone_comptime_values(comptime_values, checker.allocator), runtime_types=slice.clone(runtime_types, checker.allocator), + folded_value=INVALID_CT_VALUE, + diagnostic=source.INVALID_DIAGNOSTIC, } if index, ok := find_call_resolution(checker, expr); ok { previous := checker.call_resolutions[index] @@ -3089,9 +3137,10 @@ store_call_resolution :: proc( delete(previous.comptime_values, checker.allocator) delete(previous.runtime_types, checker.allocator) checker.call_resolutions[index] = entry - return + return index } append(&checker.call_resolutions, entry) + return len(checker.call_resolutions)-1 } resolved_call_arg_expected :: proc( @@ -5511,39 +5560,56 @@ infer_expr :: proc( defer delete(comptime_values, checker.allocator) if comptime_ok && can_specialize(checker, function, stack[frame_index].args, comptime_values) { - store_call_resolution( + resolution := store_call_resolution( checker, frame.expr, frame.mapping, comptime_values, stack[frame_index].args, ) - previous_comptime := checker.current_comptime_values - checker.current_comptime_values = comptime_values - for source_index in 0..= len(function.params) || function.params[param_index].comptime_value { - continue - } - demand := call_arg_expected(checker, function, param_index) - record_demand(checker, expr.args[source_index], demand, locals, local_types, pkg, file) + fold := Call_Fold.Runtime + if can_fold_zero_runtime_call(checker, function) { + fold = cache_zero_runtime_call(checker, resolution, frame.expr, pkg, file) } - checker.current_comptime_values = previous_comptime - spec := INVALID_SPEC - if demanded == nil { - spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values) - } else { - spec = find_spec(checker, frame.template, stack[frame_index].args, comptime_values) - if spec == INVALID_SPEC { - spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values) + if fold == .Compile_Error { + last = types.INVALID + } else if fold == .Value { + // Folded specializations are still inferred to validate their bodies and + // participate in the type-demand fixpoint. Leaving them undemanded keeps + // prune_specs from emitting an otherwise unused runtime function. + if !checker.building_hir && demanded == nil && !function_has_comptime_params(function) { + _ = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values) } - mark_spec_demanded(checker, spec, demanded) - } - if spec != INVALID_SPEC { - last = checker.specs[spec].result + value := checker.call_resolutions[resolution].folded_value + last = checker.static_state.values[value].type } else { previous_comptime := checker.current_comptime_values checker.current_comptime_values = comptime_values - declared := function_channel_type(checker, function) + for source_index in 0..= len(function.params) || function.params[param_index].comptime_value { + continue + } + demand := call_arg_expected(checker, function, param_index) + record_demand(checker, expr.args[source_index], demand, locals, local_types, pkg, file) + } checker.current_comptime_values = previous_comptime - last = declared if is_runtime_type(checker, declared) || types.is_void(declared) || types.is_noreturn(declared) else types.INVALID + spec := INVALID_SPEC + if demanded == nil { + spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values) + } else { + spec = find_spec(checker, frame.template, stack[frame_index].args, comptime_values) + if spec == INVALID_SPEC { + spec = ensure_spec(checker, frame.template, stack[frame_index].args, comptime_values) + } + mark_spec_demanded(checker, spec, demanded) + } + if spec != INVALID_SPEC { + last = checker.specs[spec].result + } else { + previous_comptime = checker.current_comptime_values + checker.current_comptime_values = comptime_values + declared := function_channel_type(checker, function) + checker.current_comptime_values = previous_comptime + last = declared if is_runtime_type(checker, declared) || types.is_void(declared) || types.is_noreturn(declared) else types.INVALID + } } } else { declared := function_channel_type(checker, function) @@ -9619,6 +9685,29 @@ build_expr :: proc( } checker.current_comptime_values = previous_comptime } + fold := Call_Fold.Runtime + if comptime_ok && can_fold_zero_runtime_call(checker, function) && + frame.resolution >= 0 && frame.resolution < len(checker.call_resolutions) { + fold = cache_zero_runtime_call(checker, frame.resolution, frame.expr, pkg, file) + } + if comptime_ok && arg_violation == source.INVALID_DIAGNOSTIC && + (fold == .Value || fold == .Compile_Error) { + if fold == .Value { + value := checker.call_resolutions[frame.resolution].folded_value + last = ct_materialize_value(&checker.static_state, value, expr.span, frame.expected) + } else { + diagnostic := checker.call_resolutions[frame.resolution].diagnostic + last = invalid_hir_expr(checker, expr.span, diagnostic, frame.expected) + } + delete(stack[frame_index].arg_types, checker.allocator) + stack[frame_index].arg_types = nil + delete(stack[frame_index].built_args, checker.allocator) + stack[frame_index].built_args = nil + delete(stack[frame_index].mapping, checker.allocator) + stack[frame_index].mapping = nil + _ = pop(&stack) + continue + } spec := INVALID_SPEC if comptime_ok { if frame.resolution >= 0 && frame.resolution < len(checker.call_resolutions) { @@ -14274,6 +14363,7 @@ check :: proc( finalize_record_field_inference(&checker) validate_external_globals(&checker) prune_specs(&checker) + checker.building_hir = true build_globals(&checker) for index := 0; index < len(checker.specs); index += 1 { build_function(&checker, spec_id(index)) diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index e5c7a4f..5d8db2a 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -236,6 +236,7 @@ Ct_Value_Kind :: enum u8 { Ct_Error_Kind :: enum u8 { None, Not_Comptime, + Compile_Error, Overflow, Div_By_Zero, Non_Exact, @@ -325,6 +326,7 @@ Ct_State :: struct { error: Ct_Error_Kind, diagnostic: source.Diagnostic_Id, silent: bool, + foldable: bool, demanded: ^[dynamic]Spec_Id, promoted_cells: [dynamic]Ct_Cell_Id, promoted_globals: [dynamic]hir.Global_Id, @@ -353,6 +355,7 @@ ct_state_make :: proc( state.error = .None state.diagnostic = source.INVALID_DIAGNOSTIC state.silent = !diagnose + state.foldable = true state.demanded = demanded state.values.allocator = checker.allocator state.children.allocator = checker.allocator @@ -479,7 +482,15 @@ ct_pop_bindings :: proc(state: ^Ct_State, start: int) { resize(&state.bindings, start) } +ct_value_has_children :: proc(kind: Ct_Value_Kind) -> bool { + return kind == .Range || kind == .Array || kind == .Struct || + kind == .Optional_Some || kind == .Fallible +} + ct_child_slice :: proc(state: ^Ct_State, value: Ct_Value) -> []Ct_Value_Id { + if !ct_value_has_children(value.kind) { + return nil + } start := int(value.start) end := start+int(value.count) if start < 0 || end > len(state.children) { @@ -850,6 +861,88 @@ ct_value_references_dead_storage :: proc(state: ^Ct_State, id: Ct_Value_Id) -> b return false } +ct_retain_value_storage :: proc(state: ^Ct_State, id: Ct_Value_Id, depth := 0) { + if depth > 64 || id == INVALID_CT_VALUE || int(id) >= len(state.values) { + return + } + value := state.values[id] + if value.kind == .Pointer || value.kind == .Slice { + place_id := Ct_Place_Id(value.index) + if place_id != INVALID_CT_PLACE && int(place_id) < len(state.places) { + cell := state.places[place_id].cell + if cell != INVALID_CT_CELL && int(cell) < len(state.cells) { + state.cells[cell].live = true + } + } + } + for child in ct_child_slice(state, value) { + ct_retain_value_storage(state, child, depth+1) + } +} + +ct_value_can_materialize :: proc(state: ^Ct_State, id: Ct_Value_Id, depth := 0) -> bool { + if depth > 64 || id == INVALID_CT_VALUE || int(id) >= len(state.values) { + return false + } + value := state.values[id] + #partial switch value.kind { + case .Invalid, .Undefined, .Function, .Type: + return false + case .Void, .Integer, .Float, .Bool, .String, .Null: + return true + case .Pointer: + store := &state.checker.module.types + pointer, pointer_ok := types.node(store, value.type) + place_id := Ct_Place_Id(value.index) + if !pointer_ok || pointer.kind != .Pointer || pointer.mutable || + place_id == INVALID_CT_PLACE || int(place_id) >= len(state.places) { + return false + } + place := state.places[place_id] + if place.cell == INVALID_CT_CELL || int(place.cell) >= len(state.cells) || len(ct_place_path(state, place)) != 0 { + return false + } + root_id := state.cells[place.cell].value + if root_id == INVALID_CT_VALUE || int(root_id) >= len(state.values) { + return false + } + root := state.values[root_id] + array, array_ok := types.node(store, root.type) + return array_ok && array.kind == .Array && + ((pointer.many && value.active == 0 && types.equal(pointer.child, array.child)) || + (!pointer.many && value.active == -1 && types.equal(pointer.child, root.type))) && + ct_value_can_materialize(state, root_id, depth+1) + case .Slice: + store := &state.checker.module.types + slice, slice_ok := types.node(store, value.type) + place_id := Ct_Place_Id(value.index) + if !slice_ok || slice.kind != .Slice || slice.mutable || value.start != 0 || + place_id == INVALID_CT_PLACE || int(place_id) >= len(state.places) { + return false + } + place := state.places[place_id] + if place.cell == INVALID_CT_CELL || int(place.cell) >= len(state.cells) || len(ct_place_path(state, place)) != 0 { + return false + } + root_id := state.cells[place.cell].value + if root_id == INVALID_CT_VALUE || int(root_id) >= len(state.values) { + return false + } + root := state.values[root_id] + array, array_ok := types.node(store, root.type) + return array_ok && array.kind == .Array && u64(value.count) == array.count && + types.equal(slice.child, array.child) && ct_value_can_materialize(state, root_id, depth+1) + case .Range, .Array, .Struct, .Optional_Some, .Fallible: + for child in ct_child_slice(state, value) { + if child != INVALID_CT_VALUE && !ct_value_can_materialize(state, child, depth+1) { + return false + } + } + return true + } + return false +} + ct_materialize_array_pointer :: proc( state: ^Ct_State, value: Ct_Value, @@ -976,6 +1069,12 @@ ct_materialize_value :: proc( } value := state.values[materialized] #partial switch value.kind { + case .Void: + return add_hir_expr(checker, hir.Expr{ + kind=.Void, span=span, type=types.VOID, + target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) case .Undefined: if state.diagnostic == source.INVALID_DIAGNOSTIC { state.diagnostic = source.add(checker.diagnostics, span, "cannot materialize an undefined comptime value") @@ -3234,6 +3333,12 @@ ct_struct_type :: proc( ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type, depth: int, expr_id := ast.INVALID_EXPR) -> (Ct_Value_Id, Ct_Flow, bool) { checker := state.checker + resolution := -1 + if expr_id != ast.INVALID_EXPR { + if index, ok := find_call_resolution(checker, expr_id); ok { + resolution = index + } + } if expr.left != ast.INVALID_EXPR { callee, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) if !ok || flow.kind != .Normal { @@ -3254,7 +3359,7 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type } } } - return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, message) + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Compile_Error, expr.span, message) } if is_intrinsic_call(checker, expr, "some") { if len(expr.args) != 1 { @@ -3447,7 +3552,10 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type } return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unresolved function '%s'", symbol_text(checker, expr.name)) } - return ct_eval_template_call(state, template, expr.args, expr.span, expected, depth+1) + if runtime_param_count(checker.ast_module.functions[template]) != 0 { + resolution = -1 + } + return ct_eval_template_call(state, template, expr.args, expr.span, expected, depth+1, resolution) } ct_eval_template_call :: proc( @@ -3457,6 +3565,7 @@ ct_eval_template_call :: proc( span: source.Span, expected: types.Type, depth: int, + resolution := -1, ) -> (Ct_Value_Id, Ct_Flow, bool) { checker := state.checker if template == ast.INVALID_FUNCTION || int(template) >= len(checker.ast_module.functions) { @@ -3466,10 +3575,22 @@ ct_eval_template_call :: proc( if !function.has_body || len(function.unsupported_reason) > 0 { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "function '%s' is runtime-only", symbol_text(checker, function.name)) } - if !valid_call_arity(function, len(args)) { + resolved := resolution >= 0 && resolution < len(checker.call_resolutions) + if !resolved && !valid_call_arity(function, len(args)) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "function '%s' arity mismatch", symbol_text(checker, function.name)) } - comptime_values, comptime_ok := collect_comptime_values(checker, function, args, state.pkg, state.file, false, checker.current_comptime_values) + comptime_values: []Comptime_Value + comptime_ok := false + if resolved { + comptime_values = clone_comptime_values( + checker.call_resolutions[resolution].comptime_values, checker.allocator, + ) + comptime_ok = true + } else { + comptime_values, comptime_ok = collect_comptime_values( + checker, function, args, state.pkg, state.file, false, checker.current_comptime_values, + ) + } defer delete(comptime_values, checker.allocator) if !comptime_ok { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "invalid comptime argument for '%s'", symbol_text(checker, function.name)) @@ -3479,6 +3600,9 @@ ct_eval_template_call :: proc( defer checker.current_comptime_values = previous_comptime result_type := function_channel_type(checker, function) + if types.is_void(result_type) || types.kind(result_type, &checker.module.types) == .Fallible { + state.foldable = false + } runtime_values: [dynamic]Ct_Value_Id runtime_values.allocator = checker.allocator runtime_types: [dynamic]types.Type @@ -3570,6 +3694,9 @@ ct_eval_template_call :: proc( state, .Not_Comptime, span, "comptime function returned an undefined value", ) } + if runtime_param_count(function) == 0 { + ct_retain_value_storage(state, result) + } if ct_value_references_dead_storage(state, result) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime function returned a pointer to expired storage") } @@ -3616,8 +3743,10 @@ ct_clone_graph_value :: proc(ctx: ^Ct_Clone_Context, id: Ct_Value_Id) -> Ct_Valu } value := ctx.src.values[id] children := ct_child_slice(ctx.src, value) - value.start = 0 - value.count = 0 + if ct_value_has_children(value.kind) { + value.start = 0 + value.count = 0 + } dst_id := ct_add_value(ctx.dst, value) ctx.values[id] = dst_id if len(children) > 0 { @@ -3632,7 +3761,8 @@ ct_clone_graph_value :: proc(ctx: ^Ct_Clone_Context, id: Ct_Value_Id) -> Ct_Valu ctx.dst.values[dst_id].count = u32(len(children)) } if value.kind == .Pointer || value.kind == .Slice { - ctx.dst.values[dst_id].index = u64(ct_clone_graph_place(ctx, Ct_Place_Id(value.index))) + cloned_place := ct_clone_graph_place(ctx, Ct_Place_Id(value.index)) + ctx.dst.values[dst_id].index = u64(cloned_place) } return dst_id } @@ -3692,6 +3822,47 @@ ct_clone_graph :: proc(dst, src: ^Ct_State, id: Ct_Value_Id) -> Ct_Value_Id { return ct_clone_graph_value(&ctx, id) } +cache_zero_runtime_call :: proc( + checker: ^Checker, + resolution: int, + expr: ast.Expr_Id, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> Call_Fold { + if resolution < 0 || resolution >= len(checker.call_resolutions) { + return .Runtime + } + if checker.call_resolutions[resolution].fold != .Unknown { + return checker.call_resolutions[resolution].fold + } + state := ct_state_make( + checker, pkg, file, values=checker.current_comptime_values, diagnose=false, + ) + value, flow, ok := ct_eval_expr(&state, expr) + if ok && flow.kind == .Normal && state.foldable && ct_value_can_materialize(&state, value) { + checker.call_resolutions[resolution].fold = .Value + checker.call_resolutions[resolution].folded_value = ct_clone_graph( + &checker.static_state, &state, value, + ) + ct_state_destroy(&state) + return .Value + } + error := state.error + ct_state_destroy(&state) + if error == .Compile_Error { + diagnosed := ct_state_make( + checker, pkg, file, values=checker.current_comptime_values, + ) + _, _, _ = ct_eval_expr(&diagnosed, expr) + checker.call_resolutions[resolution].fold = .Compile_Error + checker.call_resolutions[resolution].diagnostic = diagnosed.diagnostic + ct_state_destroy(&diagnosed) + return .Compile_Error + } + checker.call_resolutions[resolution].fold = .Runtime + return .Runtime +} + store_static_binding :: proc(checker: ^Checker, source: ^Ct_State, id: Ct_Value_Id, name: symbol.Id) -> Static_Binding { value := ct_clone_graph(&checker.static_state, source, id) value_type := types.INVALID diff --git a/compiler_tests.odin b/compiler_tests.odin index 70d0d85..01c21a0 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -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 diff --git a/std/arraylist/arraylist.bro b/std/arraylist/arraylist.hon similarity index 88% rename from std/arraylist/arraylist.bro rename to std/arraylist/arraylist.hon index 9088da7..4eb28d1 100644 --- a/std/arraylist/arraylist.bro +++ b/std/arraylist/arraylist.hon @@ -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] } diff --git a/std/arraylist/arraylist.test.bro b/std/arraylist/arraylist.test.hon similarity index 100% rename from std/arraylist/arraylist.test.bro rename to std/arraylist/arraylist.test.hon diff --git a/std/build/build.bro b/std/build/build.hon similarity index 100% rename from std/build/build.bro rename to std/build/build.hon diff --git a/std/debug/debug.bro b/std/debug/debug.hon similarity index 100% rename from std/debug/debug.bro rename to std/debug/debug.hon diff --git a/std/enums/enums.bro b/std/enums/enums.hon similarity index 100% rename from std/enums/enums.bro rename to std/enums/enums.hon diff --git a/std/enums/enums.test.bro b/std/enums/enums.test.hon similarity index 100% rename from std/enums/enums.test.bro rename to std/enums/enums.test.hon diff --git a/std/hashmap/hashmap.bro b/std/hashmap/hashmap.hon similarity index 97% rename from std/hashmap/hashmap.bro rename to std/hashmap/hashmap.hon index 6141049..0080d05 100644 --- a/std/hashmap/hashmap.bro +++ b/std/hashmap/hashmap.hon @@ -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, diff --git a/std/hashmap/hashmap.test.bro b/std/hashmap/hashmap.test.hon similarity index 100% rename from std/hashmap/hashmap.test.bro rename to std/hashmap/hashmap.test.hon diff --git a/std/io/file.bro b/std/io/file.hon similarity index 100% rename from std/io/file.bro rename to std/io/file.hon diff --git a/std/io/io.bro b/std/io/io.hon similarity index 100% rename from std/io/io.bro rename to std/io/io.hon diff --git a/std/mem/mem.bro b/std/mem/mem.hon similarity index 75% rename from std/mem/mem.bro rename to std/mem/mem.hon index 1484a8f..555065c 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.hon @@ -28,29 +28,20 @@ raw_free func(allocator Allocator, memory ?*mut u8, size usize, alignment usize) } eql func($T type, left, right []T) bool { - if left.len != right.len { - return false - } - - i usize = 0 - while i < left.len : i += 1 { - if left[i] != right[i] { - return false - } + if (left.len != right.len) return false + for (0..left.len) |i| if (left[i] != right[i]) { + return false } return true } -# allocate memory for a slice of type `T` with `count` elements. +#! allocate memory for a slice of type `T` with `count` elements. alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError { - if count == 0 { - return empty_slice(T, 0) - } + if (count == 0) return empty_slice(T, 0) element_size usize :: sizeof!(T) - if element_size == 0 { - return empty_slice(T, count) - } + if (element_size == 0) return empty_slice(T, count) + if count > divtrunc!(maxval!(usize), element_size) { return .out_of_memory } @@ -63,9 +54,9 @@ alloc func($T type, allocator Allocator, count usize) []mut T ! AllocError { return .out_of_memory } -# reallocate memory for a slice of type `T` with `new_count` elements. -# reallocating with `new_count == 0` will free the memory and return an empty slice. -# note: memory must be reallocated with the same allocator that was used to allocate it. +#! reallocate memory for a slice of type `T` with `new_count` elements. +#! reallocating with `new_count == 0` will free the memory and return an empty slice. +#! note: memory must be reallocated with the same allocator that was used to allocate it. realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mut T ! AllocError { if new_count == memory.len { return memory @@ -105,22 +96,25 @@ realloc func($T type, allocator Allocator, memory []mut T, new_count usize) []mu return .out_of_memory } -# free memory allocated for a slice of type `T`. -# note: memory must be freed with the same allocator that was used to allocate it. +#! free memory allocated for a slice of type `T`. +#! note: memory must be freed with the same allocator that was used to allocate it. free func($T type, allocator Allocator, memory []T) void { - if memory.len != 0 and sizeof!(T) != 0 { - mutable_memory []mut T :: constcast!(memory) - raw_free(allocator, ptrcast!(u8, mutable_memory.ptr), memory.len * sizeof!(T), alignof!(T)) - } + if (memory.len == 0 or sizeof!(T) == 0) return + raw_free(allocator, ptrcast!( + u8, + constcast!(memory).ptr), + memory.len * sizeof!(T), + alignof!(T), + ) } -# get an empty slice of type `T` with `count` elements. +#! get an empty slice of type `T` with `count` elements. empty_slice func($T type, count usize) []mut T { pointer *mut T :: ptrcast!(T, (&empty_storage).ptr) return pointer[..count] } -# get an empty slice of type `T` with 0 elements. +#! get an empty slice of type `T` with 0 elements. empty func($T type) []mut T { return empty_slice(T, 0) } @@ -130,26 +124,18 @@ hide empty_storage [1]mut u64 = [0] hide malloc_alignment usize :: 16 # ponytail: aarch64-macos libc malloc alignment assumption. hide power_of_two func(value usize) bool { - if value == 0 { - return false - } - + if (value == 0) return false current usize = value while current > 1 { half usize = divtrunc!(current, 2) - if half * 2 != current { - return false - } + if (half * 2 != current) return false current = half } - return true } hide c_alloc func(_ ?@mut anyopaque, size usize, alignment usize) ?*mut u8 { - if power_of_two(alignment) == false { - return null - } + if (power_of_two(alignment) == false) return null if alignment <= malloc_alignment { return ptrcast!(u8, c.malloc(c_ulong(size))) @@ -157,17 +143,13 @@ hide c_alloc func(_ ?@mut anyopaque, size usize, alignment usize) ?*mut u8 { memory [1]mut ?*mut anyopaque = [null] status c_int = c.posix_memalign((&memory).ptr, c_ulong(alignment), c_ulong(size)) - if status != 0 { - return null - } + if (status != 0) return null return ptrcast!(u8, memory[0]) } hide c_realloc func(_ ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { - if power_of_two(alignment) == false { - return null - } + if (power_of_two(alignment) == false) return null if new_size == 0 { c.free(memory) @@ -182,9 +164,7 @@ hide c_realloc func(_ ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size new_memory ?*mut u8 = c_alloc(null, new_size, alignment) if new_memory |new_bytes| { copy_size usize = old_size - if new_size < copy_size { - copy_size = new_size - } + if (new_size < copy_size) copy_size = new_size memcopy!(new_bytes[..copy_size], old_memory[..copy_size]) c.free(old_memory) } diff --git a/std/meta/meta.bro b/std/meta/meta.hon similarity index 82% rename from std/meta/meta.bro rename to std/meta/meta.hon index 2059242..5c6bcd0 100644 --- a/std/meta/meta.bro +++ b/std/meta/meta.hon @@ -1,4 +1,4 @@ -Layout :: enum { auto c } +Layout :: enum { auto, c } ArrayInfo :: struct { child type @@ -46,9 +46,9 @@ TypeInfo :: union(enum) { EnumFieldStruct func($E, $Field type, $default ?Field) type { match typeinfo!(E) { .enum |info|: { - names [info.fields.len]mut []u8 = undefined - field_types [info.fields.len]mut type = undefined - defaults [info.fields.len]mut ?Field = undefined + names [info.fields.len]mut []u8 = undefined + field_types [info.fields.len]mut type = undefined + defaults [info.fields.len]mut ?Field = undefined expand for info.fields |field, index| { names[index] = field.name field_types[index] = Field diff --git a/std/meta/meta.test.bro b/std/meta/meta.test.hon similarity index 100% rename from std/meta/meta.test.bro rename to std/meta/meta.test.hon diff --git a/std/process/process.bro b/std/process/process.hon similarity index 100% rename from std/process/process.bro rename to std/process/process.hon diff --git a/std/static_string_map/static_string_map.bro b/std/static_string_map/static_string_map.bro deleted file mode 100644 index 013b492..0000000 --- a/std/static_string_map/static_string_map.bro +++ /dev/null @@ -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 -} diff --git a/std/static_string_map/static_string_map.hon b/std/static_string_map/static_string_map.hon new file mode 100644 index 0000000..7ae48a5 --- /dev/null +++ b/std/static_string_map/static_string_map.hon @@ -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] + } +} diff --git a/std/static_string_map/static_string_map.test.bro b/std/static_string_map/static_string_map.test.bro deleted file mode 100644 index d75aeb3..0000000 --- a/std/static_string_map/static_string_map.test.bro +++ /dev/null @@ -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")) -} diff --git a/std/static_string_map/static_string_map.test.hon b/std/static_string_map/static_string_map.test.hon new file mode 100644 index 0000000..e69de29 diff --git a/std/std.bro b/std/std.hon similarity index 78% rename from std/std.bro rename to std/std.hon index 5ea6cf9..16b47ba 100644 --- a/std/std.bro +++ b/std/std.hon @@ -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 diff --git a/std/testing/testing.bro b/std/testing/testing.hon similarity index 64% rename from std/testing/testing.bro rename to std/testing/testing.hon index 964dab3..76188ca 100644 --- a/std/testing/testing.bro +++ b/std/testing/testing.hon @@ -6,14 +6,18 @@ Error :: enum { } SourceLocation :: struct { - file []u8 - line usize + file []u8 + line usize column usize } expect func(condition bool, location SourceLocation) void ! Error { if !condition { - debug.print("{s}:{d}:{d}: expectation failed\n", {location.file, location.line, location.column}) + debug.print("{s}:{d}:{d}: expectation failed\n", { + location.file, + location.line, + location.column, + }) return .expectation_failed } } @@ -26,23 +30,39 @@ expect_equal func($T type, expected, actual T, location SourceLocation) void ! E try expect_equal(expected_value, actual_value, location) return } - debug.print("{s}:{d}:{d}: expected an optional value, found null\n", {location.file, location.line, location.column}) + debug.print("{s}:{d}:{d}: expected an optional value, found null\n", { + location.file, + location.line, + location.column, + }) return .expectation_failed } if actual |_| { - debug.print("{s}:{d}:{d}: expected null, found an optional value\n", {location.file, location.line, location.column}) + debug.print("{s}:{d}:{d}: expected null, found an optional value\n", { + location.file, + location.line, + location.column, + }) return .expectation_failed } } .slice: if !mem.eql(expected, actual) { - debug.print("{s}:{d}:{d}: expected and actual slices differ\n", {location.file, location.line, location.column}) + debug.print("{s}:{d}:{d}: expected and actual slices differ\n", { + location.file, + location.line, + location.column, + }) return .expectation_failed } - else: { - if expected != actual { - debug.print("{s}:{d}:{d}: expected {}, found {}\n", {location.file, location.line, location.column, expected, actual}) - return .expectation_failed - } + else: if expected != actual { + debug.print("{s}:{d}:{d}: expected {}, found {}\n", { + location.file, + location.line, + location.column, + expected, + actual, + }) + return .expectation_failed } } } @@ -53,10 +73,10 @@ expect_type func($Expected, $Actual type, _ Actual, location SourceLocation) voi run func(name []u8, callback *func() void ! Error) bool { callback() catch |_| { - debug.print("{s} [failed]\n", {name,}) + debug.print("{s}...[failed]\n", {name,}) return false } - debug.print("{s} [ok]\n", {name,}) + debug.print("{s}...[ok]\n", {name,}) return true }