error handling kickoff

This commit is contained in:
2026-06-29 22:55:56 +02:00
parent 98c303d22c
commit ae5af37b85
14 changed files with 1229 additions and 173 deletions
+105 -88
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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)
+3
View File
@@ -95,7 +95,10 @@ Expr_Kind :: enum u8 {
Slice_Ptr,
Unwrap,
Orelse,
Try,
Catch,
Widen,
Sum_Widen,
C_Coerce,
C_Vararg_Promote,
Retype,
+1
View File
@@ -94,6 +94,7 @@ Opcode :: enum u8 {
Orelse_Begin,
Orelse,
Widen,
Sum_Widen,
C_Coerce,
C_Vararg_Promote,
Retype,
+2
View File
@@ -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
View File
@@ -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
}
+16 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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))
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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")
+82
View File
@@ -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
}