Type System
Types, generics, capabilities, and pattern matching.
Primitive types
Hew provides fixed-size numeric types, booleans, and characters:
| Type | Size | Description |
|---|---|---|
i8, i16, i32, i64 | 1/2/4/8 bytes | Signed integers |
u8, u16, u32, u64 | 1/2/4/8 bytes | Unsigned integers |
isize, usize | platform | Pointer-sized integers |
f32, f64 | 4/8 bytes | IEEE 754 floats |
bool | 1 byte | Boolean |
char | 4 bytes | Unicode 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.
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.
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:
// 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.
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 handleReference 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<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 ConfigCopy-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.
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$f64For complex bounds, use 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:
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:
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 signatureFor cases where code size matters more than performance, use dyn Trait for dynamic dispatch via vtables:
// 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,Frozentypes, andActorRef. - Frozen — Type is deeply immutable and safely shareable across actors. Implies
Send. Required forArc<T>inner types. - Copy — Bitwise-copyable value types. Copied on assignment rather than moved. Only for value types and small fixed-size aggregates.
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.
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.
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",
}