Language / Type Unions (T1 | T2 | ...)

Type Unions (T1 | T2 | ...)

Silk supports type unions in type annotations. A union type represents a value that is exactly one of several member types.

This feature exists to model small, explicit “one-of-these-types” outcomes without requiring a dedicated nominal enum declaration for every case.

See also:

  • typed errors (unparenthesized | in function signatures is reserved for typed-error contracts),
  • enums (general tagged unions with named variants),
  • flow match (match over union values).

Surface Syntax#

Union types use | between member types:

let x: Foo | Bar;
struct S { v: u8 | bool }
type U = Foo | Bar | Baz;

Return types (important disambiguation)#

In function declarations, unparenthesized | after -> is reserved for typed errors (SuccessType | ErrorType...). To write a union as a function’s return type, the union must be parenthesized:

fn f () -> (Foo | Bar);
fn g () -> (Foo | Bar) | SomeTypedError;

This disambiguation is required so the parser and checker can treat typed-error contracts as authoritative.

Rules#

The implementation intentionally supports only unions whose member types have a safe, well-defined representation in the current compiler/backend subset.

A union type T1 | T2 | ... | Tn is permitted when all member types are in the supported union-member set:

  • Primitive scalar types in { bool, char, i8, u8, i16, u16, i32, u32, i64, u64, int, usize, size, Instant, Duration } (isize is accepted as an alias for size), and/or
  • Nominal POD structs (including error types) and nominal POD enums that lower to a scalar-slot representation in Silk (no opaque structs).

Unions may freely mix primitive and nominal members in this subset.

For primitive members, the current native backend requires that each member type be distinguishable at injection sites. In practice, that means a union may not contain two primitive types that lower to the same backend scalar representation (for example int | i64, usize | u64, char | u32, Duration | i64). This restriction is specific to the backend and may be relaxed once union injection uses full type identity rather than a backend-scalar heuristic.

Notes:

  • Nested unions are flattened: (A | B) | C is the same union as A | B | C.
  • Duplicate member types are rejected.

Semantics#

A value of a union type is a tagged value:

  • It stores a runtime tag identifying which member type is active.
  • It stores the payload value in a uniform representation compatible with all members in Silk.

Representation#

In the current native backend subset, unions are lowered as:

  • (u64 tag, u64 payload_0, ..., u64 payload_(N-1))

where N is the maximum scalar-slot count across the union’s member types (primitive members contribute 1).

Member payload values are stored/loaded via raw-bit casts (cast_raw) to and from the u64 payload slots. Unused payload slots are zero-filled on injection and on widening coercions.

Union-to-union coercions (supersets)#

When a context expects a union type U_sup, a value of a union type U_sub may be used if U_sub’s member set is a subset of U_sup’s member set. The compiler remaps the runtime tag to the destination union’s tag numbering when needed so pattern matches on the destination union remain correct.

If U_sup’s payload is larger than U_sub’s payload (because U_sup contains a member with a larger scalar-slot representation), the payload is widened by copying existing payload slots and zero-filling the newly-added slots.

Construction (injection)#

When a context expects a union type, a value whose type is one of the union’s member types may be used directly and is injected into the union.

Examples:

struct A { x: int }
struct B { x: int }
type U = A | B;

fn main () -> int {
  let a: A = A{ x: 1 };
  let u: U = a; // inject `A` into `U`
  return 0;
}

Matching (match)#

Union values are consumed via match expressions using typed binder patterns:

type U = A | B;

let out: int = match u {
  a: A => a.x,
  b: B => b.x,
};

Rules (Supported forms):

  • When the scrutinee has a union type, patterns are restricted to name: Type (or _: Type) where Type is one of the union member types.
  • Union matches must be exhaustive: exactly one arm per member type (order does not matter).
  • The same typed-binder syntax may also be used with a concrete struct scrutinee before the value is injected into a union. In that form, exactly one typed arm must accept the concrete scrutinee type. Accepted arm types are the exact concrete type, a valid base type through extends, or an interface that the concrete type implements.

Source repository · Edit this page · View Markdown