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
+188 -12
View File
@@ -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 = <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).
// 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,