feat: execute fallible type-valued compile-time storage

This commit is contained in:
2026-10-30 20:00:59 +01:00
parent 0625359d62
commit 15cbebe40f
5 changed files with 813 additions and 30 deletions
+3 -1
View File
@@ -116,7 +116,9 @@ initialized records/tuples containing `type` fields live inside compile-time pro
type-valued records/tuples may also start with `undefined`: `config struct { child type, count usize } := undefined`, then `config.child = i16` and `config.count = 2`. nested record fields, arrays of records, tuple ordinals and `field!` updates use the same persistent snapshots as initialized storage; partial copies retain independent initialization state. defaults do not initialize `undefined` fields. initialized leaves and array lengths can be read while other leaves remain uninitialized, but premature leaf reads and incomplete procedure arguments/results diagnose read-before-initialization. fully initialized results cross compile-time procedure boundaries and supply erased generic inputs, generated storage and defaults. empty shapes are initialized vacuously; each expanded record field/array cell shares the construction bound.
optionals containing types also stay in owned compile-time storage: `?type`, nested `??type`, and optional records/arrays of metadata support null, contextual payloads, explicit `some!`, whole-value updates, immutable captures, null comparisons and checked unwraps. `some!(null)` preserves an absent inner optional inside a present outer one. constant-input procedures exchange these snapshots; erased generic inputs retain content-based specialization identity and nominal type leaves. `if`/`while` captures and conditional expressions select generated types or runtime bodies without runtime metadata. for generated array children, bind the unwrap first (`T :: selected?`, then `[count]T`); postfix `?` after an array type unwraps the whole expression. undefined presence and incomplete payloads diagnose before observation or procedure boundaries. runtime metadata storage and fallible type-valued channels remain unsupported.
optionals containing types also stay in owned compile-time storage: `?type`, nested `??type`, and optional records/arrays of metadata support null, contextual payloads, explicit `some!`, whole-value updates, immutable captures, null comparisons and checked unwraps. `some!(null)` preserves an absent inner optional inside a present outer one. constant-input procedures exchange these snapshots; erased generic inputs retain content-based specialization identity and nominal type leaves. `if`/`while` captures and conditional expressions select generated types or runtime bodies without runtime metadata. for generated array children, bind the unwrap first (`T :: selected?`, then `[count]T`); postfix `?` after an array type unwraps the whole expression. undefined presence and incomplete payloads diagnose before observation or procedure boundaries. runtime metadata storage remains unsupported.
fallible channels containing types also execute entirely at compile time: `type ! Error`, metadata records/arrays in either channel, and nested optionals retain owned active payloads. every producer input is explicit and constant; erased generic inputs, whole-value replacement and saved copies preserve exact nominal leaves and content-based specialization identity. `try` checks the complete error subset before propagating; `catch` captures an immutable error snapshot and selects generated types or runtime handlers. named metadata structs and tagged unions compose with ordinary error enums without runtime reflection. returns snapshot before scoped LIFO `defer`/`errdefer`; fallible `void` also permits bare returns and successful fallthrough. incomplete storage, invalid channels/payloads, capture mutation and runtime inputs diagnose. imported private types survive source/session disposal and repeated compilation. runtime locals still cannot store metadata aggregates; keep those values inside compile-time producers or pass them directly to erased inputs.
fixed type arrays may also start with `undefined` inside type/type-array producers: `types [2]mut type := undefined`, then fill `types[index] = i16` in a loop. nested and whole-array updates preserve shape, mutability and independent initialization state in local copies; `.len` is available before elements initialize. reading an uninitialized type leaf, returning an incomplete array or passing one to a compile-time procedure diagnoses a read before initialization. empty shapes are valid, but every dimension must be explicit and nonnegative. construction charges each expanded cell against the shared execution bound, including nested zero-leaf shapes.
+4 -1
View File
@@ -48,6 +48,8 @@ initialized type-valued records/tuples support schemas/defaults, nested
field/index updates and owned copies/captures inside compile-time producers.
optional type-valued storage preserves nested presence, owned metadata payloads,
checked unwraps and immutable captures across compile-time procedure boundaries.
fallible type-valued channels own their active payloads, propagate checked errors,
and select generated storage through `try`/`catch` and scoped cleanup.
`field!` selects record/tuple/enum and sequence members using compile-time names,
including runtime places and type-control updates without runtime reflection.
`struct_type!` generates ordinary/C-layout records from constant name/type/default
@@ -157,7 +159,8 @@ test discovery and FFI remain outside these completed slices.
- [x] untagged-union materialization checkpoint: address-free untagged unions own full native-layout byte snapshots through interpreter/native execution, including scalar/IEEE reinterpretation, nested padded records/arrays, smaller overlapping updates, zero-sized storage, distinct members and generated nominal unions. explicit `$()` values, compile-time arguments, full-byte specialization-cache identity, generated defaults, nested optionals/tagged payloads and both fallible channels preserve exact types and independent copies. uninitialized tail/padding bytes diagnose read-before-initialization; every member layout is checked recursively to reject data pointers, callbacks, strings and slices rather than retain interpreter handles. imported helpers/diagnostics, source/session disposal and repeated compilation retain ownership. address-bearing union layouts and broader constant materialization remain open.
- [x] readonly typed-slice materialization checkpoint: ordinary compile-time calls snapshot readonly slices of materializable elements into module-lifetime backing through interpreter/native execution. nominal bytes, wider integers, arrays/records, source-bound callbacks, nested readonly slices, empty/zero-sized elements and full-width signed/unsigned sentinels retain exact types and logical lengths. explicit `$()` values, computed/direct content-based specialization identity, generated defaults and nested optional/tagged/fallible channels preserve ownership across helper returns, imported private declarations, source/session disposal and repeat compilation. initialization, address-range, sentinel, mutable-slice and nonnull-pointer escape diagnostics remain checked; 100,000-value/128-level snapshot bounds reject oversized and cyclic storage. mutable slices, nonnull data pointers and address-bearing untagged union layouts remain open.
- [x] uninitialized type-record checkpoint: compile-time records/tuples containing type values start with `undefined` and fill through nested field/index updates or whole-value replacement. persistent snapshots preserve independent partial copies; defaults do not initialize undefined fields. initialized leaves and empty shapes work while premature type/value reads and incomplete procedure arguments/results diagnose read-before-initialization. fully initialized results supply erased generic inputs, generated storage/defaults and cached nominal types through interpreter/native execution. ordinary mutability/schema validation and shared per-field/cell construction bounds remain checked; general type-valued execution and broader materialization remain open.
- [x] optional type-storage checkpoint: `?type` and optional type-valued records/arrays use owned compile-time snapshots, including nested `some!(null)`, contextual payload construction, mutable whole-value/field/index replacement, immutable if/while/conditional captures, null comparisons and checked unwraps. constant-input procedures and erased generic arguments preserve specialization-cache identity and nominal leaves while generating runtime storage through interpreter/native execution. undefined presence, incomplete payloads, non-type values, capture mutation and runtime dependencies retain diagnostics. imported private nominal types, session/source disposal and repeat compilation preserve ownership. fallible type-valued channels, general type-valued execution and broader constant materialization remain open.
- [x] optional type-storage checkpoint: `?type` and optional type-valued records/arrays use owned compile-time snapshots, including nested `some!(null)`, contextual payload construction, mutable whole-value/field/index replacement, immutable if/while/conditional captures, null comparisons and checked unwraps. constant-input procedures and erased generic arguments preserve specialization-cache identity and nominal leaves while generating runtime storage through interpreter/native execution. undefined presence, incomplete payloads, non-type values, capture mutation and runtime dependencies retain diagnostics. imported private nominal types, session/source disposal and repeat compilation preserve ownership. fallible type-valued channels are covered below; general type-valued execution and broader constant materialization remain open.
- [x] fallible type-storage checkpoint: explicit success/error channels containing types use owned compile-time snapshots, including nested optional/record/array payloads, whole-value updates, independent copies, constant-input procedures and erased generic inputs. `try` validates error subsets and propagates through enclosing producers; `catch` binds immutable exact error payloads and selects generated types or runtime handlers. composed metadata errors preserve named variants, nominal leaves and cache identity. returned snapshots precede scoped LIFO `defer`/`errdefer` cleanup; fallible `void` supports bare returns and successful fallthrough. incomplete/uninitialized channels, invalid payloads/compositions, capture mutation and runtime dependencies retain diagnostics. imported private types, source/session disposal and repeat compilation preserve ownership through interpreter/native execution. runtime metadata storage, general type-valued execution and broader materialization remain open.
- [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including address-bearing untagged unions, nonnull data pointers and mutable slices), plus remaining existing-library acceptance for broader `meta` operations and `io.print`. formatting parsing and broader imported C-binding support may interleave with later gates.
- [ ] remaining source constructs: multiline strings. boolean and optional-capture conditional expressions, yielding value blocks, `yield`, compound updates and bitwise operators are covered above. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`.
- [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library.
+88
View File
@@ -1208,3 +1208,91 @@ optional_type_metadata_recompiles_owned_private_nominal_payloads test {
try testing.expect_equal(i64(42), outcome.value.data.integer)
}
}
fallible_type_metadata_recompiles_owned_private_nominal_payloads test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
modules [2]mut ir.Module := undefined
{
session := init(mem.c_allocator)
defer deinit(&session)
root :: try add_package(&session)
dependency :: try add_package(&session)
bytes := try owned_text(
`lib :: import "../library"
`Child :: struct { value bool }
`ErrorChild :: struct { value bool }
`main proc() i16 {
` if (lib.Box($(lib.make(true))) != lib.Box(lib.make(true))) return -1
` if (lib.Box($(lib.make(false))) != lib.Box(lib.make(false))) return -2
` Selected :: lib.Selected(true) catch bool
` Recovered :: lib.Recovered()
` if (Selected == Child or Recovered == ErrorChild) return -3
` selected Selected :: { value = 40 }
` recovered Recovered :: { value = 2 }
` snapshot lib.Snapshot($(lib.make(true))) :: {
` original = { values = [{ value = 19 }, { value = 21 }] },
` replacement = { values = [{ value = 2 }] },
` }
` if (snapshot.original.values[0].value + snapshot.original.values[1].value != 40) return -4
` if (snapshot.replacement.values[0].value != 2) return -5
` failed lib.Box($(lib.make(false))) :: { values = [{ value = 2 }] }
` if (failed.values[0].value != recovered.value) return -6
` return selected.value + recovered.value
`}
,
)
defer arraylist.deinit(&bytes)
root_source :: try add_source(&session, root, "/fallible_types/app/main.hon", &bytes, &diagnostic_store)
imported := try owned_text(
`@hide:file
`Child :: struct { value i16 }
`@hide:file
`ErrorChild :: struct { value i16 }
`@hide:file
`Config :: struct { child type, count usize }
`@hide:file
`Failure :: struct { child type, count usize }
`make proc(success bool) Config ! Failure {
` if (success) return Config { child = Child, count = 2 }
` return Failure { child = ErrorChild, count = 1 }
`}
`Box proc(input Config ! Failure) type {
` info :: input catch |err| { return struct { values [err.count]err.child } }
` return struct { values [info.count]info.child }
`}
`Selected proc(success bool) type ! Failure {
` info :: try make(success)
` return info.child
`}
`Recovered proc() type {
` return Selected(false) catch |err| err.child
`}
`Snapshot proc(input Config ! Failure) type {
` changed := input
` saved :: changed
` changed = make(false)
` return struct { original Box(saved), replacement Box(changed) }
`}
,
)
defer arraylist.deinit(&imported)
_ = try add_source(&session, dependency, "/fallible_types/library/types.hon", &imported, &diagnostic_store)
try arraylist.append(&session.imports, Import{
node = declaration(&session, root_source, 0).node,
target = dependency,
})
modules[0] = try compile(&session, root, &diagnostic_store)
errdefer ir.deinit(&modules[0])
modules[1] = try compile(&session, root, &diagnostic_store)
}
defer ir.deinit(&modules[0])
defer ir.deinit(&modules[1])
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
for modules |module| {
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
defer interpreter.deinit(&outcome)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
}
}
+369 -28
View File
@@ -431,6 +431,12 @@ expand_expected proc(state @mut State, id, expected ast.NodeId) void ! Error {
if (class == .expression and ast.expr_kind(&state.tree, id) == .catch_expr) {
expression :: ast.catch_expr(&state.tree, id)
try expand_expected(state, expression.operand, expected)
operand_type :: try expression_type(state, expression.operand, 0)
if (type_record_storage(state, operand_type, 0)
and ast.expr_kind(&state.tree, operand_type) == .fallible_type) {
try specialize_type_catch(state, id, operand_type, expected)
return
}
if (expression.capture) |capture| {
annotation :: try expression_type(state, expression.operand, 0)
if (annotation != no_node and ast.expr_kind(&state.tree, annotation) == .fallible_type) {
@@ -1393,6 +1399,11 @@ inline_sum_fields proc(state @mut State, input ast.NodeId, fields @mut arraylist
or ast.stmt_kind(&state.tree, declaration) != .declaration) continue
if (ast.statement_decl(&state.tree, declaration).initializer != type) continue
name :: try member_name(state, declaration)
for (fields.items) |previous| {
field :: ast.field(&state.tree, previous)
if (ast.name_id(&state.tree, field.name?) != ast.name_id(&state.tree, name)) continue
return try same_type(state, field.type?, type)
}
field :: try ast.add_field(state.builder, ast.node_span(&state.tree, type), name, type, null)
try refresh(state)
try arraylist.append(fields, field)
@@ -1401,6 +1412,9 @@ inline_sum_fields proc(state @mut State, input ast.NodeId, fields @mut arraylist
return false
}
if (kind != .enum_type and kind != .union_type) return false
if (kind == .enum_type and ast.enum_type(&state.tree, type).backing != null) return false
if (kind == .union_type and !ast.union_type(&state.tree, type).auto_tag
and ast.union_type(&state.tree, type).tag == null) return false
count :: if (kind == .enum_type) ast.enum_member_count(&state.tree, type)
else ast.record_field_count(&state.tree, type)
for (0..count) |i| {
@@ -1412,8 +1426,10 @@ inline_sum_fields proc(state @mut State, input ast.NodeId, fields @mut arraylist
if (kind == .union_type and field.type == null) return false
duplicate := false
for (fields.items) |previous| {
if (ast.name_id(&state.tree, ast.field(&state.tree, previous).name?)
== ast.name_id(&state.tree, field.name?)) duplicate = true
name :: ast.field(&state.tree, previous).name?
if (ast.name_id(&state.tree, name) != ast.name_id(&state.tree, field.name?)) continue
if (name != field.name?) return false
duplicate = true
}
if (duplicate) continue
payload :: if (kind == .enum_type) try builtin(state, member, .void) else field.type?
@@ -1537,7 +1553,7 @@ expand_inline_match proc(state @mut State, id, subject_type ast.NodeId) void ! E
union_view :: ast.union_type(&state.tree, members_type)
if (kind != .sum_type and union_view.tag != null)
tag_type = try canonical_type(state, union_view.tag?)
else if (type_record_storage(state, annotation, 0)) tag_type = try metadata_tags(state, annotation)
else if (type_record_storage(state, annotation, 0)) tag_type = try metadata_tags(state, members_type)
else {
concrete :: try materialize(state, Value{ kind = .type, node = annotation })
tag_type = try ast.add_union_tag_type(state.builder, span, concrete.node)
@@ -1813,6 +1829,11 @@ type_storage_annotation proc(tree @ast.Tree, bindings []sema.Binding, input ast.
kind :: ast.expr_kind(tree, input)
if (kind == .alias_expr or kind == .optional_type)
return type_storage_annotation(tree, bindings, ast.type_operand(tree, input), depth + 1)
if (kind == .fallible_type or kind == .sum_type) {
channels :: ast.binary_expr(tree, input)
return (type_storage_annotation(tree, bindings, channels.left, depth + 1)
or type_storage_annotation(tree, bindings, channels.right, depth + 1))
}
if (kind == .array_type)
return type_storage_annotation(tree, bindings, ast.array_type(tree, input).element, depth + 1)
if (kind == .slice_type)
@@ -2958,6 +2979,7 @@ TypeExecution :: struct {
first_owned ast.NodeId
returned ast.NodeId = no_node
result_type ast.NodeId
failed bool = false
yielded ast.NodeId = no_node
yield_type ast.NodeId = no_node
yield_depth usize = 0
@@ -2990,8 +3012,22 @@ return_type_value proc(state @mut State, block, first_owned, result_type ast.Nod
defer arraylist.deinit(&execution.locals)
defer mem.free(state.allocator, execution.mapping)
mem.set(execution.mapping, no_node)
if (try execute_type_block(state, &execution, block) != .returned) return no_node
return execution.returned
flow :: try execute_type_block(state, &execution, block)
if (flow == .returned) return execution.returned
if (flow == .next) return try type_void_completion(state, result_type, block)
return no_node
}
@hide:file
type_void_completion proc(state @mut State, annotation, at ast.NodeId) ast.NodeId ! Error {
type :: try canonical_type(state, annotation)
if (type == no_node or ast.expr_kind(&state.tree, type) != .fallible_type) return no_node
child :: try canonical_type(state, ast.binary_expr(&state.tree, type).left)
resolved :: binding(state, child)
if (resolved.kind != .builtin or resolved.data.builtin != .void) return no_node
value :: try ast.add_literal(state.builder, .void, ast.node_span(&state.tree, at))
try refresh(state)
return try type_execution_value(state, value, annotation)
}
@hide:file
@@ -3131,6 +3167,12 @@ prepare_type_snapshot proc(state @mut State, execution @mut TypeExecution, id as
return
}
}
if (kind == .try_expr) {
execution.mapping[usize(id)] = try type_snapshot_try(state, execution, id)
if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next)
execution.pending_flow = .invalid
return
}
if (kind == .catch_expr) {
execution.mapping[usize(id)] = try type_snapshot_catch(state, execution, id, no_node)
if (execution.mapping[usize(id)] == no_node and execution.pending_flow == .next)
@@ -3235,22 +3277,30 @@ type_snapshot_catch proc(
owned :: try type_execution_value(state, operand, operand_type)
if (owned == no_node) return no_node
span :: ast.node_span(&state.tree, id)
boolean :: try builtin(state, id, .bool)
yes :: try ast.add_boolean(state.builder, span, true)
no :: try ast.add_boolean(state.builder, span, false)
failed :: try ast.add_return_stmt(state.builder, span, yes)
failure_statements [1]ast.NodeId :: [failed]
handler :: try ast.add_block(state.builder, span, failure_statements[..])
caught :: try ast.add_catch_expr(state.builder, span, owned, null, handler, true)
discarded :: try ast.add_expression_stmt(state.builder, caught)
success :: try ast.add_return_stmt(state.builder, span, no)
statements [2]ast.NodeId :: [discarded, success]
body :: try ast.add_block(state.builder, span, statements[..])
try refresh(state)
guard :: try evaluate_ir_body(state, id, body, boolean)
if (guard.kind != .boolean) return no_node
failed := false
if (type_record_storage(state, operand_type, 0)) {
if (!try type_array_initialized(state, owned, id)) return no_node
failed = !try same_type(state,
ast.typed_constant(&state.tree, ast.typed_constant(&state.tree, owned).value).type, channels.left)
} else {
boolean :: try builtin(state, id, .bool)
yes :: try ast.add_boolean(state.builder, span, true)
no :: try ast.add_boolean(state.builder, span, false)
returned :: try ast.add_return_stmt(state.builder, span, yes)
failure_statements [1]ast.NodeId :: [returned]
handler :: try ast.add_block(state.builder, span, failure_statements[..])
caught :: try ast.add_catch_expr(state.builder, span, owned, null, handler, true)
discarded :: try ast.add_expression_stmt(state.builder, caught)
success :: try ast.add_return_stmt(state.builder, span, no)
statements [2]ast.NodeId :: [discarded, success]
body :: try ast.add_block(state.builder, span, statements[..])
try refresh(state)
guard :: try evaluate_ir_body(state, id, body, boolean)
if (guard.kind != .boolean) return no_node
failed = guard.boolean
}
payload :: ast.typed_constant(&state.tree, owned).value
if (!guard.boolean) return try type_execution_value(state, payload, channels.left)
if (!failed) return try type_execution_value(state, payload, channels.left)
saved :: execution.locals.items.len
defer execution.locals.items = execution.locals.items[..saved]
if (view.capture) |capture| {
@@ -3279,6 +3329,125 @@ type_snapshot_catch proc(
return try type_execution_value(state, selected, result_type)
}
@hide:file
specialize_type_catch proc(state @mut State, id, annotation, expected ast.NodeId) void ! Error {
view :: ast.catch_expr(&state.tree, id)
value :: try evaluate_expected(state, view.operand, annotation)
if (value.kind != .aggregate) {
try reject_evaluation(state, id)
return
}
channels :: ast.binary_expr(&state.tree, annotation)
payload :: value.node
success :: try same_type(state, ast.typed_constant(&state.tree, payload).type, channels.left)
selected := no_node
if (success) selected = try type_execution_value(state, payload, channels.left)
else {
execution := TypeExecution{
locals = arraylist.init(state.allocator),
mapping = try mem.alloc(ast.NodeId, state.allocator, ast.node_count(&state.tree)),
first_owned = ast.NodeId(0),
result_type = no_node,
}
defer arraylist.deinit(&execution.locals)
defer mem.free(state.allocator, execution.mapping)
mem.set(execution.mapping, no_node)
if (view.capture) |capture| {
resolved :: binding(state, capture)
if (resolved.kind == .invalid) {
state.evaluation_diagnostic = resolved.data.diagnostic
try reject_evaluation(state, id)
return
}
error :: try type_execution_value(state, payload, channels.right)
if (error == no_node) {
try reject_evaluation(state, id)
return
}
try arraylist.append(&execution.locals, TypeLocal{
declaration = capture, value = error, annotation = channels.right,
})
}
selected = try type_snapshot(state, &execution, view.handler)
if (view.handler_is_block and selected != no_node) {
selected = try ast.add_block_expr(state.builder, ast.node_span(&state.tree, id), selected)
try refresh(state)
}
}
if (selected == no_node) {
try reject_evaluation(state, id)
return
}
context :: if (expected != no_node) expected else channels.left
try expand_expected(state, selected, context)
if (is_type(state, channels.left) or type_record_storage(state, channels.left, 0)) {
selected = try type_execution_value(state, selected, channels.left)
if (selected == no_node) {
try reject_evaluation(state, id)
return
}
} else {
result_type := channels.left
contextual :: try canonical_type(state, context)
if (contextual != no_node) {
kind :: ast.expr_kind(&state.tree, contextual)
if (kind == .optional_type
and try same_type(state, ast.type_operand(&state.tree, contextual), channels.left))
result_type = context
if (kind == .fallible_type
and try same_type(state, ast.binary_expr(&state.tree, contextual).left, channels.left))
result_type = context
}
selected = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, id), result_type, selected)
try refresh(state)
}
if (is_type(state, try expression_type(state, selected, 0))) {
selected = try ast.add_type_operand(state.builder, ast.node_span(&state.tree, id), selected, true)
try refresh(state)
}
try replace(state, id, selected)
try refresh(state)
state.bindings.items[usize(id)] = binding(state, selected)
}
@hide:file
type_snapshot_try proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error {
operand :: try type_snapshot(state, execution, ast.type_operand(&state.tree, id))
if (operand == no_node) return no_node
try expand(state, operand)
annotation :: try expression_type(state, operand, 0)
if (annotation == no_node or ast.expr_kind(&state.tree, annotation) != .fallible_type) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_fallible_operand, ast.node_span(&state.tree, id))
return no_node
}
result :: try canonical_type(state, execution.result_type)
if (execution.cleanup_depth > 0 or result == no_node
or ast.expr_kind(&state.tree, result) != .fallible_type) {
code diagnostics.Code :: if (execution.cleanup_depth > 0)
.cleanup_control_flow else .try_outside_fallible
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, code, ast.node_span(&state.tree, id))
return no_node
}
channels :: ast.binary_expr(&state.tree, annotation)
if (!try type_subset(state, channels.right, ast.binary_expr(&state.tree, result).right)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.incompatible_propagated_error, ast.node_span(&state.tree, id))
return no_node
}
# propagation is a catch whose error branch returns from the enclosing frame.
span :: ast.node_span(&state.tree, id)
capture :: try ast.add_identifier(state.builder, span, strpool.no_str)
try refresh(state)
value :: try reference(state, id, capture)
returned :: try ast.add_return_stmt(state.builder, span, value)
statements [1]ast.NodeId :: [returned]
handler :: try ast.add_block(state.builder, span, statements[..])
caught :: try ast.add_catch_expr(state.builder, span, operand, capture, handler, true)
try refresh(state)
return try type_snapshot_catch(state, execution, caught, channels.left)
}
@hide:file
type_dispatch_snapshot proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error {
subject :: try type_snapshot(state, execution, ast.match_expr(&state.tree, id).subject)
@@ -3521,7 +3690,7 @@ type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error {
} else {
if (ast.expr_kind(&state.tree, value.node) != .record_literal) return no_index
active :: ast.field(&state.tree, ast.record_field(&state.tree, value.node, 0))
tags :: try metadata_tags(state, annotation)
tags :: try metadata_tags(state, try inline_match_shape(state, annotation))
tag :: try ast.add_enum_literal(state.builder, span, active.name?)
dispatch = try ast.add_typed_constant(state.builder, span, tags, tag)
}
@@ -3553,12 +3722,13 @@ type_match_arm proc(state @mut State, id ast.NodeId) usize ! Error {
valid = false
continue
}
slot :: type_record_slot(state, annotation, ast.enum_literal(&state.tree, pattern))
shape :: try inline_match_shape(state, annotation)
slot :: type_record_slot(state, shape, ast.enum_literal(&state.tree, pattern))
if (slot == no_index) {
valid = false
continue
}
payload :: ast.field(&state.tree, ast.record_field(&state.tree, annotation, slot)).type?
payload :: ast.field(&state.tree, ast.record_field(&state.tree, shape, slot)).type?
if (binding(state, payload).kind == .builtin and binding(state, payload).data.builtin == .void)
valid = false
if (expected != no_node and !try same_type(state, expected, payload)) valid = false
@@ -3902,8 +4072,48 @@ type_record_check proc(state @mut State, annotation ast.NodeId) bool ! Error {
if (is_type(state, annotation)) return true
type :: try canonical_type(state, annotation)
if (type == no_node) return false
if (!type_record_storage(state, type, 0)) {
constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, type), type, true)
try refresh(state)
try arraylist.append(&state.rewrites.constraints, constraint)
return true
}
if (ast.expr_kind(&state.tree, type) == .optional_type)
return try type_record_check(state, ast.type_operand(&state.tree, type))
if (ast.expr_kind(&state.tree, type) == .fallible_type) {
channels :: ast.binary_expr(&state.tree, type)
error_type :: try canonical_type(state, channels.right)
if (error_type == no_node) return false
kind :: ast.expr_kind(&state.tree, error_type)
if (kind != .enum_type and kind != .record_type and kind != .union_type and kind != .sum_type) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_fallible_type, ast.node_span(&state.tree, type))
return false
}
if (!type_record_storage(state, error_type, 0)) {
checked :: try ast.add_binary_type(state.builder, ast.node_span(&state.tree, type),
try builtin(state, type, .void), error_type, true)
constraint :: try ast.add_type_operand(state.builder, ast.node_span(&state.tree, type), checked, true)
try refresh(state)
try arraylist.append(&state.rewrites.constraints, constraint)
} else if (kind == .union_type and !ast.union_type(&state.tree, error_type).auto_tag
and ast.union_type(&state.tree, error_type).tag == null) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_fallible_type, ast.node_span(&state.tree, type))
return false
}
if (!try type_record_check(state, channels.left)) return false
return try type_record_check(state, channels.right)
}
if (ast.expr_kind(&state.tree, type) == .sum_type) {
members :: ast.binary_expr(&state.tree, type)
if (!try type_record_check(state, members.left) or !try type_record_check(state, members.right)) return false
if (try inline_match_shape(state, type) != no_node) return true
if (state.evaluation_diagnostic == diagnostics.no_diagnostic)
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.invalid_sum_type, ast.node_span(&state.tree, type))
return false
}
if (ast.expr_kind(&state.tree, type) == .slice_type)
return try type_record_check(state, ast.slice_type(&state.tree, type).element)
if (ast.expr_kind(&state.tree, type) == .array_type) {
@@ -4345,6 +4555,108 @@ type_optional_value proc(state @mut State, expression, type ast.NodeId) ast.Node
return result
}
@hide:file
type_variant proc(state @mut State, annotation, name ast.NodeId) bool ! Error {
type :: try canonical_type(state, annotation)
if (type == no_node) return false
kind :: ast.expr_kind(&state.tree, type)
if (kind == .sum_type) {
members :: ast.binary_expr(&state.tree, type)
return try type_variant(state, members.left, name) or try type_variant(state, members.right, name)
}
if (kind == .union_type) return type_record_slot(state, type, name) != no_index
if (kind != .enum_type) return false
for (0..ast.enum_member_count(&state.tree, type)) |i| {
member :: ast.field(&state.tree, ast.enum_member(&state.tree, type, i)).name?
if (ast.name_id(&state.tree, member) == ast.name_id(&state.tree, name)) return true
}
return false
}
# the payload's declared channel is retained even when success and error values
# have overlapping shapes. no type-bearing snapshot enters runtime IR.
@hide:file
type_fallible_value proc(state @mut State, expression, type ast.NodeId) ast.NodeId ! Error {
if (!try type_record_check(state, type)) return no_node
span :: ast.node_span(&state.tree, expression)
kind :: ast.expr_kind(&state.tree, expression)
if (kind == .literal_undefined) {
result :: try ast.add_typed_constant(state.builder, span, type, expression)
try refresh(state)
return result
}
channels :: ast.binary_expr(&state.tree, type)
actual :: try expression_type(state, expression, 0)
if (actual != no_node and ast.expr_kind(&state.tree, actual) == .fallible_type) {
if (try same_type(state, actual, type)) return expression
source :: ast.binary_expr(&state.tree, actual)
if (!try same_type(state, source.left, channels.left)
or !try type_subset(state, source.right, channels.right)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .type_mismatch, span)
return no_node
}
return try type_fallible_value(state, ast.typed_constant(&state.tree, expression).value, type)
}
channel := channels.left
if (actual != no_node and !try same_type(state, actual, channels.left)
and try type_subset(state, actual, channels.right)) channel = channels.right
name := no_node
if (kind == .enum_literal) name = ast.enum_literal(&state.tree, expression)
if (kind == .record_literal and ast.record_literal(&state.tree, expression).type != null) {
tag :: ast.record_literal(&state.tree, expression).type?
if (ast.expr_kind(&state.tree, tag) == .enum_literal) name = ast.enum_literal(&state.tree, tag)
}
if (name != no_node and try type_variant(state, channels.right, name)
and !try type_variant(state, channels.left, name)) channel = channels.right
if (kind == .record_literal and ast.record_literal(&state.tree, expression).type == null) {
success :: try canonical_type(state, channels.left)
failure :: try canonical_type(state, channels.right)
if ((success == no_node or ast.expr_kind(&state.tree, success) != .record_type)
and ast.expr_kind(&state.tree, failure) == .record_type) channel = channels.right
}
payload := try type_execution_value(state, expression, channel)
if (payload == no_node) return no_node
if (ast.expr_kind(&state.tree, payload) != .typed_constant) {
payload = try ast.add_typed_constant(state.builder, span, channel, payload)
try refresh(state)
}
result :: try ast.add_typed_constant(state.builder, span, type, payload)
try refresh(state)
return result
}
@hide:file
type_sum_value proc(state @mut State, expression, type ast.NodeId) ast.NodeId ! Error {
if (!try type_record_check(state, type)) return no_node
actual :: try expression_type(state, expression, 0)
if (actual != no_node and try same_type(state, actual, type)) return expression
shape :: try inline_match_shape(state, type)
if (shape == no_node) return no_node
span :: ast.node_span(&state.tree, expression)
input := expression
if (actual != no_node and try type_subset(state, actual, type)) {
source :: try canonical_type(state, actual)
if (ast.expr_kind(&state.tree, source) == .record_type) {
for (0..ast.record_field_count(&state.tree, shape)) |i| {
field :: ast.field(&state.tree, ast.record_field(&state.tree, shape, i))
if (!try same_type(state, field.type?, actual)) continue
member :: try ast.add_field(state.builder, span, field.name, null, expression)
fields [1]ast.NodeId :: [member]
input = try ast.add_record_literal(state.builder, span, null, fields[..])
try refresh(state)
break
}
} else if (ast.expr_kind(&state.tree, expression) == .typed_constant)
input = ast.typed_constant(&state.tree, expression).value
}
value :: try type_union_value(state, input, shape)
if (value == no_node) return no_node
result :: try ast.add_typed_constant(state.builder, span, type,
ast.typed_constant(&state.tree, value).value)
try refresh(state)
return result
}
@hide:file
type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.NodeId ! Error {
if (input == no_node or annotation == no_node) return no_node
@@ -4412,6 +4724,10 @@ type_execution_value proc(state @mut State, input, annotation ast.NodeId) ast.No
type :: try canonical_type(state, annotation)
if (type != no_node and ast.expr_kind(&state.tree, type) == .optional_type)
return try type_optional_value(state, expression, type)
if (type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type)
return try type_fallible_value(state, expression, type)
if (type != no_node and ast.expr_kind(&state.tree, type) == .sum_type)
return try type_sum_value(state, expression, type)
}
if (!is_type(state, annotation) and type_record_storage(state, annotation, 0))
return try type_record_value(state, expression, annotation)
@@ -4562,13 +4878,24 @@ execute_type_block proc(state @mut State, execution @mut TypeExecution, block as
while (reached > 0) {
reached -= 1
statement :: ast.block_statement(&state.tree, block, reached)
if (ast.stmt_kind(&state.tree, statement) != .defer_stmt) continue
kind :: ast.stmt_kind(&state.tree, statement)
if (kind != .defer_stmt and !(kind == .errdefer_stmt and execution.failed)) continue
saved_depth :: execution.cleanup_yield_depth
saved_loop :: execution.cleanup_loop_depth
execution.cleanup_loop_depth = execution.loop_depth
execution.cleanup_yield_depth = execution.yield_depth
execution.cleanup_depth += 1
cleanup :: try execute_type_block(state, execution, ast.cleanup_stmt(&state.tree, statement).body)
cleanup_view :: ast.cleanup_stmt(&state.tree, statement)
if (kind == .errdefer_stmt and cleanup_view.capture != null) {
annotation :: ast.binary_expr(&state.tree, try canonical_type(state, execution.result_type)).right
value :: ast.typed_constant(&state.tree, execution.returned).value
try arraylist.append(&execution.locals, TypeLocal{
declaration = cleanup_view.capture?, value = value, annotation = annotation,
})
}
cleanup :: try execute_type_block(state, execution, cleanup_view.body)
if (kind == .errdefer_stmt and cleanup_view.capture != null)
execution.locals.items = execution.locals.items[..execution.locals.items.len - 1]
execution.cleanup_depth -= 1
execution.cleanup_yield_depth = saved_depth
execution.cleanup_loop_depth = saved_loop
@@ -4641,9 +4968,12 @@ execute_type_statement_body proc(state @mut State, execution @mut TypeExecution,
}
.defer_stmt: {}
.errdefer_stmt: {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.errdefer_outside_fallible, ast.node_span(&state.tree, statement))
return .invalid
result :: try canonical_type(state, execution.result_type)
if (result == no_node or ast.expr_kind(&state.tree, result) != .fallible_type) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.errdefer_outside_fallible, ast.node_span(&state.tree, statement))
return .invalid
}
}
.yield_stmt: {
if (execution.yield_depth == 0) {
@@ -4673,6 +5003,17 @@ execute_type_statement_body proc(state @mut State, execution @mut TypeExecution,
execution.returned = try type_execution_value(state, expression, execution.result_type)
if (execution.returned != no_node
and !try type_array_initialized(state, execution.returned, value)) return .invalid
if (execution.returned != no_node and type_record_storage(state, execution.result_type, 0)) {
type :: try canonical_type(state, execution.result_type)
if (type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type) {
payload :: ast.typed_constant(&state.tree, execution.returned).value
execution.failed = !try same_type(state, ast.typed_constant(&state.tree, payload).type,
ast.binary_expr(&state.tree, type).left)
}
}
if (execution.returned != no_node) return .returned
} else {
execution.returned = try type_void_completion(state, execution.result_type, statement)
if (execution.returned != no_node) return .returned
}
return .invalid
+349
View File
@@ -2415,6 +2415,355 @@ CASES = {
}
""", b"", b"compile-time value",
),
"fallible_types_record_channels_select_runtime_storage": (
"""
Config :: struct { child ?type, count usize }
Failure :: struct { child type, count usize }
make proc(failed bool) Config ! Failure {
if (failed) return Failure { child = bool, count = 2 }
return Config { child = i16, count = 2 }
}
Selected proc(failed bool) type {
config :: make(failed) catch |err| Config { child = err.child, count = err.count }
T :: config.child?
return [config.count]T
}
main proc() [2]i16 {
values Selected(false) :: [40, 2]
flags Selected(true) :: [true, false]
if (!flags[0] or flags[1]) return [0, 0]
return [values[0] + values[1], i16(flags.len)]
}
""", b"[42, 2]\n", None,
),
"fallible_types_optional_success_try_and_composed_errors": (
"""
First :: enum { first }
Second :: enum { second }
choose proc(present bool, failed bool) ?type ! First {
if (failed) return .first
if (present) return i16
return null
}
forward proc(present bool, failed bool) ?type ! (First | Second) {
selected :: try choose(present, failed)
return selected
}
Selected proc(present bool, failed bool) type {
selected :: forward(present, failed) catch |err| match err {
.first: some!(bool),
.second: some!(u16),
}
return if selected |T| T else u8
}
main proc() [2]i16 {
value Selected(true, false) :: 40
missing Selected(false, false) :: 2
recovered Selected(true, true) :: true
if (!recovered) return [0, 0]
return [value + i16(missing), i16(sizeof!(Selected(false, true)))]
}
""", b"[42, 1]\n", None,
),
"fallible_types_copies_procedure_inputs_and_erased_cache_identity": (
"""
Config :: struct { child type, count usize }
Failure :: struct { child type, count usize }
good proc() Config ! Failure { return Config { child = i16, count = 2 } }
bad proc() Config ! Failure { return Failure { child = bool, count = 2 } }
pass proc(value Config ! Failure) Config ! Failure { return value }
Token proc($value Config ! Failure) type { return struct {} }
Selected proc(value Config ! Failure) type {
config :: value catch |err| Config { child = err.child, count = err.count }
return [config.count]config.child
}
exact proc($value Config ! Failure, input Selected(value)) Selected(value) { return input }
Storage proc() type {
value Config ! Failure := pass(good())
saved :: value
copied := pass(value)
value = bad()
copied = Config { child = u8, count = 3 }
if (Token(saved) != Token(good())) return bool
if (Token(value) != Token(bad())) return bool
if (Token(saved) == Token(value) or Token(saved) == Token(copied)) return bool
if (Token(saved) != Token(Config { child = i16, count = 2 })) return bool
if (Token(value) == Token(Failure { child = u8, count = 2 })) return bool
if (Token(value) == Token(Failure { child = bool, count = 3 })) return bool
if (Token(saved) == Token(Failure { child = i16, count = 2 })) return bool
return Selected(saved)
}
main proc() i16 {
values Storage() :: exact(good(), [40, 2])
flags :: exact(bad(), [true, false])
if (!flags[0] or flags[1]) return 0
return values[0] + values[1]
}
""", b"42\n", None,
),
"fallible_types_nested_optional_arrays_preserve_owned_replacements": (
"""
Config :: struct { child type, count usize }
Failure :: struct { child ?type, count usize }
Packet :: struct { rows [2]mut (?Config ! Failure) }
make proc() Packet {
return { rows = [
some!(Config { child = i16, count = 2 }),
Failure { child = bool, count = 3 },
] }
}
pass proc(value Packet) Packet { return value }
Storage proc() type {
value := make()
saved :: pass(value)
copied := saved
value.rows[0] = null
value.rows[1] = some!(Config { child = u8, count = 4 })
copied.rows[0] = Failure { child = null, count = 5 }
if ((value.rows[0] catch null) != null) return bool
changed :: value.rows[1] catch null
if (changed?.child != u8 or changed?.count != 4) return bool
copied.rows[0] catch |err| {
if (err.child != null or err.count != 5) return bool
yield null
}
config :: (saved.rows[0] catch null)?
saved.rows[1] catch |err| {
if (err.child? != bool or err.count != 3) return bool
return [config.count]config.child
}
return bool
}
main proc() Storage() { return [40, 2] }
""", b"[40, 2]\n", None,
),
"fallible_types_return_snapshots_and_cleanup_order": (
"""
Config :: struct { child type, count usize, trace usize }
Failure :: struct { child type, count usize, trace usize }
make proc(failed bool) Config ! Failure {
trace usize := 0
value Config ! Failure := undefined
defer {
if (trace != (if (failed) 321 else 31)) unreachable
value catch |err| {
if (err.child != u8 or err.count != 9 or err.trace != 9) unreachable
yield Config { child = u8, count = 9, trace = 9 }
}
}
defer value = Failure { child = u8, count = 9, trace = 9 }
defer trace = trace * 10 + 1
errdefer |err| {
if (err.child != bool or err.count != 2 or err.trace != 3) unreachable
trace = trace * 10 + 2
}
{ defer trace = trace * 10 + 3 }
value = Config { child = i16, count = 2, trace = trace }
if (failed) value = Failure { child = bool, count = 2, trace = trace }
return value
}
Storage proc() type {
config :: make(false) catch { return bool }
make(true) catch |err| {
if (err.child != bool or err.count != 2) return bool
if (config.child != i16 or config.count != 2) return bool
return struct {
values [config.count]config.child
success usize = config.trace
failure usize = err.trace
}
}
return bool
}
main proc() [3]i16 {
storage Storage() :: { values = [40, 2] }
return [storage.values[0] + storage.values[1], i16(storage.success), i16(storage.failure)]
}
""", b"[42, 3, 3]\n", None,
),
"fallible_types_reject_incomplete_success_payload": (
"""
Config :: struct { child type, count usize }
Error :: enum { failed }
bad proc() Config ! Error {
value Config := undefined
value.child = i16
return value
}
Selected proc() type { return (bad() catch { return bool }).child }
main proc() Selected() { return 42 }
""", b"", b"read before initialization",
),
"fallible_types_reject_incomplete_error_payload": (
"""
Failure :: struct { child type, count usize }
bad proc() type ! Failure {
value Failure := undefined
value.child = i16
return value
}
Selected proc() type { return bad() catch |err| err.child }
main proc() Selected() { return 42 }
""", b"", b"read before initialization",
),
"fallible_types_reject_uninitialized_channel": (
"""
Error :: enum { failed }
Selected proc() type {
value type ! Error := undefined
return value catch bool
}
main proc() Selected() { return 42 }
""", b"", b"read before initialization",
),
"fallible_types_reject_non_type_payload": (
"""
Error :: enum { failed }
bad proc() type ! Error { return 42 }
Selected proc() type { return bad() catch bool }
main proc() Selected() { return 42 }
""", b"", b"incompatible value type",
),
"fallible_types_reject_incompatible_error_propagation": (
"""
First :: enum { first }
Second :: enum { second }
leaf proc() type ! First { return .first }
bad proc() type ! Second { return try leaf() }
Selected proc() type { return bad() catch i16 }
main proc() Selected() { return 42 }
""", b"", b"propagated errors are not a subset",
),
"fallible_types_reject_catch_capture_mutation": (
"""
Failure :: struct { child type }
bad proc() type ! Failure { return Failure { child = i16 } }
Selected proc() type {
return bad() catch |err| {
err.child = bool
yield err.child
}
}
main proc() Selected() { return 42 }
""", b"", b"writable array element",
),
"fallible_types_reject_errdefer_capture_mutation": (
"""
Failure :: struct { child type }
bad proc() type ! Failure {
errdefer |err| { err.child = bool }
return Failure { child = i16 }
}
Selected proc() type { return bad() catch |err| err.child }
main proc() Selected() { return 42 }
""", b"", b"writable array element",
),
"fallible_types_reject_runtime_dependency": (
"""
Error :: enum { failed }
choose proc(failed bool) type ! Error {
if (failed) return .failed
return i16
}
main proc() i16 {
failed := false
T :: choose(failed) catch bool
value T :: 42
return value
}
""", b"", b"compile-time value",
),
"fallible_types_direct_catches_select_types_and_runtime_handlers": (
"""
Error :: enum { failed }
Failure :: struct { child type }
choose proc(failed bool) type ! Error {
if (failed) return .failed
return i16
}
compute proc() i16 ! Failure { return Failure { child = i16 } }
recover proc(input i16) i16 {
return compute() catch |err| {
value err.child :: input
yield value + 2
}
}
main proc() [2]i16 {
Selected :: choose(false) catch bool
Recovered :: choose(true) catch i16
first Selected :: 40
second Recovered :: 2
return [first + second, recover(40)]
}
""", b"[42, 42]\n", None,
),
"fallible_types_composed_metadata_errors_preserve_payloads_and_nominals": (
"""
Child :: distinct i16
Failure :: struct { child type, count usize }
Other :: enum { missing }
leaf proc(failed bool) type ! Failure {
if (failed) return Failure { child = Child, count = 2 }
return bool
}
forward proc(failed bool) type ! (Failure | Other) { return try leaf(failed) }
Storage proc() type {
return forward(true) catch |err| match err {
.Failure |info|: [info.count]info.child,
.missing: bool,
}
}
main proc() i16 {
values Storage() :: [40, 2]
return i16(values[0]) + i16(values[1])
}
""", b"42\n", None,
),
"fallible_types_reject_invalid_direct_catch_result": (
"""
Error :: enum { failed }
choose proc() type ! Error { return .failed }
main proc() i16 {
Selected :: choose() catch 42
return 42
}
""", b"", b"incompatible value type",
),
"fallible_types_void_completion_and_success_precedence": (
"""
Success :: enum { same, good }
Failure :: union(enum) { same type, bad type }
work proc(mode usize) void ! Failure {
errdefer |err| {
if (mode != 2) unreachable
match err { .same |T|: if (T != i16) unreachable, .bad: unreachable }
}
if (mode == 0) return
if (mode == 2) return Failure { same = i16 }
}
choose proc() Success ! Failure { return .same }
Storage proc() type {
work(0) catch { return bool }
work(1) catch { return bool }
tag :: choose() catch { return bool }
if (tag != .same) return bool
work(2) catch |err| {
return match err { .same |T|: T, .bad: bool }
}
return bool
}
main proc() Storage() { return 42 }
""", b"42\n", None,
),
"fallible_types_reject_backed_enum_metadata_composition": (
"""
Failure :: struct { child type }
Other :: enum(i16) { failed = 1 }
bad proc() type ! (Failure | Other) { return i16 }
Selected proc() type { return bad() catch bool }
main proc() Selected() { return 42 }
""", b"", b"sum composition requires compatible",
),
"type_records_undefined_nested_updates_preserve_partial_copies": (
"""
Config :: struct {