break and continue in loops
This commit is contained in:
@@ -269,13 +269,34 @@
|
|||||||
- `++` concatenation (the spec's "mixing" examples) is a separate, unimplemented
|
- `++` concatenation (the spec's "mixing" examples) is a separate, unimplemented
|
||||||
operator and is out of scope here
|
operator and is out of scope here
|
||||||
|
|
||||||
18. add `defer` statement (inspired by zig)
|
18. add `break` and `continue` statements (implemented)
|
||||||
|
- `break` exits the innermost enclosing loop; `continue` skips to that loop's next
|
||||||
|
iteration (running the `while` update / `for` index increment first). Both target
|
||||||
|
the innermost loop only (no labeled break) and carry no value
|
||||||
|
- new `Keyword_Break`/`Keyword_Continue` tokens; `Break`/`Continue` AST and HIR
|
||||||
|
statement kinds (no fields beyond kind/span); parsed by `parse_loop_control`
|
||||||
|
- the checker tracks loop nesting (`Build_Ctx.loop_depth`, bumped around loop-body
|
||||||
|
builds) and rejects `break`/`continue` outside a loop; `all_paths_return` no longer
|
||||||
|
treats a `while true` whose body can `break` as non-terminating (so a non-void
|
||||||
|
function that breaks out without returning is correctly diagnosed)
|
||||||
|
- lowering keeps an innermost-last loop-target stack (`State.loops`): `break` branches
|
||||||
|
to the loop's exit label, `continue` to its update/latch label. The range-for routes
|
||||||
|
`continue` through the end-of-iteration bounds/overflow guard, so
|
||||||
|
`for 0..=255 |b: u8|` exits cleanly instead of overflowing the increment
|
||||||
|
- the LLVM emitter opens a fresh recovery block after any terminator (not just `ret`),
|
||||||
|
so dead code following a `break`/`continue` branch stays well-formed
|
||||||
|
|
||||||
19. unions and tagged unions
|
19. add `defer` statement (inspired by zig)
|
||||||
|
- now unblocked: `defer` reuses the loop-target stack and loop tracking added in
|
||||||
|
milestone 18 to flush deferred statements on `break`/`continue` exits too
|
||||||
|
|
||||||
20. match statements with tagged unions payload unwrapping
|
20. add `yield` statement (see below)
|
||||||
|
|
||||||
21. dynamic heap allocation
|
21. unions and tagged unions
|
||||||
|
|
||||||
|
22. match statements with tagged unions payload unwrapping
|
||||||
|
|
||||||
|
23. 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
|
||||||
@@ -478,6 +499,38 @@ message = "Header:\t" ++
|
|||||||
++ "Footer"
|
++ "Footer"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## A word on `yield`
|
||||||
|
|
||||||
|
The `yield` keyword provides a value from a block to its enclosing expression and **exits the block immediately** — just as `return` exits a function, `yield` exits the enclosing scope. Code after a `yield` is unreachable, and the compiler flags it. This makes `yield` part of a consistent set of scope-exiting control flow: `return` exits a function, `yield` exits a block, `break` exits a loop, and `continue` skips to the next iteration.
|
||||||
|
|
||||||
|
It is used in scoped blocks, match arms, and catch handlers.
|
||||||
|
|
||||||
|
**General rule:** When a block needs to produce a value, single expressions yield implicitly while multi-statement blocks require explicit `yield`. This rule applies uniformly across the language:
|
||||||
|
|
||||||
|
```
|
||||||
|
# scoped block
|
||||||
|
data :: {
|
||||||
|
result := compute()
|
||||||
|
yield result
|
||||||
|
}
|
||||||
|
|
||||||
|
# match arms
|
||||||
|
label []u8 = match p {
|
||||||
|
.high: "HIGH", # single expression: implicit yield
|
||||||
|
.low: {
|
||||||
|
log("low priority")
|
||||||
|
yield "LOW" # block: explicit yield
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# catch handlers
|
||||||
|
data []u8 = read(path) catch default_data # single expression: implicit
|
||||||
|
data []u8 = read(path) catch |e| {
|
||||||
|
log(e)
|
||||||
|
yield fallback_data # block: explicit yield
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## A word on memory allocation
|
## A word on memory allocation
|
||||||
|
|
||||||
(NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY)
|
(NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY)
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ Stmt_Kind :: enum u8 {
|
|||||||
If,
|
If,
|
||||||
While,
|
While,
|
||||||
For,
|
For,
|
||||||
|
Break,
|
||||||
|
Continue,
|
||||||
}
|
}
|
||||||
|
|
||||||
Assignment_Op :: enum u8 {
|
Assignment_Op :: enum u8 {
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ Build_Ctx :: struct {
|
|||||||
global_reads: ^[dynamic]hir.Global_Id,
|
global_reads: ^[dynamic]hir.Global_Id,
|
||||||
calls: ^[dynamic]hir.Function_Id,
|
calls: ^[dynamic]hir.Function_Id,
|
||||||
problematic: ^bool,
|
problematic: ^bool,
|
||||||
|
// Number of enclosing loops being built. `break`/`continue` are only valid
|
||||||
|
// when this is > 0; bumped around loop-body builds in `build_block`.
|
||||||
|
loop_depth: int,
|
||||||
}
|
}
|
||||||
|
|
||||||
Constant_Kind :: enum {
|
Constant_Kind :: enum {
|
||||||
@@ -734,6 +737,7 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi
|
|||||||
case .For:
|
case .For:
|
||||||
mark_expr_imports_used(checker, statement.expr, file)
|
mark_expr_imports_used(checker, statement.expr, file)
|
||||||
mark_block_imports_used(checker, statement.body, file)
|
mark_block_imports_used(checker, statement.body, file)
|
||||||
|
case .Break, .Continue:
|
||||||
case .Invalid:
|
case .Invalid:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4439,7 +4443,9 @@ build_block :: proc(
|
|||||||
condition = invalid_hir_expr(checker, statement.span, id, types.BOOL)
|
condition = invalid_hir_expr(checker, statement.span, id, types.BOOL)
|
||||||
ctx.problematic^ = true
|
ctx.problematic^ = true
|
||||||
}
|
}
|
||||||
|
ctx.loop_depth += 1
|
||||||
loop_body := build_block(ctx, statement.body)
|
loop_body := build_block(ctx, statement.body)
|
||||||
|
ctx.loop_depth -= 1
|
||||||
update := hir.INVALID_STMT
|
update := hir.INVALID_STMT
|
||||||
if statement.update != ast.INVALID_STMT {
|
if statement.update != ast.INVALID_STMT {
|
||||||
update_ast := [1]ast.Stmt_Id{statement.update}
|
update_ast := [1]ast.Stmt_Id{statement.update}
|
||||||
@@ -4531,7 +4537,9 @@ build_block :: proc(
|
|||||||
append(ctx.locals, Build_Local{name=statement.index_name, type=types.USIZE, mutable=false, id=index_local})
|
append(ctx.locals, Build_Local{name=statement.index_name, type=types.USIZE, mutable=false, id=index_local})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ctx.loop_depth += 1
|
||||||
loop_body := build_block(ctx, statement.body, capture_start)
|
loop_body := build_block(ctx, statement.body, capture_start)
|
||||||
|
ctx.loop_depth -= 1
|
||||||
resize(ctx.locals, capture_start)
|
resize(ctx.locals, capture_start)
|
||||||
|
|
||||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||||
@@ -4562,6 +4570,24 @@ build_block :: proc(
|
|||||||
})
|
})
|
||||||
ctx.problematic^ = true
|
ctx.problematic^ = true
|
||||||
}
|
}
|
||||||
|
case .Break, .Continue:
|
||||||
|
if ctx.loop_depth == 0 {
|
||||||
|
keyword := "break" if statement.kind == .Break else "continue"
|
||||||
|
id := source.addf(checker.diagnostics, statement.span, "'%s' outside of a loop", keyword)
|
||||||
|
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||||
|
append(&checker.module.statements, hir.Stmt{
|
||||||
|
kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR,
|
||||||
|
local = hir.INVALID_LOCAL, diagnostic = id,
|
||||||
|
})
|
||||||
|
ctx.problematic^ = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||||
|
append(&checker.module.statements, hir.Stmt{
|
||||||
|
kind = .Break if statement.kind == .Break else .Continue,
|
||||||
|
span = statement.span, expr = hir.INVALID_EXPR,
|
||||||
|
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
case .Invalid:
|
case .Invalid:
|
||||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||||
append(&checker.module.statements, hir.Stmt{
|
append(&checker.module.statements, hir.Stmt{
|
||||||
@@ -4593,9 +4619,12 @@ all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
case .While:
|
case .While:
|
||||||
|
// A literal `while true` makes the end of the block unreachable —
|
||||||
|
// unless its body can `break` out of this loop.
|
||||||
if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) {
|
if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) {
|
||||||
condition := module.exprs[statement.expr]
|
condition := module.exprs[statement.expr]
|
||||||
if condition.kind == .Bool && condition.integer != 0 {
|
if condition.kind == .Bool && condition.integer != 0 &&
|
||||||
|
!loop_body_breaks(module, statement.then_body) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4604,6 +4633,25 @@ all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reports whether `stmts` contains a `break` that targets the enclosing loop:
|
||||||
|
// a `.Break` at this level or inside `if`/`else` branches counts, but a `break`
|
||||||
|
// inside a nested `.While`/`.For` targets that inner loop, so we do not descend.
|
||||||
|
loop_body_breaks :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
|
||||||
|
for id in stmts {
|
||||||
|
statement := module.statements[id]
|
||||||
|
#partial switch statement.kind {
|
||||||
|
case .Break:
|
||||||
|
return true
|
||||||
|
case .If:
|
||||||
|
if loop_body_breaks(module, statement.then_body) ||
|
||||||
|
loop_body_breaks(module, statement.else_body) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
||||||
spec := checker.specs[id]
|
spec := checker.specs[id]
|
||||||
function := checker.ast_module.functions[spec.template]
|
function := checker.ast_module.functions[spec.template]
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ Stmt_Kind :: enum u8 {
|
|||||||
If,
|
If,
|
||||||
While,
|
While,
|
||||||
For,
|
For,
|
||||||
|
Break,
|
||||||
|
Continue,
|
||||||
}
|
}
|
||||||
|
|
||||||
Assignment_Op :: enum u8 {
|
Assignment_Op :: enum u8 {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ keyword_kind :: proc(text: string) -> token.Kind {
|
|||||||
case "if": return .Keyword_If
|
case "if": return .Keyword_If
|
||||||
case "while": return .Keyword_While
|
case "while": return .Keyword_While
|
||||||
case "for": return .Keyword_For
|
case "for": return .Keyword_For
|
||||||
|
case "break": return .Keyword_Break
|
||||||
|
case "continue": return .Keyword_Continue
|
||||||
case "else": return .Keyword_Else
|
case "else": return .Keyword_Else
|
||||||
case "true": return .Keyword_True
|
case "true": return .Keyword_True
|
||||||
case "false": return .Keyword_False
|
case "false": return .Keyword_False
|
||||||
|
|||||||
+15
-6
@@ -652,12 +652,18 @@ emit_instruction_stream :: proc(
|
|||||||
sret_name := "",
|
sret_name := "",
|
||||||
) -> ir.Instruction_Id {
|
) -> ir.Instruction_Id {
|
||||||
return_value := ir.INVALID_INSTRUCTION
|
return_value := ir.INVALID_INSTRUCTION
|
||||||
after_return := false
|
// Set after any terminator (`ret`, `br`, conditional `br`). Code reachable
|
||||||
|
// only by falling off a terminator is dead; it needs a fresh label to form a
|
||||||
|
// well-formed basic block — unless the next instruction is already a `.Label`,
|
||||||
|
// which opens its own block (the normal terminator-then-label sequence).
|
||||||
|
after_terminator := false
|
||||||
for instruction, instruction_index in instructions {
|
for instruction, instruction_index in instructions {
|
||||||
instruction_id := ir.instruction_id(instruction_index)
|
instruction_id := ir.instruction_id(instruction_index)
|
||||||
if after_return {
|
if after_terminator {
|
||||||
fmt.sbprintf(&emitter.builder, "recover_after_return_%d:\n", instruction_index)
|
if instruction.op != .Label {
|
||||||
after_return = false
|
fmt.sbprintf(&emitter.builder, "recover_after_return_%d:\n", instruction_index)
|
||||||
|
}
|
||||||
|
after_terminator = false
|
||||||
}
|
}
|
||||||
switch instruction.op {
|
switch instruction.op {
|
||||||
case .Param, .Const:
|
case .Param, .Const:
|
||||||
@@ -1662,14 +1668,17 @@ emit_instruction_stream :: proc(
|
|||||||
fmt.sbprintf(&emitter.builder, "bro_block_%d:\n", instruction.integer)
|
fmt.sbprintf(&emitter.builder, "bro_block_%d:\n", instruction.integer)
|
||||||
case .Br:
|
case .Br:
|
||||||
fmt.sbprintf(&emitter.builder, " br label %%bro_block_%d\n", instruction.integer)
|
fmt.sbprintf(&emitter.builder, " br label %%bro_block_%d\n", instruction.integer)
|
||||||
|
after_terminator = true
|
||||||
case .Cond_Br:
|
case .Cond_Br:
|
||||||
if !valid_value(instructions, instruction.a, types.BOOL, &emitter.module.types) {
|
if !valid_value(instructions, instruction.a, types.BOOL, &emitter.module.types) {
|
||||||
fmt.sbprintf(&emitter.builder, " br label %%bro_block_%d\n", u32(instruction.target))
|
fmt.sbprintf(&emitter.builder, " br label %%bro_block_%d\n", u32(instruction.target))
|
||||||
|
after_terminator = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
strings.write_string(&emitter.builder, " br i1 ")
|
strings.write_string(&emitter.builder, " br i1 ")
|
||||||
write_operand(&emitter.builder, instructions, instruction.a, types.BOOL, &emitter.module.types)
|
write_operand(&emitter.builder, instructions, instruction.a, types.BOOL, &emitter.module.types)
|
||||||
fmt.sbprintf(&emitter.builder, ", label %%bro_block_%d, label %%bro_block_%d\n", instruction.integer, u32(instruction.target))
|
fmt.sbprintf(&emitter.builder, ", label %%bro_block_%d, label %%bro_block_%d\n", instruction.integer, u32(instruction.target))
|
||||||
|
after_terminator = true
|
||||||
case .Trap:
|
case .Trap:
|
||||||
message := diagnostic_message(emitter, instruction.diagnostic, instruction.span, "invalid recovered source")
|
message := diagnostic_message(emitter, instruction.diagnostic, instruction.span, "invalid recovered source")
|
||||||
emit_trap_call(emitter, message)
|
emit_trap_call(emitter, message)
|
||||||
@@ -1703,7 +1712,7 @@ emit_instruction_stream :: proc(
|
|||||||
write_operand(&emitter.builder, instructions, instruction.a, function.result, &emitter.module.types)
|
write_operand(&emitter.builder, instructions, instruction.a, function.result, &emitter.module.types)
|
||||||
strings.write_string(&emitter.builder, "\n")
|
strings.write_string(&emitter.builder, "\n")
|
||||||
}
|
}
|
||||||
after_return = true
|
after_terminator = true
|
||||||
case .Return_Void:
|
case .Return_Void:
|
||||||
if global_initializer {
|
if global_initializer {
|
||||||
continue
|
continue
|
||||||
@@ -1713,7 +1722,7 @@ emit_instruction_stream :: proc(
|
|||||||
} else {
|
} else {
|
||||||
strings.write_string(&emitter.builder, " ret void\n")
|
strings.write_string(&emitter.builder, " ret void\n")
|
||||||
}
|
}
|
||||||
after_return = true
|
after_terminator = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return return_value
|
return return_value
|
||||||
|
|||||||
@@ -16,10 +16,19 @@ State :: struct {
|
|||||||
func_locals: []hir.Local,
|
func_locals: []hir.Local,
|
||||||
func_result: types.Type,
|
func_result: types.Type,
|
||||||
expr_stack: [dynamic]Lower_Expr_Frame,
|
expr_stack: [dynamic]Lower_Expr_Frame,
|
||||||
|
// Innermost-last stack of enclosing loop targets for `break`/`continue`.
|
||||||
|
loops: [dynamic]Loop_Ctx,
|
||||||
next_label: i64,
|
next_label: i64,
|
||||||
allocator: mem.Allocator,
|
allocator: mem.Allocator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `break` branches to `exit_lbl`; `continue` branches to `continue_lbl` (the
|
||||||
|
// loop's update/latch, which runs the update clause then re-tests the condition).
|
||||||
|
Loop_Ctx :: struct {
|
||||||
|
exit_lbl: i64,
|
||||||
|
continue_lbl: i64,
|
||||||
|
}
|
||||||
|
|
||||||
fresh_label :: proc(state: ^State) -> i64 {
|
fresh_label :: proc(state: ^State) -> i64 {
|
||||||
id := state.next_label
|
id := state.next_label
|
||||||
state.next_label += 1
|
state.next_label += 1
|
||||||
@@ -742,6 +751,18 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
|
|||||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
case .Break, .Continue:
|
||||||
|
// The checker guarantees these only appear inside a loop, so the
|
||||||
|
// stack is non-empty; guard defensively regardless.
|
||||||
|
if len(state.loops) > 0 {
|
||||||
|
target := state.loops[len(state.loops)-1]
|
||||||
|
label := target.exit_lbl if statement.kind == .Break else target.continue_lbl
|
||||||
|
append_instruction(state, ir.Instruction{
|
||||||
|
op=.Br, span=statement.span, type=types.VOID, integer=label,
|
||||||
|
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||||
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
}
|
||||||
case .Expression, .Sink:
|
case .Expression, .Sink:
|
||||||
_ = lower_expr(state, statement.expr)
|
_ = lower_expr(state, statement.expr)
|
||||||
case .Trap:
|
case .Trap:
|
||||||
@@ -919,7 +940,9 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
|
|||||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
|
append(&state.loops, Loop_Ctx{exit_lbl=exit_lbl, continue_lbl=update_lbl})
|
||||||
lower_statements(state, statement.then_body)
|
lower_statements(state, statement.then_body)
|
||||||
|
pop(&state.loops)
|
||||||
append_instruction(state, ir.Instruction{
|
append_instruction(state, ir.Instruction{
|
||||||
op=.Br, span=statement.span, type=types.VOID, integer=update_lbl,
|
op=.Br, span=statement.span, type=types.VOID, integer=update_lbl,
|
||||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||||
@@ -1043,7 +1066,25 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
|
|||||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
|
// `continue` rejoins the normal end-of-iteration path (via a fresh
|
||||||
|
// label before the bounds/overflow guard) rather than jumping straight
|
||||||
|
// to the increment, so it behaves exactly like falling off the body —
|
||||||
|
// e.g. `for 0..=255 |b: u8| { ... continue }` exits cleanly instead of
|
||||||
|
// overflowing the increment on the final element.
|
||||||
|
continue_lbl := fresh_label(state)
|
||||||
|
append(&state.loops, Loop_Ctx{exit_lbl=exit_lbl, continue_lbl=continue_lbl})
|
||||||
lower_statements(state, statement.then_body)
|
lower_statements(state, statement.then_body)
|
||||||
|
pop(&state.loops)
|
||||||
|
append_instruction(state, ir.Instruction{
|
||||||
|
op=.Br, span=statement.span, type=types.VOID, integer=continue_lbl,
|
||||||
|
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||||
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
append_instruction(state, ir.Instruction{
|
||||||
|
op=.Label, span=statement.span, type=types.VOID, integer=continue_lbl,
|
||||||
|
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||||
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
after_body := append_instruction(state, ir.Instruction{
|
after_body := append_instruction(state, ir.Instruction{
|
||||||
op=.Load, span=statement.span, type=child,
|
op=.Load, span=statement.span, type=child,
|
||||||
target=ir.INVALID_REF, a=current_slot, b=ir.INVALID_INSTRUCTION,
|
target=ir.INVALID_REF, a=current_slot, b=ir.INVALID_INSTRUCTION,
|
||||||
@@ -1216,7 +1257,9 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
|
|||||||
target=ir.INVALID_REF, a=capture_slot, b=captured,
|
target=ir.INVALID_REF, a=capture_slot, b=captured,
|
||||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
|
append(&state.loops, Loop_Ctx{exit_lbl=exit_lbl, continue_lbl=update_lbl})
|
||||||
lower_statements(state, statement.then_body)
|
lower_statements(state, statement.then_body)
|
||||||
|
pop(&state.loops)
|
||||||
append_instruction(state, ir.Instruction{
|
append_instruction(state, ir.Instruction{
|
||||||
op=.Br, span=statement.span, type=types.VOID, integer=update_lbl,
|
op=.Br, span=statement.span, type=types.VOID, integer=update_lbl,
|
||||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||||
@@ -1267,10 +1310,12 @@ lower_body :: proc(hir_module: ^hir.Module, function: hir.Function, allocator: m
|
|||||||
}
|
}
|
||||||
state.instructions.allocator = allocator
|
state.instructions.allocator = allocator
|
||||||
state.expr_stack.allocator = allocator
|
state.expr_stack.allocator = allocator
|
||||||
|
state.loops.allocator = allocator
|
||||||
defer {
|
defer {
|
||||||
delete(state.local_values, allocator)
|
delete(state.local_values, allocator)
|
||||||
delete(state.local_slots, allocator)
|
delete(state.local_slots, allocator)
|
||||||
delete(state.expr_stack)
|
delete(state.expr_stack)
|
||||||
|
delete(state.loops)
|
||||||
}
|
}
|
||||||
for _, index in state.local_values {
|
for _, index in state.local_values {
|
||||||
state.local_values[index] = ir.INVALID_INSTRUCTION
|
state.local_values[index] = ir.INVALID_INSTRUCTION
|
||||||
|
|||||||
@@ -986,6 +986,21 @@ parse_return :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `break` / `continue` carry no value and target the innermost loop; the
|
||||||
|
// checker rejects them outside a loop.
|
||||||
|
parse_loop_control :: proc(parser: ^Parser, kind: ast.Stmt_Kind) -> ast.Stmt_Id {
|
||||||
|
marker := advance(parser) // consume 'break' / 'continue'
|
||||||
|
id := ast.stmt_id(len(parser.module.statements))
|
||||||
|
append(&parser.module.statements, ast.Stmt{
|
||||||
|
kind=kind,
|
||||||
|
span=marker.span,
|
||||||
|
expr=ast.INVALID_EXPR,
|
||||||
|
update=ast.INVALID_STMT,
|
||||||
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
starts_declared_type :: proc(parser: ^Parser) -> bool {
|
starts_declared_type :: proc(parser: ^Parser) -> bool {
|
||||||
if current(parser).kind != .Left_Bracket {
|
if current(parser).kind != .Left_Bracket {
|
||||||
return is_type_token(current(parser).kind)
|
return is_type_token(current(parser).kind)
|
||||||
@@ -1024,6 +1039,12 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
if current(parser).kind == .Keyword_For {
|
if current(parser).kind == .Keyword_For {
|
||||||
return parse_for(parser)
|
return parse_for(parser)
|
||||||
}
|
}
|
||||||
|
if current(parser).kind == .Keyword_Break {
|
||||||
|
return parse_loop_control(parser, .Break)
|
||||||
|
}
|
||||||
|
if current(parser).kind == .Keyword_Continue {
|
||||||
|
return parse_loop_control(parser, .Continue)
|
||||||
|
}
|
||||||
|
|
||||||
if current(parser).kind == .Identifier || current(parser).kind == .Underscore {
|
if current(parser).kind == .Identifier || current(parser).kind == .Underscore {
|
||||||
start_cursor := parser.cursor
|
start_cursor := parser.cursor
|
||||||
@@ -1312,7 +1333,7 @@ parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
statement := &parser.module.statements[update]
|
statement := &parser.module.statements[update]
|
||||||
switch statement.kind {
|
switch statement.kind {
|
||||||
case .Assignment, .Expression:
|
case .Assignment, .Expression:
|
||||||
case .Invalid, .Declaration, .Return, .If, .While, .For:
|
case .Invalid, .Declaration, .Return, .If, .While, .For, .Break, .Continue:
|
||||||
diagnostic := source.add(
|
diagnostic := source.add(
|
||||||
parser.diagnostics,
|
parser.diagnostics,
|
||||||
statement.span,
|
statement.span,
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ Kind :: enum u8 {
|
|||||||
Keyword_If,
|
Keyword_If,
|
||||||
Keyword_While,
|
Keyword_While,
|
||||||
Keyword_For,
|
Keyword_For,
|
||||||
|
Keyword_Break,
|
||||||
|
Keyword_Continue,
|
||||||
Keyword_Else,
|
Keyword_Else,
|
||||||
Keyword_True,
|
Keyword_True,
|
||||||
Keyword_False,
|
Keyword_False,
|
||||||
|
|||||||
@@ -2001,6 +2001,66 @@ control_flow_compiles_and_runs :: proc(t: ^testing.T) {
|
|||||||
testing.expect(t, strings.contains(string(stdout), "or-taken"))
|
testing.expect(t, strings.contains(string(stdout), "or-taken"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
break_and_continue_compile_and_run :: proc(t: ^testing.T) {
|
||||||
|
output := "/tmp/brolang-test-break-continue"
|
||||||
|
defer _ = os.remove(output)
|
||||||
|
status := compiler_core.compile_package("examples/programs/break_continue", output)
|
||||||
|
testing.expect_value(t, status, 0)
|
||||||
|
state := run_executable(output)
|
||||||
|
// `while` break, range-for `continue`, nested innermost-targeting break, a
|
||||||
|
// `continue` on the final element of an inclusive `u8` range (no overflow
|
||||||
|
// trap), and an exitable `while true` together produce 42.
|
||||||
|
testing.expect_value(t, state.exit_code, 42)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
break_and_continue_misuse_is_diagnosed :: proc(t: ^testing.T) {
|
||||||
|
// `break`/`continue` outside any loop are rejected, and a non-void function
|
||||||
|
// that exits a `while true` via `break` without returning is flagged as
|
||||||
|
// missing a return (the `all_paths_return` refinement).
|
||||||
|
text := `main :: func() i32 {
|
||||||
|
bad_break()
|
||||||
|
bad_continue()
|
||||||
|
return missing_return()
|
||||||
|
}
|
||||||
|
bad_break :: func() void {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
bad_continue :: func() void {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
missing_return :: func() i32 {
|
||||||
|
while true {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
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)
|
||||||
|
|
||||||
|
break_outside := false
|
||||||
|
continue_outside := false
|
||||||
|
missing := false
|
||||||
|
for diagnostic in diagnostics.items {
|
||||||
|
break_outside = break_outside || strings.contains(diagnostic.message, "'break' outside of a loop")
|
||||||
|
continue_outside = continue_outside || strings.contains(diagnostic.message, "'continue' outside of a loop")
|
||||||
|
missing = missing || strings.contains(diagnostic.message, "'missing_return' does not return a value")
|
||||||
|
}
|
||||||
|
testing.expect(t, break_outside)
|
||||||
|
testing.expect(t, continue_outside)
|
||||||
|
testing.expect(t, missing)
|
||||||
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
foreign_function_links_from_c_source :: proc(t: ^testing.T) {
|
foreign_function_links_from_c_source :: proc(t: ^testing.T) {
|
||||||
output := "/tmp/brolang-test-foreign-source"
|
output := "/tmp/brolang-test-foreign-source"
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Milestone 18: `break` and `continue`.
|
||||||
|
#
|
||||||
|
# `break` exits the innermost loop; `continue` skips to that loop's next
|
||||||
|
# iteration (running the update / index increment first). Both target the
|
||||||
|
# innermost enclosing loop. Each section returns a distinct code on failure so
|
||||||
|
# a regression points at the broken behaviour; success falls through to 42.
|
||||||
|
|
||||||
|
main :: func() i32 {
|
||||||
|
# 1. `break` out of a `while` once i reaches 5.
|
||||||
|
i i32 = 0
|
||||||
|
a i32 = 0
|
||||||
|
while i < 100 : i += 1 {
|
||||||
|
if (i == 5) break
|
||||||
|
a += 1
|
||||||
|
}
|
||||||
|
if (a != 5) return 101
|
||||||
|
|
||||||
|
# 2. `continue` past n == 3 while summing 0..9 (45 - 3 = 42).
|
||||||
|
b i32 = 0
|
||||||
|
for 0..10 |n| {
|
||||||
|
if (n == 3) continue
|
||||||
|
b = b + n
|
||||||
|
}
|
||||||
|
if (b != 42) return 102
|
||||||
|
|
||||||
|
# 3. Nested loops: the inner `break` exits only the inner loop, so the outer
|
||||||
|
# loop still runs all three iterations (each contributing one y == 0 pass).
|
||||||
|
c i32 = 0
|
||||||
|
for 0..3 |x| {
|
||||||
|
for 0..3 |y| {
|
||||||
|
if (y == 1) break
|
||||||
|
c += 1
|
||||||
|
}
|
||||||
|
_ = x
|
||||||
|
}
|
||||||
|
if (c != 3) return 103
|
||||||
|
|
||||||
|
# 4. `continue` on the final element of an inclusive range bounded by the
|
||||||
|
# element type's maximum must exit cleanly, not overflow the increment.
|
||||||
|
hi u8 :: 255
|
||||||
|
d i32 = 0
|
||||||
|
for 0..=hi |v| {
|
||||||
|
if (v == 255) continue
|
||||||
|
d += 1
|
||||||
|
}
|
||||||
|
if (d != 255) return 104
|
||||||
|
|
||||||
|
# 5. `while true` is exitable via `break` (so it is not an infinite loop and
|
||||||
|
# the code after it is reachable).
|
||||||
|
e i32 = 0
|
||||||
|
while true {
|
||||||
|
e += 1
|
||||||
|
if (e == 7) break
|
||||||
|
}
|
||||||
|
if (e != 7) return 105
|
||||||
|
|
||||||
|
return 42
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user