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 and enums are declared with the struct and enum keywords. Struct fields are immutable by default; use var for mutable fields.
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.
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:
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.
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 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: 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.
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$f64For complex bounds, use 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:
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:
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 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 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,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)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.
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.
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",}