conditionally unwrapping while loops

This commit is contained in:
2026-08-11 18:33:54 +02:00
parent ba6052eef3
commit fa53ca2219
8 changed files with 486 additions and 307 deletions
+5 -5
View File
@@ -39,7 +39,7 @@ roadmap and milestone history.
- unsafe `constcast!(value)` for restoring mutability to pointers, optional pointers, and slices without changing their child type or shape - unsafe `constcast!(value)` for restoring mutability to pointers, optional pointers, and slices without changing their child type or shape
- UTF-8 string literals as immutable pointers to static zero-terminated byte arrays, plus raw backtick multiline strings - UTF-8 string literals as immutable pointers to static zero-terminated byte arrays, plus raw backtick multiline strings
- narrow immutable zero-terminated byte pointer/slice conversion to `*c_char` / `?*c_char` without general `u8`/`c_char` interchange - narrow immutable zero-terminated byte pointer/slice conversion to `*c_char` / `?*c_char` without general `u8`/`c_char` interchange
- optionals with `null`, `orelse`, postfix `?`, conditional unwraps, guarded unwraps, and left-to-right short-circuiting multi-unwraps - optionals with `null`, `orelse`, postfix `?`, conditional `if`/`while` unwraps, guarded unwraps, and left-to-right short-circuiting multi-unwraps
- nominal distinct types with explicit scalar backing conversion during construction and explicit scalar backing extraction, native enums with optional explicit integer backing and explicit backing-to-scalar casts, contextual enum literals, and imported C enums as target-backed integer aliases - nominal distinct types with explicit scalar backing conversion during construction and explicit scalar backing extraction, native enums with optional explicit integer backing and explicit backing-to-scalar casts, contextual enum literals, and imported C enums as target-backed integer aliases
- compiler-reordered native structs with fields laid out by decreasing alignment (declaration order breaks ties and remains the reflection/diagnostic order), opaque nominal records with `Name :: opaque`, complete source-order `c_struct { ... }`, keyed record literals, native untagged unions, and native tagged unions `union(Enum)` / `union(enum)` - compiler-reordered native structs with fields laid out by decreasing alignment (declaration order breaks ties and remains the reflection/diagnostic order), opaque nominal records with `Name :: opaque`, complete source-order `c_struct { ... }`, keyed record literals, native untagged unions, and native tagged unions `union(Enum)` / `union(enum)`
- named native struct fields may declare defaults with `field T = expression`; keyed literals use defaults for omitted fields and explicit initializers override them - named native struct fields may declare defaults with `field T = expression`; keyed literals use defaults for omitted fields and explicit initializers override them
@@ -119,7 +119,7 @@ fields. `_` is not a keyword member name.
- assignments and compound assignments `+= -= *= /= &= |= xor= <<= >>= <<|=` with single evaluation of complex lvalues; `/=` is float-only and `xor=` is contiguous - assignments and compound assignments `+= -= *= /= &= |= xor= <<= >>= <<|=` with single evaluation of complex lvalues; `/=` is float-only and `xor=` is contiguous
- field access through struct values and pointers, index/slice bounds contextually coerced to `usize`, and unsigned narrower index support - field access through struct values and pointers, index/slice bounds contextually coerced to `usize`, and unsigned narrower index support
- boolean `if` / `else if` / `else` and `for` loops with braceless single-statement bodies when the preceding expression is parenthesized or a function call - boolean `if` / `else if` / `else` and `for` loops with braceless single-statement bodies when the preceding expression is parenthesized or a function call
- `while` loops with optional post-iteration update clauses - `while` loops with conditional unwrap captures and guards plus optional post-iteration update clauses
- `for` loops over ranges, arrays, slices, and pointers-to-arrays with copy captures, pointer captures `|@item|`, and optional `usize` index captures; `inline for` specializes a comptime aggregate into one checked body per element - `for` loops over ranges, arrays, slices, and pointers-to-arrays with copy captures, pointer captures `|@item|`, and optional `usize` index captures; `inline for` specializes a comptime aggregate into one checked body per element
- `break`, `continue`, labeled `break :label`, labeled `continue :label`, and labeled plain blocks; `break :label` can cross nested scopes to exit a labeled block - `break`, `continue`, labeled `break :label`, labeled `continue :label`, and labeled plain blocks; `break :label` can cross nested scopes to exit a labeled block
- bare block scopes, `defer`, and fallible-function `errdefer` with optional error capture; cleanup is block-scoped and LIFO - bare block scopes, `defer`, and fallible-function `errdefer` with optional error capture; cleanup is block-scoped and LIFO
@@ -154,9 +154,9 @@ and
or or
``` ```
Each level is left-associative. Because `|` also delimits `if` and `for` captures, a bitwise-OR Each level is left-associative. Because `|` also delimits `if`, `while`, and `for` captures, a
header expression must be parenthesized before a capture list, for example bitwise-OR header expression must be parenthesized before a capture list, for example
`if (flags | mask) |value| { ... }`. `while (flags | mask) |value| { ... }`.
#### division #### division
+6 -7
View File
@@ -202,13 +202,12 @@ Stmt :: struct {
assignment_op: Assignment_Op, assignment_op: Assignment_Op,
target: Expr_Id, target: Expr_Id,
expr: Expr_Id, expr: Expr_Id,
// `If` statements use `expr` as the condition, `captures` as optional // `If` and `While` statements use `expr` as the condition, `captures` as
// unwrap binding names, `guard` as the optional post-unwrap boolean // optional unwrap binding names, and `guard` as the optional post-unwrap
// condition, `body` as the then-block, and `else_body` as the else-block. // boolean condition. `If` uses `body` as the then-block and `else_body` as
// An `else if` chain is represented as an `else_body` holding a single // the else-block; an `else if` chain is represented as an `else_body`
// nested `If` statement. // holding a single nested `If` statement. `While` uses `body` as the loop
// `While` statements use `expr` as the condition, `body` as the loop body, // 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.
+208 -126
View File
@@ -363,7 +363,7 @@ block_reads_name :: proc(checker: ^Checker, statements: []ast.Stmt_Id, name: sym
append(&statement_stack, ..statement.body) append(&statement_stack, ..statement.body)
append(&statement_stack, ..statement.else_body) append(&statement_stack, ..statement.else_body)
case .While: case .While:
append(&expr_stack, statement.expr) append(&expr_stack, statement.expr, statement.guard)
append(&statement_stack, ..statement.body) append(&statement_stack, ..statement.body)
if statement.update != ast.INVALID_STMT { if statement.update != ast.INVALID_STMT {
append(&statement_stack, statement.update) append(&statement_stack, statement.update)
@@ -3800,6 +3800,9 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi
mark_block_imports_used(checker, statement.else_body, file) mark_block_imports_used(checker, statement.else_body, file)
case .While: case .While:
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)
if statement.update != ast.INVALID_STMT { if statement.update != ast.INVALID_STMT {
update := [1]ast.Stmt_Id{statement.update} update := [1]ast.Stmt_Id{statement.update}
@@ -6309,12 +6312,40 @@ infer_statements :: proc(
} }
} }
case .While: case .While:
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) capture_start := len(locals^)
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, local_types)
}
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, declared=capture_type, statement=ast.INVALID_STMT})
}
if statement.guard != ast.INVALID_EXPR {
_ = infer_expr(checker, statement.guard, locals^[:], pkg, file, demanded, local_types)
}
delete(operand_types, checker.allocator)
delete(operands)
} else {
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
}
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
if statement.update != ast.INVALID_STMT { if statement.update != ast.INVALID_STMT {
update := [1]ast.Stmt_Id{statement.update} update := [1]ast.Stmt_Id{statement.update}
infer_statements(checker, update[:], locals, local_types, pkg, file, demanded, result, result_hint) infer_statements(checker, update[:], locals, local_types, pkg, file, demanded, result, result_hint)
} }
resize(locals, capture_start)
case .For: case .For:
if statement.expand { if statement.expand {
bindings, expand_error := expand_field_bindings(checker, statement.expr, statement.name, pkg, file) bindings, expand_error := expand_field_bindings(checker, statement.expr, statement.name, pkg, file)
@@ -11042,6 +11073,122 @@ flatten_expand_iteration :: proc(
return .Normal return .Normal
} }
build_conditional_unwrap_header :: proc(
ctx: ^Build_Ctx,
statement: ast.Stmt,
keyword: string,
) -> (
unwraps: []hir.Conditional_Unwrap,
guard: hir.Expr_Id,
capture_start: int,
diagnostic: source.Diagnostic_Id,
valid: bool,
) {
checker := ctx.checker
ast_operands: [dynamic]ast.Expr_Id
ast_operands.allocator = checker.allocator
defer delete(ast_operands)
flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &ast_operands)
valid = true
diagnostic = source.INVALID_DIAGNOSTIC
if len(ast_operands) != len(statement.captures) {
diagnostic = source.addf(
checker.diagnostics,
statement.span,
"'%s' unwrap has %d operands but %d captures",
keyword,
len(ast_operands),
len(statement.captures),
)
valid = false
}
values := make([]hir.Expr_Id, len(ast_operands), checker.allocator)
defer delete(values, checker.allocator)
child_types := make([]types.Type, len(ast_operands), checker.allocator)
defer delete(child_types, 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 = 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,
"'%s' unwrap requires an optional value (operand %d)",
keyword,
index + 1,
)
valid = false
} else {
child_types[index] = types.child_type(value_type, &checker.module.types)
}
}
capture_start = len(ctx.locals^)
unwrap_list: [dynamic]hir.Conditional_Unwrap
unwrap_list.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.addf(
checker.diagnostics,
statement.span,
"'%s' unwrap captures must have distinct names",
keyword,
)
valid = false
} else if id := add_shadow_diagnostic(
checker, statement.span, capture, "capture",
ctx.pkg, ctx.file, ctx.locals^[:capture_start], ctx.loop_labels^[:], ctx.yield_targets^[:],
); id != source.INVALID_DIAGNOSTIC {
diagnostic = id
valid = false
}
local = append_build_local(ctx, capture, child, false, statement.span)
}
if index < len(values) {
append(&unwrap_list, 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 = false
if diagnostic == source.INVALID_DIAGNOSTIC {
diagnostic = checker.module.exprs[guard].diagnostic
}
} else if !types.is_bool(checker.module.exprs[guard].type) &&
!types.is_noreturn(checker.module.exprs[guard].type) {
diagnostic = source.addf(
checker.diagnostics,
checker.ast_module.exprs[statement.guard].span,
"'%s' unwrap guard must be a bool",
keyword,
)
valid = false
}
}
return unwrap_list[:], guard, capture_start, diagnostic, valid
}
build_block :: proc( build_block :: proc(
ctx: ^Build_Ctx, ctx: ^Build_Ctx,
statements: []ast.Stmt_Id, statements: []ast.Stmt_Id,
@@ -11763,101 +11910,8 @@ build_block :: proc(
} }
} }
if len(statement.captures) > 0 { if len(statement.captures) > 0 {
ast_operands: [dynamic]ast.Expr_Id unwraps, guard, capture_start, diagnostic, valid_unwrap :=
ast_operands.allocator = checker.allocator build_conditional_unwrap_header(ctx, statement, "if")
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
}
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
} else if id := add_shadow_diagnostic(
checker, statement.span, capture, "capture",
ctx.pkg, ctx.file, ctx.locals^[:capture_start], ctx.loop_labels^[:], ctx.yield_targets^[:],
); id != source.INVALID_DIAGNOSTIC {
diagnostic = id
valid_unwrap = false
}
local = append_build_local(ctx, capture, child, false, statement.span)
}
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) &&
!types.is_noreturn(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) then_body := build_block(ctx, statement.body, capture_start)
resize(ctx.locals, capture_start) resize(ctx.locals, capture_start)
else_body: []hir.Stmt_Id = nil else_body: []hir.Stmt_Id = nil
@@ -11870,7 +11924,7 @@ build_block :: proc(
kind=.If, kind=.If,
span=statement.span, span=statement.span,
expr=hir.INVALID_EXPR, expr=hir.INVALID_EXPR,
unwraps=unwraps[:], unwraps=unwraps,
guard=guard, guard=guard,
then_body=then_body, then_body=then_body,
else_body=else_body, else_body=else_body,
@@ -11879,7 +11933,7 @@ build_block :: proc(
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
} else { } else {
delete(unwraps) delete(unwraps, checker.allocator)
delete(then_body, checker.allocator) delete(then_body, checker.allocator)
delete(else_body, checker.allocator) delete(else_body, checker.allocator)
append(&checker.module.statements, hir.Stmt{ append(&checker.module.statements, hir.Stmt{
@@ -11893,9 +11947,6 @@ build_block :: proc(
}) })
ctx.problematic^ = true 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)
@@ -11921,16 +11972,27 @@ build_block :: proc(
}) })
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid
case .While: case .While:
condition := build_expr( condition := hir.INVALID_EXPR
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, unwraps: []hir.Conditional_Unwrap = nil
types.BOOL, ctx.pkg, ctx.file, guard := hir.INVALID_EXPR
) capture_start := len(ctx.locals^)
if checker.module.exprs[condition].kind != .Invalid && unwrap_diagnostic := source.INVALID_DIAGNOSTIC
!types.is_bool(checker.module.exprs[condition].type) && valid_unwrap := true
!types.is_noreturn(checker.module.exprs[condition].type) { if len(statement.captures) > 0 {
id := source.add(checker.diagnostics, statement.span, "'while' condition must be a bool") unwraps, guard, capture_start, unwrap_diagnostic, valid_unwrap =
condition = invalid_hir_expr(checker, statement.span, id, types.BOOL) build_conditional_unwrap_header(ctx, statement, "while")
ctx.problematic^ = true } else {
condition = build_expr(
checker, statement.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) &&
!types.is_noreturn(checker.module.exprs[condition].type) {
id := source.add(checker.diagnostics, statement.span, "'while' condition must be a bool")
condition = invalid_hir_expr(checker, statement.span, id, types.BOOL)
ctx.problematic^ = true
}
} }
if id := add_label_shadow_diagnostic(ctx, statement.span, statement.label); if id := add_label_shadow_diagnostic(ctx, statement.span, statement.label);
id != source.INVALID_DIAGNOSTIC { id != source.INVALID_DIAGNOSTIC {
@@ -11944,32 +12006,52 @@ build_block :: proc(
append(ctx.loop_defer_starts, len(ctx.defers^)) append(ctx.loop_defer_starts, len(ctx.defers^))
append(ctx.loop_labels, statement.label) append(ctx.loop_labels, statement.label)
append(ctx.loop_is_loop, true) append(ctx.loop_is_loop, true)
loop_body := build_block(ctx, statement.body) loop_body := build_block(ctx, statement.body, capture_start)
pop(ctx.loop_is_loop) pop(ctx.loop_is_loop)
pop(ctx.loop_labels) pop(ctx.loop_labels)
pop(ctx.loop_defer_starts) pop(ctx.loop_defer_starts)
update := hir.INVALID_STMT update := hir.INVALID_STMT
if statement.update != ast.INVALID_STMT { if statement.update != ast.INVALID_STMT {
update_ast := [1]ast.Stmt_Id{statement.update} update_ast := [1]ast.Stmt_Id{statement.update}
update_body := build_block(ctx, update_ast[:]) update_body := build_block(ctx, update_ast[:], capture_start)
if len(update_body) > 0 { if len(update_body) > 0 {
update = update_body[0] update = update_body[0]
} }
delete(update_body, checker.allocator) delete(update_body, checker.allocator)
} }
resize(ctx.locals, capture_start)
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=.While, append(&checker.module.statements, hir.Stmt{
span=statement.span, kind=.While,
label=statement.label, span=statement.span,
expr=condition, label=statement.label,
then_body=loop_body, expr=condition,
update=update, unwraps=unwraps,
local=hir.INVALID_LOCAL, guard=guard,
target=hir.INVALID_EXPR, then_body=loop_body,
diagnostic=source.INVALID_DIAGNOSTIC, update=update,
}) local=hir.INVALID_LOCAL,
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid target=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
delete(unwraps, checker.allocator)
delete(loop_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=unwrap_diagnostic,
})
ctx.problematic^ = true
}
if condition != hir.INVALID_EXPR {
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid
}
case .For: case .For:
if statement.expand { if statement.expand {
if statement.pointer_capture { if statement.pointer_capture {
+96 -51
View File
@@ -5258,6 +5258,70 @@ ct_exec_assignment :: proc(state: ^Ct_State, statement: ast.Stmt, depth: int) ->
return ct_flow(.Normal), true return ct_flow(.Normal), true
} }
ct_eval_conditional_unwrap :: proc(
state: ^Ct_State,
statement: ast.Stmt,
keyword: string,
depth: int,
) -> (matched: bool, scope_start: int, flow: Ct_Flow, ok: bool) {
checker := state.checker
operands: [dynamic]ast.Expr_Id
operands.allocator = checker.allocator
defer delete(operands)
flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &operands)
if len(operands) != len(statement.captures) {
return false, len(state.bindings), ct_flow(.Normal), ct_failf(
state, .Not_Comptime, statement.span,
"'%s' unwrap capture count mismatch",
keyword,
)
}
scope_start = len(state.bindings)
matched = true
for operand, index in operands {
value, operand_flow, operand_ok := ct_eval_expr(state, operand, types.INVALID, depth+1)
if !operand_ok || operand_flow.kind != .Normal {
ct_pop_bindings(state, scope_start)
return false, scope_start, operand_flow, operand_ok
}
v := state.values[value]
if v.kind == .Null {
matched = false
break
}
if v.kind != .Optional_Some {
ct_pop_bindings(state, scope_start)
return false, scope_start, ct_flow(.Normal), ct_failf(
state, .Not_Comptime, statement.span,
"'%s' unwrap requires an optional value",
keyword,
)
}
children := ct_child_slice(state, v)
if len(children) > 0 && statement.captures[index] != checker.sink_symbol {
ct_bind_value(state, statement.captures[index], state.values[children[0]].type, children[0], false)
}
}
if matched && statement.guard != ast.INVALID_EXPR {
guard, guard_flow, guard_ok := ct_eval_expr(state, statement.guard, types.BOOL, depth+1)
if !guard_ok || guard_flow.kind != .Normal {
ct_pop_bindings(state, scope_start)
return false, scope_start, guard_flow, guard_ok
}
guard_value, bool_ok := ct_bool_value(state, guard)
if !bool_ok {
ct_pop_bindings(state, scope_start)
return false, scope_start, ct_flow(.Normal), ct_failf(
state, .Not_Comptime, statement.span,
"'%s' unwrap guard must be a bool",
keyword,
)
}
matched = guard_value
}
return matched, scope_start, ct_flow(.Normal), true
}
ct_exec_if :: proc(state: ^Ct_State, statement: ast.Stmt, yield_returns: bool, depth: int) -> (Ct_Flow, bool) { ct_exec_if :: proc(state: ^Ct_State, statement: ast.Stmt, yield_returns: bool, depth: int) -> (Ct_Flow, bool) {
checker := state.checker checker := state.checker
if yield_returns && statement.else_body == nil { if yield_returns && statement.else_body == nil {
@@ -5284,47 +5348,9 @@ ct_exec_if :: proc(state: ^Ct_State, statement: ast.Stmt, yield_returns: bool, d
} }
return ct_exec_statements(state, body, false, depth+1) return ct_exec_statements(state, body, false, depth+1)
} }
operands: [dynamic]ast.Expr_Id matched, scope_start, unwrap_flow, unwrap_ok := ct_eval_conditional_unwrap(state, statement, "if", depth)
operands.allocator = checker.allocator if !unwrap_ok || unwrap_flow.kind != .Normal {
defer delete(operands) return unwrap_flow, unwrap_ok
flatten_conditional_unwrap_operands(checker.ast_module, statement.expr, &operands)
if len(operands) != len(statement.captures) {
return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "'if' unwrap capture count mismatch")
}
scope_start := len(state.bindings)
matched := true
for operand, index in operands {
value, flow, ok := ct_eval_expr(state, operand, types.INVALID, depth+1)
if !ok || flow.kind != .Normal {
ct_pop_bindings(state, scope_start)
return flow, ok
}
v := state.values[value]
if v.kind == .Null {
matched = false
break
}
if v.kind != .Optional_Some {
ct_pop_bindings(state, scope_start)
return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "'if' unwrap requires an optional value")
}
children := ct_child_slice(state, v)
if len(children) > 0 && statement.captures[index] != checker.sink_symbol {
ct_bind_value(state, statement.captures[index], state.values[children[0]].type, children[0], false)
}
}
if matched && statement.guard != ast.INVALID_EXPR {
guard, flow, ok := ct_eval_expr(state, statement.guard, types.BOOL, depth+1)
if !ok || flow.kind != .Normal {
ct_pop_bindings(state, scope_start)
return flow, ok
}
guard_value, guard_ok := ct_bool_value(state, guard)
if !guard_ok {
ct_pop_bindings(state, scope_start)
return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "'if' unwrap guard must be a bool")
}
matched = guard_value
} }
body := statement.body if matched else statement.else_body body := statement.body if matched else statement.else_body
flow := ct_flow(.Normal) flow := ct_flow(.Normal)
@@ -5357,34 +5383,53 @@ ct_exec_while :: proc(state: ^Ct_State, statement: ast.Stmt, yield_returns: bool
if !ct_step(state, statement.span) { if !ct_step(state, statement.span) {
return ct_flow(.Normal), false return ct_flow(.Normal), false
} }
condition, flow, ok := ct_eval_expr(state, statement.expr, types.BOOL, depth+1) scope_start := len(state.bindings)
if !ok || flow.kind != .Normal { if len(statement.captures) > 0 {
return flow, ok matched, unwrap_scope_start, flow, ok :=
} ct_eval_conditional_unwrap(state, statement, "while", depth)
value, bool_ok := ct_bool_value(state, condition) scope_start = unwrap_scope_start
if !bool_ok { if !ok || flow.kind != .Normal {
return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "'while' condition must be a bool") return flow, ok
} }
if !value { if !matched {
return ct_flow(.Normal), true ct_pop_bindings(state, scope_start)
return ct_flow(.Normal), true
}
} else {
condition, flow, ok := ct_eval_expr(state, statement.expr, types.BOOL, depth+1)
if !ok || flow.kind != .Normal {
return flow, ok
}
value, bool_ok := ct_bool_value(state, condition)
if !bool_ok {
return ct_flow(.Normal), ct_fail(state, .Not_Comptime, statement.span, "'while' condition must be a bool")
}
if !value {
return ct_flow(.Normal), true
}
} }
body_flow, body_ok := ct_exec_statements(state, statement.body, yield_returns, depth+1) body_flow, body_ok := ct_exec_statements(state, statement.body, yield_returns, depth+1)
if !body_ok { if !body_ok {
ct_pop_bindings(state, scope_start)
return body_flow, false return body_flow, false
} }
if ct_loop_consumes_flow(body_flow, statement.label, false) { if ct_loop_consumes_flow(body_flow, statement.label, false) {
ct_pop_bindings(state, scope_start)
return ct_flow(.Normal), true return ct_flow(.Normal), true
} }
if body_flow.kind != .Normal && !ct_loop_consumes_flow(body_flow, statement.label, true) { if body_flow.kind != .Normal && !ct_loop_consumes_flow(body_flow, statement.label, true) {
ct_pop_bindings(state, scope_start)
return body_flow, true return body_flow, true
} }
if statement.update != ast.INVALID_STMT { if statement.update != ast.INVALID_STMT {
update := [1]ast.Stmt_Id{statement.update} update := [1]ast.Stmt_Id{statement.update}
update_flow, update_ok := ct_exec_statements(state, update[:], false, depth+1) update_flow, update_ok := ct_exec_statements(state, update[:], false, depth+1)
if !update_ok || update_flow.kind != .Normal { if !update_ok || update_flow.kind != .Normal {
ct_pop_bindings(state, scope_start)
return update_flow, update_ok return update_flow, update_ok
} }
} }
ct_pop_bindings(state, scope_start)
} }
} }
+6 -6
View File
@@ -229,12 +229,12 @@ Stmt :: struct {
// by computing the target address once, loading its current value, applying // by computing the target address once, loading its current value, applying
// the operation to `expr`, and storing through the original address. // the operation to `expr`, and storing through the original address.
assignment_op: Assignment_Op, assignment_op: Assignment_Op,
// Boolean `If` statements use `expr` as the condition. Conditional unwraps // Boolean `If` and `While` statements use `expr` as the condition.
// use `unwraps` for the ordered optional expressions and capture locals, and // Conditional unwraps use `unwraps` for the ordered optional expressions
// `guard` for the optional boolean checked after every unwrap succeeds. // and capture locals, and `guard` for the optional boolean checked after
// Both forms use `then_body`/`else_body` as the branch statement lists. // every unwrap succeeds. `If` uses `then_body`/`else_body` as its branches.
// `While` statements use `expr` as the condition, `then_body` as the loop // `While` uses `then_body` as its loop body and `update` as the optional
// body, and `update` as the optional post-iteration statement. // 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.
+106 -97
View File
@@ -924,6 +924,101 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
return last return last
} }
lower_conditional_unwrap_header :: proc(
state: ^State,
statement: hir.Stmt,
success_lbl, false_lbl: i64,
) {
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,
})
next_lbl := fresh_label(state)
append_instruction(state, ir.Instruction{
op=.Cond_Br,
span=statement.span,
type=types.VOID,
a=present,
integer=next_lbl,
target=ir.Ref(u32(false_lbl)),
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Label,
span=statement.span,
type=types.VOID,
integer=next_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=success_lbl,
target=ir.Ref(u32(false_lbl)),
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
append_instruction(state, ir.Instruction{
op=.Br,
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,
})
}
}
lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) { lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
hir_module := state.hir_module hir_module := state.hir_module
for statement_id in statements { for statement_id in statements {
@@ -1136,97 +1231,7 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
false_target := else_lbl if has_else else merge_lbl false_target := else_lbl if has_else else merge_lbl
if len(statement.unwraps) > 0 { if len(statement.unwraps) > 0 {
// Evaluate each optional exactly once, entering the next operand only lower_conditional_unwrap_header(state, statement, then_lbl, false_target)
// 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 { } else {
cond := lower_expr(state, statement.expr) cond := lower_expr(state, statement.expr)
append_instruction(state, ir.Instruction{ append_instruction(state, ir.Instruction{
@@ -1282,12 +1287,16 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
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,
}) })
condition := lower_expr(state, statement.expr) if len(statement.unwraps) > 0 {
append_instruction(state, ir.Instruction{ lower_conditional_unwrap_header(state, statement, body_lbl, exit_lbl)
op=.Cond_Br, span=statement.span, type=types.VOID, } else {
a=condition, integer=body_lbl, target=ir.Ref(u32(exit_lbl)), condition := lower_expr(state, statement.expr)
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, append_instruction(state, ir.Instruction{
}) op=.Cond_Br, span=statement.span, type=types.VOID,
a=condition, integer=body_lbl, target=ir.Ref(u32(exit_lbl)),
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=body_lbl, op=.Label, span=statement.span, type=types.VOID, integer=body_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,
+26 -15
View File
@@ -2229,19 +2229,7 @@ parse_control_body :: proc(parser: ^Parser, header: ast.Expr_Id, diagnostic: str
return single return single
} }
parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id { parse_conditional_captures :: proc(parser: ^Parser) -> ([]symbol.Id, ast.Expr_Id) {
start := advance(parser) // consume 'if'
skip_newlines(parser)
saved := parser.no_struct_literal
parser.no_struct_literal = true
saved_capture_pipe := parser.capture_pipe
saved_if_condition := parser.if_condition
parser.capture_pipe = true
parser.if_condition = true
condition := parse_expression(parser)
parser.if_condition = saved_if_condition
parser.capture_pipe = saved_capture_pipe
parser.no_struct_literal = saved
captures: [dynamic]symbol.Id captures: [dynamic]symbol.Id
captures.allocator = parser.module.allocator captures.allocator = parser.module.allocator
guard := ast.INVALID_EXPR guard := ast.INVALID_EXPR
@@ -2267,9 +2255,9 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id {
if current(parser).kind == .Pipe { if current(parser).kind == .Pipe {
source.add(parser.diagnostics, current(parser).span, "expected a guard expression after ':'") source.add(parser.diagnostics, current(parser).span, "expected a guard expression after ':'")
} else { } else {
saved = parser.no_struct_literal saved := parser.no_struct_literal
parser.no_struct_literal = true parser.no_struct_literal = true
saved_capture_pipe = parser.capture_pipe saved_capture_pipe := parser.capture_pipe
parser.capture_pipe = true parser.capture_pipe = true
guard = parse_expression(parser) guard = parse_expression(parser)
parser.capture_pipe = saved_capture_pipe parser.capture_pipe = saved_capture_pipe
@@ -2280,6 +2268,23 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id {
source.add(parser.diagnostics, current(parser).span, "expected '|' to close unwrap captures") source.add(parser.diagnostics, current(parser).span, "expected '|' to close unwrap captures")
} }
} }
return captures[:], guard
}
parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id {
start := advance(parser) // consume 'if'
skip_newlines(parser)
saved := parser.no_struct_literal
parser.no_struct_literal = true
saved_capture_pipe := parser.capture_pipe
saved_if_condition := parser.if_condition
parser.capture_pipe = true
parser.if_condition = true
condition := parse_expression(parser)
parser.if_condition = saved_if_condition
parser.capture_pipe = saved_capture_pipe
parser.no_struct_literal = saved
captures, guard := parse_conditional_captures(parser)
then_body := parse_control_body( then_body := parse_control_body(
parser, parser,
condition, condition,
@@ -2608,8 +2613,12 @@ parse_while :: proc(parser: ^Parser) -> ast.Stmt_Id {
skip_newlines(parser) skip_newlines(parser)
saved := parser.no_struct_literal saved := parser.no_struct_literal
parser.no_struct_literal = true parser.no_struct_literal = true
saved_capture_pipe := parser.capture_pipe
parser.capture_pipe = true
condition := parse_expression(parser) condition := parse_expression(parser)
parser.capture_pipe = saved_capture_pipe
parser.no_struct_literal = saved parser.no_struct_literal = saved
captures, guard := parse_conditional_captures(parser)
skip_newlines(parser) skip_newlines(parser)
update := ast.INVALID_STMT update := ast.INVALID_STMT
@@ -2631,6 +2640,8 @@ parse_while :: proc(parser: ^Parser) -> ast.Stmt_Id {
span=span_from(start.span, previous(parser).span), span=span_from(start.span, previous(parser).span),
expr=condition, expr=condition,
body=body, body=body,
captures=captures,
guard=guard,
label=label, label=label,
update=update, update=update,
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
+33
View File
@@ -11514,6 +11514,39 @@ braceless_while_compiles_and_runs :: proc(t: ^testing.T) {
testing.expect_value(t, state.exit_code, 42) testing.expect_value(t, state.exit_code, 42)
} }
@(test)
while_unwrap_compiles_and_runs_at_runtime_and_comptime :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-while-unwrap"
main_path := "/tmp/brolang-test-while-unwrap/main.bro"
output := "/tmp/brolang-test-while-unwrap-output"
text := `next func(index @mut usize, limit usize) ?i32 {
if (index^ == limit) return null
index^ += 1
return i32(index^)
}
sum func(limit, cap usize) i32 {
index usize := 0
total i32 := 0
while next(&index, limit) |value : usize(value) <= cap| : total += value {}
return total
}
COMPTIME_SUM :: $sum(4, 4)
COMPTIME_GUARDED :: $sum(5, 3)
main func() i32 {
if COMPTIME_SUM != 10 or COMPTIME_GUARDED != 6 { return 1 }
return sum(4, 4) + sum(5, 3) - 16
}
`
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test) @(test)
parser_diagnoses_braceless_for_and_unwrap_without_parens_or_call :: proc(t: ^testing.T) { parser_diagnoses_braceless_for_and_unwrap_without_parens_or_call :: proc(t: ^testing.T) {
text := `main func() void { text := `main func() void {