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
+9 -3
View File
@@ -105,11 +105,17 @@ sentinel slices use `[;sentinel]T` and checked construction such as `pointer[..l
optionals use `?T`; values of `T` and `null` initialize optional storage, parameters, results and aggregate fields. `== null` and `!= null` test presence. postfix `value?` unwraps or traps, while `if value |payload|` and `while value |payload|` bind a payload only inside the taken body; loop guards are re-evaluated and `continue` returns to the guard. optional data/function pointers use a nullable pointer representation; other optionals retain a separate presence flag and by-value payload. optional pointer weakening preserves aliases and readonly restrictions.
fallible values use explicit `T ! E` channels, where `E` is an enum, record or tagged union. success and error values initialize storage, arguments and returns; fallible values also work in arrays, records and callbacks. `void ! E` functions may fall through successfully. native unbacked enums and tagged unions compose with `A | B`, including grouped/imported error sets; compatible subsets widen without changing variant identities. immutable match subjects narrow within explicit/grouped arms and final `else` arms, allowing residual errors to propagate into smaller channels.
`try expression` evaluates once, extracts success or returns a compatible error from the enclosing fallible function. `expression catch [|err|] fallback` evaluates only the selected path; fallbacks may be expressions, terminal statement blocks or, for void success, blocks that fall through. inferred `T!` signatures and yielding catch blocks are not implemented. write `builtin!(...)` without a gap before `!`; in general expressions a spaced `T ! (E)` denotes a fallible type.
`defer statement` and `errdefer [|err|] statement` register reached cleanup in the current block; braces introduce explicit nested scopes. cleanup runs LIFO when its scope exits, including return, break and continue; return values are evaluated and copied before cleanup. `errdefer` requires a fallible function and runs only on a language-error return while its scope remains active. cleanup cannot return, propagate with `try` or transfer control out of itself. source errors remain ordinary values: catch does not intercept diagnostic traps or compiler allocation/IO failures, and traps do not unwind source cleanup. a fallible `main` prints its success value normally; an unhandled error prints nothing and exits with status `1`, without creating a diagnostic.
`sizeof!(T)` and `alignof!(T)` report the supported target's native layout, including aggregate padding, slice descriptors and nullable pointers. `ptrcast!(value)` takes its full result pointer type from context; `ptrcast!(T, value)` states it explicitly. casts preserve the storage location and optional presence, permit byte reinterpretation, and cannot remove readonly qualification. `constcast!` changes pointer or slice mutability without changing the pointee type; it does not make genuinely readonly storage writable or extend its lifetime. `anyopaque` is a pointer-only pointee, not a storable or dereferenceable value.
`memcopy!(destination, source)` copies matching typed slices or pointers to arrays, checking equal lengths and rejecting nonempty overlap. `memset!(destination, value)` repeats complete typed values, including aggregate fields, rather than repeating a byte. destinations require writable elements; empty and zero-sized copies/sets do not access storage. interpreter byte views synchronize with the original typed storage so casts and memory operations preserve aliases.
native external declarations use `malloc c_func(size c_ulong) ?*mut anyopaque`, without a body. supported signatures contain concrete integer/float/boolean scalars, data pointers, optional data pointers and `void` results; aggregate C ABI and variadics remain outside this gate. Darwin `c_char`/`c_uchar`, `c_short`/`c_ushort`, `c_int`/`c_uint`, `c_long`/`c_ulong` and `c_longlong`/`c_ulonglong` map to the corresponding 8/16/32/64-bit integer representations. executable checks cover `malloc`, `realloc`, `free` and `posix_memalign` through allocator-style callbacks. foreign calls trap only when reached in the interpreter; arbitrary interpreter FFI is not supported. compiling the existing generic/fallible standard library remains a later gate.
native external declarations use `malloc c_func(size c_ulong) ?*mut anyopaque`, without a body. supported signatures contain concrete integer/float/boolean scalars, data pointers, optional data pointers and `void` results; aggregate C ABI and variadics remain outside this gate. Darwin `c_char`/`c_uchar`, `c_short`/`c_ushort`, `c_int`/`c_uint`, `c_long`/`c_ulong` and `c_longlong`/`c_ulonglong` map to the corresponding 8/16/32/64-bit integer representations. executable checks cover `malloc`, `realloc`, `free` and `posix_memalign` through allocator-style callbacks. foreign calls trap only when reached in the interpreter; arbitrary interpreter FFI is not supported. compiling the existing generic standard library remains a later gate.
records use `Point :: struct { x i16, y i16 = 2 }`, with comma- or newline-separated fields. named record declarations have nominal identity; two separately declared records are not interchangeable. construct them with `Point { x = 40 }` or a contextual keyed literal, such as `point Point := { x = 40 }`. omitted fields use their declared defaults; missing required fields, duplicate/unknown fields, incompatible values and recursive by-value layouts are diagnosed. explicit initializers run in source order, followed by omitted defaults in declaration order. an explicit value overrides its field's default without evaluating that default.
@@ -121,7 +127,7 @@ enums use `Kind :: enum { number, empty }`, qualified members such as `Kind.numb
untagged `Bits :: union { integer u64, real f64 }` overlays its members in shared storage. keyed construction selects exactly one member; reading another member reinterprets the same bytes. tagged unions use `union(enum)` or `union(Kind)`; an explicit enum must contain every union variant. construct them with `Choice { number = 42 }`, contextual `.number{42}`, `.point{x = 40, y = 2}` for record payloads, or `.empty` for `void` payloads. field reads/writes validate the active tag. nested aggregates copy by value, including across calls. CLI union results print `{payload}` (or `{}` for a void payload); untagged results print the first member.
`match value { .number |n|: n + 2, .empty: 0 }` selects one arm after evaluating its subject once. matches support enums, tagged unions, numeric/boolean scalars, comma-separated patterns, numeric ranges, final `else` arms, arm-local payload captures, statement bodies and expression results. checking rejects missing coverage, duplicate/overlapping patterns, unreachable arms and incompatible captures/results. block arms can return or transfer loop control; yielding value blocks, pointer captures, inline specialization and composed sum types belong to later gates.
`match value { .number |n|: n + 2, .empty: 0 }` selects one arm after evaluating its subject once. matches support enums, tagged unions, composed error sets, numeric/boolean scalars, comma-separated patterns, numeric ranges, final `else` arms, arm-local payload captures, statement bodies and expression results. checking rejects missing coverage, duplicate/overlapping patterns, unreachable arms and incompatible captures/results. block arms can return or transfer loop control; yielding value blocks, pointer captures and inline specialization belong to later gates.
`while` repeats a block or single statement while its `bool` condition holds, re-evaluating the condition on every iteration. guards follow the same braced/braceless convention as `if`. loop bodies introduce child scopes; their locals, including locals in nested conditional arms, begin uninitialized on each scope entry.
@@ -143,7 +149,7 @@ resolver scopes use `@std/hashmap.AutoHashMap(K, V)`, which shares `init`, `put`
`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, package namespace, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. `sema.resolve_packages` additionally borrows package roots and import links, predeclares every package before resolving qualified uses, and keeps import aliases in their originating source file. selected declarations have the same binding identity as unqualified declarations. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax and failed imports retain their existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration.
type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types, pointer/container/record/enum descriptors and construction mappings; query with `check.type_of`, `check.pointer_type`, `check.array_type`, `check.record_type`, `check.enum_type`, `check.field_index` and `check.initializer_field`, and release with `check.deinit`. `ArrayType.kind` distinguishes fixed arrays, slices and optionals; its element records the slice element or optional payload, with mutability and sentinel metadata for slices. declarations infer their initializer types, including forward references and function aliases; function types identify an AST declaration or explicit signature node supplying the callable signature. annotations support builtin types, data pointers, callback signatures, fixed arrays, slices, optionals and local/imported records, enums, unions and distinct/alias types; type names are not ordinary runtime values. `void` is valid as a function result or tagged-union payload, not as a parameter or standalone stored value.
type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types, pointer/container/record/enum descriptors and construction mappings; its result retains an AST borrow for callable-signature compatibility queries. query with `check.type_of`, `check.pointer_type`, `check.array_type`, `check.record_type`, `check.enum_type`, `check.field_index` and `check.initializer_field`, and release with `check.deinit` before releasing the AST. `ArrayType.kind` distinguishes fixed arrays, slices, optionals and fallibles; `element` records their element/success type, `error_type` the fallible error channel, with mutability and sentinel metadata for slices. declarations infer their initializer types, including forward references and function aliases; function types identify an AST declaration or explicit signature node supplying the callable signature. annotations support builtin types, data pointers, callback signatures, fixed arrays, slices, optionals, fallibles and local/imported records, enums, unions and distinct/alias types; type names are not ordinary runtime values. `void` is valid as a function result, success channel or tagged-union payload, not as a parameter or standalone stored value.
function fallthrough obligations are separate from body types. `check.fallthrough_diagnostic(&result, function)` accepts a function declaration's `NodeId` and returns its original missing-return diagnostic ID, or `null` when no obligation exists. nested body errors retain their original identities. lowering consumes this fact without interpreting diagnostic codes and places its trap only on a surviving fallthrough path; `check.deinit` releases the obligation storage.
+6 -3
View File
@@ -14,6 +14,9 @@ storage; explicit callback signatures, many-pointer addition, mutable/sentinel
slices, optionals and layout/cast/memory builtins work through both execution paths.
native scalar/pointer `c_func` declarations support allocator-required libc calls;
executed foreign calls remain diagnostic traps in the interpreter.
explicit fallible channels, composed error sets, `try`/`catch` and scoped LIFO
`defer`/`errdefer` run through both paths; source errors are values, not diagnostic
traps or compiler infrastructure failures.
the driver retains frontend storage and source provenance through
`@source/compilation`, with compilation-wide AST node identities.
file commands retain single-file behavior; directory commands compile the
@@ -43,12 +46,12 @@ test discovery and FFI remain outside these completed slices.
- [x] concrete data models: required integer widths/conversions, records/tuples and construction, fields, enums, distinct/alias types, unions and matching. examples: `source/lexer/token.hon`, `source/sema/resolve.hon`, `std/mem/mem.hon`.
- [x] native integer widths (`i8`–`i64`, `u8`–`u64`, `isize`/`usize`) and checked explicit numeric conversions, including float truncation/range checks and integer-to-float rounding. preserve exact types through arrays, calls and range captures; retain strict implicit conversion rules.
- [x] records/tuples, typed/contextual/anonymous construction, field defaults and reads/writes, nested aggregate value copies, imported record types and constructors through interpreter/native execution.
- [x] nominal enums and distinct types, transparent declaration/type aliases, overlapping untagged/tagged unions, contextual payload construction, active-field checks and exhaustive scalar/enum/tagged-union matching through interpreter/native execution. pointer captures, inline specialization, sum composition and yielding value blocks remain with their respective later gates.
- [x] nominal enums and distinct types, transparent declaration/type aliases, overlapping untagged/tagged unions, contextual payload construction, active-field checks and exhaustive scalar/enum/tagged-union matching through interpreter/native execution. pointer captures, inline specialization and yielding value blocks remain with their respective later gates; error-set composition is covered below.
- [x] memory representation: source pointers/references, mutable and sentinel slices, optionals, layout/cast builtins and allocator-required libc calls. internal IR array addresses are not source pointers. examples: `std/mem/mem.hon`, `std/arraylist/arraylist.hon`.
- [x] source data pointers/references: `@T`/`*T`, `mut`, address-taking and dereference, pointer-aware fields/indexing, array `.len`/`.ptr`, readonly weakening, location equality, recursive pointer-linked records and shared pointees across interpreter/native calls and aggregate copies.
- [x] function-pointer signatures: explicit `@proc(...) Result`, structural signature checking, callbacks in parameters/results/records/arrays, vtable calls and imported signature aliases; many-pointer `+ usize` arithmetic preserves pointee type, mutability, stride and location identity through interpreter/native execution.
- [x] mutable/sentinel slices with checked bounds, borrowed aliases and string views; optional values/pointers/callbacks, null checks, unwrap and conditional/loop captures; target layout, pointer/const casts and typed memory copy/set through interpreter/native execution. scalar/pointer C ABI declarations build/run allocator callbacks using `malloc`, `realloc`, `free` and `posix_memalign`; arbitrary interpreter FFI, generic/fallible standard-library compilation and native startup/IO remain with later gates.
- [ ] fallible results and cleanup: error-set composition, `try`/`catch`, `defer`/`errdefer`; infrastructure errors remain distinct from diagnostic traps. examples: `source/main.hon:41`, `source/sema/check/check.hon:117`.
- [x] mutable/sentinel slices with checked bounds, borrowed aliases and string views; optional values/pointers/callbacks, null checks, unwrap and conditional/loop captures; target layout, pointer/const casts and typed memory copy/set through interpreter/native execution. scalar/pointer C ABI declarations build/run allocator callbacks using `malloc`, `realloc`, `free` and `posix_memalign`; arbitrary interpreter FFI, generic standard-library compilation and native startup/IO remain with later gates.
- [x] fallible results and cleanup: explicit `T ! E`, error-set composition, `try`/`catch`, `defer`/`errdefer`, error captures and matched error residuals through interpreter/native execution; infrastructure errors remain distinct from diagnostic traps. fallible `void` may complete on fallthrough; unhandled entry errors exit with status `1`. inferred `T!` signatures and yielding catch blocks remain with inference/value-control-flow work. examples: `source/main.hon`, `source/sema/check/check.hon`.
- [ ] generic specialization and compile-time execution/reflection sufficient for existing `ArrayList`, hash maps, `enummap`, `meta` and `io.print`; include type/value parameters and compile-time generated storage.
- [ ] remaining source constructs: collection/index iteration, expression-valued control flow and `yield`, compound updates and multiline strings. 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.
+124 -6
View File
@@ -1,17 +1,17 @@
#! grammar:
#! * type ..... -> identifier {'.' member} | '?' type | ('@' | '*') ['mut'] type | '@proc' '(' parameters ')' type | '[' (expr | '_') ']' ['mut'] type | '[' [';' expr] ']' ['mut'] type | 'struct' '{' fields '}' | 'enum' ['(' type ')'] '{' members '}' | 'union' ['(' (type | 'enum') ')'] '{' fields '}' | ('distinct' | 'alias') type
#! * type ..... -> type_sum ['!' type]; type_sum -> type_atom {'|' type_atom}; type_atom includes '(' type ')', identifier {'.' member}, '?' type_atom, ('@' | '*') ['mut'] type_atom, '@proc' '(' parameters ')' type, '[' (expr | '_') ']' ['mut'] type_atom, '[' [';' expr] ']' ['mut'] type_atom, record/enum/union declarations, and ('distinct' | 'alias') type.
#! * binding ... -> :: | :=
#! * literal .. -> int | float | string | 'true' | 'false' | 'null'
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or'
#! * expr ..... -> identifier | literal | type | '.' member | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!' | '&') expr | expr ('^' | '?') | expr op expr | expr '(' [expr {',' expr} [',']] ')' | identifier '!' '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '[' [expr] '..' [expr] [';' expr] ']' | expr '.' (member | int) | expr ('..' | '..=') expr | 'match' expr '{' match_arms '}'
#! * expr ..... -> identifier | literal | type | '.' member | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!' | '&' | 'try') expr | expr ('^' | '?') | expr op expr | expr '(' [expr {',' expr} [',']] ')' | identifier '!' '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '[' [expr] '..' [expr] [';' expr] ']' | expr '.' (member | int) | expr ('..' | '..=') expr | 'match' expr '{' match_arms '}' | expr 'catch' ['|' identifier '|'] (expr | block)
#! * decl ..... -> identifier [type] binding expr | identifier 'c_func' '(' parameters ')' type | ['test'] [identifier '::'] 'import' string
#! * block .... -> '{' {stmt} '}'
#! * arm ...... -> block | stmt (single statements become canonical blocks)
#! * match_arm -> (expr {',' expr} ['|' identifier '|'] | 'else') ':' (block | stmt | expr); arms are comma/newline separated.
#! enum members and tagged-union variants accept keyword names; member names do not enter lexical scopes.
#! * stmt ..... -> decl | expr | expr '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr ['|' identifier '|'] arm ['else' arm] | 'while' expr ['|' identifier '|'] arm | 'for' expr '|' ['@'] identifier [',' identifier] '|' arm
#! * stmt ..... -> decl | expr | block | ('defer' | 'errdefer' ['|' identifier '|']) arm | expr '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr ['|' identifier '|'] arm ['else' arm] | 'while' expr ['|' identifier '|'] arm | 'for' expr '|' ['@'] identifier [',' identifier] '|' arm
#! braceless guards/iterables require parentheses unless they are calls or optional captures; else binds to the nearest if.
#! precedence (tightest first): calls/indices/selections/dereference/unwrap, unary -/!/& higher than multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges. compound range endpoints require their own parentheses.
#! precedence (tightest first): calls/indices/selections/dereference/unwrap, unary -/!/&/try higher than multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges/catch. type composition | binds tighter than fallible !; pointer/optional/array prefixes bind to type atoms. compound range endpoints require their own parentheses.
#! binary operators associate left; unary operators associate right.
import "@std/debug"
@@ -63,6 +63,10 @@ ExprKind :: enum {
slice
builtin_call
literal_null
fallible_type
sum_type
try_expr
catch_expr
}
DeclKind :: enum {
@@ -82,6 +86,9 @@ StmtKind :: enum {
if_stmt
while_stmt
for_stmt
defer_stmt
errdefer_stmt
block_stmt
invalid
}
@@ -155,6 +162,13 @@ NodeKind :: enum {
expr_slice
expr_builtin_call
literal_null
expr_fallible_type
expr_sum_type
expr_try
expr_catch
stmt_defer
stmt_errdefer
stmt_block
}
@hide:file
@@ -239,6 +253,20 @@ BinaryExprView :: struct {
right NodeId
}
CatchExprView :: struct {
span Span
operand NodeId
capture ?NodeId
handler NodeId
handler_is_block bool
}
CleanupView :: struct {
span Span
body NodeId
capture ?NodeId
}
CallExprView :: struct {
span Span
callee NodeId
@@ -485,6 +513,41 @@ add_binary proc(
}, span)
}
add_binary_type proc(
builder @mut Builder, span Span, left, right NodeId, fallible bool,
) NodeId ! mem.AllocError {
kind NodeKind :: if (fallible) .expr_fallible_type else .expr_sum_type
return try add_node(builder, Node{
kind = kind,
main = NodeData{ binary_operator = .add },
data0 = NodeData{ node_id = left },
data1 = NodeData{ node_id = right },
}, span)
}
add_try_expr proc(builder @mut Builder, span Span, operand NodeId) NodeId ! mem.AllocError {
return try add_node(builder, Node{
kind = .expr_try,
main = NodeData{ node_id = operand },
}, span)
}
add_catch_expr proc(
builder @mut Builder, span Span, operand NodeId, capture ?NodeId,
handler NodeId, handler_is_block bool,
) NodeId ! mem.AllocError {
capture_id :: if (capture) |id| u32(id) else u32(no_node)
block_flag :: if (handler_is_block) u32(1) else u32(0)
extra :: try add_extra(builder, capture_id)
_ = try add_extra(builder, block_flag)
return try add_node(builder, Node{
kind = .expr_catch,
main = NodeData{ extra = extra },
data0 = NodeData{ node_id = operand },
data1 = NodeData{ node_id = handler },
}, span)
}
add_range proc(
builder @mut Builder, span Span, start NodeId, end NodeId, inclusive bool,
) NodeId ! mem.AllocError {
@@ -735,6 +798,25 @@ add_expression_stmt proc(builder @mut Builder, expression NodeId) NodeId ! mem.A
}, builder_span(builder, expression))
}
add_cleanup_stmt proc(
builder @mut Builder, span Span, body NodeId, capture ?NodeId, error_only bool,
) NodeId ! mem.AllocError {
kind NodeKind :: if (error_only) .stmt_errdefer else .stmt_defer
capture_id :: if (capture) |id| id else no_node
return try add_node(builder, Node{
kind = kind,
main = NodeData{ node_id = body },
data0 = NodeData{ node_id = capture_id },
}, span)
}
add_block_stmt proc(builder @mut Builder, body NodeId) NodeId ! mem.AllocError {
return try add_node(builder, Node{
kind = .stmt_block,
main = NodeData{ node_id = body },
}, builder_span(builder, body))
}
add_return_stmt proc(builder @mut Builder, span Span, value ?NodeId) NodeId ! mem.AllocError {
raw_value :: if (value) |id| id else no_node
return try add_node(builder, Node{
@@ -1041,6 +1123,9 @@ stmt_kind proc(tree @Tree, id NodeId) StmtKind {
.stmt_assignment: .assignment_stmt
.stmt_while: .while_stmt
.stmt_for: .for_stmt
.stmt_defer: .defer_stmt
.stmt_errdefer: .errdefer_stmt
.stmt_block: .block_stmt
.invalid_stmt: .invalid
else: unreachable
}
@@ -1052,6 +1137,22 @@ statement_expr proc(tree @Tree, id NodeId) NodeId {
return node.main.node_id
}
cleanup_stmt proc(tree @Tree, id NodeId) CleanupView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .stmt_defer or node.kind == .stmt_errdefer)
return CleanupView{
span = get_span(&tree.storage, id),
body = node.main.node_id,
capture = maybe_node_id(u32(node.data0.node_id)),
}
}
statement_block proc(tree @Tree, id NodeId) NodeId {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .stmt_block)
return node.main.node_id
}
return_stmt proc(tree @Tree, id NodeId) ReturnView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .stmt_return)
@@ -1234,6 +1335,10 @@ expression_kind proc(kind NodeKind) ExprKind {
.expr_slice: .slice
.expr_builtin_call: .builtin_call
.literal_null: .literal_null
.expr_fallible_type: .fallible_type
.expr_sum_type: .sum_type
.expr_try: .try_expr
.expr_catch: .catch_expr
else: unreachable
}
}
@@ -1250,7 +1355,7 @@ unary_expr proc(tree @Tree, id NodeId) UnaryExprView {
binary_expr proc(tree @Tree, id NodeId) BinaryExprView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_binary)
debug.assert(node.kind == .expr_binary or node.kind == .expr_fallible_type or node.kind == .expr_sum_type)
return BinaryExprView{
span = get_span(&tree.storage, id),
operator = node.main.binary_operator,
@@ -1259,6 +1364,19 @@ binary_expr proc(tree @Tree, id NodeId) BinaryExprView {
}
}
catch_expr proc(tree @Tree, id NodeId) CatchExprView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_catch)
extra :: usize(node.main.extra)
return CatchExprView{
span = get_span(&tree.storage, id),
operand = node.data0.node_id,
capture = maybe_node_id(tree.storage.extra.items[extra]),
handler = node.data1.node_id,
handler_is_block = tree.storage.extra.items[extra + 1] != 0,
}
}
range_expr proc(tree @Tree, id NodeId) RangeView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_range)
@@ -1403,7 +1521,7 @@ union_type proc(tree @Tree, id NodeId) UnionTypeView {
type_operand proc(tree @Tree, id NodeId) NodeId {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .expr_distinct_type or node.kind == .expr_alias or node.kind == .expr_optional_type)
debug.assert(node.kind == .expr_distinct_type or node.kind == .expr_alias or node.kind == .expr_optional_type or node.kind == .expr_try)
return node.main.node_id
}
+16
View File
@@ -99,6 +99,14 @@ Code :: enum {
sentinel_mismatch
overlapping_memory_copy
unsupported_foreign_call
invalid_fallible_type
invalid_sum_type
invalid_fallible_operand
try_outside_fallible
incompatible_propagated_error
catch_type_mismatch
errdefer_outside_fallible
cleanup_control_flow
}
Diagnostic :: struct {
@@ -219,5 +227,13 @@ messages :: $enummap.init(Code, []u8, {
sentinel_mismatch = "slice sentinel does not match the expected value",
overlapping_memory_copy = "memory copy source and destination overlap",
unsupported_foreign_call = "foreign function calls are unsupported by the interpreter",
invalid_fallible_type = "fallible type requires a concrete success type and an enum, struct, or tagged union error channel",
invalid_sum_type = "sum composition requires compatible named enum, struct, or tagged union variants",
invalid_fallible_operand = "try and catch require a fallible operand",
try_outside_fallible = "try requires an enclosing fallible function",
incompatible_propagated_error = "propagated errors are not a subset of the function error channel",
catch_type_mismatch = "catch handler must produce the success type or terminate",
errdefer_outside_fallible = "errdefer requires an enclosing fallible function",
cleanup_control_flow = "cleanup cannot escape with return, break, continue, or try",
})
+1
View File
@@ -219,6 +219,7 @@ Function :: struct {
locals arraylist.ArrayList(Type)
result Type = .void
result_array_type ArrayId = no_array
result_fallible bool = false
array_storage arraylist.ArrayList(ArrayStorage)
aggregate_cells usize = 0
has_addresses bool = false
+4
View File
@@ -26,6 +26,10 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
{ "return", .return },
{ "break", .break },
{ "continue", .continue },
{ "try", .try },
{ "catch", .catch },
{ "defer", .defer },
{ "errdefer", .errdefer },
{ "if", .if },
{ "for", .for },
{ "else", .else },
+2
View File
@@ -23,6 +23,8 @@ TokenKind :: enum {
match
proc, return
break, continue
try, catch
defer, errdefer
true, false
and, or
struct, enum, union
+26 -2
View File
@@ -118,8 +118,32 @@ emit proc(
try io.write_all(output, " ")
try function_symbol(output, module, entry)
try io.write_all(output, "()\n")
if (result.kind != .void and result.kind != .invalid) {
try print_call(output, module, result, 0)
printable := result
operand usize := 0
if (function.result_fallible) {
shape :: module.arrays.items[usize(result.shape)]
success :: shape.fields[0]
try io.write_all(output, " %result = alloca ")
try write_type(output, module, result)
try io.write_all(output, "\n store ")
try write_type(output, module, result)
try io.write_all(output, " %r0, ptr %result\n %tag = load ")
try write_type(output, module, Type{ kind = shape.tag_type })
try io.write_all(output, ", ptr %result\n %success = icmp eq ")
try write_type(output, module, Type{ kind = shape.tag_type })
try io.write_all(output, " %tag, ")
try write_constant(output, module, success.tag)
try io.write_all(output, "\n br i1 %success, label %ok, label %error\nerror:\n call void @report(i64 -1)\n ret i32 1\nok:\n")
printable = Type{ kind = success.type, shape = success.array_type }
operand = 1
if (printable.kind != .void) {
try io.print(" %payload = getelementptr i8, ptr %result, i64 {}\n %r1 = load ", output, {shape.payload_offset})
try write_type(output, module, printable)
try io.write_all(output, ", ptr %payload\n")
}
}
if (printable.kind != .void and printable.kind != .invalid) {
try print_call(output, module, printable, operand)
try io.write_all(output, " call void @out(i32 1, ptr @newline, i64 1)\n")
}
try io.write_all(output, " call void @report(i64 -1)\n")
+383 -10
View File
@@ -21,6 +21,7 @@ Storage :: struct {
LoopTargets :: struct {
continue_target ir.BlockId
exit ir.BlockId
cleanup_depth usize
}
@hide:file
@@ -33,12 +34,15 @@ State :: struct {
storage []mut Storage
arrays []mut ir.ArrayId
arguments arraylist.ArrayList(ir.ValueId)
cleanups arraylist.ArrayList(ast.NodeId)
records []mut ir.ArrayId
function usize = 0
block ir.BlockId = ir.BlockId(0)
terminated bool = false
loop ?LoopTargets = null
return_type typecheck.Type = typecheck.Type{}
fallthrough_block ?ast.NodeId = null
fallthrough_diagnostic ?diagnostics.DiagnosticId = null
}
#! lowers checked source into caller-owned executable storage; release with ir.deinit.
@@ -90,8 +94,10 @@ lower_package proc(
arrays = arrays,
records = records,
arguments = arraylist.init(ir.ValueId, allocator),
cleanups = arraylist.init(ast.NodeId, allocator),
}
defer arraylist.deinit(&state.arguments)
defer arraylist.deinit(&state.cleanups)
# allocate every package identity before lowering any initializer or body.
for (0..ast.package_decl_count(tree)) |i| {
@@ -138,6 +144,7 @@ lower_type proc(state @State, type typecheck.Type) ir.Type {
return lower_type(state, typecheck.Type{ kind = typecheck.enum_type(state.checked, type.enum_type).backing })
}
if (type.kind == .slice) return .record
if (type.kind == .fallible) return .record
if (type.kind == .optional) return lower_optional_type(state, typecheck.array_type(state.checked, type.array_type).element)
return match type.kind {
.int: .int
@@ -160,7 +167,7 @@ lower_type proc(state @State, type typecheck.Type) ir.Type {
.function: .function
.enumeration: unreachable
.none, .invalid, .range, .type_value, .null, .anyopaque: .invalid
.slice, .optional: unreachable
.slice, .optional, .fallible: unreachable
}
}
@@ -173,7 +180,7 @@ lower_optional_type proc(state @State, child typecheck.Type) ir.Type {
@hide:file
array_shape proc(state @mut State, type typecheck.Type) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) {
if (type.kind == .record) return try prepare_record(state, type.record_type)
if (type.kind == .slice or type.kind == .optional) return try prepare_container(state, type)
if (type.kind == .slice or type.kind == .optional or type.kind == .fallible) return try prepare_container(state, type)
if (type.kind != .array) return ir.no_array
# checker history includes unresolved shapes; materialize only final used types.
return try prepare_array(state, type.array_type)
@@ -202,6 +209,23 @@ prepare_container proc(state @mut State, type typecheck.Type) ir.ArrayId ! (mem.
if (type.kind == .optional and (descriptor.element.kind == .pointer or descriptor.element.kind == .function)) return ir.no_array
index :: usize(type.array_type)
if (state.arrays[index] != ir.no_array) return state.arrays[index]
if (type.kind == .fallible) {
fields [2]ir.RecordField :: [
{
type = lower_type(state, descriptor.element),
array_type = try array_shape(state, descriptor.element),
tag = ir.Value{ type = .int, data = ir.ValueData{ integer = 0 } },
},
{
type = lower_type(state, descriptor.error_type),
array_type = try array_shape(state, descriptor.error_type),
tag = ir.Value{ type = .int, data = ir.ValueData{ integer = 1 } },
},
]
shape :: try ir.add_union(state.module, fields[..], true)
state.arrays[index] = shape
return shape
}
fields [2]mut ir.RecordField := [{ type = .pointer }, { type = .usize }]
if (type.kind == .optional) {
fields[0] = ir.RecordField{ type = .bool }
@@ -354,9 +378,13 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di
state.function = usize(state.storage[usize(id)].index)
state.block = ir.BlockId(0)
state.terminated = false
state.loop = null
state.fallthrough_block = declaration.body
state.fallthrough_diagnostic = typecheck.fallthrough_diagnostic(state.checked, id)
state.return_type = typecheck.type_of(state.checked, declaration.return_type)
state.module.functions.items[state.function].result = lower_type(state, typecheck.type_of(state.checked, declaration.return_type))
state.module.functions.items[state.function].result_array_type = try array_shape(state, typecheck.type_of(state.checked, declaration.return_type))
state.module.functions.items[state.function].result_fallible = state.return_type.kind == .fallible
for (0..ast.function_param_count(tree, id)) |i| {
parameter :: ast.function_param(tree, id, i)
type :: typecheck.type_of(state.checked, parameter)
@@ -389,7 +417,9 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di
trap(state, diagnostic, ast.node_span(tree, declaration.body))
return
}
finish(state, ir.no_value, ast.node_span(tree, declaration.body))
value := ir.no_value
if (state.return_type.kind == .fallible) value = try wrap_fallible(state, value, state.return_type, false, ast.node_span(tree, declaration.body))
finish(state, value, ast.node_span(tree, declaration.body))
}
@hide:file
@@ -433,6 +463,12 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocErro
}
try predeclare_locals(state, loop.body)
}
.block_stmt: try predeclare_locals(state, ast.statement_block(state.tree, statement))
.defer_stmt, .errdefer_stmt: {
cleanup :: ast.cleanup_stmt(state.tree, statement)
if (cleanup.capture) |capture| try predeclare_capture(state, capture)
try predeclare_locals(state, cleanup.body)
}
else: {}
}
}
@@ -453,6 +489,8 @@ store_capture proc(state @mut State, capture ast.NodeId, value ir.ValueId, optio
@hide:file
lower_block proc(state @mut State, block ast.NodeId, fresh_scope bool) void ! (mem.AllocError | diagnostics.CapacityError) {
cleanup_depth :: state.cleanups.items.len
defer state.cleanups.items = state.cleanups.items.ptr[..cleanup_depth]
# reset every direct declaration before control can return or trap within the scope.
if (fresh_scope) {
for (0..ast.block_statement_count(state.tree, block)) |i| {
@@ -471,6 +509,76 @@ lower_block proc(state @mut State, block ast.NodeId, fresh_scope bool) void ! (m
if (state.terminated) return
try lower_statement(state, ast.block_statement(state.tree, block, i))
}
if (!state.terminated) {
if (state.fallthrough_block) |function_body| {
if (function_body == block) {
if (state.fallthrough_diagnostic) |diagnostic| {
trap(state, diagnostic, ast.node_span(state.tree, block))
return
}
}
}
}
if (!state.terminated) try lower_cleanups(state, cleanup_depth, ir.no_value)
}
@hide:file
lower_cleanups proc(state @mut State, depth usize, error_value ir.ValueId) void ! (mem.AllocError | diagnostics.CapacityError) {
index := state.cleanups.items.len
while (index > depth) {
index -= 1
id :: state.cleanups.items[index]
if (ast.stmt_kind(state.tree, id) == .errdefer_stmt and error_value == ir.no_value) continue
cleanup :: ast.cleanup_stmt(state.tree, id)
if (cleanup.capture) |capture| {
_ = try emit(state, ir.Instruction{
op = .store_local, left = error_value, slot = state.storage[usize(capture)].index, span = cleanup.span,
})
}
try lower_block(state, cleanup.body, true)
if (state.terminated) return
}
}
@hide:file
wrap_fallible proc(state @mut State, value ir.ValueId, type typecheck.Type, failed bool, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
slot :: if (failed) u32(1) else u32(0)
shape :: try array_shape(state, type)
return try emit(state, ir.Instruction{
op = .union_literal, type = .record, array_type = shape, slot = slot,
left = value, constant = state.module.arrays.items[usize(shape)].fields[usize(slot)].tag, span = span,
})
}
@hide:file
fallible_failed proc(state @mut State, value ir.ValueId, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
tag :: try emit(state, ir.Instruction{ op = .union_tag, type = .int, left = value, span = span })
error_tag :: try constant(state, ir.Value{ type = .int, data = ir.ValueData{ integer = 1 } }, span)
return try emit(state, ir.Instruction{ op = .equal, type = .bool, left = tag, right = error_tag, span = span })
}
@hide:file
lower_return proc(state @mut State, value ir.ValueId, span diagnostics.Span) void ! (mem.AllocError | diagnostics.CapacityError) {
if (state.return_type.kind != .fallible) {
try lower_cleanups(state, 0, ir.no_value)
if (!state.terminated) finish(state, value, span)
return
}
failed :: try fallible_failed(state, value, span)
success :: try add_block(state)
failure :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = failed, target = failure, alternative = success, span = span,
}
state.block = failure
error_type :: typecheck.array_type(state.checked, state.return_type.array_type).error_type
error_value :: try project(state, value, lower_type(state, error_type), try array_shape(state, error_type), 1, span)
try lower_cleanups(state, 0, error_value)
if (!state.terminated) finish(state, value, span)
state.block = success
state.terminated = false
try lower_cleanups(state, 0, ir.no_value)
if (!state.terminated) finish(state, value, span)
}
@hide:file
@@ -551,7 +659,7 @@ lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagn
span = ast.node_span(state.tree, loop.condition),
}
previous_loop :: state.loop
state.loop = LoopTargets{ continue_target = guard, exit = exit }
state.loop = LoopTargets{ continue_target = guard, exit = exit, cleanup_depth = state.cleanups.items.len }
defer state.loop = previous_loop
state.block = body
if (loop.capture) |capture| try store_capture(state, capture, value, typecheck.type_of(state.checked, loop.condition), loop.span)
@@ -598,7 +706,7 @@ lower_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnos
kind = .branch, value = condition_value, target = body, alternative = exit, span = interval.span,
}
previous_loop :: state.loop
state.loop = LoopTargets{ continue_target = update, exit = exit }
state.loop = LoopTargets{ continue_target = update, exit = exit, cleanup_depth = state.cleanups.items.len }
defer state.loop = previous_loop
state.block = body
if (loop.capture) |capture| _ = trap_binding(state, capture)
@@ -633,6 +741,11 @@ lower_for proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnos
lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
tree :: state.tree
match ast.stmt_kind(tree, id) {
.block_stmt: try lower_block(state, ast.statement_block(tree, id), true)
.defer_stmt, .errdefer_stmt: {
if (trap_type(state, id)) return
try arraylist.append(&state.cleanups, id)
}
.if_stmt: try lower_if(state, id)
.while_stmt: try lower_while(state, id)
.for_stmt: try lower_for(state, id)
@@ -642,6 +755,8 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
target := targets.exit
# each loop supplies its own guard or guarded-update entry.
if (ast.stmt_kind(tree, id) == .continue_stmt) target = targets.continue_target
try lower_cleanups(state, targets.cleanup_depth, ir.no_value)
if (state.terminated) return
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = target, span = ast.node_span(tree, id),
}
@@ -712,7 +827,10 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
value := ir.no_value
if (statement.value) |expression| value = try lower_as(state, expression, state.return_type)
if (state.terminated or trap_type(state, id)) return
finish(state, value, statement.span)
if (statement.value == null and state.return_type.kind == .fallible) {
value = try wrap_fallible(state, value, state.return_type, false, statement.span)
}
try lower_return(state, value, statement.span)
}
}
}
@@ -1127,6 +1245,14 @@ lower_as proc(state @mut State, id ast.NodeId, expected typecheck.Type) ir.Value
actual :: typecheck.type_of(state.checked, id)
span :: ast.node_span(state.tree, id)
if (actual.kind == .invalid or expected.kind == .invalid) return try lower_expression(state, id)
if (expected.kind == .fallible and actual.kind != .fallible) {
descriptor :: typecheck.array_type(state.checked, expected.array_type)
failed :: !typecheck.value_assignable(state.checked, actual, descriptor.element)
channel :: if (failed) descriptor.error_type else descriptor.element
payload :: try lower_as(state, id, channel)
if (state.terminated) return ir.no_value
return try wrap_fallible(state, payload, expected, failed, span)
}
if (expected.kind == .optional and actual.kind != .optional) {
child :: typecheck.array_type(state.checked, expected.array_type).element
runtime :: lower_type(state, child)
@@ -1144,7 +1270,245 @@ lower_as proc(state @mut State, id ast.NodeId, expected typecheck.Type) ir.Value
if (state.terminated) return ir.no_value
return try record_pair(state, try array_shape(state, expected), parts.pointer, parts.bound, span)
}
return try lower_expression(state, id)
value :: try lower_expression(state, id)
if (state.terminated) return ir.no_value
return try convert_value(state, value, actual, expected, span)
}
@hide:file
convert_value proc(state @mut State, value ir.ValueId, actual, expected typecheck.Type, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
if (actual.kind == .fallible and expected.kind == .fallible) {
if (actual.array_type == expected.array_type) return value
source :: typecheck.array_type(state.checked, actual.array_type)
destination :: typecheck.array_type(state.checked, expected.array_type)
shape :: try array_shape(state, expected)
slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), .record, shape))
failed :: try fallible_failed(state, value, span)
success :: try add_block(state)
failure :: try add_block(state)
join :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = failed, target = failure, alternative = success, span = span,
}
for (0..2) |i| {
state.block = if (i == 0) success else failure
child :: if (i == 0) source.element else source.error_type
target :: if (i == 0) destination.element else destination.error_type
payload := ir.no_value
if (child.kind != .void) payload = try project(state, value, lower_type(state, child), try array_shape(state, child), u32(i), span)
converted :: try convert_value(state, payload, child, target, span)
wrapped :: try wrap_fallible(state, converted, expected, i == 1, span)
_ = try emit(state, ir.Instruction{ op = .store_local, left = wrapped, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
}
state.block = join
return try emit(state, ir.Instruction{ op = .load_local, type = .record, array_type = shape, slot = slot, span = span })
}
if (expected.kind == .optional and actual.kind != .optional) {
child :: typecheck.array_type(state.checked, expected.array_type).element
runtime :: lower_type(state, child)
nullable :: child.kind == .pointer or child.kind == .function
if (actual.kind == .null and nullable) return try null_constant(state, runtime, span)
payload := ir.no_value
if (actual.kind == .null) payload = try zero_value(state, runtime, try array_shape(state, child), span)
else payload = try convert_value(state, value, actual, child, span)
if (nullable) return payload
present :: try constant(state, ir.Value{ type = .bool, data = ir.ValueData{ boolean = actual.kind != .null } }, span)
return try record_pair(state, try array_shape(state, expected), present, payload, span)
}
if (actual.kind == .optional and expected.kind == .optional and actual.array_type != expected.array_type) {
source :: typecheck.array_type(state.checked, actual.array_type).element
child :: typecheck.array_type(state.checked, expected.array_type).element
if (source.kind == .pointer or source.kind == .function) return value
shape :: try array_shape(state, expected)
slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), .record, shape))
present :: try optional_present(state, value, actual, span)
some :: try add_block(state)
none :: try add_block(state)
join :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = present, target = some, alternative = none, span = span,
}
state.block = some
payload :: try optional_payload(state, value, actual, span)
converted :: try convert_value(state, payload, source, child, span)
wrapped :: try record_pair(state, shape, present, converted, span)
_ = try emit(state, ir.Instruction{ op = .store_local, left = wrapped, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = none
absent :: try zero_value(state, .record, shape, span)
_ = try emit(state, ir.Instruction{ op = .store_local, left = absent, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = join
return try emit(state, ir.Instruction{ op = .load_local, type = .record, array_type = shape, slot = slot, span = span })
}
if (expected.kind == .slice and actual.kind == .string) {
pointer :: try emit(state, ir.Instruction{ op = .string_pointer, type = .pointer, left = value, span = span })
length :: try emit(state, ir.Instruction{ op = .string_length, type = .usize, left = value, span = span })
return try record_pair(state, try array_shape(state, expected), pointer, length, span)
}
if (expected.kind == .slice and actual.kind == .array) {
source_shape :: try array_shape(state, actual)
slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), .array, source_shape))
_ = try emit(state, ir.Instruction{ op = .store_local, left = value, slot = slot, span = span })
address :: try emit(state, ir.Instruction{
op = .address_local, type = .address, pointee = .array, array_type = source_shape, slot = slot, span = span,
})
pointer :: try emit(state, ir.Instruction{
op = .pointer_from_address, type = .pointer, left = address, pointee = .array, array_type = source_shape, span = span,
})
length :: try usize_constant(state, typecheck.array_type(state.checked, actual.array_type).len, span)
return try record_pair(state, try array_shape(state, expected), pointer, length, span)
}
if (!typecheck.error_assignable(state.checked, actual, expected)
and !typecheck.error_assignable(state.checked, expected, actual)) return value
source_union := false
if (actual.kind == .record) source_union = typecheck.record_type(state.checked, actual.record_type).is_union
if (expected.kind == .enumeration) {
if (!source_union) return value
return try emit(state, ir.Instruction{ op = .union_tag, type = lower_type(state, expected), left = value, span = span })
}
if (expected.kind != .record) return value
destination :: typecheck.record_type(state.checked, expected.record_type)
if (!destination.is_union) return value
if (actual.kind == .record and actual.record_type == expected.record_type) return value
shape :: try array_shape(state, expected)
if (actual.kind == .record and !source_union) {
for destination.fields |field, i| {
if (field.type.kind != .record or field.type.record_type != actual.record_type or field.type.nominal != actual.nominal) continue
return try emit(state, ir.Instruction{
op = .union_literal, type = .record, array_type = shape, slot = u32(i), left = value,
constant = state.module.arrays.items[usize(shape)].fields[i].tag, span = span,
})
}
unreachable
}
discriminant := value
source_shape := ir.no_array
if (source_union) {
source_shape = try array_shape(state, actual)
tag_type :: typecheck.record_type(state.checked, actual.record_type).tag_type
discriminant = try emit(state, ir.Instruction{ op = .union_tag, type = lower_type(state, tag_type), left = value, span = span })
}
slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), .record, shape))
join :: try add_block(state)
for destination.fields |field, i| {
selected :: try add_block(state)
next :: try add_block(state)
tag :: state.module.arrays.items[usize(shape)].fields[i].tag
expected_tag :: try constant(state, tag, span)
condition :: try emit(state, ir.Instruction{ op = .equal, type = .bool, left = discriminant, right = expected_tag, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = condition, target = selected, alternative = next, span = span,
}
state.block = selected
payload := ir.no_value
if (source_union and field.type.kind != .void) {
source_fields :: state.module.arrays.items[usize(source_shape)].fields
for source_fields |source_field, j| {
if (source_field.tag.data.unsigned != tag.data.unsigned) continue
payload = try project(state, value, source_field.type, source_field.array_type, u32(j), span)
break
}
}
wrapped :: try emit(state, ir.Instruction{
op = .union_literal, type = .record, array_type = shape, slot = u32(i), left = payload, constant = tag, span = span,
})
_ = try emit(state, ir.Instruction{ op = .store_local, left = wrapped, slot = slot, span = span })
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = next
}
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ kind = .unreachable, span = span }
state.block = join
return try emit(state, ir.Instruction{ op = .load_local, type = .record, array_type = shape, slot = slot, span = span })
}
@hide:file
lower_try proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
operand :: ast.type_operand(state.tree, id)
value :: try lower_expression(state, operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
span :: ast.node_span(state.tree, id)
descriptor :: typecheck.array_type(state.checked, typecheck.type_of(state.checked, operand).array_type)
failed :: try fallible_failed(state, value, span)
success :: try add_block(state)
failure :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = failed, target = failure, alternative = success, span = span,
}
state.block = failure
payload :: try project(state, value, lower_type(state, descriptor.error_type), try array_shape(state, descriptor.error_type), 1, span)
target_error :: typecheck.array_type(state.checked, state.return_type.array_type).error_type
converted :: try convert_value(state, payload, descriptor.error_type, target_error, span)
result :: try wrap_fallible(state, converted, state.return_type, true, span)
try lower_cleanups(state, 0, converted)
if (!state.terminated) finish(state, result, span)
state.block = success
state.terminated = false
if (descriptor.element.kind == .void) return ir.no_value
return try project(state, value, lower_type(state, descriptor.element), try array_shape(state, descriptor.element), 0, span)
}
@hide:file
lower_catch proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
expression :: ast.catch_expr(state.tree, id)
value :: try lower_expression(state, expression.operand)
if (state.terminated or trap_type(state, id)) return ir.no_value
span :: expression.span
descriptor :: typecheck.array_type(state.checked, typecheck.type_of(state.checked, expression.operand).array_type)
type :: typecheck.type_of(state.checked, id)
runtime :: lower_type(state, type)
shape :: try array_shape(state, type)
slot := u32(0)
if (runtime != .void) slot = u32(try ir.add_local(state.module, ir.FunctionId(state.function), runtime, shape))
failed :: try fallible_failed(state, value, span)
success :: try add_block(state)
failure :: try add_block(state)
join :: try add_block(state)
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .branch, value = failed, target = failure, alternative = success, span = span,
}
state.block = success
if (runtime != .void) {
payload :: try project(state, value, lower_type(state, descriptor.element), try array_shape(state, descriptor.element), 0, span)
converted :: try convert_value(state, payload, descriptor.element, type, span)
_ = try emit(state, ir.Instruction{ op = .store_local, left = converted, slot = slot, span = span })
}
state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = failure
if (expression.capture) |capture| {
try predeclare_capture(state, capture)
payload :: try project(state, value, lower_type(state, descriptor.error_type), try array_shape(state, descriptor.error_type), 1, span)
_ = try emit(state, ir.Instruction{ op = .store_local, left = payload, slot = state.storage[usize(capture)].index, span = span })
}
if (!trap_type(state, expression.handler)) {
if (expression.handler_is_block) {
try predeclare_locals(state, expression.handler)
try lower_block(state, expression.handler, true)
} else {
fallback :: try lower_as(state, expression.handler, type)
if (!state.terminated and runtime != .void) _ = try emit(state, ir.Instruction{
op = .store_local, left = fallback, slot = slot, span = span,
})
}
}
if (!state.terminated) state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{
kind = .jump, target = join, span = span,
}
state.block = join
state.terminated = false
if (runtime == .void) return ir.no_value
return try emit(state, ir.Instruction{ op = .load_local, type = runtime, array_type = shape, slot = slot, span = span })
}
@hide:file
@@ -1412,7 +1776,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
# ranges have no physical value; only lower_for decomposes an iterable.
if (!trap_type(state, id)) unreachable
}
.array_type, .pointer_type, .function_type, .record_type, .enum_type, .union_type, .distinct_type, .slice_type, .optional_type: {
.array_type, .pointer_type, .function_type, .record_type, .enum_type, .union_type, .distinct_type, .slice_type, .optional_type, .fallible_type, .sum_type: {
if (!trap_type(state, id)) {
trap(state, try diagnostics.add(state.diagnostics, .type_not_value, span), span)
}
@@ -1422,6 +1786,8 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
if (!trap_type(state, id)) unreachable
}
.match_expr: return try lower_match(state, id)
.try_expr: return try lower_try(state, id)
.catch_expr: return try lower_catch(state, id)
.literal_null: return try null_constant(state, .pointer, span)
.slice: return try lower_slice(state, id)
.builtin_call: return try lower_builtin(state, id)
@@ -1544,8 +1910,15 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
type = .function,
data = ir.ValueData{ function = ir.FunctionId(location.index) },
}, span)
.global: return try emit(state, ir.Instruction{ op = .load_global, type = type, array_type = shape, slot = location.index, span = span })
.local: return try emit(state, ir.Instruction{ op = .load_local, type = type, array_type = shape, slot = location.index, span = span })
.global, .local: {
declaration_type :: typecheck.type_of(state.checked, binding.data.declaration)
operation :: if (location.kind == .global) ir.Op.load_global else ir.Op.load_local
value :: try emit(state, ir.Instruction{
op = operation, type = lower_type(state, declaration_type),
array_type = try array_shape(state, declaration_type), slot = location.index, span = span,
})
return try convert_value(state, value, declaration_type, typecheck.type_of(state.checked, id), span)
}
.none: unreachable
}
}
+33
View File
@@ -1924,3 +1924,36 @@ empty_array_entry_results_need_no_escaped_allocation test {
interpreter.deinit(&outcome)
try testing.expect_equal(usize(0), state.live)
}
fallible_source_handlers_do_not_catch_runtime_allocation_failures test {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`Error :: enum { out_of_memory }
`fail proc() int ! Error { return .out_of_memory }
`main proc() int { return fail() catch 42 }
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(usize(0), diagnostics.count(&diagnostic_store))
succeeded := false
failures := usize(0)
for (usize(0)..usize(128)) |limit| {
state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
outcome := interpreter.run(allocator, &module, &diagnostic_store) catch |error| {
try testing.expect(error == .out_of_memory)
failures += 1
try testing.expect_equal(usize(0), state.live)
try testing.expect_equal(usize(0), diagnostics.count(&diagnostic_store))
continue
}
defer interpreter.deinit(&outcome)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
interpreter.deinit(&outcome)
try testing.expect_equal(usize(0), state.live)
succeeded = true
break
}
try testing.expect(succeeded and failures > 0)
}
+12 -2
View File
@@ -187,9 +187,19 @@ run_file proc(allocator mem.Allocator, runtime io.Io, path [;0]u8) i32 ! DriverE
defer ir.deinit(&module)
outcome := try interpreter.run(allocator, &module, &diagnostic_store)
defer interpreter.deinit(&outcome)
if (outcome.trap == null) try print_result(io.stdout(runtime), &module, outcome.value)
value := outcome.value
failed := false
if (outcome.trap == null) {
if (module.entry) |entry| {
if (module.functions.items[usize(entry)].result_fallible) {
failed = ir.union_active(&module, value, 1)
if (!failed) value = ir.aggregate_item(&module, value, 0)
}
}
if (!failed) try print_result(io.stdout(runtime), &module, value)
}
try report_diagnostics(allocator, io.stderr(runtime), compilation.source_files(&session), &diagnostic_store, outcome.trap)
if (outcome.trap != null or diagnostics.count(&diagnostic_store) != 0) return 1
if (failed or outcome.trap != null or diagnostics.count(&diagnostic_store) != 0) return 1
return 0
}
+158 -33
View File
@@ -30,6 +30,7 @@ State :: struct {
next_token usize
paren_depth usize = 0
allow_record_literals bool = true
allow_sum_types bool = true
match_arm_body bool = false
}
@@ -176,6 +177,7 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError {
parse_parameters proc(
state @mut State, params @mut arraylist.ArrayList(ast.NodeId),
) void ! ParseError {
skip_newlines(state)
names arraylist.ArrayList(lexer.Token) := arraylist.init(state.allocator)
defer arraylist.deinit(&names)
while !next_is(state, .close_paren) {
@@ -187,24 +189,42 @@ parse_parameters proc(
_ = try consume(state, .comma)
}
type_id :: try parse_type(state)
type_span :: ast.builder_span(state.builder, type_id)
type_end :: state.tokens[state.next_token - 1].span.end
for (names.items) |parameter_name| {
param_id :: try ast.add_param(
state.builder,
ast.Span{ start = parameter_name.span.start, end = type_span.end, source = parameter_name.span.source },
ast.Span{ start = parameter_name.span.start, end = type_end, source = parameter_name.span.source },
parameter_name.span,
parameter_name.data.str_id,
type_id,
)
try arraylist.append(params, param_id)
}
if next_is(state, .comma) _ = try consume(state, .comma)
skip_newlines(state)
if next_is(state, .comma) {
state.next_token += 1
skip_newlines(state)
}
}
}
@hide:file
parse_block proc(state @mut State) ast.NodeId ! ParseError {
open :: try consume(state, .open_curly)
previous_depth :: state.paren_depth
previous_record_literals :: state.allow_record_literals
previous_sum_types :: state.allow_sum_types
previous_match_arm_body :: state.match_arm_body
state.paren_depth = 0
state.allow_record_literals = true
state.allow_sum_types = true
state.match_arm_body = false
defer {
state.paren_depth = previous_depth
state.allow_record_literals = previous_record_literals
state.allow_sum_types = previous_sum_types
state.match_arm_body = previous_match_arm_body
}
statements arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
defer arraylist.deinit(&statements)
@@ -249,6 +269,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
or next_n_is(state, 1, .distinct)
or next_n_is(state, 1, .at)
or next_n_is(state, 1, .question)
or (next_n_is(state, 1, .open_paren) and pointer_binding_starts(state))
or (next_n_is(state, 1, .star) and pointer_binding_starts(state))
or (next_n_is(state, 1, .open_bracket) and array_starts_type(state))
or next_n_is(state, 1, .double_colon)
@@ -263,8 +284,10 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
.for: return try parse_for(state, allow_else)
.return: return try parse_return(state, allow_else)
.break, .continue: return try parse_loop_control(state, allow_else)
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .open_curly, .struct,
.enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null: {
.defer, .errdefer: return try parse_cleanup(state, allow_else)
.open_curly: return try ast.add_block_stmt(state.builder, try parse_block(state))
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .struct,
.enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null, .try: {
expression :: try parse_expr(state)
if next_is(state, .equal) return try parse_assignment(state, expression, allow_else)
return try ast.add_expression_stmt(state.builder, expression)
@@ -273,6 +296,30 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
}
}
@hide:file
parse_error_capture proc(state @mut State) ?ast.NodeId ! ParseError {
capture ?ast.NodeId := null
if (!next_is(state, .pipe)) return capture
state.next_token += 1
capture = try parse_capture_name(state)
_ = try consume(state, .pipe)
return capture
}
@hide:file
parse_cleanup proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
keyword :: current(state)
state.next_token += 1
error_only :: keyword.kind == .errdefer
capture ?ast.NodeId :: if (error_only) try parse_error_capture(state) else null
body :: try parse_arm(state, allow_else)
return try ast.add_cleanup_stmt(state.builder, ast.Span{
start = keyword.span.start,
end = ast.builder_span(state.builder, body).end,
source = keyword.span.source,
}, body, capture, error_only)
}
@hide:file
parse_return proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
keyword :: try consume(state, .return)
@@ -517,8 +564,14 @@ parse_for proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
@hide:file
parse_guard_expr proc(state @mut State) ast.NodeId ! ParseError {
previous :: state.allow_record_literals
previous_sum_types :: state.allow_sum_types
state.allow_record_literals = false
defer state.allow_record_literals = previous
# capture pipes stay boundaries even when an index/call permits record literals.
state.allow_sum_types = false
defer {
state.allow_record_literals = previous
state.allow_sum_types = previous_sum_types
}
return try parse_expr(state)
}
@@ -630,12 +683,49 @@ parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError {
@hide:file
parse_type proc(state @mut State) ast.NodeId ! ParseError {
skip_operand_newlines(state)
start :: current(state).span
left := try parse_type_atom(state)
while true {
skip_continuation_newlines(state)
if (!next_is(state, .pipe)) break
state.next_token += 1
right :: try parse_type_atom(state)
left = try ast.add_binary_type(state.builder, ast.Span{
start = start.start,
end = state.tokens[state.next_token - 1].span.end,
source = start.source,
}, left, right, false)
}
if (next_is(state, .bang)) {
state.next_token += 1
right :: try parse_type(state)
left = try ast.add_binary_type(state.builder, ast.Span{
start = start.start,
end = state.tokens[state.next_token - 1].span.end,
source = start.source,
}, left, right, true)
}
return left
}
@hide:file
parse_type_atom proc(state @mut State) ast.NodeId ! ParseError {
skip_operand_newlines(state)
if (next_is(state, .open_paren)) {
state.next_token += 1
state.paren_depth += 1
defer state.paren_depth -= 1
child :: try parse_type(state)
_ = try consume(state, .close_paren)
return child
}
if (next_is(state, .question)) {
token :: current(state)
state.next_token += 1
child :: try parse_type(state)
child :: try parse_type_atom(state)
return try ast.add_optional_type(state.builder, ast.Span{
start = token.span.start, end = ast.builder_span(state.builder, child).end, source = token.span.source,
start = token.span.start, end = state.tokens[state.next_token - 1].span.end, source = token.span.source,
}, child)
}
if (next_is(state, .at) or next_is(state, .star)) {
@@ -653,7 +743,7 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError {
result :: try parse_type(state)
span :: ast.Span{
start = token.span.start,
end = ast.builder_span(state.builder, result).end,
end = state.tokens[state.next_token - 1].span.end,
source = token.span.source,
}
if (mutable or token.kind == .star) {
@@ -662,10 +752,10 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError {
}
return try ast.add_function_type(state.builder, span, result, params.items)
}
element :: try parse_type(state)
element :: try parse_type_atom(state)
return try ast.add_pointer_type(state.builder, ast.Span{
start = token.span.start,
end = ast.builder_span(state.builder, element).end,
end = state.tokens[state.next_token - 1].span.end,
source = token.span.source,
}, element, mutable, token.kind == .star)
}
@@ -680,9 +770,9 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError {
_ = try consume(state, .close_bracket)
mutable :: next_is(state, .mut)
if (mutable) state.next_token += 1
element :: try parse_type(state)
element :: try parse_type_atom(state)
return try ast.add_slice_type(state.builder, ast.Span{
start = open.span.start, end = ast.builder_span(state.builder, element).end, source = open.span.source,
start = open.span.start, end = state.tokens[state.next_token - 1].span.end, source = open.span.source,
}, element, mutable, sentinel)
}
count ?ast.NodeId := null
@@ -698,12 +788,12 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError {
_ = try consume(state, .close_bracket)
mutable :: next_is(state, .mut)
if (mutable) state.next_token += 1
element :: try parse_type(state)
element :: try parse_type_atom(state)
return try ast.add_array_type(
state.builder,
ast.Span{
start = open.span.start,
end = ast.builder_span(state.builder, element).end,
end = state.tokens[state.next_token - 1].span.end,
source = open.span.source,
},
count, element, mutable,
@@ -733,7 +823,7 @@ starts_type proc(kind lexer.TokenKind) bool {
return (
kind == .ident or kind == .open_bracket or kind == .struct
or kind == .enum or kind == .union or kind == .distinct or kind == .alias
or kind == .at or kind == .star or kind == .question
or kind == .at or kind == .star or kind == .question or kind == .open_paren
)
}
@@ -742,7 +832,7 @@ member_name proc(kind lexer.TokenKind) bool {
return match kind {
.ident, .if, .else, .for, .while, .proc, .return, .break, .continue,
.true, .false, .and, .or, .mut, .struct, .enum, .union, .distinct,
.alias, .match, .import, .test, .c_func, .null: true
.alias, .match, .import, .test, .c_func, .null, .try, .catch, .defer, .errdefer: true
else: false
}
}
@@ -961,7 +1051,7 @@ parse_match_arm proc(state @mut State) ast.NodeId ! ParseError {
state.match_arm_body = true
defer state.match_arm_body = previous
statement_body :: match current(state).kind {
.open_curly, .return, .break, .continue, .if, .while, .for: true
.open_curly, .return, .break, .continue, .if, .while, .for, .defer, .errdefer: true
else: false
}
if (statement_body or named_binding_starts(state)) {
@@ -1144,21 +1234,21 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
state.next_token += 1
operand :: try parse_expr_bp(state, right_bp)
operand_span :: ast.builder_span(state.builder, operand)
span :: ast.Span{
start = token.span.start,
end = operand_span.end,
source = token.span.source,
}
operator ast.UnaryOperator :: match token.kind {
.bang: .logical_not
.ampersand: .address_of
else: .negate
}
yield try ast.add_unary(
state.builder,
ast.Span{
start = token.span.start,
end = operand_span.end,
source = token.span.source,
},
operator,
operand,
)
prefixed :: if (token.kind == .try)
try ast.add_try_expr(state.builder, span, operand)
else
try ast.add_unary(state.builder, span, operator, operand)
yield prefixed
} else {
yield try parse_primary(state)
}
@@ -1178,7 +1268,11 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
left_grouped = false
continue
}
if (token.kind == .bang and next_n_is(state, 1, .open_paren)) {
# adjacent bang calls stay intrinsics; spaced 'T ! (E)' is a fallible type.
if (
token.kind == .bang and next_n_is(state, 1, .open_paren)
and state.tokens[state.next_token - 1].span.end == token.span.start
) {
state.next_token += 1
left = try parse_call(state, left, state.tokens[expression_start].span.start, true)
left_grouped = false
@@ -1218,6 +1312,34 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
left_grouped = false
continue
}
if (token.kind == .catch) {
if (min_bp > BindingPowerScalar(0)) break
state.next_token += 1
capture :: try parse_error_capture(state)
skip_operand_newlines(state)
handler_is_block :: next_is(state, .open_curly)
handler :: if (handler_is_block) try parse_block(state) else try parse_expr_bp(state, BindingPowerScalar(0))
left = try ast.add_catch_expr(state.builder, ast.Span{
start = state.tokens[expression_start].span.start,
end = state.tokens[state.next_token - 1].span.end,
source = token.span.source,
}, left, capture, handler, handler_is_block)
left_grouped = false
continue
}
if (token.kind == .bang or (token.kind == .pipe and state.allow_sum_types)) {
power :: if (token.kind == .bang) BindingPowerScalar(1) else BindingPowerScalar(2)
if (power < min_bp) break
state.next_token += 1
right :: try parse_expr_bp(state, BindingPowerScalar(u32(power) + 1))
left = try ast.add_binary_type(state.builder, ast.Span{
start = state.tokens[expression_start].span.start,
end = state.tokens[state.next_token - 1].span.end,
source = token.span.source,
}, left, right, token.kind == .bang)
left_grouped = false
continue
}
if (token.kind == .range or token.kind == .range_inclusive) {
if (min_bp > BindingPowerScalar(0)) break
left = try validate_range_bound(state, left, left_grouped)
@@ -1421,7 +1543,7 @@ invalid_array_literal proc(state @mut State, start, end u32) ast.NodeId ! ParseE
@hide:file
pointer_binding_starts proc(state @State) bool {
depth := usize(0)
for (state.next_token + 2)..(state.tokens.len) |i| {
for (state.next_token + 1)..(state.tokens.len) |i| {
kind :: state.tokens[i].kind
if (kind == .open_curly or kind == .open_bracket or kind == .open_paren) depth += 1
if (kind == .close_curly or kind == .close_bracket or kind == .close_paren) {
@@ -1444,6 +1566,7 @@ named_binding_starts proc(state @State) bool {
or next_n_is(state, 1, .ident) or next_n_is(state, 1, .proc)
or next_n_is(state, 1, .at)
or next_n_is(state, 1, .question)
or (next_n_is(state, 1, .open_paren) and pointer_binding_starts(state))
or (next_n_is(state, 1, .star) and pointer_binding_starts(state))
)
)
@@ -1538,7 +1661,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
@hide:file
is_expression_boundary proc(kind lexer.TokenKind) bool {
return match kind {
.close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .while, .for, .eof: true
.close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .while, .for, .defer, .errdefer, .eof: true
else: false
}
}
@@ -1560,6 +1683,7 @@ is_expression_recovery_boundary proc(kind lexer.TokenKind) bool {
or kind == .and
or kind == .or
or kind == .pipe
or kind == .catch
or is_expression_boundary(kind)
)
}
@@ -1588,6 +1712,7 @@ skip_continuation_newlines proc(state @mut State) void {
kind != .close_paren and kind != .comma and kind != .open_paren
and kind != .close_bracket and kind != .open_bracket
and kind != .range and kind != .range_inclusive
and kind != .pipe and kind != .bang and kind != .catch
and infix_binding_power(kind) == null
) state.next_token = start
}
@@ -1667,7 +1792,7 @@ recovery_closes_delimiters proc(state @State, paren_depth, square_depth usize) b
kind :: state.tokens[i].kind
match kind {
.open_curly, .close_curly, .eof, .if, .else, .while, .for,
.return, .break, .continue: return false
.return, .break, .continue, .defer, .errdefer: return false
.ident: {
if (i + 1 < state.tokens.len) {
next :: state.tokens[i + 1].kind
@@ -1739,7 +1864,7 @@ consume_either proc(state @mut State, kinds []lexer.TokenKind) lexer.Token ! Syn
@hide:file
prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar {
return match kind {
.minus, .bang, .ampersand: BindingPowerScalar(50)
.minus, .bang, .ampersand, .try: BindingPowerScalar(50)
else: null
}
}
+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)
+360
View File
@@ -11,6 +11,317 @@ HONEY = ROOT / "build/honey"
# Explicit expectations guard against frontend bugs shared by both execution paths.
CASES = {
"fallible_success_and_catch_short_circuit": (
"""
Error :: enum { failed }
good proc() i16 ! Error { return 42 }
bad_fallback proc() i16 { value i16 :: 32767
return value + 1 }
main proc() i16 { return good() catch bad_fallback() }
""", b"42\n", None,
),
"fallible_try_composed_errors_and_capture": (
"""
First :: enum { first }
Second :: enum { second }
leaf proc() i16 ! Second { return .second }
middle proc() i16 ! (First | Second) { return try leaf() }
main proc() i16 {
return middle() catch |err| match err {
.first: 0,
.second: 42,
}
}
""", b"42\n", None,
),
"fallible_matched_error_residual_return": (
"""
First :: enum { first }
Second :: enum { second }
broad proc() i16 ! (First | Second) { return .second }
narrow proc() i16 ! Second {
broad() catch |err| {
match err {
.first: return 0,
else: return err,
}
}
return 0
}
main proc() i16 { return narrow() catch 42 }
""", b"42\n", None,
),
"fallible_matched_tagged_error_residual_return": (
"""
Missing :: enum { missing }
Detail :: union(enum) { failed i16 }
broad proc() i16 ! (Missing | Detail) { return .failed{42} }
narrow proc() i16 ! Detail {
broad() catch |err| {
match err {
.missing: return 0,
else: return err,
}
}
return 0
}
main proc() i16 {
return narrow() catch |err| match err { .failed |code|: code }
}
""", b"42\n", None,
),
"fallible_storage_arrays_and_callbacks": (
"""
Error :: enum { failed }
Callback :: alias @proc(value i16) i16 ! Error
Box :: struct { callback Callback, result i16 ! Error }
maybe proc(value i16) i16 ! Error {
if (value == 0) return .failed
return value
}
main proc() i16 {
box Box :: { callback = maybe, result = 40 }
values [2](i16 ! Error) :: [box.result, .failed]
first :: values[0] catch 0
second :: values[1] catch 2
return box.callback(first + second) catch 0
}
""", b"42\n", None,
),
"fallible_struct_error_payload": (
"""
Failure :: struct { code i16 }
fail proc() i16 ! Failure { return Failure { code = 42 } }
main proc() i16 { return fail() catch |err| err.code }
""", b"42\n", None,
),
"fallible_tagged_error_payload": (
"""
Error :: union(enum) { failed i16, empty void }
fail proc() i16 ! Error { return .failed{42} }
main proc() i16 {
return fail() catch |err| match err {
.failed |code|: code,
.empty: 0,
}
}
""", b"42\n", None,
),
"fallible_composed_tagged_payload_propagation": (
"""
Missing :: enum { missing }
Detail :: union(enum) { failed i16, empty void }
leaf proc() i16 ! Detail { return .failed{42} }
forward proc() i16 ! (Missing | Detail) { return try leaf() }
main proc() i16 {
return forward() catch |err| match err {
.missing: 0,
.failed |code|: code,
.empty: 0,
}
}
""", b"42\n", None,
),
"fallible_terminal_catch_block": (
"""
Error :: enum { failed }
fail proc() i16 ! Error { return .failed }
main proc() i16 {
return fail() catch |err| {
if (err == Error.failed) return 42
return 0
}
}
""", b"42\n", None,
),
"fallible_void_catch_and_fallthrough": (
"""
Error :: enum { failed }
good proc(value @mut i16) void ! Error { value^ = 40 }
fail proc() void ! Error { return .failed }
main proc() i16 {
value i16 := 0
good(&value) catch { value = 0 }
fail() catch { value = value + 2 }
return value
}
""", b"42\n", None,
),
"cleanup_scope_lifo_and_live_values": (
"""
main proc() int {
value := 0
{
defer value = value * 10 + 2
defer value = value * 10 + 4
}
return value
}
""", b"42\n", None,
),
"cleanup_return_payload_before_mutation": (
"""
Pair :: struct { a i16, b i16 }
get proc() Pair {
value Pair := { a = 40, b = 2 }
defer value.a = 0
return value
}
main proc() i16 { value :: get()
return value.a + value.b }
""", b"42\n", None,
),
"cleanup_loop_exited_scopes": (
"""
main proc() int {
value := 0
for 0..4 |i| {
defer value = value + 1
if (i == 0) continue
{
defer value = value + 10
break
}
}
return value
}
""", b"12\n", None,
),
"cleanup_only_reached_registrations": (
"""
write proc(value @mut i64) void {
defer value^ = value^ + 40
if (true) return
defer value^ = 0
}
main proc() int { value := 2
write(&value)
return value }
""", b"42\n", None,
),
"cleanup_errdefer_capture_and_order": (
"""
Error :: enum { failed }
fail proc(value @mut i64) int ! Error {
defer value^ = value^ * 10 + 2
errdefer |err| if (err == Error.failed) value^ = value^ * 10 + 4
return .failed
}
main proc() int { value := 0
ignored :: fail(&value) catch 0
return value + ignored }
""", b"42\n", None,
),
"cleanup_errdefer_skip_on_success_and_scope_exit": (
"""
Error :: enum { failed }
good proc(value @mut i64) int ! Error {
errdefer value^ = 0
{ errdefer value^ = 0 }
defer value^ = value^ + 2
return 1
}
main proc() int { value := 40
ignored :: good(&value) catch 0
return value }
""", b"42\n", None,
),
"cleanup_try_propagation": (
"""
Error :: enum { failed }
leaf proc() void ! Error { return .failed }
outer proc(value @mut i64) void ! Error {
defer value^ = value^ + 2
errdefer value^ = value^ + 40
try leaf()
value^ = 0
}
main proc() int { value := 0
outer(&value) catch {}
return value }
""", b"42\n", None,
),
"cleanup_direct_fallible_return_and_expired_scope": (
"""
First :: enum { first }
Second :: enum { second }
leaf proc() i16 ! Second { return .second }
forward proc(value @mut i64) i16 ! (First | Second) {
{ errdefer value^ = 0 }
errdefer value^ = value^ + 40
defer value^ = value^ + 2
return leaf()
}
main proc() int { value := 0
ignored :: forward(&value) catch 0
return value }
""", b"42\n", None,
),
"fallible_aggregate_success_copy": (
"""
Error :: enum { failed }
Pair :: struct { left i16, right i16 }
read proc() Pair ! Error {
value Pair := { left = 40, right = 2 }
defer value.left = 0
return value
}
main proc() i16 {
value :: read() catch Pair { left = 0, right = 0 }
return value.left + value.right
}
""", b"42\n", None,
),
"fallible_borrowed_slice_success": (
"""
Error :: enum { failed }
borrow proc(values []mut i16) []mut i16 ! Error { return values }
main proc() i16 {
values [2]mut i16 := [40, 2]
view :: borrow(values[..]) catch &[]
view[0] = view[0] + view[1]
return values[0]
}
""", b"42\n", None,
),
"fallible_optional_pointer_success": (
"""
Error :: enum { failed }
borrow proc(value @mut i16) ?@mut i16 ! Error { return value }
main proc() i16 {
value i16 := 40
pointer :: borrow(&value) catch null
if pointer |present| present^ = present^ + 2
return value
}
""", b"42\n", None,
),
"fallible_trap_is_not_caught": (
"""
Error :: enum { failed }
bad proc() int ! Error { value :: [1]
return value[2] }
main proc() int { return bad() catch 42 }
""", b"", b"index out of bounds",
),
"fallible_try_requires_channel": (
"main proc() int { return try 42 }", b"", b"",
),
"fallible_incompatible_propagation": (
"""
First :: enum { first }
Second :: enum { second }
leaf proc() int ! First { return .first }
bad proc() int ! Second { return try leaf() }
main proc() int { return bad() catch 42 }
""", b"", b"",
),
"cleanup_return_rejected": (
"main proc() int { defer return 0\nreturn 42 }", b"", b"",
),
"cleanup_errdefer_requires_fallible_function": (
"main proc() int { errdefer main()\nreturn 42 }", b"", b"",
),
"slice_mutation_alias_calls": (
"""
update proc(values []mut i16) []mut i16 {
@@ -1991,6 +2302,54 @@ def check_allocator_libc(temporary, executable):
def check_fallible_packages_and_entry(temporary, executable):
project = temporary / "fallible_packages"
app = project / "app"
library = project / "library"
app.mkdir(parents=True)
library.mkdir()
dependency = library / "error.hon"
dependency.write_text(
"Error :: enum { unavailable }\n"
"read proc() i16 ! Error { return .unavailable }\n"
)
source = app / "main.hon"
source.write_text(textwrap.dedent("""
library :: import "../library"
LocalError :: enum { local }
read proc() i16 ! (LocalError | library.Error) {
return try library.read()
}
main proc() i16 {
return read() catch |err| match err {
.local: 0,
.unavailable: 42,
}
}
""").strip() + "\n")
check(app, executable, (b"42\n", None))
count = 1
for result, body, expected in (
("i16", "return 42", (0, b"42\n", b"")),
("void", "return", (0, b"", b"")),
("void", "", (0, b"", b"")),
("i16", "return .failed", (1, b"", b"")),
("void", "return .failed", (1, b"", b"")),
):
source.write_text(
f"Error :: enum {{ failed }}\n"
f"main proc() {result} ! Error {{ {body} }}\n"
)
assert outcome(run(str(HONEY), "run", str(source))) == expected
for optimization in ("-O0", "-O2"):
built = run(str(HONEY), "build", str(source), "-o", str(executable), optimization)
assert outcome(built) == (0, b"", b""), outcome(built)
assert outcome(run(str(executable))) == expected, optimization
executable.unlink()
count += 1
return count
def main():
with tempfile.TemporaryDirectory(prefix="honey native ' ; $") as directory:
temporary = Path(directory)
@@ -2009,6 +2368,7 @@ def main():
count += check_namespace_packages(temporary, executable)
count += check_record_packages(temporary, executable)
count += check_data_model_packages(temporary, executable)
count += check_fallible_packages_and_entry(temporary, executable)
count += check_dependency_provenance(temporary, executable)
source = ROOT / "programs/factorial.hon"
emitted = run(str(HONEY), "emit-llvm", str(source))