Type System

Types, generics, capabilities, and pattern matching.

Primitive types

Hew provides fixed-size numeric types, booleans, and characters:

TypeSizeDescription
i8, i16, i32, i641/2/4/8 bytesSigned integers
u8, u16, u32, u641/2/4/8 bytesUnsigned integers
isize, usizeplatformPointer-sized integers
f32, f644/8 bytesIEEE 754 floats
bool1 byteBoolean
char4 bytesUnicode scalar value

Additionally, String is an owned, heap-allocated UTF-8 string type available through the standard library.

Custom types

Structs and enums are declared with the struct and enum keywords. Struct fields are immutable by default; use var for mutable fields.

Structs and enums
struct Point { x: f64, y: f64 }struct MutableBuffer {    var data: Vec<u8>,    let capacity: usize,}enum Shape {    Circle(f64),    Rectangle(f64, f64),    Triangle { a: f64, b: f64, c: f64 },}

No intra-actor borrow checker

This is Hew's key differentiator from Rust. Rust's borrow checker prevents data races in arbitrary threaded code by forbidding multiple mutable references. But Hew actors are single-threaded — each actor processes one message at a time, so mutable aliasing cannot cause data races. Within an actor, values behave like any single-threaded language. The borrow checker applies only at actor boundaries via Send trait enforcement.

What Rust forbids but Hew allows inside actors
actor Example {    var data: Vec<i32> = Vec::new();    receive fn demo() {        // Multiple mutable references — ALLOWED (single-threaded)        let ref1 = self.data;        let ref2 = self.data;        ref1.push(1);        ref2.push(2);     // ok — no data race possible        // Aliasing mutable data — ALLOWED        var x = self.data;        var y = x;        x.push(3);        y.push(4);        // ok — same actor, sequential execution    }}

The only ownership constraint is at actor boundaries. When a value crosses a boundary (via <-, direct method calls, or spawn), it must be moved or cloned:

Boundary enforcement
receive fn forward(message: Message, target: ActorRef<Handler>) {    target <- message;       // message is MOVED to target    // message is now invalid — compile error if used}receive fn broadcast(message: Message, targets: Vec<ActorRef<Handler>>) {    for target in targets {        target <- message.clone();  // clone and send copy    }    // message still valid — we only sent clones}

RAII memory model

Hew uses per-actor ownership with RAII-style deterministic destruction. There is no garbage collector — no tracing GC, no generational GC, no GC pauses. Each actor has a private heap. When the owner goes out of scope, the value is dropped. When an actor terminates, its entire heap is freed.

Deterministic destruction (Drop trait)
struct FileHandle { fd: i32 }impl Drop for FileHandle {    fn drop(self) {        close(self.fd);  // runs exactly once, at scope exit    }}fn example() {    let file = File::open("data.txt");  // file owns the handle    process(file);}  // file.drop() runs here, closing the handle

Reference counting: Rc<T> provides non-atomic reference counting within a single actor (does not implement Send). Arc<T: Frozen> uses atomic reference counting for cross-actor sharing — the inner type must be Frozen (deeply immutable).

Rc and Arc
// Rc<T> — shared ownership within one actor (non-Send)let data: Rc<LargeStruct> = Rc::new(expensive_computation());let alias = data.clone();  // refcount++, no data copy// Arc<T> — cross-actor sharing (requires T: Frozen)let config: Arc<Config> = Arc::new(load_config());worker1 <- config.clone();  // atomic refcount++worker2 <- config.clone();  // both workers share same Config

Copy-on-send: messages between actors are deep-copied (the Erlang model). The sender retains ownership of the original; the receiver owns an independent copy. The compiler may optimize away copies when the sender provably does not use the value after send, and may use arena allocation for message handler temporaries.

Generics

Hew uses monomorphization for generics — specialized code is generated for each type instantiation, ensuring zero runtime overhead (like Rust). Type parameters use angle brackets. Bidirectional type inference flows types from calling contexts into lambda expressions, minimizing explicit annotations.

Generic functions
fn max<T: Ord>(a: T, b: T) -> T {    if a > b { a } else { b }}// Each call generates specialized machine code:max(42, 17);           // max$i32max("hello", "world"); // max$Stringmax(3.14, 2.71);       // max$f64

For complex bounds, use where clauses:

Where clauses
fn merge<K, V>(a: Map<K, V>, b: Map<K, V>) -> Map<K, V>where    K: Hash + Eq + Send,    V: Clone + Send,{    // implementation}

Traits can declare associated types that implementors must specify:

Associated types
trait Iterator {    type Item;    fn next(self) -> Option<Self::Item>;}trait Container {    type Item;    type Iter: Iterator<Item = Self::Item>;    fn iter(self) -> Self::Iter;    fn len(self) -> usize;}

Lambda parameter types are inferred from context via bidirectional type inference:

Lambda type inference
fn apply(f: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 { f(a, b) }// Lambda parameters infer i32 from apply's signaturelet sum = apply((x, y) => x + y, 3, 4);// Method chaining — types flow through each stepnumbers    .filter((x) => x > 0)           // x: i32 inferred from Vec<i32>    .map((x) => x * 2)              // x: i32, result: i32    .reduce((a, b) => a + b)        // a: i32, b: i32 from reduce signature

For cases where code size matters more than performance, use dyn Trait for dynamic dispatch via vtables:

Dynamic dispatch
// Static dispatch (default)fn render_static<T: Display>(item: T) {    print(item.display());}// Dynamic dispatch via vtablefn render_dynamic(item: dyn Display) {    print(item.display());}

Capability types

Hew uses marker traits to enforce safety at compile time. These are the foundation of Hew's data-race-free guarantee:

  • Send — Type can safely cross actor boundaries (via <- or direct method calls). Satisfied by value types, owned values transferred by move, Frozen types, and ActorRef.
  • Frozen — Type is deeply immutable and safely shareable across actors. Implies Send. Required for Arc<T> inner types.
  • Copy — Bitwise-copyable value types. Copied on assignment rather than moved. Only for value types and small fixed-size aggregates.
Capability bounds
fn broadcast<T: Send + Clone>(message: T, targets: Vec<ActorRef<Receiver>>) {    for target in targets {        target <- message.clone();    }}fn share<T: Frozen>(data: Arc<T>) -> Arc<T> {    data  // Safe to share without copying — T is deeply immutable}// Compiler auto-derives capabilities:// Point is Send + Frozen + Copy (all fields are)struct Point { x: f64, y: f64 }// Container<T> is Send if T is Sendstruct Container<T> { value: T }

Option<T> and Result<T, E>

Hew uses Option<T> for nullable values and Result<T, E> for recoverable errors. There are no exceptions — error handling is explicit and type-checked.

Option and Result
enum Option<T> {    Some(T),    None,}enum Result<T, E> {    Ok(T),    Err(E),}// The ? operator propagates errorsfn read_file(path: String) -> Result<String, IoError> {    let handle = open(path)?;   // Early return on error    let content = read(handle)?;    Ok(content)}

Pattern matching

The match expression is exhaustive — the compiler ensures every possible case is handled. Patterns can destructure structs, enums, tuples, and literals. Or-patterns and guards provide additional flexibility.

Pattern matching
match result {    Ok(value) => process(value),    Err(IoError::NotFound) => create_default(),    Err(e) => return Err(e),}match point {    Point { x: 0, y: 0 } => "origin",    Point { x, y } if x == y => "diagonal",    Point { x, y } => f"({x}, {y})",}// Or-patternsmatch direction {    North | South => "vertical",    East | West => "horizontal",}