tagged unions

This commit is contained in:
2026-06-28 22:54:20 +02:00
parent f328c44154
commit 981ccb047a
7 changed files with 396 additions and 14 deletions
+19
View File
@@ -1048,6 +1048,25 @@ validate_type_nodes :: proc(checker: ^Checker) {
)
}
}
// A tagged union (`item.child` set) must tag with an enum, and each variant
// must name a member of it. The synthesized `union(enum)` tag satisfies this
// by construction; the check guards the explicit `union(Enum)` form.
if item.kind == .Union && types.is_valid(item.child) {
if !types.is_enum(item.child, &checker.module.types) {
source.add(checker.diagnostics, source.Span{}, "a tagged union's tag must be an enum")
} else {
for field in types.fields_for(&checker.module.types, id) {
if _, ok := find_enum_member(checker, item.child, symbol.Id(field.name)); !ok {
source.addf(
checker.diagnostics,
source.Span{},
"union variant '%s' is not a member of the tag enum",
symbol_text(checker, symbol.Id(field.name)),
)
}
}
}
}
}
if item.kind == .Function {
if !item.c_abi {
+36 -4
View File
@@ -711,9 +711,27 @@ emit_instruction_stream :: proc(
}
fmt.sbprintf(&emitter.builder, " %%union_slot%d = alloca %s, align %d\n", instruction_index, type_name, types.alignment_of(instruction.type, &emitter.module.types, emitter.module.target))
fmt.sbprintf(&emitter.builder, " store %s zeroinitializer, ptr %%union_slot%d\n", type_name, instruction_index)
// A tagged union writes the discriminant (the tag enum member matching the
// active variant's name) at offset 0, then the payload after it; untagged
// unions write the payload at offset 0.
payload_ptr := fmt.tprintf("%%union_slot%d", instruction_index)
tag_enum := types.union_tag_enum(instruction.type, &emitter.module.types)
if types.is_valid(tag_enum) {
tag_value: i128 = 0
for member in types.enum_members_for(&emitter.module.types, tag_enum) {
if member.name == fields[field_index].name {
tag_value = member.value
break
}
}
fmt.sbprintf(&emitter.builder, " store %s %d, ptr %%union_slot%d\n", llvm_type(tag_enum, &emitter.module.types), tag_value, instruction_index)
offset := types.union_payload_offset(instruction.type, &emitter.module.types, emitter.module.target)
fmt.sbprintf(&emitter.builder, " %%union_payload%d = getelementptr i8, ptr %%union_slot%d, i64 %d\n", instruction_index, instruction_index, offset)
payload_ptr = fmt.tprintf("%%union_payload%d", instruction_index)
}
fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(fields[field_index].type, &emitter.module.types))
write_operand(&emitter.builder, instructions, instruction.args[0], fields[field_index].type, &emitter.module.types)
fmt.sbprintf(&emitter.builder, ", ptr %%union_slot%d\n", instruction_index)
fmt.sbprintf(&emitter.builder, ", ptr %s\n", payload_ptr)
fmt.sbprintf(&emitter.builder, " %%v%d = load %s, ptr %%union_slot%d\n", instruction_index, type_name, instruction_index)
continue
}
@@ -941,7 +959,7 @@ emit_instruction_stream :: proc(
continue
}
if types.is_union(base_type, &emitter.module.types) {
fmt.sbprintf(&emitter.builder, " %%v%d = getelementptr i8, ptr %%v%d, i64 0\n", instruction_index, instruction.a)
fmt.sbprintf(&emitter.builder, " %%v%d = getelementptr i8, ptr %%v%d, i64 %d\n", instruction_index, instruction.a, types.union_payload_offset(base_type, &emitter.module.types, emitter.module.target))
} else {
fmt.sbprintf(
&emitter.builder,
@@ -1804,10 +1822,24 @@ emit_types :: proc(emitter: ^Emitter) {
fmt.sbprintf(&emitter.builder, "[%d x i8]\n", total_size)
continue
}
// A tagged union lays out `{ tag, [pad], carrier, [pad] }`; the explicit i8
// padding makes the LLVM type's size and field offsets match the byte offsets
// used by construction and field access. Untagged unions have offset 0 and no tag.
payload_offset := types.union_payload_offset(id, &emitter.module.types, emitter.module.target)
strings.write_string(&emitter.builder, "{ ")
if types.is_tagged_union(id, &emitter.module.types) {
tag_type := types.union_tag_enum(id, &emitter.module.types)
tag_size := types.size(tag_type, &emitter.module.types, emitter.module.target)
strings.write_string(&emitter.builder, llvm_type(tag_type, &emitter.module.types))
if payload_offset > tag_size {
fmt.sbprintf(&emitter.builder, ", [%d x i8]", payload_offset-tag_size)
}
strings.write_string(&emitter.builder, ", ")
}
strings.write_string(&emitter.builder, llvm_type(carrier, &emitter.module.types))
if carrier_size < total_size {
fmt.sbprintf(&emitter.builder, ", [%d x i8]", total_size-carrier_size)
used := payload_offset + carrier_size
if used < total_size {
fmt.sbprintf(&emitter.builder, ", [%d x i8]", total_size-used)
}
strings.write_string(&emitter.builder, " }\n")
continue
+44 -2
View File
@@ -1730,6 +1730,22 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_union := false) {
start := advance(parser)
id := types.named(&parser.module.type_store, u32(parser.pkg), u32(name.symbol))
// A tagged union spells its discriminant in parens: `union(Enum)` reuses an existing
// enum; `union(enum)` synthesizes one from the variant names after the body is parsed.
tag := types.INVALID
inferred_tag := false
if is_union {
if _, ok := allow(parser, .Left_Paren); ok {
if _, enum_ok := allow(parser, .Keyword_Enum); enum_ok {
inferred_tag = true
} else {
tag = parse_type(parser)
}
if _, close_ok := allow(parser, .Right_Paren); !close_ok {
source.add(parser.diagnostics, current(parser).span, "expected ')' after union tag")
}
}
}
ended_by_newline := current(parser).kind == .Newline
skip_newlines(parser)
if current(parser).kind != .Left_Brace {
@@ -1739,7 +1755,7 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_unio
"native union declarations require a body" if is_union else "native struct declarations require a body",
)
}
if !types.define_record(&parser.module.type_store, id, nil, c_layout, true, is_union) {
if !types.define_record(&parser.module.type_store, id, nil, c_layout, true, is_union, tag=tag) {
source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name))
}
if !ended_by_newline {
@@ -1775,12 +1791,38 @@ parse_struct :: proc(parser: ^Parser, name: token.Token, c_layout: bool, is_unio
if _, ok := allow(parser, .Right_Brace); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '}' after struct fields")
}
if !types.define_record(&parser.module.type_store, id, fields[:], c_layout, false, is_union) {
if inferred_tag {
tag = synthesize_union_tag(parser, fields[:])
}
if !types.define_record(&parser.module.type_store, id, fields[:], c_layout, false, is_union, tag=tag) {
source.addf(parser.diagnostics, name.span, "duplicate type declaration '%s'", token_text(parser, name))
}
_ = finish_statement(parser)
}
// synthesize_union_tag builds the anonymous discriminant enum for a `union(enum)`:
// one member per variant, dense 0-based, in the smallest fitting unsigned backing
// (mirroring `parse_enum`'s unbacked pick).
synthesize_union_tag :: proc(parser: ^Parser, fields: []types.Field) -> types.Type {
members := make([]types.Enum_Member, len(fields), parser.module.allocator)
defer delete(members, parser.module.allocator)
for field, index in fields {
members[index] = types.Enum_Member{name=field.name, value=i128(index)}
}
max_value := u64(max(len(fields)-1, 0))
backing := types.U8
if max_value > 0xff {
backing = types.U16
}
if max_value > 0xffff {
backing = types.U32
}
if max_value > 0xffff_ffff {
backing = types.U64
}
return types.enum_anonymous(&parser.module.type_store, members, backing)
}
parse_distinct :: proc(parser: ^Parser, name: token.Token) {
start := advance(parser)
child := parse_type(parser)
+61 -3
View File
@@ -229,6 +229,7 @@ define_record :: proc(
is_union := false,
explicit_size: u64 = 0,
explicit_alignment: u32 = 0,
tag: Type = INVALID,
) -> bool {
existing, ok := node(store, id)
if !ok || (existing.kind != .Named && existing.kind != .Struct && existing.kind != .Union) ||
@@ -242,12 +243,31 @@ define_record :: proc(
store.nodes[index].declared = true
store.nodes[index].explicit_size = explicit_size
store.nodes[index].explicit_alignment = explicit_alignment
// A tagged union stashes its discriminant enum in `child` (untagged unions and
// structs leave it INVALID); the per-variant tag value is derived from the enum
// member whose name matches the variant, so no extra storage is needed.
store.nodes[index].child = tag
store.nodes[index].field_start = u32(len(store.fields))
store.nodes[index].field_count = u32(len(fields))
append(&store.fields, ..fields)
return true
}
// enum_anonymous interns an unnamed enum (used as the synthesized discriminant of a
// `union(enum)` tagged union). Members carry the variant names with dense 0-based
// values, so the same namemembervalue lookup used for `union(Enum)` resolves tags.
enum_anonymous :: proc(store: ^Store, members: []Enum_Member, backing: Type) -> Type {
start := u32(len(store.enum_members))
append(&store.enum_members, ..members)
return intern(store, Node{
kind=.Enum,
child=backing,
field_start=start,
field_count=u32(len(members)),
declared=true,
})
}
define_struct :: proc(store: ^Store, id: Type, fields: []Field, c_layout, opaque: bool) -> bool {
return define_record(store, id, fields, c_layout, opaque)
}
@@ -541,6 +561,36 @@ is_union :: proc(value: Type, store: ^Store) -> bool {
return kind(value, store) == .Union
}
// A tagged union is a `.Union` whose `child` is a valid enum (the discriminant).
is_tagged_union :: proc(value: Type, store: ^Store) -> bool {
item, ok := node(store, value)
return ok && item.kind == .Union && is_enum(item.child, store)
}
union_tag_enum :: proc(value: Type, store: ^Store) -> Type {
if !is_tagged_union(value, store) {
return INVALID
}
item, _ := node(store, value)
return item.child
}
// union_payload_offset is the byte offset of a tagged union's payload carrier (after
// the discriminant), shared by `size` and the LLVM emitter so construction, field
// access, and layout agree. Zero for untagged unions.
union_payload_offset :: proc(value: Type, store: ^Store, selected := target.DEFAULT) -> u64 {
if !is_tagged_union(value, store) {
return 0
}
item, _ := node(store, value)
tag_size := size(item.child, store, selected)
payload_align: u64 = 1
for field in fields_for(store, value) {
payload_align = max(payload_align, u64(alignment_of(field.type, store, selected)))
}
return (tag_size+payload_align-1)/payload_align*payload_align
}
is_distinct :: proc(value: Type, store: ^Store) -> bool {
return kind(value, store) == .Distinct
}
@@ -1048,13 +1098,18 @@ size :: proc(value: Type, store: ^Store, selected := target.DEFAULT) -> u64 {
if item.explicit_size > 0 {
return item.explicit_size
}
result: u64
carrier_size: u64
max_align: u64 = 1
for field in fields_for(store, value) {
result = max(result, size(field.type, store, selected))
carrier_size = max(carrier_size, size(field.type, store, selected))
max_align = max(max_align, u64(alignment_of(field.type, store, selected)))
}
return (result+max_align-1)/max_align*max_align
if is_enum(item.child, store) {
payload_offset := union_payload_offset(value, store, selected)
total_align := max(max_align, u64(alignment_of(item.child, store, selected)))
return (payload_offset+carrier_size+total_align-1)/total_align*total_align
}
return (carrier_size+max_align-1)/max_align*max_align
case:
return 0
}
@@ -1091,6 +1146,9 @@ alignment_of :: proc(value: Type, store: ^Store, selected := target.DEFAULT) ->
for field in fields_for(store, value) {
result = max(result, alignment_of(field.type, store, selected))
}
if is_enum(item.child, store) {
result = max(result, alignment_of(item.child, store, selected))
}
return result
case:
return 1