conditional optional unwrapping

This commit is contained in:
2026-06-21 20:38:12 +02:00
parent c90ada608e
commit f4194492cc
10 changed files with 391 additions and 12 deletions
+69 -8
View File
@@ -58,7 +58,6 @@ Build_Ctx :: struct {
global_reads: ^[dynamic]hir.Global_Id,
calls: ^[dynamic]hir.Function_Id,
problematic: ^bool,
has_return: ^bool,
}
Constant_Kind :: enum {
@@ -1412,9 +1411,19 @@ infer_statements :: proc(
}
}
case .If:
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result)
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
if statement.name != symbol.INVALID {
// Conditional unwrap: `v` is in scope (with the unwrapped type) only inside the then-block.
child := types.child_type(value_type, &checker.module.types) if types.is_optional(value_type, &checker.module.types) else types.INVALID
binding_start := len(locals^)
append(locals, Infer_Local{name = statement.name, type = child})
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
resize(locals, binding_start)
infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result)
} else {
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result)
}
}
}
resize(locals, scope_start)
@@ -2949,7 +2958,6 @@ build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
case .Return:
ctx.has_return^ = true
if statement.expr == ast.INVALID_EXPR {
if !types.is_void(ctx.result) {
id := source.add(checker.diagnostics, statement.span, "'return _' is only valid in a void function")
@@ -3007,6 +3015,39 @@ build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id
})
}
case .If:
if statement.name != symbol.INVALID {
// Conditional unwrap `if opt |v| { ... }`: `expr` is the optional, `v`
// binds the unwrapped value (immutable) for the duration of the then-block.
value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file)
value_type := checker.module.exprs[value].type
child := types.INVALID
if checker.module.exprs[value].kind != .Invalid && !types.is_optional(value_type, &checker.module.types) {
id := source.add(checker.diagnostics, statement.span, "'if' unwrap requires an optional value")
value = invalid_hir_expr(checker, statement.span, id)
ctx.problematic^ = true
} else if checker.module.exprs[value].kind != .Invalid {
child = types.child_type(value_type, &checker.module.types)
}
binding := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name = statement.name, type = child, mutable = false})
locals_before := len(ctx.locals^)
append(ctx.locals, Build_Local{name = statement.name, type = child, mutable = false, id = binding})
then_body := build_block(ctx, statement.body)
resize(ctx.locals, locals_before)
else_body: []hir.Stmt_Id = nil
if statement.else_body != nil {
else_body = build_block(ctx, statement.else_body)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .If, span = statement.span, expr = value,
then_body = then_body, else_body = else_body,
local = binding, target = hir.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
continue
}
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) {
id := source.add(checker.diagnostics, statement.span, "'if' condition must be a bool")
@@ -3039,6 +3080,27 @@ build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id
return body[:]
}
// Reports whether every control-flow path through `stmts` terminates (returns or traps),
// so the end of the block is unreachable. A `.Return` or `.Trap` terminates outright; an
// `.If` terminates only when it has an `else` and both arms terminate. Recursion into the
// `then_body`/`else_body` slices handles nested ifs and `else if` chains.
all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
for id in stmts {
statement := module.statements[id]
#partial switch statement.kind {
case .Return, .Trap:
return true
case .If:
if statement.else_body != nil &&
all_paths_return(module, statement.then_body) &&
all_paths_return(module, statement.else_body) {
return true
}
}
}
return false
}
build_function :: proc(checker: ^Checker, id: Spec_Id) {
spec := checker.specs[id]
function := checker.ast_module.functions[spec.template]
@@ -3118,7 +3180,6 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
return
}
has_return := false
if signature_diagnostic != source.INVALID_DIAGNOSTIC {
append(&body, hir.stmt_id(len(checker.module.statements)))
append(
@@ -3142,15 +3203,15 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
global_reads = &global_reads,
calls = &calls,
problematic = &problematic,
has_return = &has_return,
}
block := build_block(&ctx, function.body)
returns := all_paths_return(&checker.module, block)
for block_stmt in block {
append(&body, block_stmt)
}
delete(block, checker.allocator)
if !types.is_void(spec.result) && !has_return {
if !types.is_void(spec.result) && !returns {
id := source.addf(
checker.diagnostics,
function.span,