feat: support fallible results and scoped cleanup

This commit is contained in:
2026-10-26 12:37:15 +01:00
parent cd64afbb40
commit 9058135326
15 changed files with 1732 additions and 76 deletions
+580 -15
View File
@@ -28,6 +28,7 @@ TypeKind :: enum {
optional
null
anyopaque
fallible
}
RecordId :: distinct u32
@@ -47,6 +48,7 @@ EnumType :: struct {
backing TypeKind
explicit_backing bool = false
members []EnumMember
composed bool = false
}
RecordField :: struct {
@@ -61,6 +63,7 @@ RecordType :: struct {
is_union bool = false
tag_type Type = Type{}
declared_tag_type Type = Type{}
composed bool = false
}
@hide:file
@@ -77,6 +80,7 @@ ArrayType :: struct {
mutable bool = false
kind TypeKind = .array
sentinel ?i64 = null
error_type Type = Type{}
}
PointerId :: distinct u32
@@ -103,6 +107,7 @@ Type :: struct {
@hide:file
Storage :: struct {
tree @ast.Tree
types []mut Type
arrays arraylist.ArrayList(ArrayType)
pointers arraylist.ArrayList(PointerType)
@@ -118,7 +123,7 @@ Storage :: struct {
}
#! owns node-indexed types, aggregate descriptors, initializer mappings, and function fallthrough obligations; release with deinit.
#! function IDs refer to the caller-owned AST; diagnostic IDs refer to the caller-owned store.
#! borrows the caller-owned AST for signature comparisons; diagnostic IDs refer to the caller-owned store.
Result :: struct { storage Storage }
@hide:file
@@ -141,6 +146,7 @@ Role :: enum {
for_stmt,
loop_capture,
loop_control,
cleanup,
}
@hide:file
@@ -174,6 +180,11 @@ Cell :: struct {
match_pattern bool = false
first_edge usize = no_index
return_annotation ast.NodeId = ast.NodeId(maxval!(u32))
in_cleanup bool = false
narrowing_arm ast.NodeId = ast.NodeId(maxval!(u32))
enclosing_arm ast.NodeId = ast.NodeId(maxval!(u32))
match_owner ast.NodeId = ast.NodeId(maxval!(u32))
narrowed_type Type = Type{}
}
@hide:file
@@ -208,13 +219,15 @@ State :: struct {
variant_slots []mut usize
return_annotation ast.NodeId = ast.NodeId(maxval!(u32))
in_loop bool = false
in_cleanup bool = false
narrowing_arm ast.NodeId = ast.NodeId(maxval!(u32))
slots []mut usize
collecting_assignment bool = false
has_assignments bool = false
pointer_depth usize = 0
}
#! borrows the AST, resolution, and diagnostic store only during checking.
#! borrows resolution and diagnostics during checking; Result retains the AST borrow for signature comparisons.
#! temporary constraints are freed on every exit; Result owns types, aggregate descriptors, initializer mappings, and fallthrough obligations.
check proc(
allocator mem.Allocator,
@@ -327,7 +340,8 @@ check proc(
if (cells[i].role != .expression or !declaration_reference(tree, id)) continue
if (cells[i].type.kind == .invalid) continue
binding :: sema.binding(resolution, id)
if (binding.kind == .declaration and !cells[i].assignment_value and !cells[usize(binding.data.declaration)].fixed) {
if (binding.kind == .declaration and !cells[i].assignment_value
and !cells[usize(binding.data.declaration)].fixed and !has_narrowing(&state, id)) {
join(&state, id, binding.data.declaration)
}
}
@@ -535,6 +549,7 @@ check proc(
function :: ast.function_decl(tree, id)
result :: types[usize(function.return_type)]
if (result.kind == .void or result.kind == .invalid or types[usize(id)].kind == .invalid) continue
if (result.kind == .fallible and state.arrays.items[usize(result.array_type)].element.kind == .void) continue
if (block_falls_through(&state, function.body)) {
# fallthrough is independent of errors in paths that may not execute.
diagnostic :: try diagnostics.add(diagnostic_store, .missing_return, ast.node_span(tree, function.body))
@@ -547,6 +562,7 @@ check proc(
}
}
return Result{ storage = Storage{
tree = tree,
types = types,
arrays = state.arrays,
pointers = state.pointers,
@@ -968,6 +984,9 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b
state.cells[usize(statement)].role = .return_stmt
state.cells[usize(statement)].type = Type{ kind = .void }
state.cells[usize(statement)].return_annotation = return_annotation
if (state.in_cleanup) state.cells[usize(statement)].type = invalid_type(try diagnostics.add(
state.diagnostics, .cleanup_control_flow, ast.node_span(tree, statement),
))
if (ast.return_stmt(tree, statement).value) |value| {
try collect_expression(state, value)
try depend(state, value, statement)
@@ -1051,11 +1070,38 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b
try collect_block(state, loop.body, return_annotation, true)
try depend(state, loop.body, id)
}
.block_stmt: {
body :: ast.statement_block(tree, statement)
state.cells[usize(statement)].role = .body
try collect_block(state, body, return_annotation, in_loop)
try depend(state, body, statement)
}
.defer_stmt, .errdefer_stmt: {
cleanup :: ast.cleanup_stmt(tree, statement)
state.cells[usize(statement)].role = .cleanup
state.cells[usize(statement)].type = Type{ kind = .void }
result :: state.types[usize(return_annotation)]
if (ast.stmt_kind(tree, statement) == .errdefer_stmt and result.kind != .fallible and result.kind != .invalid) {
state.cells[usize(statement)].type = invalid_type(try diagnostics.add(
state.diagnostics, .errdefer_outside_fallible, cleanup.span,
))
}
if (cleanup.capture) |capture| {
try collect_capture(state, capture, cleanup.body)
state.cells[usize(capture)].type = if (result.kind == .fallible)
state.arrays.items[usize(result.array_type)].error_type else result
}
saved_cleanup :: state.in_cleanup
state.in_cleanup = true
try collect_block(state, cleanup.body, return_annotation, false)
state.in_cleanup = saved_cleanup
}
.break_stmt, .continue_stmt: {
state.cells[usize(statement)].role = .loop_control
state.cells[usize(statement)].type = Type{ kind = .void }
if (!in_loop) {
code :: if (ast.stmt_kind(tree, statement) == .break_stmt)
code :: if (state.in_cleanup) diagnostics.Code.cleanup_control_flow
else if (ast.stmt_kind(tree, statement) == .break_stmt)
diagnostics.Code.break_outside_loop
else
diagnostics.Code.continue_outside_loop
@@ -1077,7 +1123,27 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
tree :: state.tree
state.cells[usize(id)].role = .expression
state.cells[usize(id)].assignment_value = state.collecting_assignment
state.cells[usize(id)].return_annotation = state.return_annotation
state.cells[usize(id)].in_cleanup = state.in_cleanup
state.cells[usize(id)].narrowing_arm = state.narrowing_arm
match ast.expr_kind(tree, id) {
.try_expr: {
operand :: ast.type_operand(tree, id)
try collect_expression(state, operand)
try depend(state, operand, id)
}
.catch_expr: {
expression :: ast.catch_expr(tree, id)
try collect_expression(state, expression.operand)
try depend(state, expression.operand, id)
if (expression.capture) |capture| {
try collect_capture(state, capture, expression.handler)
state.cells[usize(capture)].type = Type{}
}
if (expression.handler_is_block) {
try collect_block(state, expression.handler, state.return_annotation, state.in_loop)
} else try collect_expression(state, expression.handler)
}
.literal_int: {
state.cells[usize(id)].numeric = true
state.cells[usize(id)].anchored = true
@@ -1203,7 +1269,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .type_not_value, ast.node_span(tree, id)))
}
.record_type, .union_type, .enum_type, .distinct_type,
.pointer_type, .function_type, .slice_type, .optional_type: {
.pointer_type, .function_type, .slice_type, .optional_type, .fallible_type, .sum_type: {
type := try check_type_expression(state, id, state.cells[usize(id)].nominal)
if (type.kind != .invalid) {
type.value_kind = type.kind
@@ -1437,6 +1503,35 @@ demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Cod
return
}
}
if (expected.kind == .fallible) {
descriptor :: state.arrays.items[usize(expected.array_type)]
actual := group_type(state, id)
if (state.cells[usize(id)].role == .expression and declaration_reference(state.tree, id)
and has_narrowing(state, id)) actual = try infer_shape(state, id, false)
if (actual.kind == .fallible) return
target := descriptor.element
literal :: state.cells[index].literal
if (actual.kind == .none and state.cells[index].expected.kind != .none) actual = state.cells[index].expected
if (literal != ast.NodeId(maxval!(u32))) {
name := ast.NodeId(maxval!(u32))
if (ast.expr_kind(state.tree, literal) == .enum_literal) name = ast.enum_literal(state.tree, literal)
if (ast.expr_kind(state.tree, literal) == .record_literal) {
if (ast.record_literal(state.tree, literal).type) |annotation| {
if (ast.expr_kind(state.tree, annotation) == .enum_literal) name = ast.enum_literal(state.tree, annotation)
}
if (name == ast.NodeId(maxval!(u32)) and actual.kind == .none
and descriptor.element.kind != .record and descriptor.error_type.kind == .record
and !state.records.items[usize(descriptor.error_type.record_type)].is_union) target = descriptor.error_type
}
if (name != ast.NodeId(maxval!(u32)) and type_has_variant(state, descriptor.error_type, name)
and !type_has_variant(state, descriptor.element, name)) target = descriptor.error_type
else if (actual.kind != .none and !assignable_type(state, actual, target)
and assignable_type(state, actual, descriptor.error_type)) target = descriptor.error_type
} else if (actual.kind != .none and !assignable_type(state, actual, target)
and assignable_type(state, actual, descriptor.error_type)) target = descriptor.error_type
try demand(state, id, target, code)
return
}
if (expected.kind == .optional) {
if (group_type(state, id).kind != .optional and group_type(state, id).kind != .null) {
try demand(state, id, state.arrays.items[usize(expected.array_type)].element, code)
@@ -1661,6 +1756,22 @@ same_signature proc(state @State, left, right ast.NodeId) bool {
@hide:file
assignable_type proc(state @State, actual, expected Type) bool {
if (same_type(state, actual, expected)) return true
if (expected.kind == .fallible) {
target :: state.arrays.items[usize(expected.array_type)]
if (actual.kind == .fallible) {
source :: state.arrays.items[usize(actual.array_type)]
return (
assignable_type(state, source.element, target.element)
and error_subset(state.records.items, state.enums.items, source.error_type, target.error_type)
)
}
return (
assignable_type(state, actual, target.element)
or error_subset(state.records.items, state.enums.items, actual, target.error_type)
)
}
if ((composed_type(state, actual) or composed_type(state, expected))
and error_subset(state.records.items, state.enums.items, actual, expected)) return true
if (expected.kind == .optional) {
child :: state.arrays.items[usize(expected.array_type)].element
if (actual.kind == .null) return true
@@ -1691,6 +1802,13 @@ assignable_type proc(state @State, actual, expected Type) bool {
return assignable_type(state, a.element, b.element)
}
@hide:file
composed_type proc(state @State, type Type) bool {
if (type.kind == .enumeration) return state.enums.items[usize(type.enum_type)].composed
if (type.kind == .record) return state.records.items[usize(type.record_type)].composed
return false
}
@hide:file
compatible proc(state @mut State, value ast.NodeId, expected Type) void ! (mem.AllocError | diagnostics.CapacityError) {
actual :: state.types[usize(value)]
@@ -1752,6 +1870,7 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
statement :: ast.return_stmt(tree, id)
expected :: state.types[usize(state.cells[usize(id)].return_annotation)]
if (expected.kind == .invalid) return
if (state.types[usize(id)].kind == .invalid) return
if (statement.value) |value| {
actual :: state.types[usize(value)]
if (actual.kind == .invalid) {
@@ -1760,12 +1879,14 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
try fail(state, value, .return_type_mismatch, ast.node_span(tree, value))
state.types[usize(id)] = state.types[usize(value)]
}
} else if (expected.kind != .void) {
} else if (expected.kind != .void
and !(expected.kind == .fallible and state.arrays.items[usize(expected.array_type)].element.kind == .void)) {
try fail(state, id, .return_type_mismatch, statement.span)
}
}
.expression: {
match ast.expr_kind(tree, id) {
.try_expr, .catch_expr: try validate_fallible_expression(state, id)
.unary: {
expression :: ast.unary_expr(tree, id)
if (expression.operator == .logical_not) {
@@ -1952,6 +2073,7 @@ block_falls_through proc(state @State, id ast.NodeId) bool {
if (state.types[usize(source)].kind == .invalid) return false
match kind {
.return_stmt, .break_stmt, .continue_stmt, .invalid: return false
.block_stmt: if (!block_falls_through(state, ast.statement_block(tree, statement))) return false
.if_stmt: {
conditional :: ast.if_stmt(tree, statement)
then_falls :: block_falls_through(state, conditional.then_block)
@@ -2128,7 +2250,8 @@ check_function_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError
intern_array proc(state @mut State, descriptor ArrayType) Type ! mem.AllocError {
for state.arrays.items |existing, i| {
if (existing.kind == descriptor.kind and existing.len == descriptor.len and existing.mutable == descriptor.mutable
and same_sentinel(existing.sentinel, descriptor.sentinel) and same_type(state, existing.element, descriptor.element)) {
and same_sentinel(existing.sentinel, descriptor.sentinel) and same_type(state, existing.element, descriptor.element)
and same_type(state, existing.error_type, descriptor.error_type)) {
return Type{ kind = descriptor.kind, array_type = ArrayId(i) }
}
}
@@ -2341,7 +2464,14 @@ infer_builtin proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.A
same_type proc(state @State, left, right Type) bool {
if (left.kind != right.kind) return false
if (left.nominal != right.nominal) return false
if (left.kind == .enumeration) return left.enum_type == right.enum_type
if (left.kind == .enumeration) {
if (left.enum_type == right.enum_type) return true
if (!state.enums.items[usize(left.enum_type)].composed and !state.enums.items[usize(right.enum_type)].composed) return false
return (
error_subset(state.records.items, state.enums.items, left, right)
and error_subset(state.records.items, state.enums.items, right, left)
)
}
if (left.kind == .pointer) {
a :: state.pointers.items[usize(left.pointer_type)]
b :: state.pointers.items[usize(right.pointer_type)]
@@ -2359,6 +2489,10 @@ same_type proc(state @State, left, right Type) bool {
if (left.record_type == right.record_type) return true
a :: state.records.items[usize(left.record_type)]
b :: state.records.items[usize(right.record_type)]
if (a.composed or b.composed) return (
error_subset(state.records.items, state.enums.items, left, right)
and error_subset(state.records.items, state.enums.items, right, left)
)
if (a.is_union or b.is_union) return false
if (a.declaration != ast.NodeId(maxval!(u32)) or b.declaration != ast.NodeId(maxval!(u32))
or a.fields.len != b.fields.len) return false
@@ -2372,10 +2506,13 @@ same_type proc(state @State, left, right Type) bool {
}
return true
}
if (left.kind != .array and left.kind != .slice and left.kind != .optional) return true
if (left.kind != .array and left.kind != .slice and left.kind != .optional and left.kind != .fallible) return true
a :: state.arrays.items[usize(left.array_type)]
b :: state.arrays.items[usize(right.array_type)]
return a.len == b.len and a.mutable == b.mutable and same_sentinel(a.sentinel, b.sentinel) and same_type(state, a.element, b.element)
return (
a.len == b.len and a.mutable == b.mutable and same_sentinel(a.sentinel, b.sentinel)
and same_type(state, a.element, b.element) and same_type(state, a.error_type, b.error_type)
)
}
@hide:file
@@ -2397,6 +2534,16 @@ sentinel_bits proc(value ConstantInteger) i64 {
@hide:file
array_cells proc(state @State, type Type) ?usize {
if (type.kind == .void) return usize(0)
if (type.kind == .fallible) {
descriptor :: state.arrays.items[usize(type.array_type)]
success := usize(0)
failure := usize(0)
if (array_cells(state, descriptor.element)) |count| success = count else return null
if (array_cells(state, descriptor.error_type)) |count| failure = count else return null
payload :: if (success > failure) success else failure
if (payload == maxval!(usize)) return null
return payload + 1
}
if (type.kind == .slice) return usize(2)
if (type.kind == .optional) {
child :: state.arrays.items[usize(type.array_type)].element
@@ -2480,6 +2627,14 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
defer state.cells[index].visiting = false
if (state.cells[usize(id)].role == .expression) {
kind :: ast.expr_kind(state.tree, id)
if (declaration_reference(state.tree, id) and has_narrowing(state, id)) {
narrowed :: try narrow_reference(state, id)
if (narrowed.kind != .none) {
seed(state, id, narrowed)
return narrowed
}
}
if (kind == .try_expr or kind == .catch_expr) return try infer_fallible_expression(state, id, defaults)
if (kind == .builtin_call) return try infer_builtin(state, id, defaults)
if (kind == .slice) return try infer_slice(state, id, defaults)
if (kind == .unary and ast.unary_expr(state.tree, id).operator == .unwrap) {
@@ -2727,6 +2882,11 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
@hide:file
prepare_optional_captures proc(state @mut State, defaults bool) void ! (mem.AllocError | diagnostics.CapacityError) {
for 0..(state.cells.len) |i| {
if (state.cells[i].role == .expression and ast.expr_kind(state.tree, ast.NodeId(i)) == .catch_expr) {
_ = try infer_shape(state, ast.NodeId(i), defaults)
}
}
for (0..(state.cells.len)) |i| {
capture :: state.cells[i].capture
if (capture == ast.NodeId(maxval!(u32)) or state.cells[i].role != .expression) continue
@@ -2738,7 +2898,8 @@ prepare_optional_captures proc(state @mut State, defaults bool) void ! (mem.Allo
for (0..(state.cells.len)) |j| {
if (state.cells[j].role != .expression or !declaration_reference(state.tree, ast.NodeId(j))) continue
binding :: sema.binding(state.resolution, ast.NodeId(j))
if (binding.kind == .declaration and binding.data.declaration == capture) seed(state, ast.NodeId(j), child)
if (binding.kind == .declaration and binding.data.declaration == capture
and !has_narrowing(state, ast.NodeId(j))) seed(state, ast.NodeId(j), child)
}
}
}
@@ -2832,7 +2993,7 @@ apply_contexts proc(state @mut State, assignment_values, early bool) void ! (mem
@hide:file
context_demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Code, early bool) void ! (mem.AllocError | diagnostics.CapacityError) {
if (early and expected.kind != .array and expected.kind != .record
and expected.kind != .pointer and expected.kind != .slice and expected.kind != .optional
and expected.kind != .pointer and expected.kind != .slice and expected.kind != .optional and expected.kind != .fallible
and expected.kind != .enumeration and !integer_kind(expected.kind)) return
try demand(state, id, expected, code)
}
@@ -2887,7 +3048,8 @@ prepare_ranges proc(state @mut State, assignment_values, constrain bool) void !
or !declaration_reference(tree, id)
) continue
binding :: sema.binding(state.resolution, id)
if (binding.kind == .declaration and state.cells[usize(binding.data.declaration)].role == .loop_capture) {
if (binding.kind == .declaration and state.cells[usize(binding.data.declaration)].role == .loop_capture
and !has_narrowing(state, id)) {
seed(state, id, group_type(state, binding.data.declaration))
}
}
@@ -2948,7 +3110,8 @@ specialize_annotations proc(state @mut State) void ! (mem.AllocError | diagnosti
or !declaration_reference(state.tree, id)
) continue
binding :: sema.binding(state.resolution, id)
if (binding.kind == .declaration and state.cells[usize(binding.data.declaration)].fixed) {
if (binding.kind == .declaration and state.cells[usize(binding.data.declaration)].fixed
and !has_narrowing(state, id)) {
seed(state, id, state.cells[usize(binding.data.declaration)].type)
}
}
@@ -3217,6 +3380,7 @@ type_syntax proc(kind ast.ExprKind) bool {
kind == .record_type or kind == .union_type or kind == .enum_type
or kind == .distinct_type or kind == .alias_expr
or kind == .pointer_type or kind == .function_type or kind == .slice_type or kind == .optional_type
or kind == .fallible_type or kind == .sum_type
)
}
@@ -3248,6 +3412,8 @@ check_type_expression proc(state @mut State, id ast.NodeId, nominal bool) Type !
if (kind == .pointer_type) return try check_pointer_type(state, id)
if (kind == .function_type) return try check_function_type(state, id)
if (kind == .slice_type or kind == .optional_type) return try check_container_type(state, id)
if (kind == .fallible_type) return try check_fallible_type(state, id)
if (kind == .sum_type) return try check_sum_type(state, id)
if (kind == .union_type) {
type :: try check_union(state, id, nominal)
state.cells[usize(id)].type = type
@@ -3662,11 +3828,16 @@ collect_match proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | dia
if (binding.kind == .invalid) state.cells[usize(capture)].type = invalid_type(binding.data.diagnostic)
try depend(state, capture, arm.body)
}
saved_arm :: state.narrowing_arm
state.cells[usize(arm_id)].enclosing_arm = saved_arm
state.cells[usize(arm_id)].match_owner = id
state.narrowing_arm = arm_id
if (arm.expression) {
try collect_expression(state, arm.body)
} else {
try collect_block(state, arm.body, state.return_annotation, state.in_loop)
}
state.narrowing_arm = saved_arm
}
}
@@ -3733,7 +3904,8 @@ infer_match proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
for 0..(state.cells.len) |j| {
if (state.cells[j].role != .expression or !declaration_reference(state.tree, ast.NodeId(j))) continue
binding :: sema.binding(state.resolution, ast.NodeId(j))
if (binding.kind == .declaration and binding.data.declaration == capture) seed(state, ast.NodeId(j), payload)
if (binding.kind == .declaration and binding.data.declaration == capture
and !has_narrowing(state, ast.NodeId(j))) seed(state, ast.NodeId(j), payload)
}
}
}
@@ -3940,3 +4112,396 @@ global_variant proc(state @mut State, name ast.NodeId, payload Type) u64 ! mem.A
try arraylist.append(&state.variants, Variant{ name = name, payload = payload })
return u64(state.variants.items.len)
}
#! applies the same value-type coercion rules used for storage, arguments, and returns.
value_assignable proc(result @Result, actual, expected Type) bool {
# assignability reads only descriptors, signature annotations, and the borrowed AST.
state State := undefined
state.tree = result.storage.tree
state.types = result.storage.types
state.arrays = result.storage.arrays
state.pointers = result.storage.pointers
state.records = result.storage.records
state.enums = result.storage.enums
return assignable_type(&state, actual, expected)
}
#! tests source-error subset membership using the descriptors retained by checking.
error_assignable proc(result @Result, actual, expected Type) bool {
return error_subset(result.storage.records.items, result.storage.enums.items, actual, expected)
}
@hide:file
error_subset proc(records []RecordType, enums []EnumType, actual, expected Type) bool {
if (actual.nominal != expected.nominal) return false
if (actual.kind == .record and expected.kind == .record and actual.record_type == expected.record_type) return true
if (actual.kind == .enumeration and expected.kind == .enumeration and actual.enum_type == expected.enum_type) return true
target := expected
if (expected.kind == .record) {
descriptor :: records[usize(expected.record_type)]
if (!descriptor.is_union or descriptor.tag_type.kind != .enumeration) return false
if (descriptor.composed and actual.kind == .record and !records[usize(actual.record_type)].is_union) {
for descriptor.fields |field| {
if (field.type.kind == .record and field.type.record_type == actual.record_type
and field.type.nominal == actual.nominal) return true
}
return false
}
target = descriptor.tag_type
}
if (target.kind != .enumeration) return false
destination :: enums[usize(target.enum_type)]
source := actual
if (actual.kind == .record) {
descriptor :: records[usize(actual.record_type)]
if (!descriptor.is_union or descriptor.tag_type.kind != .enumeration) return false
source = descriptor.tag_type
}
if (source.kind != .enumeration) return false
origin :: enums[usize(source.enum_type)]
if (origin.explicit_backing or destination.explicit_backing) {
if (!origin.explicit_backing or !destination.explicit_backing
or origin.declaration != destination.declaration or origin.backing != destination.backing) return false
}
for origin.members |member| {
found := false
for destination.members |other| {
if (member.magnitude == other.magnitude and member.negative == other.negative) found = true
}
if (!found) return false
}
return true
}
@hide:file
type_has_variant proc(state @State, type Type, name ast.NodeId) bool {
if (type.kind == .enumeration) return enum_member_slot(state, state.enums.items[usize(type.enum_type)], name) != no_index
if (type.kind != .record) return false
descriptor :: state.records.items[usize(type.record_type)]
return (
descriptor.is_union and descriptor.tag_type.kind == .enumeration
and named_field(state.tree, descriptor.fields, name) != no_index
)
}
@hide:file
check_fallible_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError | diagnostics.CapacityError) {
cached := state.cells[usize(id)].type
if (cached.kind == .type_value) cached.kind = cached.value_kind
if (cached.kind != .none) return cached
expression :: ast.binary_expr(state.tree, id)
success :: try check_annotation(state, expression.left, true)
failure :: try check_annotation(state, expression.right, false)
if (success.kind == .invalid) return success
if (failure.kind == .invalid) return failure
valid_error :: failure.kind == .enumeration or (failure.kind == .record
and (!state.records.items[usize(failure.record_type)].is_union
or state.records.items[usize(failure.record_type)].tag_type.kind == .enumeration))
if (!valid_error or success.kind == .none or success.kind == .range or success.kind == .type_value
or success.kind == .anyopaque or has_inferred_count(state, success)) {
return invalid_type(try diagnostics.add(state.diagnostics, .invalid_fallible_type, ast.node_span(state.tree, id)))
}
type :: try intern_array(state, ArrayType{ kind = .fallible, element = success, error_type = failure })
state.cells[usize(id)].type = type
return type
}
@hide:file
SumVariant :: struct {
name ast.NodeId
payload Type
tag u64
}
@hide:file
append_sum_variants proc(state @mut State, variants @mut arraylist.ArrayList(SumVariant), type Type) bool ! mem.AllocError {
if (type.kind == .enumeration) {
descriptor :: state.enums.items[usize(type.enum_type)]
if (descriptor.explicit_backing) return false
for (descriptor.members) |member| try arraylist.append(variants, SumVariant{
name = member.name, payload = Type{ kind = .void }, tag = member.magnitude,
})
return true
}
if (type.kind != .record or type.nominal != ast.NodeId(maxval!(u32))) return false
descriptor :: state.records.items[usize(type.record_type)]
if (descriptor.is_union) {
if (descriptor.tag_type.kind != .enumeration) return false
members :: state.enums.items[usize(descriptor.tag_type.enum_type)].members
for (descriptor.fields) |field, i| try arraylist.append(variants, SumVariant{
name = field.name?, payload = field.type, tag = members[i].magnitude,
})
return true
}
name := ast.NodeId(maxval!(u32))
for 0..ast.package_decl_count(state.tree) |i| {
declaration :: ast.package_decl(state.tree, i)
if (ast.decl_kind(state.tree, declaration) == .statement
and ast.statement_decl(state.tree, declaration).initializer == descriptor.declaration) name = declaration
}
if (name == ast.NodeId(maxval!(u32))) {
for 0..(state.cells.len) |i| {
if (state.cells[i].role != .declaration) continue
if (ast.statement_decl(state.tree, ast.NodeId(i)).initializer == descriptor.declaration) name = ast.NodeId(i)
}
}
if (name == ast.NodeId(maxval!(u32))) return false
try arraylist.append(variants, SumVariant{ name = name, payload = type, tag = try global_variant(state, name, type) })
return true
}
@hide:file
check_sum_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError | diagnostics.CapacityError) {
cached := state.cells[usize(id)].type
if (cached.kind == .type_value) cached.kind = cached.value_kind
if (cached.kind != .none) return cached
if (state.cells[usize(id)].record_checking) {
return invalid_type(try diagnostics.add(state.diagnostics, .recursive_record, ast.node_span(state.tree, id)))
}
state.cells[usize(id)].record_checking = true
defer state.cells[usize(id)].record_checking = false
expression :: ast.binary_expr(state.tree, id)
left :: try check_annotation(state, expression.left, false)
right :: try check_annotation(state, expression.right, false)
if (left.kind == .invalid) return left
if (right.kind == .invalid) return right
variants arraylist.ArrayList(SumVariant) := arraylist.init(state.records.allocator)
defer arraylist.deinit(&variants)
valid := try append_sum_variants(state, &variants, left)
if (valid) valid = try append_sum_variants(state, &variants, right)
if (!valid) {
return invalid_type(try diagnostics.add(state.diagnostics, .invalid_sum_type, ast.node_span(state.tree, id)))
}
count := usize(0)
all_void := true
for 0..(variants.items.len) |i| {
variant :: variants.items[i]
duplicate := false
for variants.items[0..count] |previous| {
if (ast.name_id(state.tree, previous.name) != ast.name_id(state.tree, variant.name)) continue
if (previous.tag != variant.tag) {
return invalid_type(try diagnostics.add(state.diagnostics, .invalid_sum_type, ast.node_span(state.tree, id)))
}
duplicate = true
}
if (duplicate) continue
variants.items[count] = variant
count += 1
if (variant.payload.kind != .void) all_void = false
}
members :: try mem.alloc(EnumMember, state.enums.allocator, count)
members_owned := true
defer if (members_owned) mem.free(state.enums.allocator, members)
for variants.items[0..count] |variant, i| {
if (variant.tag > u64(maxval!(u16))) {
return invalid_type(try diagnostics.add(state.diagnostics, .invalid_sum_type, ast.node_span(state.tree, id)))
}
members[i] = EnumMember{ name = variant.name, magnitude = variant.tag }
}
try arraylist.append(&state.enums, EnumType{ declaration = id, backing = .u16, members = members, composed = true })
members_owned = false
tag :: Type{ kind = .enumeration, enum_type = EnumId(state.enums.items.len - 1) }
type := tag
if (!all_void) {
fields :: try mem.alloc(RecordField, state.records.allocator, count)
errdefer mem.free(state.records.allocator, fields)
for (variants.items[0..count]) |variant, i| fields[i] = RecordField{
name = variant.name, type = variant.payload, default = null,
}
try arraylist.append(&state.records, RecordType{
declaration = id, fields = fields, is_union = true, tag_type = tag, composed = true,
})
type = Type{ kind = .record, record_type = RecordId(state.records.items.len - 1) }
}
state.cells[usize(id)].type = type
return type
}
@hide:file
seed_capture proc(state @mut State, capture ast.NodeId, type Type) void {
seed(state, capture, type)
for 0..(state.cells.len) |i| {
if (state.cells[i].role != .expression or !declaration_reference(state.tree, ast.NodeId(i))) continue
binding :: sema.binding(state.resolution, ast.NodeId(i))
if (binding.kind == .declaration and binding.data.declaration == capture
and !has_narrowing(state, ast.NodeId(i))) seed(state, ast.NodeId(i), type)
}
}
@hide:file
infer_fallible_expression proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.AllocError | diagnostics.CapacityError) {
catching :: ast.expr_kind(state.tree, id) == .catch_expr
operand :: if (catching) ast.catch_expr(state.tree, id).operand else ast.type_operand(state.tree, id)
source :: try infer_shape(state, operand, defaults)
if (source.kind == .invalid) {
seed(state, id, source)
return source
}
if (source.kind != .fallible) {
if (defaults and source.kind != .none) {
try fail(state, id, .invalid_fallible_operand, ast.node_span(state.tree, operand))
seed(state, id, state.types[usize(id)])
}
return group_type(state, id)
}
descriptor :: state.arrays.items[usize(source.array_type)]
seed(state, id, descriptor.element)
if (catching) {
expression :: ast.catch_expr(state.tree, id)
if (expression.capture) |capture| seed_capture(state, capture, descriptor.error_type)
if (!expression.handler_is_block) {
try demand(state, expression.handler, descriptor.element, .catch_type_mismatch)
_ = try infer_shape(state, expression.handler, defaults)
}
}
return group_type(state, id)
}
@hide:file
validate_fallible_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
catching :: ast.expr_kind(state.tree, id) == .catch_expr
operand :: if (catching) ast.catch_expr(state.tree, id).operand else ast.type_operand(state.tree, id)
source :: state.types[usize(operand)]
if (source.kind == .invalid) return
if (source.kind != .fallible) {
try fail(state, id, .invalid_fallible_operand, ast.node_span(state.tree, operand))
return
}
descriptor :: state.arrays.items[usize(source.array_type)]
if (catching) {
expression :: ast.catch_expr(state.tree, id)
if (expression.handler_is_block) {
if (descriptor.element.kind != .void and block_falls_through(state, expression.handler)) {
try fail(state, expression.handler, .catch_type_mismatch, ast.node_span(state.tree, expression.handler))
}
} else {
actual :: state.types[usize(expression.handler)]
if (actual.kind != .invalid and !assignable_value(state, expression.handler, actual, descriptor.element)) {
try fail(state, expression.handler, .catch_type_mismatch, ast.node_span(state.tree, expression.handler))
}
}
return
}
if (state.cells[usize(id)].in_cleanup) {
try fail(state, id, .cleanup_control_flow, ast.node_span(state.tree, id))
return
}
annotation :: state.cells[usize(id)].return_annotation
if (annotation == ast.NodeId(maxval!(u32))) {
try fail(state, id, .try_outside_fallible, ast.node_span(state.tree, id))
return
}
result :: state.types[usize(annotation)]
if (result.kind == .invalid) return
if (result.kind != .fallible) {
try fail(state, id, .try_outside_fallible, ast.node_span(state.tree, id))
} else if (!error_subset(state.records.items, state.enums.items, descriptor.error_type,
state.arrays.items[usize(result.array_type)].error_type)) {
try fail(state, id, .incompatible_propagated_error, ast.node_span(state.tree, id))
}
}
@hide:file
matching_narrowing_arm proc(state @State, id ast.NodeId) ast.NodeId {
binding :: sema.binding(state.resolution, id)
if (binding.kind != .declaration) return ast.NodeId(maxval!(u32))
target :: binding.data.declaration
if (state.cells[usize(target)].role == .declaration
and ast.statement_decl(state.tree, target).mutability == .variable) return ast.NodeId(maxval!(u32))
arm := state.cells[usize(id)].narrowing_arm
while (arm != ast.NodeId(maxval!(u32))) {
owner :: state.cells[usize(arm)].match_owner
subject :: ast.match_expr(state.tree, owner).subject
original :: sema.binding(state.resolution, subject)
if (original.kind == .declaration and original.data.declaration == binding.data.declaration) return arm
arm = state.cells[usize(arm)].enclosing_arm
}
return ast.NodeId(maxval!(u32))
}
@hide:file
has_narrowing proc(state @State, id ast.NodeId) bool {
return matching_narrowing_arm(state, id) != ast.NodeId(maxval!(u32))
}
@hide:file
arm_names_variant proc(state @State, arm, name ast.NodeId) bool {
for 0..ast.match_pattern_count(state.tree, arm) |i| {
pattern :: ast.match_pattern(state.tree, arm, i)
if (ast.expr_kind(state.tree, pattern) == .enum_literal
and ast.name_id(state.tree, ast.enum_literal(state.tree, pattern)) == ast.name_id(state.tree, name)) return true
}
return false
}
@hide:file
arm_keeps_variant proc(state @State, owner, arm, name ast.NodeId) bool {
if (!ast.match_arm_view(state.tree, arm).catch_all) return arm_names_variant(state, arm, name)
for 0..ast.match_arm_count(state.tree, owner) |i| {
previous :: ast.match_arm(state.tree, owner, i)
if (previous == arm) break
if (ast.match_arm_view(state.tree, previous).catch_all or arm_names_variant(state, previous, name)) return false
}
return true
}
@hide:file
narrow_reference proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError | diagnostics.CapacityError) {
arm :: matching_narrowing_arm(state, id)
if (arm == ast.NodeId(maxval!(u32))) return Type{}
cached :: state.cells[usize(arm)].narrowed_type
if (cached.kind != .none) return cached
owner :: state.cells[usize(arm)].match_owner
subject :: ast.match_expr(state.tree, owner).subject
source :: try infer_shape(state, subject, false)
key := source
if (source.kind == .record) {
descriptor :: state.records.items[usize(source.record_type)]
if (!descriptor.is_union) return source
key = descriptor.tag_type
}
if (key.kind != .enumeration) return source
descriptor :: state.enums.items[usize(key.enum_type)]
count := usize(0)
for descriptor.members |member| {
if (arm_keeps_variant(state, owner, arm, member.name)) count += 1
}
if (count == descriptor.members.len) {
state.cells[usize(arm)].narrowed_type = source
return source
}
members :: try mem.alloc(EnumMember, state.enums.allocator, count)
members_owned := true
defer if (members_owned) mem.free(state.enums.allocator, members)
next := usize(0)
for descriptor.members |member| {
if (!arm_keeps_variant(state, owner, arm, member.name)) continue
members[next] = member
next += 1
}
try arraylist.append(&state.enums, EnumType{
declaration = descriptor.declaration, backing = descriptor.backing,
explicit_backing = descriptor.explicit_backing, members = members, composed = true,
})
members_owned = false
tag :: Type{ kind = .enumeration, enum_type = EnumId(state.enums.items.len - 1) }
narrowed := tag
if (source.kind == .record) {
original :: state.records.items[usize(source.record_type)]
fields :: try mem.alloc(RecordField, state.records.allocator, count)
errdefer mem.free(state.records.allocator, fields)
next = 0
for original.fields |field| {
if (!arm_keeps_variant(state, owner, arm, field.name?)) continue
fields[next] = field
next += 1
}
try arraylist.append(&state.records, RecordType{
declaration = ast.NodeId(maxval!(u32)), fields = fields, is_union = true,
tag_type = tag, composed = true,
})
narrowed = Type{ kind = .record, record_type = RecordId(state.records.items.len - 1) }
}
state.cells[usize(arm)].narrowed_type = narrowed
return narrowed
}
+18 -2
View File
@@ -382,6 +382,11 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me
if (ast.return_stmt(tree, statement).value) |value| try resolve_expr(state, scope, value)
}
.break_stmt, .continue_stmt: {}
.block_stmt: try resolve_arm(state, scope, ast.statement_block(tree, statement))
.defer_stmt, .errdefer_stmt: {
cleanup :: ast.cleanup_stmt(tree, statement)
try resolve_capture_arm(state, scope, cleanup.body, cleanup.capture)
}
.if_stmt: {
conditional :: ast.if_stmt(tree, statement)
try resolve_expr(state, scope, conditional.condition)
@@ -511,7 +516,7 @@ resolve_expr proc(
match ast.expr_kind(tree, id) {
.literal_int, .literal_float, .literal_bool, .literal_string, .literal_null: {}
.enum_literal: {}
.distinct_type, .alias_expr, .optional_type: try resolve_expr(state, scope, ast.type_operand(tree, id))
.distinct_type, .alias_expr, .optional_type, .try_expr: try resolve_expr(state, scope, ast.type_operand(tree, id))
.enum_type: {
if (ast.enum_type(tree, id).backing) |backing| try resolve_expr(state, scope, backing)
for (0..ast.enum_member_count(tree, id)) |i| {
@@ -534,6 +539,17 @@ resolve_expr proc(
else try resolve_block(state, &arm_scope, arm.body)
}
}
.catch_expr: {
expression :: ast.catch_expr(tree, id)
try resolve_expr(state, scope, expression.operand)
handler_scope := Scope{ names = hashmap.init(state.allocator), parent = scope }
defer hashmap.deinit(&handler_scope.names)
if (expression.capture) |capture| {
try predeclare(state, &handler_scope, capture, ast.node_span(tree, capture))
}
if (expression.handler_is_block) try resolve_block(state, &handler_scope, expression.handler)
else try resolve_expr(state, &handler_scope, expression.handler)
}
.identifier: {
span :: ast.node_span(tree, id)
name :: ast.name_id(tree, id)
@@ -577,7 +593,7 @@ resolve_expr proc(
state.bindings[usize(expression.member)] = value
}
.unary: try resolve_expr(state, scope, ast.unary_expr(tree, id).operand)
.binary: {
.binary, .fallible_type, .sum_type: {
expression :: ast.binary_expr(tree, id)
try resolve_expr(state, scope, expression.left)
try resolve_expr(state, scope, expression.right)