condtional multi-unwrap and guard clauses

This commit is contained in:
2026-06-22 20:37:37 +02:00
parent 27f42dd253
commit 663f4dc658
9 changed files with 567 additions and 96 deletions
+8 -3
View File
@@ -136,14 +136,18 @@ Stmt :: struct {
pointer_capture: bool,
target: Expr_Id,
expr: Expr_Id,
// `If` statements use `expr` as the condition, `body` as the then-block, and
// `else_body` as the else-block. An `else if` chain is represented as an
// `else_body` holding a single nested `If` statement.
// `If` statements use `expr` as the condition, `captures` as optional
// unwrap binding names, `guard` as the optional post-unwrap boolean
// condition, `body` as the then-block, and `else_body` as the else-block.
// An `else if` chain is represented as an `else_body` holding a single
// nested `If` statement.
// `While` statements use `expr` as the condition, `body` as the loop body,
// and `update` as the optional post-iteration statement.
// `For` statements use `expr` as the iterable, `name` as the item capture,
// `index_name` as the optional index capture, and `pointer_capture` to
// distinguish `|@item|` from copy capture.
captures: []symbol.Id,
guard: Expr_Id,
body: []Stmt_Id,
else_body: []Stmt_Id,
update: Stmt_Id,
@@ -263,6 +267,7 @@ destroy_module :: proc(module: ^Module) {
delete(expr.args, module.allocator)
}
for statement in module.statements {
delete(statement.captures, module.allocator)
delete(statement.body, module.allocator)
delete(statement.else_body, module.allocator)
}
+172 -32
View File
@@ -629,6 +629,9 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi
}
case .If:
mark_expr_imports_used(checker, statement.expr, file)
if statement.guard != ast.INVALID_EXPR {
mark_expr_imports_used(checker, statement.guard, file)
}
mark_block_imports_used(checker, statement.body, file)
mark_block_imports_used(checker, statement.else_body, file)
case .While:
@@ -1403,6 +1406,24 @@ infer_expr :: proc(
return last
}
flatten_conditional_unwrap_operands :: proc(
module: ^ast.Module,
expr_id: ast.Expr_Id,
operands: ^[dynamic]ast.Expr_Id,
) {
if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(module.exprs) {
append(operands, expr_id)
return
}
expr := module.exprs[expr_id]
if expr.kind == .And {
flatten_conditional_unwrap_operands(module, expr.left, operands)
flatten_conditional_unwrap_operands(module, expr.right, operands)
return
}
append(operands, expr_id)
}
infer_statements :: proc(
checker: ^Checker,
statements: []ast.Stmt_Id,
@@ -1438,16 +1459,36 @@ infer_statements :: proc(
}
}
case .If:
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
if statement.name != symbol.INVALID {
// Conditional unwrap: `v` is in scope (with the unwrapped type) only inside the then-block.
child := types.child_type(value_type, &checker.module.types) if types.is_optional(value_type, &checker.module.types) else types.INVALID
binding_start := len(locals^)
append(locals, Infer_Local{name = statement.name, type = child})
if len(statement.captures) > 0 {
operands: [dynamic]ast.Expr_Id
operands.allocator = checker.allocator
flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &operands)
operand_types := make([]types.Type, len(operands), checker.allocator)
for operand, index in operands {
operand_types[index] = infer_expr(checker, operand, locals^[:], pkg, file, demanded)
}
capture_start := len(locals^)
for capture, index in statement.captures {
if capture == checker.sink_symbol {
continue
}
capture_type := types.INVALID
if index < len(operand_types) &&
types.is_optional(operand_types[index], &checker.module.types) {
capture_type = types.child_type(operand_types[index], &checker.module.types)
}
append(locals, Infer_Local{name=capture, type=capture_type})
}
if statement.guard != ast.INVALID_EXPR {
_ = infer_expr(checker, statement.guard, locals^[:], pkg, file, demanded)
}
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
resize(locals, binding_start)
resize(locals, capture_start)
infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result)
delete(operand_types, checker.allocator)
delete(operands)
} else {
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result)
}
@@ -3142,37 +3183,135 @@ build_block :: proc(
})
}
case .If:
if statement.name != symbol.INVALID {
// Conditional unwrap `if opt |v| { ... }`: `expr` is the optional, `v`
// binds the unwrapped value (immutable) for the duration of the then-block.
value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
value_type := checker.module.exprs[value].type
child := types.INVALID
if checker.module.exprs[value].kind != .Invalid && !types.is_optional(value_type, &checker.module.types) {
id := source.add(checker.diagnostics, statement.span, "'if' unwrap requires an optional value")
value = invalid_hir_expr(checker, statement.span, id)
ctx.problematic^ = true
} else if checker.module.exprs[value].kind != .Invalid {
child = types.child_type(value_type, &checker.module.types)
if len(statement.captures) > 0 {
ast_operands: [dynamic]ast.Expr_Id
ast_operands.allocator = checker.allocator
flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &ast_operands)
valid_unwrap := true
diagnostic := source.INVALID_DIAGNOSTIC
if len(ast_operands) != len(statement.captures) {
diagnostic = source.addf(
checker.diagnostics,
statement.span,
"'if' unwrap has %d operands but %d captures",
len(ast_operands),
len(statement.captures),
)
valid_unwrap = false
}
binding := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name = statement.name, type = child, mutable = false})
locals_before := len(ctx.locals^)
append(ctx.locals, Build_Local{name = statement.name, type = child, mutable = false, id = binding})
then_body := build_block(ctx, statement.body)
resize(ctx.locals, locals_before)
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
value := build_expr(
checker, operand, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.INVALID, ctx.pkg, ctx.file,
)
values[index] = value
value_type := checker.module.exprs[value].type
if checker.module.exprs[value].kind == .Invalid {
valid_unwrap = false
if diagnostic == source.INVALID_DIAGNOSTIC {
diagnostic = checker.module.exprs[value].diagnostic
}
} else if !types.is_optional(value_type, &checker.module.types) {
diagnostic = source.addf(
checker.diagnostics,
checker.ast_module.exprs[operand].span,
"'if' unwrap requires an optional value (operand %d)",
index + 1,
)
valid_unwrap = false
} else {
child_types[index] = types.child_type(value_type, &checker.module.types)
}
}
capture_start := len(ctx.locals^)
unwraps: [dynamic]hir.Conditional_Unwrap
unwraps.allocator = checker.allocator
for capture, index in statement.captures {
child := child_types[index] if index < len(child_types) else types.INVALID
local := hir.INVALID_LOCAL
if capture != checker.sink_symbol {
if _, duplicate := find_build_local(ctx.locals^[capture_start:], capture); duplicate {
diagnostic = source.add(
checker.diagnostics,
statement.span,
"'if' unwrap captures must have distinct names",
)
valid_unwrap = 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(&unwraps, hir.Conditional_Unwrap{expr=values[index], local=local})
}
}
guard := hir.INVALID_EXPR
if statement.guard != ast.INVALID_EXPR {
guard = build_expr(
checker, statement.guard, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.BOOL, ctx.pkg, ctx.file,
)
if checker.module.exprs[guard].kind == .Invalid {
valid_unwrap = false
if diagnostic == source.INVALID_DIAGNOSTIC {
diagnostic = checker.module.exprs[guard].diagnostic
}
} else if !types.is_bool(checker.module.exprs[guard].type) {
diagnostic = source.add(
checker.diagnostics,
checker.ast_module.exprs[statement.guard].span,
"'if' unwrap guard must be a bool",
)
valid_unwrap = false
}
}
then_body := build_block(ctx, statement.body, capture_start)
resize(ctx.locals, capture_start)
else_body: []hir.Stmt_Id = nil
if statement.else_body != nil {
else_body = build_block(ctx, statement.else_body)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .If, span = statement.span, expr = value,
then_body = then_body, else_body = else_body,
local = binding, target = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
if valid_unwrap {
append(&checker.module.statements, hir.Stmt{
kind=.If,
span=statement.span,
expr=hir.INVALID_EXPR,
unwraps=unwraps[:],
guard=guard,
then_body=then_body,
else_body=else_body,
local=hir.INVALID_LOCAL,
target=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
delete(unwraps)
delete(then_body, checker.allocator)
delete(else_body, checker.allocator)
append(&checker.module.statements, hir.Stmt{
kind=.Trap,
span=statement.span,
expr=hir.INVALID_EXPR,
guard=hir.INVALID_EXPR,
local=hir.INVALID_LOCAL,
target=hir.INVALID_EXPR,
diagnostic=diagnostic,
})
ctx.problematic^ = true
}
delete(values, checker.allocator)
delete(child_types, checker.allocator)
delete(ast_operands)
continue
}
condition := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file)
@@ -3189,6 +3328,7 @@ build_block :: proc(
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .If, span = statement.span, expr = condition,
guard = hir.INVALID_EXPR,
then_body = then_body, else_body = else_body,
local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
+12 -2
View File
@@ -134,6 +134,11 @@ Local :: struct {
parameter: bool,
}
Conditional_Unwrap :: struct {
expr: Expr_Id,
local: Local_Id,
}
Stmt_Kind :: enum u8 {
Declaration,
Assignment,
@@ -155,13 +160,17 @@ Stmt :: struct {
expr: Expr_Id,
iterator_type: types.Type,
pointer_capture: bool,
// `If` statements use `expr` as the condition and `then_body`/`else_body` as
// the branch statement lists.
// Boolean `If` statements use `expr` as the condition. Conditional unwraps
// use `unwraps` for the ordered optional expressions and capture locals, and
// `guard` for the optional boolean checked after every unwrap succeeds.
// Both forms use `then_body`/`else_body` as the branch statement lists.
// `While` statements use `expr` as the condition, `then_body` as the loop
// body, and `update` as the optional post-iteration statement.
// `For` statements use `expr` as the iterable, `local` as the item capture,
// `index_local` as the optional sequence index, and `iterator_type` as the
// normalized many-item pointer type for sequence iteration.
unwraps: []Conditional_Unwrap,
guard: Expr_Id,
then_body: []Stmt_Id,
else_body: []Stmt_Id,
update: Stmt_Id,
@@ -232,6 +241,7 @@ destroy_module :: proc(module: ^Module) {
delete(expr.args, module.allocator)
}
for statement in module.statements {
delete(statement.unwraps, module.allocator)
delete(statement.then_body, module.allocator)
delete(statement.else_body, module.allocator)
}
+101 -41
View File
@@ -669,57 +669,117 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
diagnostic=statement.diagnostic,
})
case .If:
// A conditional unwrap (`if opt |v| { ... }`) carries the binding local id in
// `statement.local`; `expr` is then the optional, not a bool condition. Test it
// for presence, and inside the then-block bind the unwrapped value to the local.
is_unwrap := statement.local != hir.INVALID_LOCAL
opt := ir.INVALID_INSTRUCTION
cond: ir.Instruction_Id
if is_unwrap {
opt = lower_expr(state, statement.expr)
cond = append_instruction(state, ir.Instruction{
op=.Optional_Is_Some, span=statement.span, type=types.BOOL,
target=ir.INVALID_REF, a=opt, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
cond = lower_expr(state, statement.expr)
}
has_else := statement.else_body != nil
then_lbl := fresh_label(state)
else_lbl := fresh_label(state) if has_else else then_lbl
merge_lbl := fresh_label(state)
false_target := else_lbl if has_else else merge_lbl
append_instruction(state, ir.Instruction{
op=.Cond_Br, span=statement.span, type=types.VOID,
a=cond, integer=then_lbl, target=ir.Ref(u32(false_target)),
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
})
if len(statement.unwraps) > 0 {
// Evaluate each optional exactly once, entering the next operand only
// after the previous one is present. Capture storage is initialized in
// these success blocks so the optional guard can use every binding.
for unwrap in statement.unwraps {
optional := lower_expr(state, unwrap.expr)
present := append_instruction(state, ir.Instruction{
op=.Optional_Is_Some,
span=statement.span,
type=types.BOOL,
target=ir.INVALID_REF,
a=optional,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
success_lbl := fresh_label(state)
append_instruction(state, ir.Instruction{
op=.Cond_Br,
span=statement.span,
type=types.VOID,
a=present,
integer=success_lbl,
target=ir.Ref(u32(false_target)),
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Label,
span=statement.span,
type=types.VOID,
integer=success_lbl,
target=ir.INVALID_REF,
a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
if unwrap.local != hir.INVALID_LOCAL && int(unwrap.local) < len(state.func_locals) {
local := state.func_locals[unwrap.local]
slot := append_instruction(state, ir.Instruction{
op=.Alloca,
span=statement.span,
type=local.type,
target=ir.local_ref(ir.Local_Id(unwrap.local)),
a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
state.local_slots[unwrap.local] = slot
inner := append_instruction(state, ir.Instruction{
op=.Optional_Value,
span=statement.span,
type=local.type,
target=ir.INVALID_REF,
a=optional,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Store,
span=statement.span,
type=local.type,
target=ir.INVALID_REF,
a=slot,
b=inner,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
if statement.guard != hir.INVALID_EXPR {
guard := lower_expr(state, statement.guard)
append_instruction(state, ir.Instruction{
op=.Cond_Br,
span=statement.span,
type=types.VOID,
a=guard,
integer=then_lbl,
target=ir.Ref(u32(false_target)),
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
append_instruction(state, ir.Instruction{
op=.Br,
span=statement.span,
type=types.VOID,
integer=then_lbl,
target=ir.INVALID_REF,
a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
} else {
cond := lower_expr(state, statement.expr)
append_instruction(state, ir.Instruction{
op=.Cond_Br, span=statement.span, type=types.VOID,
a=cond, integer=then_lbl, target=ir.Ref(u32(false_target)),
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
append_instruction(state, ir.Instruction{
op=.Label, span=statement.span, type=types.VOID, integer=then_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
if is_unwrap && int(statement.local) < len(state.func_locals) {
local := state.func_locals[statement.local]
slot := append_instruction(state, ir.Instruction{
op=.Alloca, span=statement.span, type=local.type,
target=ir.local_ref(ir.Local_Id(statement.local)),
a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
state.local_slots[statement.local] = slot
inner := append_instruction(state, ir.Instruction{
op=.Optional_Value, span=statement.span, type=local.type,
target=ir.INVALID_REF, a=opt, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Store, span=statement.span, type=local.type,
target=ir.INVALID_REF, a=slot, b=inner,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
lower_statements(state, statement.then_body)
append_instruction(state, ir.Instruction{
op=.Br, span=statement.span, type=types.VOID, integer=merge_lbl,
+32 -8
View File
@@ -1129,16 +1129,39 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id {
parser.no_struct_literal = true
condition := parse_expression(parser)
parser.no_struct_literal = saved
binding := symbol.INVALID
captures: [dynamic]symbol.Id
captures.allocator = parser.module.allocator
guard := ast.INVALID_EXPR
if _, ok := allow(parser, .Pipe); ok {
name_tok, name_ok := allow(parser, .Identifier)
if name_ok {
binding = name_tok.symbol
} else {
source.add(parser.diagnostics, current(parser).span, "expected a binding name after '|'")
for {
name_tok := current(parser)
if name_tok.kind == .Identifier || name_tok.kind == .Underscore {
advance(parser)
append(&captures, name_tok.symbol)
} else {
source.add(parser.diagnostics, current(parser).span, "expected an unwrap capture name")
break
}
if _, comma_ok := allow(parser, .Comma); !comma_ok {
break
}
if current(parser).kind == .Colon || current(parser).kind == .Pipe {
source.add(parser.diagnostics, current(parser).span, "expected an unwrap capture after ','")
break
}
}
if _, guard_ok := allow(parser, .Colon); guard_ok {
if current(parser).kind == .Pipe {
source.add(parser.diagnostics, current(parser).span, "expected a guard expression after ':'")
} else {
saved = parser.no_struct_literal
parser.no_struct_literal = true
guard = parse_expression(parser)
parser.no_struct_literal = saved
}
}
if _, close_ok := allow(parser, .Pipe); !close_ok {
source.add(parser.diagnostics, current(parser).span, "expected '|' to close the unwrap binding")
source.add(parser.diagnostics, current(parser).span, "expected '|' to close unwrap captures")
}
}
skip_newlines(parser)
@@ -1164,8 +1187,9 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id {
append(&parser.module.statements, ast.Stmt{
kind=.If,
span=span_from(start.span, previous(parser).span),
name=binding,
expr=condition,
captures=captures[:],
guard=guard,
body=then_body,
else_body=else_body,
diagnostic=source.INVALID_DIAGNOSTIC,