tagged unions
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 name→member→value 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
|
||||
|
||||
Reference in New Issue
Block a user