error handling kickoff
This commit is contained in:
@@ -552,11 +552,46 @@
|
||||
inside arm bodies. As a side effect the rvalue pointer-capture guard from 22.5 is now reachable
|
||||
(`match make_box() { .v |@p|: … }` correctly errors "requires an addressable subject")
|
||||
- checker-only; no parser/AST/HIR/IR/lowering/codegen change
|
||||
- deferred: payload-carrying contextual construction (`.variant{…}`, milestone 23); a
|
||||
`yield call()` inside a value-block/value-match arm still misses its spec (`infer_statements`
|
||||
has no `.Yield` case either — a separate pre-existing gap)
|
||||
- deferred: payload-carrying contextual construction (`.variant{…}`) remains outside milestone
|
||||
23 v1. The separate `yield call()` inference gap is fixed in milestone 23.
|
||||
|
||||
23. error types (see below)
|
||||
23. sum-type composition, fallible channels, and yield inference (implemented; v1)
|
||||
- `.Yield` is now visited by specialization-demand inference, so `yield call()` inside value
|
||||
blocks, value-if/value-match branches, and match arms requests the needed call specialization
|
||||
- native unbacked enums and native `union(enum)` / `union(SomeEnum)` variants are registered in a
|
||||
program-global variant table keyed by `(name, payload-type)`. Runtime tags are fixed `u16`
|
||||
global ids; `0` is reserved for fallible success/no-error, and overflow is diagnosed
|
||||
- tagged-union construction, tag reads, matching, and LLVM layout use those global ids rather
|
||||
than per-type dense tags. Component-to-composite widening preserves the tag and copies only the
|
||||
active payload carrier bytes from the source carrier offset to the destination carrier offset
|
||||
- `A | B` composes native unbacked enums and native tagged unions: matching `(name, payload-type)`
|
||||
variants merge, same-name/different-payload conflicts are diagnosed, and backed enums, C enums,
|
||||
and untagged unions are rejected for v1
|
||||
- `T ! E` is represented as a real synthetic fallible channel type in `types.Store`. A fallible
|
||||
function returns success with code `0` or an error variant's global id; `return expr` dispatches
|
||||
based on whether `expr` coerces to the success type `T` or the error type `E`
|
||||
- `try expr` unwraps success and propagates the enclosing function's exact error channel;
|
||||
`expr catch fallback` unwraps success or evaluates the fallback value
|
||||
- runtime coverage lives in `examples/programs/errors`; compiler coverage includes global-id sum
|
||||
merge/conflict/widening/rejections and the `.Yield` inference regression
|
||||
- deferred follow-ups are split below; 23.5 keeps the next user-visible slice small
|
||||
|
||||
23.5. fallible ergonomics: catch blocks + composable try widening
|
||||
- implement `expr catch |e| { ... }` with `e : E`
|
||||
- allow `try` to widen `T ! E1` into an enclosing `T ! (E1 | E2)` channel
|
||||
- add focused tests/examples for both
|
||||
- leave ABI/layout/lint/design polish for later milestones
|
||||
|
||||
23.6. contextual payload construction + inline error types
|
||||
- implement contextual payload `.variant{...}` construction
|
||||
- allow named fallible signatures to use inline enum / tagged-union error types
|
||||
- settle qualified same-name disambiguation only if contextual construction needs it
|
||||
|
||||
23.7. sum-type ABI/layout polish
|
||||
- consider dynamic tag-width shrinking after the fixed-`u16` ABI has real pressure
|
||||
- consider all-void channel collapse after fallible channels are otherwise stable
|
||||
- define cross-module/global-id ABI determinism before multi-module builds depend on it
|
||||
- keep backed/C enum composition and must-consume fallible linting as later policy work
|
||||
|
||||
24. dynamic heap allocation
|
||||
- see below for direction
|
||||
@@ -572,7 +607,7 @@ name: ?[]u8 = get_name()
|
||||
age: ?u8 = get_age()
|
||||
if name and age |n, a| {
|
||||
# both n and a are guaranteed non-none here
|
||||
print("{s} is {d} years old", {n, a})
|
||||
print("{s} is {d} years old", n, a)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -580,7 +615,7 @@ if name and age |n, a| {
|
||||
|
||||
```
|
||||
if name and hat |n, h : n == "Huginn" and h.brand == .gucci| {
|
||||
print("{s}'s got that drip\n", {n})
|
||||
print("{s}'s got that drip\n", n)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -765,7 +800,7 @@ message = "Header:\t" ++
|
||||
|
||||
The `yield` keyword provides a value from a block to its enclosing expression and **exits the block immediately** — just as `return` exits a function, `yield` exits the enclosing scope. Code after a `yield` is unreachable, and the compiler flags it. This makes `yield` part of a consistent set of scope-exiting control flow: `return` exits a function, `yield` exits a block, `break` exits a loop, and `continue` skips to the next iteration.
|
||||
|
||||
It is used in scoped blocks, match arms, and catch handlers.
|
||||
It is used in scoped blocks and match arms today; catch-handler blocks are planned.
|
||||
|
||||
**General rule:** When a block needs to produce a value, single expressions yield implicitly while multi-statement blocks require explicit `yield`. This rule applies uniformly across the language:
|
||||
|
||||
@@ -785,8 +820,7 @@ label []u8 = match p {
|
||||
},
|
||||
}
|
||||
|
||||
# catch handlers
|
||||
data []u8 = read(path) catch default_data # single expression: implicit
|
||||
# catch handlers (planned; block form deferred in milestone 23 v1)
|
||||
data []u8 = read(path) catch |e| {
|
||||
log(e)
|
||||
yield fallback_data # block: explicit yield
|
||||
@@ -895,7 +929,7 @@ match result {
|
||||
process(data)
|
||||
}
|
||||
.failure |info|: {
|
||||
print("error {d}: {s}", { info.code, info.msg })
|
||||
print("error {d}: {s}", info.code, info.msg)
|
||||
}
|
||||
.pending: {
|
||||
# void payload - no capture needed
|
||||
@@ -938,14 +972,16 @@ message :: match code {
|
||||
|
||||
## A word on error handling
|
||||
|
||||
Honey handles errors as values. There is no hidden control flow — a function that can fail declares this in its signature, and the caller must explicitly handle the possibility of failure.
|
||||
Brolang handles errors as values. There is no hidden control flow — a function that can fail declares this in its signature, and the caller must explicitly handle the possibility of failure.
|
||||
|
||||
Milestone 23 v1 implements named error channels, native sum composition, `return`-based error dispatch, exact-channel `try`, and fallback `catch`. It intentionally defers the `error` keyword shorthand, inline error types, `catch |e|` blocks, and `try` widening across different-but-composable error channels.
|
||||
|
||||
### Fallible Functions
|
||||
|
||||
Functions that can fail declare their error type after `!`:
|
||||
|
||||
```honey
|
||||
read_file :: func(path: []u8) []u8 ! IoError { ... }
|
||||
```
|
||||
read_file :: func(path []u8) []u8 ! IoError { ... }
|
||||
```
|
||||
|
||||
This reads as: "returns `[]u8` or fails with `IoError`." The space around `!` is idiomatic but not required.
|
||||
@@ -956,7 +992,7 @@ Errors can be enums (when you only need to identify what went wrong) or tagged u
|
||||
|
||||
**Simple errors (enum):**
|
||||
|
||||
```honey
|
||||
```
|
||||
SimpleError :: enum {
|
||||
OutOfMemory,
|
||||
InvalidSize,
|
||||
@@ -966,12 +1002,12 @@ SimpleError :: enum {
|
||||
|
||||
**Rich errors (tagged union):**
|
||||
|
||||
```honey
|
||||
```
|
||||
IoError :: union(enum) {
|
||||
NotFound: struct { path: []u8 },
|
||||
PermissionDenied: struct { path: []u8, operation: []u8 },
|
||||
Timeout: struct { after_ms: u64 },
|
||||
ConnectionReset: void, # no additional data needed
|
||||
NotFound struct { path: []u8 },
|
||||
PermissionDenied struct { path []u8, operation []u8 },
|
||||
Timeout struct { after_ms u64 },
|
||||
ConnectionReset void, # no additional data needed
|
||||
}
|
||||
```
|
||||
|
||||
@@ -979,7 +1015,7 @@ IoError :: union(enum) {
|
||||
|
||||
When you have a predefined set of error kinds, you can constrain the union:
|
||||
|
||||
```honey
|
||||
```
|
||||
IoErrorKind :: enum {
|
||||
not_found,
|
||||
permission_denied,
|
||||
@@ -987,64 +1023,43 @@ IoErrorKind :: enum {
|
||||
}
|
||||
|
||||
IoError :: union(IoErrorKind) {
|
||||
not_found: struct { path: []u8 },
|
||||
permission_denied: struct { path: []u8 },
|
||||
timeout: struct { after_ms: u64 },
|
||||
not_found struct { path []u8 },
|
||||
permission_denied struct { path []u8 },
|
||||
timeout struct { after_ms u64 },
|
||||
}
|
||||
```
|
||||
|
||||
### Error Composition
|
||||
|
||||
Functions that can fail with multiple error types use `|` to compose them:
|
||||
Functions that can fail with multiple error types use `|` to compose a named error type:
|
||||
|
||||
```honey
|
||||
process :: func(path: []u8) Ast ! IoError | ParseError { ... }
|
||||
```
|
||||
ProcessError :: alias IoError | ParseError
|
||||
process :: func(path []u8) Ast ! ProcessError { ... }
|
||||
```
|
||||
|
||||
Parentheses are optional but can aid readability:
|
||||
Parentheses are optional in the composed type and can aid readability:
|
||||
|
||||
```honey
|
||||
process :: func(path: []u8) Ast ! (IoError | ParseError) { ... }
|
||||
```
|
||||
|
||||
Composed error types can also be defined standalone:
|
||||
|
||||
```honey
|
||||
ProcessError :: IoError | ParseError | ValidationError
|
||||
process :: func(path: []u8) Ast ! ProcessError { ... }
|
||||
ProcessError :: alias (IoError | ParseError)
|
||||
```
|
||||
|
||||
### Inline Error Types
|
||||
|
||||
For simple one-off cases, you can inline the error type:
|
||||
|
||||
```honey
|
||||
# Inline enum
|
||||
simple :: func() void ! enum{Failed, Timeout} { ... }
|
||||
|
||||
# Inline tagged union (when payloads are needed)
|
||||
complex :: func() Data ! union(enum) {
|
||||
OutOfMemory: struct { requested: usize },
|
||||
InvalidInput: void,
|
||||
} { ... }
|
||||
```
|
||||
|
||||
For anything non-trivial, prefer named error types for clarity.
|
||||
Inline error types are planned, but not part of milestone 23 v1. Use named enums, named tagged unions, or named aliases for now.
|
||||
|
||||
### Returning Errors
|
||||
|
||||
The `error` keyword is a control flow statement that exits the function via the error channel — symmetric to return for success values:
|
||||
Fallible functions use ordinary `return` for both channels. If the returned expression coerces to the success type `T`, the function returns success with channel code `0`. If it coerces to the error type `E`, the function returns the error with that variant's global tag id:
|
||||
|
||||
```honey
|
||||
```
|
||||
parse_section :: func(p: @mut Parser) void ! ParseError {
|
||||
start_line := p.line
|
||||
start_line Line = p.line
|
||||
p.advance()
|
||||
|
||||
# ... parsing logic ...
|
||||
|
||||
if p.pos >= p.input.len or p.input[p.pos] != ']' {
|
||||
error .unclosed_section{.line = start_line}
|
||||
}
|
||||
if p.pos >= p.input.len or p.input[p.pos] != ']' return ParseError{ unclosed_section = start_line }
|
||||
|
||||
# ... continue on success ...
|
||||
}
|
||||
@@ -1052,48 +1067,49 @@ parse_section :: func(p: @mut Parser) void ! ParseError {
|
||||
|
||||
Since errors are just union values, you can also construct them separately:
|
||||
|
||||
```honey
|
||||
```
|
||||
# Construct error value (it's just a union)
|
||||
e: ParseError = .timeout{.ms = 500}
|
||||
e ParseError = ParseError{ timeout = 500 }
|
||||
|
||||
# Return it via error channel later
|
||||
error e
|
||||
return e
|
||||
```
|
||||
|
||||
The symmetry:
|
||||
|
||||
* `return x` — exit function with success value
|
||||
* `error e` — exit function with error value
|
||||
* `return x` — exits with success when `x` is the success type
|
||||
* `return e` — exits with error when `e` is the error type
|
||||
|
||||
### Propagation with `try`
|
||||
|
||||
The `try` keyword unwraps a successful result or returns early with the error:
|
||||
|
||||
```honey
|
||||
process :: func(path: []u8) Ast ! IoError | ParseError {
|
||||
data := try read_file(path) # returns early if IoError
|
||||
ast := try parse(data) # returns early if ParseError
|
||||
```
|
||||
ProcessError :: alias IoError | ParseError
|
||||
process :: func(path []u8) Ast ! ProcessError {
|
||||
data :: try read_file(path) # read_file also returns []u8 ! ProcessError in v1
|
||||
ast :: try parse(data) # parse also returns Ast ! ProcessError in v1
|
||||
return ast
|
||||
}
|
||||
```
|
||||
|
||||
When the function's error type is a composition, `try` automatically widens narrower error types to match the return type.
|
||||
In v1, `try` propagates only when the callee's error channel exactly matches the enclosing function's error channel. Widening narrower error types into a composed return channel is a planned follow-up.
|
||||
|
||||
### Handling with `catch`
|
||||
|
||||
The `catch` keyword handles errors and provides a value to continue with.
|
||||
The `catch` keyword handles errors and provides a value to continue with. Milestone 23 v1 implements the fallback-value form.
|
||||
|
||||
**Provide a fallback value:**
|
||||
|
||||
```honey
|
||||
data := read_file(path) catch default_data
|
||||
```
|
||||
data :: read_file(path) catch default_data
|
||||
```
|
||||
|
||||
**Handle with a block using** `yield`:
|
||||
**Planned block form using** `yield` (deferred in v1):
|
||||
|
||||
```honey
|
||||
data := read_file(path) catch |e| {
|
||||
log("read failed: {}", {e})
|
||||
```
|
||||
data :: read_file(path) catch |e| {
|
||||
log("read failed: {}", e)
|
||||
yield empty_data
|
||||
}
|
||||
use(data) # continues with data = empty_data
|
||||
@@ -1101,11 +1117,11 @@ use(data) # continues with data = empty_data
|
||||
|
||||
The `yield` keyword provides a value from a block to the enclosing expression. Execution continues after the statement. For single expressions, yield is implicit (e.g., `catch default_data`). For blocks, explicit `yield` is required. See the Yield section under Control Flow for the full rule.
|
||||
|
||||
**Return from the function instead:**
|
||||
**Planned return-from-handler form** (deferred in v1):
|
||||
|
||||
```honey
|
||||
data := read_file(path) catch |e| {
|
||||
log("read failed: {}", {e})
|
||||
```
|
||||
data :: read_file(path) catch |e| {
|
||||
log("read failed: {}", e)
|
||||
return # exits the enclosing function
|
||||
}
|
||||
use(data) # never reached if error occurred
|
||||
@@ -1113,16 +1129,16 @@ use(data) # never reached if error occurred
|
||||
|
||||
Use `return` when the error is unrecoverable at this level.
|
||||
|
||||
**Match on specific error variants:**
|
||||
**Planned match-on-error form** (deferred in v1):
|
||||
|
||||
```honey
|
||||
data := read_file(path) catch |e| match e {
|
||||
```
|
||||
data :: read_file(path) catch |e| match e {
|
||||
.NotFound |info|: {
|
||||
print("file not found: {s}", {info.path})
|
||||
print("file not found: {s}", info.path)
|
||||
yield create_default(info.path)
|
||||
},
|
||||
.Timeout |info|: {
|
||||
print("timed out after {d}ms", {info.after_ms})
|
||||
print("timed out after {d}ms", info.after_ms)
|
||||
yield retry(path)
|
||||
},
|
||||
.PermissionDenied: panic("cannot recover from permission error"),
|
||||
@@ -1135,16 +1151,17 @@ data := read_file(path) catch |e| match e {
|
||||
| Syntax | Meaning |
|
||||
| -- | -- |
|
||||
| `T ! E` | Function returns `T` or fails with `E` |
|
||||
| `T ! E1 | E2` | Function can fail with either error type |
|
||||
| `T ! (E1 | E2)` | Same, with optional parentheses |
|
||||
| `error e` | Exit function via error channel |
|
||||
| `error .variant{...}` | Construct and return error in one step |
|
||||
| `try expr` | Unwrap success or propagate error |
|
||||
| `T ! E1 | E2` | Planned direct spelling; use a named alias in v1 |
|
||||
| `T ! (E1 | E2)` | Planned direct spelling with parentheses; use a named alias in v1 |
|
||||
| `return e` | Exit function via error channel when `e : E` |
|
||||
| `error e` | Planned shorthand, not v1 |
|
||||
| `error .variant{...}` | Planned shorthand, not v1 |
|
||||
| `try expr` | Unwrap success or propagate an exact matching error channel |
|
||||
| `expr catch fallback` | Provide fallback value on error |
|
||||
| `expr catch |e| { ... }` | Handle error with block |
|
||||
| `expr catch |e| { ... }` | Planned block handler, not v1 |
|
||||
| `yield value` | Provide value from innermost block |
|
||||
| `yield :label value` | Provide value from labeled block |
|
||||
| `return` / `return value` | Exit function from within catch block |
|
||||
| `return` / `return value` | Exit the current function; in fallible functions, return value dispatches by type |
|
||||
|
||||
## A word on memory allocation
|
||||
|
||||
|
||||
@@ -99,6 +99,8 @@ Expr_Kind :: enum u8 {
|
||||
Or,
|
||||
Range,
|
||||
Call,
|
||||
Try,
|
||||
Catch,
|
||||
}
|
||||
|
||||
Expr :: struct {
|
||||
@@ -109,6 +111,7 @@ Expr :: struct {
|
||||
name: symbol.Id,
|
||||
left: Expr_Id,
|
||||
right: Expr_Id,
|
||||
body: []Stmt_Id,
|
||||
diagnostic: source.Diagnostic_Id,
|
||||
parenthesized: bool,
|
||||
kind: Expr_Kind,
|
||||
@@ -204,6 +207,7 @@ Function :: struct {
|
||||
variadic: bool,
|
||||
params: []Param,
|
||||
result: Type_Syntax,
|
||||
error: Type_Syntax,
|
||||
body: []Stmt_Id,
|
||||
link_name: string,
|
||||
unsupported_reason: string,
|
||||
@@ -304,6 +308,7 @@ init_module :: proc(allocator := context.allocator) -> Module {
|
||||
destroy_module :: proc(module: ^Module) {
|
||||
for expr in module.exprs {
|
||||
delete(expr.args, module.allocator)
|
||||
delete(expr.body, module.allocator)
|
||||
}
|
||||
for statement in module.statements {
|
||||
delete(statement.captures, module.allocator)
|
||||
|
||||
+246
-23
@@ -167,6 +167,7 @@ Checker :: struct {
|
||||
cycle_stack: [dynamic]Cycle_Frame,
|
||||
main_symbol: symbol.Id,
|
||||
sink_symbol: symbol.Id,
|
||||
current_result: types.Type,
|
||||
target: target.Target,
|
||||
allocator: mem.Allocator,
|
||||
}
|
||||
@@ -322,6 +323,14 @@ type_from_syntax :: proc(value: ast.Type_Syntax) -> types.Type {
|
||||
return value
|
||||
}
|
||||
|
||||
function_channel_type :: proc(checker: ^Checker, function: ast.Function) -> types.Type {
|
||||
result := type_from_syntax(function.result)
|
||||
if types.is_valid(function.error) {
|
||||
return types.fallible(&checker.module.types, result, type_from_syntax(function.error))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
is_runtime_type :: proc(checker: ^Checker, value: types.Type) -> bool {
|
||||
return types.is_runtime_value(value, &checker.module.types)
|
||||
}
|
||||
@@ -609,11 +618,15 @@ add_unsupported_type_diagnostic :: proc(
|
||||
if types.is_valid(item.child) {
|
||||
return add_unsupported_type_diagnostic(checker, span, item.child, depth+1)
|
||||
}
|
||||
if types.is_valid(item.extra) {
|
||||
return add_unsupported_type_diagnostic(checker, span, item.extra, depth+1)
|
||||
}
|
||||
return source.INVALID_DIAGNOSTIC
|
||||
}
|
||||
|
||||
function_signatures_equal :: proc(left, right: ast.Function) -> bool {
|
||||
if left.result != right.result || left.variadic != right.variadic || len(left.params) != len(right.params) {
|
||||
if left.result != right.result || left.error != right.error ||
|
||||
left.variadic != right.variadic || len(left.params) != len(right.params) {
|
||||
return false
|
||||
}
|
||||
for param, index in left.params {
|
||||
@@ -665,7 +678,7 @@ function_value_signature :: proc(
|
||||
return nil, types.INVALID, false
|
||||
}
|
||||
function := checker.ast_module.functions[template]
|
||||
if !function.c_abi {
|
||||
if !function.c_abi || types.is_valid(function.error) {
|
||||
return nil, types.INVALID, false
|
||||
}
|
||||
result = type_from_syntax(function.result)
|
||||
@@ -744,8 +757,14 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as
|
||||
if expr.left != ast.INVALID_EXPR {
|
||||
append(&stack, expr.left)
|
||||
}
|
||||
case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Keyed:
|
||||
case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed:
|
||||
append(&stack, expr.left)
|
||||
case .Catch:
|
||||
append(&stack, expr.left)
|
||||
if expr.right != ast.INVALID_EXPR {
|
||||
append(&stack, expr.right)
|
||||
}
|
||||
mark_block_imports_used(checker, expr.body, file)
|
||||
case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
|
||||
append(&stack, expr.left, expr.right)
|
||||
case .Invalid, .Integer, .Float, .String, .Bool, .None, .Undefined, .Name, .Enum_Literal:
|
||||
@@ -916,6 +935,24 @@ validate_declarations :: proc(checker: ^Checker) {
|
||||
symbol_text(checker, function.name),
|
||||
)
|
||||
}
|
||||
if types.is_valid(function.error) {
|
||||
error_type := type_from_syntax(function.error)
|
||||
error_sum := types.is_enum(error_type, &checker.module.types) ||
|
||||
types.is_tagged_union(error_type, &checker.module.types)
|
||||
if function.c_abi {
|
||||
checker.template_diagnostics[function_id] = source.add(
|
||||
checker.diagnostics,
|
||||
function.span,
|
||||
"fallible functions must use 'func', not 'c_func'",
|
||||
)
|
||||
} else if !error_sum {
|
||||
checker.template_diagnostics[function_id] = source.add(
|
||||
checker.diagnostics,
|
||||
function.span,
|
||||
"fallible function error type must be a native enum or tagged union",
|
||||
)
|
||||
}
|
||||
}
|
||||
if !function.has_body && !function.c_abi {
|
||||
checker.template_diagnostics[function_id] = source.addf(
|
||||
checker.diagnostics,
|
||||
@@ -1072,15 +1109,15 @@ validate_type_nodes :: proc(checker: ^Checker) {
|
||||
)
|
||||
}
|
||||
}
|
||||
// A tagged union (`item.child` set) must tag with an enum, and each variant
|
||||
// must name a member of it. The synthesized `union(enum)` tag satisfies this
|
||||
// by construction; the check guards the explicit `union(Enum)` form.
|
||||
// A tagged union stores a hidden runtime tag enum keyed by global variant IDs.
|
||||
// An explicit `union(Enum)` keeps that declared enum only for validation.
|
||||
if item.kind == .Union && types.is_valid(item.child) {
|
||||
if !types.is_enum(item.child, &checker.module.types) {
|
||||
declared_tag := types.union_declared_tag_enum(id, &checker.module.types)
|
||||
if !types.is_enum(declared_tag, &checker.module.types) {
|
||||
source.add(checker.diagnostics, source.Span{}, "a tagged union's tag must be an enum")
|
||||
} else {
|
||||
for field in types.fields_for(&checker.module.types, id) {
|
||||
if _, ok := find_enum_member(checker, item.child, symbol.Id(field.name)); !ok {
|
||||
if _, ok := find_enum_member(checker, declared_tag, symbol.Id(field.name)); !ok {
|
||||
source.addf(
|
||||
checker.diagnostics,
|
||||
source.Span{},
|
||||
@@ -1187,8 +1224,9 @@ ensure_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: [
|
||||
}
|
||||
append(&signature, specialized_param_type(checker, param.type, actual))
|
||||
}
|
||||
result := type_from_syntax(function.result)
|
||||
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
|
||||
result := function_channel_type(checker, function)
|
||||
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT &&
|
||||
!types.is_valid(function.error) {
|
||||
result = types.I32
|
||||
}
|
||||
index := spec_id(len(checker.specs))
|
||||
@@ -1346,6 +1384,31 @@ infer_compound_expr :: proc(
|
||||
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
|
||||
return types.child_type(value, store) if types.is_optional(value, store) else types.INVALID
|
||||
case .Try:
|
||||
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||
return types.fallible_success(value, store) if types.kind(value, store) == .Fallible else types.INVALID
|
||||
case .Catch:
|
||||
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
|
||||
success := types.fallible_success(value, store)
|
||||
error_type := types.fallible_error(value, store)
|
||||
if expr.right != ast.INVALID_EXPR {
|
||||
fallback := infer_nested_expr(checker, expr.right, locals, pkg, file, demanded, local_types)
|
||||
if types.is_valid(success) && types.is_valid(fallback) && !types.equal(success, fallback) {
|
||||
return types.widest(success, fallback)
|
||||
}
|
||||
return success if types.is_valid(success) else fallback
|
||||
}
|
||||
block_locals: [dynamic]Infer_Local
|
||||
block_locals.allocator = checker.allocator
|
||||
defer delete(block_locals)
|
||||
append(&block_locals, ..locals)
|
||||
capture_start := len(block_locals)
|
||||
if symbol.is_valid(expr.name) && expr.name != checker.sink_symbol && types.is_valid(error_type) {
|
||||
append(&block_locals, Infer_Local{name=expr.name, type=error_type, declared=error_type, statement=ast.INVALID_STMT})
|
||||
}
|
||||
infer_statements(checker, expr.body, &block_locals, local_types, pkg, file, demanded, &success, success)
|
||||
resize(&block_locals, capture_start)
|
||||
return success
|
||||
case .Struct_Literal:
|
||||
for keyed in expr.args {
|
||||
_ = infer_nested_expr(checker, checker.ast_module.exprs[keyed].left, locals, pkg, file, demanded, local_types)
|
||||
@@ -1417,7 +1480,7 @@ infer_expr :: proc(
|
||||
last = types.F64
|
||||
_ = pop(&stack)
|
||||
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
|
||||
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, .Enum_Literal,
|
||||
.Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed, .Enum_Literal,
|
||||
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
|
||||
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded, local_types)
|
||||
_ = pop(&stack)
|
||||
@@ -1556,7 +1619,7 @@ infer_expr :: proc(
|
||||
continue
|
||||
}
|
||||
if checker.template_diagnostics[template] != source.INVALID_DIAGNOSTIC {
|
||||
declared := type_from_syntax(checker.ast_module.functions[template].result)
|
||||
declared := function_channel_type(checker, checker.ast_module.functions[template])
|
||||
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID
|
||||
_ = pop(&stack)
|
||||
continue
|
||||
@@ -1656,12 +1719,13 @@ infer_expr :: proc(
|
||||
if spec != INVALID_SPEC {
|
||||
last = checker.specs[spec].result
|
||||
} else {
|
||||
declared := type_from_syntax(function.result)
|
||||
declared := function_channel_type(checker, function)
|
||||
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID
|
||||
}
|
||||
} else {
|
||||
declared := type_from_syntax(function.result)
|
||||
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
|
||||
declared := function_channel_type(checker, function)
|
||||
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT &&
|
||||
!types.is_valid(function.error) {
|
||||
last = types.I32
|
||||
} else {
|
||||
last = declared if is_runtime_type(checker, declared) || types.is_void(declared) else types.INVALID
|
||||
@@ -1889,6 +1953,10 @@ infer_statements :: proc(
|
||||
}
|
||||
case .Expression:
|
||||
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
||||
case .Yield:
|
||||
if statement.expr != ast.INVALID_EXPR {
|
||||
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
||||
}
|
||||
case .Return:
|
||||
if statement.expr != ast.INVALID_EXPR {
|
||||
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
|
||||
@@ -2581,6 +2649,23 @@ find_build_local :: proc(locals: []Build_Local, name: symbol.Id) -> (Build_Local
|
||||
return Build_Local{}, false
|
||||
}
|
||||
|
||||
can_implicitly_convert_type :: proc(checker: ^Checker, actual, expected: types.Type) -> bool {
|
||||
store := &checker.module.types
|
||||
if types.equal(actual, expected) ||
|
||||
types.can_widen(actual, expected) ||
|
||||
types.can_coerce_c_integer(actual, expected) ||
|
||||
types.can_weaken_pointer(actual, expected, store) ||
|
||||
types.can_weaken_slice(actual, expected, store) ||
|
||||
types.can_decay_array_pointer(actual, expected, store) ||
|
||||
types.can_sum_widen(actual, expected, store) {
|
||||
return true
|
||||
}
|
||||
if types.is_optional(expected, store) {
|
||||
return can_implicitly_convert_type(checker, actual, types.child_type(expected, store))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
coerce_expr :: proc(
|
||||
checker: ^Checker,
|
||||
expr_id: hir.Expr_Id,
|
||||
@@ -2627,6 +2712,17 @@ coerce_expr :: proc(
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
}
|
||||
if types.can_sum_widen(actual, expected, &checker.module.types) {
|
||||
return add_hir_expr(checker, hir.Expr{
|
||||
kind=.Sum_Widen,
|
||||
span=span,
|
||||
type=expected,
|
||||
left=expr_id,
|
||||
target=hir.INVALID_REF,
|
||||
right=hir.INVALID_EXPR,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
}
|
||||
if types.is_optional(expected, &checker.module.types) {
|
||||
child := types.child_type(expected, &checker.module.types)
|
||||
if types.equal(actual, child) ||
|
||||
@@ -3049,6 +3145,28 @@ build_nested_expr :: proc(
|
||||
return result
|
||||
}
|
||||
|
||||
fallible_aggregate :: proc(
|
||||
checker: ^Checker,
|
||||
span: source.Span,
|
||||
channel: types.Type,
|
||||
value: hir.Expr_Id,
|
||||
error_path: bool,
|
||||
) -> hir.Expr_Id {
|
||||
values := make([]hir.Expr_Id, 1, checker.allocator)
|
||||
values[0] = value
|
||||
return add_hir_expr(checker, hir.Expr{
|
||||
kind=.Struct,
|
||||
span=span,
|
||||
type=channel,
|
||||
integer=1 if error_path else 0,
|
||||
args=values,
|
||||
target=hir.INVALID_REF,
|
||||
left=hir.INVALID_EXPR,
|
||||
right=hir.INVALID_EXPR,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
}
|
||||
|
||||
build_compound_expr :: proc(
|
||||
checker: ^Checker,
|
||||
expr: ast.Expr,
|
||||
@@ -3303,6 +3421,52 @@ build_compound_expr :: proc(
|
||||
kind=.Orelse, span=expr.span, type=child, left=optional, right=fallback,
|
||||
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
case .Try:
|
||||
channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
|
||||
channel_type := checker.module.exprs[channel].type
|
||||
success := types.fallible_success(channel_type, store)
|
||||
if !types.is_valid(success) {
|
||||
id := source.add(checker.diagnostics, expr.span, "'try' requires a fallible expression")
|
||||
return invalid_hir_expr(checker, expr.span, id)
|
||||
}
|
||||
if !types.equal(channel_type, checker.current_result) {
|
||||
// ponytail: exact channel propagation; add fallible-error widening when cross-error-set try matters.
|
||||
id := source.add(checker.diagnostics, expr.span, "'try' can only propagate the enclosing function's exact error channel in v1")
|
||||
return invalid_hir_expr(checker, expr.span, id, success)
|
||||
}
|
||||
return add_hir_expr(checker, hir.Expr{
|
||||
kind=.Try,
|
||||
span=expr.span,
|
||||
type=success,
|
||||
left=channel,
|
||||
target=hir.INVALID_REF,
|
||||
right=hir.INVALID_EXPR,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
case .Catch:
|
||||
channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
|
||||
channel_type := checker.module.exprs[channel].type
|
||||
success := types.fallible_success(channel_type, store)
|
||||
if !types.is_valid(success) {
|
||||
id := source.add(checker.diagnostics, expr.span, "'catch' requires a fallible expression")
|
||||
return invalid_hir_expr(checker, expr.span, id)
|
||||
}
|
||||
if expr.right == ast.INVALID_EXPR {
|
||||
// ponytail: catch blocks need Build_Ctx threading through expression build; fallback catch covers v1.
|
||||
id := source.add(checker.diagnostics, expr.span, "catch block form is not implemented in v1")
|
||||
return invalid_hir_expr(checker, expr.span, id, success)
|
||||
}
|
||||
fallback := build_nested_expr(checker, expr.right, locals, global_reads, calls, success, pkg, file)
|
||||
fallback = coerce_expr(checker, fallback, success, checker.module.exprs[fallback].span)
|
||||
return add_hir_expr(checker, hir.Expr{
|
||||
kind=.Catch,
|
||||
span=expr.span,
|
||||
type=success,
|
||||
left=channel,
|
||||
right=fallback,
|
||||
target=hir.INVALID_REF,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
case .Range:
|
||||
expected_child := types.INVALID
|
||||
if types.is_range(expected, store) {
|
||||
@@ -3610,8 +3774,8 @@ build_expr :: proc(
|
||||
continue
|
||||
}
|
||||
switch expr.kind {
|
||||
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
|
||||
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed,
|
||||
case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice,
|
||||
.Field, .Unwrap, .Orelse, .Try, .Catch, .Struct_Literal, .Keyed,
|
||||
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range,
|
||||
.Enum_Literal:
|
||||
last = build_compound_expr(
|
||||
@@ -4169,6 +4333,7 @@ build_block :: proc(
|
||||
close := true,
|
||||
) -> []hir.Stmt_Id {
|
||||
checker := ctx.checker
|
||||
store := &checker.module.types
|
||||
body: [dynamic]hir.Stmt_Id
|
||||
body.allocator = checker.allocator
|
||||
scope_start := len(ctx.locals^)
|
||||
@@ -4488,7 +4653,16 @@ build_block :: proc(
|
||||
continue
|
||||
}
|
||||
if statement.expr == ast.INVALID_EXPR {
|
||||
if !types.is_void(ctx.result) {
|
||||
if types.kind(ctx.result, store) == .Fallible &&
|
||||
types.is_void(types.fallible_success(ctx.result, store)) {
|
||||
flush_defers(ctx, &body, 0)
|
||||
value := fallible_aggregate(checker, statement.span, ctx.result, hir.INVALID_EXPR, false)
|
||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
kind = .Return, span = statement.span, expr = value,
|
||||
local = hir.INVALID_LOCAL, diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
} else if !types.is_void(ctx.result) {
|
||||
id := source.add(checker.diagnostics, statement.span, "'return _' is only valid in a void function")
|
||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||
append(&checker.module.statements, hir.Stmt{
|
||||
@@ -4516,11 +4690,57 @@ build_block :: proc(
|
||||
ctx.problematic^ = true
|
||||
continue
|
||||
}
|
||||
value := build_expr(
|
||||
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
|
||||
ctx.result, ctx.pkg, ctx.file,
|
||||
)
|
||||
value = coerce_expr(checker, value, ctx.result, statement.span)
|
||||
value := hir.INVALID_EXPR
|
||||
if types.kind(ctx.result, store) == .Fallible {
|
||||
success := types.fallible_success(ctx.result, store)
|
||||
error_type := types.fallible_error(ctx.result, store)
|
||||
error_path := false
|
||||
expr_ast := checker.ast_module.exprs[statement.expr]
|
||||
if expr_ast.kind == .Enum_Literal {
|
||||
success_has := types.sum_has_name(store, success, u32(expr_ast.name))
|
||||
error_has := types.sum_has_name(store, error_type, u32(expr_ast.name))
|
||||
if error_has && !success_has {
|
||||
error_path = true
|
||||
} else if error_has && success_has {
|
||||
id := source.add(checker.diagnostics, expr_ast.span, "ambiguous fallible return member")
|
||||
value = invalid_hir_expr(checker, expr_ast.span, id, ctx.result)
|
||||
}
|
||||
} else if expr_ast.kind == .Struct_Literal {
|
||||
target_pkg, available := expr_package(checker, expr_ast, ctx.pkg, ctx.file, true)
|
||||
named := types.find_named(store, u32(target_pkg), u32(expr_ast.name)) if available else types.INVALID
|
||||
named = types.resolve_alias(named, store)
|
||||
error_path = can_implicitly_convert_type(checker, named, error_type)
|
||||
} else {
|
||||
probe := build_expr(
|
||||
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
|
||||
types.INVALID, ctx.pkg, ctx.file,
|
||||
)
|
||||
probe_type := checker.module.exprs[probe].type
|
||||
if can_implicitly_convert_type(checker, probe_type, error_type) &&
|
||||
!can_implicitly_convert_type(checker, probe_type, success) {
|
||||
error_path = true
|
||||
value = probe
|
||||
} else if can_implicitly_convert_type(checker, probe_type, success) {
|
||||
value = probe
|
||||
}
|
||||
}
|
||||
if value == hir.INVALID_EXPR {
|
||||
expected := error_type if error_path else success
|
||||
value = build_expr(
|
||||
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
|
||||
expected, ctx.pkg, ctx.file,
|
||||
)
|
||||
}
|
||||
expected := error_type if error_path else success
|
||||
value = coerce_expr(checker, value, expected, statement.span)
|
||||
value = fallible_aggregate(checker, statement.span, ctx.result, value, error_path)
|
||||
} else {
|
||||
value = build_expr(
|
||||
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
|
||||
ctx.result, ctx.pkg, ctx.file,
|
||||
)
|
||||
value = coerce_expr(checker, value, ctx.result, statement.span)
|
||||
}
|
||||
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid
|
||||
// Run deferred statements before returning, but capture the return value
|
||||
// first (spill it to a temp) so a defer that mutates the returned local
|
||||
@@ -6506,7 +6726,10 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
|
||||
loop_is_loop = &loop_is_loop,
|
||||
yield_targets = &yield_targets,
|
||||
}
|
||||
previous_result := checker.current_result
|
||||
checker.current_result = spec.result
|
||||
block := build_block(&ctx, function.body)
|
||||
checker.current_result = previous_result
|
||||
returns := all_paths_return(&checker.module, block)
|
||||
for block_stmt in block {
|
||||
append(&body, block_stmt)
|
||||
|
||||
@@ -95,7 +95,10 @@ Expr_Kind :: enum u8 {
|
||||
Slice_Ptr,
|
||||
Unwrap,
|
||||
Orelse,
|
||||
Try,
|
||||
Catch,
|
||||
Widen,
|
||||
Sum_Widen,
|
||||
C_Coerce,
|
||||
C_Vararg_Promote,
|
||||
Retype,
|
||||
|
||||
@@ -94,6 +94,7 @@ Opcode :: enum u8 {
|
||||
Orelse_Begin,
|
||||
Orelse,
|
||||
Widen,
|
||||
Sum_Widen,
|
||||
C_Coerce,
|
||||
C_Vararg_Promote,
|
||||
Retype,
|
||||
|
||||
@@ -24,6 +24,8 @@ keyword_kind :: proc(text: string) -> token.Kind {
|
||||
case "alias": return .Keyword_Alias
|
||||
case "import": return .Keyword_Import
|
||||
case "return": return .Keyword_Return
|
||||
case "try": return .Keyword_Try
|
||||
case "catch": return .Keyword_Catch
|
||||
case "mut": return .Keyword_Mut
|
||||
case "none": return .Keyword_None
|
||||
case "undefined": return .Keyword_Undefined
|
||||
|
||||
+117
-4
@@ -163,7 +163,7 @@ llvm_type :: proc(value: types.Type, store: ^types.Store = nil) -> string {
|
||||
return "ptr"
|
||||
}
|
||||
return fmt.tprintf("{{ i1, %s }}", llvm_type(item.child, store))
|
||||
case .Struct, .Union:
|
||||
case .Struct, .Union, .Fallible:
|
||||
return fmt.tprintf("%%bro.type.%d", resolved)
|
||||
}
|
||||
selected := store.selected if store != nil else target.DEFAULT
|
||||
@@ -256,7 +256,7 @@ valid_value :: proc(
|
||||
.Load_Global, .Function_Address, .Address_Of, .Load, .Union_Tag, .Slice, .Length, .Slice_Ptr,
|
||||
.Extract, .Select, .Unwrap,
|
||||
.Optional_Is_Some, .Optional_Value, .Orelse,
|
||||
.Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer,
|
||||
.Widen, .Sum_Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer,
|
||||
.Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, .Pointer_Add, .Not, .Compare, .Call:
|
||||
return true
|
||||
case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin,
|
||||
@@ -690,6 +690,8 @@ emit_instruction_stream :: proc(
|
||||
expected_count = int(item.field_count)
|
||||
} else if ok && item.kind == .Union {
|
||||
expected_count = 1
|
||||
} else if ok && item.kind == .Fallible {
|
||||
expected_count = 1
|
||||
} else if ok && item.kind == .Range {
|
||||
expected_count = 3
|
||||
} else {
|
||||
@@ -701,6 +703,56 @@ emit_instruction_stream :: proc(
|
||||
continue
|
||||
}
|
||||
type_name := llvm_type(instruction.type, &emitter.module.types)
|
||||
if item.kind == .Fallible {
|
||||
success := item.child
|
||||
error_type := item.extra
|
||||
error_path := instruction.integer != 0
|
||||
payload_offset := types.fallible_payload_offset(instruction.type, &emitter.module.types, emitter.module.target)
|
||||
fmt.sbprintf(&emitter.builder, " %%fallible_slot%d = alloca %s, align %d\n", instruction_index, type_name, types.alignment_of(instruction.type, &emitter.module.types, emitter.module.target))
|
||||
fmt.sbprintf(&emitter.builder, " store %s zeroinitializer, ptr %%fallible_slot%d\n", type_name, instruction_index)
|
||||
if !error_path {
|
||||
fmt.sbprintf(&emitter.builder, " store i16 0, ptr %%fallible_slot%d\n", instruction_index)
|
||||
if !types.is_void(success) {
|
||||
if !valid_value(instructions, instruction.args[0], success, &emitter.module.types) {
|
||||
emit_recovery_value(emitter, instruction_index, instruction, "invalid fallible success operand")
|
||||
continue
|
||||
}
|
||||
fmt.sbprintf(&emitter.builder, " %%fallible_payload%d = getelementptr i8, ptr %%fallible_slot%d, i64 %d\n", instruction_index, instruction_index, payload_offset)
|
||||
fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(success, &emitter.module.types))
|
||||
write_operand(&emitter.builder, instructions, instruction.args[0], success, &emitter.module.types)
|
||||
fmt.sbprintf(&emitter.builder, ", ptr %%fallible_payload%d\n", instruction_index)
|
||||
}
|
||||
} else if types.is_enum(error_type, &emitter.module.types) {
|
||||
if !valid_value(instructions, instruction.args[0], error_type, &emitter.module.types) {
|
||||
emit_recovery_value(emitter, instruction_index, instruction, "invalid fallible enum error operand")
|
||||
continue
|
||||
}
|
||||
fmt.sbprintf(&emitter.builder, " store i16 ")
|
||||
write_operand(&emitter.builder, instructions, instruction.args[0], error_type, &emitter.module.types)
|
||||
fmt.sbprintf(&emitter.builder, ", ptr %%fallible_slot%d\n", instruction_index)
|
||||
} else if types.is_tagged_union(error_type, &emitter.module.types) {
|
||||
if !valid_value(instructions, instruction.args[0], error_type, &emitter.module.types) {
|
||||
emit_recovery_value(emitter, instruction_index, instruction, "invalid fallible union error operand")
|
||||
continue
|
||||
}
|
||||
error_slot_align := types.alignment_of(error_type, &emitter.module.types, emitter.module.target)
|
||||
fmt.sbprintf(&emitter.builder, " %%fallible_error_slot%d = alloca %s, align %d\n", instruction_index, llvm_type(error_type, &emitter.module.types), error_slot_align)
|
||||
fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(error_type, &emitter.module.types))
|
||||
write_operand(&emitter.builder, instructions, instruction.args[0], error_type, &emitter.module.types)
|
||||
fmt.sbprintf(&emitter.builder, ", ptr %%fallible_error_slot%d\n", instruction_index)
|
||||
fmt.sbprintf(&emitter.builder, " %%fallible_error_code%d = load i16, ptr %%fallible_error_slot%d\n", instruction_index, instruction_index)
|
||||
fmt.sbprintf(&emitter.builder, " store i16 %%fallible_error_code%d, ptr %%fallible_slot%d\n", instruction_index, instruction_index)
|
||||
error_payload_offset := types.union_payload_offset(error_type, &emitter.module.types, emitter.module.target)
|
||||
error_payload_size := types.sum_payload_size(error_type, &emitter.module.types, emitter.module.target)
|
||||
if error_payload_size > 0 {
|
||||
fmt.sbprintf(&emitter.builder, " %%fallible_error_payload%d = getelementptr i8, ptr %%fallible_error_slot%d, i64 %d\n", instruction_index, instruction_index, error_payload_offset)
|
||||
fmt.sbprintf(&emitter.builder, " %%fallible_payload%d = getelementptr i8, ptr %%fallible_slot%d, i64 %d\n", instruction_index, instruction_index, payload_offset)
|
||||
fmt.sbprintf(&emitter.builder, " call void @llvm.memcpy.p0.p0.i64(ptr %%fallible_payload%d, ptr %%fallible_error_payload%d, i64 %d, i1 false)\n", instruction_index, instruction_index, error_payload_size)
|
||||
}
|
||||
}
|
||||
fmt.sbprintf(&emitter.builder, " %%v%d = load %s, ptr %%fallible_slot%d\n", instruction_index, type_name, instruction_index)
|
||||
continue
|
||||
}
|
||||
if item.kind == .Union {
|
||||
fields := types.fields_for(&emitter.module.types, instruction.type)
|
||||
field_index := int(instruction.integer)
|
||||
@@ -958,6 +1010,10 @@ emit_instruction_stream :: proc(
|
||||
if types.is_pointer(base_type, &emitter.module.types) {
|
||||
base_type = types.child_type(base_type, &emitter.module.types)
|
||||
}
|
||||
if types.kind(base_type, &emitter.module.types) == .Fallible {
|
||||
fmt.sbprintf(&emitter.builder, " %%v%d = getelementptr i8, ptr %%v%d, i64 %d\n", instruction_index, instruction.a, types.fallible_payload_offset(base_type, &emitter.module.types, emitter.module.target))
|
||||
continue
|
||||
}
|
||||
fields := types.fields_for(&emitter.module.types, base_type)
|
||||
field_index := int(instruction.integer)
|
||||
if field_index < 0 || field_index >= len(fields) ||
|
||||
@@ -1254,6 +1310,45 @@ emit_instruction_stream :: proc(
|
||||
fmt.sbprintf(&emitter.builder, " %%v%d = %s %s ", instruction_index, operation, llvm_type(from_type, &emitter.module.types))
|
||||
write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types)
|
||||
fmt.sbprintf(&emitter.builder, " to %s\n", llvm_type(instruction.type, &emitter.module.types))
|
||||
case .Sum_Widen:
|
||||
if !valid_instruction(instructions, instruction.a) ||
|
||||
!types.can_sum_widen(instructions[instruction.a].type, instruction.type, &emitter.module.types) {
|
||||
emit_recovery_value(emitter, instruction_index, instruction, "invalid sum widening operand")
|
||||
continue
|
||||
}
|
||||
from_type := instructions[instruction.a].type
|
||||
if types.is_enum(from_type, &emitter.module.types) && types.is_enum(instruction.type, &emitter.module.types) {
|
||||
type_name := llvm_type(instruction.type, &emitter.module.types)
|
||||
fmt.sbprintf(&emitter.builder, " %%v%d = select i1 true, %s ", instruction_index, type_name)
|
||||
write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types)
|
||||
fmt.sbprintf(&emitter.builder, ", %s zeroinitializer\n", type_name)
|
||||
continue
|
||||
}
|
||||
if types.is_tagged_union(from_type, &emitter.module.types) && types.is_tagged_union(instruction.type, &emitter.module.types) {
|
||||
from_name := llvm_type(from_type, &emitter.module.types)
|
||||
to_name := llvm_type(instruction.type, &emitter.module.types)
|
||||
from_align := types.alignment_of(from_type, &emitter.module.types, emitter.module.target)
|
||||
to_align := types.alignment_of(instruction.type, &emitter.module.types, emitter.module.target)
|
||||
fmt.sbprintf(&emitter.builder, " %%sum_from_slot%d = alloca %s, align %d\n", instruction_index, from_name, from_align)
|
||||
fmt.sbprintf(&emitter.builder, " store %s ", from_name)
|
||||
write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types)
|
||||
fmt.sbprintf(&emitter.builder, ", ptr %%sum_from_slot%d\n", instruction_index)
|
||||
fmt.sbprintf(&emitter.builder, " %%sum_to_slot%d = alloca %s, align %d\n", instruction_index, to_name, to_align)
|
||||
fmt.sbprintf(&emitter.builder, " store %s zeroinitializer, ptr %%sum_to_slot%d\n", to_name, instruction_index)
|
||||
fmt.sbprintf(&emitter.builder, " %%sum_tag%d = load i16, ptr %%sum_from_slot%d\n", instruction_index, instruction_index)
|
||||
fmt.sbprintf(&emitter.builder, " store i16 %%sum_tag%d, ptr %%sum_to_slot%d\n", instruction_index, instruction_index)
|
||||
from_payload_offset := types.union_payload_offset(from_type, &emitter.module.types, emitter.module.target)
|
||||
to_payload_offset := types.union_payload_offset(instruction.type, &emitter.module.types, emitter.module.target)
|
||||
payload_size := types.sum_payload_size(from_type, &emitter.module.types, emitter.module.target)
|
||||
if payload_size > 0 {
|
||||
fmt.sbprintf(&emitter.builder, " %%sum_from_payload%d = getelementptr i8, ptr %%sum_from_slot%d, i64 %d\n", instruction_index, instruction_index, from_payload_offset)
|
||||
fmt.sbprintf(&emitter.builder, " %%sum_to_payload%d = getelementptr i8, ptr %%sum_to_slot%d, i64 %d\n", instruction_index, instruction_index, to_payload_offset)
|
||||
fmt.sbprintf(&emitter.builder, " call void @llvm.memcpy.p0.p0.i64(ptr %%sum_to_payload%d, ptr %%sum_from_payload%d, i64 %d, i1 false)\n", instruction_index, instruction_index, payload_size)
|
||||
}
|
||||
fmt.sbprintf(&emitter.builder, " %%v%d = load %s, ptr %%sum_to_slot%d\n", instruction_index, to_name, instruction_index)
|
||||
continue
|
||||
}
|
||||
emit_recovery_value(emitter, instruction_index, instruction, "unsupported sum widening operand")
|
||||
case .C_Coerce:
|
||||
if !valid_instruction(instructions, instruction.a) ||
|
||||
!types.can_coerce_c_integer(instructions[instruction.a].type, instruction.type) {
|
||||
@@ -1814,7 +1909,7 @@ emit_globals :: proc(emitter: ^Emitter) {
|
||||
|
||||
emit_types :: proc(emitter: ^Emitter) {
|
||||
for item, index in emitter.module.types.nodes {
|
||||
if item.kind != .Struct && item.kind != .Union {
|
||||
if item.kind != .Struct && item.kind != .Union && item.kind != .Fallible {
|
||||
continue
|
||||
}
|
||||
id := types.DYNAMIC_START+types.Type(index)
|
||||
@@ -1823,6 +1918,24 @@ emit_types :: proc(emitter: ^Emitter) {
|
||||
strings.write_string(&emitter.builder, "opaque\n")
|
||||
continue
|
||||
}
|
||||
if item.kind == .Fallible {
|
||||
payload_offset := types.fallible_payload_offset(id, &emitter.module.types, emitter.module.target)
|
||||
payload_size := types.fallible_payload_size(id, &emitter.module.types, emitter.module.target)
|
||||
total_size := types.size(id, &emitter.module.types, emitter.module.target)
|
||||
strings.write_string(&emitter.builder, "{ i16")
|
||||
if payload_offset > 2 {
|
||||
fmt.sbprintf(&emitter.builder, ", [%d x i8]", payload_offset-2)
|
||||
}
|
||||
if payload_size > 0 {
|
||||
fmt.sbprintf(&emitter.builder, ", [%d x i8]", payload_size)
|
||||
}
|
||||
used := payload_offset + payload_size
|
||||
if used < total_size {
|
||||
fmt.sbprintf(&emitter.builder, ", [%d x i8]", total_size-used)
|
||||
}
|
||||
strings.write_string(&emitter.builder, " }\n")
|
||||
continue
|
||||
}
|
||||
if item.kind == .Union {
|
||||
fields := types.fields_for(&emitter.module.types, id)
|
||||
carrier := types.INVALID
|
||||
@@ -1839,7 +1952,7 @@ emit_types :: proc(emitter: ^Emitter) {
|
||||
}
|
||||
}
|
||||
total_size := types.size(id, &emitter.module.types, emitter.module.target)
|
||||
if !types.is_valid(carrier) {
|
||||
if !types.is_valid(carrier) || types.is_void(carrier) {
|
||||
fmt.sbprintf(&emitter.builder, "[%d x i8]\n", total_size)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ translate_c_type :: proc(
|
||||
}
|
||||
|
||||
function_signatures_equal :: proc(left: ast.Function, params: []ast.Param, result: types.Type, variadic: bool) -> bool {
|
||||
if left.result != result || left.variadic != variadic || len(left.params) != len(params) {
|
||||
if left.result != result || types.is_valid(left.error) || left.variadic != variadic || len(left.params) != len(params) {
|
||||
return false
|
||||
}
|
||||
for param, index in params {
|
||||
@@ -1333,12 +1333,20 @@ canonical_type :: proc(
|
||||
}
|
||||
if item.kind == .Struct || item.kind == .Union {
|
||||
mapping[index] = value
|
||||
module.type_store.nodes[index].extra = canonical_type(module, item.extra, mapping, visiting)
|
||||
fields := types.fields_for(&module.type_store, value)
|
||||
for &field in fields {
|
||||
field.type = canonical_type(module, field.type, mapping, visiting)
|
||||
}
|
||||
return value
|
||||
}
|
||||
if item.kind == .Fallible {
|
||||
item.child = canonical_type(module, item.child, mapping, visiting)
|
||||
item.extra = canonical_type(module, item.extra, mapping, visiting)
|
||||
resolved := types.fallible(&module.type_store, item.child, item.extra)
|
||||
mapping[index] = resolved
|
||||
return resolved
|
||||
}
|
||||
if item.kind == .Function {
|
||||
params := make([]types.Type, int(item.field_count), context.temp_allocator)
|
||||
for param, param_index in types.params_for(&module.type_store, value) {
|
||||
@@ -1352,6 +1360,9 @@ canonical_type :: proc(
|
||||
if types.is_valid(item.child) {
|
||||
item.child = canonical_type(module, item.child, mapping, visiting)
|
||||
}
|
||||
if types.is_valid(item.extra) {
|
||||
item.extra = canonical_type(module, item.extra, mapping, visiting)
|
||||
}
|
||||
resolved := types.intern(&module.type_store, item)
|
||||
mapping[index] = resolved
|
||||
return resolved
|
||||
@@ -1368,6 +1379,7 @@ canonicalize_types :: proc(module: ^ast.Module, allocator: mem.Allocator) {
|
||||
param.type = canonical_type(module, param.type, mapping, visiting)
|
||||
}
|
||||
function.result = canonical_type(module, function.result, mapping, visiting)
|
||||
function.error = canonical_type(module, function.error, mapping, visiting)
|
||||
}
|
||||
for &global in module.globals {
|
||||
global.type = canonical_type(module, global.type, mapping, visiting)
|
||||
@@ -1378,6 +1390,9 @@ canonicalize_types :: proc(module: ^ast.Module, allocator: mem.Allocator) {
|
||||
for index := 0; index < original_count; index += 1 {
|
||||
_ = canonical_type(module, types.DYNAMIC_START+types.Type(index), mapping, visiting)
|
||||
}
|
||||
for &variant in module.type_store.variants {
|
||||
variant.payload = canonical_type(module, variant.payload, mapping, visiting)
|
||||
}
|
||||
}
|
||||
|
||||
load :: proc(
|
||||
|
||||
+102
-2
@@ -366,6 +366,105 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
|
||||
target=ir.INVALID_REF, a=begin, b=fallback,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
case .Try, .Catch:
|
||||
channel := lower_nested_expr(state, expr.left)
|
||||
channel_type := state.hir_module.exprs[expr.left].type
|
||||
success := types.fallible_success(channel_type, &state.hir_module.types)
|
||||
channel_slot := append_instruction(state, ir.Instruction{
|
||||
op=.Alloca, span=expr.span, type=channel_type,
|
||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
append_instruction(state, ir.Instruction{
|
||||
op=.Store, span=expr.span, type=channel_type,
|
||||
target=ir.INVALID_REF, a=channel_slot, b=channel, diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
code := append_instruction(state, ir.Instruction{
|
||||
op=.Union_Tag, span=expr.span, type=types.U16,
|
||||
target=ir.INVALID_REF, a=channel_slot, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
zero := append_instruction(state, ir.Instruction{
|
||||
op=.Const, span=expr.span, type=types.U16, integer=0,
|
||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
ok := append_instruction(state, ir.Instruction{
|
||||
op=.Compare, span=expr.span, type=types.BOOL, integer=i64(ir.Compare_Predicate.Eq),
|
||||
target=ir.INVALID_REF, a=code, b=zero, diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
success_lbl := fresh_label(state)
|
||||
error_lbl := fresh_label(state)
|
||||
merge_lbl := fresh_label(state)
|
||||
slot := append_instruction(state, ir.Instruction{
|
||||
op=.Alloca, span=expr.span, type=success,
|
||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
append_instruction(state, ir.Instruction{
|
||||
op=.Cond_Br, span=expr.span, type=types.VOID,
|
||||
integer=success_lbl, target=ir.Ref(u32(error_lbl)), a=ok,
|
||||
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
append_instruction(state, ir.Instruction{
|
||||
op=.Label, span=expr.span, type=types.VOID, integer=error_lbl,
|
||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
if expr.kind == .Try {
|
||||
append_instruction(state, ir.Instruction{
|
||||
op=.Return, span=expr.span, type=state.func_result,
|
||||
target=ir.INVALID_REF, a=channel, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
} else {
|
||||
fallback := lower_nested_expr(state, expr.right)
|
||||
append_instruction(state, ir.Instruction{
|
||||
op=.Store, span=expr.span, type=success,
|
||||
target=ir.INVALID_REF, a=slot, b=fallback, diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
append_instruction(state, ir.Instruction{
|
||||
op=.Br, span=expr.span, type=types.VOID, integer=merge_lbl,
|
||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
}
|
||||
append_instruction(state, ir.Instruction{
|
||||
op=.Label, span=expr.span, type=types.VOID, integer=success_lbl,
|
||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
if !types.is_void(success) {
|
||||
payload := append_instruction(state, ir.Instruction{
|
||||
op=.Field_Address, span=expr.span, type=success, integer=0,
|
||||
target=ir.INVALID_REF, a=channel_slot, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
loaded := append_instruction(state, ir.Instruction{
|
||||
op=.Load, span=expr.span, type=success,
|
||||
target=ir.INVALID_REF, a=payload, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
append_instruction(state, ir.Instruction{
|
||||
op=.Store, span=expr.span, type=success,
|
||||
target=ir.INVALID_REF, a=slot, b=loaded, diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
}
|
||||
append_instruction(state, ir.Instruction{
|
||||
op=.Br, span=expr.span, type=types.VOID, integer=merge_lbl,
|
||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
append_instruction(state, ir.Instruction{
|
||||
op=.Label, span=expr.span, type=types.VOID, integer=merge_lbl,
|
||||
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
return append_instruction(state, ir.Instruction{
|
||||
op=.Load, span=expr.span, type=success,
|
||||
target=ir.INVALID_REF, a=slot, b=ir.INVALID_INSTRUCTION,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
case .Not:
|
||||
value := lower_nested_expr(state, expr.left)
|
||||
return append_instruction(state, ir.Instruction{
|
||||
@@ -475,7 +574,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
|
||||
_ = pop(&stack)
|
||||
case .String, .Array, .Struct, .Range, .None, .Optional_Some, .Address, .Deref,
|
||||
.Index, .Slice, .Field, .Union_Tag, .Length, .Slice_Ptr, .Unwrap, .Orelse,
|
||||
.Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
|
||||
.Try, .Catch, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
|
||||
last = lower_compound_expr(state, frame.expr)
|
||||
_ = pop(&stack)
|
||||
case .Local:
|
||||
@@ -518,7 +617,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
|
||||
})
|
||||
}
|
||||
_ = pop(&stack)
|
||||
case .Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer:
|
||||
case .Widen, .Sum_Widen, .C_Coerce, .C_Vararg_Promote, .Retype, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer:
|
||||
stack[frame_index].stage = 1
|
||||
append(&stack, Lower_Expr_Frame{expr=expr.left})
|
||||
case .Negate:
|
||||
@@ -572,6 +671,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
|
||||
if frame.stage == 1 {
|
||||
op := ir.Opcode.Widen
|
||||
#partial switch expr.kind {
|
||||
case .Sum_Widen: op = .Sum_Widen
|
||||
case .Weaken_Pointer: op = .Weaken_Pointer
|
||||
case .Weaken_Slice: op = .Weaken_Slice
|
||||
case .Decay_Array_Pointer: op = .Decay_Array_Pointer
|
||||
|
||||
+87
-33
@@ -165,7 +165,7 @@ parse_type_constant :: proc(parser: ^Parser) -> (u64, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax {
|
||||
parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
|
||||
tok := current(parser)
|
||||
if tok.kind == .Question {
|
||||
advance(parser)
|
||||
@@ -366,6 +366,25 @@ parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax {
|
||||
return types.INVALID
|
||||
}
|
||||
|
||||
parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax {
|
||||
left := parse_type_atom(parser)
|
||||
for current(parser).kind == .Pipe {
|
||||
operator := advance(parser)
|
||||
right := parse_type_atom(parser)
|
||||
composed, compose_error := types.compose_sum(&parser.module.type_store, left, right)
|
||||
if compose_error == .Unsupported {
|
||||
source.add(parser.diagnostics, operator.span, "only native unbacked enums and tagged unions can be composed with '|'")
|
||||
left = types.INVALID
|
||||
} else if compose_error == .Conflict {
|
||||
source.add(parser.diagnostics, operator.span, "sum composition contains the same variant name with different payload types")
|
||||
left = types.INVALID
|
||||
} else {
|
||||
left = composed
|
||||
}
|
||||
}
|
||||
return left
|
||||
}
|
||||
|
||||
skip_parenthesized :: proc(parser: ^Parser) -> source.Span {
|
||||
start := current(parser)
|
||||
depth := 0
|
||||
@@ -706,7 +725,7 @@ infix_binding_power :: proc(kind: token.Kind) -> (left, right: int, ok: bool) {
|
||||
#partial switch kind {
|
||||
case .Range, .Range_Inclusive:
|
||||
return 0, 1, true
|
||||
case .Keyword_Orelse:
|
||||
case .Keyword_Orelse, .Keyword_Catch:
|
||||
return 2, 3, true
|
||||
case .Keyword_Or:
|
||||
return 4, 5, true
|
||||
@@ -726,6 +745,7 @@ infix_expr_kind :: proc(kind: token.Kind) -> ast.Expr_Kind {
|
||||
#partial switch kind {
|
||||
case .Range, .Range_Inclusive: return .Range
|
||||
case .Keyword_Orelse: return .Orelse
|
||||
case .Keyword_Catch: return .Catch
|
||||
case .Keyword_Or: return .Or
|
||||
case .Keyword_And: return .And
|
||||
case .Equal_Equal: return .Eq
|
||||
@@ -766,7 +786,7 @@ is_simple_range_bound :: proc(expr: ast.Expr) -> bool {
|
||||
|
||||
prefix_binding_power :: proc(kind: token.Kind) -> (right: int, ok: bool) {
|
||||
#partial switch kind {
|
||||
case .Minus, .Ampersand, .Bang:
|
||||
case .Minus, .Ampersand, .Bang, .Keyword_Try:
|
||||
return 20, true
|
||||
}
|
||||
return 0, false
|
||||
@@ -792,6 +812,7 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
|
||||
#partial switch operator.kind {
|
||||
case .Ampersand: prefix_kind = .Address
|
||||
case .Bang: prefix_kind = .Not
|
||||
case .Keyword_Try: prefix_kind = .Try
|
||||
}
|
||||
left = add_expr(parser, ast.Expr{
|
||||
kind=prefix_kind,
|
||||
@@ -921,6 +942,42 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
|
||||
}
|
||||
operator := advance(parser)
|
||||
skip_newlines(parser)
|
||||
if operator.kind == .Keyword_Catch {
|
||||
left_expr := parser.module.exprs[left]
|
||||
if _, pipe_ok := allow(parser, .Pipe); pipe_ok {
|
||||
capture := current(parser)
|
||||
if capture.kind != .Identifier && capture.kind != .Underscore {
|
||||
source.add(parser.diagnostics, capture.span, "expected a catch capture name")
|
||||
} else {
|
||||
advance(parser)
|
||||
}
|
||||
if _, close_ok := allow(parser, .Pipe); !close_ok {
|
||||
source.add(parser.diagnostics, current(parser).span, "expected '|' after catch capture")
|
||||
}
|
||||
body := parse_block(parser)
|
||||
end := previous(parser)
|
||||
left = add_expr(parser, ast.Expr{
|
||||
kind=.Catch,
|
||||
span=span_from(left_expr.span, end.span),
|
||||
name=capture.symbol,
|
||||
left=left,
|
||||
right=ast.INVALID_EXPR,
|
||||
body=body,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
continue
|
||||
}
|
||||
right := parse_expression_bp(parser, right_power, nesting+1)
|
||||
right_expr := parser.module.exprs[right]
|
||||
left = add_expr(parser, ast.Expr{
|
||||
kind=.Catch,
|
||||
span=span_from(left_expr.span, right_expr.span),
|
||||
left=left,
|
||||
right=right,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
continue
|
||||
}
|
||||
right := parse_expression_bp(parser, right_power, nesting+1)
|
||||
left_expr := parser.module.exprs[left]
|
||||
right_expr := parser.module.exprs[right]
|
||||
@@ -1812,6 +1869,10 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
|
||||
}
|
||||
skip_newlines(parser)
|
||||
result := parse_type(parser)
|
||||
error_type := types.INVALID
|
||||
if _, ok := allow(parser, .Bang); ok {
|
||||
error_type = parse_type(parser)
|
||||
}
|
||||
end := previous(parser)
|
||||
ended_by_newline := current(parser).kind == .Newline
|
||||
if current(parser).kind == .Newline {
|
||||
@@ -1832,6 +1893,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
|
||||
variadic=variadic,
|
||||
params=params,
|
||||
result=result,
|
||||
error=error_type,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
return
|
||||
@@ -1849,6 +1911,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
|
||||
variadic=variadic,
|
||||
params=params,
|
||||
result=result,
|
||||
error=error_type,
|
||||
body=body,
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
@@ -1860,13 +1923,14 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_unio
|
||||
// A tagged union spells its discriminant in parens: `union(Enum)` reuses an existing
|
||||
// enum; `union(enum)` synthesizes one from the variant names after the body is parsed.
|
||||
tag := types.INVALID
|
||||
declared_tag := types.INVALID
|
||||
inferred_tag := false
|
||||
if is_union {
|
||||
if _, ok := allow(parser, .Left_Paren); ok {
|
||||
if _, enum_ok := allow(parser, .Keyword_Enum); enum_ok {
|
||||
inferred_tag = true
|
||||
} else {
|
||||
tag = parse_type(parser)
|
||||
declared_tag = parse_type(parser)
|
||||
}
|
||||
if _, close_ok := allow(parser, .Right_Paren); !close_ok {
|
||||
source.add(parser.diagnostics, current(parser).span, "expected ')' after union tag")
|
||||
@@ -1882,7 +1946,7 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_unio
|
||||
"native union declarations require a body" if is_union else "native struct declarations require a body",
|
||||
)
|
||||
}
|
||||
if !types.define_record(&parser.module.type_store, id, nil, c_layout, true, is_union, tag=tag) {
|
||||
if !types.define_record(&parser.module.type_store, id, nil, c_layout, true, is_union, tag=tag, declared_tag=declared_tag) {
|
||||
source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name))
|
||||
}
|
||||
if !ended_by_newline {
|
||||
@@ -1918,36 +1982,29 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_unio
|
||||
if _, ok := allow(parser, .Right_Brace); !ok {
|
||||
source.add(parser.diagnostics, current(parser).span, "expected '}' after struct fields")
|
||||
}
|
||||
if inferred_tag {
|
||||
if is_union && (inferred_tag || types.is_valid(declared_tag)) {
|
||||
tag = synthesize_union_tag(parser, fields[:])
|
||||
}
|
||||
if !types.define_record(&parser.module.type_store, id, fields[:], c_layout, false, is_union, tag=tag) {
|
||||
if !types.define_record(&parser.module.type_store, id, fields[:], c_layout, false, is_union, tag=tag, declared_tag=declared_tag) {
|
||||
source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name))
|
||||
}
|
||||
_ = finish_statement(parser)
|
||||
}
|
||||
|
||||
// synthesize_union_tag builds the anonymous discriminant enum for a `union(enum)`:
|
||||
// one member per variant, dense 0-based, in the smallest fitting unsigned backing
|
||||
// (mirroring `parse_enum`'s unbacked pick).
|
||||
// synthesize_union_tag builds the anonymous runtime discriminant enum for a tagged
|
||||
// union: one member per variant, valued by the program-global (name, payload-type)
|
||||
// ID. The declared tag enum, if any, remains only the validation surface.
|
||||
synthesize_union_tag :: proc(parser: ^Parser, fields: []types.Field) -> types.Type {
|
||||
members := make([]types.Enum_Member, len(fields), parser.module.allocator)
|
||||
defer delete(members, parser.module.allocator)
|
||||
for field, index in fields {
|
||||
members[index] = types.Enum_Member{name=field.name, value=i128(index)}
|
||||
id, ok := types.variant_id(&parser.module.type_store, field.name, field.type)
|
||||
if !ok {
|
||||
source.add(parser.diagnostics, current(parser).span, "too many global sum variants for u16 tags")
|
||||
}
|
||||
members[index] = types.Enum_Member{name=field.name, value=i128(id)}
|
||||
}
|
||||
max_value := u64(max(len(fields)-1, 0))
|
||||
backing := types.U8
|
||||
if max_value > 0xff {
|
||||
backing = types.U16
|
||||
}
|
||||
if max_value > 0xffff {
|
||||
backing = types.U32
|
||||
}
|
||||
if max_value > 0xffff_ffff {
|
||||
backing = types.U64
|
||||
}
|
||||
return types.enum_anonymous(&parser.module.type_store, members, backing)
|
||||
return types.enum_anonymous(&parser.module.type_store, members, types.U16)
|
||||
}
|
||||
|
||||
parse_distinct :: proc(parser: ^Parser, name: token.Token) {
|
||||
@@ -2069,16 +2126,13 @@ parse_enum :: proc(parser: ^Parser, name: token.Token) {
|
||||
source.add(parser.diagnostics, start.span, "enum declarations require at least one member")
|
||||
}
|
||||
if !explicit_backing {
|
||||
max_value := u64(max(len(members)-1, 0))
|
||||
backing = types.U8
|
||||
if max_value > 0xff {
|
||||
backing = types.U16
|
||||
}
|
||||
if max_value > 0xffff {
|
||||
backing = types.U32
|
||||
}
|
||||
if max_value > 0xffff_ffff {
|
||||
backing = types.U64
|
||||
backing = types.U16
|
||||
for &member in members {
|
||||
id, ok := types.variant_id(&parser.module.type_store, member.name, types.VOID)
|
||||
if !ok {
|
||||
source.add(parser.diagnostics, start.span, "too many global sum variants for u16 tags")
|
||||
}
|
||||
member.value = i128(id)
|
||||
}
|
||||
}
|
||||
id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol))
|
||||
|
||||
@@ -59,6 +59,8 @@ Kind :: enum u8 {
|
||||
Keyword_Alias,
|
||||
Keyword_Import,
|
||||
Keyword_Return,
|
||||
Keyword_Try,
|
||||
Keyword_Catch,
|
||||
Keyword_Mut,
|
||||
Keyword_None,
|
||||
Keyword_Undefined,
|
||||
|
||||
+299
-3
@@ -72,11 +72,13 @@ Kind :: enum u8 {
|
||||
Enum,
|
||||
Struct,
|
||||
Union,
|
||||
Fallible,
|
||||
}
|
||||
|
||||
Node :: struct {
|
||||
kind: Kind,
|
||||
child: Type,
|
||||
extra: Type,
|
||||
count: u64,
|
||||
sentinel: u64,
|
||||
explicit_size: u64,
|
||||
@@ -110,10 +112,29 @@ Enum_Member :: struct {
|
||||
value: i128,
|
||||
}
|
||||
|
||||
Variant :: struct {
|
||||
name: u32,
|
||||
payload: Type,
|
||||
id: u16,
|
||||
}
|
||||
|
||||
Sum_Variant :: struct {
|
||||
name: u32,
|
||||
payload: Type,
|
||||
id: u16,
|
||||
}
|
||||
|
||||
Compose_Error :: enum u8 {
|
||||
None,
|
||||
Unsupported,
|
||||
Conflict,
|
||||
}
|
||||
|
||||
Store :: struct {
|
||||
nodes: [dynamic]Node,
|
||||
fields: [dynamic]Field,
|
||||
enum_members: [dynamic]Enum_Member,
|
||||
variants: [dynamic]Variant,
|
||||
selected: target.Target,
|
||||
allocator: mem.Allocator,
|
||||
}
|
||||
@@ -123,6 +144,7 @@ init_store :: proc(allocator := context.allocator) -> Store {
|
||||
store.nodes.allocator = allocator
|
||||
store.fields.allocator = allocator
|
||||
store.enum_members.allocator = allocator
|
||||
store.variants.allocator = allocator
|
||||
store.selected = target.DEFAULT
|
||||
store.allocator = allocator
|
||||
return store
|
||||
@@ -132,6 +154,7 @@ destroy_store :: proc(store: ^Store) {
|
||||
delete(store.nodes)
|
||||
delete(store.fields)
|
||||
delete(store.enum_members)
|
||||
delete(store.variants)
|
||||
}
|
||||
|
||||
clone_store :: proc(source: ^Store, allocator := context.allocator) -> Store {
|
||||
@@ -139,6 +162,7 @@ clone_store :: proc(source: ^Store, allocator := context.allocator) -> Store {
|
||||
append(&store.nodes, ..source.nodes[:])
|
||||
append(&store.fields, ..source.fields[:])
|
||||
append(&store.enum_members, ..source.enum_members[:])
|
||||
append(&store.variants, ..source.variants[:])
|
||||
store.selected = source.selected
|
||||
return store
|
||||
}
|
||||
@@ -230,6 +254,7 @@ define_record :: proc(
|
||||
explicit_size: u64 = 0,
|
||||
explicit_alignment: u32 = 0,
|
||||
tag: Type = INVALID,
|
||||
declared_tag: Type = INVALID,
|
||||
) -> bool {
|
||||
existing, ok := node(store, id)
|
||||
if !ok || (existing.kind != .Named && existing.kind != .Struct && existing.kind != .Union) ||
|
||||
@@ -247,6 +272,7 @@ define_record :: proc(
|
||||
// structs leave it INVALID); the per-variant tag value is derived from the enum
|
||||
// member whose name matches the variant, so no extra storage is needed.
|
||||
store.nodes[index].child = tag
|
||||
store.nodes[index].extra = declared_tag
|
||||
store.nodes[index].field_start = u32(len(store.fields))
|
||||
store.nodes[index].field_count = u32(len(fields))
|
||||
append(&store.fields, ..fields)
|
||||
@@ -268,6 +294,180 @@ enum_anonymous :: proc(store: ^Store, members: []Enum_Member, backing: Type) ->
|
||||
})
|
||||
}
|
||||
|
||||
union_anonymous :: proc(store: ^Store, fields: []Field, tag: Type) -> Type {
|
||||
start := u32(len(store.fields))
|
||||
append(&store.fields, ..fields)
|
||||
return intern(store, Node{
|
||||
kind=.Union,
|
||||
child=tag,
|
||||
field_start=start,
|
||||
field_count=u32(len(fields)),
|
||||
declared=true,
|
||||
})
|
||||
}
|
||||
|
||||
variant_id :: proc(store: ^Store, name: u32, payload: Type) -> (u16, bool) {
|
||||
for variant in store.variants {
|
||||
if variant.name == name && variant.payload == payload {
|
||||
return variant.id, true
|
||||
}
|
||||
}
|
||||
next := len(store.variants)+1
|
||||
if next > 0xffff {
|
||||
return 0, false
|
||||
}
|
||||
id := u16(next)
|
||||
append(&store.variants, Variant{name=name, payload=payload, id=id})
|
||||
return id, true
|
||||
}
|
||||
|
||||
fallible :: proc(store: ^Store, success, error: Type) -> Type {
|
||||
return intern(store, Node{kind=.Fallible, child=success, extra=error, declared=true})
|
||||
}
|
||||
|
||||
fallible_success :: proc(value: Type, store: ^Store) -> Type {
|
||||
item, ok := node(store, value)
|
||||
return item.child if ok && item.kind == .Fallible else INVALID
|
||||
}
|
||||
|
||||
fallible_error :: proc(value: Type, store: ^Store) -> Type {
|
||||
item, ok := node(store, value)
|
||||
return item.extra if ok && item.kind == .Fallible else INVALID
|
||||
}
|
||||
|
||||
append_sum_variants :: proc(store: ^Store, value: Type, out: ^[dynamic]Sum_Variant) -> bool {
|
||||
item, ok := node(store, value)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if item.kind == .Enum {
|
||||
if item.explicit_backing {
|
||||
return false
|
||||
}
|
||||
for member in enum_members_for(store, value) {
|
||||
if member.value <= 0 || member.value > 0xffff {
|
||||
return false
|
||||
}
|
||||
append(out, Sum_Variant{name=member.name, payload=VOID, id=u16(member.value)})
|
||||
}
|
||||
return true
|
||||
}
|
||||
if item.kind != .Union || !is_enum(item.child, store) || item.c_layout {
|
||||
return false
|
||||
}
|
||||
tag_members := enum_members_for(store, item.child)
|
||||
for field in fields_for(store, value) {
|
||||
found := false
|
||||
for member in tag_members {
|
||||
if member.name == field.name {
|
||||
if member.value <= 0 || member.value > 0xffff {
|
||||
return false
|
||||
}
|
||||
append(out, Sum_Variant{name=field.name, payload=field.type, id=u16(member.value)})
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
compose_sum :: proc(store: ^Store, left, right: Type) -> (Type, Compose_Error) {
|
||||
variants: [dynamic]Sum_Variant
|
||||
variants.allocator = store.allocator
|
||||
defer delete(variants)
|
||||
if !append_sum_variants(store, left, &variants) {
|
||||
return INVALID, .Unsupported
|
||||
}
|
||||
right_variants: [dynamic]Sum_Variant
|
||||
right_variants.allocator = store.allocator
|
||||
defer delete(right_variants)
|
||||
if !append_sum_variants(store, right, &right_variants) {
|
||||
return INVALID, .Unsupported
|
||||
}
|
||||
for candidate in right_variants {
|
||||
merged := false
|
||||
for existing in variants {
|
||||
if existing.id == candidate.id {
|
||||
merged = true
|
||||
break
|
||||
}
|
||||
if existing.name == candidate.name && existing.payload != candidate.payload {
|
||||
return INVALID, .Conflict
|
||||
}
|
||||
}
|
||||
if !merged {
|
||||
append(&variants, candidate)
|
||||
}
|
||||
}
|
||||
all_void := true
|
||||
for variant in variants {
|
||||
all_void = all_void && variant.payload == VOID
|
||||
}
|
||||
members := make([]Enum_Member, len(variants), store.allocator)
|
||||
defer delete(members, store.allocator)
|
||||
for variant, index in variants {
|
||||
members[index] = Enum_Member{name=variant.name, value=i128(variant.id)}
|
||||
}
|
||||
if all_void {
|
||||
return enum_anonymous(store, members, U16), .None
|
||||
}
|
||||
fields := make([]Field, len(variants), store.allocator)
|
||||
defer delete(fields, store.allocator)
|
||||
for variant, index in variants {
|
||||
fields[index] = Field{name=variant.name, type=variant.payload}
|
||||
}
|
||||
tag := enum_anonymous(store, members, U16)
|
||||
return union_anonymous(store, fields, tag), .None
|
||||
}
|
||||
|
||||
sum_has_name :: proc(store: ^Store, value: Type, name: u32) -> bool {
|
||||
variants: [dynamic]Sum_Variant
|
||||
variants.allocator = store.allocator
|
||||
defer delete(variants)
|
||||
if !append_sum_variants(store, value, &variants) {
|
||||
return false
|
||||
}
|
||||
for variant in variants {
|
||||
if variant.name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
can_sum_widen :: proc(from, to: Type, store: ^Store) -> bool {
|
||||
if from == to {
|
||||
return true
|
||||
}
|
||||
from_variants: [dynamic]Sum_Variant
|
||||
from_variants.allocator = store.allocator
|
||||
defer delete(from_variants)
|
||||
to_variants: [dynamic]Sum_Variant
|
||||
to_variants.allocator = store.allocator
|
||||
defer delete(to_variants)
|
||||
if !append_sum_variants(store, from, &from_variants) ||
|
||||
!append_sum_variants(store, to, &to_variants) {
|
||||
return false
|
||||
}
|
||||
for needed in from_variants {
|
||||
found := false
|
||||
for available in to_variants {
|
||||
if available.id == needed.id && available.payload == needed.payload {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
define_struct :: proc(store: ^Store, id: Type, fields: []Field, c_layout, opaque: bool) -> bool {
|
||||
return define_record(store, id, fields, c_layout, opaque)
|
||||
}
|
||||
@@ -514,7 +714,7 @@ is_concrete :: proc(value: Type, store: ^Store = nil) -> bool {
|
||||
value_kind := kind(value, store)
|
||||
if value_kind == .Scalar || value_kind == .Array || value_kind == .Pointer ||
|
||||
value_kind == .Slice || value_kind == .Range || value_kind == .Optional ||
|
||||
value_kind == .Enum {
|
||||
value_kind == .Enum || value_kind == .Fallible {
|
||||
return true
|
||||
}
|
||||
if value_kind == .Struct || value_kind == .Union {
|
||||
@@ -575,6 +775,14 @@ union_tag_enum :: proc(value: Type, store: ^Store) -> Type {
|
||||
return item.child
|
||||
}
|
||||
|
||||
union_declared_tag_enum :: proc(value: Type, store: ^Store) -> Type {
|
||||
if !is_tagged_union(value, store) {
|
||||
return INVALID
|
||||
}
|
||||
item, _ := node(store, value)
|
||||
return item.extra if is_valid(item.extra) else item.child
|
||||
}
|
||||
|
||||
// union_payload_offset is the byte offset of a tagged union's payload carrier (after
|
||||
// the discriminant), shared by `size` and the LLVM emitter so construction, field
|
||||
// access, and layout agree. Zero for untagged unions.
|
||||
@@ -591,6 +799,16 @@ union_payload_offset :: proc(value: Type, store: ^Store, selected := target.DEFA
|
||||
return (tag_size+payload_align-1)/payload_align*payload_align
|
||||
}
|
||||
|
||||
sum_tag_type :: proc(value: Type, store: ^Store) -> Type {
|
||||
if is_enum(value, store) {
|
||||
return value
|
||||
}
|
||||
if is_tagged_union(value, store) {
|
||||
return union_tag_enum(value, store)
|
||||
}
|
||||
return INVALID
|
||||
}
|
||||
|
||||
is_distinct :: proc(value: Type, store: ^Store) -> bool {
|
||||
return kind(value, store) == .Distinct
|
||||
}
|
||||
@@ -666,6 +884,13 @@ is_runtime_value :: proc(value: Type, store: ^Store, depth := 0) -> bool {
|
||||
item, ok := node(store, value)
|
||||
return ok && item.declared && !item.opaque && (!item.c_layout || item.field_count > 0)
|
||||
}
|
||||
if value_kind == .Fallible {
|
||||
item, ok := node(store, value)
|
||||
if !ok || !is_runtime_value(item.extra, store, depth+1) {
|
||||
return false
|
||||
}
|
||||
return is_void(item.child) || is_runtime_value(item.child, store, depth+1)
|
||||
}
|
||||
if value_kind == .Distinct || value_kind == .Enum {
|
||||
item, ok := node(store, value)
|
||||
return ok && item.declared && is_runtime_value(item.child, store, depth+1)
|
||||
@@ -689,6 +914,59 @@ runtime_representation :: proc(value: Type, store: ^Store, depth := 0) -> Type {
|
||||
return runtime_representation(item.child, store, depth+1)
|
||||
}
|
||||
|
||||
sum_payload_size :: proc(value: Type, store: ^Store, selected := target.DEFAULT) -> u64 {
|
||||
if is_enum(value, store) {
|
||||
return 0
|
||||
}
|
||||
if !is_tagged_union(value, store) {
|
||||
return size(value, store, selected)
|
||||
}
|
||||
result: u64
|
||||
for field in fields_for(store, value) {
|
||||
result = max(result, size(field.type, store, selected))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
sum_payload_alignment :: proc(value: Type, store: ^Store, selected := target.DEFAULT) -> u64 {
|
||||
if is_enum(value, store) {
|
||||
return 1
|
||||
}
|
||||
if !is_tagged_union(value, store) {
|
||||
return u64(alignment_of(value, store, selected))
|
||||
}
|
||||
result: u64 = 1
|
||||
for field in fields_for(store, value) {
|
||||
result = max(result, u64(alignment_of(field.type, store, selected)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fallible_payload_offset :: proc(value: Type, store: ^Store, selected := target.DEFAULT) -> u64 {
|
||||
item, ok := node(store, value)
|
||||
if !ok || item.kind != .Fallible {
|
||||
return 0
|
||||
}
|
||||
payload_align := max(
|
||||
u64(1),
|
||||
max(
|
||||
u64(alignment_of(item.child, store, selected)) if !is_void(item.child) else u64(1),
|
||||
sum_payload_alignment(item.extra, store, selected),
|
||||
),
|
||||
)
|
||||
tag_size := size(U16, store, selected)
|
||||
return (tag_size+payload_align-1)/payload_align*payload_align
|
||||
}
|
||||
|
||||
fallible_payload_size :: proc(value: Type, store: ^Store, selected := target.DEFAULT) -> u64 {
|
||||
item, ok := node(store, value)
|
||||
if !ok || item.kind != .Fallible {
|
||||
return 0
|
||||
}
|
||||
success_size := u64(0) if is_void(item.child) else size(item.child, store, selected)
|
||||
return max(success_size, sum_payload_size(item.extra, store, selected))
|
||||
}
|
||||
|
||||
contains_c_struct_by_value :: proc(value: Type, store: ^Store, depth := 0) -> bool {
|
||||
if depth > 256 {
|
||||
return true
|
||||
@@ -717,6 +995,10 @@ contains_c_struct_by_value :: proc(value: Type, store: ^Store, depth := 0) -> bo
|
||||
if item.kind == .Array || item.kind == .Slice || item.kind == .Range || item.kind == .Optional {
|
||||
return contains_c_struct_by_value(item.child, store, depth+1)
|
||||
}
|
||||
if item.kind == .Fallible {
|
||||
return contains_c_struct_by_value(item.child, store, depth+1) ||
|
||||
contains_c_struct_by_value(item.extra, store, depth+1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -774,7 +1056,8 @@ contains_distinct_seen :: proc(value: Type, store: ^Store, seen: ^[dynamic]Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
return is_valid(item.child) && contains_distinct_seen(item.child, store, seen)
|
||||
return (is_valid(item.child) && contains_distinct_seen(item.child, store, seen)) ||
|
||||
(is_valid(item.extra) && contains_distinct_seen(item.extra, store, seen))
|
||||
}
|
||||
|
||||
is_c_integer_promotion_candidate :: proc(value: Type) -> bool {
|
||||
@@ -1110,6 +1393,11 @@ size :: proc(value: Type, store: ^Store, selected := target.DEFAULT) -> u64 {
|
||||
return (payload_offset+carrier_size+total_align-1)/total_align*total_align
|
||||
}
|
||||
return (carrier_size+max_align-1)/max_align*max_align
|
||||
case .Fallible:
|
||||
payload_offset := fallible_payload_offset(value, store, selected)
|
||||
payload_size := fallible_payload_size(value, store, selected)
|
||||
total_align := u64(alignment_of(value, store, selected))
|
||||
return (payload_offset+payload_size+total_align-1)/total_align*total_align
|
||||
case:
|
||||
return 0
|
||||
}
|
||||
@@ -1150,6 +1438,14 @@ alignment_of :: proc(value: Type, store: ^Store, selected := target.DEFAULT) ->
|
||||
result = max(result, alignment_of(item.child, store, selected))
|
||||
}
|
||||
return result
|
||||
case .Fallible:
|
||||
item, _ := node(store, value)
|
||||
result := alignment_of(U16, store, selected)
|
||||
if !is_void(item.child) {
|
||||
result = max(result, alignment_of(item.child, store, selected))
|
||||
}
|
||||
result = max(result, int(sum_payload_alignment(item.extra, store, selected)))
|
||||
return result
|
||||
case:
|
||||
return 1
|
||||
}
|
||||
@@ -1271,6 +1567,6 @@ name :: proc(value: Type) -> string {
|
||||
case C_DOUBLE: return "c_double"
|
||||
case C_LONGDOUBLE: return "c_longdouble"
|
||||
case:
|
||||
return fmt.tprintf("<type %d>", value)
|
||||
return "fallible" if kind(value) == .Fallible else fmt.tprintf("<type %d>", value)
|
||||
}
|
||||
}
|
||||
|
||||
+162
-19
@@ -90,7 +90,7 @@ main :: func() void { _ = value }
|
||||
compact_ids_reserve_invalid_values_and_preserve_layout :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, size_of(source.Span), 12)
|
||||
testing.expect_value(t, size_of(token.Token), 24)
|
||||
testing.expect(t, size_of(ast.Expr) <= 64)
|
||||
testing.expect(t, size_of(ast.Expr) <= 88)
|
||||
testing.expect(t, size_of(hir.Expr) <= 88)
|
||||
testing.expect(t, size_of(ir.Instruction) <= 88)
|
||||
testing.expect_value(t, size_of(types.Type), 4)
|
||||
@@ -2169,9 +2169,9 @@ tagged_union_compiles_and_runs :: proc(t: ^testing.T) {
|
||||
@(test)
|
||||
tagged_union_stores_the_discriminant :: proc(t: ^testing.T) {
|
||||
// The runtime test observes only the payload; this one checks the *tag* is written.
|
||||
// `Animal` is unbacked/dense (dog=0, cat=1, bird=2) in a u8 backing, so `Data{ bird = 99 }`
|
||||
// lays out as `{ i8, [3 x i8], i32 }` (tag at 0, i32 payload at offset 4) and writes the
|
||||
// discriminant `store i8 2` beside the payload `store i32 99`.
|
||||
// Native sum tags are global u16 IDs. `Animal` contributes dog/cat/bird (1..3),
|
||||
// then `Data` contributes dog:i32/bird:i32 (4..5), so `Data{ bird = 99 }`
|
||||
// writes discriminant `store i16 5` beside the payload.
|
||||
text := `Animal :: enum {
|
||||
dog
|
||||
cat
|
||||
@@ -2203,8 +2203,8 @@ main :: func() i32 {
|
||||
defer delete(llvm_text)
|
||||
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
testing.expect(t, strings.contains(llvm_text, "{ i8, [3 x i8], i32 }"))
|
||||
testing.expect(t, strings.contains(llvm_text, "store i8 2,"))
|
||||
testing.expect(t, strings.contains(llvm_text, "{ i16, [2 x i8], i32 }"))
|
||||
testing.expect(t, strings.contains(llvm_text, "store i16 5,"))
|
||||
testing.expect(t, strings.contains(llvm_text, "store i32 99,"))
|
||||
}
|
||||
|
||||
@@ -2251,6 +2251,151 @@ main :: func() i32 {
|
||||
testing.expect(t, found_variant)
|
||||
}
|
||||
|
||||
@(test)
|
||||
sum_composition_merges_widens_and_rejects_conflicts :: proc(t: ^testing.T) {
|
||||
text := `A :: enum {
|
||||
same
|
||||
left
|
||||
}
|
||||
B :: enum {
|
||||
same
|
||||
right
|
||||
}
|
||||
Both :: alias A | B
|
||||
UA :: union(enum) {
|
||||
item i32
|
||||
}
|
||||
UB :: union(enum) {
|
||||
empty void
|
||||
}
|
||||
UBoth :: alias UA | UB
|
||||
pick :: func(value Both) i32 {
|
||||
match value {
|
||||
.same: return 1
|
||||
.left: return 2
|
||||
.right: return 3
|
||||
}
|
||||
}
|
||||
payload :: func(value UBoth) i32 {
|
||||
match value {
|
||||
.item |n|: return n
|
||||
.empty: return 5
|
||||
}
|
||||
}
|
||||
main :: func() i32 {
|
||||
a A = .left
|
||||
b B = .right
|
||||
u UA = UA{ item = 4 }
|
||||
v UB = .empty
|
||||
return pick(.same) + pick(a) + pick(b) + payload(u) + payload(v)
|
||||
}
|
||||
`
|
||||
source_file := source.Source{path="test.bro", text=text}
|
||||
diagnostics := source.init_diagnostics(&source_file)
|
||||
defer source.destroy_diagnostics(&diagnostics)
|
||||
symbols := symbol.init_table()
|
||||
defer symbol.destroy_table(&symbols)
|
||||
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
||||
defer delete(stream.items)
|
||||
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
||||
defer ast.destroy_module(&ast_module)
|
||||
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
||||
defer hir.destroy_module(&hir_module)
|
||||
ir_module := lower.lower(&hir_module)
|
||||
defer ir.destroy_module(&ir_module)
|
||||
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
|
||||
defer delete(llvm_text)
|
||||
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
testing.expect(t, strings.contains(llvm_text, "call void @llvm.memcpy.p0.p0.i64"))
|
||||
|
||||
conflict := `A :: union(enum) {
|
||||
dup i32
|
||||
}
|
||||
B :: union(enum) {
|
||||
dup i64
|
||||
}
|
||||
Bad :: alias A | B
|
||||
main :: func() void {}
|
||||
`
|
||||
conflict_source := source.Source{path="conflict.bro", text=conflict}
|
||||
conflict_diagnostics := source.init_diagnostics(&conflict_source)
|
||||
defer source.destroy_diagnostics(&conflict_diagnostics)
|
||||
conflict_symbols := symbol.init_table()
|
||||
defer symbol.destroy_table(&conflict_symbols)
|
||||
conflict_stream := lexer.lex(&conflict_source, &conflict_diagnostics, &conflict_symbols)
|
||||
defer delete(conflict_stream.items)
|
||||
conflict_module := parser.parse(&conflict_stream, &conflict_source, &conflict_diagnostics)
|
||||
defer ast.destroy_module(&conflict_module)
|
||||
found_conflict := false
|
||||
for diagnostic in conflict_diagnostics.items {
|
||||
found_conflict = found_conflict || strings.contains(diagnostic.message, "same variant name")
|
||||
}
|
||||
testing.expect(t, found_conflict)
|
||||
|
||||
backed := `A :: enum(u8) {
|
||||
a = 1
|
||||
}
|
||||
B :: enum {
|
||||
b
|
||||
}
|
||||
Bad :: alias A | B
|
||||
main :: func() void {}
|
||||
`
|
||||
backed_source := source.Source{path="backed.bro", text=backed}
|
||||
backed_diagnostics := source.init_diagnostics(&backed_source)
|
||||
defer source.destroy_diagnostics(&backed_diagnostics)
|
||||
backed_symbols := symbol.init_table()
|
||||
defer symbol.destroy_table(&backed_symbols)
|
||||
backed_stream := lexer.lex(&backed_source, &backed_diagnostics, &backed_symbols)
|
||||
defer delete(backed_stream.items)
|
||||
backed_module := parser.parse(&backed_stream, &backed_source, &backed_diagnostics)
|
||||
defer ast.destroy_module(&backed_module)
|
||||
found_backed := false
|
||||
for diagnostic in backed_diagnostics.items {
|
||||
found_backed = found_backed || strings.contains(diagnostic.message, "native unbacked")
|
||||
}
|
||||
testing.expect(t, found_backed)
|
||||
}
|
||||
|
||||
@(test)
|
||||
yield_inference_visits_yielded_calls :: proc(t: ^testing.T) {
|
||||
text := `identity :: func(value int) int {
|
||||
return value
|
||||
}
|
||||
main :: func() i32 {
|
||||
value :: {
|
||||
yield identity(41)
|
||||
}
|
||||
return value - 41
|
||||
}
|
||||
`
|
||||
source_file := source.Source{path="yield_call.bro", text=text}
|
||||
diagnostics := source.init_diagnostics(&source_file)
|
||||
defer source.destroy_diagnostics(&diagnostics)
|
||||
symbols := symbol.init_table()
|
||||
defer symbol.destroy_table(&symbols)
|
||||
stream := lexer.lex(&source_file, &diagnostics, &symbols)
|
||||
defer delete(stream.items)
|
||||
ast_module := parser.parse(&stream, &source_file, &diagnostics)
|
||||
defer ast.destroy_module(&ast_module)
|
||||
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
|
||||
defer hir.destroy_module(&hir_module)
|
||||
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
testing.expect(t, len(hir_module.functions) > 1)
|
||||
}
|
||||
|
||||
@(test)
|
||||
errors_example_compiles_and_runs :: proc(t: ^testing.T) {
|
||||
output := "/tmp/brolang-test-errors"
|
||||
defer _ = os.remove(output)
|
||||
status := compiler_core.compile_package("examples/programs/errors", output)
|
||||
testing.expect_value(t, status, 0)
|
||||
state := run_executable(output)
|
||||
testing.expect_value(t, state.exit_code, 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
match_compiles_and_runs :: proc(t: ^testing.T) {
|
||||
output := "/tmp/brolang-test-match"
|
||||
@@ -2268,8 +2413,7 @@ match_compiles_and_runs :: proc(t: ^testing.T) {
|
||||
match_dispatches_on_the_tag :: proc(t: ^testing.T) {
|
||||
// A tagged-union match desugars to: read the discriminant once (a load of the tag
|
||||
// enum at the union's offset 0), then compare it against each variant's tag value.
|
||||
// `Animal` is dense in a u8 backing (dog=0, bird=2), so the dog arm compares the
|
||||
// loaded i8 tag against 0.
|
||||
// Data's runtime tag is the hidden global u16 variant ID.
|
||||
text := `Animal :: enum {
|
||||
dog
|
||||
cat
|
||||
@@ -2306,9 +2450,8 @@ main :: func() i32 {
|
||||
defer delete(llvm_text)
|
||||
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
// The tag is loaded as an i8 and compared (icmp eq) against the dog variant's tag (0).
|
||||
testing.expect(t, strings.contains(llvm_text, "load i8"))
|
||||
testing.expect(t, strings.contains(llvm_text, "icmp eq i8"))
|
||||
testing.expect(t, strings.contains(llvm_text, "load i16"))
|
||||
testing.expect(t, strings.contains(llvm_text, "icmp eq i16"))
|
||||
}
|
||||
|
||||
@(test)
|
||||
@@ -7038,21 +7181,21 @@ main :: func() i32 {
|
||||
testing.expect_value(t, len(diagnostics.items), 0)
|
||||
testing.expect(t, animal_ok && nat_ok)
|
||||
testing.expect(t, types.is_enum(animal, &ast_module.type_store))
|
||||
testing.expect_value(t, animal_node.child, types.U8)
|
||||
testing.expect_value(t, animal_node.child, types.U16)
|
||||
testing.expect(t, !animal_node.explicit_backing)
|
||||
testing.expect_value(t, nat_node.child, types.U16)
|
||||
testing.expect(t, nat_node.explicit_backing)
|
||||
testing.expect_value(t, len(animal_members), 3)
|
||||
testing.expect_value(t, animal_members[0].value, i128(0))
|
||||
testing.expect_value(t, animal_members[2].value, i128(2))
|
||||
testing.expect_value(t, animal_members[0].value, i128(1))
|
||||
testing.expect_value(t, animal_members[2].value, i128(3))
|
||||
testing.expect_value(t, nat_members[0].value, i128(1))
|
||||
testing.expect_value(t, nat_members[1].value, i128(2))
|
||||
testing.expect_value(t, nat_members[2].value, i128(5))
|
||||
testing.expect_value(t, types.runtime_representation(animal, &ast_module.type_store), types.U8)
|
||||
testing.expect_value(t, types.size(animal, &ast_module.type_store), u64(1))
|
||||
testing.expect_value(t, types.runtime_representation(animal, &ast_module.type_store), types.U16)
|
||||
testing.expect_value(t, types.size(animal, &ast_module.type_store), u64(2))
|
||||
testing.expect(t, hir_module.globals[0].is_static)
|
||||
testing.expect_value(t, hir_module.globals[0].static_value, i64(0))
|
||||
testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i8 0"))
|
||||
testing.expect_value(t, hir_module.globals[0].static_value, i64(1))
|
||||
testing.expect(t, strings.contains(llvm_text, "@bro.g.0 = internal constant i16 1"))
|
||||
found_promotion := false
|
||||
for function in ir_module.functions {
|
||||
for instruction in function.instructions {
|
||||
@@ -7063,7 +7206,7 @@ main :: func() i32 {
|
||||
}
|
||||
|
||||
@(test)
|
||||
unbacked_enum_selects_the_smallest_fitting_unsigned_backing :: proc(t: ^testing.T) {
|
||||
unbacked_enum_uses_global_u16_backing :: proc(t: ^testing.T) {
|
||||
builder := strings.builder_make()
|
||||
defer strings.builder_destroy(&builder)
|
||||
strings.write_string(&builder, "Large :: enum {\n")
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
BasicError :: enum {
|
||||
bad
|
||||
worse
|
||||
}
|
||||
|
||||
DetailError :: union(enum) {
|
||||
code i32
|
||||
empty void
|
||||
}
|
||||
|
||||
Left :: enum {
|
||||
left
|
||||
}
|
||||
|
||||
Right :: enum {
|
||||
right
|
||||
}
|
||||
|
||||
Both :: alias Left | Right
|
||||
|
||||
BoxA :: union(enum) {
|
||||
a i32
|
||||
}
|
||||
|
||||
BoxB :: union(enum) {
|
||||
b void
|
||||
}
|
||||
|
||||
Box :: alias BoxA | BoxB
|
||||
|
||||
maybe :: func(value i32) i32 ! BasicError {
|
||||
if (value == 0) return .bad
|
||||
return value + 1
|
||||
}
|
||||
|
||||
via_try :: func(value i32) i32 ! BasicError {
|
||||
unwrapped :: try maybe(value)
|
||||
return unwrapped + 1
|
||||
}
|
||||
|
||||
with_detail :: func(value i32) i32 ! DetailError {
|
||||
if (value == 0) return DetailError{ code = 5 }
|
||||
if (value == 1) return .empty
|
||||
return value
|
||||
}
|
||||
|
||||
pick :: func(value Both) i32 {
|
||||
match value {
|
||||
.left: return 10
|
||||
.right: return 20
|
||||
}
|
||||
}
|
||||
|
||||
payload :: func(value Box) i32 {
|
||||
match value {
|
||||
.a |n|: return n
|
||||
.b: return 3
|
||||
}
|
||||
}
|
||||
|
||||
main :: func() i32 {
|
||||
acc i32 = 0
|
||||
|
||||
a :: maybe(0) catch 7
|
||||
b :: maybe(4) catch 99
|
||||
c :: via_try(3) catch 99
|
||||
d :: via_try(0) catch 11
|
||||
e :: with_detail(0) catch 13
|
||||
f :: with_detail(2) catch 99
|
||||
|
||||
acc = acc + a + b + c + d + e + f
|
||||
acc = acc + pick(.left)
|
||||
r Right = .right
|
||||
acc = acc + pick(r)
|
||||
box BoxA = BoxA{ a = 8 }
|
||||
acc = acc + payload(box)
|
||||
empty BoxB = .b
|
||||
acc = acc + payload(empty)
|
||||
|
||||
# 7 + 5 + 5 + 11 + 13 + 2 + 10 + 20 + 8 + 3 = 84
|
||||
return acc - 84
|
||||
}
|
||||
Reference in New Issue
Block a user