Skip to Content
Cloth

Casts

Cloth has three cast forms — is for runtime kind checks, as for unsafe downcasts, and as? for safe downcasts that fall back to null. They appear after a value: value as Target, value is Target. Each also covers primitive conversions where the source and target are both primitive types.

Reference casts

value as Target (unsafe)

as produces a value of Target. At runtime the value’s hidden vtable header is compared against the target class’s vtable global; on mismatch the program aborts.

let speaker = new Speaker(); Greeter g = speaker; // upcast — always safe, no runtime check let back = g as Speaker; // downcast — vtable matches, returns the receiver back.greet();

as is the right choice when you know — for whatever reason the analyzer can’t see — that the cast will succeed. If you’re wrong, the program halts via abort() rather than continuing with a misinterpreted pointer.

value as? Target (safe)

as? returns Target? — a nullable target. On vtable match it yields the receiver; on mismatch it yields null. Combine with ?? to provide a fallback:

Greeter g = ...; Speaker? maybe = g as? Speaker; let s = maybe ?? new Speaker(); // fallback when g wasn't actually a Speaker

Because as? produces a nullable, the result must flow into a T? slot or be coalesced immediately. Binding it to a non-nullable let is rejected (S006).

value is Target (kind check)

is returns bool. It loads the same vtable header and compares it against the target’s vtable global:

if (g is Speaker) { let s = g as Speaker; s.greet(); }

The compiler does not smart-cast inside the if body — g still has its declared type. Use as (or as? + ??) inside the body when you want to use the value at the narrower type. Flow typing is on the future-work list.

Compile-time legality

Every cast is checked at the analyzer level. Illegal combinations fail with S01F InvalidCast. The accepted shapes:

SourceTargetVerdict
primitiveprimitiveOK (any combo; explicit cast = user accepts narrowing risk)
class Cinterface IOK iff C transitively implements I (upcast)
interface Iclass COK iff C transitively implements I (downcast)
interface I1interface I2OK; assignability honors the inheritance chain
class C1class C2OK when one extends the other (in either direction)
primitive ↔ referencerejected

“Transitively implements” means C either lists I directly in its -> ... clause, or lists some interface that extends I (possibly through several : hops). For example, if Polite : Greeter and class C -> Polite, then C is assignable to both Polite and Greeter, and a value of static type Polite is assignable to a Greeter slot via interface→ancestor-interface upcast.

Same-type, class→interface upcasts, and class→ancestor upcasts compile to no-ops at runtime — the underlying pointer is already valid. Class→descendant and class→sibling downcasts walk the receiver’s class lineage at runtime.

Class inheritance and the parent chain

Every class has a hidden vtable header at offset 0. The vtable carries (a) the parent class’s vtable pointer and (b) the slot table for any interface methods. as, as?, and is against a class target walk the receiver’s vtable up the parent chain, comparing each link against the target. The walk terminates either at the target (success) or at the chain’s null root (failure):

public class Animal () { public Animal {} public ~Animal {} } public class Dog () : Animal { public Dog {} public ~Dog {} public func bark(): string { return "woof"; } } let dog = new Dog(); Animal a = dog; // implicit upcast — no runtime work if (a is Dog) { // chain walk finds Dog at the head let back = a as Dog; // chain walk again, returns the receiver println(back.bark()); // "woof" } delete dog; let alone = new Animal(); Dog? optDog = alone as? Dog; // chain walk: Animal → null, no Dog → null result

Primitive conversions

When both source and target are primitive (i8/i16/i32/i64/u8/u16/u32/u64/f32/f64/bool/char/string/etc.), as performs the obvious conversion:

i64 wide = 70000; i32 narrow = wide as i32; // truncate i32 small = 7; f64 promoted = small as f64; // sitofp

Lossy primitive conversions (i64 → i32 truncating, f64 → f32 fptrunc, float → int fptosi) all require an explicit as. The analyzer does not silently narrow.

is on primitive types is rarely useful — primitives don’t have vtables and the answer is statically known — so is is generally reserved for class and interface receivers.