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 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.
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.
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:
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.
#[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.
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.
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:
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:
// 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:
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:
// 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.
// 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.
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.
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),
}
}