Status: IT0--IT4 are complete. As of TY2,
anyboxing codegen, the checkedcast, andtype-ofship for every payload kind (int/bool/float/nil/cstr/ptr, ADTs, and heap-boxed structs). The remaining deferred item is generalstruct { int tag; union { ... } }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, along with the any type, are enabled by default; no flag is required.
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 at codegen as a tur_tagged_t
({ int64_t tag; int64_t val; }): the tag is the payload's TypeKind and
the val carries the payload. Immediate values (int/bool/nil) ride the
carrier directly, floats are stored as their IEEE-754 bit pattern, pointer
payloads (cstr, ptr, ADT handles) store the pointer, and by-value structs
are heap-boxed (a malloc'd copy whose pointer rides the carrier). A value is
boxed automatically wherever it is widened to any -- at a call argument, a
function's : any return position, or an if branch facing an any sibling.
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))
cast, and type-ofA value widened to any is boxed into a tur_tagged_t that records the
payload's runtime type. Two forms read that box back:
(type-of x) returns the payload's type name as a cstr:
"int", "bool", "float", "cstr", "ptr", "struct", or "adt". The
tag has kind granularity -- every struct reports "struct" and every ADT
"adt", not the specific struct/ADT name.(cast x : T) is a checked downcast. It verifies the box tag matches
T and returns the unboxed value; on a mismatch it panics (aborts with a
message like cast: any holds cstr, not int). T may be a primitive type
name or a struct/ADT name.(defn box-it [] : any "hello")
(defn main [] : int
(let [a (box-it)]
(println (type-of a)) ;; => cstr
(println (cast a cstr)) ;; => hello
;; (cast a int) ;; would panic: any holds cstr, not int
0))
By-value structs are heap-boxed on widening (a malloc'd copy) and unboxed by
dereference on cast; ADTs and cstr are pointer-carried and ride the carrier
directly; floats are stored by their bit pattern so no precision is lost.
Note: the struct heap-box is owned by the
anyvalue's (untracked) lifetime, so themalloc'd copy is not freed -- widening a struct toanyleaks one allocation per widen. This is acceptable for the gradual-typing use casesanytargets; if you need a struct inanyon a hot path, prefer a pointer/ADT payload, which is carrier-resident and allocation-free.
any, not a desugar)An early plan proposed desugaring defdata into a union type internally so the
two share one code path. That internal unification is not pursued: the union
machinery is monomorphic, closed, and non-recursive, while every defdata in
the tree is parametric (Either [L R]), higher-kinded (Fix [^f],
Free [^f a]), recursive, or a GADT (Nat). Lowering those onto today's union
representation would mean rebuilding parametric/HKT/recursive/GADT sum-type
support on top of unions -- a far larger change with no user-visible payoff.
The user-facing goal that desugar was meant to deliver -- ADT values
participating in union-style dispatch -- already ships through the any top
type. An ADT value widens to any (boxing codegen), reports its kind via
type-of ("adt"), and lands back in match through a checked cast. See the
defdata-as-union and any-box-adt fixtures.
| 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.
if-Guard Narrowing (any)Flow-sensitive narrowing works in if guards on an any-typed variable. A
type-test guard in the condition refines the variable to the tested type inside
the then-branch, so it can be used at that type with no explicit cast. Two
guard shapes are recognized:
;; (is? x T) -- the dedicated type-test predicate
(defn bump [x : any] : int
(if (is? x int)
(+ x 1) ;; x is narrowed to int here -- no cast needed
0))
;; (= (type-of x) "T") -- type-of compared against a string literal
(if (= (type-of x) "int")
(+ x 1)
0)
Chaining handles multi-type dispatch:
(defn describe [v : any] : cstr
(if (is? v int) "int"
(if (is? v bool) "bool"
(if (is? v Point) "point" "other"))))
(is? x T) is also a plain boolean predicate (it requires an any-typed
argument, like type-of and cast). The runtime check compares the value's
box tag to T; T may be a primitive, struct, or ADT name.
Supported guard shapes (narrow): a direct (is? x T) or
(= (type-of x) "T") test on a single any variable, used as the whole if
condition. Unsupported (do not narrow): negation ((not (is? x T))),
conjunction/disjunction of tests, the else-branch complement, and tests on
union (A | B) variables. For unions, use match, which narrows exhaustively:
(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.
| Item | Notes |
|---|---|
any boxing codegen |
All payload kinds box: immediates ride the carrier, floats by bit pattern, cstr/ptr/ADT by pointer, by-value structs heap-boxed. Boxing happens at every widening site (call arg, : any return, if branch). |
(cast x : T) |
Checked downcast from any; verifies the box tag and panics on mismatch. T may be a primitive, struct, or ADT name. |
(type-of x) |
Returns the payload's type name ("int", ..., "struct", "adt") at kind granularity. |
The following IT4 items are not yet implemented:
| Item | Notes |
|---|---|
| Tagged union C codegen | General struct { int tag; union { A a; B b; } data; } emission for (A \| B) unions (the any top type ships via tur_tagged_t) |
Per-name type-of/cast granularity |
type-of reports "struct"/"adt", not the specific struct/ADT name; cast checks at that same granularity |
| ADT-as-union sugar | Not pursued -- infeasible against monomorphic unions (defdata is parametric/HKT/recursive/GADT). ADTs already interoperate with unions via any boxing; see ADTs and Unions. |
| 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