From 870f946b523c66b40811262278c1dc9783408fef Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Wed, 1 Jul 2026 09:08:58 +0200 Subject: [PATCH] fix interop and indexing oversights --- TODO.md | 63 +--- compiler/ast/ast.odin | 2 + compiler/checker/checker.odin | 348 +++++++++++++++--- compiler/hir/hir.odin | 1 + compiler/ir/ir.odin | 1 + compiler/llvm/llvm.odin | 49 ++- compiler/lower/lower.odin | 3 +- compiler/parser/parser.odin | 72 +++- compiler/types/types.odin | 26 +- compiler_tests.odin | 29 ++ .../programs/array_const_size_error/main.bro | 5 + .../index_int_constraint_error/main.bro | 6 + examples/programs/index_signed_error/main.bro | 6 + examples/programs/milestone_24/main.bro | 93 +++++ examples/programs/scalar_cast_error/main.bro | 5 + 15 files changed, 593 insertions(+), 116 deletions(-) create mode 100644 examples/programs/array_const_size_error/main.bro create mode 100644 examples/programs/index_int_constraint_error/main.bro create mode 100644 examples/programs/index_signed_error/main.bro create mode 100644 examples/programs/milestone_24/main.bro create mode 100644 examples/programs/scalar_cast_error/main.bro diff --git a/TODO.md b/TODO.md index 00604de..8e31986 100644 --- a/TODO.md +++ b/TODO.md @@ -603,53 +603,22 @@ - structurally identical anonymous struct payloads share type identity, so sum composition merges matching variants and still rejects same-name variants with different payload shapes -24. bug fixes & interop/indexing oversights (surfaced by the raylib testbed) - - the bouncing-shapes testbed (`testbed/game`) drove most of the language at once and - exposed several gaps; grouped here as the next polish pass. it currently compiles only by - keeping three non-interconverting integer "worlds" (`usize` for indices, the `int` - constraint for `c_int` args, `c_float` for physics) and routing around the items below - - `return match ...` doesn't work (and `yield match ...` probably doesn't either): a `match` - is not accepted directly as a `return`/`yield` operand, so the value must be bound to a - local first, or a statement-`match` with per-arm `return` used — the testbed's `next_kind` - had to do the latter - - signed-index array access must be a diagnostic, not a miscompile: `arr[i]` with a signed - index (`i32`/`int`, including the `int` constraint) currently lowers to malformed LLVM IR - (the indexed slot's address and the stored value get swapped: `store , ptr` with a - value where a `ptr` is expected) for both reads and writes; only `usize` and compile-time - literal indices work. settle the rule — index expressions require an unsigned - (`usize`-coercible) type, as in Rust/Zig — and report a clear checker error instead of - emitting bad IR - - native scalar types should coerce to their C equivalents in general: `f32 -> c_float`, - `f64 -> c_double`, and the integer cases (`i32 -> c_int`, `u8 -> c_uchar`, - `usize -> the matching C width`, …) at call/return/assignment boundaries, following C's own - same-width/family conversions. today only the `int` constraint coerces to `c_int`; - concrete-width native scalars (`i32`, `usize`, `f32`) are stranded, which is why the testbed - had to store physics directly in `c_float` and maintain parallel `usize`/`int` counters - - explicit scalar type casting: there is no cast syntax yet (`c_float(x)` is rejected — that - spelling is distinct-type construction, not a numeric cast), so any conversion the coercion - rules above don't cover is currently impossible. settle a cast spelling; this removes most - of the manual world-juggling the testbed needed - - c scalars must be comparable with numeric literals: `x < 0.0` and `x != 0.0` where - `x : c_float` currently error ("comparison requires compatible numeric operands") even - though `x + 1.0` / `x * 2.0` already accept the literal. a comparison operand literal should - adopt the other operand's concrete (c) scalar type exactly as arithmetic does — otherwise - every zero/bound needs a `c_float` constant alias (the testbed added `ZF`, `SPINMAX`, etc.) - - array sizes should accept compile-time-constant expressions, not just integer literals: - `[CAP]T` with `CAP :: 64` (and `[N + 1]T`, …) should resolve through the same constant - folding used for global initializers. runtime *variable* lengths stay rejected by design — - a runtime-length array is a slice + allocation (milestone 25), not an array type (Zig/Rust - parity: array lengths are comptime-known). the testbed had to hard-code `[64]` and keep a - separate `CAP usize :: 64` for the bounds checks - - peer-type resolution for string literals in value-`match` / value-`if` arms (Zig parity, - not a brolang-specific limitation): arms yielding differently-sized string literals - (`@[6;0]u8` for "circle" vs `@[8;0]u8` for "triangle") currently fail to unify, forcing a - per-arm `DrawText("…")` workaround instead of `tag :: match k { … }; DrawText(tag, …)`. - verified Zig 0.16 behavior: `switch`/`if` arms of string literals peer-resolve to a single - sentinel slice `[:0]const u8` (preserving the common `:0`), which then coerces directly to a - C string `[*c]const u8`. brolang should (a) peer-type-resolve same-sentinel string literals - to a sentinel byte slice `[;0]u8`, and (b) allow a zero-terminated byte slice to convert to - `*c_char` (extending milestone 3.5's pointer-view → `*c_char` rule to the matching sentinel - slice). with both, a value-`match` label passes straight to a `?*c_char` parameter +24. bug fixes & interop/indexing oversights (implemented) + - `return match ...` and `yield match ...` are accepted as direct value-control-flow + operands, matching declaration/assignment value sources + - index and slice-bound expressions are contextually coerced to `usize`; unsigned narrower + integer indices work, while signed runtime indices diagnose instead of reaching LLVM + - concrete native scalars coerce to same-family C scalar types at call, return, assignment, + aggregate, and optional boundaries when the target C type can represent the source width + - scalar keyword casts (`i32(x)`, `usize(x)`, `c_float(x)`, etc.) provide explicit numeric + conversions for cases that should not be implicit + - C scalar comparisons accept numeric literals by typing the literal from the concrete C + operand, so aliases like `ZF` are no longer needed + - array counts accept compile-time integer expressions such as `[CAP]T` and `[N + 1]T`; + runtime variables remain rejected because arrays are fixed-size values + - string literals in value-`match` / value-`if` peers resolve to a common zero-terminated + byte slice when possible, and `[;0]u8` slices can decay to immutable `*c_char`/`?*c_char` + parameters 25. dynamic heap allocation - see below for direction diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index d2b9e45..b166f1a 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -83,6 +83,7 @@ Expr_Kind :: enum u8 { Orelse, Struct_Literal, Keyed, + Cast, Negate, Not, Add, @@ -109,6 +110,7 @@ Expr :: struct { args: []Expr_Id, qualifier: symbol.Id, name: symbol.Id, + type: Type_Syntax, left: Expr_Id, right: Expr_Id, body: []Stmt_Id, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index b3ff851..a17525f 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -296,6 +296,73 @@ eval_constant :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> Constant { return checker.constants[expr_id] } +eval_integer_constant_in_context :: proc( + checker: ^Checker, + expr_id: ast.Expr_Id, + pkg: ast.Package_Id, + file: ast.File_Id, + depth := 0, +) -> Constant { + if depth > 64 || expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return Constant{kind = .Not_Constant} + } + expr := checker.ast_module.exprs[expr_id] + #partial switch expr.kind { + case .Integer: + return Constant{kind = .Value, value = i128(expr.integer)} + case .Name: + target_pkg, available := expr_package(checker, expr, pkg, file, false) + if !available { + return Constant{kind = .Not_Constant} + } + global := find_global(checker, expr.name, target_pkg) + if global == ast.INVALID_GLOBAL || int(global) >= len(checker.ast_module.globals) { + return Constant{kind = .Not_Constant} + } + g := checker.ast_module.globals[global] + if g.external || !g.immutable { + return Constant{kind = .Not_Constant} + } + return eval_integer_constant_in_context(checker, g.expr, g.pkg, g.file, depth+1) + case .Negate: + operand := eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1) + if operand.kind == .Value { + value, overflow := intrinsics.overflow_sub(i128(0), operand.value) + return Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value} + } + return operand + case .Add, .Sub, .Mul, .Div: + left := eval_integer_constant_in_context(checker, expr.left, pkg, file, depth+1) + right := eval_integer_constant_in_context(checker, expr.right, pkg, file, depth+1) + if left.kind == .Div_By_Zero || right.kind == .Div_By_Zero { + return Constant{kind = .Div_By_Zero} + } + if left.kind == .Overflow || right.kind == .Overflow { + return Constant{kind = .Overflow} + } + if left.kind != .Value || right.kind != .Value { + return Constant{kind = .Not_Constant} + } + value: i128 + overflow: bool + #partial switch expr.kind { + case .Sub: + value, overflow = intrinsics.overflow_sub(left.value, right.value) + case .Mul: + value, overflow = intrinsics.overflow_mul(left.value, right.value) + case .Div: + if right.value == 0 { + return Constant{kind = .Div_By_Zero} + } + value = left.value / right.value + case: + value, overflow = intrinsics.overflow_add(left.value, right.value) + } + return Constant{kind = .Overflow} if overflow else Constant{kind = .Value, value = value} + } + return Constant{kind = .Not_Constant} +} + fits_signed_type :: proc(value: i128, value_type: types.Type, selected := target.DEFAULT) -> bool { if !types.is_signed(value_type, selected) { return false @@ -320,14 +387,83 @@ fits_i64 :: proc(value: i128) -> bool { return fits_signed_type(value, types.I64) } -type_from_syntax :: proc(value: ast.Type_Syntax) -> types.Type { +type_from_syntax :: proc( + checker: ^Checker, + value: ast.Type_Syntax, + pkg := ast.Package_Id(0), + file := ast.File_Id(0), + depth := 0, +) -> types.Type { + if depth > 64 { + return types.INVALID + } + item, ok := types.node(&checker.module.types, value) + if !ok { + return value + } + store := &checker.module.types + changed := false + #partial switch item.kind { + case .Array: + child := type_from_syntax(checker, item.child, pkg, file, depth+1) + changed = changed || child != item.child + item.child = child + if item.unresolved_count { + expr_id := ast.Expr_Id(item.count_expr) + span := source.Span{} + if expr_id != ast.INVALID_EXPR && int(expr_id) < len(checker.ast_module.exprs) { + span = checker.ast_module.exprs[expr_id].span + } + constant := eval_integer_constant_in_context(checker, expr_id, pkg, file) + if constant.kind == .Value { + switch { + case constant.value < 0: + source.add(checker.diagnostics, span, "array count must be non-negative") + return types.INVALID + case constant.value > i128(0xffff_ffff_ffff_ffff): + source.add(checker.diagnostics, span, "array count does not fit in u64") + return types.INVALID + case: + item.count = u64(constant.value) + item.unresolved_count = false + item.count_expr = 0 + changed = true + } + } else { + source.add(checker.diagnostics, span, "array count must be a compile-time integer expression") + return types.INVALID + } + } + case .Pointer, .Slice, .Optional, .Range, .Distinct, .Enum, .Fallible: + child := type_from_syntax(checker, item.child, pkg, file, depth+1) + extra := type_from_syntax(checker, item.extra, pkg, file, depth+1) + changed = child != item.child || extra != item.extra + item.child = child + item.extra = extra + case .Function: + params := types.params_for(store, value) + resolved_params := make([]types.Type, len(params), checker.allocator) + defer delete(resolved_params, checker.allocator) + params_changed := false + for param, index in params { + resolved_params[index] = type_from_syntax(checker, param.type, pkg, file, depth+1) + params_changed = params_changed || resolved_params[index] != param.type + } + result := type_from_syntax(checker, item.child, pkg, file, depth+1) + if params_changed || result != item.child { + return types.function(store, resolved_params, result, item.c_abi, item.variadic) + } + } + if changed { + return types.intern(store, item) + } return value } function_channel_type :: proc(checker: ^Checker, function: ast.Function) -> types.Type { - result := type_from_syntax(function.result) + result := type_from_syntax(checker, function.result, function.pkg, function.file) if types.is_valid(function.error) { - return types.fallible(&checker.module.types, result, type_from_syntax(function.error)) + return types.fallible(&checker.module.types, result, type_from_syntax(checker, function.error, function.pkg, function.file)) } return result } @@ -642,11 +778,11 @@ valid_call_arity :: proc(function: ast.Function, count: int) -> bool { return count >= len(function.params) if function.variadic else count == len(function.params) } -call_arg_expected :: proc(function: ast.Function, index: int) -> types.Type { +call_arg_expected :: proc(checker: ^Checker, function: ast.Function, index: int) -> types.Type { if index < 0 || index >= len(function.params) { return types.INVALID } - declared := type_from_syntax(function.params[index].type) + declared := type_from_syntax(checker, function.params[index].type, function.pkg, function.file) // A `float` param defaults to f64 so an integer-literal argument builds as a // float constant (e.g. `f(3)` -> 3.0), mirroring `pi float = 3` for locals. // `int`/`range` constraints have no single default and keep building naturally. @@ -682,13 +818,13 @@ function_value_signature :: proc( if !function.c_abi || types.is_valid(function.error) { return nil, types.INVALID, false } - result = type_from_syntax(function.result) + result = type_from_syntax(checker, function.result, function.pkg, function.file) if !types.is_void(result) && !is_runtime_type(checker, result) { return nil, types.INVALID, false } params = make([]types.Type, len(function.params), checker.allocator) for param, index in function.params { - param_type := type_from_syntax(param.type) + param_type := type_from_syntax(checker, param.type, function.pkg, function.file) if !is_runtime_type(checker, param_type) { delete(params, checker.allocator) return nil, types.INVALID, false @@ -758,7 +894,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as if expr.left != ast.INVALID_EXPR { append(&stack, expr.left) } - case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal: + case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal, .Cast: append(&stack, expr.left) case .Catch: append(&stack, expr.left) @@ -894,7 +1030,7 @@ validate_declarations :: proc(checker: ^Checker) { locals: [dynamic]symbol.Id locals.allocator = checker.allocator for param in function.params { - if diagnostic := add_unsupported_type_diagnostic(checker, param.span, type_from_syntax(param.type)); + if diagnostic := add_unsupported_type_diagnostic(checker, param.span, type_from_syntax(checker, param.type, function.pkg, function.file)); diagnostic != source.INVALID_DIAGNOSTIC { checker.template_diagnostics[function_id] = diagnostic continue @@ -915,7 +1051,7 @@ validate_declarations :: proc(checker: ^Checker) { ) } append(&locals, param.name) - if types.contains_c_struct_by_value(type_from_syntax(param.type), &checker.module.types) { + if types.contains_c_struct_by_value(type_from_syntax(checker, param.type, function.pkg, function.file), &checker.module.types) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, param.span, @@ -924,11 +1060,11 @@ validate_declarations :: proc(checker: ^Checker) { ) } } - if diagnostic := add_unsupported_type_diagnostic(checker, function.span, type_from_syntax(function.result)); + if diagnostic := add_unsupported_type_diagnostic(checker, function.span, type_from_syntax(checker, function.result, function.pkg, function.file)); diagnostic != source.INVALID_DIAGNOSTIC { checker.template_diagnostics[function_id] = diagnostic } - if types.contains_c_struct_by_value(type_from_syntax(function.result), &checker.module.types) { + if types.contains_c_struct_by_value(type_from_syntax(checker, function.result, function.pkg, function.file), &checker.module.types) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, function.span, @@ -937,7 +1073,7 @@ validate_declarations :: proc(checker: ^Checker) { ) } if types.is_valid(function.error) { - error_type := type_from_syntax(function.error) + error_type := type_from_syntax(checker, function.error, function.pkg, function.file) error_sum := types.is_enum(error_type, &checker.module.types) || types.is_tagged_union(error_type, &checker.module.types) if function.c_abi { @@ -972,7 +1108,7 @@ validate_declarations :: proc(checker: ^Checker) { } if !function.has_body && function.c_abi { for param in function.params { - param_type := type_from_syntax(param.type) + param_type := type_from_syntax(checker, param.type, function.pkg, function.file) if add_unsupported_type_diagnostic(checker, param.span, param_type) != source.INVALID_DIAGNOSTIC { continue @@ -989,7 +1125,7 @@ validate_declarations :: proc(checker: ^Checker) { ) } } - result := type_from_syntax(function.result) + result := type_from_syntax(checker, function.result, function.pkg, function.file) if add_unsupported_type_diagnostic(checker, function.span, result) == source.INVALID_DIAGNOSTIC && !types.contains_c_struct_by_value(result, &checker.module.types) && !types.is_c_signature_type(result, &checker.module.types, true) { @@ -1174,7 +1310,7 @@ find_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []t if param_index < len(actual_args) { actual = actual_args[param_index] } - if !types.equal(spec.args[param_index], specialized_param_type(checker, param.type, actual)) { + if !types.equal(spec.args[param_index], specialized_param_type(checker, param.type, actual, function.pkg, function.file)) { matches = false break } @@ -1190,8 +1326,14 @@ find_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []t // type for a given actual argument. A constraint param (`int`/`float`/`range`) // resolves to the actual's family member (INVALID if out of family), so a call // passing an out-of-family argument fails to specialize and is rejected. -specialized_param_type :: proc(checker: ^Checker, syntax: ast.Type_Syntax, actual: types.Type) -> types.Type { - declared := type_from_syntax(syntax) +specialized_param_type :: proc( + checker: ^Checker, + syntax: ast.Type_Syntax, + actual: types.Type, + pkg: ast.Package_Id, + file: ast.File_Id, +) -> types.Type { + declared := type_from_syntax(checker, syntax, pkg, file) if types.is_constraint(declared) { return types.constraint_target(declared, actual, &checker.module.types) } @@ -1204,7 +1346,7 @@ can_specialize :: proc(checker: ^Checker, function: ast.Function, actual_args: [ if index < len(actual_args) { actual = actual_args[index] } - if !is_runtime_type(checker, specialized_param_type(checker, param.type, actual)) { + if !is_runtime_type(checker, specialized_param_type(checker, param.type, actual, function.pkg, function.file)) { return false } } @@ -1223,7 +1365,7 @@ ensure_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: [ if index < len(actual_args) { actual = actual_args[index] } - append(&signature, specialized_param_type(checker, param.type, actual)) + append(&signature, specialized_param_type(checker, param.type, actual, function.pkg, function.file)) } result := function_channel_type(checker, function) if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT && @@ -1335,6 +1477,9 @@ infer_compound_expr :: proc( _ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) } return types.INVALID + case .Cast: + _ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) + return type_from_syntax(checker, expr.type, pkg, file) case .Address: child := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) return types.pointer(store, child, false, false) @@ -1487,7 +1632,7 @@ infer_expr :: proc( last = types.F64 _ = pop(&stack) case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice, - .Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, .Enum_Literal, + .Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, .Enum_Literal, .Cast, .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: last = infer_compound_expr(checker, expr, locals, pkg, file, demanded, local_types) _ = pop(&stack) @@ -1701,7 +1846,7 @@ infer_expr :: proc( // it (e.g. `take_u16(a)` resolves `a` to u16). Constraint params have no single // type to demand; the callee's result flowing back is milestone 14.5. for arg_index in 0.. ([]types.Type, types.Type) { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] - declared := type_from_syntax(function.result) + declared := type_from_syntax(checker, function.result, function.pkg, function.file) if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT { declared = types.I32 } @@ -2446,7 +2591,7 @@ infer_all :: proc(checker: ^Checker) { // Demands accumulate in global_demands so the default never blocks a later // cross-family demand (e.g. integer literal -> unsigned or float). for global, index in checker.ast_module.globals { - declared := type_from_syntax(global.type) + declared := type_from_syntax(checker, global.type, global.pkg, global.file) if is_runtime_type(checker, declared) { checker.global_types[index] = declared continue @@ -2494,7 +2639,7 @@ infer_all :: proc(checker: ^Checker) { continue } inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file) - if is_runtime_type(checker, type_from_syntax(global.type)) { + if is_runtime_type(checker, type_from_syntax(checker, global.type, global.pkg, global.file)) { continue } if is_runtime_type(checker, checker.global_demands[index]) { @@ -2660,9 +2805,11 @@ can_implicitly_convert_type :: proc(checker: ^Checker, actual, expected: types.T store := &checker.module.types if types.equal(actual, expected) || types.can_widen(actual, expected) || - types.can_coerce_c_integer(actual, expected) || + types.can_coerce_c_integer(actual, expected, checker.target) || + types.can_coerce_c_scalar(actual, expected, checker.target) || types.can_weaken_pointer(actual, expected, store) || types.can_weaken_slice(actual, expected, store) || + types.can_decay_slice_c_string(actual, expected, store) || types.can_decay_array_pointer(actual, expected, store) || types.can_sum_widen(actual, expected, store) { return true @@ -2719,6 +2866,17 @@ coerce_expr :: proc( diagnostic=source.INVALID_DIAGNOSTIC, }) } + if types.can_decay_slice_c_string(actual, expected, &checker.module.types) { + return add_hir_expr(checker, hir.Expr{ + kind=.Slice_Ptr, + span=span, + type=expected, + left=expr_id, + target=hir.INVALID_REF, + right=hir.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } if types.can_sum_widen(actual, expected, &checker.module.types) { return add_hir_expr(checker, hir.Expr{ kind=.Sum_Widen, @@ -2734,8 +2892,11 @@ coerce_expr :: proc( child := types.child_type(expected, &checker.module.types) if types.equal(actual, child) || types.can_widen(actual, child) || + types.can_coerce_c_integer(actual, child, checker.target) || + types.can_coerce_c_scalar(actual, child, checker.target) || types.can_weaken_pointer(actual, child, &checker.module.types) || types.can_weaken_slice(actual, child, &checker.module.types) || + types.can_decay_slice_c_string(actual, child, &checker.module.types) || types.can_decay_array_pointer(actual, child, &checker.module.types) { value := coerce_expr(checker, expr_id, child, span) return add_hir_expr(checker, hir.Expr{ @@ -2763,7 +2924,8 @@ coerce_expr :: proc( }, ) } - if types.can_coerce_c_integer(actual, expected) { + if types.can_coerce_c_integer(actual, expected, checker.target) || + types.can_coerce_c_scalar(actual, expected, checker.target) { return add_hir_expr( checker, hir.Expr { @@ -3311,6 +3473,31 @@ build_compound_expr :: proc( return invalid_hir_expr(checker, expr.span, id, expected) } return enum_member_hir(checker, expected, expr.name, expr.span) + 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) + valid_actual := types.is_concrete_scalar(actual) && !types.is_bool(actual) + 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, + }) case .Address: value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) if !hir_is_location(checker, value) { @@ -3343,6 +3530,7 @@ build_compound_expr :: proc( case .Index: container := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) index := build_nested_expr(checker, expr.right, locals, global_reads, calls, types.USIZE, pkg, file) + index = coerce_expr(checker, index, types.USIZE, expr.span) container_type := checker.module.exprs[container].type item, ok := types.container(container_type, store) if !ok { @@ -3367,6 +3555,7 @@ build_compound_expr :: proc( for bound, index in expr.args { if bound != ast.INVALID_EXPR { bounds[index] = build_nested_expr(checker, bound, locals, global_reads, calls, types.USIZE, pkg, file) + bounds[index] = coerce_expr(checker, bounds[index], types.USIZE, checker.ast_module.exprs[bound].span) } } preserve_sentinel := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR @@ -3603,17 +3792,19 @@ build_compound_expr :: proc( left, right: hir.Expr_Id left_expr := checker.ast_module.exprs[expr.left] 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 { 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 { 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_const.kind == .Value && left_const.kind != .Value { + } else if right_numeric_const && !left_numeric_const { left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) hint := checker.module.exprs[left].type right = build_nested_expr(checker, expr.right, locals, global_reads, calls, hint, pkg, file) - } else if left_const.kind == .Value && right_const.kind != .Value { + } else if left_numeric_const && !right_numeric_const { right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) hint := checker.module.exprs[right].type left = build_nested_expr(checker, expr.left, locals, global_reads, calls, hint, pkg, file) @@ -3841,7 +4032,7 @@ build_expr :: proc( switch expr.kind { case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice, .Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, - .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range, + .Bool, .Cast, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range, .Enum_Literal: last = build_compound_expr( checker, expr, locals, global_reads, calls, frame.expected, pkg, file, @@ -4086,7 +4277,7 @@ build_expr :: proc( stack[frame_index].arg_types = make([]types.Type, len(expr.args), checker.allocator) stack[frame_index].stage = 3 if len(expr.args) > 0 { - arg_expected := call_arg_expected(function, 0) + arg_expected := call_arg_expected(checker, function, 0) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } @@ -4141,7 +4332,7 @@ build_expr :: proc( stack[frame_index].arg_index += 1 if frame.arg_index+1 < len(expr.args) { next := frame.arg_index+1 - arg_expected := call_arg_expected(checker.ast_module.functions[frame.template], next) + arg_expected := call_arg_expected(checker, checker.ast_module.functions[frame.template], next) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } @@ -4156,7 +4347,7 @@ build_expr :: proc( if index >= len(stack[frame_index].arg_types) { break } - declared := type_from_syntax(params[index].type) + declared := type_from_syntax(checker, params[index].type, checker.ast_module.functions[frame.template].pkg, checker.ast_module.functions[frame.template].file) actual := stack[frame_index].arg_types[index] if types.is_constraint(declared) && types.is_valid(actual) && !types.is_valid(types.constraint_target(declared, actual, &checker.module.types)) { @@ -4413,9 +4604,9 @@ build_block :: proc( // declare the local from the yielded value (its type for an untyped `::`). if statement.expr == ast.INVALID_EXPR { expected := types.INVALID - typed := is_runtime_type(checker, type_from_syntax(statement.type)) + typed := is_runtime_type(checker, type_from_syntax(checker, statement.type, ctx.pkg, ctx.file)) if typed { - expected = type_from_syntax(statement.type) + expected = type_from_syntax(checker, statement.type, ctx.pkg, ctx.file) } value, value_type := build_value_source(ctx, &body, statement.body, expected, statement.span, statement.label, statement.value_control_flow) if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found { @@ -4445,7 +4636,7 @@ build_block :: proc( }) continue } - declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) + declared := resolve_inferred_array(checker, type_from_syntax(checker, statement.type, ctx.pkg, ctx.file), statement.expr) // Adopt the type inference resolved for this local when the declaration has no // concrete annotation and inference carried useful numeric context: constraints, // `undefined`, open numeric constants, or arithmetic expressions. @@ -4717,7 +4908,7 @@ build_block :: proc( ctx.problematic^ = true continue } - if statement.expr == ast.INVALID_EXPR { + if statement.expr == ast.INVALID_EXPR && !statement.value_control_flow { if types.kind(ctx.result, store) == .Fallible && types.is_void(types.fallible_success(ctx.result, store)) { flush_defers(ctx, &body, 0) @@ -4756,7 +4947,17 @@ build_block :: proc( continue } value := hir.INVALID_EXPR - if types.kind(ctx.result, store) == .Fallible { + if statement.value_control_flow { + if types.kind(ctx.result, store) == .Fallible { + success := types.fallible_success(ctx.result, store) + value, _ = build_value_source(ctx, &body, statement.body, success, statement.span, symbol.INVALID, statement.value_control_flow) + value = coerce_expr(checker, value, success, statement.span) + value = fallible_aggregate(checker, statement.span, ctx.result, value, false) + } else { + value, _ = build_value_source(ctx, &body, statement.body, ctx.result, statement.span, symbol.INVALID, statement.value_control_flow) + value = coerce_expr(checker, value, ctx.result, statement.span) + } + } else if types.kind(ctx.result, store) == .Fallible { success := types.fallible_success(ctx.result, store) error_type := types.fallible_error(ctx.result, store) error_path := false @@ -5249,7 +5450,12 @@ build_block :: proc( continue } target := &ctx.yield_targets^[target_index] - yielded := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target.slot_type, ctx.pkg, ctx.file) + yielded := hir.INVALID_EXPR + if statement.value_control_flow { + yielded, _ = build_value_source(ctx, &body, statement.body, target.slot_type, statement.span, symbol.INVALID, statement.value_control_flow) + } else { + yielded = build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target.slot_type, ctx.pkg, ctx.file) + } yielded = resolve_loop_slot(ctx, target, yielded, checker.module.exprs[yielded].type if yielded != hir.INVALID_EXPR else types.INVALID, statement.span) if target.slot == hir.INVALID_LOCAL || yielded == hir.INVALID_EXPR { id := source.add(checker.diagnostics, statement.span, @@ -5391,10 +5597,14 @@ build_value_block :: proc( delete(leading, checker.allocator) yield_stmt := checker.ast_module.statements[body_stmts[n - 1]] - value = build_expr( - checker, yield_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, - expected, ctx.pkg, ctx.file, - ) + if yield_stmt.value_control_flow { + value, value_type = build_value_source(ctx, body, yield_stmt.body, expected, yield_stmt.span, symbol.INVALID, yield_stmt.value_control_flow) + } else { + value = build_expr( + checker, yield_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, + expected, ctx.pkg, ctx.file, + ) + } value_type = checker.module.exprs[value].type if is_runtime_type(checker, expected) { value = coerce_expr(checker, value, expected, yield_stmt.span) @@ -5486,6 +5696,38 @@ emit_slot_assign :: proc(checker: ^Checker, out: ^[dynamic]hir.Stmt_Id, slot: hi }) } +string_peer_slice_type :: proc(checker: ^Checker, value: types.Type) -> types.Type { + pointer, array, ok := types.array_pointer(value, &checker.module.types) + if ok && !pointer.mutable && !array.mutable && + array.child == types.U8 && array.has_sentinel && array.sentinel == 0 { + return types.slice(&checker.module.types, types.U8, false, true, 0) + } + return types.INVALID +} + +adopt_value_slot :: proc( + ctx: ^Build_Ctx, + slot: ^hir.Local_Id, + slot_type: ^types.Type, + value: hir.Expr_Id, + vtype: types.Type, + span: source.Span, +) -> hir.Expr_Id { + checker := ctx.checker + if slot^ == hir.INVALID_LOCAL { + peer := string_peer_slice_type(checker, vtype) + if types.is_valid(peer) { + slot_type^ = peer + slot^ = new_value_slot(ctx, slot_type^) + return coerce_expr(checker, value, slot_type^, span) + } + slot_type^ = vtype + slot^ = new_value_slot(ctx, slot_type^) + return value + } + return coerce_expr(checker, value, slot_type^, span) +} + // build_value_if turns `if c { … yield A } else { … yield B }` into a result slot // each branch assigns, read after the if. Every path must yield: a mandatory `else`, // each branch ends in `yield`, and all branches share a type (the first establishes it @@ -5703,12 +5945,7 @@ emit_value_branch :: proc( if checker.module.exprs[value].kind == .Invalid { return false } - if slot^ == hir.INVALID_LOCAL { - slot_type^ = vtype - slot^ = new_value_slot(ctx, slot_type^) - } else { - value = coerce_expr(checker, value, slot_type^, span) - } + value = adopt_value_slot(ctx, slot, slot_type, value, vtype, span) emit_slot_assign(checker, out, slot^, value, span) return true } @@ -6192,12 +6429,7 @@ build_value_arm :: proc( return false } vtype := checker.module.exprs[value].type - if slot^ == hir.INVALID_LOCAL { - slot_type^ = vtype - slot^ = new_value_slot(ctx, slot_type^) - } else { - value = coerce_expr(checker, value, slot_type^, span) - } + value = adopt_value_slot(ctx, slot, slot_type, value, vtype, span) emit_slot_assign(checker, out, slot^, value, span) return true } @@ -6937,7 +7169,7 @@ build_globals :: proc(checker: ^Checker) { dependencies.allocator = checker.allocator calls: [dynamic]hir.Function_Id calls.allocator = checker.allocator - declared := resolve_inferred_array(checker, type_from_syntax(global.type), global.expr) + declared := resolve_inferred_array(checker, type_from_syntax(checker, global.type, global.pkg, global.file), global.expr) expected := types.INVALID if is_runtime_type(checker, declared) { expected = declared diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index ee190a1..97ad203 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -102,6 +102,7 @@ Expr_Kind :: enum u8 { C_Coerce, C_Vararg_Promote, Retype, + Scalar_Cast, Weaken_Pointer, Weaken_Slice, Decay_Array_Pointer, diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index 2870e07..0c39330 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -99,6 +99,7 @@ Opcode :: enum u8 { C_Coerce, C_Vararg_Promote, Retype, + Scalar_Cast, Weaken_Pointer, Weaken_Slice, Decay_Array_Pointer, diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index fc989c6..8ba9112 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -256,7 +256,7 @@ valid_value :: proc( .Load_Global, .Function_Address, .Address_Of, .Load, .Union_Tag, .Slice, .Length, .Slice_Ptr, .Fallible_Error, .Extract, .Select, .Unwrap, .Optional_Is_Some, .Optional_Value, .Orelse, - .Widen, .Sum_Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer, + .Widen, .Sum_Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Scalar_Cast, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer, .Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, .Pointer_Add, .Not, .Compare, .Call: return true case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, @@ -1403,16 +1403,15 @@ emit_instruction_stream :: proc( emit_recovery_value(emitter, instruction_index, instruction, "unsupported sum widening operand") case .C_Coerce: if !valid_instruction(instructions, instruction.a) || - !types.can_coerce_c_integer(instructions[instruction.a].type, instruction.type) { - emit_recovery_value(emitter, instruction_index, instruction, "invalid C integer coercion operand") + !(types.can_coerce_c_integer(instructions[instruction.a].type, instruction.type, emitter.module.target) || + types.can_coerce_c_scalar(instructions[instruction.a].type, instruction.type, emitter.module.target)) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid C scalar coercion operand") continue } from_type := instructions[instruction.a].type from_bits := types.bits(from_type, emitter.module.target) to_bits := types.bits(instruction.type, emitter.module.target) if from_bits == to_bits { - // Same-width signedness change: c_uint and c_int both lower to the - // identical `iN`, so this is a pure reinterpret (no-op `select`). type_name := llvm_type(instruction.type, &emitter.module.types) fmt.sbprintf(&emitter.builder, " %%v%d = select i1 true, %s ", instruction_index, type_name) write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) @@ -1421,7 +1420,8 @@ emit_instruction_stream :: proc( strings.write_string(&emitter.builder, "\n") continue } - operation := "sext" if types.is_signed(from_type, emitter.module.target) else "zext" + operation := "fpext" if types.is_float(from_type, emitter.module.target) else + ("sext" if types.is_signed(from_type, emitter.module.target) else "zext") fmt.sbprintf(&emitter.builder, " %%v%d = %s %s ", instruction_index, operation, llvm_type(from_type, &emitter.module.types)) write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) fmt.sbprintf(&emitter.builder, " to %s\n", llvm_type(instruction.type, &emitter.module.types)) @@ -1467,6 +1467,43 @@ emit_instruction_stream :: proc( &emitter.module.types, ) fmt.sbprintf(&emitter.builder, ", %s zeroinitializer\n", type_name) + case .Scalar_Cast: + if !valid_instruction(instructions, instruction.a) || + !types.is_concrete_scalar(instructions[instruction.a].type) || + !types.is_concrete_scalar(instruction.type) || + types.is_bool(instructions[instruction.a].type) || + types.is_bool(instruction.type) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid scalar cast operand") + continue + } + from_type := instructions[instruction.a].type + from_bits := types.bits(from_type, emitter.module.target) + to_bits := types.bits(instruction.type, emitter.module.target) + from_float := types.is_float(from_type, emitter.module.target) + to_float := types.is_float(instruction.type, emitter.module.target) + if types.equal(from_type, instruction.type) || from_bits == to_bits && from_float == to_float { + type_name := llvm_type(instruction.type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, " %%v%d = select i1 true, %s ", instruction_index, type_name) + write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, ", %s ", type_name) + write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) + strings.write_string(&emitter.builder, "\n") + continue + } + operation := "" + switch { + case from_float && to_float: + operation = "fpext" if from_bits < to_bits else "fptrunc" + case !from_float && !to_float: + operation = "trunc" if from_bits > to_bits else ("sext" if types.is_signed(from_type, emitter.module.target) else "zext") + case from_float: + operation = "fptosi" if types.is_signed(instruction.type, emitter.module.target) else "fptoui" + case: + operation = "sitofp" if types.is_signed(from_type, emitter.module.target) else "uitofp" + } + fmt.sbprintf(&emitter.builder, " %%v%d = %s %s ", instruction_index, operation, llvm_type(from_type, &emitter.module.types)) + write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) + fmt.sbprintf(&emitter.builder, " to %s\n", llvm_type(instruction.type, &emitter.module.types)) case .Weaken_Pointer: if !valid_instruction(instructions, instruction.a) || !types.can_weaken_pointer(instructions[instruction.a].type, instruction.type, &emitter.module.types) { diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 60b10ff..cbade56 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -665,7 +665,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { }) } _ = pop(&stack) - case .Widen, .Sum_Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer: + case .Widen, .Sum_Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Scalar_Cast, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer: stack[frame_index].stage = 1 append(&stack, Lower_Expr_Frame{expr=expr.left}) case .Negate: @@ -726,6 +726,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { case .C_Coerce: op = .C_Coerce case .C_Vararg_Promote: op = .C_Vararg_Promote case .Retype: op = .Retype + case .Scalar_Cast: op = .Scalar_Cast case: op = .Widen } last = append_instruction(state, ir.Instruction{ diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 77e148e..6292c0e 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -212,9 +212,15 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax { if _, ok := allow(parser, .Underscore); ok { node.inferred_count = true } else { - count, ok := parse_type_constant(parser) - if ok { + if (current(parser).kind == .Integer || current(parser).kind == .Character) && + (peek(parser).kind == .Right_Bracket || peek(parser).kind == .Semicolon) { + count, ok := parse_type_constant(parser) node.count = count + _ = ok + } else { + expr := parse_expression(parser) + node.count_expr = u32(expr) + node.unresolved_count = true } } if _, ok := allow(parser, .Semicolon); ok { @@ -585,6 +591,37 @@ parse_integer_magnitude :: proc(text: string) -> (u64, bool) { parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id { tok := current(parser) #partial switch tok.kind { + case .Keyword_I8, .Keyword_I16, .Keyword_I32, .Keyword_I64, + .Keyword_U8, .Keyword_U16, .Keyword_U32, .Keyword_U64, + .Keyword_Isize, .Keyword_Usize, .Keyword_F32, .Keyword_F64, + .Keyword_C_Char, .Keyword_C_Schar, .Keyword_C_Uchar, + .Keyword_C_Short, .Keyword_C_Ushort, .Keyword_C_Int, .Keyword_C_Uint, + .Keyword_C_Long, .Keyword_C_Ulong, .Keyword_C_Longlong, .Keyword_C_Ulonglong, + .Keyword_C_Float, .Keyword_C_Double, .Keyword_C_Longdouble: + start := tok + target := parse_type_atom(parser) + if _, ok := allow(parser, .Left_Paren); !ok { + return invalid_expr(parser, current(parser).span, "expected '(' after scalar cast type") + } + parser.delimiter_depth += 1 + skip_newlines(parser) + operand := parse_expression_bp(parser, 0, nesting+1) + skip_newlines(parser) + end := current(parser) + if close, ok := allow(parser, .Right_Paren); ok { + end = close + } else { + source.add(parser.diagnostics, current(parser).span, "expected ')' after scalar cast") + } + parser.delimiter_depth -= 1 + return add_expr(parser, ast.Expr{ + kind=.Cast, + span=span_from(start.span, end.span), + type=target, + left=operand, + right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) case .Integer: advance(parser) value, ok := parse_integer_magnitude(token_text(parser, tok)) @@ -1126,6 +1163,21 @@ parse_return :: proc(parser: ^Parser) -> ast.Stmt_Id { }) return id } + if cf, is_cf := parse_value_control_flow(parser); is_cf { + cf_span := parser.module.statements[cf].span + body := make([]ast.Stmt_Id, 1, parser.module.allocator) + body[0] = cf + id := ast.stmt_id(len(parser.module.statements)) + append(&parser.module.statements, ast.Stmt{ + kind=.Return, + span=span_from(start.span, cf_span), + expr=ast.INVALID_EXPR, + body=body, + value_control_flow=true, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return id + } expr := parse_expression(parser) id := ast.stmt_id(len(parser.module.statements)) append(&parser.module.statements, ast.Stmt{ @@ -1154,6 +1206,22 @@ parse_yield :: proc(parser: ^Parser) -> ast.Stmt_Id { source.add(parser.diagnostics, current(parser).span, "expected a loop label after ':'") } } + if cf, is_cf := parse_value_control_flow(parser); is_cf { + cf_span := parser.module.statements[cf].span + body := make([]ast.Stmt_Id, 1, parser.module.allocator) + body[0] = cf + id := ast.stmt_id(len(parser.module.statements)) + append(&parser.module.statements, ast.Stmt{ + kind=.Yield, + span=span_from(start.span, cf_span), + label=label, + expr=ast.INVALID_EXPR, + body=body, + value_control_flow=true, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + return id + } expr := parse_expression(parser) id := ast.stmt_id(len(parser.module.statements)) append(&parser.module.statements, ast.Stmt{ diff --git a/compiler/types/types.odin b/compiler/types/types.odin index c70e289..1ad747e 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -80,6 +80,7 @@ Node :: struct { child: Type, extra: Type, count: u64, + count_expr: u32, sentinel: u64, explicit_size: u64, field_start: u32, @@ -93,6 +94,7 @@ Node :: struct { many: bool, has_sentinel: bool, inferred_count: bool, + unresolved_count: bool, c_abi: bool, variadic: bool, c_layout: bool, @@ -1308,6 +1310,8 @@ with_array_count :: proc(store: ^Store, value: Type, count: u64) -> Type { } item.count = count item.inferred_count = false + item.unresolved_count = false + item.count_expr = 0 return intern(store, item) } @@ -1339,6 +1343,16 @@ can_weaken_slice :: proc(from, to: Type, store: ^Store) -> bool { (from_node.has_sentinel && from_node.sentinel == to_node.sentinel)) } +can_decay_slice_c_string :: proc(from, to: Type, store: ^Store) -> bool { + from_node, from_ok := node(store, from) + to_node, to_ok := node(store, to) + return from_ok && to_ok && + from_node.kind == .Slice && to_node.kind == .Pointer && to_node.many && + from_node.child == U8 && to_node.child == C_CHAR && + from_node.has_sentinel && from_node.sentinel == 0 && !from_node.mutable && + !to_node.mutable +} + can_decay_array_pointer :: proc(from, to: Type, store: ^Store) -> bool { from_pointer, array, from_ok := array_pointer(from, store) to_node, to_ok := node(store, to) @@ -1536,10 +1550,18 @@ can_widen :: proc(from, to: Type) -> bool { // make C interop cumbersome. Scope: widening (sext/zext) and same-width // signedness changes (no-op reinterpret); narrowing is intentionally excluded so // lossy conversions stay an error, matching brolang's trap-on-narrow philosophy. -can_coerce_c_integer :: proc(from, to: Type) -> bool { +can_coerce_c_integer :: proc(from, to: Type, selected := target.DEFAULT) -> bool { return from != to && is_c(from) && is_c(to) && is_concrete_integer(from) && is_concrete_integer(to) && - bits(from) <= bits(to) + bits(from, selected) <= bits(to, selected) +} + +can_coerce_c_scalar :: proc(from, to: Type, selected := target.DEFAULT) -> bool { + return from != to && !is_c(from) && is_c(to) && + is_concrete_scalar(from) && is_concrete_scalar(to) && + category(from, selected) == category(to, selected) && + category(from, selected) != .None && + bits(from, selected) <= bits(to, selected) } widest :: proc(a, b: Type) -> Type { diff --git a/compiler_tests.odin b/compiler_tests.odin index a9e57fc..38fe31d 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2033,6 +2033,35 @@ sentinel_pointer_views_compile_and_run :: proc(t: ^testing.T) { testing.expect_value(t, state.exit_code, 303) } +@(test) +milestone_24_regressions_compile_and_run :: proc(t: ^testing.T) { + output := "/tmp/brolang-test-milestone-24" + defer _ = os.remove(output) + status := compiler_core.compile_package("examples/programs/milestone_24", output) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +milestone_24_rejects_invalid_forms :: proc(t: ^testing.T) { + Case :: struct { + directory: string, + output: string, + } + cases := [?]Case{ + {"examples/programs/index_signed_error", "/tmp/brolang-test-index-signed-error"}, + {"examples/programs/index_int_constraint_error", "/tmp/brolang-test-index-int-constraint-error"}, + {"examples/programs/scalar_cast_error", "/tmp/brolang-test-scalar-cast-error"}, + {"examples/programs/array_const_size_error", "/tmp/brolang-test-array-const-size-error"}, + } + for test_case in cases { + status := compiler_core.compile_package(test_case.directory, test_case.output) + testing.expect_value(t, status, 1) + _ = os.remove(test_case.output) + } +} + @(test) control_flow_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-control-flow" diff --git a/examples/programs/array_const_size_error/main.bro b/examples/programs/array_const_size_error/main.bro new file mode 100644 index 0000000..d8be2b4 --- /dev/null +++ b/examples/programs/array_const_size_error/main.bro @@ -0,0 +1,5 @@ +main func() i32 { + n usize = 4 + items [n]mut i32 = undefined + return 0 +} diff --git a/examples/programs/index_int_constraint_error/main.bro b/examples/programs/index_int_constraint_error/main.bro new file mode 100644 index 0000000..50324a9 --- /dev/null +++ b/examples/programs/index_int_constraint_error/main.bro @@ -0,0 +1,6 @@ +main func() i32 { + items [3]mut i32 = undefined + i int = 1 + items[i] = 42 + return 0 +} diff --git a/examples/programs/index_signed_error/main.bro b/examples/programs/index_signed_error/main.bro new file mode 100644 index 0000000..290d241 --- /dev/null +++ b/examples/programs/index_signed_error/main.bro @@ -0,0 +1,6 @@ +main func() i32 { + items [3]mut i32 = undefined + i i32 = 1 + items[i] = 42 + return 0 +} diff --git a/examples/programs/milestone_24/main.bro b/examples/programs/milestone_24/main.bro new file mode 100644 index 0000000..ac06836 --- /dev/null +++ b/examples/programs/milestone_24/main.bro @@ -0,0 +1,93 @@ +CAP :: 4 +LEN :: CAP + 1 + +Kind :: enum { + circle + square + triangle +} + +take_c_int func(value c_int) c_int { + return value +} + +take_c_uchar func(value c_uchar) c_uchar { + return value +} + +take_c_float func(value c_float) c_float { + return value +} + +take_c_double func(value c_double) c_double { + return value +} + +take_c_string func(value ?*c_char) i32 { + _ = value + return 0 +} + +next_kind func(k Kind) Kind { + return match k { + .circle: .square + .square: .triangle + .triangle: .circle + } +} + +score_for func(k Kind) i32 { + score :: { + yield match k { + .circle: 1 + .square: 2 + .triangle: 3 + } + } + return score +} + +main func() i32 { + items [LEN]mut i32 = undefined + items[0] = 10 + items[1] = 20 + + idx u8 = 2 + items[idx] = items[0] + items[1] + if (items[2] != 30) return 1 + + native_i i32 = 12 + if (take_c_int(native_i) != 12) return 2 + + native_u u8 = 7 + if (take_c_uchar(native_u) != 7) return 3 + + native_f f32 = 3.25 + cf :: take_c_float(native_f) + if (cf < 3.0 or cf > 4.0) return 4 + if (cf == 0.0) return 5 + + native_d f64 = 5.0 + cd :: take_c_double(native_d) + if (cd != 5.0) return 6 + + as_i32 :: i32(cf) + if (as_i32 != 3) return 7 + as_float :: f32(native_i) + if (as_float < 11.5 or as_float > 12.5) return 8 + as_c_float :: c_float(as_i32) + if (as_c_float != 3.0) return 9 + + kind :: next_kind(.circle) + if (kind != .square) return 10 + if (score_for(kind) != 2) return 11 + + label :: match kind { + .circle: "circle" + .square: "square" + .triangle: "triangle" + } + if (take_c_string(label) != 0) return 12 + + return 0 +} diff --git a/examples/programs/scalar_cast_error/main.bro b/examples/programs/scalar_cast_error/main.bro new file mode 100644 index 0000000..7972747 --- /dev/null +++ b/examples/programs/scalar_cast_error/main.bro @@ -0,0 +1,5 @@ +main func() i32 { + flag bool = true + value :: i32(flag) + return value +}