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
+12 -2
View File
@@ -31,8 +31,14 @@ NodeId :: distinct u32
@hide:file
no_node :: maxval!(NodeId)
#! void literals are generated constants only; there is no source void literal.
LiteralKind :: enum {
int, float, string, bool, null
int
float
string
bool
null
void
}
ExprKind :: enum {
@@ -71,6 +77,7 @@ ExprKind :: enum {
catch_expr
typed_constant
comptime_expr
literal_void
}
DeclKind :: enum {
@@ -176,6 +183,7 @@ NodeKind :: enum {
expr_typed_constant
expr_comptime
literal_float_value
literal_void
}
@hide:file
@@ -490,6 +498,7 @@ add_literal proc(builder @mut Builder, kind LiteralKind, span Span) NodeId ! mem
.string: .literal_string
.bool: .literal_bool
.null: .literal_null
.void: .literal_void
}
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_comptime: .comptime_expr
.literal_float_value: .literal_float
.literal_void: .literal_void
else: unreachable
}
}
@@ -1803,7 +1813,7 @@ node_layout proc(node Node) NodeLayout {
layout := NodeLayout{}
match node.kind {
.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_binary, .expr_range, .expr_index, .expr_selection, .expr_typed_constant,
.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] }
`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 {
` if $(i16 != i16) { return $(i8(128)) }
` values :: $(data())
` box Box(i16) :: { value = identity(i16, values[0]) }
` fixed Fixed(count()) :: { values = [box.value, 2] }
` 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
`}
,
+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)
.catch_expr: return try lower_catch(state, id)
.literal_null: return try null_constant(state, .pointer, span)
.literal_void: return ir.no_value
.slice: return try lower_slice(state, id)
.builtin_call: return try lower_builtin(state, id)
.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)
.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,
.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)
if next_is(state, .equal) return try parse_assignment(state, expression, allow_else)
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_string: state.cells[usize(id)].type = Type{ kind = .string }
.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)
.slice: {
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.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: {}
.distinct_type, .alias_expr, .optional_type, .try_expr: try resolve_expr(state, scope, ast.type_operand(tree, id))
.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) {
state.evaluation_diagnostic = diagnostics.no_diagnostic
annotation := view.type
if (contextual_literal(state, argument))
annotation = try argument_annotation(state, function, view.type, arguments)
contextual := contextual_literal(state, argument)
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)
if (arguments[i].kind == .invalid) {
try reject_evaluation(state, call)
@@ -726,21 +730,14 @@ argument_annotation proc(
@hide:file
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)
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)
}
@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.
# Rebuild contextual source literals while the module and its escaped flat cells are alive.
@hide:file
@@ -751,12 +748,10 @@ serialize_value proc(
result := no_node
if (type.kind == .array) {
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)
defer arraylist.deinit(&elements)
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)
if (node == no_node) return no_node
try arraylist.append(&elements, node)
@@ -764,13 +759,18 @@ serialize_value proc(
result = try ast.add_array_literal(state.builder, span, elements.items)
} else if (type.kind == .record) {
record :: typecheck.record_type(checked, type.record_type)
if (record.is_union) return no_node
shape :: module.arrays.items[usize(value.array_type)]
# Untagged storage has no active member; choosing one could discard bytes.
if (record.is_union and record.tag_type.kind != .enumeration) return no_node
fields := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&fields)
for (record.fields) |field, i| {
layout :: shape.fields[i]
child :: aggregate_child(module, value, layout.type, layout.array_type, layout.offset)
if (record.is_union and !ir.union_active(module, value, i)) continue
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)
if (node == no_node) return no_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)
}
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) {
enumeration :: typecheck.enum_type(checked, type.enum_type)
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 (value.data.unsigned != 0) return no_node
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)
} else {
shape :: module.arrays.items[usize(value.array_type)]
payload :: shape.fields[1]
child :: aggregate_child(module, value, payload.type, payload.array_type, payload.offset)
child :: ir.aggregate_item(module, value, 1)
return try serialize_value(state, module, checked, container.element, child, span)
}
} 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)
} else if (type.kind == .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)
return result
@@ -821,6 +838,12 @@ serialize_value proc(
constant_equal proc(state @mut State, left, right ast.NodeId) bool ! Error {
kind :: ast.expr_kind(&state.tree, left)
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) {
count :: ast.array_element_count(&state.tree, left)
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 {
kind :: ast.expr_kind(&state.tree, id)
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) {
result := hash
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{}
}
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)
if (node == no_node) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span)
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 }
result := try evaluate(state, node)
result.annotation = result_type
@@ -1405,7 +1433,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
match kind {
.typed_constant: {
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
return value
}
@@ -1439,6 +1467,7 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
return Value{ kind = .string, 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,
.enum_type, .union_type, .distinct_type, .fallible_type, .sum_type:
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)
.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))
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_bool: return try builtin(state, id, .bool)
.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: {
view :: ast.record_literal(&state.tree, id)
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 == .string) return value.kind == .string
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
bits := u32(64)
signed := false