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
+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.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 {
+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
}
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)
}
}