3.18. Distinct Types

A distinct type promotes a plain “weak” type (int, float, void?, or any other workhorse type) to a unique nominal type. It has the exact ABI of its underlying type — same size, alignment, and pass-by-value semantics — but it does not interconvert with it: functions taking int won’t take a distinct type over int, and vice versa. This makes handles, IDs, and unit-like values type-safe at zero runtime cost.

typedef distinct EntityId = int
typedef distinct Meters = float
typedef distinct FileHandle = void?

Distinct types are declared at module scope with typedef distinct — the typedef family form that creates a nominal type instead of a transparent alias (distinct itself is not a keyword, so identifiers named distinct — like the linq function — keep working). The underlying type must be a workhorse type (a value type that passes by value — int/uint and friends, float, double, bool, string, pointers, vector types like float3, ranges). A distinct type cannot be defined in terms of another distinct type.

3.18.1. Construction and Deref

The way in is explicit construction from a value of exactly the underlying type — a pure compile-time relabel with no runtime cost — unless the module writes an operator = for the pair (see Assignment across types below):

var id = EntityId(5)
var m : Meters = Meters(2.5)
// var bad : EntityId = 5      // error: no implicit conversion, unless an operator = (EntityId&, int) exists

The only way out is the dereference operator *, which peels exactly one distinct level and yields the underlying type as a reference. Constness flows from the handle: dereferencing a var gives writable access, a let gives const access.

var id = EntityId(5)
print("{*id}\n")        // output: 5
*id = 7                 // writes through - *id is int&
if (*id == 7) {
    print("seven\n")    // output: seven
}

For a distinct type over a pointer, * peels the distinct level, not the pointer: *fileHandle yields void?.

var id : EntityId zero-initializes, and default<EntityId> is the zero value.

3.18.2. Operators

Only == and != are borrowed from the underlying type, both sides must be the same distinct type, and an operator == or operator != you define for the distinct type takes precedence over the borrow. Everything else — arithmetic, ordering — is deliberately absent: EntityId + EntityId is meaningless, so it doesn’t compile. Where an operation is meaningful, define it explicitly:

def operator + (a, b : Meters) : Meters => Meters(*a + *b)

let d = Meters(2.0) + Meters(3.0)   // Meters(5.0)

A distinct type keys a table when its underlying type can: the key hashes as the underlying value, and the nominal wall still holds, so table<EntityId; string> refuses a plain int key and a key of any other distinct type over int at compile time. Arrays, struct fields, tuple and variant fields all work as well.

3.18.3. Assignment across types

The wall between a distinct type and everything else is opened only by an operator you write. = and <- overload on the pair of types (see Functions), and every init or assignment of that pair - a local, a global, a struct field default, a field in Sfx(current = 0), a plain assignment - goes through it. Code that predates a distinct type, such as handle : SoundHandle = 0 and handle = 0, keeps compiling once the module defines the pair it uses:

typedef distinct SoundHandle = uint

def operator = (var dst : SoundHandle&; src : int) {
    dst !== SoundHandle(uint(src))
}

def operator != (h : SoundHandle; z : int) : bool => *h != uint(z)

struct Sfx {
    current : SoundHandle = 0       // the operator, at field-default time
}

def stop(var sfx : Sfx) {
    if (sfx.current != 0) {
        sfx.current = 0             // the operator again
    }
}

The raw form dst !== src inside the operator body is the built-in copy, so an overload on the same pair never recurses into itself.

3.18.4. Overloads

Because the mangled names differ, overloads on the distinct type and its underlying type coexist — this is the primary use case:

def describe(x : int) : string => "int {x}"
def describe(x : EntityId) : string => "entity {*x}"

3.18.5. Modules and Privacy

Distinct types are module-nominal: same-named distinct types in two modules are different types. typedef private distinct Foo = int limits visibility to the declaring module, like enum private.

3.18.6. Runtime Erasure

Distinct types exist only in the type system. At runtime — in the interpreter, JIT, AOT, and reflection — values carry their underlying type: print(id) prints 7, not EntityId(7), and RTTI reports the underlying type. The compile-time relabel is free in all three execution tiers.

3.18.7. C++ Integration

A C++ module can register a distinct type with DistinctTypeAnnotation and expose extern functions whose signatures carry it:

// struct NativeId { int32_t value; ... };  with das::cast/WrapType specializations
MAKE_DISTINCT_TYPE_FACTORY(NativeId, NativeId);
// in the module constructor:
addAnnotation(new DistinctTypeAnnotation("NativeId", makeType<int32_t>(lib), "NativeId"));
addExtern<DAS_BIND_FUN(nativeIdNext)>(*this, lib, "native_id_next", SideEffects::none, "nativeIdNext");

On the das side NativeId behaves exactly like a das-declared distinct type: constructed with NativeId(v), dereferenced with *, and never interchangeable with int.

See also

Type Aliases for transparent (non-nominal) type names, Datatypes for the workhorse types that can be made distinct, Constants and Enumerations for the other nominal value types.