diff --git a/TODO.md b/TODO.md index a693d6d..8a3c5d4 100644 --- a/TODO.md +++ b/TODO.md @@ -358,17 +358,44 @@ 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 + - follow-ups: a branch that early-`return`s instead of yielding, unwrap-`if` as a value + source, and `none`-before-concrete typing in untyped loops are done in 20.6; labeled value + blocks and `yield`/`break` to an outer loop are 20.7 (need labels in the IR) + +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 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 - 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 diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index e2010a5..ff2ccb8 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -5063,27 +5063,102 @@ emit_value_if :: proc( ) -> 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. + + condition := hir.INVALID_EXPR + guard := hir.INVALID_EXPR + unwraps: []hir.Conditional_Unwrap = nil + capture_start := len(ctx.locals^) + 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 + // 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 + return false + } + 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) { + 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-branch (unwrap captures, if any, are in scope here, then dropped before else). 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) { + 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(unwraps) 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) + delete(unwraps) ctx.problematic^ = true return false } @@ -5098,11 +5173,12 @@ emit_value_if :: proc( if !branch_ok { delete(then_body) delete(else_body) + delete(unwraps) 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, + kind = .If, span = if_stmt.span, expr = condition, guard = guard, unwraps = unwraps, then_body = then_body[:], else_body = else_body[:], 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 + // trailing yield) and appends `slot = `. The first branch of an untyped value-if // 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( ctx: ^Build_Ctx, out: ^[dynamic]hir.Stmt_Id, @@ -5121,6 +5199,27 @@ emit_value_branch :: proc( span: source.Span, ) -> bool { 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) if checker.module.exprs[value].kind == .Invalid { 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) } +// 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 // 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` @@ -5216,6 +5383,15 @@ build_value_loop :: proc( slot_type = expected slot = new_value_slot(ctx, slot_type) 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{ label = loop_stmt.label, slot = slot, slot_type = slot_type, result_optional = result_optional, diff --git a/compiler_tests.odin b/compiler_tests.odin index d206c51..55e393c 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2131,10 +2131,11 @@ yield_compiles_and_runs :: proc(t: ^testing.T) { status := compiler_core.compile_package("examples/programs/yield", output) testing.expect_value(t, status, 0) state := run_executable(output) - // Value blocks (untyped/typed, defer-spill, reassignment) plus value if-statements - // (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. + // Value blocks (untyped/typed, defer-spill, reassignment), value if-statements + // (untyped/typed/reassign/else-if/defer, a branch that `return`s instead of yielding, + // and an unwrap-`if` as a value source), `orelse`, and value loops (a labeled `for` + // 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) } @@ -2179,9 +2180,9 @@ yield_misuse_is_diagnosed :: proc(t: ^testing.T) { @(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. + // Value if/loop misuse: an `if` value without an `else`; a branch that neither + // yields nor exits on every path (milestone 20.6); 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 @@ -2218,7 +2219,7 @@ yield_control_flow_is_diagnosed :: proc(t: ^testing.T) { 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'") + 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'") stray_label = stray_label || strings.contains(diagnostic.message, "no enclosing value loop is labeled 'stray'") } diff --git a/examples/programs/yield/main.bro b/examples/programs/yield/main.bro index da97aa5..6b1e250 100644 --- a/examples/programs/yield/main.bro +++ b/examples/programs/yield/main.bro @@ -130,6 +130,50 @@ loop_while :: func() i32 { 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 ? 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 { if (basic() != 42) return 101 if (typed() != 100) return 102 @@ -149,5 +193,13 @@ main :: func() i32 { if (loop_none() != 0) return 114 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 }