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 are declared with the type keyword; enums with enum. Struct fields are immutable by default; use var for mutable fields.

Structs and enums
type Point {
    x: f64;
    y: f64;
}
enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle { a: f64; b: f64; c: f64; },
}

Full mutable freedom inside actors

This is Hew's key differentiator from Rust. Rust's borrow checker prevents data races in arbitrary threaded code by tracking mutable references. Hew actors are single-threaded — each actor processes one message at a time, so single-message processing guarantees data-race freedom. Within an actor, values behave like any single-threaded language. The compiler checks ownership only at actor boundaries via Send trait enforcement.

Mutable access inside actors
actor Example {
    let data: Vec<i32>;
    receive fn demo() {
        // Within an actor, mutable access is safe —
        // only one message is processed at a time
        data.push(1);
        data.push(2);
        let len = data.len();
        println(f"items: {len}");
    }
}

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
// Named actor: direct method calls
let handler = spawn Handler(0);
handler.process(message);         // message is sent to actor's mailbox
// Lambda actor: <- send operator
let worker = spawn (msg: i32) => { println(msg); };
worker <- 42;

RAII memory model

Hew uses per-actor ownership with RAII-style deterministic destruction. Memory is reclaimed deterministically — predictable latency, zero GC overhead. 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)
type FileHandle { fd: i32; }
impl Drop for FileHandle {
    fn drop(h: FileHandle) {
        close(h.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: sending a value to another actor is a move at the language level — the sender loses access after the send. At runtime, a deep copy is made into the receiver's private heap, ensuring complete isolation (the Erlang model). The compiler may optimize to a zero-copy transfer 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$i32
max("hello", "world"); // max$String
max(3.14, 2.71);       // max$f64

For complex bounds, use where clauses:

Where clauses
fn merge<K, V>(a: HashMap<K, V>, b: HashMap<K, V>) -> HashMap<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(it: Self) -> Option<Self::Item>;
}
trait Container {
    type Item;
    type Iter: Iterator<Item = Self::Item>;
    fn iter(c: Self) -> Self::Iter;
    fn len(c: 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 signature
let sum = apply((x, y) => x + y, 3, 4);
// Method chaining — types flow through each step
numbers
    .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 vtable
fn 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)
type Point { x: f64; y: f64; }
// Container<T> is Send if T is Send
type Container<T> { value: T; }

Option<T> and Result<T, E>

Hew uses Option<T> for nullable values and Result<T, E> for recoverable errors. Error handling is explicit and type-checked — every failure path is visible in the signature.

Option and Result
enum Option<T> {
    Some(T),
    None,
}
enum Result<T, E> {
    Ok(T),
    Err(E),
}
// The ? operator propagates errors
fn 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-patterns
match direction {
    North | South => "vertical",
    East | West => "horizontal",
}