Skip to content

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. The unsafe keyword may prefix Type for unsafe casts, and const 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 to target_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 = 42
var 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, Upcast
var derived: *Derived = base as *Derived // Valid, Downcast with RTTI
var 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 block
var 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 upcast
var 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 is null.
  • Unsafe pointers bypass null safety checks.

Example

var x: i32? = 12
var y: i32 = x as i32 // Safe: x is non-null
x = null
var z: i32 = x as i32 // Valid, runtime exception: NullCollapseException
var i: unsafe *i32 = &null
var 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 = 10
var 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 TypeCompile-Time CostRuntime Cost
Static CastHigh (verification)None
Polymorphic CastMedium (RTTI setup)Low (RTTI check)
Unsafe CastNoneNone
Const CastLow (type check)None
Null-Safe CastLow (null check setup)Low (null check)
Custom Cast OperatorVariesVaries

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 a null 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)
}