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. Theunsafekeyword may prefixTypefor unsafe casts, andconstmay 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
valuetotarget_typewith compile-time validation. - Applicable to numeric types, user-defined types with
op asoperators, 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 f80Constraints
- Types must have compatible memory layouts (size, alignment, padding), or must define a custom
op asoperator. - No runtime overhead unless a custom
op asoperator 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 errorConstraints
- 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
unsafeblock 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 pointerConstraints
- May lead to undefined behavior if types are incompatible.
- Requires explicit
unsafeannotation 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 *Tto*Tor 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
unsafeunless 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 downcastNull-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
NullCollapseExceptionif 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 castConstraints
- 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 asConstraints
- 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 castFFI Integration
Foreign Primitive Bindings
Helix maps foreign primitives to native types with identical memory layouts.
Example
// the helix ABI API format is still being finalizedCustom 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 logicMemory 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 asoperators for user-defined types.
Runtime Errors
NullCollapseException: Thrown when collapsing anullquestionable 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)}