diff --git a/LANGUAGE.md b/LANGUAGE.md index 18a17de..2ca386b 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -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 - 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 -- 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 - 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 @@ -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 - 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 -- `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 - `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 @@ -154,9 +154,9 @@ and or ``` -Each level is left-associative. Because `|` also delimits `if` and `for` captures, a bitwise-OR -header expression must be parenthesized before a capture list, for example -`if (flags | mask) |value| { ... }`. +Each level is left-associative. Because `|` also delimits `if`, `while`, and `for` captures, a +bitwise-OR header expression must be parenthesized before a capture list, for example +`while (flags | mask) |value| { ... }`. #### division diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 8b397e8..5de72c8 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -202,13 +202,12 @@ Stmt :: struct { assignment_op: Assignment_Op, target: Expr_Id, expr: Expr_Id, - // `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. + // `If` and `While` statements use `expr` as the condition, `captures` as + // optional unwrap binding names, and `guard` as the optional post-unwrap + // boolean condition. `If` uses `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` uses `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. diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index f125c72..ca829af 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -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.else_body) case .While: - append(&expr_stack, statement.expr) + append(&expr_stack, statement.expr, statement.guard) append(&statement_stack, ..statement.body) if statement.update != ast.INVALID_STMT { 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) case .While: 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) if statement.update != ast.INVALID_STMT { update := [1]ast.Stmt_Id{statement.update} @@ -6309,12 +6312,40 @@ infer_statements :: proc( } } 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) if statement.update != ast.INVALID_STMT { update := [1]ast.Stmt_Id{statement.update} infer_statements(checker, update[:], locals, local_types, pkg, file, demanded, result, result_hint) } + resize(locals, capture_start) case .For: if statement.expand { bindings, expand_error := expand_field_bindings(checker, statement.expr, statement.name, pkg, file) @@ -11042,6 +11073,122 @@ flatten_expand_iteration :: proc( 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( ctx: ^Build_Ctx, statements: []ast.Stmt_Id, @@ -11763,101 +11910,8 @@ build_block :: proc( } } 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 - } - - 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 - } - } - + unwraps, guard, capture_start, diagnostic, valid_unwrap := + build_conditional_unwrap_header(ctx, statement, "if") then_body := build_block(ctx, statement.body, capture_start) resize(ctx.locals, capture_start) else_body: []hir.Stmt_Id = nil @@ -11870,7 +11924,7 @@ build_block :: proc( kind=.If, span=statement.span, expr=hir.INVALID_EXPR, - unwraps=unwraps[:], + unwraps=unwraps, guard=guard, then_body=then_body, else_body=else_body, @@ -11879,7 +11933,7 @@ build_block :: proc( diagnostic=source.INVALID_DIAGNOSTIC, }) } else { - delete(unwraps) + delete(unwraps, checker.allocator) delete(then_body, checker.allocator) delete(else_body, checker.allocator) append(&checker.module.statements, hir.Stmt{ @@ -11893,9 +11947,6 @@ build_block :: proc( }) 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) @@ -11921,16 +11972,27 @@ build_block :: proc( }) ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid case .While: - 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 + condition := hir.INVALID_EXPR + unwraps: []hir.Conditional_Unwrap = nil + guard := hir.INVALID_EXPR + capture_start := len(ctx.locals^) + unwrap_diagnostic := source.INVALID_DIAGNOSTIC + valid_unwrap := true + if len(statement.captures) > 0 { + unwraps, guard, capture_start, unwrap_diagnostic, valid_unwrap = + build_conditional_unwrap_header(ctx, statement, "while") + } 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); id != source.INVALID_DIAGNOSTIC { @@ -11944,32 +12006,52 @@ build_block :: proc( append(ctx.loop_defer_starts, len(ctx.defers^)) append(ctx.loop_labels, statement.label) 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_labels) pop(ctx.loop_defer_starts) update := hir.INVALID_STMT if statement.update != ast.INVALID_STMT { 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 { update = update_body[0] } delete(update_body, checker.allocator) } + resize(ctx.locals, capture_start) append(&body, hir.stmt_id(len(checker.module.statements))) - append(&checker.module.statements, hir.Stmt{ - kind=.While, - span=statement.span, - label=statement.label, - expr=condition, - then_body=loop_body, - update=update, - local=hir.INVALID_LOCAL, - target=hir.INVALID_EXPR, - diagnostic=source.INVALID_DIAGNOSTIC, - }) - ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid + if valid_unwrap { + append(&checker.module.statements, hir.Stmt{ + kind=.While, + span=statement.span, + label=statement.label, + expr=condition, + unwraps=unwraps, + guard=guard, + then_body=loop_body, + update=update, + local=hir.INVALID_LOCAL, + 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: if statement.expand { if statement.pointer_capture { diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 0f0e2b4..254a0f3 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -5258,6 +5258,70 @@ ct_exec_assignment :: proc(state: ^Ct_State, statement: ast.Stmt, depth: int) -> 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) { checker := state.checker 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) } - 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 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 + matched, scope_start, unwrap_flow, unwrap_ok := ct_eval_conditional_unwrap(state, statement, "if", depth) + if !unwrap_ok || unwrap_flow.kind != .Normal { + return unwrap_flow, unwrap_ok } body := statement.body if matched else statement.else_body 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) { return ct_flow(.Normal), false } - 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 + scope_start := len(state.bindings) + if len(statement.captures) > 0 { + matched, unwrap_scope_start, flow, ok := + ct_eval_conditional_unwrap(state, statement, "while", depth) + scope_start = unwrap_scope_start + if !ok || flow.kind != .Normal { + return flow, ok + } + if !matched { + 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) if !body_ok { + ct_pop_bindings(state, scope_start) return body_flow, false } if ct_loop_consumes_flow(body_flow, statement.label, false) { + ct_pop_bindings(state, scope_start) return ct_flow(.Normal), 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 } if statement.update != ast.INVALID_STMT { update := [1]ast.Stmt_Id{statement.update} update_flow, update_ok := ct_exec_statements(state, update[:], false, depth+1) if !update_ok || update_flow.kind != .Normal { + ct_pop_bindings(state, scope_start) return update_flow, update_ok } } + ct_pop_bindings(state, scope_start) } } diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 5bb38c3..4c17874 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -229,12 +229,12 @@ Stmt :: struct { // by computing the target address once, loading its current value, applying // the operation to `expr`, and storing through the original address. assignment_op: Assignment_Op, - // 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. + // Boolean `If` and `While` 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. `If` uses `then_body`/`else_body` as its branches. + // `While` uses `then_body` as its 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. diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index cc25325..4c26c8a 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -924,6 +924,101 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id { 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) { hir_module := state.hir_module 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 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, - }) - } + lower_conditional_unwrap_header(state, statement, then_lbl, false_target) } else { cond := lower_expr(state, statement.expr) 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, diagnostic=source.INVALID_DIAGNOSTIC, }) - condition := lower_expr(state, statement.expr) - 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, - }) + if len(statement.unwraps) > 0 { + lower_conditional_unwrap_header(state, statement, body_lbl, exit_lbl) + } else { + condition := lower_expr(state, statement.expr) + 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{ op=.Label, span=statement.span, type=types.VOID, integer=body_lbl, target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 584f8e7..2b2c4b6 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -2229,19 +2229,7 @@ parse_control_body :: proc(parser: ^Parser, header: ast.Expr_Id, diagnostic: str return single } -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 +parse_conditional_captures :: proc(parser: ^Parser) -> ([]symbol.Id, ast.Expr_Id) { captures: [dynamic]symbol.Id captures.allocator = parser.module.allocator guard := ast.INVALID_EXPR @@ -2267,9 +2255,9 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id { if current(parser).kind == .Pipe { source.add(parser.diagnostics, current(parser).span, "expected a guard expression after ':'") } else { - saved = parser.no_struct_literal + saved := parser.no_struct_literal parser.no_struct_literal = true - saved_capture_pipe = parser.capture_pipe + saved_capture_pipe := parser.capture_pipe parser.capture_pipe = true guard = parse_expression(parser) 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") } } + 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( parser, condition, @@ -2608,8 +2613,12 @@ parse_while :: proc(parser: ^Parser) -> ast.Stmt_Id { skip_newlines(parser) saved := parser.no_struct_literal parser.no_struct_literal = true + saved_capture_pipe := parser.capture_pipe + parser.capture_pipe = true condition := parse_expression(parser) + parser.capture_pipe = saved_capture_pipe parser.no_struct_literal = saved + captures, guard := parse_conditional_captures(parser) skip_newlines(parser) update := ast.INVALID_STMT @@ -2631,6 +2640,8 @@ parse_while :: proc(parser: ^Parser) -> ast.Stmt_Id { span=span_from(start.span, previous(parser).span), expr=condition, body=body, + captures=captures, + guard=guard, label=label, update=update, diagnostic=source.INVALID_DIAGNOSTIC, diff --git a/compiler_tests.odin b/compiler_tests.odin index 9c7ccbd..efe87a4 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -11514,6 +11514,39 @@ braceless_while_compiles_and_runs :: proc(t: ^testing.T) { 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) parser_diagnoses_braceless_for_and_unwrap_without_parens_or_call :: proc(t: ^testing.T) { text := `main func() void {