Cast Expressions
This document specifies the syntax, semantics, and implementation details of cast expressions in the Helix programming language. It ensures consistent behavior across implementations and aligns with type safety and performance requirements. It covers all aspects of cast expressions, notwithstanding static, polymorphic, unsafe, const, pointer, and null-safe casts, as well as user-defined cast operators, generic type casting, and Foreign Function Interface (FFI) integration.
Syntax
expression
“as”type
expression
: The value to convert, of any valid Helix type.type
: The target type, including primitives, structs, classes, pointers, or interfaces. Theunsafe
keyword may prefixType
for unsafe casts, andconst
may be used for mutability adjustments.
Cast Expression Types
Static Casts
Static casts perform compile-time verified conversions between compatible types, analogous to C++ static_cast
.
Syntax
value
“as”target_type
Semantics
- Converts
value
totarget_type
with compile-time validation. - Applicable to numeric types, user-defined types with
op as
operators, or upcasts in inheritance hierarchies. - Fails at compile time if types are incompatible.
Example
var x: i64 = 42var y: f80 = x as f80 // Valid, Converts i64 to f80
Constraints
- Types must have compatible memory layouts (size, alignment, padding), or must define a custom
op as
operator. - No runtime overhead unless a custom
op as
operator is invoked.
Polymorphic Casts
Polymorphic casts handle conversions within inheritance hierarchies, supporting runtime type checking via Runtime Type Information (RTTI).
Syntax
pointer
“as”target_pointer_type
Semantics
- Upcasts (derived to base) are resolved at compile time and are always safe.
- Downcasts (base to derived) require RTTI and may throw a
CastException
. - RTTI metadata is generated for classes with virtual functions.
Example
default class Base { fn identify() -> string { return "Base" }}
default class Derived derives Base { override fn identify() -> string { return "Derived" }}
default class Another derives Base { fn function() -> string { return "Custom Methods" }}
var obj: *Derived = std::Memory::new<Derived>()
var base: *Base = obj as *Base // Valid, Upcastvar derived: *Derived = base as *Derived // Valid, Downcast with RTTIvar another: *Another = base as *Another // Invalid, Compile-time error
Constraints
- Requires classes with virtual functions for RTTI.
- Downcasts failing RTTI checks throw a
CastException
.
Unsafe Casts
Unsafe casts reinterpret the bits of a value as a different type, bypassing compile-time checks.
Syntax
value
“as” “unsafe”target_type
Semantics
- Performed within an
unsafe
block for value types or with out an unsafe block for pointer types. - No compile-time verification of type compatibility.
- Programmer is responsible for ensuring memory layout compatibility.
Example
unsafe { var x: i64 = 42 var y: f80 = x as unsafe f80 // Valid, unsafe Non-Pointer Cast, requires careful use and unsafe block}
var ptr: *i64 = std::Memory::new<i64>(100)var unsafe_ptr: *f80 = ptr as unsafe *f80 // Valid, unsafe Pointer Cast, requires careful use, but does not require an unsafe blockvar void_ptr: *void = unsafe_ptr as unsafe *void // Valid, unsafe Pointer Cast to void pointer
Constraints
- May lead to undefined behavior if types are incompatible.
- Requires explicit
unsafe
annotation to prevent accidental misuse.
Const Casts
Const casts modify the mutability of pointer types.
Syntax
pointer
“as” (”!”)? “const”target_pointer_type
Semantics
- Converts a
const *T
to*T
or vice versa. - Issues a compile-time warning to ensure review.
- Does not cause undefined behavior but requires writable memory.
Example
fn modify(const x: *i64) { var mutable_ref: *i64 = x as !const *i64 // Valid, Const cast *mutable_ref = 100 // Modifies memory
var const_ref: const *i64 = mutable_ref as const *i64 // Back to const *const_ref = 200 // Invalid, causes compile-time error}
Constraints
- Applicable only to pointer types.
- Memory must be writable to avoid runtime error.
Pointer Casts
Pointer casts convert between pointer types, respecting inheritance and memory layout. Very similar to Polymorphic Casts.
Syntax
pointer
“as”target_pointer_type
Semantics
- Upcasts are safe and resolved at compile time.
- Downcasts require
unsafe
unless RTTI is available. - Memory layout compatibility is verified for non-polymorphic casts.
Example
default class C1 { var x: i32 }default class C2 derives C1 { var y: i32 }
var y: *C2 = std::Memory::new<C2>()var upcast: *C1 = y as *C1 // Valid, Safe upcastvar z: *C1 = std::Memory::new<C1>()var downcast: *C2 = z as unsafe *C2 // Valid, Unsafe downcast
Null-Safe Casts
Null-safe casts integrate with Helix’s questionable type system (?
).
Syntax
value
“as”target_type
Semantics
- Verifies at runtime that a questionable type is not
null
. - Throws
NullCollapseException
if the value isnull
. - Unsafe pointers bypass null safety checks.
Example
var x: i32? = 12var y: i32 = x as i32 // Safe: x is non-null
x = nullvar z: i32 = x as i32 // Valid, runtime exception: NullCollapseException
var i: unsafe *i32 = &nullvar o: unsafe *f80 = i as unsafe *f80 // Valid, no null checks, same as a unsafe pointer cast
Constraints
- Regular pointers cannot hold
&null
. - Unsafe pointers disable null safety, requiring manual validation.
User-Defined Cast Operators
Custom cast behavior is defined using the op as
operator.
Syntax
class Type { fn op as (self) -> TargetType { // Conversion logic }}
Semantics
- Allows types to specify conversion to other types.
- Supports multiple overloads for different target types.
- Inlined by the compiler for performance.
Example
default class MyType { var x: i32 fn MyType(self) { self.x = 1 } fn op as (self) -> i32 { return self.x }}
var f: MyType = MyType()var g: i32 = f as i32 // Uses op as
Constraints
- Must be defined within the type’s scope.
- Target type must be explicitly specified.
Generic Type Casting
Generic casts are supported through type constraints.
Syntax
interface <U> CastableTo { fn op as(self) -> U
where { // NOTE: This syntax is undecided and will change in the future, this is only a placeholder || self as U }}
fn <T, U> convert(value: T) -> U where T impl CastableTo<U> { return value as U}
Semantics
- Resolves casts at compile time based on instantiated types.
- Ensures type safety via trait constraints.
Example
var x: i64 = 10var y: f80 = convert<i64, f80>(x) // Constrained cast
FFI Integration
Foreign Primitive Bindings
Helix maps foreign primitives to native types with identical memory layouts.
Example
// the helix ABI API format is still being finalized
Custom FFI Types
Custom FFI types require explicit memory layout specifications.
Example
var helix_struct: HelixStruct = HelixStruct { x: 1, y: 2.0 }var c_struct: CStruct = helix_struct as CStruct // Requires matching layout or conversion logic
Memory Layout Verification
The compiler verifies:
- Size and alignment compatibility.
- Padding and packing rules.
- Endianness for cross-platform support.
Performance Characteristics
Cast Type | Compile-Time Cost | Runtime Cost |
---|---|---|
Static Cast | High (verification) | None |
Polymorphic Cast | Medium (RTTI setup) | Low (RTTI check) |
Unsafe Cast | None | None |
Const Cast | Low (type check) | None |
Null-Safe Cast | Low (null check setup) | Low (null check) |
Custom Cast Operator | Varies | Varies |
The compiler optimizes casts by inlining operations and eliminating redundant checks.
Error Handling
Compile-Time Errors
- Invalid static casts (e.g., incompatible types).
- Missing
op as
operators for user-defined types.
Runtime Errors
NullCollapseException
: Thrown when collapsing anull
questionable type.CastException
: Thrown for failed polymorphic downcasts.
Runtime Exceptions are quite expensive, so they should be avoided in performance-critical code. Use static casts or ensure type safety through design.
Example
try { var x: i32? = null var y: i32 = x as i32 // Throws NullCollapseException} catch (e: CastException) { std::print("Error: {}", e.message)}