feat: materialize tagged-union and fallible constants

This commit is contained in:
2026-10-27 01:22:09 +01:00
parent e5215c4092
commit bf65f3c2c3
10 changed files with 312 additions and 38 deletions
+2 -2
View File
@@ -104,9 +104,9 @@ type-producing bodies also execute scalar/enum `match` statements and type-value
matches also select nested types, array lengths, generic arguments, callback signatures and field defaults in type-producing bodies. for example, `return struct { values [2](match wide { true: i16, false: i8 }) }` generates concrete storage without elaborating the unselected arm. snapshots preserve loop-local type/value bindings, selected literals retain their spelling and source provenance, and nominal results are cached before their recursive fields are expanded. deferred type selection shares the producer's execution bound; failed elaboration retains its original diagnostic without leaving unchecked generated types behind. tagged-union payload captures, general type-valued storage and broader generic-body match specialization remain open. matches also select nested types, array lengths, generic arguments, callback signatures and field defaults in type-producing bodies. for example, `return struct { values [2](match wide { true: i16, false: i8 }) }` generates concrete storage without elaborating the unselected arm. snapshots preserve loop-local type/value bindings, selected literals retain their spelling and source provenance, and nominal results are cached before their recursive fields are expanded. deferred type selection shares the producer's execution bound; failed elaboration retains its original diagnostic without leaving unchecked generated types behind. tagged-union payload captures, general type-valued storage and broader generic-body match specialization remain open.
ordinary procedures may compute integer, boolean, float and string compile-time arguments and return nested records/tuples and fixed arrays from constant inputs, such as `Fixed(i16, length(2))`. calls reuse the ordinary checker, typed IR and interpreter, preserving local type inference, checked arithmetic, mutable locals, range/while loops, break/continue and recursive or specialized helper calls. aggregate arguments receive their declared context, including dependent `[N]T` parameters; cached specializations compare normalized values, not literal spelling or evaluation identity. returned aggregates become owned source literals before temporary interpreter storage is released, preserving by-value copies, exact integer types, string bytes, enum fields, optional fields and IEEE float bits. ordinary procedures may compute integer, boolean, float and string compile-time arguments and return nested records/tuples, fixed arrays, tagged unions and explicit fallible results from constant inputs, such as `Fixed(i16, length(2))`. calls reuse the ordinary checker, typed IR and interpreter, preserving local type inference, checked arithmetic, mutable locals, range/while loops, break/continue and recursive or specialized helper calls. aggregate and fallible arguments receive their declared context, including dependent `[N]T` parameters; cached specializations compare normalized values, not literal spelling or evaluation identity. returned constants become owned source literals before temporary interpreter storage is released, preserving by-value copies, exact integer types, string bytes, enum/optional fields and IEEE float bits. tagged unions retain their actual discriminant and only the active payload, including nested aggregates and void/zero-cell variants. fallible constants retain success/error identity, composed or payload-bearing errors and void success; `catch` can consume them in generated lengths, defaults and runtime expressions.
`$(expression)` explicitly evaluates a constant expression, including an ordinary procedure call, even in a package without templates. use a typed expression when its shape cannot be inferred independently, such as `$(Point { x = 40 })`; integer literals retain contextual typing. the resulting value works in runtime expressions, bindings and array lengths, while the erased expression still receives ordinary checking. each interpreter evaluation is isolated and limited to 100,000 IR instructions/terminators and 128 active frames; failures retain their originating source locations. runtime parameters and mutable local/global dependencies cannot supply compile-time inputs. data pointers, slices, callbacks, unions and fallible results cannot escape temporary evaluation as generated constants; records/arrays containing these unsupported values are diagnosed rather than retaining interpreter handles. executed foreign calls remain interpreter traps. general type-valued execution, broader compile-time match/`inline` expansion, reflection, generated type/default arrays and existing generic-library acceptance remain open. `$(expression)` explicitly evaluates a constant expression, including an ordinary procedure call, even in a package without templates. use a typed expression when its shape cannot be inferred independently, such as `$(Point { x = 40 })`; integer literals retain contextual typing. the resulting value works in runtime expressions, bindings, expression statements and array lengths, while the erased expression still receives ordinary checking. each interpreter evaluation is isolated and limited to 100,000 IR instructions/terminators and 128 active frames; failures retain their originating source locations. runtime parameters and mutable local/global dependencies cannot supply compile-time inputs. data pointers, slices, callbacks and untagged unions cannot escape temporary evaluation as generated constants; active payloads or aggregate fields containing unsupported values are diagnosed rather than retaining interpreter handles. untagged unions lack an active-member identity, so selecting a source variant could discard observable bytes. executed foreign calls remain interpreter traps. general type-valued execution, broader compile-time match/`inline` expansion, reflection, generated type/default arrays and existing generic-library acceptance remain open.
`if $(condition) { ... } else { ... }` evaluates a boolean guard before expanding or checking either body, including calls to ordinary constant-input procedures. concrete generic specializations also select branches whose guards use constants, immutable constant bindings, type comparisons or substituted compile-time parameters. only the selected body is specialized and checked; both bodies are still parsed and name-resolved. selected bodies retain their scope, cleanup and return behavior, and erased guards retain ordinary type checking and source diagnostics. ordinary calls in an implicit guard stay runtime; use `$()` to request their evaluation. optional captures remain runtime, and ordinary nongeneric `if (true)` still checks both bodies. `if $(condition) { ... } else { ... }` evaluates a boolean guard before expanding or checking either body, including calls to ordinary constant-input procedures. concrete generic specializations also select branches whose guards use constants, immutable constant bindings, type comparisons or substituted compile-time parameters. only the selected body is specialized and checked; both bodies are still parsed and name-resolved. selected bodies retain their scope, cleanup and return behavior, and erased guards retain ordinary type checking and source diagnostics. ordinary calls in an implicit guard stay runtime; use `$()` to request their evaluation. optional captures remain runtime, and ordinary nongeneric `if (true)` still checks both bodies.
+7 -5
View File
@@ -19,10 +19,11 @@ explicit fallible channels, composed error sets, `try`/`catch` and scoped LIFO
traps or compiler infrastructure failures. traps or compiler infrastructure failures.
cached generic specialization and type-producing procedures support explicit/inferred cached generic specialization and type-producing procedures support explicit/inferred
compile-time arguments and generated record/array storage through both paths. compile-time arguments and generated record/array storage through both paths.
ordinary procedures can compute scalar and nested record/array compile-time values ordinary procedures can compute scalar, nested record/array, tagged-union and
using mutable locals, loops and recursive calls; explicit `$()` expressions use fallible compile-time values using mutable locals, loops and recursive calls;
the same constant evaluation path. compile-time `if` selection and type comparisons explicit `$()` expressions use the same constant evaluation path. compile-time
specialize only the selected body. type-producing procedures also accept ordinary `if` selection and type comparisons specialize only the selected body.
type-producing procedures also accept ordinary
constant inputs, cached and erased like explicit compile-time arguments. mutable constant inputs, cached and erased like explicit compile-time arguments. mutable
type/value locals, direct assignments, branches, scalar/enum matching and bounded type/value locals, direct assignments, branches, scalar/enum matching and bounded
integer-range/while loops now generate types and storage. matches select nested integer-range/while loops now generate types and storage. matches select nested
@@ -72,7 +73,8 @@ test discovery and FFI remain outside these completed slices.
- [x] mutable type-execution checkpoint: type-producing bodies execute mutable type/value locals, direct binding assignments, scoped blocks, if/else and early returns, integer-range/while loops, break/continue and nested/recursive producers. generated nested types, scalar-computed lengths/defaults, nominal cache identity, exact value types, overflow/guard/assignment diagnostics, imported producers and repeatable compilation work through interpreter/native execution. type-control execution shares a 4096-step bound across nested producers; scalar operations reuse checked IR execution. field/index updates, optional captures, cleanup and type-valued aggregate storage in these bodies remain open. - [x] mutable type-execution checkpoint: type-producing bodies execute mutable type/value locals, direct binding assignments, scoped blocks, if/else and early returns, integer-range/while loops, break/continue and nested/recursive producers. generated nested types, scalar-computed lengths/defaults, nominal cache identity, exact value types, overflow/guard/assignment diagnostics, imported producers and repeatable compilation work through interpreter/native execution. type-control execution shares a 4096-step bound across nested producers; scalar operations reuse checked IR execution. field/index updates, optional captures, cleanup and type-valued aggregate storage in these bodies remain open.
- [x] type-match checkpoint: type-producing bodies execute scalar/enum match statements and direct type-valued match expressions in initializers, assignments and returns, including nested selected arms. ordinary checking/IR execution select the arm and validate subject/pattern types, ranges, overlap, unreachable arms and exhaustiveness; only the selected body is elaborated. generated storage, scoped locals, loop transfers, early returns, nominal cache identity, selected-body diagnostics, dependency provenance and repeatable compilation work through interpreter/native execution. all bodies remain parsed/name-resolved and execution shares the type-control bound. tagged-union payload captures remain open. - [x] type-match checkpoint: type-producing bodies execute scalar/enum match statements and direct type-valued match expressions in initializers, assignments and returns, including nested selected arms. ordinary checking/IR execution select the arm and validate subject/pattern types, ranges, overlap, unreachable arms and exhaustiveness; only the selected body is elaborated. generated storage, scoped locals, loop transfers, early returns, nominal cache identity, selected-body diagnostics, dependency provenance and repeatable compilation work through interpreter/native execution. all bodies remain parsed/name-resolved and execution shares the type-control bound. tagged-union payload captures remain open.
- [x] nested type-match checkpoint: matches select types inside constructors, array lengths, generic arguments, local annotations, callback signatures and field defaults in type-producing bodies. selection precedes ordinary expansion, preserves literal spelling/source provenance and loop snapshots, and caches nominal results before expanding recursive fields. selected-body and pattern diagnostics, failed deferred elaboration, imported producers and repeatable compilation work through interpreter/native execution; deferred selection shares the producer's type-control bound. - [x] nested type-match checkpoint: matches select types inside constructors, array lengths, generic arguments, local annotations, callback signatures and field defaults in type-producing bodies. selection precedes ordinary expansion, preserves literal spelling/source provenance and loop snapshots, and caches nominal results before expanding recursive fields. selected-body and pattern diagnostics, failed deferred elaboration, imported producers and repeatable compilation work through interpreter/native execution; deferred selection shares the producer's type-control bound.
- [ ] remaining gate: general type-valued compile-time execution, broader compile-time match/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), broader constant materialization (including unions, fallible results and storage-bearing values), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. - [x] tagged/fallible constant checkpoint: explicit `$()` expressions, generic value parameters and type-procedure inputs materialize tagged unions and both fallible channels through interpreter/native execution. preserve active tags/payloads, nested raw aggregate projections, string ownership, void/zero-cell variants, composed/payload errors, declared channel types, normalized cache identity and repeatable compilation. `catch` computes generated lengths and runtime values. untagged unions and storage-bearing active payloads remain escape diagnostics rather than retaining temporary interpreter handles.
- [ ] remaining gate: general type-valued compile-time execution, broader compile-time match/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), broader constant materialization (including untagged unions and storage-bearing values), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates.
- [ ] remaining source constructs: collection/index iteration, expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. - [ ] remaining source constructs: collection/index iteration, expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`.
- [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library. - [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library.
- [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support. - [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support.
+12 -2
View File
@@ -31,8 +31,14 @@ NodeId :: distinct u32
@hide:file @hide:file
no_node :: maxval!(NodeId) no_node :: maxval!(NodeId)
#! void literals are generated constants only; there is no source void literal.
LiteralKind :: enum { LiteralKind :: enum {
int, float, string, bool, null int
float
string
bool
null
void
} }
ExprKind :: enum { ExprKind :: enum {
@@ -71,6 +77,7 @@ ExprKind :: enum {
catch_expr catch_expr
typed_constant typed_constant
comptime_expr comptime_expr
literal_void
} }
DeclKind :: enum { DeclKind :: enum {
@@ -176,6 +183,7 @@ NodeKind :: enum {
expr_typed_constant expr_typed_constant
expr_comptime expr_comptime
literal_float_value literal_float_value
literal_void
} }
@hide:file @hide:file
@@ -490,6 +498,7 @@ add_literal proc(builder @mut Builder, kind LiteralKind, span Span) NodeId ! mem
.string: .literal_string .string: .literal_string
.bool: .literal_bool .bool: .literal_bool
.null: .literal_null .null: .literal_null
.void: .literal_void
} }
return try add_node(builder, Node{ kind = node_kind }, span) return try add_node(builder, Node{ kind = node_kind }, span)
} }
@@ -1408,6 +1417,7 @@ expression_kind proc(kind NodeKind) ExprKind {
.expr_typed_constant: .typed_constant .expr_typed_constant: .typed_constant
.expr_comptime: .comptime_expr .expr_comptime: .comptime_expr
.literal_float_value: .literal_float .literal_float_value: .literal_float
.literal_void: .literal_void
else: unreachable else: unreachable
} }
} }
@@ -1803,7 +1813,7 @@ node_layout proc(node Node) NodeLayout {
layout := NodeLayout{} layout := NodeLayout{}
match node.kind { match node.kind {
.literal_int, .literal_float, .literal_float_value, .literal_string, .literal_bool, .literal_null, .literal_int, .literal_float, .literal_float_value, .literal_string, .literal_bool, .literal_null,
.expr_identifier, .invalid_expr: {} .literal_void, .expr_identifier, .invalid_expr: {}
.expr_unary: layout.direct_children = 2 .expr_unary: layout.direct_children = 2
.expr_binary, .expr_range, .expr_index, .expr_selection, .expr_typed_constant, .expr_binary, .expr_range, .expr_index, .expr_selection, .expr_typed_constant,
.expr_fallible_type, .expr_sum_type, .expr_array_type, .expr_slice_type: .expr_fallible_type, .expr_sum_type, .expr_array_type, .expr_slice_type:
+21
View File
@@ -105,12 +105,33 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
` } ` }
`} `}
`data proc() [2]i16 { return [40, 2] } `data proc() [2]i16 { return [40, 2] }
`Tag :: enum(i16) { empty = -7, payload = 90 }
`Choice :: union(Tag) { payload struct { values [2]i16, text string }, empty void }
`Error :: union(enum) { failed struct { code i16, text string } }
`Packet :: struct { choices [2]Choice, results [2](i16 ! Error) }
`packet_data proc() Packet {
` return {
` choices = [.payload { values = [40, 2], text = "ok" }, .empty],
` results = [40, .failed { code = 2, text = "owned" }],
` }
`}
`Token proc($value Packet) type { return struct {} }
`main proc() i16 { `main proc() i16 {
` if $(i16 != i16) { return $(i8(128)) } ` if $(i16 != i16) { return $(i8(128)) }
` values :: $(data()) ` values :: $(data())
` box Box(i16) :: { value = identity(i16, values[0]) } ` box Box(i16) :: { value = identity(i16, values[0]) }
` fixed Fixed(count()) :: { values = [box.value, 2] } ` fixed Fixed(count()) :: { values = [box.value, 2] }
` same Fixed(2) :: fixed ` same Fixed(2) :: fixed
` packet :: $(packet_data())
` if (Token(packet_data()) != Token(packet)) return 0
` payload :: packet.choices[0].payload
` if (payload.values[0] != 40 or payload.values[1] != 2 or payload.text.len != 2) return 0
` match packet.choices[1] { .empty: {}, .payload: return 0 }
` if ((packet.results[0] catch 0) != 40) return 0
` error_code :: packet.results[1] catch |err| match err {
` .failed |detail|: detail.code + i16(detail.text.len),
` }
` if (error_code != 7) return 0
` return identity(same.values[0]) + same.values[$(usize(1))] + same.bias ` return identity(same.values[0]) + same.values[$(usize(1))] + same.bias
`} `}
, ,
+1
View File
@@ -1823,6 +1823,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
.try_expr: return try lower_try(state, id) .try_expr: return try lower_try(state, id)
.catch_expr: return try lower_catch(state, id) .catch_expr: return try lower_catch(state, id)
.literal_null: return try null_constant(state, .pointer, span) .literal_null: return try null_constant(state, .pointer, span)
.literal_void: return ir.no_value
.slice: return try lower_slice(state, id) .slice: return try lower_slice(state, id)
.builtin_call: return try lower_builtin(state, id) .builtin_call: return try lower_builtin(state, id)
.record_literal: { .record_literal: {
+1 -1
View File
@@ -303,7 +303,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
.defer, .errdefer: return try parse_cleanup(state, allow_else) .defer, .errdefer: return try parse_cleanup(state, allow_else)
.open_curly: return try ast.add_block_stmt(state.builder, try parse_block(state)) .open_curly: return try ast.add_block_stmt(state.builder, try parse_block(state))
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .struct, .ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .struct,
.enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null, .try: { .enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null, .try, .dollar: {
expression :: try parse_expr(state) expression :: try parse_expr(state)
if next_is(state, .equal) return try parse_assignment(state, expression, allow_else) if next_is(state, .equal) return try parse_assignment(state, expression, allow_else)
return try ast.add_expression_stmt(state.builder, expression) return try ast.add_expression_stmt(state.builder, expression)
+1
View File
@@ -1183,6 +1183,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
.literal_bool: state.cells[usize(id)].type = Type{ kind = .bool } .literal_bool: state.cells[usize(id)].type = Type{ kind = .bool }
.literal_string: state.cells[usize(id)].type = Type{ kind = .string } .literal_string: state.cells[usize(id)].type = Type{ kind = .string }
.literal_null: state.cells[usize(id)].type = Type{ kind = .null } .literal_null: state.cells[usize(id)].type = Type{ kind = .null }
.literal_void: state.cells[usize(id)].type = Type{ kind = .void }
.builtin_call: try collect_builtin(state, id) .builtin_call: try collect_builtin(state, id)
.slice: { .slice: {
view :: ast.slice_expr(tree, id) view :: ast.slice_expr(tree, id)
+1 -1
View File
@@ -532,7 +532,7 @@ resolve_expr proc(
try resolve_expr(state, scope, value.type) try resolve_expr(state, scope, value.type)
try resolve_expr(state, scope, value.value) try resolve_expr(state, scope, value.value)
} }
.literal_int, .literal_float, .literal_bool, .literal_string, .literal_null: {} .literal_int, .literal_float, .literal_bool, .literal_string, .literal_null, .literal_void: {}
.enum_literal: {} .enum_literal: {}
.distinct_type, .alias_expr, .optional_type, .try_expr: try resolve_expr(state, scope, ast.type_operand(tree, id)) .distinct_type, .alias_expr, .optional_type, .try_expr: try resolve_expr(state, scope, ast.type_operand(tree, id))
.enum_type: { .enum_type: {
+66 -27
View File
@@ -390,8 +390,12 @@ specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error {
if (type_result or view.comptime) { if (type_result or view.comptime) {
state.evaluation_diagnostic = diagnostics.no_diagnostic state.evaluation_diagnostic = diagnostics.no_diagnostic
annotation := view.type annotation := view.type
if (contextual_literal(state, argument)) contextual := contextual_literal(state, argument)
annotation = try argument_annotation(state, function, view.type, arguments) if (!contextual and !is_type(state, annotation)) {
type :: try canonical_type(state, annotation)
contextual = type != no_node and ast.expr_kind(&state.tree, type) == .fallible_type
}
if (contextual) annotation = try argument_annotation(state, function, view.type, arguments)
arguments[i] = try evaluate_expected(state, argument, annotation) arguments[i] = try evaluate_expected(state, argument, annotation)
if (arguments[i].kind == .invalid) { if (arguments[i].kind == .invalid) {
try reject_evaluation(state, call) try reject_evaluation(state, call)
@@ -726,21 +730,14 @@ argument_annotation proc(
@hide:file @hide:file
evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Error { evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Error {
if (contextual_literal(state, id)) { if (!is_type(state, annotation)) {
type :: try canonical_type(state, annotation) type :: try canonical_type(state, annotation)
if (type != no_node) return try evaluate_ir(state, id, type) if (type != no_node and (contextual_literal(state, id) or ast.expr_kind(&state.tree, type) == .fallible_type))
return try evaluate_ir(state, id, type)
} }
return try evaluate(state, id) return try evaluate(state, id)
} }
@hide:file
aggregate_child proc(module @ir.Module, parent ir.Value, type ir.Type, shape ir.ArrayId, offset usize) ir.Value {
if (!ir.aggregate(type)) return parent.data.elements[offset]
cells :: module.arrays.items[usize(shape)].cells
return ir.Value{ type = type, array_type = shape,
data = ir.ValueData{ elements = parent.data.elements[offset..offset + cells] } }
}
# Values leaving the temporary interpreter must own no interpreter addresses or module IDs. # Values leaving the temporary interpreter must own no interpreter addresses or module IDs.
# Rebuild contextual source literals while the module and its escaped flat cells are alive. # Rebuild contextual source literals while the module and its escaped flat cells are alive.
@hide:file @hide:file
@@ -751,12 +748,10 @@ serialize_value proc(
result := no_node result := no_node
if (type.kind == .array) { if (type.kind == .array) {
array :: typecheck.array_type(checked, type.array_type) array :: typecheck.array_type(checked, type.array_type)
shape :: module.arrays.items[usize(value.array_type)]
stride :: if (ir.aggregate(shape.element)) module.arrays.items[usize(shape.element_array)].cells else usize(1)
elements := arraylist.init(ast.NodeId, state.allocator) elements := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&elements) defer arraylist.deinit(&elements)
for (0..array.len) |i| { for (0..array.len) |i| {
child :: aggregate_child(module, value, shape.element, shape.element_array, i * stride) child :: ir.aggregate_item(module, value, i)
node :: try serialize_value(state, module, checked, array.element, child, span) node :: try serialize_value(state, module, checked, array.element, child, span)
if (node == no_node) return no_node if (node == no_node) return no_node
try arraylist.append(&elements, node) try arraylist.append(&elements, node)
@@ -764,13 +759,18 @@ serialize_value proc(
result = try ast.add_array_literal(state.builder, span, elements.items) result = try ast.add_array_literal(state.builder, span, elements.items)
} else if (type.kind == .record) { } else if (type.kind == .record) {
record :: typecheck.record_type(checked, type.record_type) record :: typecheck.record_type(checked, type.record_type)
if (record.is_union) return no_node # Untagged storage has no active member; choosing one could discard bytes.
shape :: module.arrays.items[usize(value.array_type)] if (record.is_union and record.tag_type.kind != .enumeration) return no_node
fields := arraylist.init(ast.NodeId, state.allocator) fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields) defer arraylist.deinit(&fields)
for (record.fields) |field, i| { for (record.fields) |field, i| {
layout :: shape.fields[i] if (record.is_union and !ir.union_active(module, value, i)) continue
child :: aggregate_child(module, value, layout.type, layout.array_type, layout.offset) if (record.is_union and field.type.kind == .void) {
result = try ast.add_enum_literal(state.builder, span, field.name?)
try refresh(state)
return result
}
child :: ir.aggregate_item(module, value, i)
node :: try serialize_value(state, module, checked, field.type, child, span) node :: try serialize_value(state, module, checked, field.type, child, span)
if (node == no_node) return no_node if (node == no_node) return no_node
generated :: try ast.add_field(state.builder, span, field.name, null, node) generated :: try ast.add_field(state.builder, span, field.name, null, node)
@@ -778,6 +778,23 @@ serialize_value proc(
try arraylist.append(&fields, generated) try arraylist.append(&fields, generated)
} }
result = try ast.add_record_literal(state.builder, span, null, fields.items) result = try ast.add_record_literal(state.builder, span, null, fields.items)
} else if (type.kind == .fallible) {
container :: typecheck.array_type(checked, type.array_type)
failed :: ir.union_tag(module, value).data.integer == 1
index :: if (failed) usize(1) else usize(0)
channel :: if (failed) container.error_type else container.element
child :: ir.aggregate_item(module, value, index)
result = try serialize_value(state, module, checked, channel, child, span)
if (result == no_node) return no_node
# Keep named success/error types distinct even when their members overlap.
annotation := no_node
if (channel.nominal != no_node) annotation = channel.nominal
else if (channel.kind == .enumeration) annotation = typecheck.enum_type(checked, channel.enum_type).declaration
else if (channel.kind == .record) annotation = typecheck.record_type(checked, channel.record_type).declaration
if (annotation != no_node) {
concrete :: try materialize(state, Value{ kind = .type, node = annotation })
result = try ast.add_typed_constant(state.builder, span, concrete.node, result)
}
} else if (type.kind == .enumeration) { } else if (type.kind == .enumeration) {
enumeration :: typecheck.enum_type(checked, type.enum_type) enumeration :: typecheck.enum_type(checked, type.enum_type)
negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0 negative :: !ir.unsigned_integer(value.type) and value.data.integer < 0
@@ -793,12 +810,10 @@ serialize_value proc(
if (container.element.kind == .pointer or container.element.kind == .function) { if (container.element.kind == .pointer or container.element.kind == .function) {
if (value.data.unsigned != 0) return no_node if (value.data.unsigned != 0) return no_node
result = try ast.add_literal(state.builder, .null, span) result = try ast.add_literal(state.builder, .null, span)
} else if (!value.data.elements[0].data.boolean) { } else if (!ir.aggregate_item(module, value, 0).data.boolean) {
result = try ast.add_literal(state.builder, .null, span) result = try ast.add_literal(state.builder, .null, span)
} else { } else {
shape :: module.arrays.items[usize(value.array_type)] child :: ir.aggregate_item(module, value, 1)
payload :: shape.fields[1]
child :: aggregate_child(module, value, payload.type, payload.array_type, payload.offset)
return try serialize_value(state, module, checked, container.element, child, span) return try serialize_value(state, module, checked, container.element, child, span)
} }
} else if (ir.integer_bits(value.type) != 0 and type.kind != .pointer) { } else if (ir.integer_bits(value.type) != 0 and type.kind != .pointer) {
@@ -812,6 +827,8 @@ serialize_value proc(
result = try ast.add_boolean(state.builder, span, value.data.boolean) result = try ast.add_boolean(state.builder, span, value.data.boolean)
} else if (type.kind == .string) { } else if (type.kind == .string) {
result = try ast.add_string(state.builder, span, ir.string_bytes(module, value.data.string)) result = try ast.add_string(state.builder, span, ir.string_bytes(module, value.data.string))
} else if (type.kind == .void) {
result = try ast.add_literal(state.builder, .void, span)
} }
try refresh(state) try refresh(state)
return result return result
@@ -821,6 +838,12 @@ serialize_value proc(
constant_equal proc(state @mut State, left, right ast.NodeId) bool ! Error { constant_equal proc(state @mut State, left, right ast.NodeId) bool ! Error {
kind :: ast.expr_kind(&state.tree, left) kind :: ast.expr_kind(&state.tree, left)
if (kind != ast.expr_kind(&state.tree, right)) return false if (kind != ast.expr_kind(&state.tree, right)) return false
if (kind == .literal_void) return true
if (kind == .typed_constant) {
a :: ast.typed_constant(&state.tree, left)
b :: ast.typed_constant(&state.tree, right)
return try same_type(state, a.type, b.type) and try constant_equal(state, a.value, b.value)
}
if (kind == .array_literal) { if (kind == .array_literal) {
count :: ast.array_element_count(&state.tree, left) count :: ast.array_element_count(&state.tree, left)
if (count != ast.array_element_count(&state.tree, right)) return false if (count != ast.array_element_count(&state.tree, right)) return false
@@ -850,6 +873,11 @@ constant_equal proc(state @mut State, left, right ast.NodeId) bool ! Error {
constant_hash proc(state @mut State, id ast.NodeId) usize ! Error { constant_hash proc(state @mut State, id ast.NodeId) usize ! Error {
kind :: ast.expr_kind(&state.tree, id) kind :: ast.expr_kind(&state.tree, id)
hash :: enum_index(kind) hash :: enum_index(kind)
if (kind == .literal_void) return hash
if (kind == .typed_constant) {
value :: ast.typed_constant(&state.tree, id)
return mix(mix(hash, try type_hash(state, value.type)), try constant_hash(state, value.value))
}
if (kind == .array_literal) { if (kind == .array_literal) {
result := hash result := hash
for (0..ast.array_element_count(&state.tree, id)) |i| for (0..ast.array_element_count(&state.tree, id)) |i|
@@ -962,13 +990,13 @@ evaluate_ir proc(state @mut State, expression, result_type ast.NodeId) Value ! E
return Value{} return Value{}
} }
value :: outcome.value value :: outcome.value
source_type :: typecheck.type_of(&checked, expression) source_type :: typecheck.type_of(&checked, result_type)
node :: try serialize_value(state, &module, &checked, source_type, value, span) node :: try serialize_value(state, &module, &checked, source_type, value, span)
if (node == no_node) { if (node == no_node) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span) state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span)
return Value{} return Value{}
} }
if (ir.aggregate(value.type) or source_type.kind == .enumeration) if (ir.aggregate(value.type) or source_type.kind == .enumeration or source_type.kind == .void)
return Value{ kind = .aggregate, node = node, annotation = result_type } return Value{ kind = .aggregate, node = node, annotation = result_type }
result := try evaluate(state, node) result := try evaluate(state, node)
result.annotation = result_type result.annotation = result_type
@@ -1405,7 +1433,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
match kind { match kind {
.typed_constant: { .typed_constant: {
view :: ast.typed_constant(&state.tree, id) view :: ast.typed_constant(&state.tree, id)
value := try evaluate(state, view.value) value := try evaluate_expected(state, view.value, view.type)
value.annotation = view.type value.annotation = view.type
return value return value
} }
@@ -1439,6 +1467,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
return Value{ kind = .string, node = id } return Value{ kind = .string, node = id }
} }
.literal_null: return Value{ kind = .null, node = id } .literal_null: return Value{ kind = .null, node = id }
.literal_void: return Value{ kind = .aggregate, node = id, annotation = try builtin(state, id, .void) }
.record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type, .record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type,
.enum_type, .union_type, .distinct_type, .fallible_type, .sum_type: .enum_type, .union_type, .distinct_type, .fallible_type, .sum_type:
return Value{ kind = .type, node = id } return Value{ kind = .type, node = id }
@@ -1458,7 +1487,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
} }
.binary: return try evaluate_binary(state, id) .binary: return try evaluate_binary(state, id)
.call: return try evaluate_call(state, id) .call: return try evaluate_call(state, id)
.record_literal, .array_literal, .selection, .index, .match_expr, .builtin_call: .record_literal, .array_literal, .selection, .index, .match_expr, .builtin_call, .catch_expr:
return try evaluate_ir(state, id, try expression_type(state, id, 0)) return try evaluate_ir(state, id, try expression_type(state, id, 0))
else: return Value{} else: return Value{}
} }
@@ -1759,6 +1788,12 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId !
.literal_float: return try builtin(state, id, .f64) .literal_float: return try builtin(state, id, .f64)
.literal_bool: return try builtin(state, id, .bool) .literal_bool: return try builtin(state, id, .bool)
.literal_string: return try builtin(state, id, .string) .literal_string: return try builtin(state, id, .string)
.literal_void: return try builtin(state, id, .void)
.catch_expr: {
type :: try expression_type(state, ast.catch_expr(&state.tree, id).operand, depth + 1)
if (type == no_node or ast.expr_kind(&state.tree, type) != .fallible_type) return no_node
return try canonical_type(state, ast.binary_expr(&state.tree, type).left)
}
.record_literal: { .record_literal: {
view :: ast.record_literal(&state.tree, id) view :: ast.record_literal(&state.tree, id)
if (view.type) |annotation| return try canonical_type(state, annotation) if (view.type) |annotation| return try canonical_type(state, annotation)
@@ -2001,6 +2036,10 @@ valid_value proc(state @mut State, annotation ast.NodeId, value Value) bool ! Er
if (kind == .bool) return value.kind == .boolean if (kind == .bool) return value.kind == .boolean
if (kind == .string) return value.kind == .string if (kind == .string) return value.kind == .string
if (kind == .float or kind == .f64) return value.kind == .real if (kind == .float or kind == .f64) return value.kind == .real
if (kind == .void) return (
value.kind == .aggregate and value.annotation != no_node
and try same_type(state, type, value.annotation)
)
if (value.kind != .integer) return false if (value.kind != .integer) return false
bits := u32(64) bits := u32(64)
signed := false signed := false
+200
View File
@@ -817,6 +817,206 @@ CASES = {
main proc() usize { return $(make())[0].len } main proc() usize { return $(make())[0].len }
""", b"", b"compile-time result cannot escape evaluation", """, b"", b"compile-time result cannot escape evaluation",
), ),
"comptime_tagged_union_preserves_tags_and_nested_raw_payloads": (
"""
Tag :: enum(i16) { number = -7, empty = 12, zero = 41, payload = 90 }
Empty :: struct {}
Payload :: struct { rows [2]struct { values [2]i16 }, text string }
Choice :: union(Tag) { payload Payload, empty void, zero Empty, number i16 }
make proc() Choice {
return .payload { rows = [{ values = [10, 20] }, { values = [4, 6] }], text = "ok" }
}
numeric proc() Choice { return .number{42} }
empty proc() Choice { return .empty }
zero proc() Choice { return .zero{} }
score proc(value Choice) i16 {
return match value {
.number |number|: number,
.payload |payload|: (
payload.rows[0].values[0] + payload.rows[0].values[1]
+ payload.rows[1].values[0] + payload.rows[1].values[1] + i16(payload.text.len)
),
.empty: 3,
.zero: 5,
}
}
main proc() [5]i16 {
value :: $(make())
return [score(value), value.payload.rows[1].values[1], score($(numeric())),
score($(empty())), score($(zero()))]
}
""", b"[42, 6, 42, 3, 5]\n", None,
),
"comptime_tagged_union_value_parameters_and_cache_identity": (
"""
Choice :: union(enum) { left i16, right i16 }
make proc() Choice { return .left{2} }
Fixed proc($choice Choice) type {
return struct { values [usize(choice.left)]i16 }
}
Token proc($choice Choice) type { return struct {} }
score proc($choice Choice) i16 {
return match choice { .left |value|: value, .right |value|: value + 1 }
}
main proc() i16 {
first Fixed(make()) :: { values = [20, 20] }
same Fixed(Choice { left = 2 }) :: first
if (Token(make()) != Token(Choice { left = 2 })) return 0
if (Token(make()) == Token(Choice { right = 2 })) return 0
if (Token(make()) == Token(Choice { left = 3 })) return 0
return same.values[0] + same.values[1] + score(make())
}
""", b"42\n", None,
),
"comptime_fallible_nested_storage_preserves_both_channels": (
"""
Error :: enum { failed }
Storage :: struct { values [2](i16 ! Error), text string ! Error }
make proc() Storage { return { values = [40, .failed], text = "ok" } }
main proc() i16 {
storage :: $(make())
text :: storage.text catch ""
return (storage.values[0] catch 0) + (storage.values[1] catch i16(text.len))
}
""", b"42\n", None,
),
"comptime_fallible_materializes_composed_and_payload_errors": (
"""
Missing :: enum { missing }
Detail :: union(enum) { failed struct { rows [2]i16, text string }, empty void }
Failure :: struct { code i16 }
missing proc() i16 ! (Missing | Detail) { return .missing }
detail proc() i16 ! (Missing | Detail) {
return .failed { rows = [20, 20], text = "ok" }
}
record proc() i16 ! Failure { return Failure { code = 42 } }
read proc(value i16 ! (Missing | Detail)) i16 {
return value catch |err| match err {
.missing: 7,
.failed |payload|: payload.rows[0] + payload.rows[1] + i16(payload.text.len),
.empty: 0,
}
}
main proc() [3]i16 {
return [read($(missing())), read($(detail())), $(record()) catch |err| err.code]
}
""", b"[7, 42, 42]\n", None,
),
"comptime_fallible_value_parameters_type_inputs_and_cache_identity": (
"""
Error :: enum { failed, other }
good proc() i16 ! Error { return 2 }
bad proc() i16 ! Error { return .failed }
Fixed proc($result i16 ! Error) type {
return struct { values [usize(result catch 1)]i16 }
}
Token proc($result i16 ! Error) type { return struct {} }
score proc($result i16 ! Error) i16 { return result catch 40 }
main proc() i16 {
first Fixed(good()) :: { values = [20, 20] }
same Fixed(2) :: first
if (Token(good()) != Token(2) or Token(bad()) != Token(Error.failed)) return 0
if (Token(good()) == Token(3) or Token(bad()) == Token(Error.other)) return 0
if (Token(good()) == Token(bad())) return 0
return same.values[0] + same.values[1] + score(good())
}
""", b"42\n", None,
),
"comptime_fallible_void_success_and_error_channels": (
"""
Error :: enum { failed }
good proc() void ! Error {}
bad proc() void ! Error { return .failed }
score proc($result void ! Error) i16 {
result catch { return 2 }
return 40
}
main proc() i16 {
value i16 := 0
$(good()) catch { value = 100 }
$(bad()) catch { value = 2 }
return value + score(good()) + score(bad()) - 2
}
""", b"42\n", None,
),
"comptime_fallible_preserves_overlapping_channel_types": (
"""
Success :: enum { failed }
Error :: enum { failed }
Value :: struct { code i16 }
Failure :: struct { code i16 }
good proc() Success ! Error { return Success.failed }
bad proc() Success ! Error { return Error.failed }
good_record proc() Value ! Failure { return Value { code = 40 } }
bad_record proc() Value ! Failure { return Failure { code = 2 } }
enum_score proc() i16 {
value :: $(good()) catch { return -1 }
if (value != Success.failed) return -2
$(bad()) catch { return 42 }
return 0
}
main proc() [2]i16 {
left :: $(good_record()) catch Value { code = 0 }
right :: $(bad_record()) catch |err| Value { code = err.code }
return [enum_score(), left.code + right.code]
}
""", b"[42, 42]\n", None,
),
"comptime_tagged_union_projects_raw_nested_channels": (
"""
Error :: enum { failed }
Choice :: union(enum) {
data struct { maybe ?i16, result i16 ! Error }
pointer @i16
empty void
}
make proc() Choice { return .data { maybe = 40, result = .failed } }
absent proc() Choice { return .empty }
main proc() i16 {
value :: $(make())
empty :: $(absent())
match empty { .empty: {}, else: return 0 }
return value.data.maybe? + (value.data.result catch 2)
}
""", b"42\n", None,
),
"comptime_tagged_union_rejects_nonnull_pointer_payload_escape": (
"""
Choice :: union(enum) { pointer @i16, empty void }
make proc() Choice {
value i16 :: 42
return .pointer{&value}
}
main proc() i16 { return $(make()).pointer^ }
""", b"", b"compile-time result cannot escape evaluation",
),
"comptime_fallible_rejects_nonnull_success_pointer_escape": (
"""
Error :: enum { failed }
make proc() @i16 ! Error {
value i16 :: 42
return &value
}
main proc() i16 { return ($(make()) catch null)^ }
""", b"", b"compile-time result cannot escape evaluation",
),
"comptime_fallible_rejects_nonnull_error_pointer_escape": (
"""
Failure :: struct { pointer @i16 }
make proc() i16 ! Failure {
value i16 :: 42
return Failure { pointer = &value }
}
main proc() i16 { return $(make()) catch |err| err.pointer^ }
""", b"", b"compile-time result cannot escape evaluation",
),
"comptime_untagged_union_remains_an_escape_trap": (
"""
Storage :: union { number i16, bits u16 }
make proc() Storage { return Storage { number = 42 } }
main proc() i16 { return $(make()).number }
""", b"", b"compile-time result cannot escape evaluation",
),
"comptime_float_bits_survive_aggregate_materialization": ( "comptime_float_bits_survive_aggregate_materialization": (
""" """
Floats :: struct { zero f64, nan f64 } Floats :: struct { zero f64, nan f64 }