error handling kickoff
This commit is contained in:
+246
-23
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user