diff --git a/TODO.md b/TODO.md index 7826bef..62f95e0 100644 --- a/TODO.md +++ b/TODO.md @@ -584,12 +584,18 @@ - focused coverage lives in `examples/programs/errors` and the fallible ergonomics compiler test - 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.6. contextual payload construction + inline error types (implemented) + - contextual payload `.variant{expr}` construction now works in tagged-union contexts + (assignment, return, calls, and fallible error dispatch); bare `.variant` remains the + spelling for void-payload variants + - named fallible signatures can use inline unbacked enum and `union(enum)` error types + after `!` + - qualified same-name disambiguation remains deferred; existing ambiguous fallible return + diagnostics cover the current surface -23.7. sum-type ABI/layout polish +23.7. anonymous struct payloads and keyed payload sugar + +23.8. 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 @@ -976,7 +982,7 @@ message :: match code { 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`. Milestone 23.5 adds `catch |e|` blocks and `try` widening across composable error channels. It still defers the `error` keyword shorthand, inline error types, contextual payload construction, and match-on-error shorthand. +Milestone 23 v1 implements named error channels, native sum composition, `return`-based error dispatch, exact-channel `try`, and fallback `catch`. Milestone 23.5 adds `catch |e|` blocks and `try` widening across composable error channels. Milestone 23.6 adds contextual payload construction and inline error types. It still defers the `error` keyword shorthand, anonymous struct payload sugar, and match-on-error shorthand. ### Fallible Functions @@ -1048,7 +1054,25 @@ ProcessError :: alias (IoError | ParseError) ### Inline Error Types -Inline error types are planned, but not part of milestone 23 v1. Use named enums, named tagged unions, or named aliases for now. +Fallible signatures can define small private error channels inline: + +``` +read_count func(path []u8) i32 ! union(enum) { + not_found PathErrorInfo + timeout_ms u64 +} { ... } +``` + +Inline enums are also supported: + +``` +parse_flag func(text []u8) bool ! enum { + empty + invalid +} { ... } +``` + +Use a named error type when the channel is shared or needs stable public identity. ### Returning Errors @@ -1061,7 +1085,7 @@ parse_section func(p: @mut Parser) void ! ParseError { # ... parsing logic ... - if p.pos >= p.input.len or p.input[p.pos] != ']' return ParseError{ unclosed_section = start_line } + if p.pos >= p.input.len or p.input[p.pos] != ']' return .unclosed_section{start_line} # ... continue on success ... } @@ -1071,7 +1095,7 @@ Since errors are just union values, you can also construct them separately: ``` # Construct error value (it's just a union) -e ParseError = ParseError{ timeout = 500 } +e ParseError = .timeout{500} # Return it via error channel later return e @@ -1157,6 +1181,7 @@ data :: read_file(path) catch |e| match e { | `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 | +| `.variant{payload}` | Construct a payload-carrying tagged-union variant from context | | `error .variant{...}` | Planned shorthand, not v1 | | `try expr` | Unwrap success or propagate an exact/sum-widenable error channel | | `expr catch fallback` | Provide fallback value on error | diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 17c5610..0947816 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -758,7 +758,7 @@ 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, .Try, .Keyed: + case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal: append(&stack, expr.left) case .Catch: append(&stack, expr.left) @@ -768,7 +768,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as 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: + case .Invalid, .Integer, .Float, .String, .Bool, .None, .Undefined, .Name: } } } @@ -1331,6 +1331,9 @@ infer_compound_expr :: proc( case .Undefined: return types.INVALID case .Enum_Literal: + if expr.left != ast.INVALID_EXPR { + _ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) + } return types.INVALID case .Address: child := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) @@ -3256,29 +3259,45 @@ build_compound_expr :: proc( ) return invalid_hir_expr(checker, expr.span, id, expected) case .Enum_Literal: - // A bare enum literal in a tagged-union context constructs a variant. Only a - // void-payload variant can be built this way (it has no value); a payload variant - // must use `T{ variant = ... }`. (`.variant{...}` payload construction is the - // milestone-23 error-channel form.) if types.is_tagged_union(expected, store) { index, field, found := find_struct_field(checker, expected, expr.name) if !found { id := source.addf(checker.diagnostics, expr.span, "unknown variant '.%s' on '%s'", symbol_text(checker, expr.name), type_label(checker, expected)) return invalid_hir_expr(checker, expr.span, id, expected) } - if !types.is_void(field.type) { - id := source.addf(checker.diagnostics, expr.span, "variant '.%s' on '%s' needs a payload; only void variants can be built from a bare '.%s'", - symbol_text(checker, expr.name), type_label(checker, expected), symbol_text(checker, expr.name)) - return invalid_hir_expr(checker, expr.span, id, expected) - } values := make([]hir.Expr_Id, 1, checker.allocator) - values[0] = hir.INVALID_EXPR + if expr.left == ast.INVALID_EXPR { + if !types.is_void(field.type) { + id := source.addf(checker.diagnostics, expr.span, "variant '.%s' on '%s' needs a payload; only void variants can be built from a bare '.%s'", + symbol_text(checker, expr.name), type_label(checker, expected), symbol_text(checker, expr.name)) + delete(values, checker.allocator) + return invalid_hir_expr(checker, expr.span, id, expected) + } + values[0] = hir.INVALID_EXPR + } else { + if types.is_void(field.type) { + id := source.addf(checker.diagnostics, expr.span, "void variant '%s' takes no value", symbol_text(checker, expr.name)) + delete(values, checker.allocator) + return invalid_hir_expr(checker, expr.span, id, expected) + } + values[0] = build_nested_expr(checker, expr.left, locals, global_reads, calls, field.type, pkg, file) + values[0] = coerce_expr(checker, values[0], field.type, checker.module.exprs[values[0]].span) + } return add_hir_expr(checker, hir.Expr{ kind=.Struct, span=expr.span, type=expected, args=values, integer=i64(index), target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) } + if expr.left != ast.INVALID_EXPR { + id := source.addf( + checker.diagnostics, + expr.span, + "'.%s{...}' requires a tagged-union context", + symbol_text(checker, expr.name), + ) + return invalid_hir_expr(checker, expr.span, id, expected) + } if !types.is_enum(expected, store) { id := source.addf( checker.diagnostics, diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 7e579e6..c1ed4c2 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -366,23 +366,40 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax { return types.INVALID } -parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax { - left := parse_type_atom(parser) +parse_type_pipe_tail :: proc(parser: ^Parser, left: ast.Type_Syntax) -> ast.Type_Syntax { + result := left 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) + composed, compose_error := types.compose_sum(&parser.module.type_store, result, 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 + result = 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 + result = types.INVALID } else { - left = composed + result = composed } } - return left + return result +} + +parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax { + return parse_type_pipe_tail(parser, parse_type_atom(parser)) +} + +parse_error_type :: proc(parser: ^Parser) -> ast.Type_Syntax { + left := types.INVALID + #partial switch current(parser).kind { + case .Keyword_Enum: + left = parse_inline_enum_type(parser) + case .Keyword_Union: + left = parse_inline_union_type(parser) + case: + left = parse_type_atom(parser) + } + return parse_type_pipe_tail(parser, left) } skip_parenthesized :: proc(parser: ^Parser) -> source.Span { @@ -650,11 +667,36 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id { return invalid_expr(parser, member.span, "expected an enum member after '.'") } advance(parser) + payload := ast.INVALID_EXPR + end := member.span + if _, ok := allow(parser, .Left_Brace); ok { + parser.delimiter_depth += 1 + skip_newlines(parser) + if current(parser).kind == .Right_Brace { + source.add(parser.diagnostics, current(parser).span, "contextual variant payload requires exactly one expression") + } else { + payload = parse_expression_bp(parser, 0, nesting+1) + skip_newlines(parser) + if _, comma_ok := allow(parser, .Comma); comma_ok { + source.add(parser.diagnostics, current(parser).span, "contextual variant payload requires exactly one expression") + for current(parser).kind != .Right_Brace && current(parser).kind != .Eof { + advance(parser) + } + } + } + right_brace, close_ok := allow(parser, .Right_Brace) + if !close_ok { + source.add(parser.diagnostics, current(parser).span, "expected '}' after contextual variant payload") + right_brace = member + } + parser.delimiter_depth -= 1 + end = right_brace.span + } return add_expr(parser, ast.Expr{ kind=.Enum_Literal, - span=span_from(start.span, member.span), + span=span_from(start.span, end), name=member.symbol, - left=ast.INVALID_EXPR, + left=payload, right=ast.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) @@ -1871,7 +1913,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { result := parse_type(parser) error_type := types.INVALID if _, ok := allow(parser, .Bang); ok { - error_type = parse_type(parser) + error_type = parse_error_type(parser) } end := previous(parser) ended_by_newline := current(parser).kind == .Newline @@ -1917,6 +1959,67 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { }) } +parse_record_body :: proc(parser: ^Parser, fields: ^[dynamic]types.Field, expected_open: string) -> bool { + if _, ok := allow(parser, .Left_Brace); !ok { + source.add(parser.diagnostics, current(parser).span, expected_open) + return false + } + skip_newlines(parser) + for current(parser).kind != .Right_Brace && current(parser).kind != .Eof { + if current(parser).kind != .Identifier { + source.add(parser.diagnostics, current(parser).span, "expected a struct field name") + for current(parser).kind != .Newline && + current(parser).kind != .Right_Brace && + current(parser).kind != .Eof { + advance(parser) + } + skip_newlines(parser) + continue + } + field_name := advance(parser) + field_type := parse_type(parser) + append(fields, types.Field{name=u32(field_name.symbol), type=field_type}) + if _, ok := allow(parser, .Comma); ok { + skip_newlines(parser) + continue + } + _ = finish_statement(parser, true) + } + if _, ok := allow(parser, .Right_Brace); !ok { + source.add(parser.diagnostics, current(parser).span, "expected '}' after struct fields") + } + return true +} + +parse_inline_union_type :: proc(parser: ^Parser) -> types.Type { + advance(parser) + valid := true + if _, ok := allow(parser, .Left_Paren); !ok { + source.add(parser.diagnostics, current(parser).span, "expected '(enum)' after inline union error type") + valid = false + } else { + if _, enum_ok := allow(parser, .Keyword_Enum); !enum_ok { + source.add(parser.diagnostics, current(parser).span, "inline union error types must use 'union(enum)'") + if current(parser).kind != .Right_Paren { + _ = parse_type(parser) + } + valid = false + } + if _, close_ok := allow(parser, .Right_Paren); !close_ok { + source.add(parser.diagnostics, current(parser).span, "expected ')' after union tag") + valid = false + } + } + fields: [dynamic]types.Field + fields.allocator = parser.module.allocator + defer delete(fields) + if !parse_record_body(parser, &fields, "expected '{' after inline union error type") || !valid { + return types.INVALID + } + tag := synthesize_union_tag(parser, fields[:]) + return types.union_anonymous(&parser.module.type_store, fields[:], tag) +} + parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_union := false) { start := advance(parser) id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol)) @@ -1954,34 +2057,10 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_unio } return } - advance(parser) fields: [dynamic]types.Field fields.allocator = parser.module.allocator defer delete(fields) - skip_newlines(parser) - for current(parser).kind != .Right_Brace && current(parser).kind != .Eof { - if current(parser).kind != .Identifier { - source.add(parser.diagnostics, current(parser).span, "expected a struct field name") - for current(parser).kind != .Newline && - current(parser).kind != .Right_Brace && - current(parser).kind != .Eof { - advance(parser) - } - skip_newlines(parser) - continue - } - field_name := advance(parser) - field_type := parse_type(parser) - append(&fields, types.Field{name=u32(field_name.symbol), type=field_type}) - if _, ok := allow(parser, .Comma); ok { - skip_newlines(parser) - continue - } - _ = finish_statement(parser, true) - } - if _, ok := allow(parser, .Right_Brace); !ok { - source.add(parser.diagnostics, current(parser).span, "expected '}' after struct fields") - } + _ = parse_record_body(parser, &fields, "expected '{' after struct fields") if is_union && (inferred_tag || types.is_valid(declared_tag)) { tag = synthesize_union_tag(parser, fields[:]) } @@ -2033,26 +2112,19 @@ parse_alias :: proc(parser: ^Parser, name: token.Token) { _ = finish_statement(parser) } -parse_enum :: proc(parser: ^Parser, name: token.Token) { - start := advance(parser) - explicit_backing := false - backing := types.INVALID - if _, ok := allow(parser, .Left_Paren); ok { - explicit_backing = true - backing = parse_type(parser) - if _, close_ok := allow(parser, .Right_Paren); !close_ok { - source.add(parser.diagnostics, current(parser).span, "expected ')' after enum backing type") - } - } +parse_enum_body :: proc( + parser: ^Parser, + start: source.Span, + explicit_backing: bool, + backing: ^types.Type, + members: ^[dynamic]types.Enum_Member, + expected_open: string, +) -> bool { skip_newlines(parser) if _, ok := allow(parser, .Left_Brace); !ok { - source.add(parser.diagnostics, current(parser).span, "expected '{' after enum declaration") - _ = finish_statement(parser) - return + source.add(parser.diagnostics, current(parser).span, expected_open) + return false } - members: [dynamic]types.Enum_Member - members.allocator = parser.module.allocator - defer delete(members) next_value: i128 previous_value: i128 has_previous := false @@ -2070,7 +2142,7 @@ parse_enum :: proc(parser: ^Parser, name: token.Token) { } member := advance(parser) duplicate := false - for existing in members { + for existing in members^ { if existing.name == u32(member.symbol) { duplicate = true break @@ -2108,7 +2180,7 @@ parse_enum :: proc(parser: ^Parser, name: token.Token) { source.add(parser.diagnostics, member.span, "enum values must be strictly increasing") } if !duplicate { - append(&members, types.Enum_Member{name=u32(member.symbol), value=value}) + append(members, types.Enum_Member{name=u32(member.symbol), value=value}) } previous_value = value has_previous = true @@ -2122,19 +2194,61 @@ parse_enum :: proc(parser: ^Parser, name: token.Token) { if _, ok := allow(parser, .Right_Brace); !ok { source.add(parser.diagnostics, current(parser).span, "expected '}' after enum members") } - if len(members) == 0 { - source.add(parser.diagnostics, start.span, "enum declarations require at least one member") + if len(members^) == 0 { + source.add(parser.diagnostics, start, "enum declarations require at least one member") } if !explicit_backing { - backing = types.U16 - for &member in members { + 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") + source.add(parser.diagnostics, start, "too many global sum variants for u16 tags") } member.value = i128(id) } } + return true +} + +parse_inline_enum_type :: proc(parser: ^Parser) -> types.Type { + start := advance(parser) + valid := true + if _, ok := allow(parser, .Left_Paren); ok { + source.add(parser.diagnostics, start.span, "inline enum error types cannot declare a backing type") + _ = parse_type(parser) + if _, close_ok := allow(parser, .Right_Paren); !close_ok { + source.add(parser.diagnostics, current(parser).span, "expected ')' after enum backing type") + } + valid = false + } + backing := types.INVALID + members: [dynamic]types.Enum_Member + members.allocator = parser.module.allocator + defer delete(members) + if !parse_enum_body(parser, start.span, false, &backing, &members, "expected '{' after inline enum error type") || !valid { + return types.INVALID + } + return types.enum_anonymous(&parser.module.type_store, members[:], backing) +} + +parse_enum :: proc(parser: ^Parser, name: token.Token) { + start := advance(parser) + explicit_backing := false + backing := types.INVALID + if _, ok := allow(parser, .Left_Paren); ok { + explicit_backing = true + backing = parse_type(parser) + if _, close_ok := allow(parser, .Right_Paren); !close_ok { + source.add(parser.diagnostics, current(parser).span, "expected ')' after enum backing type") + } + } + members: [dynamic]types.Enum_Member + members.allocator = parser.module.allocator + defer delete(members) + if !parse_enum_body(parser, start.span, explicit_backing, &backing, &members, "expected '{' after enum declaration") { + _ = finish_statement(parser) + return + } id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol)) if !types.define_enum(&parser.module.type_store, id, backing, members[:], explicit_backing) { source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name)) diff --git a/compiler_tests.odin b/compiler_tests.odin index 0c28564..f06a1e6 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2514,6 +2514,181 @@ main func() i32 { testing.expect_value(t, compiler_core.compile_package(directory, output), 1) } +@(test) +contextual_payload_variants_compile :: proc(t: ^testing.T) { + text := `DetailError :: union(enum) { + code i32 + empty void +} +make_payload func() i32 { + return 7 +} +accept func(value DetailError) i32 { + match value { + .code |n|: return n + .empty: return 0 + } +} +make_code func(value i32) DetailError { + return .code{value} +} +with_detail func(value i32) i32 ! DetailError { + if (value == 0) return .code{5} + if (value == 1) return .empty + return value +} +main func() i32 { + e DetailError = .code{3} + recovered :: with_detail(0) catch |err| { + match err { + .code |n|: yield n + .empty: yield 9 + } + } + return accept(e) + accept(.code{4}) + accept(make_code(5)) + accept(.code{make_payload()}) + recovered - 24 +} +` + source_file := source.Source{path="contextual_payload.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")) +} + +@(test) +inline_fallible_error_types_compile :: proc(t: ^testing.T) { + text := `inline_enum func(value i32) i32 ! enum { + inline_bad + inline_worse +} { + if (value == 0) return .inline_bad + if (value < 0) return .inline_worse + return value +} +inline_union func(value i32) i32 ! union(enum) { + inline_code i32 + inline_empty void +} { + if (value == 0) return .inline_code{6} + if (value == 1) return .inline_empty + return value +} +main func() i32 { + a :: inline_enum(0) catch |e| { + if (e == .inline_bad) { + yield 10 + } else { + yield 11 + } + } + b :: inline_union(0) catch |e| { + match e { + .inline_code |n|: yield n + .inline_empty: yield 12 + } + } + return a + b - 16 +} +` + source_file := source.Source{path="inline_errors.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, len(hir_module.functions) > 1) + testing.expect(t, len(llvm_text) > 0) +} + +@(test) +contextual_payload_variants_reject_bad_forms :: proc(t: ^testing.T) { + text := `DetailError :: union(enum) { + code i32 + empty void +} +missing_payload func() DetailError { + return .code +} +void_payload func() DetailError { + return .empty{1} +} +empty_payload func() DetailError { + return .code{} +} +multi_payload func() DetailError { + return .code{1, 2} +} +unknown_payload func() DetailError { + return .missing{1} +} +no_context func() i32 { + _ = .code{1} + return 0 +} +main func() i32 { + _ = missing_payload() + _ = void_payload() + _ = empty_payload() + _ = multi_payload() + _ = unknown_payload() + return no_context() +} +` + source_file := source.Source{path="bad_contextual_payload.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) + + found_missing := false + found_void := false + payload_arity_errors := 0 + found_unknown := false + found_no_context := false + for diagnostic in diagnostics.items { + found_missing = found_missing || strings.contains(diagnostic.message, "needs a payload") + found_void = found_void || strings.contains(diagnostic.message, "void variant 'empty' takes no value") + payload_arity_errors += 1 if strings.contains(diagnostic.message, "requires exactly one expression") else 0 + found_unknown = found_unknown || strings.contains(diagnostic.message, "unknown variant '.missing'") + found_no_context = found_no_context || strings.contains(diagnostic.message, "requires a tagged-union context") + } + testing.expect(t, found_missing) + testing.expect(t, found_void) + testing.expect(t, payload_arity_errors >= 2) + testing.expect(t, found_unknown) + testing.expect(t, found_no_context) +} + @(test) errors_example_compiles_and_runs :: proc(t: ^testing.T) { output := "/tmp/brolang-test-errors" diff --git a/examples/programs/errors/main.bro b/examples/programs/errors/main.bro index 3da2fcf..7c55305 100644 --- a/examples/programs/errors/main.bro +++ b/examples/programs/errors/main.bro @@ -42,7 +42,7 @@ via_try func(value i32) i32 ! BasicError { } with_detail func(value i32) i32 ! DetailError { - if (value == 0) return DetailError{ code = 5 } + if (value == 0) return .code{5} if (value == 1) return .empty return value } @@ -96,6 +96,24 @@ payload func(value Box) i32 { } } +inline_enum func(value i32) i32 ! enum { + inline_bad + inline_worse +} { + if (value == 0) return .inline_bad + if (value < 0) return .inline_worse + return value +} + +inline_detail func(value i32) i32 ! union(enum) { + inline_code i32 + inline_empty void +} { + if (value == 0) return .inline_code{6} + if (value == 1) return .inline_empty + return value +} + main func() i32 { acc i32 = 0 @@ -112,16 +130,30 @@ main func() i32 { k :: via_widen(3) catch 99 l :: catch_widen(0) m :: catch_widen(-1) + n :: inline_enum(0) catch |e| { + if (e == .inline_bad) { + yield 60 + } else { + yield 61 + } + } + o :: inline_detail(0) catch |e| { + match e { + .inline_code |value|: yield value + 70 + .inline_empty: yield 80 + } + } - acc = acc + a + b + c + d + e + f + g + h + i + j + k + l + m + acc = acc + a + b + c + d + e + f + g + h + i + j + k + l + m + n + o acc = acc + pick(.left) r Right = .right acc = acc + pick(r) - box BoxA = BoxA{ a = 8 } + box BoxA = .a{8} acc = acc + payload(box) empty BoxB = .b acc = acc + payload(empty) + acc = acc + payload(.a{9}) - # 7 + 5 + 5 + 11 + 13 + 2 + 21 + 22 + 35 + 40 + 6 + 50 + 51 + 10 + 20 + 8 + 3 = 309 - return acc - 309 + # 7 + 5 + 5 + 11 + 13 + 2 + 21 + 22 + 35 + 40 + 6 + 50 + 51 + 60 + 76 + 10 + 20 + 8 + 3 + 9 = 454 + return acc - 454 }