value if/loop follow-ups
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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'")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user