feat: materialize address-free compile-time untagged unions
Snapshot full union storage rather than choosing an overlapping field. Preserve structural union source syntax independently of nominal identity so direct literal arguments materialize and share normalized specialization keys. Reject uninitialized bytes and address-bearing member layouts.
This commit is contained in:
+24
-1
@@ -83,6 +83,7 @@ ExprKind :: enum {
|
||||
try_expr
|
||||
catch_expr
|
||||
typed_constant
|
||||
union_constant
|
||||
comptime_expr
|
||||
literal_void
|
||||
literal_undefined
|
||||
@@ -196,6 +197,7 @@ NodeKind :: enum {
|
||||
stmt_errdefer
|
||||
stmt_block
|
||||
expr_typed_constant
|
||||
expr_union_constant
|
||||
expr_comptime
|
||||
literal_float_value
|
||||
literal_void
|
||||
@@ -611,6 +613,17 @@ add_typed_constant proc(
|
||||
}, at)
|
||||
}
|
||||
|
||||
#! retains an untagged union's complete native-layout bytes as a string literal.
|
||||
add_union_constant proc(
|
||||
builder @mut Builder, at Span, type, bytes NodeId,
|
||||
) NodeId ! mem.AllocError {
|
||||
return try add_node(builder, Node{
|
||||
kind = .expr_union_constant,
|
||||
data0 = NodeData{ node_id = type },
|
||||
data1 = NodeData{ node_id = bytes },
|
||||
}, at)
|
||||
}
|
||||
|
||||
add_comptime_expr proc(builder @mut Builder, at Span, operand NodeId) NodeId ! mem.AllocError {
|
||||
return try add_node(builder, Node{
|
||||
kind = .expr_comptime,
|
||||
@@ -1599,6 +1612,7 @@ expression_kind proc(kind NodeKind) ExprKind {
|
||||
.expr_try: .try_expr
|
||||
.expr_catch: .catch_expr
|
||||
.expr_typed_constant: .typed_constant
|
||||
.expr_union_constant: .union_constant
|
||||
.expr_comptime: .comptime_expr
|
||||
.expr_block: .block_expr
|
||||
.literal_float_value: .literal_float
|
||||
@@ -1652,6 +1666,15 @@ typed_constant proc(tree @Tree, id NodeId) TypedConstantView {
|
||||
}
|
||||
}
|
||||
|
||||
union_constant proc(tree @Tree, id NodeId) TypedConstantView {
|
||||
node :: get_node(&tree.storage, id)
|
||||
debug.assert(node.kind == .expr_union_constant)
|
||||
return TypedConstantView{
|
||||
type = node.data0.node_id,
|
||||
value = node.data1.node_id,
|
||||
}
|
||||
}
|
||||
|
||||
catch_expr proc(tree @Tree, id NodeId) CatchExprView {
|
||||
node :: get_node(&tree.storage, id)
|
||||
debug.assert(node.kind == .expr_catch)
|
||||
@@ -2007,7 +2030,7 @@ node_layout proc(node Node) NodeLayout {
|
||||
.literal_int, .literal_float, .literal_float_value, .literal_string, .literal_bool, .literal_null,
|
||||
.literal_void, .literal_undefined, .expr_identifier, .invalid_expr: {}
|
||||
.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_union_constant,
|
||||
.expr_fallible_type, .expr_sum_type, .expr_array_type, .expr_slice_type:
|
||||
layout.direct_children = 6
|
||||
.expr_pointer_type: layout.direct_children = 4
|
||||
|
||||
@@ -1006,3 +1006,63 @@ computed_byte_slices_recompile_without_borrowing_evaluation_storage test {
|
||||
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||
}
|
||||
}
|
||||
|
||||
computed_untagged_unions_recompile_without_borrowing_storage_bytes test {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
modules [2]mut ir.Module := undefined
|
||||
{
|
||||
session := init(mem.c_allocator)
|
||||
defer deinit(&session)
|
||||
root :: try add_package(&session)
|
||||
dependency :: try add_package(&session)
|
||||
bytes := try owned_text(
|
||||
`lib :: import "../library"
|
||||
`main proc() u64 {
|
||||
` value := $(lib.make())
|
||||
` saved :: value
|
||||
` value.small = 0
|
||||
` value.words[1] = 99
|
||||
` if (saved.small != 9 or saved.bytes[1] != 255 or saved.bytes[7] != 255) return 0
|
||||
` if (saved.bytes[17] != 255 or saved.bytes[23] != 255) return 0
|
||||
` generated lib.Generated() :: {}
|
||||
` if (generated.value.words[1] != 40 or generated.value.small != 9) return 0
|
||||
` return saved.words[1] + saved.words[3]
|
||||
`}
|
||||
,
|
||||
)
|
||||
defer arraylist.deinit(&bytes)
|
||||
root_source :: try add_source(&session, root, "/union_bytes/app/main.hon", &bytes, &diagnostic_store)
|
||||
imported := try owned_text(
|
||||
`Pair :: struct { small u8, large u64 }
|
||||
`Storage :: union { small u8, rows [2]Pair, words [4]mut u64, bytes [32]u8 }
|
||||
`make proc() Storage {
|
||||
` value := Storage { words = [maxval!(u64), 40, maxval!(u64), 2] }
|
||||
` value.small = 9
|
||||
` return value
|
||||
`}
|
||||
`Generated proc() type {
|
||||
` return struct_type!(.auto, ["value"], [Storage], [make()])
|
||||
`}
|
||||
,
|
||||
)
|
||||
defer arraylist.deinit(&imported)
|
||||
_ = try add_source(&session, dependency, "/union_bytes/library/storage.hon", &imported, &diagnostic_store)
|
||||
try arraylist.append(&session.imports, Import{
|
||||
node = declaration(&session, root_source, 0).node,
|
||||
target = dependency,
|
||||
})
|
||||
modules[0] = try compile(&session, root, &diagnostic_store)
|
||||
errdefer ir.deinit(&modules[0])
|
||||
modules[1] = try compile(&session, root, &diagnostic_store)
|
||||
}
|
||||
defer ir.deinit(&modules[0])
|
||||
defer ir.deinit(&modules[1])
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
for modules |module| {
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@std/debug"
|
||||
import "@std/mem"
|
||||
import "@std/arraylist"
|
||||
import "@std/strconv"
|
||||
@@ -2097,6 +2098,35 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
|
||||
if (trap_type(state, id)) return ir.no_value
|
||||
return try lower_as(state, ast.typed_constant(tree, id).value, typecheck.type_of(state.checked, id))
|
||||
}
|
||||
.union_constant: {
|
||||
if (trap_type(state, id)) return ir.no_value
|
||||
bytes :: ast.union_constant(tree, id).value
|
||||
text :: ast.literal_text(tree, bytes)
|
||||
count :: ir.storage_size(state.module, .record, shape)
|
||||
byte_shape :: try ir.add_array(state.module, .u8, ir.no_array, count, false)
|
||||
scratch :: state.arguments.items.len
|
||||
defer state.arguments.items = state.arguments.items.ptr[..scratch]
|
||||
cursor := usize(1)
|
||||
while (cursor < text.len - 1) {
|
||||
byte :: ast.string_next(text, &cursor) catch |_| {
|
||||
trap(state, try diagnostics.add(state.diagnostics, .invalid_escape, span), span)
|
||||
return ir.no_value
|
||||
}
|
||||
element :: try constant(state, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = u64(byte) } }, span)
|
||||
try arraylist.append(&state.arguments, element)
|
||||
}
|
||||
debug.assert(state.arguments.items.len - scratch == count)
|
||||
block :: &state.module.functions.items[state.function].blocks.items[usize(state.block)]
|
||||
start :: block.arguments.items.len
|
||||
for (state.arguments.items[scratch..]) |element| try arraylist.append(&block.arguments, element)
|
||||
payload :: try emit(state, ir.Instruction{
|
||||
op = .array_literal, type = .array, array_type = byte_shape,
|
||||
arguments_start = start, arguments_count = count, span = span,
|
||||
})
|
||||
return try emit(state, ir.Instruction{
|
||||
op = .union_literal, type = .record, array_type = shape, left = payload, span = span,
|
||||
})
|
||||
}
|
||||
.invalid: {
|
||||
invalid :: ast.invalid(tree, id)
|
||||
trap(state, invalid.diagnostic, invalid.span)
|
||||
|
||||
@@ -60,6 +60,8 @@ RecordField :: struct {
|
||||
|
||||
RecordType :: struct {
|
||||
declaration ast.NodeId
|
||||
# union syntax remains available even when structural checking erases nominal identity.
|
||||
source ast.NodeId = ast.NodeId(maxval!(u32))
|
||||
fields []RecordField
|
||||
is_union bool = false
|
||||
tag_type Type = Type{}
|
||||
@@ -1291,6 +1293,14 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
|
||||
try depend(state, value.type, id)
|
||||
try depend(state, value.value, id)
|
||||
}
|
||||
.union_constant: {
|
||||
value :: ast.union_constant(tree, id)
|
||||
state.cells[usize(id)].fixed = true
|
||||
state.cells[usize(id)].type = try check_annotation(state, value.type, false)
|
||||
try collect_expression(state, value.value)
|
||||
try depend(state, value.type, id)
|
||||
try depend(state, value.value, id)
|
||||
}
|
||||
.try_expr: {
|
||||
operand :: ast.type_operand(tree, id)
|
||||
try collect_expression(state, operand)
|
||||
@@ -2156,6 +2166,18 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
|
||||
value :: ast.typed_constant(tree, id)
|
||||
try compatible(state, value.value, state.cells[usize(id)].type)
|
||||
}
|
||||
.union_constant: {
|
||||
value :: ast.union_constant(tree, id)
|
||||
type :: state.cells[usize(id)].type
|
||||
if (type.kind == .invalid) return
|
||||
if (type.kind != .record or ast.expr_kind(tree, value.value) != .literal_string) {
|
||||
try fail(state, id, .invalid_operand, ast.node_span(tree, id))
|
||||
return
|
||||
}
|
||||
descriptor :: state.records.items[usize(type.record_type)]
|
||||
if (!descriptor.is_union or descriptor.tag_type.kind != .none)
|
||||
try fail(state, id, .invalid_operand, ast.node_span(tree, id))
|
||||
}
|
||||
.try_expr, .catch_expr: try validate_fallible_expression(state, id)
|
||||
.unary: {
|
||||
expression :: ast.unary_expr(tree, id)
|
||||
@@ -4195,7 +4217,7 @@ check_union proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.Allo
|
||||
empty :: try mem.alloc(RecordField, state.records.allocator, 0)
|
||||
declaration :: if (nominal) id else ast.NodeId(maxval!(u32))
|
||||
arraylist.append(&state.records, RecordType{
|
||||
declaration = declaration, fields = empty, is_union = true,
|
||||
declaration = declaration, source = id, fields = empty, is_union = true,
|
||||
}) catch |err| {
|
||||
mem.free(state.records.allocator, empty)
|
||||
return err
|
||||
|
||||
@@ -546,6 +546,11 @@ resolve_expr proc(
|
||||
try resolve_expr(state, scope, value.type)
|
||||
try resolve_expr(state, scope, value.value)
|
||||
}
|
||||
.union_constant: {
|
||||
value :: ast.union_constant(tree, id)
|
||||
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_void,
|
||||
.literal_undefined: {}
|
||||
.enum_literal: {}
|
||||
|
||||
@@ -1706,7 +1706,8 @@ branch_constant proc(state @State, id ast.NodeId, depth usize) bool {
|
||||
)
|
||||
}
|
||||
match ast.expr_kind(&state.tree, id) {
|
||||
.literal_int, .literal_float, .literal_bool, .literal_string, .literal_null, .literal_void: return true
|
||||
.literal_int, .literal_float, .literal_bool, .literal_string, .literal_null, .literal_void,
|
||||
.union_constant: return true
|
||||
.typed_constant: return branch_constant(state, ast.typed_constant(&state.tree, id).value, depth + 1)
|
||||
.enum_literal: return true
|
||||
.record_literal: {
|
||||
@@ -2332,6 +2333,32 @@ evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Erro
|
||||
return try evaluate(state, id)
|
||||
}
|
||||
|
||||
# raw union snapshots cannot distinguish addresses from overlapping integer bits.
|
||||
# require every view to be address-free before retaining the complete representation.
|
||||
@hide:file
|
||||
union_bytes_materializable proc(checked @typecheck.Result, type typecheck.Type, depth usize) bool {
|
||||
if (depth >= 128) return false
|
||||
match type.kind {
|
||||
.int, .i8, .i16, .i32, .isize, .u8, .u16, .u32, .u64, .usize,
|
||||
.float, .bool, .void, .enumeration: return true
|
||||
.array, .optional: {
|
||||
return union_bytes_materializable(checked, typecheck.array_type(checked, type.array_type).element, depth + 1)
|
||||
}
|
||||
.fallible: {
|
||||
channels :: typecheck.array_type(checked, type.array_type)
|
||||
return (union_bytes_materializable(checked, channels.element, depth + 1)
|
||||
and union_bytes_materializable(checked, channels.error_type, depth + 1))
|
||||
}
|
||||
.record: {
|
||||
for (typecheck.record_type(checked, type.record_type).fields) |field| {
|
||||
if (!union_bytes_materializable(checked, field.type, depth + 1)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
else: return false
|
||||
}
|
||||
}
|
||||
|
||||
# values leaving temporary evaluation must own no interpreter addresses or module IDs.
|
||||
# snapshot readonly bytes and rebuild source literals/references before releasing their backing.
|
||||
@hide:file
|
||||
@@ -2417,8 +2444,25 @@ 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)
|
||||
# untagged storage has no active member; choosing one could discard bytes.
|
||||
if (record.is_union and record.tag_type.kind != .enumeration) return no_node
|
||||
if (record.is_union and record.tag_type.kind != .enumeration) {
|
||||
if (!union_bytes_materializable(checked, type, 0)) return no_node
|
||||
bytes :: try mem.alloc(u8, state.allocator, value.data.elements.len)
|
||||
defer mem.free(state.allocator, bytes)
|
||||
for (value.data.elements) |byte, index| {
|
||||
if (byte.uninitialized) {
|
||||
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span)
|
||||
return no_node
|
||||
}
|
||||
bytes[index] = u8(byte.data.unsigned)
|
||||
}
|
||||
annotation :: if (type.nominal != no_node) type.nominal
|
||||
else if (record.declaration != no_node) record.declaration else record.source
|
||||
concrete :: try materialize(state, Value{ kind = .type, node = annotation })
|
||||
literal :: try ast.add_string(state.builder, span, bytes)
|
||||
result = try ast.add_union_constant(state.builder, span, concrete.node, literal)
|
||||
try refresh(state)
|
||||
return result
|
||||
}
|
||||
fields := arraylist.init(ast.NodeId, state.allocator)
|
||||
defer arraylist.deinit(&fields)
|
||||
for (record.fields) |field, i| {
|
||||
@@ -2513,9 +2557,9 @@ 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)
|
||||
if (kind == .typed_constant or kind == .union_constant) {
|
||||
a :: if (kind == .union_constant) ast.union_constant(&state.tree, left) else ast.typed_constant(&state.tree, left)
|
||||
b :: if (kind == .union_constant) ast.union_constant(&state.tree, right) else ast.typed_constant(&state.tree, right)
|
||||
return try same_type_context(state, a.type, b.type, true) and try constant_equal(state, a.value, b.value)
|
||||
}
|
||||
if (kind == .slice) {
|
||||
@@ -2561,8 +2605,8 @@ 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)
|
||||
if (kind == .typed_constant or kind == .union_constant) {
|
||||
value :: if (kind == .union_constant) ast.union_constant(&state.tree, id) else ast.typed_constant(&state.tree, id)
|
||||
return mix(mix(hash, try type_hash_context(state, value.type, true)), try constant_hash(state, value.value))
|
||||
}
|
||||
if (kind == .slice) {
|
||||
@@ -4803,6 +4847,10 @@ evaluate proc(state @mut State, id ast.NodeId) Value ! Error {
|
||||
return Value{}
|
||||
}
|
||||
match kind {
|
||||
.union_constant: {
|
||||
value :: ast.union_constant(&state.tree, id)
|
||||
return Value{ kind = .aggregate, node = id, annotation = value.type }
|
||||
}
|
||||
.typed_constant: {
|
||||
view :: ast.typed_constant(&state.tree, id)
|
||||
if (!is_type(state, view.type) and type_record_storage(state, view.type, 0)) {
|
||||
@@ -5287,8 +5335,9 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId !
|
||||
.record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type,
|
||||
.enum_type, .union_type, .union_tag_type, .distinct_type, .fallible_type, .sum_type, .alias_expr:
|
||||
return try builtin(state, id, .type)
|
||||
.typed_constant: {
|
||||
annotation :: ast.typed_constant(&state.tree, id).type
|
||||
.typed_constant, .union_constant: {
|
||||
annotation :: if (kind == .union_constant) ast.union_constant(&state.tree, id).type
|
||||
else ast.typed_constant(&state.tree, id).type
|
||||
return if (is_type(state, annotation)) annotation else try canonical_type(state, annotation)
|
||||
}
|
||||
.literal_int: return try builtin(state, id, .i64)
|
||||
|
||||
Reference in New Issue
Block a user