Skip to main content

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 the built-in owned, heap-allocated UTF-8 string type, and bytes is a mutable heap-allocated byte buffer.

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; }
}

Mutation stays local to an actor

Each actor processes one handler at a time, including across suspension. Its fields are unavailable to other actors, so ordinary mutation inside that actor does not race another handler. Ownership still matters inside an actor for affine resources, consuming calls, tasks, and deterministic cleanup; Send adds the check at actor boundaries. Start with Ownership and Affine Values if those terms are new.

Mutable access inside actors
actor Example {
    var data: Vec<i64> = Vec.new();
 
    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}");
    }
}

When a value crosses a boundary through an actor call, capture, or spawn, it must satisfy Send. Ordinary data arrives as a logical snapshot and remains usable by the sender. A sendable affine resource transfers its sole owner:

Boundary enforcement
actor Handler {
    var count: i64 = 0;
    receive fn process(msg: i64) { println(msg); }
}
 
fn main() {
    let message: i64 = 42;
 
    // Named actor: direct method calls
    let handler = spawn Handler { count: 0 };
    handler.process(message).expect("actor stopped"); // the actor receives a snapshot
    println(message);                                // the sender keeps ordinary data
 
    // Lambda actors use the same completion-call model
    let worker = actor |msg: i64| -> i64 { msg * 2 };
    println(worker(21).expect("actor stopped"));
    stop(handler);
    stopped(handler);
    stop(worker);
    stopped(worker);
}

Read Actor Isolation and Message Passing for snapshot, transfer, completion, and submission semantics.

Ownership and deterministic cleanup

Hew uses ownership and deterministic cleanup rather than a tracing garbage collector. Ordinary values release the storage they own when their lifetime ends. A #[resource] value adds a cleanup contract: its sole owner must either call close early or let Hew call it at scope exit.

Deterministic destruction (#[resource] with close)
#[resource]
type FileHandle {
    fd: i64;
}
 
impl FileHandle {
    fn close(consume self) {
        // runs exactly once, at scope exit (or explicit call)
    _ = self.fd;
    }
}
 
fn example() {
    let _file = FileHandle { fd: 1 };
    // _file.close() is called automatically when _file goes out of scope
}

No impl Drop: Hew does not support impl Drop for any type — the compiler rejects it. Deterministic cleanup is expressed with the #[resource] attribute and a close(consume self) method in a sibling impl block. The runtime calls close automatically at scope exit, or you can call it explicitly before then. Stdlib resource types like process.Child and channel handles follow the same idiom. Resources and linear values explains when to use #[resource] or #[linear].

Reference counting: Rc<T> shares an allocation within one actor. clone() creates another strong owner; downgrade() creates a Weak<T> that can break cycles. A weak handle's upgrade() returns Option<Rc<T>>. Strong cycles need weak back-edges because Hew has no tracing collector.

Shared values within an actor
fn main() {
    let shared = Rc.new(10);
    let alias = shared.clone();
    shared.set(20);
    assert(alias.get() == 20);
    let weak = shared.downgrade();
    assert(weak.upgrade().is_some());
}

Rc and Weak stay within their owning actor. Cross-actor communication uses owned messages: immutable string and byte backing can be retained, while mutable values preserve isolation through their copy or move semantics. Hew v0.9.3 does not expose a public Arc<T> API. Keep shared mutable state inside an actor and communicate with it through messages.

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 }
}
 
fn main() {
    // Each call generates specialized machine code:
    _ = max(42, 17);           // max$i64
    _ = max("hello", "world"); // max$string
}

For complex bounds, use where clauses:

Where clauses
trait Printable {
    fn print(self);
}
 
trait Cloneable {
    fn duplicate(self) -> Self;
}
 
fn process_pair<A, B>(a: A, b: B)
where
    A: Printable + Cloneable,
    B: Printable,
{
    let copy = a.duplicate();
    copy.print();
    b.print();
}

Traits can declare associated types that implementors must specify:

Associated types
// hew-sample: partial
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
import std.iter;
 
fn apply(f: fn(i64, i64) -> i64, a: i64, b: i64) -> i64 { f(a, b) }
 
fn main() {
    // Closures use |params| body. Parameters infer i64 from apply's signature.
    let sum = apply(|x, y| x + y, 3, 4);
    println(sum);
 
    // Types flow through each iterator adapter
    let numbers: Vec<i64> = Vec.new();
    let positives = iter.filter(numbers.into_iter(), |x| x > 0);
    let doubled = iter.map(positives, |x| x * 2);
    let result: Vec<i64> = iter.collect(doubled);
    _ = result;
}

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.fmt());
}
 
// Dynamic dispatch via vtable
fn render_dynamic(item: dyn Display) {
    print(item.fmt());
}

Capability types

These marker traits describe value sharing and transfer. For controlling access to files and other services, see authority confinement in Hew v0.9.3, which also checks required resource cleanup.

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. The compiler derives it through records, enum payloads, tuples, options, results, and collections. Ordinary data snapshots; admitted affine values transfer.
  • Frozen — Type is deeply immutable and safely shareable across actors. Implies Send.
  • Copy — Bitwise-copyable value types. Copied on assignment rather than moved. Only for value types and small fixed-size aggregates.
Capability bounds
// Capabilities follow the fields of an aggregate.
type Point { x: f64; y: f64; }
 
// Container<T> is Send if and only 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.

A bare expression statement cannot silently discard a Result with a possible error. Handle it with match, propagate the error with ?, or stop on failure with expect. When you deliberately do not need either outcome, write _ = actor.method();. The call still waits for its handler; only its returned result is ignored.

Option and Result
enum IoError { NotFound; PermissionDenied; }
 
// The ? operator propagates errors
fn parse_int(s: string) -> Result<i64, string> {
    let n = s.len();
    if n == 0 { Result.Err("empty input") } else { Result.Ok(n) }
}
 
fn compute(s: string) -> i64 fails string {
    let n = parse_int(s)?;   // unwrap or propagate
    n * 2
}
 
fn main() {
    // Option<T> — present or absent
    let opt: Option<i64> = Option.Some(42);
    match opt {
        .Some(v) => println(v),
        .None    => println("nothing"),
    }
 
    // Result<T, E> — success or failure
    match compute("hello") {
        .Ok(v)  => println(v),
        .Err(e) => println(f"error: {e}"),
    }
}

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
enum IoError { NotFound; PermissionDenied; }
enum Direction { North; South; East; West; }
enum Color { Red; Green; Blue; Named(string); }
 
fn describe(direction: Direction) -> string {
    match direction {
        .North | .South => "vertical",
        .East | .West   => "horizontal",
    }
}
 
fn label(c: Color) -> string {
    match c {
        .Red          => "red",
        .Green        => "green",
        .Blue         => "blue",
        .Named(name)  => name,
    }
}
 
fn handle(result: Result<i64, IoError>) -> Result<i64, IoError> {
    match result {
        .Ok(value)                     => Result.Ok(value * 2),
        .Err(IoError.NotFound)         => Result.Ok(0),
        .Err(IoError.PermissionDenied) => Result.Err(IoError.PermissionDenied),
    }
}