From ae5af37b85965b080197d63ccf327baf149b427f Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Mon, 29 Jun 2026 22:55:56 +0200 Subject: [PATCH] error handling kickoff --- TODO.md | 193 ++++++++++--------- compiler/ast/ast.odin | 5 + compiler/checker/checker.odin | 269 +++++++++++++++++++++++--- compiler/hir/hir.odin | 3 + compiler/ir/ir.odin | 1 + compiler/lexer/lexer.odin | 2 + compiler/llvm/llvm.odin | 121 +++++++++++- compiler/loader/loader.odin | 17 +- compiler/lower/lower.odin | 104 +++++++++- compiler/parser/parser.odin | 120 ++++++++---- compiler/token/token.odin | 2 + compiler/types/types.odin | 302 +++++++++++++++++++++++++++++- compiler_tests.odin | 181 ++++++++++++++++-- examples/programs/errors/main.bro | 82 ++++++++ 14 files changed, 1229 insertions(+), 173 deletions(-) create mode 100644 examples/programs/errors/main.bro diff --git a/TODO.md b/TODO.md index 032901c..9048c90 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 9460f1b..135c028 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -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) diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 8bfb0c5..db0f03e 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -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) diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 2d5e988..77c9961 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -95,7 +95,10 @@ Expr_Kind :: enum u8 { Slice_Ptr, Unwrap, Orelse, + Try, + Catch, Widen, + Sum_Widen, C_Coerce, C_Vararg_Promote, Retype, diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index aee872e..428bf8f 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -94,6 +94,7 @@ Opcode :: enum u8 { Orelse_Begin, Orelse, Widen, + Sum_Widen, C_Coerce, C_Vararg_Promote, Retype, diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index bbe1aa3..a4c8362 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -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 diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index da1601a..a9bac36 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -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 } diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index 7b86439..bf165bf 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -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( diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index f386705..9157149 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -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 diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index aa99e35..2ab30d4 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -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)) diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 482a895..78b73d5 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -59,6 +59,8 @@ Kind :: enum u8 { Keyword_Alias, Keyword_Import, Keyword_Return, + Keyword_Try, + Keyword_Catch, Keyword_Mut, Keyword_None, Keyword_Undefined, diff --git a/compiler/types/types.odin b/compiler/types/types.odin index a9cc752..6533393 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -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("", value) + return "fallible" if kind(value) == .Fallible else fmt.tprintf("", value) } } diff --git a/compiler_tests.odin b/compiler_tests.odin index 40357a8..7ebfefd 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -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") diff --git a/examples/programs/errors/main.bro b/examples/programs/errors/main.bro new file mode 100644 index 0000000..7e36e18 --- /dev/null +++ b/examples/programs/errors/main.bro @@ -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 +}