fix catch handlers / match statements yielding past scope

This commit is contained in:
2026-06-30 23:21:47 +02:00
parent d6f9c24314
commit 7fe3552c01
6 changed files with 117 additions and 44 deletions
+49 -1
View File
@@ -603,7 +603,55 @@
- structurally identical anonymous struct payloads share type identity, so sum composition merges - structurally identical anonymous struct payloads share type identity, so sum composition merges
matching variants and still rejects same-name variants with different payload shapes matching variants and still rejects same-name variants with different payload shapes
24. dynamic heap allocation 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 <struct>, 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
25. dynamic heap allocation
- see below for direction - see below for direction
- notes below are too big in scope for a first pass and the language is not mature enough to support it yet - notes below are too big in scope for a first pass and the language is not mature enough to support it yet
- this first pass should focus on just basic heap allocation, so we have something to work with - this first pass should focus on just basic heap allocation, so we have something to work with
+1
View File
@@ -160,6 +160,7 @@ Stmt :: struct {
label: symbol.Id, label: symbol.Id,
type: Type_Syntax, type: Type_Syntax,
immutable: bool, immutable: bool,
value_control_flow: bool,
pointer_capture: bool, pointer_capture: bool,
// Assignments store the lvalue in `target`, the right-hand side in `expr`, // Assignments store the lvalue in `target`, the right-hand side in `expr`,
// and the source operator in `assignment_op`. `Set` is ordinary `=`; // and the source operator in `assignment_op`. `Set` is ordinary `=`;
+10 -9
View File
@@ -4417,7 +4417,7 @@ build_block :: proc(
if typed { if typed {
expected = type_from_syntax(statement.type) expected = type_from_syntax(statement.type)
} }
value, value_type := build_value_source(ctx, &body, statement.body, expected, statement.span, statement.label) 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 { if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found {
id := source.addf( id := source.addf(
checker.diagnostics, statement.span, checker.diagnostics, statement.span,
@@ -4628,7 +4628,7 @@ build_block :: proc(
} else if statement.expr == ast.INVALID_EXPR { } else if statement.expr == ast.INVALID_EXPR {
// `target = { ... yield v }`: build the value block against the // `target = { ... yield v }`: build the value block against the
// target's type (build_value_block coerces internally). // target's type (build_value_block coerces internally).
value, _ = build_value_source(ctx, &body, statement.body, target_type, statement.span, statement.label) value, _ = build_value_source(ctx, &body, statement.body, target_type, statement.span, statement.label, statement.value_control_flow)
} else { } else {
value = build_expr( value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
@@ -4648,7 +4648,7 @@ build_block :: proc(
if statement.name == checker.sink_symbol { if statement.name == checker.sink_symbol {
value: hir.Expr_Id value: hir.Expr_Id
if statement.expr == ast.INVALID_EXPR { if statement.expr == ast.INVALID_EXPR {
value, _ = build_value_source(ctx, &body, statement.body, types.INVALID, statement.span, statement.label) value, _ = build_value_source(ctx, &body, statement.body, types.INVALID, statement.span, statement.label, statement.value_control_flow)
} else { } else {
value = build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) value = build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
} }
@@ -4692,7 +4692,7 @@ build_block :: proc(
} }
value: hir.Expr_Id value: hir.Expr_Id
if statement.expr == ast.INVALID_EXPR { if statement.expr == ast.INVALID_EXPR {
value, _ = build_value_source(ctx, &body, statement.body, local.type, statement.span, statement.label) value, _ = build_value_source(ctx, &body, statement.body, local.type, statement.span, statement.label, statement.value_control_flow)
} else { } else {
value = build_expr( value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
@@ -5430,10 +5430,10 @@ build_value_block :: proc(
return value, value_type return value, value_type
} }
// build_value_source feeds a declaration/assignment RHS into the right value builder: // build_value_source feeds a declaration/assignment RHS into the right value builder.
// a labeled `blk: { ... }` block, a plain `{ ... }` block, an `if` whose branches yield, // Braced blocks are plain value blocks and must end in their own `yield`; bare RHS
// or a `for`/`while` whose iterations yield. All return the produced value and its type // control flow (`x :: match ...`) sets `value_control_flow` and can produce directly.
// for the enclosing binding. `label` is the labeled-block label (INVALID otherwise). // `label` is the labeled-block label (INVALID otherwise).
build_value_source :: proc( build_value_source :: proc(
ctx: ^Build_Ctx, ctx: ^Build_Ctx,
body: ^[dynamic]hir.Stmt_Id, body: ^[dynamic]hir.Stmt_Id,
@@ -5441,12 +5441,13 @@ build_value_source :: proc(
expected: types.Type, expected: types.Type,
span: source.Span, span: source.Span,
label := symbol.INVALID, label := symbol.INVALID,
value_control_flow := false,
) -> (value: hir.Expr_Id, value_type: types.Type) { ) -> (value: hir.Expr_Id, value_type: types.Type) {
checker := ctx.checker checker := ctx.checker
if symbol.is_valid(label) { if symbol.is_valid(label) {
return build_value_labeled_block(ctx, body, body_stmts, label, expected, span) return build_value_labeled_block(ctx, body, body_stmts, label, expected, span)
} }
if len(body_stmts) == 1 { if value_control_flow && len(body_stmts) == 1 {
#partial switch checker.ast_module.statements[body_stmts[0]].kind { #partial switch checker.ast_module.statements[body_stmts[0]].kind {
case .If: case .If:
return build_value_if(ctx, body, body_stmts[0], expected, span) return build_value_if(ctx, body, body_stmts[0], expected, span)
+2
View File
@@ -1376,6 +1376,7 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
name=name.symbol, name=name.symbol,
type=type_syntax, type=type_syntax,
immutable=immutable, immutable=immutable,
value_control_flow=true,
target=ast.INVALID_EXPR, target=ast.INVALID_EXPR,
expr=ast.INVALID_EXPR, expr=ast.INVALID_EXPR,
body=body, body=body,
@@ -1443,6 +1444,7 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
kind=.Assignment, kind=.Assignment,
span=span_from(parser.module.exprs[expr].span, previous(parser).span), span=span_from(parser.module.exprs[expr].span, previous(parser).span),
target=expr, target=expr,
value_control_flow=true,
expr=ast.INVALID_EXPR, expr=ast.INVALID_EXPR,
body=body, body=body,
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
+30 -15
View File
@@ -2459,9 +2459,16 @@ bad_catch func() i32 {
_ = e _ = e
} }
} }
bad_nested_match func() i32 {
return fa() catch |e| {
match e {
.a: yield 3
}
}
}
main func() i32 { main func() i32 {
_ = bad_error() catch 0 _ = bad_error() catch 0
return bad_catch() return bad_catch() + bad_nested_match()
} }
` `
source_file := source.Source{path="fallible_ergonomics.bro", text=text} source_file := source.Source{path="fallible_ergonomics.bro", text=text}
@@ -2478,12 +2485,15 @@ main func() i32 {
found_error := false found_error := false
found_yield := false found_yield := false
found_misplaced_yield := false
for diagnostic in diagnostics.items { for diagnostic in diagnostics.items {
found_error = found_error || strings.contains(diagnostic.message, "'try' error channel cannot be widened") found_error = found_error || strings.contains(diagnostic.message, "'try' error channel cannot be widened")
found_yield = found_yield || strings.contains(diagnostic.message, "a value block must end with an explicit 'yield'") found_yield = found_yield || strings.contains(diagnostic.message, "a value block must end with an explicit 'yield'")
found_misplaced_yield = found_misplaced_yield || strings.contains(diagnostic.message, "'yield' is only valid as the final statement")
} }
testing.expect(t, found_error) testing.expect(t, found_error)
testing.expect(t, found_yield) testing.expect(t, found_yield)
testing.expect(t, found_misplaced_yield)
success_text := `A :: enum { success_text := `A :: enum {
a a
@@ -2540,10 +2550,11 @@ with_detail func(value i32) i32 ! DetailError {
main func() i32 { main func() i32 {
e DetailError = .code{3} e DetailError = .code{3}
recovered :: with_detail(0) catch |err| { recovered :: with_detail(0) catch |err| {
match err { result i32 :: match err {
.code |n|: yield n .code |n|: n
.empty: yield 9 .empty: 9
} }
yield result
} }
return accept(e) + accept(.code{4}) + accept(make_code(5)) + accept(.code{make_payload()}) + recovered - 24 return accept(e) + accept(.code{4}) + accept(make_code(5)) + accept(.code{make_payload()}) + recovered - 24
} }
@@ -2617,17 +2628,19 @@ main func() i32 {
a :: accept(e) a :: accept(e)
b :: accept(.wrapped{line = 4, path = 3}) b :: accept(.wrapped{line = 4, path = 3})
c :: with_payload(0) catch |err| { c :: with_payload(0) catch |err| {
match err { result i32 :: match err {
.not_found |info|: yield info.path + info.line .not_found |info|: info.path + info.line
.wrapped |info|: yield info.path + info.line .wrapped |info|: info.path + info.line
.scalar |n|: yield n .scalar |n|: n
.empty: yield 0 .empty: 0
} }
yield result
} }
d :: inline_payload(0) catch |err| { d :: inline_payload(0) catch |err| {
match err { result i32 :: match err {
.inline_bad |info|: yield info.code + info.line .inline_bad |info|: info.code + info.line
} }
yield result
} }
return a + b + c + d - 59 return a + b + c + d - 59
} }
@@ -2672,17 +2685,19 @@ inline_union func(value i32) i32 ! union(enum) {
} }
main func() i32 { main func() i32 {
a :: inline_enum(0) catch |e| { a :: inline_enum(0) catch |e| {
if (e == .inline_bad) { result i32 :: if (e == .inline_bad) {
yield 10 yield 10
} else { } else {
yield 11 yield 11
} }
yield result
} }
b :: inline_union(0) catch |e| { b :: inline_union(0) catch |e| {
match e { result i32 :: match e {
.inline_code |n|: yield n .inline_code |n|: n
.inline_empty: yield 12 .inline_empty: 12
} }
yield result
} }
return a + b - 16 return a + b - 16
} }
+25 -19
View File
@@ -59,33 +59,36 @@ via_widen func(value i32) i32 ! BasicOrDetail {
catch_basic func(value i32) i32 { catch_basic func(value i32) i32 {
return maybe(value) catch |e| { return maybe(value) catch |e| {
if (e == .bad) { result i32 :: if (e == .bad) {
yield 21 yield 21
} else { } else {
yield 22 yield 22
} }
yield result
} }
} }
catch_detail func(value i32) i32 { catch_detail func(value i32) i32 {
return with_detail(value) catch |e| { return with_detail(value) catch |e| {
match e { result i32 :: match e {
.code |n|: yield n + 30 .code |n|: n + 30
.info |info|: yield info.code + info.extra + 30 .info |info|: info.code + info.extra + 30
.empty: yield 40 .empty: 40
} }
yield result
} }
} }
catch_widen func(value i32) i32 { catch_widen func(value i32) i32 {
return via_widen(value) catch |e| { return via_widen(value) catch |e| {
match e { result i32 :: match e {
.bad: yield 50 .bad: 50
.worse: yield 51 .worse: 51
.code |n|: yield n .code |n|: n
.info |info|: yield info.code + info.extra .info |info|: info.code + info.extra
.empty: yield 52 .empty: 52
} }
yield result
} }
} }
@@ -138,24 +141,27 @@ main func() i32 {
l :: catch_widen(0) l :: catch_widen(0)
m :: catch_widen(-1) m :: catch_widen(-1)
n :: inline_enum(0) catch |e| { n :: inline_enum(0) catch |e| {
if (e == .inline_bad) { result i32 :: if (e == .inline_bad) {
yield 60 yield 60
} else { } else {
yield 61 yield 61
} }
yield result
} }
o :: inline_detail(0) catch |e| { o :: inline_detail(0) catch |e| {
match e { result i32 :: match e {
.inline_code |value|: yield value + 70 .inline_code |value|: value + 70
.inline_empty: yield 80 .inline_empty: 80
} }
yield result
} }
p :: with_detail(3) catch |e| { p :: with_detail(3) catch |e| {
match e { result i32 :: match e {
.code |value|: yield value .code |value|: value
.info |info|: yield info.code + info.extra .info |info|: info.code + info.extra
.empty: yield 0 .empty: 0
} }
yield result
} }
acc = acc + a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p acc = acc + a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p