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
+1 -1
View File
@@ -25,7 +25,7 @@
- information-preserving and information-forgetting pointer-to-array decay and sentinel slice/pointer weakening; array values never implicitly decay - information-preserving and information-forgetting pointer-to-array decay and sentinel slice/pointer weakening; array values never implicitly decay
- narrow immutable zero-terminated byte pointer conversion to `*c_char` and `[*;0]c_char`, without general `u8`/`c_char` interchange - narrow immutable zero-terminated byte pointer conversion to `*c_char` and `[*;0]c_char`, without general `u8`/`c_char` interchange
- optionals with trapping postfix `?`, `orelse`, and nullable pointer representation - optionals with trapping postfix `?`, `orelse`, and nullable pointer representation
- conditional optional unwrapping with immutable then-block bindings: `if value |binding| { ... }` - conditional optional unwrapping with immutable guard/then-block bindings, guarded captures, and left-to-right short-circuiting multi-unwrap: `if first and second |a, b : guard| { ... }`
- source-order native structs, defined or opaque `c_struct`, and keyed record literals - source-order native structs, defined or opaque `c_struct`, and keyed record literals
- complete plain imported C structs and unions as runtime values; incomplete or unsupported-layout records remain pointer-only - complete plain imported C structs and unions as runtime values; incomplete or unsupported-layout records remain pointer-only
- C function pointer types as pointer-sized runtime values, including manual `*c_func(...) T` spelling and nullable imported callback typedefs - C function pointer types as pointer-sized runtime values, including manual `*c_func(...) T` spelling and nullable imported callback typedefs
+4 -4
View File
@@ -80,7 +80,7 @@
- function-like macros and non-literal macro expressions remain unsupported - function-like macros and non-literal macro expressions remain unsupported
- static inline functions (implemented) - static inline functions (implemented)
5. control flow 5. control flow (implemented)
- boolean expressions (implemented) - boolean expressions (implemented)
- `bool` type with `true` / `false` literals - `bool` type with `true` / `false` literals
- comparison operators: `==`, `!=`, `<`, `<=`, `>`, `>=` (numeric operands widen; `bool` supports only `==` / `!=`) - comparison operators: `==`, `!=`, `<`, `<=`, `>`, `>=` (numeric operands widen; `bool` supports only `==` / `!=`)
@@ -93,8 +93,8 @@
- single immutable binding scoped to the then-block; `v` not visible in `else` or after the `if` - single immutable binding scoped to the then-block; `v` not visible in `else` or after the `if`
- `|` lexes as a new `Pipe` token; the `.If` reuses AST `name` / HIR `local` to carry the binding (no new statement kind) - `|` lexes as a new `Pipe` token; the `.If` reuses AST `name` / HIR `local` to carry the binding (no new statement kind)
- new `Optional_Is_Some` / `Optional_Value` IR opcodes (the `Unwrap` presence-test + extract, minus the trap) - new `Optional_Is_Some` / `Optional_Value` IR opcodes (the `Unwrap` presence-test + extract, minus the trap)
- conditional unwrapping with guard clause: `if val |v : v >= 10| { ... } else { ... }` - unwrap `val` into `v` if it is not `none` - conditional unwrapping with guard clause (implemented): `if val |v : v >= 10| { ... } else { ... }` - enter the then-block when `val` is not `none` and the guard is true
- multi-unwrap (see section below) - multi-unwrap (implemented; see section below)
- while loops (implemented; operates on boolean conditions). examples: - while loops (implemented; operates on boolean conditions). examples:
- `while condition { ... }` - iterate while the condition is true - `while condition { ... }` - iterate while the condition is true
- `while condition : i = i + 1 { ... }` - execute the update after each completed iteration - `while condition : i = i + 1 { ... }` - execute the update after each completed iteration
@@ -179,7 +179,7 @@ Ranges represent a sequence of values, commonly used in for loops, and is itself
(a + 1)..(b - 1) # OK: both sides parenthesized (a + 1)..(b - 1) # OK: both sides parenthesized
# 0..n + 1 # ERROR: must parenthesize complex expressions # 0..n + 1 # ERROR: must parenthesize complex expressions
``` ```
This rule keeps the grammar simple and forces clarity at the call site — no precedence rules to remember. Also, being a value type, ranges can be assigned to variables and passed around like any other value. Range bounds are evaluated once, must have compatible concrete integer types, and descending ranges are empty. This rule keeps the grammar simple and forces clarity at the call site — no precedence rules to remember. Also, being a value type, ranges can be assigned to variables and passed around like any other value. Range bounds are evaluated once, must have compatible concrete integer types, and descending ranges are empty.
For-loop captures are immutable and scoped to the loop body. Sequence index captures are `usize`. Pointer capture uses `|@item|`; arrays must be passed by pointer (for example `&items`), while slices can be used directly. Sentinel elements are not included in iteration. For-loop captures are immutable and scoped to the loop body. Sequence index captures are `usize`. Pointer capture uses `|@item|`; arrays must be passed by pointer (for example `&items`), while slices can be used directly. Sentinel elements are not included in iteration.
+8 -3
View File
@@ -136,14 +136,18 @@ Stmt :: struct {
pointer_capture: bool, pointer_capture: bool,
target: Expr_Id, target: Expr_Id,
expr: Expr_Id, expr: Expr_Id,
// `If` statements use `expr` as the condition, `body` as the then-block, and // `If` statements use `expr` as the condition, `captures` as optional
// `else_body` as the else-block. An `else if` chain is represented as an // unwrap binding names, `guard` as the optional post-unwrap boolean
// `else_body` holding a single nested `If` statement. // 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, // `While` statements use `expr` as the condition, `body` as the loop body,
// and `update` as the optional post-iteration statement. // and `update` as the optional post-iteration statement.
// `For` statements use `expr` as the iterable, `name` as the item capture, // `For` statements use `expr` as the iterable, `name` as the item capture,
// `index_name` as the optional index capture, and `pointer_capture` to // `index_name` as the optional index capture, and `pointer_capture` to
// distinguish `|@item|` from copy capture. // distinguish `|@item|` from copy capture.
captures: []symbol.Id,
guard: Expr_Id,
body: []Stmt_Id, body: []Stmt_Id,
else_body: []Stmt_Id, else_body: []Stmt_Id,
update: Stmt_Id, update: Stmt_Id,
@@ -263,6 +267,7 @@ destroy_module :: proc(module: ^Module) {
delete(expr.args, module.allocator) delete(expr.args, module.allocator)
} }
for statement in module.statements { for statement in module.statements {
delete(statement.captures, module.allocator)
delete(statement.body, module.allocator) delete(statement.body, module.allocator)
delete(statement.else_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: case .If:
mark_expr_imports_used(checker, statement.expr, file) 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.body, file)
mark_block_imports_used(checker, statement.else_body, file) mark_block_imports_used(checker, statement.else_body, file)
case .While: case .While:
@@ -1403,6 +1406,24 @@ infer_expr :: proc(
return last 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( infer_statements :: proc(
checker: ^Checker, checker: ^Checker,
statements: []ast.Stmt_Id, statements: []ast.Stmt_Id,
@@ -1438,16 +1459,36 @@ infer_statements :: proc(
} }
} }
case .If: case .If:
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) if len(statement.captures) > 0 {
if statement.name != symbol.INVALID { operands: [dynamic]ast.Expr_Id
// Conditional unwrap: `v` is in scope (with the unwrapped type) only inside the then-block. operands.allocator = checker.allocator
child := types.child_type(value_type, &checker.module.types) if types.is_optional(value_type, &checker.module.types) else types.INVALID flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &operands)
binding_start := len(locals^) operand_types := make([]types.Type, len(operands), checker.allocator)
append(locals, Infer_Local{name = statement.name, type = child}) 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) 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) infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result)
delete(operand_types, checker.allocator)
delete(operands)
} else { } else {
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
infer_statements(checker, statement.body, locals, pkg, file, demanded, result) infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
infer_statements(checker, statement.else_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: case .If:
if statement.name != symbol.INVALID { if len(statement.captures) > 0 {
// Conditional unwrap `if opt |v| { ... }`: `expr` is the optional, `v` ast_operands: [dynamic]ast.Expr_Id
// binds the unwrapped value (immutable) for the duration of the then-block. ast_operands.allocator = checker.allocator
value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &ast_operands)
value_type := checker.module.exprs[value].type
child := types.INVALID valid_unwrap := true
if checker.module.exprs[value].kind != .Invalid && !types.is_optional(value_type, &checker.module.types) { diagnostic := source.INVALID_DIAGNOSTIC
id := source.add(checker.diagnostics, statement.span, "'if' unwrap requires an optional value") if len(ast_operands) != len(statement.captures) {
value = invalid_hir_expr(checker, statement.span, id) diagnostic = source.addf(
ctx.problematic^ = true checker.diagnostics,
} else if checker.module.exprs[value].kind != .Invalid { statement.span,
child = types.child_type(value_type, &checker.module.types) "'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}) values := make([]hir.Expr_Id, len(ast_operands), checker.allocator)
locals_before := len(ctx.locals^) child_types := make([]types.Type, len(ast_operands), checker.allocator)
append(ctx.locals, Build_Local{name = statement.name, type = child, mutable = false, id = binding}) for operand, index in ast_operands {
then_body := build_block(ctx, statement.body) child_types[index] = types.INVALID
resize(ctx.locals, locals_before) 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 else_body: []hir.Stmt_Id = nil
if statement.else_body != nil { if statement.else_body != nil {
else_body = build_block(ctx, statement.else_body) else_body = build_block(ctx, statement.else_body)
} }
append(&body, hir.stmt_id(len(checker.module.statements))) append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{ if valid_unwrap {
kind = .If, span = statement.span, expr = value, append(&checker.module.statements, hir.Stmt{
then_body = then_body, else_body = else_body, kind=.If,
local = binding, target = hir.INVALID_EXPR, span=statement.span,
diagnostic = source.INVALID_DIAGNOSTIC, expr=hir.INVALID_EXPR,
}) unwraps=unwraps[:],
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid 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 continue
} }
condition := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.BOOL, ctx.pkg, ctx.file) 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(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{ append(&checker.module.statements, hir.Stmt{
kind = .If, span = statement.span, expr = condition, kind = .If, span = statement.span, expr = condition,
guard = hir.INVALID_EXPR,
then_body = then_body, else_body = else_body, then_body = then_body, else_body = else_body,
local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR, local = hir.INVALID_LOCAL, target = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC, diagnostic = source.INVALID_DIAGNOSTIC,
+12 -2
View File
@@ -134,6 +134,11 @@ Local :: struct {
parameter: bool, parameter: bool,
} }
Conditional_Unwrap :: struct {
expr: Expr_Id,
local: Local_Id,
}
Stmt_Kind :: enum u8 { Stmt_Kind :: enum u8 {
Declaration, Declaration,
Assignment, Assignment,
@@ -155,13 +160,17 @@ Stmt :: struct {
expr: Expr_Id, expr: Expr_Id,
iterator_type: types.Type, iterator_type: types.Type,
pointer_capture: bool, pointer_capture: bool,
// `If` statements use `expr` as the condition and `then_body`/`else_body` as // Boolean `If` statements use `expr` as the condition. Conditional unwraps
// the branch statement lists. // 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 // `While` statements use `expr` as the condition, `then_body` as the loop
// body, and `update` as the optional post-iteration statement. // body, and `update` as the optional post-iteration statement.
// `For` statements use `expr` as the iterable, `local` as the item capture, // `For` statements use `expr` as the iterable, `local` as the item capture,
// `index_local` as the optional sequence index, and `iterator_type` as the // `index_local` as the optional sequence index, and `iterator_type` as the
// normalized many-item pointer type for sequence iteration. // normalized many-item pointer type for sequence iteration.
unwraps: []Conditional_Unwrap,
guard: Expr_Id,
then_body: []Stmt_Id, then_body: []Stmt_Id,
else_body: []Stmt_Id, else_body: []Stmt_Id,
update: Stmt_Id, update: Stmt_Id,
@@ -232,6 +241,7 @@ destroy_module :: proc(module: ^Module) {
delete(expr.args, module.allocator) delete(expr.args, module.allocator)
} }
for statement in module.statements { for statement in module.statements {
delete(statement.unwraps, module.allocator)
delete(statement.then_body, module.allocator) delete(statement.then_body, module.allocator)
delete(statement.else_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, diagnostic=statement.diagnostic,
}) })
case .If: 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 has_else := statement.else_body != nil
then_lbl := fresh_label(state) then_lbl := fresh_label(state)
else_lbl := fresh_label(state) if has_else else then_lbl else_lbl := fresh_label(state) if has_else else then_lbl
merge_lbl := fresh_label(state) merge_lbl := fresh_label(state)
false_target := else_lbl if has_else else merge_lbl false_target := else_lbl if has_else else merge_lbl
append_instruction(state, ir.Instruction{
op=.Cond_Br, span=statement.span, type=types.VOID, if len(statement.unwraps) > 0 {
a=cond, integer=then_lbl, target=ir.Ref(u32(false_target)), // Evaluate each optional exactly once, entering the next operand only
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, // 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{ append_instruction(state, ir.Instruction{
op=.Label, span=statement.span, type=types.VOID, integer=then_lbl, op=.Label, span=statement.span, type=types.VOID, integer=then_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC, 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) lower_statements(state, statement.then_body)
append_instruction(state, ir.Instruction{ append_instruction(state, ir.Instruction{
op=.Br, span=statement.span, type=types.VOID, integer=merge_lbl, 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 parser.no_struct_literal = true
condition := parse_expression(parser) condition := parse_expression(parser)
parser.no_struct_literal = saved 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 { if _, ok := allow(parser, .Pipe); ok {
name_tok, name_ok := allow(parser, .Identifier) for {
if name_ok { name_tok := current(parser)
binding = name_tok.symbol if name_tok.kind == .Identifier || name_tok.kind == .Underscore {
} else { advance(parser)
source.add(parser.diagnostics, current(parser).span, "expected a binding name after '|'") 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 { 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) skip_newlines(parser)
@@ -1164,8 +1187,9 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id {
append(&parser.module.statements, ast.Stmt{ append(&parser.module.statements, ast.Stmt{
kind=.If, kind=.If,
span=span_from(start.span, previous(parser).span), span=span_from(start.span, previous(parser).span),
name=binding,
expr=condition, expr=condition,
captures=captures[:],
guard=guard,
body=then_body, body=then_body,
else_body=else_body, else_body=else_body,
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
+181 -2
View File
@@ -3823,11 +3823,121 @@ conditional_unwrap_compiles_and_runs :: proc(t: ^testing.T) {
status := compiler_core.compile_package("examples/programs/conditional_unwrap", output) status := compiler_core.compile_package("examples/programs/conditional_unwrap", output)
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable(output) state := run_executable(output)
// present scalar binds and unwraps (40), none takes the else (+2), a present // Single unwrap, guarded two/three-value unwraps, optional pointers, false
// optional pointer binds and derefs (+0), a none optional pointer is skipped. // guards, and failed short-circuit chains preserve the expected total.
testing.expect_value(t, state.exit_code, 42) testing.expect_value(t, state.exit_code, 42)
} }
@(test)
conditional_unwrap_parser_captures_guard_and_parenthesized_chain :: proc(t: ^testing.T) {
text := `main :: func() void {
first ?i32 = 1
second ?i32 = 2
if (first and second) |a, b : a == 1 and b == 2| {
_ = a
_ = b
}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
statement := module.statements[module.functions[0].body[2]]
testing.expect_value(t, statement.kind, ast.Stmt_Kind.If)
testing.expect_value(t, len(statement.captures), 2)
testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.And)
testing.expect(t, module.exprs[statement.expr].parenthesized)
testing.expect(t, statement.guard != ast.INVALID_EXPR)
testing.expect_value(t, module.exprs[statement.guard].kind, ast.Expr_Kind.And)
}
@(test)
conditional_unwrap_allows_sink_captures :: proc(t: ^testing.T) {
text := `main :: func() void {
first ?i32 = 1
second ?i32 = 2
if first and second |_, value : value == 2| {
_ = value
}
if first |_| {}
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
main := hir_module.functions[0]
first_if := hir_module.statements[main.body[2]]
second_if := hir_module.statements[main.body[3]]
testing.expect_value(t, first_if.unwraps[0].local, hir.INVALID_LOCAL)
testing.expect(t, first_if.unwraps[1].local != hir.INVALID_LOCAL)
testing.expect_value(t, second_if.unwraps[0].local, hir.INVALID_LOCAL)
}
@(test)
parser_diagnoses_malformed_conditional_unwrap_captures_and_guards :: proc(t: ^testing.T) {
cases := [4]struct {
text: string,
needle: string,
}{
{`main :: func() void {
value ?i32 = 1
if value || {}
}
`, "expected an unwrap capture name"},
{`main :: func() void {
value ?i32 = 1
if value |capture,| {}
}
`, "expected an unwrap capture after ','"},
{`main :: func() void {
value ?i32 = 1
if value |capture :| {}
}
`, "expected a guard expression after ':'"},
{`main :: func() void {
value ?i32 = 1
if value |capture {}
}
`, "expected '|' to close unwrap captures"},
}
for test_case in cases {
source_file := source.Source{path="test.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
module := parser.parse(&stream, &source_file, &diagnostics)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.needle)
}
testing.expect(t, found)
ast.destroy_module(&module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test) @(test)
if_unwrap_on_non_optional_is_diagnosed :: proc(t: ^testing.T) { if_unwrap_on_non_optional_is_diagnosed :: proc(t: ^testing.T) {
text := `main :: func() i32 { text := `main :: func() i32 {
@@ -3857,6 +3967,75 @@ if_unwrap_on_non_optional_is_diagnosed :: proc(t: ^testing.T) {
testing.expect(t, found) testing.expect(t, found)
} }
@(test)
conditional_unwrap_diagnostics_cover_counts_guards_and_capture_scope :: proc(t: ^testing.T) {
text := `main :: func() void {
first ?i32 = 1
second ?i32 = 2
plain i32 = 3
if first and second |one| {}
if first |one, two| {}
if first and second |same, same| {}
if plain |value| {}
if first |value : value| {}
if first and earlier |earlier, later| {}
if first |value| {
value = 2
}
if first |value| {
value i32 = 2
_ = value
}
if first |value| {
_ = value
} else {
_ = value
}
_ = value
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
count_mismatches := 0
duplicate := false
non_optional := false
guard := false
outer_operand_scope := false
immutable := false
redeclaration := false
capture_scope := 0
for diagnostic in diagnostics.items {
count_mismatches += 1 if strings.contains(diagnostic.message, "unwrap has") else 0
duplicate = duplicate || strings.contains(diagnostic.message, "unwrap captures must have distinct names")
non_optional = non_optional || strings.contains(diagnostic.message, "unwrap requires an optional value")
guard = guard || strings.contains(diagnostic.message, "unwrap guard must be a bool")
outer_operand_scope = outer_operand_scope || strings.contains(diagnostic.message, "unresolved global 'earlier'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'value'")
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'value'")
capture_scope += 1 if strings.contains(diagnostic.message, "unresolved global 'value'") else 0
}
testing.expect_value(t, count_mismatches, 2)
testing.expect(t, duplicate)
testing.expect(t, non_optional)
testing.expect(t, guard)
testing.expect(t, outer_operand_scope)
testing.expect(t, immutable)
testing.expect(t, redeclaration)
testing.expect_value(t, capture_scope, 2)
}
@(test) @(test)
if_unwrap_binding_is_scoped_to_then_block :: proc(t: ^testing.T) { if_unwrap_binding_is_scoped_to_then_block :: proc(t: ^testing.T) {
// The binding `v` is usable in the then-block but not in the else-block. // The binding `v` is usable in the then-block but not in the else-block.
+56 -3
View File
@@ -1,6 +1,9 @@
# Milestone 5: conditional unwrapping `if opt |v| { ... }`. # Milestone 5: conditional optional unwrapping, guards, and multi-unwrap.
# Tests a present optional binds and unwraps, a none takes the else branch, and
# that optional pointers (?*T) unwrap the same way. observe :: func(counter @mut i32, value ?i32) ?i32 {
counter^ = counter^ + 1
return value
}
main :: func() i32 { main :: func() i32 {
total i32 = 0 total i32 = 0
@@ -34,5 +37,55 @@ main :: func() i32 {
total = total + 1000 total = total + 1000
} }
# guarded multi-unwrap exposes every capture to the guard and then-block
age ?i32 = 2
if a and age |value, years : value + years == 42| {
total = total
} else {
total = total + 100
}
# parenthesized chains and three-value unwraps are equivalent
bonus ?i32 = 0
if (a and age and bonus) |value, years, extra : value + years + extra == 42| {
total = total
} else {
total = total + 100
}
# false guards use the else branch, or simply fall through without one
if a |value : value == 0| {
total = total + 100
} else {
total = total
}
if a |value : value == 0| {
total = total + 100
}
# a failed unwrap prevents later expressions from being evaluated
calls i32 = 0
if b and observe(&calls, age) |missing, observed| {
total = total + missing + observed
}
if calls != 0 {
total = total + 100
}
# short-circuiting also applies after an earlier successful unwrap
if a and b and observe(&calls, age) |value, missing, observed| {
total = total + value + missing + observed
}
if calls != 0 {
total = total + 100
}
# optional pointers participate in multi-unwrap and guards
if a and p |value, q : value == 40 and q^ == 0| {
total = total
} else {
total = total + 100
}
return total # expect exit code 42 return total # expect exit code 42
} }