Language Tour
A quick tour of Hew's syntax and features.
Functions
Functions are declared with fn. The last expression in a block is the return value — no return keyword needed (though you can use it for early exits). Primitive type names are lowercase: i64, f64, string, bool.
fn add(a: i64, b: i64) -> i64 {
a + b
}
fn greet(name: string) {
println(f"Hello, {name}!");
}
fn abs(x: i64) -> i64 {
if x < 0 { return -x; }
x
}Variables
let creates immutable bindings; var creates mutable ones. Immutable by default keeps code predictable.
fn main() {
let x = 42;
var counter = 0;
counter = counter + 1; // OK — counter is mutable
// x = 10; // compile error: x is immutable
_ = (x, counter);
}String interpolation
Prefix a string with f to embed expressions inside {} braces. A plain string does not interpolate.
fn main() {
let name = "world";
let n = 42;
println(f"Hello, {name}! The answer is {n}.");
}Control flow
Hew provides if/else, match, while, and for. if and match are expressions — they produce values.
fn main() {
let count = 5;
let status = if count > 0 { "active" } else { "idle" };
println(status);
}enum Shape {
Circle(f64);
Rect { w: f64; h: f64; }
}
fn main() {
let shape = Shape.Circle(3.14);
match shape {
.Circle(r) => println(f"radius {r}"),
.Rect { w, h } => println(f"{w} x {h}"),
}
}fn process_next() { println("tick"); }
fn main() {
var running = true;
var ticks = 0;
while running {
process_next();
ticks += 1;
if ticks >= 3 { running = false; }
}
var items: Vec<string> = Vec.new();
for item in items {
println(item);
}
}Structs, enums, and traits
Structs hold data; enums model alternatives (with optional payloads); traits define shared behaviour. impl Trait for Type attaches methods, and dyn Trait enables dynamic dispatch through vtables. Struct fields and enum variants are ;-separated. Trait methods use a Go-style named receiver — the first parameter whose type matches the impl target is the receiver, so there is no self keyword.
type Point {
x: f64;
y: f64;
}
enum Shape {
Circle(f64);
Rect { w: f64; h: f64; }
}
trait Greetable {
fn greet(self) -> i64;
}
impl Greetable for Point {
fn greet(self) -> i64 { 1 }
}
// Dynamic dispatch via vtable
fn call_greet(obj: dyn Greetable) -> i64 {
obj.greet()
}State machines
A machine defines a value type with named states, an events { ... } vocabulary, and compiler-checked transitions. The compiler verifies that every (state, event) pair is handled — an uncovered cell is a compile error, not a runtime surprise.
machine CircuitBreaker {
events { Success; Failure; Timeout; }
state Closed { failures: i64; }
state Open;
state HalfOpen;
on Failure: Closed => Open ;
on Timeout: Open => HalfOpen ;
on Success: HalfOpen => Closed { failures: 0 }
on Failure: HalfOpen => Open ;
default { state }
}Machines are values — pass them to functions, drive them from a local var, and match on their state. The step() method advances the machine by one event, mutating it in place (so the receiver must be a var). The caller constructs the start state.
machine CircuitBreaker {
events { Success; Failure; Timeout; }
state Closed { failures: i64; }
state Open;
state HalfOpen;
on Failure: Closed => Open ;
on Timeout: Open => HalfOpen ;
on Success: HalfOpen => Closed { failures: 0 }
on Failure: HalfOpen => Open ;
default { state }
}
fn main() {
var cb = CircuitBreaker.Closed { failures: 0 };
_ = cb.step(.Failure).expect("machine step failed"); // Closed -> Open
_ = cb.step(.Timeout).expect("machine step failed"); // Open -> HalfOpen
_ = cb.step(.Success).expect("machine step failed"); // HalfOpen -> Closed
match cb {
.Closed { failures } => println(f"healthy, {failures} failures"),
.Open => println("circuit open — failing fast"),
.HalfOpen => println("testing..."),
}
}Inside transitions, state.field accesses the source state's fields and event.field accesses event payload data. Guards with when enable data-dependent transitions; reenter opts a self-transition into re-entry.
machine TokenBucket {
events { Request; Replenish; }
state Allowing { tokens: i64; }
state Throttled;
on Request: Allowing => Allowing reenter when state.tokens > 1 { tokens: state.tokens - 1 }
on Request: Allowing => Throttled when state.tokens <= 1 ;
on Replenish: Throttled => Allowing { tokens: 5 }
default { state }
}See the State Machines page for the full guide — including actor integration, wildcard transitions, and CLI tooling.
Error handling
Hew uses Result<T, E> with the ? operator for error propagation — no exceptions. A function declares its failure edge with fails E; ? unwraps a result or sends its error along that edge.
fn divide(a: i64, b: i64) -> Result<i64, string> {
if b == 0 { Result.Err("division by zero") } else { Result.Ok(a / b) }
}
fn compute() -> i64 fails string {
let x = divide(10, 2)?; // unwrap or propagate
let y = divide(x, 3)?;
y
}Regex
Hew has built-in regex support. Use re"pattern" for regex literals. The design places regex patterns inside match arms — the standalone =~/!~ operators were removed in favour of pattern matching. No imports or external libraries needed. Regex match arms are parsed and type-checked today; codegen for them is in progress.
// NOTE: re"..." patterns are accepted by the type-checker but codegen is not
// yet complete. This example shows the intended syntax; it will compile
// without error once the regex codegen pass lands.
fn classify(line: string) -> string {
match line {
re"^(GET|POST|PUT|DELETE) " => "http request",
re"^//" => "comment",
_ => "other",
}
}
fn main() {
println(classify("GET /api/users HTTP/1.1")); // http request
println(classify("// a note")); // comment
}Actors
Actors are the core concurrency primitive. Each actor is an isolated unit of state with a private mailbox. It processes one message at a time — no data races by design. Message handlers use receive fn. Ordinary calls wait for completion and return Result<R, ActorError<E, C>>, including unit-returning handlers. Use fork to start work concurrently and await its task later. State fields are referenced by bare name.
actor Counter {
var value: i64 = 0;
receive fn increment(amount: i64) {
value = value + amount;
println(f"now {value}");
}
receive fn get_count() -> i64 {
value
}
}
fn main() {
let c = spawn Counter { value: 0 };
c.increment(5).expect("actor stopped"); // wait for completion
let n = c.get_count(); // request-reply -> Result<i64, ActorError>
match n {
.Ok(v) => println(v),
.Err(_) => println("counter unavailable"),
}
}Lambda actors
Lightweight, inline actors for simple workers. They capture variables from the enclosing scope and use completion-call syntax like named actors.
fn main() {
let factor = 3;
let multiplier = actor |x: i64| -> i64 {
x * factor
};
println(multiplier(7).expect("actor stopped")); // 21
stop(multiplier);
stopped(multiplier);
}Generators
Generator functions produce a sequence of values lazily using yield. Call a gen fn to get a generator value, then consume it with a for loop. Generator functions can take parameters; the body captures them and any outer locals it needs.
gen fn count_up() -> i64 {
yield 1;
yield 2;
yield 3;
}
fn main() {
for x in count_up() {
println(x);
}
}gen fn fibonacci(limit: i64) -> i64 {
var a = 0;
var b = 1;
while a < limit {
yield a;
let next = a + b;
a = b;
b = next;
}
}
fn main() {
for val in fibonacci(100) {
println(val);
}
}Supervisors
Supervisors manage actor lifecycles and automatic restart. The declaration pairs a restart strategy (one_for_one, one_for_all, rest_for_one) with a restart budget written as intensity: N within <window>, then lists the children.
actor Logger {
var level: i64 = 0;
receive fn log(msg: string) { println(msg); }
}
actor WorkerActor {
var id: i64 = 0;
receive fn work(n: i64) { println(n); }
}
supervisor AppSupervisor {
strategy: one_for_one;
intensity: 5 within 60s;
child logger: Logger { level: 3 } restart: permanent;
child worker: WorkerActor { id: 1 } restart: transient;
}Wire types
Wire types define network-serializable data with stable field tags. They enforce forward and backward compatibility — field numbers must never be reused. Each wire type gets generated to_json/from_json (and YAML) helpers, and the declaration's #[serial(...)] attribute controls the key-naming convention.
#[wire]
#[serial(case = "camelCase")]
type UserEvent {
user_id: u64 @1;
name: string @2;
email: Option<string> @3 optional;
role: i32 @4;
}
// Per-field key override:
#[wire]
type Event {
#[serial(key = "ref")]
internal_ref: string @1;
}