contextual payload construction

This commit is contained in:
2026-06-30 21:14:59 +02:00
parent 7ca7e33033
commit 5161e99af5
5 changed files with 450 additions and 85 deletions
+34 -9
View File
@@ -584,12 +584,18 @@
- focused coverage lives in `examples/programs/errors` and the fallible ergonomics compiler test - focused coverage lives in `examples/programs/errors` and the fallible ergonomics compiler test
- leave ABI/layout/lint/design polish for later milestones - leave ABI/layout/lint/design polish for later milestones
23.6. contextual payload construction + inline error types 23.6. contextual payload construction + inline error types (implemented)
- implement contextual payload `.variant{...}` construction - contextual payload `.variant{expr}` construction now works in tagged-union contexts
- allow named fallible signatures to use inline enum / tagged-union error types (assignment, return, calls, and fallible error dispatch); bare `.variant` remains the
- settle qualified same-name disambiguation only if contextual construction needs it 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 dynamic tag-width shrinking after the fixed-`u16` ABI has real pressure
- consider all-void channel collapse after fallible channels are otherwise stable - 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 - 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. 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 ### Fallible Functions
@@ -1048,7 +1054,25 @@ ProcessError :: alias (IoError | ParseError)
### Inline Error Types ### 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 ### Returning Errors
@@ -1061,7 +1085,7 @@ parse_section func(p: @mut Parser) void ! ParseError {
# ... parsing logic ... # ... 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 ... # ... 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) # Construct error value (it's just a union)
e ParseError = ParseError{ timeout = 500 } e ParseError = .timeout{500}
# Return it via error channel later # Return it via error channel later
return e 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 | | `T ! (E1 | E2)` | Planned direct spelling with parentheses; use a named alias in v1 |
| `return e` | Exit function via error channel when `e : E` | | `return e` | Exit function via error channel when `e : E` |
| `error e` | Planned shorthand, not v1 | | `error e` | Planned shorthand, not v1 |
| `.variant{payload}` | Construct a payload-carrying tagged-union variant from context |
| `error .variant{...}` | Planned shorthand, not v1 | | `error .variant{...}` | Planned shorthand, not v1 |
| `try expr` | Unwrap success or propagate an exact/sum-widenable error channel | | `try expr` | Unwrap success or propagate an exact/sum-widenable error channel |
| `expr catch fallback` | Provide fallback value on error | | `expr catch fallback` | Provide fallback value on error |
+31 -12
View File
@@ -758,7 +758,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as
if expr.left != ast.INVALID_EXPR { if expr.left != ast.INVALID_EXPR {
append(&stack, expr.left) 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) append(&stack, expr.left)
case .Catch: case .Catch:
append(&stack, expr.left) 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) mark_block_imports_used(checker, expr.body, file)
case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
append(&stack, expr.left, expr.right) 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: case .Undefined:
return types.INVALID return types.INVALID
case .Enum_Literal: case .Enum_Literal:
if expr.left != ast.INVALID_EXPR {
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
}
return types.INVALID return types.INVALID
case .Address: case .Address:
child := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types) 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) return invalid_hir_expr(checker, expr.span, id, expected)
case .Enum_Literal: 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) { if types.is_tagged_union(expected, store) {
index, field, found := find_struct_field(checker, expected, expr.name) index, field, found := find_struct_field(checker, expected, expr.name)
if !found { if !found {
id := source.addf(checker.diagnostics, expr.span, "unknown variant '.%s' on '%s'", symbol_text(checker, expr.name), type_label(checker, expected)) 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) 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 := 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{ return add_hir_expr(checker, hir.Expr{
kind=.Struct, span=expr.span, type=expected, args=values, integer=i64(index), kind=.Struct, span=expr.span, type=expected, args=values, integer=i64(index),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC, 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) { if !types.is_enum(expected, store) {
id := source.addf( id := source.addf(
checker.diagnostics, checker.diagnostics,
+173 -59
View File
@@ -366,23 +366,40 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
return types.INVALID return types.INVALID
} }
parse_type :: proc(parser: ^Parser) -> ast.Type_Syntax { parse_type_pipe_tail :: proc(parser: ^Parser, left: ast.Type_Syntax) -> ast.Type_Syntax {
left := parse_type_atom(parser) result := left
for current(parser).kind == .Pipe { for current(parser).kind == .Pipe {
operator := advance(parser) operator := advance(parser)
right := parse_type_atom(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 { if compose_error == .Unsupported {
source.add(parser.diagnostics, operator.span, "only native unbacked enums and tagged unions can be composed with '|'") 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 { } else if compose_error == .Conflict {
source.add(parser.diagnostics, operator.span, "sum composition contains the same variant name with different payload types") source.add(parser.diagnostics, operator.span, "sum composition contains the same variant name with different payload types")
left = types.INVALID result = types.INVALID
} else { } 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 { 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 '.'") return invalid_expr(parser, member.span, "expected an enum member after '.'")
} }
advance(parser) 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{ return add_expr(parser, ast.Expr{
kind=.Enum_Literal, kind=.Enum_Literal,
span=span_from(start.span, member.span), span=span_from(start.span, end),
name=member.symbol, name=member.symbol,
left=ast.INVALID_EXPR, left=payload,
right=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
@@ -1871,7 +1913,7 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
result := parse_type(parser) result := parse_type(parser)
error_type := types.INVALID error_type := types.INVALID
if _, ok := allow(parser, .Bang); ok { if _, ok := allow(parser, .Bang); ok {
error_type = parse_type(parser) error_type = parse_error_type(parser)
} }
end := previous(parser) end := previous(parser)
ended_by_newline := current(parser).kind == .Newline 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) { parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_union := false) {
start := advance(parser) start := advance(parser)
id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol)) 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 return
} }
advance(parser)
fields: [dynamic]types.Field fields: [dynamic]types.Field
fields.allocator = parser.module.allocator fields.allocator = parser.module.allocator
defer delete(fields) defer delete(fields)
skip_newlines(parser) _ = parse_record_body(parser, &fields, "expected '{' after struct fields")
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")
}
if is_union && (inferred_tag || types.is_valid(declared_tag)) { if is_union && (inferred_tag || types.is_valid(declared_tag)) {
tag = synthesize_union_tag(parser, fields[:]) tag = synthesize_union_tag(parser, fields[:])
} }
@@ -2033,26 +2112,19 @@ parse_alias :: proc(parser: ^Parser, name: token.Token) {
_ = finish_statement(parser) _ = finish_statement(parser)
} }
parse_enum :: proc(parser: ^Parser, name: token.Token) { parse_enum_body :: proc(
start := advance(parser) parser: ^Parser,
explicit_backing := false start: source.Span,
backing := types.INVALID explicit_backing: bool,
if _, ok := allow(parser, .Left_Paren); ok { backing: ^types.Type,
explicit_backing = true members: ^[dynamic]types.Enum_Member,
backing = parse_type(parser) expected_open: string,
if _, close_ok := allow(parser, .Right_Paren); !close_ok { ) -> bool {
source.add(parser.diagnostics, current(parser).span, "expected ')' after enum backing type")
}
}
skip_newlines(parser) skip_newlines(parser)
if _, ok := allow(parser, .Left_Brace); !ok { if _, ok := allow(parser, .Left_Brace); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '{' after enum declaration") source.add(parser.diagnostics, current(parser).span, expected_open)
_ = finish_statement(parser) return false
return
} }
members: [dynamic]types.Enum_Member
members.allocator = parser.module.allocator
defer delete(members)
next_value: i128 next_value: i128
previous_value: i128 previous_value: i128
has_previous := false has_previous := false
@@ -2070,7 +2142,7 @@ parse_enum :: proc(parser: ^Parser, name: token.Token) {
} }
member := advance(parser) member := advance(parser)
duplicate := false duplicate := false
for existing in members { for existing in members^ {
if existing.name == u32(member.symbol) { if existing.name == u32(member.symbol) {
duplicate = true duplicate = true
break 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") source.add(parser.diagnostics, member.span, "enum values must be strictly increasing")
} }
if !duplicate { 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 previous_value = value
has_previous = true has_previous = true
@@ -2122,19 +2194,61 @@ parse_enum :: proc(parser: ^Parser, name: token.Token) {
if _, ok := allow(parser, .Right_Brace); !ok { if _, ok := allow(parser, .Right_Brace); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '}' after enum members") source.add(parser.diagnostics, current(parser).span, "expected '}' after enum members")
} }
if len(members) == 0 { if len(members^) == 0 {
source.add(parser.diagnostics, start.span, "enum declarations require at least one member") source.add(parser.diagnostics, start, "enum declarations require at least one member")
} }
if !explicit_backing { if !explicit_backing {
backing = types.U16 backing^ = types.U16
for &member in members { for &member in members^ {
id, ok := types.variant_id(&parser.module.type_store, member.name, types.VOID) id, ok := types.variant_id(&parser.module.type_store, member.name, types.VOID)
if !ok { 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) 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)) 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) { 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)) source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name))
+175
View File
@@ -2514,6 +2514,181 @@ main func() i32 {
testing.expect_value(t, compiler_core.compile_package(directory, output), 1) 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) @(test)
errors_example_compiles_and_runs :: proc(t: ^testing.T) { errors_example_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-errors" output := "/tmp/brolang-test-errors"
+37 -5
View File
@@ -42,7 +42,7 @@ via_try func(value i32) i32 ! BasicError {
} }
with_detail func(value i32) i32 ! DetailError { 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 if (value == 1) return .empty
return value 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 { main func() i32 {
acc i32 = 0 acc i32 = 0
@@ -112,16 +130,30 @@ main func() i32 {
k :: via_widen(3) catch 99 k :: via_widen(3) catch 99
l :: catch_widen(0) l :: catch_widen(0)
m :: catch_widen(-1) 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) acc = acc + pick(.left)
r Right = .right r Right = .right
acc = acc + pick(r) acc = acc + pick(r)
box BoxA = BoxA{ a = 8 } box BoxA = .a{8}
acc = acc + payload(box) acc = acc + payload(box)
empty BoxB = .b empty BoxB = .b
acc = acc + payload(empty) 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 # 7 + 5 + 5 + 11 + 13 + 2 + 21 + 22 + 35 + 40 + 6 + 50 + 51 + 60 + 76 + 10 + 20 + 8 + 3 + 9 = 454
return acc - 309 return acc - 454
} }