value if/loop follow-ups

This commit is contained in:
2026-06-27 23:24:36 +02:00
parent 61293a23e7
commit 3e54c6f9ad
4 changed files with 282 additions and 26 deletions
+33 -6
View File
@@ -358,17 +358,44 @@
loop body without a trailing fall-through `yield`; a `yield :blk` with no matching 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) loop. The TODO "BAD" loops (unlabeled yield from inside an `if`, an unbound labeled loop)
fall out of these naturally fall out of these naturally
- deferred to a later pass (`// ponytail:`): a value-if/loop nested as the *trailing* - follow-ups: a branch that early-`return`s instead of yielding, unwrap-`if` as a value
statement of a value block (first pass is the direct `x :: if …`/`for …` form); a branch source, and `none`-before-concrete typing in untyped loops are done in 20.6; labeled value
that early-`return`s instead of yielding; unwrap-`if` (`if v |x| { yield … }`) as a value blocks and `yield`/`break` to an outer loop are 20.7 (need labels in the IR)
source; `yield none` before any concrete yield in an untyped loop (annotate instead);
`yield :blk` to an outer (non-innermost) loop 20.6 value if/loop follow-ups (implemented; checker-only)
- a value-if branch may end in `yield` **or** exit on every path (`return`/`break`/
`continue`) — `r :: if (ok) { yield x } else { return -1 }`. A non-terminating, non-yielding
branch is rejected ("a value branch must end with 'yield' or exit on every path"). Checked
via `all_paths_exit` on the built branch in `emit_value_branch`
- unwrap-`if` as a value source: `name :: if opt |v| { yield v * 2 } else { yield d }`.
`emit_value_if` gained an unwrap path mirroring the build-pass `.If` unwrap arm (captures +
guard), each branch assigning the slot; the HIR `.If` carries the unwraps, which the existing
lowering already handles. (The simple "unwrap or fallback" case is just `orelse` —
`name :: opt orelse d` — already a plain expression.)
- untyped value loops pre-type their element from the first concrete (non-`none`) yield
regardless of source order (a capture-scoped probe build, `value_loop_element_type`), so a
`none` yielded before any concrete value still resolves the result to `?T`
- still checker-only; no HIR/lowering change
20.7 labeled value blocks + `yield`/`break` to an outer loop (introduces labels in the IR)
- `x :: blk: { …; yield :blk v }` — a labeled value *block* (the disambiguated form of "an
if/loop at the end of a block"; an unlabeled trailing if/loop stays ambiguous and is not a
value source). `yield :blk v` exits the block with a value
- `yield :outer v` / labeled `break` to a non-innermost loop
- both need exit targets to carry a label: add `label` to HIR `.While`/`.For`/`.Break` and a
labeled `.Block`; generalize the lowering's `Loop_Ctx`/`State.loops` into an exit-target
stack keyed by label (plain `break`/`continue` stay innermost-only; a labeled `.Break`
searches by label; a labeled block pushes a non-loop target + an exit label after its body).
First lowering change in the `yield` line
21. unions and tagged unions 21. unions and tagged unions
22. match statements with tagged unions payload unwrapping 22. match statements with tagged unions payload unwrapping
23. dynamic heap allocation 23. error types
- brolang should feature errors as values
24. 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
+181 -5
View File
@@ -5063,27 +5063,102 @@ emit_value_if :: proc(
) -> bool { ) -> bool {
checker := ctx.checker checker := ctx.checker
if_stmt := checker.ast_module.statements[if_id] if_stmt := checker.ast_module.statements[if_id]
// ponytail: an unwrap `if` (captures) as a value source is a later milestone.
condition := hir.INVALID_EXPR
guard := hir.INVALID_EXPR
unwraps: []hir.Conditional_Unwrap = nil
capture_start := len(ctx.locals^)
if len(if_stmt.captures) > 0 { if len(if_stmt.captures) > 0 {
source.add(checker.diagnostics, if_stmt.span, "an unwrap 'if' cannot yet be used as a value") // Unwrap value-if (`name :: if opt |v| { yield v } else { yield 0 }`): mirror the
// build-pass unwrap arm to bind captures + guard; each branch then assigns the slot
// like any other branch, and the HIR `.If` carries the unwraps (lowering handles it).
ast_operands: [dynamic]ast.Expr_Id
ast_operands.allocator = checker.allocator
flatten_conditional_unwrap_operands(checker.ast_module, if_stmt.expr, &ast_operands)
ok := true
if len(ast_operands) != len(if_stmt.captures) {
source.addf(checker.diagnostics, if_stmt.span,
"'if' unwrap has %d operands but %d captures", len(ast_operands), len(if_stmt.captures))
ok = false
}
values := make([]hir.Expr_Id, len(ast_operands), checker.allocator)
child_types := make([]types.Type, len(ast_operands), checker.allocator)
for operand, index in ast_operands {
child_types[index] = types.INVALID
v := build_expr(checker, operand, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
values[index] = v
vt := checker.module.exprs[v].type
if checker.module.exprs[v].kind == .Invalid {
ok = false
} else if !types.is_optional(vt, &checker.module.types) {
source.addf(checker.diagnostics, checker.ast_module.exprs[operand].span,
"'if' unwrap requires an optional value (operand %d)", index + 1)
ok = false
} else {
child_types[index] = types.child_type(vt, &checker.module.types)
}
}
unwrap_list: [dynamic]hir.Conditional_Unwrap
unwrap_list.allocator = checker.allocator
for capture, index in if_stmt.captures {
child := child_types[index] if index < len(child_types) else types.INVALID
local := hir.INVALID_LOCAL
if capture != checker.sink_symbol {
if _, dup := find_build_local(ctx.locals^[capture_start:], capture); dup {
source.add(checker.diagnostics, if_stmt.span, "'if' unwrap captures must have distinct names")
ok = false
}
local = hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name=capture, type=child, mutable=false})
append(ctx.locals, Build_Local{name=capture, type=child, mutable=false, id=local})
}
if index < len(values) {
append(&unwrap_list, hir.Conditional_Unwrap{expr=values[index], local=local})
}
}
if if_stmt.guard != ast.INVALID_EXPR {
guard = build_expr(checker, if_stmt.guard, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file)
if checker.module.exprs[guard].kind == .Invalid {
ok = false
} else if !types.is_bool(checker.module.exprs[guard].type) {
source.add(checker.diagnostics, checker.ast_module.exprs[if_stmt.guard].span, "'if' unwrap guard must be a bool")
ok = false
}
}
delete(values, checker.allocator)
delete(child_types, checker.allocator)
delete(ast_operands)
if !ok {
resize(ctx.locals, capture_start)
delete(unwrap_list)
ctx.problematic^ = true ctx.problematic^ = true
return false return false
} }
condition := build_expr(checker, if_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file) unwraps = unwrap_list[:]
} else {
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) { 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") 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) condition = invalid_hir_expr(checker, if_stmt.span, id, types.BOOL)
ctx.problematic^ = true ctx.problematic^ = true
} }
}
// then-branch (unwrap captures, if any, are in scope here, then dropped before else).
then_body: [dynamic]hir.Stmt_Id then_body: [dynamic]hir.Stmt_Id
then_body.allocator = checker.allocator then_body.allocator = checker.allocator
if !emit_value_branch(ctx, &then_body, if_stmt.body, slot, slot_type, span) { then_ok := emit_value_branch(ctx, &then_body, if_stmt.body, slot, slot_type, span)
resize(ctx.locals, capture_start)
if !then_ok {
delete(then_body) delete(then_body)
delete(unwraps)
return false return false
} }
if if_stmt.else_body == nil { 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") source.add(checker.diagnostics, if_stmt.span, "an 'if' used as a value must have an 'else' so every path yields")
delete(then_body) delete(then_body)
delete(unwraps)
ctx.problematic^ = true ctx.problematic^ = true
return false return false
} }
@@ -5098,11 +5173,12 @@ emit_value_if :: proc(
if !branch_ok { if !branch_ok {
delete(then_body) delete(then_body)
delete(else_body) delete(else_body)
delete(unwraps)
return false return false
} }
append(out, hir.stmt_id(len(checker.module.statements))) append(out, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{ append(&checker.module.statements, hir.Stmt{
kind = .If, span = if_stmt.span, expr = condition, guard = hir.INVALID_EXPR, kind = .If, span = if_stmt.span, expr = condition, guard = guard, unwraps = unwraps,
then_body = then_body[:], else_body = else_body[:], then_body = then_body[:], else_body = else_body[:],
local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC, local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, diagnostic = source.INVALID_DIAGNOSTIC,
}) })
@@ -5112,6 +5188,8 @@ emit_value_if :: proc(
// emit_value_branch builds one branch of a value-if as a value block (leading stmts + // 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 // 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). // fixes the slot type; later branches coerce to it (a mismatch is the "same type" error).
// A branch that does not yield is valid only if it exits on every path (return/break/
// continue) — it then produces no value and never reaches the slot read.
emit_value_branch :: proc( emit_value_branch :: proc(
ctx: ^Build_Ctx, ctx: ^Build_Ctx,
out: ^[dynamic]hir.Stmt_Id, out: ^[dynamic]hir.Stmt_Id,
@@ -5121,6 +5199,27 @@ emit_value_branch :: proc(
span: source.Span, span: source.Span,
) -> bool { ) -> bool {
checker := ctx.checker checker := ctx.checker
n := len(branch_stmts)
ends_in_yield := n > 0 &&
checker.ast_module.statements[branch_stmts[n - 1]].kind == .Yield &&
!symbol.is_valid(checker.ast_module.statements[branch_stmts[n - 1]].label)
if !ends_in_yield {
// Not a value block: only allowed if every path exits (e.g. `else { return -1 }`),
// in which case it contributes no value to the slot.
built := build_block(ctx, branch_stmts)
for s in built {
append(out, s)
}
terminates := all_paths_exit(&checker.module, built)
delete(built, checker.allocator)
if terminates {
return true
}
source.add(checker.diagnostics, span,
"a value branch must end with 'yield' or exit on every path (return/break/continue)")
ctx.problematic^ = true
return false
}
value, vtype := build_value_block(ctx, out, branch_stmts, slot_type^, span) value, vtype := build_value_block(ctx, out, branch_stmts, slot_type^, span)
if checker.module.exprs[value].kind == .Invalid { if checker.module.exprs[value].kind == .Invalid {
return false return false
@@ -5175,6 +5274,74 @@ resolve_loop_slot :: proc(ctx: ^Build_Ctx, target: ^Yield_Target, value: hir.Exp
return coerce_expr(checker, value, target.slot_type, span) return coerce_expr(checker, value, target.slot_type, span)
} }
// first_concrete_yield_expr returns the AST expr of the first yield (source order) that is
// not the literal `none`, descending into `if`/block branches but not nested loops or value
// sources (whose yields belong to them). INVALID when the loop yields only `none`.
first_concrete_yield_expr :: proc(checker: ^Checker, stmts: []ast.Stmt_Id) -> ast.Expr_Id {
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 s.expr
}
case .If:
if e := first_concrete_yield_expr(checker, s.body); e != ast.INVALID_EXPR {
return e
}
if e := first_concrete_yield_expr(checker, s.else_body); e != ast.INVALID_EXPR {
return e
}
case .Block:
if e := first_concrete_yield_expr(checker, s.body); e != ast.INVALID_EXPR {
return e
}
}
}
return ast.INVALID_EXPR
}
// value_loop_element_type pre-types the element of an untyped value loop from its first
// concrete (non-`none`) yield, so a `none` yielded before any concrete value still resolves
// the result to `?T`. The loop's captures are bound temporarily for the probe and the probe
// expr is discarded; returns INVALID when the loop yields only `none`.
value_loop_element_type :: proc(ctx: ^Build_Ctx, loop_stmt: ast.Stmt) -> types.Type {
checker := ctx.checker
yield_expr := first_concrete_yield_expr(checker, loop_stmt.body)
if yield_expr == ast.INVALID_EXPR {
return types.INVALID
}
capture_start := len(ctx.locals^)
if loop_stmt.kind == .For {
// Mirror the `.For` arm's capture-type computation just enough to type the probe.
iterable := build_expr(checker, loop_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
iterable_type := checker.module.exprs[iterable].type
capture_type := types.INVALID
if types.is_range(iterable_type, &checker.module.types) {
capture_type = types.child_type(iterable_type, &checker.module.types)
} else if item, ok := sequence_item(iterable_type, &checker.module.types); ok {
capture_type = item.child
if loop_stmt.pointer_capture {
capture_type = types.pointer(&checker.module.types, item.child, item.mutable, false)
}
}
if symbol.is_valid(loop_stmt.name) {
id := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name=loop_stmt.name, type=capture_type, mutable=false})
append(ctx.locals, Build_Local{name=loop_stmt.name, type=capture_type, mutable=false, id=id})
}
if symbol.is_valid(loop_stmt.index_name) {
id := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name=loop_stmt.index_name, type=types.USIZE, mutable=false})
append(ctx.locals, Build_Local{name=loop_stmt.index_name, type=types.USIZE, mutable=false, id=id})
}
}
probe := build_expr(checker, yield_expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
result := checker.module.exprs[probe].type if checker.module.exprs[probe].kind != .Invalid else types.INVALID
resize(ctx.locals, capture_start)
return result
}
// build_value_loop turns a labeled `for/while ... blk: { … }` whose body ends in a // 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: // 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` // the fall-through value initializes the slot before the loop, each `yield :blk x`
@@ -5216,6 +5383,15 @@ build_value_loop :: proc(
slot_type = expected slot_type = expected
slot = new_value_slot(ctx, slot_type) slot = new_value_slot(ctx, slot_type)
result_optional = types.is_optional(slot_type, &checker.module.types) result_optional = types.is_optional(slot_type, &checker.module.types)
} else if result_optional {
// Untyped loop that also yields `none`: pre-type the element from the first
// concrete yield (regardless of source order) so a `none` built before any
// concrete yield still resolves the result to `?T`.
elem := value_loop_element_type(ctx, loop_stmt)
if is_runtime_type(checker, elem) {
slot_type = types.optional(&checker.module.types, elem)
slot = new_value_slot(ctx, slot_type)
}
} }
append(ctx.yield_targets, Yield_Target{ append(ctx.yield_targets, Yield_Target{
label = loop_stmt.label, slot = slot, slot_type = slot_type, result_optional = result_optional, label = loop_stmt.label, slot = slot, slot_type = slot_type, result_optional = result_optional,
+9 -8
View File
@@ -2131,10 +2131,11 @@ 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)
// Value blocks (untyped/typed, defer-spill, reassignment) plus value if-statements // Value blocks (untyped/typed, defer-spill, reassignment), value if-statements
// (untyped/typed/reassign/else-if/defer) and value loops (a labeled `for` search // (untyped/typed/reassign/else-if/defer, a branch that `return`s instead of yielding,
// yielding `?usize` on both the found and not-found paths, and a labeled `while`) // and an unwrap-`if` as a value source), `orelse`, and value loops (a labeled `for`
// together produce 42. // search yielding `?usize` on the found/not-found paths, a labeled `while`, and an
// untyped loop that yields `none` before any concrete value) together produce 42.
testing.expect_value(t, state.exit_code, 42) testing.expect_value(t, state.exit_code, 42)
} }
@@ -2179,9 +2180,9 @@ yield_misuse_is_diagnosed :: proc(t: ^testing.T) {
@(test) @(test)
yield_control_flow_is_diagnosed :: proc(t: ^testing.T) { yield_control_flow_is_diagnosed :: proc(t: ^testing.T) {
// Value if/loop misuse (milestone 20.5): an `if` value without an `else`; a // Value if/loop misuse: an `if` value without an `else`; a branch that neither
// branch that does not end in `yield`; a value loop whose body lacks a trailing // yields nor exits on every path (milestone 20.6); a value loop whose body lacks a
// fall-through `yield`; and a `yield :label` with no matching value loop. // trailing fall-through `yield`; and a `yield :label` with no matching value loop.
text := `main :: func() i32 { text := `main :: func() i32 {
noelse :: if (true) { noelse :: if (true) {
yield 1 yield 1
@@ -2218,7 +2219,7 @@ yield_control_flow_is_diagnosed :: proc(t: ^testing.T) {
stray_label := false stray_label := false
for diagnostic in diagnostics.items { for diagnostic in diagnostics.items {
no_else = no_else || strings.contains(diagnostic.message, "an 'if' used as a value must have an 'else'") 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'") branch_no_yield = branch_no_yield || strings.contains(diagnostic.message, "a value branch must end with 'yield' or exit on every path")
loop_no_yield = loop_no_yield || strings.contains(diagnostic.message, "a value loop's body must end with a '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'") stray_label = stray_label || strings.contains(diagnostic.message, "no enclosing value loop is labeled 'stray'")
} }
+52
View File
@@ -130,6 +130,50 @@ loop_while :: func() i32 {
return 2 return 2
} }
# --- value if/loop follow-ups (milestone 20.6) -------------------------------
# A branch may exit on every path (here `return`) instead of yielding; the slot
# read after the `if` is only reached on the yielding path.
vif_return :: func(sel i32) i32 {
r :: if (sel == 0) {
yield 10
} else {
return 55 # exits the function instead of yielding
}
return r + 1 # only when sel == 0: 10 + 1 = 11
}
# unwrap-`if` as a value source: present -> transform, absent -> default.
vif_unwrap :: func(opt ?i32) i32 {
r :: if opt |v| {
yield v * 2
} else {
yield 99
}
return r
}
# The simple "unwrap or fallback" case is just `orelse` (already a plain expression).
orelse_value :: func(opt ?i32) i32 {
r :: opt orelse 7
return r
}
# Untyped value loop where `none` is yielded (in a labeled yield) before any
# concrete value: the element type still resolves to ?<i> from `yield :blk i`.
loop_none_first :: func() i32 {
r :: for 0..10 |i| blk: {
if (i > 100) yield :blk none
if (i * i > 40) yield :blk i # first concrete yield: i == 7
yield none
}
if r |found| {
if (found == 7) 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
@@ -149,5 +193,13 @@ main :: func() i32 {
if (loop_none() != 0) return 114 if (loop_none() != 0) return 114
if (loop_while() != 0) return 115 if (loop_while() != 0) return 115
if (vif_return(0) != 11) return 116
if (vif_return(1) != 55) return 117
if (vif_unwrap(21) != 42) return 118
if (vif_unwrap(none) != 99) return 119
if (orelse_value(5) != 5) return 120
if (orelse_value(none) != 7) return 121
if (loop_none_first() != 0) return 122
return 42 return 42
} }