Feature flags:
-Xunion-typesand-Xintersection-typesIT0--IT4 are complete. Some IT4 items are deferred (boxing codegen,
cast,type-of, tagged union C emission). See Deferred below.
Union types (A | B) and intersection types (A & B) extend the Turmeric type system with
structural type combinations. Together they enable gradual typing, flexible APIs, and
type-safe duck typing without wrapper ADTs.
Both features are gated behind separate flags:
turc -Xunion-types myfile.tur # union types only
turc -Xintersection-types myfile.tur # intersection types only
turc -Xunion-types -Xintersection-types # both
The any type is available when either flag is active.
A union type (A | B) represents a value that is either A or B. The compiler
emits a tagged-union C struct at runtime.
;; Named union type
(deftype IntOrString []
(int | cstr))
;; Inline in a function signature
(defn print-value [x : (int | cstr | bool)] : unit
(match x
(i : int) (println i)
(s : cstr) (println s)
(b : bool) (println (if b "true" "false"))))
Nested unions are flattened: (int | (cstr | bool)) becomes (int | cstr | bool).
Use match to narrow a union-typed value. The elaborator checks exhaustiveness across
all union members:
(defn describe [x : (int | cstr)] : cstr
(match x
(n : int) (str "number: " n)
(s : cstr) (str "string: " s)))
Omitting any member is a compile-time error (TUR_E0301).
A value of type A can be passed anywhere (A | B) is expected (widening). This is
handled implicitly at call sites and return positions:
(defn accepts-union [x : (int | cstr)] : unit ...)
(accepts-union 42) ;; int widens to (int | cstr)
(accepts-union "hello") ;; cstr widens to (int | cstr)
When x : (int | cstr), typeclass methods implemented by all union members may be
called directly without a match. Methods not in the intersection require an explicit
match to narrow first:
;; Show is implemented by both int and cstr
(show x) ;; ok -- in the instance intersection
;; Arithmetic is only on int; requires narrowing
(match x
(n : int) (+ n 1)
(s : cstr) ...)
An intersection type (A & B) represents a value that satisfies both A and B.
The primary use is combining a concrete type with typeclass constraints.
;; Named intersection type
(deftype ReadWrite []
(Readable & Writable))
;; Inline in a function signature
(defn save [x : (int & Serializable)] : unit
(file/write (serialize x) "output.bin"))
From a value of intersection type you can project either member:
(A & B) <: A and (A & B) <: BA accepts a value of type (A & B)Intersection is most useful when one side is a typeclass:
(defclass Serializable [a]
(serialize [x : a] : cstr))
(defn serialize-int [x : (int & Serializable)] : cstr
(serialize x))
The value is an int with a Serializable dictionary attached. The elaborator
resolves the instance at the intersection type site.
Intersections of known-disjoint concrete types are rejected statically (TUR_E0350):
;; Compile error: int and cstr are disjoint
(defn bad [x : (int & cstr)] : unit ...)
Intersections involving typeclasses or type variables that cannot be determined disjoint at compile time are permitted and fail during instance resolution.
any Typeany is the top type: every type is a subtype of any. It is available when either
union or intersection flag is active.
(defn debug-print [x : any] : unit
(println x))
(debug-print 42) ;; ok
(debug-print "hello") ;; ok
(debug-print true) ;; ok
any-typed values are represented as int64_t at codegen level. Pointer-sized payloads
(cstr, struct, ADT) require a boxing wrapper -- this is deferred (see below).
Union simplification: (int | cstr | any) simplifies to any.
Union types and any enable a gradual typing path:
;; Start untyped
(defn debug-print [x : any] : unit
(println x))
;; Narrow gradually as types become known
(defn typed-print [x : (int | cstr)] : unit
(debug-print x))
(defdata Option [a] (none | (some a))) desugaring to a union type is tracked in
the GADT plan (Phase G4). It requires both -Xgadt and -Xunion-types.
| Code | Message |
|---|---|
TUR_E0300 |
Union type mismatch: expected {expected}, got {actual} |
TUR_E0301 |
Non-exhaustive pattern match on union type {type} -- missing arm for {variant} |
TUR_E0350 |
Intersection type unsatisfiable: no value can be both {A} and {B} |
TUR_E0351 |
Value of type {actual} does not satisfy intersection member {missing} |
TypeScript's union types are zero-cost (erased). Turmeric emits
struct { int tag; union { A a; B b; } data; }. Every union-typed value pays
one extra int for the tag plus alignment padding to the largest member. This
matters for arrays, struct fields, and cache pressure.
Widening (passing 42 where (int | cstr) is expected) requires constructing
the tagged union at the call site -- it is not a free annotation.
match-OnlyThere is no flow-sensitive narrowing outside match. type-of checks in if
conditions do not narrow the elaborated type:
;; Does not narrow -- elaboration error inside the branch
(if (= (type-of x) "int")
(+ x 1) ;; error: (int | cstr) is not int
...)
;; Correct: use match
(match x
(n : int) (+ n 1)
(s : cstr) ...)
TypeScript and Scala 3 merge struct fields across intersections
({ x: int } & { y: bool } gives { x: int; y: bool }). Turmeric statically
rejects intersections of two known-disjoint concrete types. Intersection is only
useful when at least one side is a typeclass.
Union types are closed -- the member set is fixed at definition time. A library
returning (int | ParseError) cannot be transparently composed with one returning
(bool | ParseError) without an explicit adapter.
Variance for type constructors containing union or intersection types is not yet
specified. Passing (vec (int | cstr)) where (vec int) is expected may produce
unexpected behaviour and will be addressed before these features are enabled by
default.
The following IT4 items are not yet implemented:
| Item | Notes |
|---|---|
| Boxing codegen | Pointer-sized any payloads (cstr, struct, ADT) need a boxing wrapper struct |
(cast x : T) |
Runtime downcast from any; returns (option T) |
(type-of x) |
Returns a runtime type tag |
| Tagged union C codegen | struct { int tag; union { A a; B b; } data; } emission |
| ADT-as-union sugar | defdata desugaring to union (tracked in GADT plan G4) |
| Instance intersection on unions | Deferred failure during instance resolution may be hard to diagnose |
(-> a b), (vec T), foralldefgadt, type refinement, union types interactionResult and Option types