From 96275121be9339d5e4f45350618014ba2e76b6ae Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 24 Jul 2026 22:38:36 +0200 Subject: [PATCH] fix: bug hunt --- LANGUAGE.md | 2 +- compiler/checker/checker.odin | 142 +++++++++++------ compiler/checker/comptime.odin | 91 +++++++++-- compiler/parser/parser.odin | 5 +- compiler_tests.odin | 161 +++++++++++++++++++- std/static_string_map/static_string_map.hon | 3 +- 6 files changed, 334 insertions(+), 70 deletions(-) diff --git a/LANGUAGE.md b/LANGUAGE.md index d366e01..4bf23e5 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -28,7 +28,7 @@ roadmap and milestone history. - exact-width integers, concrete pointer-sized `isize` / `usize`, `f32`, `f64`, `bool`, `void`, `noreturn`, and `anyopaque`; `noreturn` is a bottom type valid as a native function result and coerces to any expected value type; contextual `int` accepts the whole integer family, while `uint` accepts only unsigned native and target-classified C integers - target-dependent C scalar primitives from `c_char` through `c_longdouble`, kept semantically distinct from native scalars - contextual integer/float/character literals, backward type-demand inference through names and arithmetic/bitwise expressions, and typed compile-time evaluation for numeric constant expressions -- strict numeric conversion by default, widening where valid, C scalar coercions at C boundaries, and explicit scalar keyword casts such as `i32(x)` / `c_float(x)` +- strict numeric conversion by default, widening where valid, C scalar coercions at C boundaries, and explicit scalar casts through keywords or transparent aliases, such as `i32(x)`, `c_float(x)`, or `StringId(x)` - compile-time `minval!(T)` and `maxval!(T)` bounds for concrete native and C integer scalar types - arrays `[N]T`, inferred-count arrays `[_]T`, sentinel arrays `[N;S]T`, compile-time expression array counts, slices `[]T` / `[;S]T`, single-item pointers `@T`, many-item pointers `*T`, and sentinel many-item pointers `[*;S]T` - pointer mutability via `mut`, optional pointers as nullable pointers, pointer arithmetic for many-item pointers, postfix dereference `^`, and trapping optional unwrap `?` diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 44785b5..60fdfe7 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -5407,10 +5407,15 @@ infer_expr :: proc( } _, function_item, function_type, ok := types.callable_function(callee_type, &checker.module.types) if !ok { - distinct_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) - distinct_item, distinct_ok := types.node(&checker.module.types, distinct_type) - if available && distinct_ok && distinct_item.kind == .Distinct && len(expr.args) == 1 { - stack[frame_index].left = distinct_type + named_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) + named_item, named_ok := types.node(&checker.module.types, named_type) + constructor_type := types.resolve_alias(named_type, &checker.module.types) + constructor_item, constructor_ok := types.node(&checker.module.types, constructor_type) + scalar_alias := named_ok && named_item.kind == .Alias && + types.is_concrete_scalar(constructor_type) && !types.is_bool(constructor_type) + distinct_constructor := constructor_ok && constructor_item.kind == .Distinct + if available && len(expr.args) == 1 && (scalar_alias || distinct_constructor) { + stack[frame_index].left = constructor_type stack[frame_index].stage = 7 append(&stack, Infer_Frame{expr=expr.args[0], template=ast.INVALID_FUNCTION}) continue @@ -7633,6 +7638,41 @@ enum_member_hir :: proc( }) } +build_scalar_cast :: proc( + checker: ^Checker, + value: hir.Expr_Id, + target: types.Type, + span: source.Span, +) -> hir.Expr_Id { + store := &checker.module.types + actual := checker.module.exprs[value].type + valid_target := types.is_concrete_scalar(target) && !types.is_bool(target) + actual_repr := types.runtime_representation(actual, store) + actual_item, actual_item_ok := types.node(store, actual) + explicit_enum := actual_item_ok && actual_item.kind == .Enum && actual_item.explicit_backing + valid_actual := (types.is_concrete_scalar(actual) || explicit_enum) && + types.is_concrete_scalar(actual_repr) && !types.is_bool(actual_repr) + if !valid_target || !valid_actual { + id := source.addf( + checker.diagnostics, + span, + "scalar cast requires numeric scalar types, got %s to %s", + types.name(actual), + types.name(target), + ) + return invalid_hir_expr(checker, span, id, target) + } + return add_hir_expr(checker, hir.Expr{ + kind=.Scalar_Cast, + span=span, + type=target, + left=value, + target=hir.INVALID_REF, + right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) +} + build_function_value :: proc( checker: ^Checker, template: ast.Function_Id, @@ -8041,17 +8081,22 @@ build_compound_expr :: proc( ) return invalid_hir_expr(checker, expr.span, id, expected) case .Enum_Literal: - if types.is_tagged_union(expected, store) { - index, field, found := find_struct_field(checker, expected, expr.name) + literal_expected := expected + optional_expected := types.is_optional(expected, store) + if optional_expected { + literal_expected = types.child_type(expected, store) + } + if types.is_tagged_union(literal_expected, store) { + index, field, found := find_struct_field(checker, literal_expected, expr.name) if !found { - id := source.addf(checker.diagnostics, expr.span, "unknown variant '.%s' on '%s'", symbol_text(checker, expr.name), type_label(checker, expected)) + id := source.addf(checker.diagnostics, expr.span, "unknown variant '.%s' on '%s'", symbol_text(checker, expr.name), type_label(checker, literal_expected)) return invalid_hir_expr(checker, expr.span, id, expected) } values := make([]hir.Expr_Id, 1, checker.allocator) if expr.left == ast.INVALID_EXPR { if !types.is_void(field.type) { id := source.addf(checker.diagnostics, expr.span, "variant '.%s' on '%s' needs a payload; only void variants can be built from a bare '.%s'", - symbol_text(checker, expr.name), type_label(checker, expected), symbol_text(checker, expr.name)) + symbol_text(checker, expr.name), type_label(checker, literal_expected), symbol_text(checker, expr.name)) delete(values, checker.allocator) return invalid_hir_expr(checker, expr.span, id, expected) } @@ -8065,11 +8110,12 @@ build_compound_expr :: proc( values[0] = build_nested_expr(checker, expr.left, locals, global_reads, calls, field.type, pkg, file) values[0] = coerce_expr(checker, values[0], field.type, checker.module.exprs[values[0]].span) } - return add_hir_expr(checker, hir.Expr{ - kind=.Struct, span=expr.span, type=expected, args=values, integer=i64(index), + result := add_hir_expr(checker, hir.Expr{ + kind=.Struct, span=expr.span, type=literal_expected, args=values, integer=i64(index), target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) + return coerce_expr(checker, result, expected, expr.span) if optional_expected else result } if expr.left != ast.INVALID_EXPR { id := source.addf( @@ -8080,7 +8126,7 @@ build_compound_expr :: proc( ) return invalid_hir_expr(checker, expr.span, id, expected) } - if !types.is_enum(expected, store) { + if !types.is_enum(literal_expected, store) { id := source.addf( checker.diagnostics, expr.span, @@ -8089,36 +8135,12 @@ build_compound_expr :: proc( ) return invalid_hir_expr(checker, expr.span, id, expected) } - return enum_member_hir(checker, expected, expr.name, expr.span) + result := enum_member_hir(checker, literal_expected, expr.name, expr.span) + return coerce_expr(checker, result, expected, expr.span) if optional_expected else result case .Cast: target := type_from_syntax(checker, expr.type, pkg, file) value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) - actual := checker.module.exprs[value].type - valid_target := types.is_concrete_scalar(target) && !types.is_bool(target) - actual_repr := types.runtime_representation(actual, store) - actual_item, actual_item_ok := types.node(store, actual) - explicit_enum := actual_item_ok && actual_item.kind == .Enum && actual_item.explicit_backing - valid_actual := (types.is_concrete_scalar(actual) || explicit_enum) && - types.is_concrete_scalar(actual_repr) && !types.is_bool(actual_repr) - if !valid_target || !valid_actual { - id := source.addf( - checker.diagnostics, - expr.span, - "scalar cast requires numeric scalar types, got %s to %s", - types.name(actual), - types.name(target), - ) - return invalid_hir_expr(checker, expr.span, id, target) - } - return add_hir_expr(checker, hir.Expr{ - kind=.Scalar_Cast, - span=expr.span, - type=target, - left=value, - target=hir.INVALID_REF, - right=hir.INVALID_EXPR, - diagnostic=source.INVALID_DIAGNOSTIC, - }) + return build_scalar_cast(checker, value, target, expr.span) case .Address: // `&` (Zig's `&.{...}`): the operand is an rvalue with no // address, so promote it to an anonymous global constant and take *its* @@ -9369,10 +9391,36 @@ build_expr :: proc( } } if callee == hir.INVALID_EXPR { - distinct_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) - distinct_item, distinct_ok := types.node(&checker.module.types, distinct_type) - if distinct_ok && distinct_item.kind == .Distinct { - if !is_runtime_type(checker, distinct_type) { + named_type := types.find_named(&checker.module.types, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) + named_item, named_ok := types.node(&checker.module.types, named_type) + constructor_type := types.resolve_alias(named_type, &checker.module.types) + constructor_item, constructor_ok := types.node(&checker.module.types, constructor_type) + scalar_alias := named_ok && named_item.kind == .Alias && + types.is_concrete_scalar(constructor_type) && !types.is_bool(constructor_type) + if scalar_alias { + if len(expr.args) != 1 { + id := source.addf( + checker.diagnostics, + expr.span, + "type alias '%s' expects 1 argument, got %d", + symbol_text(checker, expr.name), + len(expr.args), + ) + last = invalid_hir_expr(checker, expr.span, id, constructor_type) + _ = pop(&stack) + continue + } + stack[frame_index].target_type = constructor_type + stack[frame_index].stage = 11 + append(&stack, Build_Expr_Frame{ + expr=expr.args[0], + expected=types.INVALID, + template=ast.INVALID_FUNCTION, + }) + continue + } + if constructor_ok && constructor_item.kind == .Distinct { + if !is_runtime_type(checker, constructor_type) { id := source.addf( checker.diagnostics, expr.span, @@ -9391,15 +9439,15 @@ build_expr :: proc( symbol_text(checker, expr.name), len(expr.args), ) - last = invalid_hir_expr(checker, expr.span, id, distinct_type) + last = invalid_hir_expr(checker, expr.span, id, constructor_type) _ = pop(&stack) continue } - stack[frame_index].target_type = distinct_type + stack[frame_index].target_type = constructor_type stack[frame_index].stage = 8 append(&stack, Build_Expr_Frame{ expr=expr.args[0], - expected=distinct_item.child, + expected=constructor_item.child, template=ast.INVALID_FUNCTION, }) continue @@ -9988,6 +10036,10 @@ build_expr :: proc( } _ = pop(&stack) } + if frame.stage == 11 { + last = build_scalar_cast(checker, last, frame.target_type, expr.span) + _ = pop(&stack) + } } return last } diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 5d8db2a..b923856 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -1499,15 +1499,37 @@ ct_eval_expr :: proc( case .Catch: return ct_eval_catch_expr(state, expr, expected, depth+1) case .Range: - left, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1) - if !ok || flow.kind != .Normal { - return INVALID_CT_VALUE, flow, ok + child_hint := types.INVALID + if types.is_range(expected, store) { + child_hint = types.child_type(expected, store) } - right, right_flow, right_ok := ct_eval_expr(state, expr.right, state.values[left].type, depth+1) - if !right_ok || right_flow.kind != .Normal { - return INVALID_CT_VALUE, right_flow, right_ok + left_const := is_numeric_constant_expr(checker, expr.left) + right_const := is_numeric_constant_expr(checker, expr.right) + left, right: Ct_Value_Id + flow: Ct_Flow + ok: bool + if left_const && !right_const && !types.is_valid(child_hint) { + right, flow, ok = ct_eval_expr(state, expr.right, types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, 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 + } + } else { + left, flow, ok = ct_eval_expr(state, expr.left, child_hint, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + right_hint := child_hint if types.is_valid(child_hint) else state.values[left].type + right, flow, ok = ct_eval_expr(state, expr.right, right_hint, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } } - child_type := types.widest(state.values[left].type, state.values[right].type) + child_type := child_hint if types.is_valid(child_hint) else + types.widest(state.values[left].type, state.values[right].type) if !types.is_concrete_integer(child_type) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "range bounds must be compatible concrete integers") } @@ -1905,10 +1927,15 @@ ct_eval_slice_expr :: proc(state: ^Ct_State, expr: ast.Expr, depth: int) -> (Ct_ ct_eval_enum_literal :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type, depth: int) -> (Ct_Value_Id, Ct_Flow, bool) { checker := state.checker store := &checker.module.types - if types.is_tagged_union(expected, store) { - index, field, found := find_struct_field(checker, expected, expr.name) + literal_expected := expected + optional_expected := types.is_optional(expected, store) + if optional_expected { + literal_expected = types.child_type(expected, store) + } + if types.is_tagged_union(literal_expected, store) { + index, field, found := find_struct_field(checker, literal_expected, expr.name) if !found { - return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unknown variant '.%s' on '%s'", symbol_text(checker, expr.name), type_label(checker, expected)) + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unknown variant '.%s' on '%s'", symbol_text(checker, expr.name), type_label(checker, literal_expected)) } payload := INVALID_CT_VALUE if expr.left != ast.INVALID_EXPR { @@ -1921,23 +1948,31 @@ ct_eval_enum_literal :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.T return INVALID_CT_VALUE, ct_flow(.Normal), false } } else if !types.is_void(field.type) { - return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "variant '.%s' on '%s' needs a payload", symbol_text(checker, expr.name), type_label(checker, expected)) + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "variant '.%s' on '%s' needs a payload", symbol_text(checker, expr.name), type_label(checker, literal_expected)) } start := u32(len(state.children)) append(&state.children, payload) - return ct_add_value(state, Ct_Value{kind=.Struct, type=expected, start=start, count=1, active=i64(index)}), ct_flow(.Normal), true + result := ct_add_value(state, Ct_Value{kind=.Struct, type=literal_expected, start=start, count=1, active=i64(index)}) + if optional_expected { + return ct_coerce_expr_value(state, result, expected, expr.span) + } + return result, ct_flow(.Normal), true } if expr.left != ast.INVALID_EXPR { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "'.%s{...}' requires a tagged-union context", symbol_text(checker, expr.name)) } - if !types.is_enum(expected, store) { + if !types.is_enum(literal_expected, store) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "'.%s' requires an enum context", symbol_text(checker, expr.name)) } - member, ok := find_enum_member(checker, expected, expr.name) + member, ok := find_enum_member(checker, literal_expected, expr.name) if !ok { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unknown enum member '%s'", symbol_text(checker, expr.name)) } - return ct_add_value(state, Ct_Value{kind=.Integer, type=expected, integer=member.value}), ct_flow(.Normal), true + result := ct_add_value(state, Ct_Value{kind=.Integer, type=literal_expected, integer=member.value}) + if optional_expected { + return ct_coerce_expr_value(state, result, expected, expr.span) + } + return result, ct_flow(.Normal), true } ct_eval_field_value :: proc(state: ^Ct_State, base_id: Ct_Value_Id, name: symbol.Id, span: source.Span) -> (Ct_Value_Id, Ct_Flow, bool) { @@ -3550,6 +3585,32 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type } } } + named_type := types.find_named( + &checker.module.types, + u32(target_pkg), + u32(expr.name), + file=u32(expr_lookup_file(expr, state.file)), + ) + named_item, named_ok := types.node(&checker.module.types, named_type) + target := types.resolve_alias(named_type, &checker.module.types) + if named_ok && named_item.kind == .Alias && + types.is_concrete_scalar(target) && !types.is_bool(target) { + if len(expr.args) != 1 { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf( + state, + .Not_Comptime, + expr.span, + "type alias '%s' expects 1 argument, got %d", + symbol_text(checker, expr.name), + len(expr.args), + ) + } + value, flow, ok := ct_eval_expr(state, expr.args[0], types.INVALID, depth+1) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + return ct_scalar_cast(state, value, target, expr.span) + } return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "unresolved function '%s'", symbol_text(checker, expr.name)) } if runtime_param_count(checker.ast_module.functions[template]) != 0 { diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index c7e29d5..010e431 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -437,7 +437,8 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax { u32(parser.file), !symbol.is_valid(qualifier) && file_hidden_name(parser, name.symbol), ) - if current(parser).kind == .Bang && peek(parser).kind == .Left_Paren { + if token_text(parser, name) == "struct_type" && + current(parser).kind == .Bang && peek(parser).kind == .Left_Paren { advance(parser) if symbol.is_valid(qualifier) { source.add(parser.diagnostics, first.span, "intrinsic calls must be unqualified") @@ -1251,7 +1252,7 @@ is_simple_range_bound :: proc(expr: ast.Expr) -> bool { return true } #partial switch expr.kind { - case .Integer, .Float, .String, .Bool, .Name: + case .Integer, .Float, .String, .Bool, .Name, .Call, .Cast: return true case: return false diff --git a/compiler_tests.odin b/compiler_tests.odin index 01c21a0..83d2393 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -4570,6 +4570,75 @@ main func() i32 { testing.expect(t, undefined_found) } +@(test) +zero_runtime_fold_tracks_mutated_locals_in_dependent_array_types :: proc(t: ^testing.T) { + text := `FoldedMap func($V type) type { + return struct { + keys [][]u8 + values []V + indexes []u32 + } +} +Pair func($V type) type { return struct { []u8, V } } +build_map func($V type, $N usize, $entries [N]Pair(V)) FoldedMap(V) { + keys [N]mut []u8 = undefined + values [N]mut V = undefined + for entries |entry, i| { + keys[i] = entry.0 + values[i] = entry.1 + } + 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 + } + max_len usize :: keys[N - 1].len + indexes [max_len + 1]mut u32 = undefined + for 0..=max_len |length| { + indexes[length] = 0 + } + return FoldedMap(V){ + keys = keys[..], + values = values[..], + indexes = indexes[..], + } +} +Kind :: enum { short, long } +MAP FoldedMap(Kind) :: build_map([ + {"four", .long}, + {"a", .short}, +]) +main func() i32 { + if MAP.keys.len != 2 or MAP.indexes.len != 5 { return 1 } + return 0 +} +` + 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) + + build_map_found := false + for function in hir_module.functions { + build_map_found = build_map_found || symbol.resolve(&symbols, function.name) == "build_map" + } + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, !build_map_found) +} + @(test) zero_runtime_fold_preserves_reached_compile_error :: proc(t: ^testing.T) { text := `fail func() i32 { @@ -6536,6 +6605,7 @@ AError :: enum { a } BError :: enum { b } CError :: enum { c } DetailError :: union(enum) { out_of_memory i32 } +Code :: alias i32 key_or_alloc func(code i32) void ! (KeyError | AllocError) { if code == 1 { return .key_exists } @@ -6557,7 +6627,7 @@ via_else func() i32 ! AllocError { } return 0 } -via_group func() i32 ! (AError | BError) { +via_group func() Code ! (AError | BError) { abc(2) catch |err| { match err { .a, .b: return err @@ -9843,6 +9913,8 @@ answer :: alias dep.answer hide local_answer_alias :: alias dep.answer local_answer func() i32 { return local_answer_alias() } Scalar :: alias i32 +ScalarChain :: alias Scalar +static_scalar Scalar :: Scalar(usize(6)) MaybePoint :: alias ?@dep.Point Concrete :: alias dep.Box(i32) ` @@ -9862,10 +9934,11 @@ main func() i32 { box top.Box(i32) :: top.Box(i32) { value = 2 } point top.Point :: top.Point { value = 3 } maybe facade.MaybePoint :: null - scalar facade.Scalar :: 5 + scalar facade.Scalar :: facade.Scalar(usize(5)) + chained facade.ScalarChain :: facade.ScalarChain(usize(6)) top.counter = 7 if box.value != 2 or point.value != 3 { return 1 } - if scalar != 5 or top.answer() != 40 or facade.local_answer() != 40 or dep.counter != 7 { return 2 } + if scalar != 5 or chained != 6 or facade.static_scalar != 6 or top.answer() != 40 or facade.local_answer() != 40 or dep.counter != 7 { return 2 } if apply(top.answer) != 40 or apply(facade.answer) != 40 { return 3 } _ = maybe return 0 @@ -9882,6 +9955,75 @@ main func() i32 { testing.expect_value(t, state.exit_code, 0) } +@(test) +scalar_alias_casts_reject_bad_arity_and_operands :: proc(t: ^testing.T) { + text := `StringId :: alias u32 +main func() void { + _ = StringId() + _ = StringId(1, 2) + _ = StringId(true) +} +` + 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) + + arity := 0 + bad_operand := false + for diagnostic in diagnostics.items { + arity += 1 if strings.contains(diagnostic.message, "type alias 'StringId' expects 1 argument") else 0 + bad_operand = bad_operand || strings.contains(diagnostic.message, "scalar cast requires numeric scalar types") + } + testing.expect_value(t, arity, 2) + testing.expect(t, bad_operand) +} + +@(test) +optional_contextual_enum_literals_compile_at_runtime_and_comptime :: proc(t: ^testing.T) { + text := `Kind :: enum { first, second } +Choice :: union(enum) { empty void, payload i32 } +STATIC ?Kind :: .second +STATIC_CHOICE ?Choice :: .payload{4} +choose func(first bool) ?Kind { + if first { return .first } + return .second +} +choose_choice func() ?Choice { return .empty } +main func() i32 { + if choose(true) |value| { + if value != Kind.first { return 1 } + } else { return 2 } + if STATIC |value| { + if value != Kind.second { return 3 } + } else { return 4 } + if choose_choice() |_| {} else { return 5 } + if STATIC_CHOICE |_| {} else { return 6 } + return 0 +} +` + 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) + + testing.expect_value(t, len(diagnostics.items), 0) +} + @(test) declaration_aliases_diagnose_invalid_targets :: proc(t: ^testing.T) { root :: "/tmp/brolang-test-declaration-alias-errors" @@ -11095,7 +11237,8 @@ main func() i32 { @(test) range_bound_parenthesization_is_enforced :: proc(t: ^testing.T) { - text := `main func() void { + text := `limit_func func() usize { return 3 } +main func() void { limit :: 3 for 0..limit + 1 |bad| { _ = bad @@ -11103,6 +11246,12 @@ range_bound_parenthesization_is_enforced :: proc(t: ^testing.T) { for 0..(limit + 1) |good| { _ = good } + for 0..=usize(limit) |cast_bound| { + _ = cast_bound + } + for 0..limit_func() |call_bound| { + _ = call_bound + } } ` source_file := source.Source{path="test.bro", text=text} @@ -14934,7 +15083,7 @@ TokenKind :: enum { keyword_while } -keywords std.StaticStringMap(TokenKind) = static_string_map.init([ +keywords std.StaticStringMap(TokenKind) :: static_string_map.init([ {"if", .keyword_if}, {"else", .keyword_else}, {"for", .keyword_for}, @@ -14943,7 +15092,7 @@ keywords std.StaticStringMap(TokenKind) = static_string_map.init([ fallback :: static_string_map.init(TokenKind, [ {"while", .keyword_while}, ]) -empty std.StaticStringMap(TokenKind) = static_string_map.init([]) +empty std.StaticStringMap(TokenKind) :: static_string_map.init([]) numbers []i32 = ${ values [3]mut i32 = [7, 8, 9] yield values[..] diff --git a/std/static_string_map/static_string_map.hon b/std/static_string_map/static_string_map.hon index 7ae48a5..119f60d 100644 --- a/std/static_string_map/static_string_map.hon +++ b/std/static_string_map/static_string_map.hon @@ -64,7 +64,7 @@ init func($V type, $N usize, $entries [N]Pair(V)) StaticStringMap(V) { 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| { + 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) } @@ -90,4 +90,5 @@ get func($V type, map @StaticStringMap(V), key []u8) ?V { if (candidate.len != key.len) return null if mem.eql(u8, candidate, key) return map.values[idx] } + return null }