Compare commits

...

5 Commits

7 changed files with 355 additions and 26 deletions
+165 -22
View File
@@ -1189,7 +1189,12 @@ type_from_syntax :: proc(
}
return composed
case .Type_Call:
return resolve_type_factory_call(checker, ast.Expr_Id(item.count_expr), pkg, file)
expr_id := ast.Expr_Id(item.count_expr)
call_file := file
if expr_id != ast.INVALID_EXPR && int(expr_id) < len(checker.ast_module.exprs) {
call_file = ast.File_Id(checker.ast_module.exprs[expr_id].span.file)
}
return resolve_type_factory_call(checker, expr_id, pkg, call_file)
}
if changed {
return types.intern(store, item)
@@ -2730,8 +2735,9 @@ infer_call_comptime_values :: proc(
for value_bound in bound {
all_bound = all_bound && value_bound
}
// Concrete arguments bind first. Numeric constants are contextual and therefore
// only contribute their default type after stronger evidence has had a chance.
// Concrete arguments bind first. Numeric constants and `none` are contextual and
// therefore only contribute after stronger evidence has had a chance to bind the
// parameter type.
weak_passes := [2]bool{false, true}
for weak in weak_passes {
for arg_id, source_index in args {
@@ -2742,7 +2748,9 @@ infer_call_comptime_values :: proc(
if param_index < 0 || function.params[param_index].comptime_value {
continue
}
is_weak := is_numeric_constant_expr(checker, arg_id)
arg_expr := checker.ast_module.exprs[arg_id]
is_none := arg_expr.kind == .None
is_weak := is_numeric_constant_expr(checker, arg_id) || is_none
if is_weak != weak {
continue
}
@@ -2753,7 +2761,6 @@ infer_call_comptime_values :: proc(
// keyed record must be checked against the specialized parameter type.
// Its provisional structural type intentionally contains only the supplied
// fields, so comparing that type here would reject omitted defaulted fields.
arg_expr := checker.ast_module.exprs[arg_id]
if all_bound && arg_expr.kind == .Struct_Literal && !arg_expr.tuple && !symbol.is_valid(arg_expr.name) {
continue
}
@@ -2766,9 +2773,22 @@ infer_call_comptime_values :: proc(
}
}
}
actual := actual_args[param_index]
if is_none {
previous := checker.current_comptime_values
checker.current_comptime_values = values
contextual := type_from_syntax(
checker, function.params[param_index].type, function.pkg, function.file,
)
checker.current_comptime_values = previous
if types.is_optional(contextual, &checker.module.types) {
actual = contextual
actual_args[param_index] = contextual
}
}
matched = match_inferred_type_pattern(
checker, function, prefix, function.params[param_index].type,
actual_args[param_index], values, bound,
actual, values, bound,
checker.ast_module.exprs[arg_id].span, diagnose,
) && matched
}
@@ -4057,6 +4077,25 @@ validate_meta_schema :: proc(checker: ^Checker) {
}
validate_type_nodes :: proc(checker: ^Checker) {
node_count := len(checker.module.types.nodes)
for index in 0..<node_count {
item := checker.module.types.nodes[index]
if item.kind != .Struct && item.kind != .Union {
continue
}
start := int(item.field_start)
end := start+int(item.field_count)
if start < 0 || end > len(checker.module.types.fields) {
continue
}
for slot in start..<end {
resolved := type_from_syntax(
checker, checker.module.types.fields[slot].type,
ast.Package_Id(item.pkg), ast.File_Id(item.file),
)
checker.module.types.fields[slot].type = resolved
}
}
validate_meta_schema(checker)
for item, index in checker.module.types.nodes {
id := types.DYNAMIC_START+types.Type(index)
@@ -4482,8 +4521,18 @@ infer_compound_expr :: proc(
right := infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types, types.U64)
return left if types.is_concrete_integer(left) && types.is_unsigned(right, checker.target) else types.INVALID
case .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
left_expr := checker.ast_module.exprs[expr.left]
right_expr := checker.ast_module.exprs[expr.right]
if left_expr.kind == .None && right_expr.kind != .None {
right := infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types, right)
} else if right_expr.kind == .None && left_expr.kind != .None {
left := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types, left)
} else {
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
}
return types.BOOL
case .Range:
left := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
@@ -4519,7 +4568,7 @@ infer_compound_expr :: proc(
}
return types.array(store, element, u64(len(expr.args)), false)
case .None:
return types.INVALID
return expected if types.is_optional(expected, store) else types.INVALID
case .Undefined:
return types.INVALID
case .Enum_Literal:
@@ -4695,12 +4744,15 @@ infer_compound_expr :: proc(
if ok && item_ok {
initialized[slot-int(item.field_start)] = true
}
if !ok || !is_inferred_record_field(checker, slot) {
if !ok {
_ = infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types)
continue
}
field_expected := field.type if is_runtime_type(checker, field.type) else types.INVALID
actual := infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types, field_expected)
if !is_inferred_record_field(checker, slot) {
continue
}
_ = record_field_expr_candidate(checker, slot, keyed_expr.left, actual, locals, pkg, file)
if is_runtime_type(checker, checker.module.types.fields[slot].type) {
_ = record_demand(
@@ -6506,11 +6558,12 @@ prune_specs :: proc(checker: ^Checker) {
if checker.entry_point == .Process {
mark_spec_demanded(checker, find_spec(checker, checker.io_provider_template, nil), &stack)
}
for global in checker.ast_module.globals {
for global, index in checker.ast_module.globals {
if global.external || global.diagnostic != source.INVALID_DIAGNOSTIC {
continue
}
_ = infer_expr(checker, global.expr, nil, global.pkg, global.file, &stack)
expected := checker.global_types[index] if is_runtime_type(checker, checker.global_types[index]) else types.INVALID
_ = infer_expr(checker, global.expr, nil, global.pkg, global.file, &stack, expected=expected)
}
for len(stack) > 0 {
id := pop(&stack)
@@ -8124,8 +8177,9 @@ build_compound_expr :: proc(
left=left, right=right, target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Eq, .Ne, .Lt, .Le, .Gt, .Ge:
// Contextualize a bare integer-literal operand to the other operand's type
// so comparisons like `count > 0` or `0 < count` type-check.
// Contextualize literals whose type comes from their peer. This covers
// integer and enum literals as well as optional presence tests such as
// `value == none` and `none != value`.
left_const := eval_constant(checker, expr.left)
right_const := eval_constant(checker, expr.right)
left, right: hir.Expr_Id
@@ -8133,7 +8187,13 @@ build_compound_expr :: proc(
right_expr := checker.ast_module.exprs[expr.right]
left_numeric_const := left_const.kind == .Value || is_float_constant_expr(checker, expr.left)
right_numeric_const := right_const.kind == .Value || is_float_constant_expr(checker, expr.right)
if right_expr.kind == .Enum_Literal && left_expr.kind != .Enum_Literal {
if right_expr.kind == .None && left_expr.kind != .None {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, checker.module.exprs[left].type, pkg, file)
} else if left_expr.kind == .None && right_expr.kind != .None {
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file)
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, checker.module.exprs[right].type, pkg, file)
} else if right_expr.kind == .Enum_Literal && left_expr.kind != .Enum_Literal {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, checker.module.exprs[left].type, pkg, file)
} else if left_expr.kind == .Enum_Literal && right_expr.kind != .Enum_Literal {
@@ -8157,7 +8217,14 @@ build_compound_expr :: proc(
left_type := checker.module.exprs[left].type
right_type := checker.module.exprs[right].type
operand_type := types.INVALID
if types.is_enum(left_type, store) || types.is_enum(right_type, store) {
if left_expr.kind == .None || right_expr.kind == .None {
if expr.kind != .Eq && expr.kind != .Ne ||
!types.is_optional(left_type, store) || !types.equal(left_type, right_type) {
id := source.add(checker.diagnostics, expr.span, "'none' only supports '==' and '!=' with an optional value")
return invalid_hir_expr(checker, expr.span, id, types.BOOL)
}
operand_type = left_type
} else if types.is_enum(left_type, store) || types.is_enum(right_type, store) {
if !types.equal(left_type, right_type) || (expr.kind != .Eq && expr.kind != .Ne) {
id := source.add(checker.diagnostics, expr.span, "enum values only support '==' and '!=' with the same enum type")
return invalid_hir_expr(checker, expr.span, id, types.BOOL)
@@ -12712,12 +12779,88 @@ build_value_loop :: proc(
return value, target.slot_type
}
Return_Enum_Guard :: struct {
local: hir.Local_Id,
type: types.Type,
value: i64,
}
return_enum_guard :: proc(
module: ^hir.Module,
statement: hir.Stmt,
locals: []hir.Local,
) -> (Return_Enum_Guard, bool) {
if statement.kind != .If || statement.else_body != nil ||
statement.expr == hir.INVALID_EXPR || int(statement.expr) >= len(module.exprs) {
return {}, false
}
condition := module.exprs[statement.expr]
if condition.kind != .Eq || condition.left == hir.INVALID_EXPR || condition.right == hir.INVALID_EXPR ||
int(condition.left) >= len(module.exprs) || int(condition.right) >= len(module.exprs) {
return {}, false
}
left := module.exprs[condition.left]
right := module.exprs[condition.right]
if left.kind == .Integer {
left, right = right, left
}
if left.kind != .Local || right.kind != .Integer ||
!types.equal(left.type, right.type) || !types.is_enum(left.type, &module.types) {
return {}, false
}
local := hir.as_local(left.target)
if local == hir.INVALID_LOCAL || int(local) >= len(locals) || locals[local].mutable {
return {}, false
}
if !all_paths_return(module, statement.then_body, locals) {
return {}, false
}
return Return_Enum_Guard{local=local, type=left.type, value=right.integer}, true
}
enum_guards_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id, locals: []hir.Local) -> bool {
guards: [dynamic]Return_Enum_Guard
guards.allocator = module.allocator
defer delete(guards)
for id in stmts {
if guard, ok := return_enum_guard(module, module.statements[id], locals); ok {
append(&guards, guard)
}
}
for guard, index in guards {
seen := false
for previous in guards[:index] {
seen = seen || previous.local == guard.local
}
if seen {
continue
}
members := types.enum_members_for(&module.types, guard.type)
if len(members) == 0 {
continue
}
complete := true
for member in members {
value := i64(member.value) if member.value < 0 else transmute(i64)u64(member.value)
covered := false
for candidate in guards {
covered = covered || candidate.local == guard.local && candidate.value == value
}
complete = complete && covered
}
if complete {
return true
}
}
return false
}
// Reports whether every control-flow path through `stmts` terminates (returns or traps),
// so the end of the block is unreachable. A `.Return` or `.Trap` terminates outright; an
// `.If` terminates only when it has an `else` and both arms terminate. A literal
// `while true` cannot fall through because the language has no `break` statement.
// Recursion into the branch slices handles nested ifs and `else if` chains.
all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
// Exhaustive equality guards over one immutable enum local also terminate collectively.
all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id, locals: []hir.Local = nil) -> bool {
for id in stmts {
statement := module.statements[id]
#partial switch statement.kind {
@@ -12725,8 +12868,8 @@ all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
return true
case .If:
if statement.else_body != nil &&
all_paths_return(module, statement.then_body) &&
all_paths_return(module, statement.else_body) {
all_paths_return(module, statement.then_body, locals) &&
all_paths_return(module, statement.else_body, locals) {
return true
}
case .While:
@@ -12741,7 +12884,7 @@ all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
}
}
}
return false
return enum_guards_return(module, stmts, locals)
}
// Reports whether `stmts` contains a `break` that targets the enclosing loop:
@@ -12967,7 +13110,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
block := build_block(&ctx, function.body)
checker.current_result = previous_result
checker.current_build_ctx = previous_ctx
returns := all_paths_return(&checker.module, block)
returns := all_paths_return(&checker.module, block, hir_locals[:])
for block_stmt in block {
append(&body, block_stmt)
}
+26
View File
@@ -1287,6 +1287,20 @@ ct_eval_expr :: proc(
types.is_concrete_integer(expected) {
left_expected = expected
}
left_expr := checker.ast_module.exprs[expr.left]
right_expr := checker.ast_module.exprs[expr.right]
if left_expr.kind == .None && right_expr.kind != .None &&
(expr.kind == .Eq || expr.kind == .Ne) {
right, right_flow, right_ok := ct_eval_expr(state, expr.right, types.INVALID, depth+1)
if !right_ok || right_flow.kind != .Normal {
return INVALID_CT_VALUE, right_flow, right_ok
}
left, flow, ok := ct_eval_expr(state, expr.left, state.values[right].type, depth+1)
if !ok || flow.kind != .Normal {
return INVALID_CT_VALUE, flow, ok
}
return ct_eval_binary(state, expr.kind, left, right, expr.span)
}
left, flow, ok := ct_eval_expr(state, expr.left, left_expected, depth+1)
if !ok || flow.kind != .Normal {
return INVALID_CT_VALUE, flow, ok
@@ -2118,6 +2132,18 @@ ct_eval_binary :: proc(state: ^Ct_State, op: ast.Expr_Kind, left_id, right_id: C
left := state.values[left_id]
right := state.values[right_id]
is_compare := op == .Eq || op == .Ne || op == .Lt || op == .Le || op == .Gt || op == .Ge
if left.kind == .None || right.kind == .None {
if (op != .Eq && op != .Ne) ||
!types.is_optional(left.type, &state.checker.module.types) ||
!types.equal(left.type, right.type) {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "'none' only supports '==' and '!=' with an optional value")
}
equal := left.kind == .None && right.kind == .None
if op == .Ne {
equal = !equal
}
return ct_add_value(state, Ct_Value{kind=.Bool, type=types.BOOL, integer=1 if equal else 0}), ct_flow(.Normal), true
}
if left.kind == .Type || right.kind == .Type {
if left.kind != .Type || right.kind != .Type || (op != .Eq && op != .Ne) {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "type values only support '==' and '!=' with another type")
+19
View File
@@ -561,6 +561,25 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Eq, .Ne, .Lt, .Le, .Gt, .Ge:
left_expr := state.hir_module.exprs[expr.left]
right_expr := state.hir_module.exprs[expr.right]
if left_expr.kind == .None || right_expr.kind == .None {
optional_expr := expr.right if left_expr.kind == .None else expr.left
optional := lower_nested_expr(state, optional_expr)
present := append_instruction(state, ir.Instruction{
op=.Optional_Is_Some, span=expr.span, type=types.BOOL,
target=ir.INVALID_REF, a=optional, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
if expr.kind == .Ne {
return present
}
return append_instruction(state, ir.Instruction{
op=.Not, span=expr.span, type=types.BOOL,
target=ir.INVALID_REF, a=present, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
left := lower_nested_expr(state, expr.left)
right := lower_nested_expr(state, expr.right)
predicate := ir.Compare_Predicate.Eq
+5 -1
View File
@@ -2176,7 +2176,11 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id {
else_body: []ast.Stmt_Id = nil
saved_cursor := parser.cursor
skip_newlines(parser)
if current(parser).kind == .Keyword_Else {
// `else:` (and the reserved `else |...|:` shape) starts the next match arm;
// it is not the else-branch of a brace-less if used as the previous arm body.
match_arm_else := current(parser).kind == .Keyword_Else &&
(peek(parser).kind == .Colon || peek(parser).kind == .Pipe)
if current(parser).kind == .Keyword_Else && !match_arm_else {
advance(parser)
skip_newlines(parser)
if current(parser).kind == .Keyword_If {
+96
View File
@@ -9519,6 +9519,42 @@ main func() void {
testing.expect(t, !found)
}
@(test)
exhaustive_enum_guard_sequences_return :: proc(t: ^testing.T) {
text := `E :: enum { a, b }
complete func(value E) i32 {
if (value == .a) { return 1 }
if (value == .b) { return 2 }
}
incomplete func(value E) i32 {
if (value == .a) { return 1 }
}
main func() void {
_ = complete(.a)
_ = incomplete(.a)
}
`
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)
missing_returns := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "does not return a value") {
missing_returns += 1
}
}
testing.expect_value(t, missing_returns, 1)
}
@(test)
conditional_unwrap_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-conditional-unwrap"
@@ -9612,6 +9648,38 @@ main func() void {
testing.expect_value(t, len(unwrap_if.body), 1)
}
@(test)
match_else_arm_is_not_captured_by_braceless_if :: proc(t: ^testing.T) {
text := `ready func() bool { return true }
main func() void {
value i32 = 0
match value {
0: if ready() _ = value
else: _ = value
}
}
`
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)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
match_statement := module.statements[module.functions[1].body[1]]
testing.expect_value(t, len(match_statement.body), 2)
first_arm := module.statements[match_statement.body[0]]
first_if := module.statements[first_arm.body[0]]
testing.expect_value(t, first_if.kind, ast.Stmt_Kind.If)
testing.expect_value(t, len(first_if.else_body), 0)
else_arm := module.statements[match_statement.body[1]]
testing.expect_value(t, len(else_arm.patterns), 0)
}
@(test)
parser_diagnoses_braceless_if_without_parens_or_call :: proc(t: ^testing.T) {
text := `main func() void {
@@ -13786,6 +13854,7 @@ enum_field_struct_and_contextual_anonymous_records_compile_and_run :: proc(t: ^t
main_path := "/tmp/brolang-test-enum-field-struct/main.bro"
output := "/tmp/brolang-test-enum-field-struct-output"
text := `meta :: import "@std/meta"
testing :: import "@std/testing"
TokenKind :: enum(u8) {
ident = 3
@@ -13818,6 +13887,8 @@ Generated func($T type) type {
GeneratedInt :: alias Generated(i32)
ordered Names = {}
static_none ?i32 :: none
static_some ?i32 :: 1
Map func($E, $V type) type {
match typeinfo!(E) {
@@ -13844,8 +13915,22 @@ init func($E, $V type, values meta.EnumFieldStruct(E, ?V, some!(none))) Map(E, V
return map
}
get func($E, $V type, map @Map(E, V), key E) ?V {
match typeinfo!(E) {
.enum |info|: expand for info.fields |field, index| {
if key == field!(E, field.name) {
if map.present[index] { return map.values[index] }
return none
}
}
else: compile_error!("EnumMap key must be an enum")
}
}
main func() i32 {
_ = ordered
if !$(static_none == none) or !$(none == static_none) or
$(static_some == none) or $(none == static_some) { return 23 }
inferred :: {x = 40, name = "bro"}
if inferred.x != 40 or inferred.name.len != 3 { return 1 }
direct Direct = {x = 7}
@@ -13889,6 +13974,17 @@ main func() i32 {
})
if !map.present[0] or !map.present[1] or map.present[2] { return 9 }
if map.values[0].len != 10 or map.values[1].len != 7 { return 18 }
ident :: get(TokenKind, []u8, &map, TokenKind.ident)
if ident == none or none == ident { return 19 }
if ident |value| {
if value.len != 10 { return 19 }
} else { return 20 }
eof :: get(TokenKind, []u8, &map, TokenKind.eof)
if eof != none or none != eof { return 21 }
location testing.SourceLocation = {file = "test.bro", line = 1, column = 1}
testing.expect_equal(none, eof, location) catch |_| { return 24 }
testing.expect_equal(ident, ident, location) catch |_| { return 25 }
if eof |_| { return 22 }
return 0
}
`
+20
View File
@@ -2,6 +2,23 @@ arraylist :: import "@std/arraylist"
mem :: import "@std/mem"
std :: import "@std"
Token :: struct { value i32 }
ScanDiagnostic :: struct { value i32 }
State :: struct {
tokens std.ArrayList(Token)
diagnostics std.ArrayList(ScanDiagnostic)
}
arrlist_test std.ArrayList(Token) = arraylist.init(mem.c_allocator)
init func(allocator mem.Allocator) State {
return State {
tokens = arraylist.init(allocator),
diagnostics = arraylist.init(allocator),
}
}
hide fail_alloc func(_ ?@mut anyopaque, _ usize, _ usize) ?*mut u8 {
return none
}
@@ -24,6 +41,9 @@ hide fail_allocator mem.Allocator :: mem.Allocator {
}
run func() i32 ! mem.AllocError {
state State :: init(mem.c_allocator)
_ = state
values std.ArrayList(i32) = arraylist.init(mem.c_allocator)
defer arraylist.deinit(&values)
if (values.items.len != 0 or values.capacity != 0) return 1
+24 -3
View File
@@ -1,4 +1,5 @@
import "@std/debug"
import "@std/mem"
Error :: enum {
expectation_failed
@@ -18,9 +19,29 @@ expect func(condition bool, location SourceLocation) void ! Error {
}
expect_equal func($T type, expected, actual T, location SourceLocation) void ! Error {
if expected != actual {
debug.print("{s}:{d}:{d}: expected {}, found {}\n", {location.file, location.line, location.column, expected, actual})
return .expectation_failed
match typeinfo!(T) {
.optional: {
if expected |expected_value| {
if actual |actual_value| {
try expect_equal(expected_value, actual_value, location)
return
}
debug.print("{s}:{d}:{d}: expected an optional value, found none\n", {location.file, location.line, location.column})
return .expectation_failed
}
if actual |_| {
debug.print("{s}:{d}:{d}: expected none, 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})
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
}
}
}