expand yield to if-statements and loops
This commit is contained in:
@@ -330,7 +330,39 @@
|
|||||||
(`blk: { yield :blk v }`), implicit trailing-expression yield, and yield in match arms /
|
(`blk: { yield :blk v }`), implicit trailing-expression yield, and yield in match arms /
|
||||||
`catch` handlers (milestones 21–22)
|
`catch` handlers (milestones 21–22)
|
||||||
|
|
||||||
20.5 `yield` from if-statements and loops (see below)
|
20.5 `yield` from if-statements and loops (implemented; see below)
|
||||||
|
- an `if`/`for`/`while` on the right of a declaration or assignment is now a *value
|
||||||
|
source*, governed by the rule **if one path yields, all paths must yield** (no
|
||||||
|
optionals-as-a-crutch, so the value is always present and never needs unwrapping):
|
||||||
|
- value-if: `result :: if a { yield 1 } else if b { yield 2 } else { yield 3 }` — a
|
||||||
|
mandatory `else`, every branch ends in `yield`, all branches share a type (the first
|
||||||
|
branch fixes it when untyped; later branches coerce). Typed `T =` and reassignment
|
||||||
|
`target = if …` are supported too
|
||||||
|
- value-loop: a labeled body `for/while … blk: { … }` whose early exits are
|
||||||
|
`yield :blk x` and whose body ends in an unlabeled fall-through `yield` (the value
|
||||||
|
when the loop completes). The `{T, none}` yields resolve the result to `?T`
|
||||||
|
(a pure-AST `none`-scan picks optionality; the first concrete yield fixes the element
|
||||||
|
type). E.g. `active_ent_idx :: for 0..10 |i| blk: { if (cond) yield :blk i; yield none }`
|
||||||
|
resolves to `?usize`
|
||||||
|
- new `blk:` / `yield :blk` label surface adds one `label` field to the AST `Stmt`; no new
|
||||||
|
token (`blk:` is `Identifier Colon`, `:blk` is `Colon Identifier`). The parser carries a
|
||||||
|
value `if`/`for`/`while` as a one-element block-init `body` (the same `expr`-invalid
|
||||||
|
signal a value block uses)
|
||||||
|
- **no HIR/lowering change** (like 18/19/20): a value-if/loop desugars in the checker to a
|
||||||
|
mutable result *slot* (a poison-/fall-through-initialized local) that branches/iterations
|
||||||
|
assign and that is read after the construct — the existing alloca-backed local flow. A
|
||||||
|
`yield :blk x` desugars to `slot = x; break`, reusing the milestone-18 `Break` lowering.
|
||||||
|
Each value-if branch is a `build_value_block` call; the value-loop reuses the ordinary
|
||||||
|
`.For`/`.While` build via a peeled-body copy. HIR never holds a `.Yield`
|
||||||
|
- errors: an `if` value without `else`; a branch/value-block not ending in `yield`; a value
|
||||||
|
loop body without a trailing fall-through `yield`; a `yield :blk` with no matching value
|
||||||
|
loop. The TODO "BAD" loops (unlabeled yield from inside an `if`, an unbound labeled loop)
|
||||||
|
fall out of these naturally
|
||||||
|
- deferred to a later pass (`// ponytail:`): a value-if/loop nested as the *trailing*
|
||||||
|
statement of a value block (first pass is the direct `x :: if …`/`for …` form); a branch
|
||||||
|
that early-`return`s instead of yielding; unwrap-`if` (`if v |x| { yield … }`) as a value
|
||||||
|
source; `yield none` before any concrete yield in an untyped loop (annotate instead);
|
||||||
|
`yield :blk` to an outer (non-innermost) loop
|
||||||
|
|
||||||
21. unions and tagged unions
|
21. unions and tagged unions
|
||||||
|
|
||||||
@@ -608,6 +640,7 @@ Yielding is also possible from loops with the same constraint.
|
|||||||
# get active entity
|
# get active entity
|
||||||
active_ent_idx :: for 0..10 |i| blk: {
|
active_ent_idx :: for 0..10 |i| blk: {
|
||||||
if is_active(some_entity, i) yield :blk i
|
if is_active(some_entity, i) yield :blk i
|
||||||
|
yield none # fall-through: no active ent was found (this should imply a return type matching both the index value and `none`, meaning it should resolve to an optional in this case)
|
||||||
|
|
||||||
# note that in this case, we have to use the `blk` label to yield from the correct scope.
|
# note that in this case, we have to use the `blk` label to yield from the correct scope.
|
||||||
# otherwise, the yield should return directly from the if-statement's scope (which would be incorrect in this case).
|
# otherwise, the yield should return directly from the if-statement's scope (which would be incorrect in this case).
|
||||||
@@ -616,11 +649,13 @@ active_ent_idx :: for 0..10 |i| blk: {
|
|||||||
# BAD: yield returned from if-statement, but no name binds it: should miscompile similar to unused return values from functions.
|
# BAD: yield returned from if-statement, but no name binds it: should miscompile similar to unused return values from functions.
|
||||||
active_ent_idx :: for 0..10 |i| {
|
active_ent_idx :: for 0..10 |i| {
|
||||||
if is_active(some_entity, i) yield i # bad
|
if is_active(some_entity, i) yield i # bad
|
||||||
|
yield none
|
||||||
}
|
}
|
||||||
|
|
||||||
# BAD: likewise for loops
|
# BAD: likewise for loops
|
||||||
for 0..10 |i| blk: { # bad, no name binds returned value
|
for 0..10 |i| blk: { # bad, no name binds returned value
|
||||||
if is_active(some_entity, i) yield :blk i
|
if is_active(some_entity, i) yield :blk i
|
||||||
|
yield none
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,10 @@ Stmt :: struct {
|
|||||||
span: source.Span,
|
span: source.Span,
|
||||||
name: symbol.Id,
|
name: symbol.Id,
|
||||||
index_name: symbol.Id,
|
index_name: symbol.Id,
|
||||||
|
// `For`/`While` loop bodies may carry a label (`blk: { ... }`); a `Yield`
|
||||||
|
// may target one (`yield :blk x`), letting a yield reach past an enclosing
|
||||||
|
// `if` to exit the labeled loop. `INVALID` when absent.
|
||||||
|
label: symbol.Id,
|
||||||
type: Type_Syntax,
|
type: Type_Syntax,
|
||||||
immutable: bool,
|
immutable: bool,
|
||||||
pointer_capture: bool,
|
pointer_capture: bool,
|
||||||
|
|||||||
@@ -57,6 +57,18 @@ Build_Local :: struct {
|
|||||||
// nested control-flow blocks (if/else) can be built recursively. `locals` is a
|
// nested control-flow blocks (if/else) can be built recursively. `locals` is a
|
||||||
// scope stack: each block records its entry length and truncates back to it on
|
// scope stack: each block records its entry length and truncates back to it on
|
||||||
// exit, while `hir_locals` keeps every allocated slot for the function.
|
// exit, while `hir_locals` keeps every allocated slot for the function.
|
||||||
|
// A labeled value-loop currently being built. A `yield :label x` inside the loop
|
||||||
|
// body assigns `x` to the loop's result `slot` (typed `slot_type`) and `break`s.
|
||||||
|
// Pushed by `build_value_loop` while its body is built; innermost is last.
|
||||||
|
Yield_Target :: struct {
|
||||||
|
label: symbol.Id,
|
||||||
|
slot: hir.Local_Id,
|
||||||
|
slot_type: types.Type,
|
||||||
|
// True when the loop also yields `none` (a `{T, none}` set → `?T`); set from a
|
||||||
|
// pure-AST scan, used to pick the slot's element type on the first concrete yield.
|
||||||
|
result_optional: bool,
|
||||||
|
}
|
||||||
|
|
||||||
Build_Ctx :: struct {
|
Build_Ctx :: struct {
|
||||||
checker: ^Checker,
|
checker: ^Checker,
|
||||||
pkg: ast.Package_Id,
|
pkg: ast.Package_Id,
|
||||||
@@ -68,6 +80,8 @@ 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,
|
||||||
|
// Stack of labeled value-loops being built (innermost last); see Yield_Target.
|
||||||
|
yield_targets: ^[dynamic]Yield_Target,
|
||||||
// `defer` lowering. Deferred statements are built once at the `defer` site and
|
// `defer` lowering. Deferred statements are built once at the `defer` site and
|
||||||
// their hir stmt ids stored here as a flat stack across scopes (one entry per
|
// their hir stmt ids stored here as a flat stack across scopes (one entry per
|
||||||
// deferred statement); they are replayed (appended) at each scope exit in LIFO
|
// deferred statement); they are replayed (appended) at each scope exit in LIFO
|
||||||
@@ -4041,7 +4055,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_block(ctx, &body, statement.body, expected, statement.span)
|
value, value_type := build_value_source(ctx, &body, statement.body, expected, statement.span)
|
||||||
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,
|
||||||
@@ -4252,7 +4266,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_block(ctx, &body, statement.body, target_type, statement.span)
|
value, _ = build_value_source(ctx, &body, statement.body, target_type, statement.span)
|
||||||
} 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,
|
||||||
@@ -4272,7 +4286,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_block(ctx, &body, statement.body, types.INVALID, statement.span)
|
value, _ = build_value_source(ctx, &body, statement.body, types.INVALID, statement.span)
|
||||||
} 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)
|
||||||
}
|
}
|
||||||
@@ -4316,7 +4330,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_block(ctx, &body, statement.body, local.type, statement.span)
|
value, _ = build_value_source(ctx, &body, statement.body, local.type, statement.span)
|
||||||
} 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,
|
||||||
@@ -4740,13 +4754,71 @@ build_block :: proc(
|
|||||||
}
|
}
|
||||||
delete(block, checker.allocator)
|
delete(block, checker.allocator)
|
||||||
case .Yield:
|
case .Yield:
|
||||||
// A legitimate yield is peeled off by build_value_block as the value
|
// A labeled `yield :blk x` exits the value-loop labeled `blk`: assign the
|
||||||
// block's final statement; reaching it here means it is misplaced
|
// result slot, then `break` (which flushes defers down to the loop body and
|
||||||
// (nested in an if/loop/inner block, or in a non-value block).
|
// branches to its exit). HIR holds no `.Yield` — it becomes Assignment + Break.
|
||||||
// ponytail: yield from if/loops/labeled blocks is a later milestone.
|
if symbol.is_valid(statement.label) {
|
||||||
|
target_index := -1
|
||||||
|
for i := len(ctx.yield_targets^) - 1; i >= 0; i -= 1 {
|
||||||
|
if ctx.yield_targets^[i].label == statement.label {
|
||||||
|
target_index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if target_index < 0 {
|
||||||
|
id := source.addf(checker.diagnostics, statement.span,
|
||||||
|
"no enclosing value loop is labeled '%s'", symbol_text(checker, statement.label))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// ponytail: a `yield :blk` lowers to milestone-18 `break`, which targets the
|
||||||
|
// innermost loop only, so the label must name the innermost value loop.
|
||||||
|
if target_index != len(ctx.yield_targets^) - 1 {
|
||||||
|
id := source.addf(checker.diagnostics, statement.span,
|
||||||
|
"'yield :%s' must target the innermost loop", symbol_text(checker, statement.label))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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 = 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,
|
||||||
|
"could not determine the value loop's yield type; annotate the binding or yield a concrete value first")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// slot = value (the slot is un-nameable, so no defer can mutate it; no spill).
|
||||||
|
emit_slot_assign(checker, &body, target.slot, yielded, statement.span)
|
||||||
|
// Exit the loop: flush defers down to the loop body, then break.
|
||||||
|
flush_defers(ctx, &body, ctx.loop_defer_starts^[len(ctx.loop_defer_starts^) - 1])
|
||||||
|
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||||
|
append(&checker.module.statements, hir.Stmt{
|
||||||
|
kind = .Break, span = statement.span, expr = hir.INVALID_EXPR,
|
||||||
|
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// An unlabeled yield reaching here is misplaced: a legitimate trailing yield
|
||||||
|
// is peeled by the value builders (value block / if branch / loop fall-through).
|
||||||
id := source.add(
|
id := source.add(
|
||||||
checker.diagnostics, statement.span,
|
checker.diagnostics, statement.span,
|
||||||
"'yield' is only valid as the final statement of a value block",
|
"'yield' is only valid as the final statement of a value block, or as 'yield :label' inside a labeled value loop",
|
||||||
)
|
)
|
||||||
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{
|
||||||
@@ -4887,6 +4959,309 @@ 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:
|
||||||
|
// a `{ ... }` block, an `if` whose branches yield, or a `for`/`while` whose iterations
|
||||||
|
// yield. All three return the produced value and its type for the enclosing binding.
|
||||||
|
build_value_source :: proc(
|
||||||
|
ctx: ^Build_Ctx,
|
||||||
|
body: ^[dynamic]hir.Stmt_Id,
|
||||||
|
body_stmts: []ast.Stmt_Id,
|
||||||
|
expected: types.Type,
|
||||||
|
span: source.Span,
|
||||||
|
) -> (value: hir.Expr_Id, value_type: types.Type) {
|
||||||
|
checker := ctx.checker
|
||||||
|
if len(body_stmts) == 1 {
|
||||||
|
#partial switch checker.ast_module.statements[body_stmts[0]].kind {
|
||||||
|
case .If:
|
||||||
|
return build_value_if(ctx, body, body_stmts[0], expected, span)
|
||||||
|
case .For, .While:
|
||||||
|
return build_value_loop(ctx, body, body_stmts[0], expected, span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return build_value_block(ctx, body, body_stmts, expected, span)
|
||||||
|
}
|
||||||
|
|
||||||
|
// new_value_slot allocates a fresh, un-nameable mutable local to hold a value-if/loop
|
||||||
|
// result. Branches/iterations assign it; the construct's value is a read of it.
|
||||||
|
new_value_slot :: proc(ctx: ^Build_Ctx, slot_type: types.Type) -> hir.Local_Id {
|
||||||
|
slot := hir.local_id(len(ctx.hir_locals^))
|
||||||
|
append(ctx.hir_locals, hir.Local{name = ctx.checker.sink_symbol, type = slot_type, mutable = true})
|
||||||
|
return slot
|
||||||
|
}
|
||||||
|
|
||||||
|
// slot_read builds a `.Local` read of a result slot.
|
||||||
|
slot_read :: proc(checker: ^Checker, slot: hir.Local_Id, slot_type: types.Type, span: source.Span) -> hir.Expr_Id {
|
||||||
|
id := hir.expr_id(len(checker.module.exprs))
|
||||||
|
append(&checker.module.exprs, hir.Expr{
|
||||||
|
kind = .Local, span = span, type = slot_type, target = hir.local_ref(slot),
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit_slot_assign appends a bare-local `slot = value` assignment to `out`.
|
||||||
|
emit_slot_assign :: proc(checker: ^Checker, out: ^[dynamic]hir.Stmt_Id, slot: hir.Local_Id, value: hir.Expr_Id, span: source.Span) {
|
||||||
|
append(out, hir.stmt_id(len(checker.module.statements)))
|
||||||
|
append(&checker.module.statements, hir.Stmt{
|
||||||
|
kind = .Assignment, span = span, expr = value, local = slot,
|
||||||
|
target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// when untyped; later branches coerce). HIR holds an ordinary `.If` + a `.Local` read.
|
||||||
|
build_value_if :: proc(
|
||||||
|
ctx: ^Build_Ctx,
|
||||||
|
body: ^[dynamic]hir.Stmt_Id,
|
||||||
|
if_id: ast.Stmt_Id,
|
||||||
|
expected: types.Type,
|
||||||
|
span: source.Span,
|
||||||
|
) -> (value: hir.Expr_Id, value_type: types.Type) {
|
||||||
|
checker := ctx.checker
|
||||||
|
slot := hir.INVALID_LOCAL
|
||||||
|
slot_type := types.INVALID
|
||||||
|
if is_runtime_type(checker, expected) {
|
||||||
|
slot_type = expected
|
||||||
|
slot = new_value_slot(ctx, slot_type)
|
||||||
|
}
|
||||||
|
subtree: [dynamic]hir.Stmt_Id
|
||||||
|
subtree.allocator = checker.allocator
|
||||||
|
ok := emit_value_if(ctx, &subtree, if_id, &slot, &slot_type, span)
|
||||||
|
if !ok || slot == hir.INVALID_LOCAL {
|
||||||
|
for s in subtree {
|
||||||
|
append(body, s)
|
||||||
|
}
|
||||||
|
delete(subtree)
|
||||||
|
ctx.problematic^ = true
|
||||||
|
return invalid_hir_expr(checker, span, source.INVALID_DIAGNOSTIC), types.INVALID
|
||||||
|
}
|
||||||
|
// The slot's poison declaration precedes the if; every path assigns it.
|
||||||
|
append(body, hir.stmt_id(len(checker.module.statements)))
|
||||||
|
append(&checker.module.statements, hir.Stmt{
|
||||||
|
kind = .Declaration, span = span, local = slot, expr = hir.INVALID_EXPR,
|
||||||
|
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
for s in subtree {
|
||||||
|
append(body, s)
|
||||||
|
}
|
||||||
|
delete(subtree)
|
||||||
|
value = slot_read(checker, slot, slot_type, span)
|
||||||
|
return value, slot_type
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit_value_if builds one `if`/`else if`/`else` level of a value-if, appending the
|
||||||
|
// assembled `.If` to `out`. `slot`/`slot_type` thread through so the first branch can
|
||||||
|
// fix an untyped slot and `else if` chains share it.
|
||||||
|
emit_value_if :: proc(
|
||||||
|
ctx: ^Build_Ctx,
|
||||||
|
out: ^[dynamic]hir.Stmt_Id,
|
||||||
|
if_id: ast.Stmt_Id,
|
||||||
|
slot: ^hir.Local_Id,
|
||||||
|
slot_type: ^types.Type,
|
||||||
|
span: source.Span,
|
||||||
|
) -> bool {
|
||||||
|
checker := ctx.checker
|
||||||
|
if_stmt := checker.ast_module.statements[if_id]
|
||||||
|
// ponytail: an unwrap `if` (captures) as a value source is a later milestone.
|
||||||
|
if len(if_stmt.captures) > 0 {
|
||||||
|
source.add(checker.diagnostics, if_stmt.span, "an unwrap 'if' cannot yet be used as a value")
|
||||||
|
ctx.problematic^ = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
condition := build_expr(checker, if_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file)
|
||||||
|
if checker.module.exprs[condition].kind != .Invalid && !types.is_bool(checker.module.exprs[condition].type) {
|
||||||
|
id := source.add(checker.diagnostics, if_stmt.span, "'if' condition must be a bool")
|
||||||
|
condition = invalid_hir_expr(checker, if_stmt.span, id, types.BOOL)
|
||||||
|
ctx.problematic^ = true
|
||||||
|
}
|
||||||
|
then_body: [dynamic]hir.Stmt_Id
|
||||||
|
then_body.allocator = checker.allocator
|
||||||
|
if !emit_value_branch(ctx, &then_body, if_stmt.body, slot, slot_type, span) {
|
||||||
|
delete(then_body)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if if_stmt.else_body == nil {
|
||||||
|
source.add(checker.diagnostics, if_stmt.span, "an 'if' used as a value must have an 'else' so every path yields")
|
||||||
|
delete(then_body)
|
||||||
|
ctx.problematic^ = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
else_body: [dynamic]hir.Stmt_Id
|
||||||
|
else_body.allocator = checker.allocator
|
||||||
|
branch_ok := true
|
||||||
|
if len(if_stmt.else_body) == 1 && checker.ast_module.statements[if_stmt.else_body[0]].kind == .If {
|
||||||
|
branch_ok = emit_value_if(ctx, &else_body, if_stmt.else_body[0], slot, slot_type, span)
|
||||||
|
} else {
|
||||||
|
branch_ok = emit_value_branch(ctx, &else_body, if_stmt.else_body, slot, slot_type, span)
|
||||||
|
}
|
||||||
|
if !branch_ok {
|
||||||
|
delete(then_body)
|
||||||
|
delete(else_body)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
append(out, hir.stmt_id(len(checker.module.statements)))
|
||||||
|
append(&checker.module.statements, hir.Stmt{
|
||||||
|
kind = .If, span = if_stmt.span, expr = condition, guard = hir.INVALID_EXPR,
|
||||||
|
then_body = then_body[:], else_body = else_body[:],
|
||||||
|
local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit_value_branch builds one branch of a value-if as a value block (leading stmts +
|
||||||
|
// trailing yield) and appends `slot = <value>`. The first branch of an untyped value-if
|
||||||
|
// fixes the slot type; later branches coerce to it (a mismatch is the "same type" error).
|
||||||
|
emit_value_branch :: proc(
|
||||||
|
ctx: ^Build_Ctx,
|
||||||
|
out: ^[dynamic]hir.Stmt_Id,
|
||||||
|
branch_stmts: []ast.Stmt_Id,
|
||||||
|
slot: ^hir.Local_Id,
|
||||||
|
slot_type: ^types.Type,
|
||||||
|
span: source.Span,
|
||||||
|
) -> bool {
|
||||||
|
checker := ctx.checker
|
||||||
|
value, vtype := build_value_block(ctx, out, branch_stmts, slot_type^, span)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
emit_slot_assign(checker, out, slot^, value, span)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop_yields_none reports whether any `yield` that targets this loop (a labeled
|
||||||
|
// `yield :blk` inside `if`/block branches, or the trailing fall-through) yields the
|
||||||
|
// literal `none` — making the loop's result optional. Pure AST walk; does not descend
|
||||||
|
// into nested loops or value sources, whose yields belong to them.
|
||||||
|
loop_yields_none :: proc(checker: ^Checker, stmts: []ast.Stmt_Id) -> bool {
|
||||||
|
for id in stmts {
|
||||||
|
s := checker.ast_module.statements[id]
|
||||||
|
#partial switch s.kind {
|
||||||
|
case .Yield:
|
||||||
|
if s.expr != ast.INVALID_EXPR && checker.ast_module.exprs[s.expr].kind == .None {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case .If:
|
||||||
|
if loop_yields_none(checker, s.body) || loop_yields_none(checker, s.else_body) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case .Block:
|
||||||
|
if loop_yields_none(checker, s.body) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve_loop_slot fixes a value-loop's result slot from its first concrete yield (an
|
||||||
|
// optional element type when the loop also yields `none`) and coerces `value` into it.
|
||||||
|
// Returns INVALID when the type can't be fixed yet (a `none`/invalid first yield).
|
||||||
|
resolve_loop_slot :: proc(ctx: ^Build_Ctx, target: ^Yield_Target, value: hir.Expr_Id, vtype: types.Type, span: source.Span) -> hir.Expr_Id {
|
||||||
|
checker := ctx.checker
|
||||||
|
if target.slot == hir.INVALID_LOCAL {
|
||||||
|
if !is_runtime_type(checker, vtype) {
|
||||||
|
return hir.INVALID_EXPR
|
||||||
|
}
|
||||||
|
target.slot_type = types.optional(&checker.module.types, vtype) if target.result_optional else vtype
|
||||||
|
target.slot = new_value_slot(ctx, target.slot_type)
|
||||||
|
}
|
||||||
|
return coerce_expr(checker, value, target.slot_type, span)
|
||||||
|
}
|
||||||
|
|
||||||
|
// build_value_loop turns a labeled `for/while ... blk: { … }` whose body ends in a
|
||||||
|
// fall-through `yield` (and may early-exit via `yield :blk x`) into a result slot:
|
||||||
|
// the fall-through value initializes the slot before the loop, each `yield :blk x`
|
||||||
|
// desugars (in build_block) to `slot = x; break`, and the construct's value is a read
|
||||||
|
// of the slot after the loop. Reuses the ordinary `.For`/`.While` build via a peeled
|
||||||
|
// copy; no new HIR. The yielded type is the annotation when typed, else the first
|
||||||
|
// concrete yield's type (optional when any yield is `none`).
|
||||||
|
build_value_loop :: proc(
|
||||||
|
ctx: ^Build_Ctx,
|
||||||
|
body: ^[dynamic]hir.Stmt_Id,
|
||||||
|
loop_id: ast.Stmt_Id,
|
||||||
|
expected: types.Type,
|
||||||
|
span: source.Span,
|
||||||
|
) -> (value: hir.Expr_Id, value_type: types.Type) {
|
||||||
|
checker := ctx.checker
|
||||||
|
loop_stmt := checker.ast_module.statements[loop_id]
|
||||||
|
n := len(loop_stmt.body)
|
||||||
|
last_is_fallthrough := n > 0 &&
|
||||||
|
checker.ast_module.statements[loop_stmt.body[n - 1]].kind == .Yield &&
|
||||||
|
!symbol.is_valid(checker.ast_module.statements[loop_stmt.body[n - 1]].label)
|
||||||
|
if !symbol.is_valid(loop_stmt.label) {
|
||||||
|
id := source.add(checker.diagnostics, span,
|
||||||
|
"a value loop must label its body (e.g. 'blk:') so a 'yield :blk' can exit it")
|
||||||
|
ctx.problematic^ = true
|
||||||
|
return invalid_hir_expr(checker, span, id), types.INVALID
|
||||||
|
}
|
||||||
|
if !last_is_fallthrough {
|
||||||
|
id := source.add(checker.diagnostics, span,
|
||||||
|
"a value loop's body must end with a 'yield' for when the loop completes")
|
||||||
|
ctx.problematic^ = true
|
||||||
|
return invalid_hir_expr(checker, span, id), types.INVALID
|
||||||
|
}
|
||||||
|
fall_stmt := checker.ast_module.statements[loop_stmt.body[n - 1]]
|
||||||
|
|
||||||
|
result_optional := loop_yields_none(checker, loop_stmt.body)
|
||||||
|
slot := hir.INVALID_LOCAL
|
||||||
|
slot_type := types.INVALID
|
||||||
|
if is_runtime_type(checker, expected) {
|
||||||
|
slot_type = expected
|
||||||
|
slot = new_value_slot(ctx, slot_type)
|
||||||
|
result_optional = types.is_optional(slot_type, &checker.module.types)
|
||||||
|
}
|
||||||
|
append(ctx.yield_targets, Yield_Target{
|
||||||
|
label = loop_stmt.label, slot = slot, slot_type = slot_type, result_optional = result_optional,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Build the loop with the fall-through peeled off, reusing the normal For/While arm.
|
||||||
|
// The peeled body is a fresh copy so destroy_module won't double-free the original.
|
||||||
|
peeled := loop_stmt
|
||||||
|
peeled_body := make([]ast.Stmt_Id, n - 1, checker.ast_module.allocator)
|
||||||
|
copy(peeled_body, loop_stmt.body[:n - 1])
|
||||||
|
peeled.body = peeled_body
|
||||||
|
peeled_id := ast.stmt_id(len(checker.ast_module.statements))
|
||||||
|
append(&checker.ast_module.statements, peeled)
|
||||||
|
loop_block := build_block(ctx, []ast.Stmt_Id{peeled_id})
|
||||||
|
|
||||||
|
target := pop(ctx.yield_targets)
|
||||||
|
|
||||||
|
// The fall-through value initializes the slot before the loop (loop captures are
|
||||||
|
// out of scope here), so the loop completing leaves it as the result.
|
||||||
|
fall_value := build_expr(checker, fall_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, target.slot_type, ctx.pkg, ctx.file)
|
||||||
|
fall_value = resolve_loop_slot(ctx, &target, fall_value, checker.module.exprs[fall_value].type if fall_value != hir.INVALID_EXPR else types.INVALID, fall_stmt.span)
|
||||||
|
if target.slot == hir.INVALID_LOCAL || fall_value == hir.INVALID_EXPR {
|
||||||
|
for s in loop_block {
|
||||||
|
append(body, s)
|
||||||
|
}
|
||||||
|
delete(loop_block, checker.allocator)
|
||||||
|
id := source.add(checker.diagnostics, span,
|
||||||
|
"could not determine the value loop's yield type; annotate the binding")
|
||||||
|
ctx.problematic^ = true
|
||||||
|
return invalid_hir_expr(checker, span, id), types.INVALID
|
||||||
|
}
|
||||||
|
|
||||||
|
append(body, hir.stmt_id(len(checker.module.statements)))
|
||||||
|
append(&checker.module.statements, hir.Stmt{
|
||||||
|
kind = .Declaration, span = span, local = target.slot, expr = fall_value,
|
||||||
|
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
for s in loop_block {
|
||||||
|
append(body, s)
|
||||||
|
}
|
||||||
|
delete(loop_block, checker.allocator)
|
||||||
|
|
||||||
|
value = slot_read(checker, target.slot, target.slot_type, span)
|
||||||
|
return value, target.slot_type
|
||||||
|
}
|
||||||
|
|
||||||
// Reports whether every control-flow path through `stmts` terminates (returns or traps),
|
// Reports whether every control-flow path through `stmts` terminates (returns or traps),
|
||||||
// so the end of the block is unreachable. A `.Return` or `.Trap` terminates outright; an
|
// so the end of the block is unreachable. A `.Return` or `.Trap` terminates outright; an
|
||||||
// `.If` terminates only when it has an `else` and both arms terminate. A literal
|
// `.If` terminates only when it has an `else` and both arms terminate. A literal
|
||||||
@@ -5069,6 +5444,8 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
|||||||
defers.allocator = checker.allocator
|
defers.allocator = checker.allocator
|
||||||
loop_defer_starts: [dynamic]int
|
loop_defer_starts: [dynamic]int
|
||||||
loop_defer_starts.allocator = checker.allocator
|
loop_defer_starts.allocator = checker.allocator
|
||||||
|
yield_targets: [dynamic]Yield_Target
|
||||||
|
yield_targets.allocator = checker.allocator
|
||||||
ctx := Build_Ctx{
|
ctx := Build_Ctx{
|
||||||
checker = checker,
|
checker = checker,
|
||||||
pkg = function.pkg,
|
pkg = function.pkg,
|
||||||
@@ -5082,6 +5459,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
|||||||
problematic = &problematic,
|
problematic = &problematic,
|
||||||
defers = &defers,
|
defers = &defers,
|
||||||
loop_defer_starts = &loop_defer_starts,
|
loop_defer_starts = &loop_defer_starts,
|
||||||
|
yield_targets = &yield_targets,
|
||||||
}
|
}
|
||||||
block := build_block(&ctx, function.body)
|
block := build_block(&ctx, function.body)
|
||||||
returns := all_paths_return(&checker.module, block)
|
returns := all_paths_return(&checker.module, block)
|
||||||
@@ -5131,6 +5509,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
|||||||
}
|
}
|
||||||
delete(defers)
|
delete(defers)
|
||||||
delete(loop_defer_starts)
|
delete(loop_defer_starts)
|
||||||
|
delete(yield_targets)
|
||||||
delete(locals)
|
delete(locals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,23 @@ previous :: proc(parser: ^Parser) -> token.Token {
|
|||||||
return parser.tokens.items[max(parser.cursor-1, 0)]
|
return parser.tokens.items[max(parser.cursor-1, 0)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peek :: proc(parser: ^Parser) -> token.Token {
|
||||||
|
return parser.tokens.items[min(parser.cursor+1, len(parser.tokens.items)-1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// A loop body may be labeled `blk: { ... }` so a nested `yield :blk x` can exit
|
||||||
|
// it past an enclosing `if`. Consumes and returns the label when the next tokens
|
||||||
|
// are `Identifier Colon`; otherwise leaves the cursor untouched.
|
||||||
|
parse_optional_loop_label :: proc(parser: ^Parser) -> symbol.Id {
|
||||||
|
if current(parser).kind == .Identifier && peek(parser).kind == .Colon {
|
||||||
|
name := advance(parser) // the label name
|
||||||
|
advance(parser) // consume ':'
|
||||||
|
skip_newlines(parser)
|
||||||
|
return name.symbol
|
||||||
|
}
|
||||||
|
return symbol.INVALID
|
||||||
|
}
|
||||||
|
|
||||||
advance :: proc(parser: ^Parser) -> token.Token {
|
advance :: proc(parser: ^Parser) -> token.Token {
|
||||||
result := current(parser)
|
result := current(parser)
|
||||||
if result.kind != .Eof {
|
if result.kind != .Eof {
|
||||||
@@ -992,11 +1009,23 @@ parse_return :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
parse_yield :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
parse_yield :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
||||||
start := advance(parser) // consume 'yield'
|
start := advance(parser) // consume 'yield'
|
||||||
skip_newlines(parser)
|
skip_newlines(parser)
|
||||||
|
// `yield :blk x` targets the loop labeled `blk`; a bare `yield x` targets
|
||||||
|
// the directly-enclosing value block / if branch. No expression starts with
|
||||||
|
// ':', so a leading colon is unambiguously a label.
|
||||||
|
label := symbol.INVALID
|
||||||
|
if _, ok := allow(parser, .Colon); ok {
|
||||||
|
if name, name_ok := allow(parser, .Identifier); name_ok {
|
||||||
|
label = name.symbol
|
||||||
|
} else {
|
||||||
|
source.add(parser.diagnostics, current(parser).span, "expected a loop label after ':'")
|
||||||
|
}
|
||||||
|
}
|
||||||
expr := parse_expression(parser)
|
expr := parse_expression(parser)
|
||||||
id := ast.stmt_id(len(parser.module.statements))
|
id := ast.stmt_id(len(parser.module.statements))
|
||||||
append(&parser.module.statements, ast.Stmt{
|
append(&parser.module.statements, ast.Stmt{
|
||||||
kind=.Yield,
|
kind=.Yield,
|
||||||
span=span_from(start.span, parser.module.exprs[expr].span),
|
span=span_from(start.span, parser.module.exprs[expr].span),
|
||||||
|
label=label,
|
||||||
expr=expr,
|
expr=expr,
|
||||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
@@ -1078,6 +1107,19 @@ starts_declared_type :: proc(parser: ^Parser) -> bool {
|
|||||||
return cursor < len(parser.tokens.items) && is_type_token(parser.tokens.items[cursor].kind)
|
return cursor < len(parser.tokens.items) && is_type_token(parser.tokens.items[cursor].kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A declaration/assignment RHS may be a value-producing control-flow construct:
|
||||||
|
// an `if`/`for`/`while` whose branches/iterations `yield`. Returns the parsed
|
||||||
|
// statement (to be carried as a one-element block-init `body`) and true when the
|
||||||
|
// current token opens one.
|
||||||
|
parse_value_control_flow :: proc(parser: ^Parser) -> (ast.Stmt_Id, bool) {
|
||||||
|
#partial switch current(parser).kind {
|
||||||
|
case .Keyword_If: return parse_if(parser), true
|
||||||
|
case .Keyword_For: return parse_for(parser), true
|
||||||
|
case .Keyword_While: return parse_while(parser), true
|
||||||
|
}
|
||||||
|
return ast.INVALID_STMT, false
|
||||||
|
}
|
||||||
|
|
||||||
parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
||||||
if current(parser).kind == .Keyword_Return {
|
if current(parser).kind == .Keyword_Return {
|
||||||
return parse_return(parser)
|
return parse_return(parser)
|
||||||
@@ -1146,6 +1188,25 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
})
|
})
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
// A value-producing `if`/`for`/`while`: carried as a one-element block-init
|
||||||
|
// body, the same signal a value block uses (`expr` invalid).
|
||||||
|
if cf, is_cf := parse_value_control_flow(parser); is_cf {
|
||||||
|
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=kind,
|
||||||
|
span=span_from(name.span, previous(parser).span),
|
||||||
|
name=name.symbol,
|
||||||
|
type=type_syntax,
|
||||||
|
immutable=immutable,
|
||||||
|
target=ast.INVALID_EXPR,
|
||||||
|
expr=ast.INVALID_EXPR,
|
||||||
|
body=body,
|
||||||
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}
|
||||||
expr := parse_expression(parser)
|
expr := parse_expression(parser)
|
||||||
id := ast.stmt_id(len(parser.module.statements))
|
id := ast.stmt_id(len(parser.module.statements))
|
||||||
append(&parser.module.statements, ast.Stmt{
|
append(&parser.module.statements, ast.Stmt{
|
||||||
@@ -1181,6 +1242,21 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
})
|
})
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
// A value-producing `if`/`for`/`while` assigned to a complex target.
|
||||||
|
if cf, is_cf := parse_value_control_flow(parser); is_cf {
|
||||||
|
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=.Assignment,
|
||||||
|
span=span_from(parser.module.exprs[expr].span, previous(parser).span),
|
||||||
|
target=expr,
|
||||||
|
expr=ast.INVALID_EXPR,
|
||||||
|
body=body,
|
||||||
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
|
})
|
||||||
|
return id
|
||||||
|
}
|
||||||
value := parse_expression(parser)
|
value := parse_expression(parser)
|
||||||
id := ast.stmt_id(len(parser.module.statements))
|
id := ast.stmt_id(len(parser.module.statements))
|
||||||
append(&parser.module.statements, ast.Stmt{
|
append(&parser.module.statements, ast.Stmt{
|
||||||
@@ -1494,6 +1570,7 @@ parse_for :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
skip_newlines(parser)
|
skip_newlines(parser)
|
||||||
|
label := parse_optional_loop_label(parser)
|
||||||
body := parse_block(parser)
|
body := parse_block(parser)
|
||||||
|
|
||||||
id := ast.stmt_id(len(parser.module.statements))
|
id := ast.stmt_id(len(parser.module.statements))
|
||||||
@@ -1502,6 +1579,7 @@ parse_for :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
span=span_from(start.span, previous(parser).span),
|
span=span_from(start.span, previous(parser).span),
|
||||||
name=item_name,
|
name=item_name,
|
||||||
index_name=index_name,
|
index_name=index_name,
|
||||||
|
label=label,
|
||||||
pointer_capture=pointer_capture,
|
pointer_capture=pointer_capture,
|
||||||
expr=iterable,
|
expr=iterable,
|
||||||
body=body,
|
body=body,
|
||||||
@@ -1526,6 +1604,7 @@ parse_while :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
update = parse_while_update(parser)
|
update = parse_while_update(parser)
|
||||||
skip_newlines(parser)
|
skip_newlines(parser)
|
||||||
}
|
}
|
||||||
|
label := parse_optional_loop_label(parser)
|
||||||
body := parse_block(parser)
|
body := parse_block(parser)
|
||||||
|
|
||||||
id := ast.stmt_id(len(parser.module.statements))
|
id := ast.stmt_id(len(parser.module.statements))
|
||||||
@@ -1534,6 +1613,7 @@ parse_while :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
|||||||
span=span_from(start.span, previous(parser).span),
|
span=span_from(start.span, previous(parser).span),
|
||||||
expr=condition,
|
expr=condition,
|
||||||
body=body,
|
body=body,
|
||||||
|
label=label,
|
||||||
update=update,
|
update=update,
|
||||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||||
})
|
})
|
||||||
|
|||||||
+55
-2
@@ -2131,8 +2131,10 @@ yield_compiles_and_runs :: proc(t: ^testing.T) {
|
|||||||
status := compiler_core.compile_package("examples/programs/yield", output)
|
status := compiler_core.compile_package("examples/programs/yield", output)
|
||||||
testing.expect_value(t, status, 0)
|
testing.expect_value(t, status, 0)
|
||||||
state := run_executable(output)
|
state := run_executable(output)
|
||||||
// Untyped/typed value blocks, the value captured before block defers run, and
|
// Value blocks (untyped/typed, defer-spill, reassignment) plus value if-statements
|
||||||
// reassignment from a value block together produce 42.
|
// (untyped/typed/reassign/else-if/defer) and value loops (a labeled `for` search
|
||||||
|
// yielding `?usize` on both the found and not-found paths, and a labeled `while`)
|
||||||
|
// together produce 42.
|
||||||
testing.expect_value(t, state.exit_code, 42)
|
testing.expect_value(t, state.exit_code, 42)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2175,6 +2177,57 @@ yield_misuse_is_diagnosed :: proc(t: ^testing.T) {
|
|||||||
testing.expect(t, misplaced_yield)
|
testing.expect(t, misplaced_yield)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
yield_control_flow_is_diagnosed :: proc(t: ^testing.T) {
|
||||||
|
// Value if/loop misuse (milestone 20.5): an `if` value without an `else`; a
|
||||||
|
// branch that does not end in `yield`; a value loop whose body lacks a trailing
|
||||||
|
// fall-through `yield`; and a `yield :label` with no matching value loop.
|
||||||
|
text := `main :: func() i32 {
|
||||||
|
noelse :: if (true) {
|
||||||
|
yield 1
|
||||||
|
}
|
||||||
|
badbranch :: if (true) {
|
||||||
|
k :: 5
|
||||||
|
} else {
|
||||||
|
yield 2
|
||||||
|
}
|
||||||
|
noloopyield :: for 0..10 |i| blk: {
|
||||||
|
if (i == 0) yield :blk i
|
||||||
|
}
|
||||||
|
for 0..10 |j| stray: {
|
||||||
|
yield :stray j
|
||||||
|
}
|
||||||
|
return noelse + badbranch + noloopyield
|
||||||
|
}
|
||||||
|
`
|
||||||
|
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)
|
||||||
|
|
||||||
|
no_else := false
|
||||||
|
branch_no_yield := false
|
||||||
|
loop_no_yield := false
|
||||||
|
stray_label := false
|
||||||
|
for diagnostic in diagnostics.items {
|
||||||
|
no_else = no_else || strings.contains(diagnostic.message, "an 'if' used as a value must have an 'else'")
|
||||||
|
branch_no_yield = branch_no_yield || strings.contains(diagnostic.message, "a value block must end with an explicit 'yield'")
|
||||||
|
loop_no_yield = loop_no_yield || strings.contains(diagnostic.message, "a value loop's body must end with a 'yield'")
|
||||||
|
stray_label = stray_label || strings.contains(diagnostic.message, "no enclosing value loop is labeled 'stray'")
|
||||||
|
}
|
||||||
|
testing.expect(t, no_else)
|
||||||
|
testing.expect(t, branch_no_yield)
|
||||||
|
testing.expect(t, loop_no_yield)
|
||||||
|
testing.expect(t, stray_label)
|
||||||
|
}
|
||||||
|
|
||||||
@(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"
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
# Milestone 20: `yield` and value blocks.
|
# Milestones 20 / 20.5: `yield`, value blocks, and value if/loops.
|
||||||
#
|
#
|
||||||
# A `{ ... }` on the right of a declaration or assignment is a value block: its
|
# A `{ ... }`, an `if`, or a labeled `for`/`while` on the right of a declaration
|
||||||
# final `yield <expr>` supplies the value (the block analogue of `return`). The
|
# or assignment is a *value source*: `yield <expr>` supplies the value (the block
|
||||||
# yielded value is captured before the block's defers run. Each section returns a
|
# analogue of `return`). Every path must yield. Each section returns a distinct
|
||||||
# distinct code on failure; success falls through to 42.
|
# code on failure; success falls through to 42.
|
||||||
|
|
||||||
|
# --- value blocks (milestone 20) ---------------------------------------------
|
||||||
|
|
||||||
# Untyped `::`: the local's type is the yield's natural type.
|
# Untyped `::`: the local's type is the yield's natural type.
|
||||||
basic :: func() i32 {
|
basic :: func() i32 {
|
||||||
@@ -43,10 +45,109 @@ reassign :: func() i32 {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- value if-statements (milestone 20.5) ------------------------------------
|
||||||
|
|
||||||
|
# Untyped `::` over an `else if` chain; the first branch fixes the type.
|
||||||
|
vif_untyped :: func(sel i32) i32 {
|
||||||
|
r :: if (sel == 0) {
|
||||||
|
yield 10
|
||||||
|
} else if (sel == 1) {
|
||||||
|
yield 20
|
||||||
|
} else {
|
||||||
|
yield 30
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
# Typed `T =`: every branch coerces to the annotation.
|
||||||
|
vif_typed :: func(sel i32) i32 {
|
||||||
|
r i32 = if (sel == 0) { yield 100 } else { yield 200 }
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
# Assigned into an existing local.
|
||||||
|
vif_reassign :: func(sel i32) i32 {
|
||||||
|
r i32 = 0
|
||||||
|
r = if (sel == 0) { yield 7 } else { yield 9 }
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
# A branch is a full value block: leading statements + a defer captured before
|
||||||
|
# the yield.
|
||||||
|
vif_defer :: func() i32 {
|
||||||
|
r :: if (true) {
|
||||||
|
n i32 = 5
|
||||||
|
defer n = 999
|
||||||
|
yield n
|
||||||
|
} else {
|
||||||
|
yield 0
|
||||||
|
}
|
||||||
|
return r # 5
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- value loops (milestone 20.5) --------------------------------------------
|
||||||
|
|
||||||
|
# Labeled `for` used as a value: `yield :blk i` exits early with a value, the
|
||||||
|
# trailing `yield none` supplies the value when the loop completes. The `{i,
|
||||||
|
# none}` yields resolve the result to an optional.
|
||||||
|
loop_search :: func() i32 {
|
||||||
|
# first i in 0..10 whose square exceeds 40 (6*6=36 no, 7*7=49 yes -> 7).
|
||||||
|
idx :: for 0..10 |i| blk: {
|
||||||
|
if (i * i > 40) yield :blk i
|
||||||
|
yield none
|
||||||
|
}
|
||||||
|
if idx |found| {
|
||||||
|
if (found == 7) return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Same loop, but nothing matches -> the fall-through `yield none` is the result.
|
||||||
|
loop_none :: func() i32 {
|
||||||
|
idx :: for 0..10 |i| blk: {
|
||||||
|
if (i > 100) yield :blk i
|
||||||
|
yield none
|
||||||
|
}
|
||||||
|
if idx |found| {
|
||||||
|
_ = found
|
||||||
|
return 1 # should be unreachable: no match
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Labeled `while` value loop (label follows the `: update` clause).
|
||||||
|
loop_while :: func() i32 {
|
||||||
|
n i32 = 0
|
||||||
|
found :: while n < 100 : n += 1 blk: {
|
||||||
|
if (n == 8) yield :blk n
|
||||||
|
yield none
|
||||||
|
}
|
||||||
|
if found |v| {
|
||||||
|
if (v == 8) return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
main :: func() i32 {
|
main :: func() i32 {
|
||||||
if (basic() != 42) return 101
|
if (basic() != 42) return 101
|
||||||
if (typed() != 100) return 102
|
if (typed() != 100) return 102
|
||||||
if (spill() != 5) return 103
|
if (spill() != 5) return 103
|
||||||
if (reassign() != 7) return 104
|
if (reassign() != 7) return 104
|
||||||
|
|
||||||
|
if (vif_untyped(0) != 10) return 105
|
||||||
|
if (vif_untyped(1) != 20) return 106
|
||||||
|
if (vif_untyped(2) != 30) return 107
|
||||||
|
if (vif_typed(0) != 100) return 108
|
||||||
|
if (vif_typed(1) != 200) return 109
|
||||||
|
if (vif_reassign(0) != 7) return 110
|
||||||
|
if (vif_reassign(9) != 9) return 111
|
||||||
|
if (vif_defer() != 5) return 112
|
||||||
|
|
||||||
|
if (loop_search() != 0) return 113
|
||||||
|
if (loop_none() != 0) return 114
|
||||||
|
if (loop_while() != 0) return 115
|
||||||
|
|
||||||
return 42
|
return 42
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user