error handling kickoff

This commit is contained in:
2026-06-29 22:55:56 +02:00
parent 98c303d22c
commit ae5af37b85
14 changed files with 1229 additions and 173 deletions
+246 -23
View File
@@ -167,6 +167,7 @@ Checker :: struct {
cycle_stack: [dynamic]Cycle_Frame,
main_symbol: symbol.Id,
sink_symbol: symbol.Id,
current_result: types.Type,
target: target.Target,
allocator: mem.Allocator,
}
@@ -322,6 +323,14 @@ type_from_syntax :: proc(value: ast.Type_Syntax) -> types.Type {
return value
}
function_channel_type :: proc(checker: ^Checker, function: ast.Function) -> types.Type {
result := type_from_syntax(function.result)
if types.is_valid(function.error) {
return types.fallible(&checker.module.types, result, type_from_syntax(function.error))
}
return result
}
is_runtime_type :: proc(checker: ^Checker, value: types.Type) -> bool {
return types.is_runtime_value(value, &checker.module.types)
}
@@ -609,11 +618,15 @@ add_unsupported_type_diagnostic :: proc(
if types.is_valid(item.child) {
return add_unsupported_type_diagnostic(checker, span, item.child, depth+1)
}
if types.is_valid(item.extra) {
return add_unsupported_type_diagnostic(checker, span, item.extra, depth+1)
}
return source.INVALID_DIAGNOSTIC
}
function_signatures_equal :: proc(left, right: ast.Function) -> bool {
if left.result != right.result || left.variadic != right.variadic || len(left.params) != len(right.params) {
if left.result != right.result || left.error != right.error ||
left.variadic != right.variadic || len(left.params) != len(right.params) {
return false
}
for param, index in left.params {
@@ -665,7 +678,7 @@ function_value_signature :: proc(
return nil, types.INVALID, false
}
function := checker.ast_module.functions[template]
if !function.c_abi {
if !function.c_abi || types.is_valid(function.error) {
return nil, types.INVALID, false
}
result = type_from_syntax(function.result)
@@ -744,8 +757,14 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as
if expr.left != ast.INVALID_EXPR {
append(&stack, expr.left)
}
case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Keyed:
case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed:
append(&stack, expr.left)
case .Catch:
append(&stack, expr.left)
if expr.right != ast.INVALID_EXPR {
append(&stack, expr.right)
}
mark_block_imports_used(checker, expr.body, file)
case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
append(&stack, expr.left, expr.right)
case .Invalid, .Integer, .Float, .String, .Bool, .None, .Undefined, .Name, .Enum_Literal:
@@ -916,6 +935,24 @@ validate_declarations :: proc(checker: ^Checker) {
symbol_text(checker, function.name),
)
}
if types.is_valid(function.error) {
error_type := type_from_syntax(function.error)
error_sum := types.is_enum(error_type, &checker.module.types) ||
types.is_tagged_union(error_type, &checker.module.types)
if function.c_abi {
checker.template_diagnostics[function_id] = source.add(
checker.diagnostics,
function.span,
"fallible functions must use 'func', not 'c_func'",
)
} else if !error_sum {
checker.template_diagnostics[function_id] = source.add(
checker.diagnostics,
function.span,
"fallible function error type must be a native enum or tagged union",
)
}
}
if !function.has_body && !function.c_abi {
checker.template_diagnostics[function_id] = source.addf(
checker.diagnostics,
@@ -1072,15 +1109,15 @@ validate_type_nodes :: proc(checker: ^Checker) {
)
}
}
// A tagged union (`item.child` set) must tag with an enum, and each variant
// must name a member of it. The synthesized `union(enum)` tag satisfies this
// by construction; the check guards the explicit `union(Enum)` form.
// A tagged union stores a hidden runtime tag enum keyed by global variant IDs.
// An explicit `union(Enum)` keeps that declared enum only for validation.
if item.kind == .Union && types.is_valid(item.child) {
if !types.is_enum(item.child, &checker.module.types) {
declared_tag := types.union_declared_tag_enum(id, &checker.module.types)
if !types.is_enum(declared_tag, &checker.module.types) {
source.add(checker.diagnostics, source.Span{}, "a tagged union's tag must be an enum")
} else {
for field in types.fields_for(&checker.module.types, id) {
if _, ok := find_enum_member(checker, item.child, symbol.Id(field.name)); !ok {
if _, ok := find_enum_member(checker, declared_tag, symbol.Id(field.name)); !ok {
source.addf(
checker.diagnostics,
source.Span{},
@@ -1187,8 +1224,9 @@ ensure_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: [
}
append(&signature, specialized_param_type(checker, param.type, actual))
}
result := type_from_syntax(function.result)
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
result := function_channel_type(checker, function)
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT &&
!types.is_valid(function.error) {
result = types.I32
}
index := spec_id(len(checker.specs))
@@ -1346,6 +1384,31 @@ infer_compound_expr :: proc(
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
return types.child_type(value, store) if types.is_optional(value, store) else types.INVALID
case .Try:
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
return types.fallible_success(value, store) if types.kind(value, store) == .Fallible else types.INVALID
case .Catch:
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
success := types.fallible_success(value, store)
error_type := types.fallible_error(value, store)
if expr.right != ast.INVALID_EXPR {
fallback := infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
if types.is_valid(success) && types.is_valid(fallback) && !types.equal(success, fallback) {
return types.widest(success, fallback)
}
return success if types.is_valid(success) else fallback
}
block_locals: [dynamic]Infer_Local
block_locals.allocator = checker.allocator
defer delete(block_locals)
append(&block_locals, ..locals)
capture_start := len(block_locals)
if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol && types.is_valid(error_type) {
append(&block_locals, Infer_Local{name=expr.name, type=error_type, declared=error_type, statement=ast.INVALID_STMT})
}
infer_statements(checker, expr.body, &block_locals, local_types, pkg, file, demanded, &success, success)
resize(&block_locals, capture_start)
return success
case .Struct_Literal:
for keyed in expr.args {
_ = infer_nested_expr(checker, checker.ast_module.exprs[keyed].left, locals, pkg, file, demanded, local_types)
@@ -1417,7 +1480,7 @@ infer_expr :: proc(
last = types.F64
_ = pop(&stack)
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, .Enum_Literal,
.Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, .Enum_Literal,
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded, local_types)
_ = pop(&stack)
@@ -1556,7 +1619,7 @@ infer_expr :: proc(
continue
}
if checker.template_diagnostics[template] != source.INVALID_DIAGNOSTIC {
declared := type_from_syntax(checker.ast_module.functions[template].result)
declared := function_channel_type(checker, checker.ast_module.functions[template])
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID
_ = pop(&stack)
continue
@@ -1656,12 +1719,13 @@ infer_expr :: proc(
if spec != INVALID_SPEC {
last = checker.specs[spec].result
} else {
declared := type_from_syntax(function.result)
declared := function_channel_type(checker, function)
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID
}
} else {
declared := type_from_syntax(function.result)
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
declared := function_channel_type(checker, function)
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT &&
!types.is_valid(function.error) {
last = types.I32
} else {
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID
@@ -1889,6 +1953,10 @@ infer_statements :: proc(
}
case .Expression:
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
case .Yield:
if statement.expr != ast.INVALID_EXPR {
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
}
case .Return:
if statement.expr != ast.INVALID_EXPR {
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
@@ -2581,6 +2649,23 @@ find_build_local :: proc(locals: []Build_Local, name: symbol.Id) -> (Build_Local
return Build_Local{}, false
}
can_implicitly_convert_type :: proc(checker: ^Checker, actual, expected: types.Type) -> bool {
store := &checker.module.types
if types.equal(actual, expected) ||
types.can_widen(actual, expected) ||
types.can_coerce_c_integer(actual, expected) ||
types.can_weaken_pointer(actual, expected, store) ||
types.can_weaken_slice(actual, expected, store) ||
types.can_decay_array_pointer(actual, expected, store) ||
types.can_sum_widen(actual, expected, store) {
return true
}
if types.is_optional(expected, store) {
return can_implicitly_convert_type(checker, actual, types.child_type(expected, store))
}
return false
}
coerce_expr :: proc(
checker: ^Checker,
expr_id: hir.Expr_Id,
@@ -2627,6 +2712,17 @@ coerce_expr :: proc(
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if types.can_sum_widen(actual, expected, &checker.module.types) {
return add_hir_expr(checker, hir.Expr{
kind=.Sum_Widen,
span=span,
type=expected,
left=expr_id,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if types.is_optional(expected, &checker.module.types) {
child := types.child_type(expected, &checker.module.types)
if types.equal(actual, child) ||
@@ -3049,6 +3145,28 @@ build_nested_expr :: proc(
return result
}
fallible_aggregate :: proc(
checker: ^Checker,
span: source.Span,
channel: types.Type,
value: hir.Expr_Id,
error_path: bool,
) -> hir.Expr_Id {
values := make([]hir.Expr_Id, 1, checker.allocator)
values[0] = value
return add_hir_expr(checker, hir.Expr{
kind=.Struct,
span=span,
type=channel,
integer=1 if error_path else 0,
args=values,
target=hir.INVALID_REF,
left=hir.INVALID_EXPR,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
build_compound_expr :: proc(
checker: ^Checker,
expr: ast.Expr,
@@ -3303,6 +3421,52 @@ build_compound_expr :: proc(
kind=.Orelse, span=expr.span, type=child, left=optional, right=fallback,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Try:
channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
channel_type := checker.module.exprs[channel].type
success := types.fallible_success(channel_type, store)
if !types.is_valid(success) {
id := source.add(checker.diagnostics, expr.span, "'try' requires a fallible expression")
return invalid_hir_expr(checker, expr.span, id)
}
if !types.equal(channel_type, checker.current_result) {
// ponytail: exact channel propagation; add fallible-error widening when cross-error-set try matters.
id := source.add(checker.diagnostics, expr.span, "'try' can only propagate the enclosing function's exact error channel in v1")
return invalid_hir_expr(checker, expr.span, id, success)
}
return add_hir_expr(checker, hir.Expr{
kind=.Try,
span=expr.span,
type=success,
left=channel,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Catch:
channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
channel_type := checker.module.exprs[channel].type
success := types.fallible_success(channel_type, store)
if !types.is_valid(success) {
id := source.add(checker.diagnostics, expr.span, "'catch' requires a fallible expression")
return invalid_hir_expr(checker, expr.span, id)
}
if expr.right == ast.INVALID_EXPR {
// ponytail: catch blocks need Build_Ctx threading through expression build; fallback catch covers v1.
id := source.add(checker.diagnostics, expr.span, "catch block form is not implemented in v1")
return invalid_hir_expr(checker, expr.span, id, success)
}
fallback := build_nested_expr(checker, expr.right, locals, global_reads, calls, success, pkg, file)
fallback = coerce_expr(checker, fallback, success, checker.module.exprs[fallback].span)
return add_hir_expr(checker, hir.Expr{
kind=.Catch,
span=expr.span,
type=success,
left=channel,
right=fallback,
target=hir.INVALID_REF,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Range:
expected_child := types.INVALID
if types.is_range(expected, store) {
@@ -3610,8 +3774,8 @@ build_expr :: proc(
continue
}
switch expr.kind {
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed,
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed,
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range,
.Enum_Literal:
last = build_compound_expr(
@@ -4169,6 +4333,7 @@ build_block :: proc(
close := true,
) -> []hir.Stmt_Id {
checker := ctx.checker
store := &checker.module.types
body: [dynamic]hir.Stmt_Id
body.allocator = checker.allocator
scope_start := len(ctx.locals^)
@@ -4488,7 +4653,16 @@ build_block :: proc(
continue
}
if statement.expr == ast.INVALID_EXPR {
if !types.is_void(ctx.result) {
if types.kind(ctx.result, store) == .Fallible &&
types.is_void(types.fallible_success(ctx.result, store)) {
flush_defers(ctx, &body, 0)
value := fallible_aggregate(checker, statement.span, ctx.result, hir.INVALID_EXPR, false)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind = .Return, span = statement.span, expr = value,
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
})
} else if !types.is_void(ctx.result) {
id := source.add(checker.diagnostics, statement.span, "'return _' is only valid in a void function")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
@@ -4516,11 +4690,57 @@ build_block :: proc(
ctx.problematic^ = true
continue
}
value := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
ctx.result, ctx.pkg, ctx.file,
)
value = coerce_expr(checker, value, ctx.result, statement.span)
value := hir.INVALID_EXPR
if types.kind(ctx.result, store) == .Fallible {
success := types.fallible_success(ctx.result, store)
error_type := types.fallible_error(ctx.result, store)
error_path := false
expr_ast := checker.ast_module.exprs[statement.expr]
if expr_ast.kind == .Enum_Literal {
success_has := types.sum_has_name(store, success, u32(expr_ast.name))
error_has := types.sum_has_name(store, error_type, u32(expr_ast.name))
if error_has && !success_has {
error_path = true
} else if error_has && success_has {
id := source.add(checker.diagnostics, expr_ast.span, "ambiguous fallible return member")
value = invalid_hir_expr(checker, expr_ast.span, id, ctx.result)
}
} else if expr_ast.kind == .Struct_Literal {
target_pkg, available := expr_package(checker, expr_ast, ctx.pkg, ctx.file, true)
named := types.find_named(store, u32(target_pkg), u32(expr_ast.name)) if available else types.INVALID
named = types.resolve_alias(named, store)
error_path = can_implicitly_convert_type(checker, named, error_type)
} else {
probe := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.INVALID, ctx.pkg, ctx.file,
)
probe_type := checker.module.exprs[probe].type
if can_implicitly_convert_type(checker, probe_type, error_type) &&
!can_implicitly_convert_type(checker, probe_type, success) {
error_path = true
value = probe
} else if can_implicitly_convert_type(checker, probe_type, success) {
value = probe
}
}
if value == hir.INVALID_EXPR {
expected := error_type if error_path else success
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
expected, ctx.pkg, ctx.file,
)
}
expected := error_type if error_path else success
value = coerce_expr(checker, value, expected, statement.span)
value = fallible_aggregate(checker, statement.span, ctx.result, value, error_path)
} else {
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
ctx.result, ctx.pkg, ctx.file,
)
value = coerce_expr(checker, value, ctx.result, statement.span)
}
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
// Run deferred statements before returning, but capture the return value
// first (spill it to a temp) so a defer that mutates the returned local
@@ -6506,7 +6726,10 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
loop_is_loop = &loop_is_loop,
yield_targets = &yield_targets,
}
previous_result := checker.current_result
checker.current_result = spec.result
block := build_block(&ctx, function.body)
checker.current_result = previous_result
returns := all_paths_return(&checker.module, block)
for block_stmt in block {
append(&body, block_stmt)