value if/loop follow-ups
This commit is contained in:
+188
-12
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user