feat: support fallible results and scoped cleanup
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -23,6 +23,8 @@ TokenKind :: enum {
|
||||
match
|
||||
proc, return
|
||||
break, continue
|
||||
try, catch
|
||||
defer, errdefer
|
||||
true, false
|
||||
and, or
|
||||
struct, enum, union
|
||||
|
||||
+26
-2
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user