CEP-0001: Concrete Type Unions¶
Draft
Draft proposal for future concrete value unions. V1 still supports | only for enum unions and error-set unions.
Summary¶
Catalyst should eventually support closed concrete type unions over ordinary concrete value types.
Example¶
Possible future shape:
const Fact = ImplementsFact | SatisfiesFact | TypeKindFact | DynSafeContractFact
A value of any member type should coerce into the union when an expected type supplies the union:
const fact: Fact = SatisfiesFact{
.subject = T,
.shape = Shape,
.scope = scope,
}
This proposal captures the ergonomic direction. It does not change V1, where | remains limited to closed enum unions and error-set unions.
Motivation¶
Some Catalyst metadata needs to return heterogeneous but closed sets of values. Predicate reflection facts are the motivating example:
const Fact = ImplementsFact | SatisfiesFact | TypeKindFact | DynSafeContractFact
Without concrete type unions, V1 must model this either as separate fact arrays or as one struct with optional fields whose validity depends on a tag. Separate arrays are V1-compatible, but they lose the ergonomic shape of a single deterministic list. Nullable tag-dependent fields are compact, but they make invalid states easy to represent.
Concrete type unions provide the intended shape: one closed value type, implicit member injection, and no nullable fields that depend on a separate kind tag.
Proposed Direction¶
A | B over concrete value types creates a closed concrete union type when every operand is a concrete value type accepted by the union rules.
If future constraint unions use the same | spelling, this proposal must either include their normalization and narrowing rules explicitly or split them into a separate CEP before acceptance.
Member values coerce into the union in expected-type contexts:
fn record(fact: Fact) void
record(TypeKindFact{ .subject = T, .kind = .sized })
The union stores exactly one member value at a time. It is not a constraint, interface, structural type, or dynamic dispatch surface.
Direct field or method access through a concrete union is not available without narrowing. A union value does not expose the intersection of member fields by default.
Dependencies¶
Concrete type unions depend on a narrowing story. The likely dependencies are:
- pattern matching over closed unions;
- an
isoperator or equivalent type test for union members; - reflection for union member types;
- deterministic diagnostics for ambiguous or unreachable narrowing.
V1 pattern matching and general runtime narrowing are deferred, so this proposal is not a V1 feature.
Representation Direction¶
Concrete type unions should have a predictable closed representation:
- a discriminant identifying the active member;
- payload storage large and aligned enough for any member;
- no heap allocation implied by the union itself;
- copyability only when every member is copyable;
- cleanup by active member when any member has resource cleanup.
The first accepted version should not promise C ABI layout. External representation, @repr controls, and ABI guarantees can be designed later.
Open Questions¶
- Are union operands restricted to sized concrete types in the first version?
- Are duplicate member types and aliases normalized by semantic type identity?
- How does
A | B | Cnormalize and display in diagnostics? - What source syntax narrows a concrete union value?
- Should member-to-union coercion be implicit only in expected-type contexts?
- Do constraint unions share this syntax, or do they need a separate accepted model?
- How should equality, ordering, and hashing conformances be derived or rejected?
- How does resource cleanup interact with partially initialized union payloads?
V1 Compatibility¶
V1 remains unchanged:
|composes only closed enum types/enum-union types and closed error sets/error-set unions.- General
A | Btype unions are rejected outside those families. - Predicate reflection should use V1-compatible metadata shapes until this proposal is accepted.