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).

Functions
fn add(a: Int, b: Int) -> Int {
    a + b
}
fn greet(name: String) {
    println(f"Hello, {name}!");
}
fn abs(x: Int) -> Int {
    if x < 0 { return -x; }
    x
}

Variables

let creates immutable bindings; var creates mutable ones. Immutable by default keeps code predictable.

Variables
let x = 42;
var counter = 0;
counter = counter + 1;  // OK — counter is mutable
// x = 10;              // compile error: x is immutable

String interpolation

Prefix a string with f to embed expressions inside {} braces. The compiler infers the formatting for each type.

String interpolation
let name = "world";
let n = 42;
println(f"Hello, {name}! The answer is {n}.");

Control flow

Hew provides if/else, match, while, and for. All are expressions — they produce values.

if / else
let status = if count > 0 { "active" } else { "idle" };
match
match shape {
    Circle(r) => println(f"radius {r}"),
    Rectangle(w, h) => println(f"{w} x {h}"),
}
Loops
while running {
    process_next();
}
for item in items {
    println(item);
}

Structs, enums, and traits

Structs hold data; enums model alternatives (with optional payloads); traits define shared behaviour. impl blocks attach methods, and dyn Trait enables dynamic dispatch through vtables.

Structs and enums
type Point {
    x: f64;
    y: f64;
}
enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
}
trait Greetable {
    fn greet(g: Self) -> Int;
}
impl Greetable for Point {
    fn greet(p: Point) -> Int { 1 }
}
// Dynamic dispatch via vtable
fn call_greet(obj: dyn Greetable) -> Int {
    obj.greet()
}

State machines

A machine defines a value type with named states, typed events, and compiler-checked transitions. The compiler verifies that every state handles every event — missing transitions are a compile error, not a runtime surprise.

Declaring a machine
machine CircuitBreaker {
    state Closed   { failures: Int; }
    state Open;
    state HalfOpen;
    event Success;
    event Failure;
    event Timeout;
    on Failure: Closed   -> Open;
    on Timeout: Open     -> HalfOpen;
    on Success: HalfOpen -> Closed { failures: 0 }
    on Failure: HalfOpen -> Open;
    default { state }
}

Machines are values — store them in actors, pass them to functions, match on their state. The step() method advances the machine by one event, mutating it in place.

Using a machine
var cb = CircuitBreaker::Closed { failures: 0 };
cb.step(Failure);        // Closed → Open
cb.step(Timeout);        // Open → HalfOpen
cb.step(Success);        // HalfOpen → Closed
match cb {
    Closed { failures } => println(f"healthy, {failures} failures"),
    Open => println("circuit open — failing fast"),
    HalfOpen => println("testing..."),
}

Inside transitions, state accesses the source state's fields and event accesses event payload data. Guards with when enable data-dependent transitions.

State data, event payloads, and guards
machine TokenBucket {
    state Allowing  { tokens: Int; }
    state Throttled;
    event Request;
    event Replenish;
    on Request: Allowing -> Allowing 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. Functions return Result and use ? to unwrap or propagate errors to the caller.

Result and ?
fn divide(a: Int, b: Int) -> Result<Int, Int> {
    if b == 0 { Err(1) } else { Ok(a / b) }
}
fn compute() -> Result<Int, Int> {
    let x = divide(10, 2)?;   // unwrap or propagate
    let y = divide(x, 3)?;
    Ok(y)
}

Regex

Hew has built-in regex support. Use re"pattern" for regex literals, =~ to test for a match, and !~ to test for a non-match. No imports or external libraries needed.

Regex literals and matching
let email = "user@example.com";
// =~ returns true if the string matches the pattern
if email =~ re"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" {
    println(f"'{email}' is a valid email");
}
// !~ returns true if the string does NOT match
let code = "let x = 42;";
if code !~ re"^//" {
    println(f"'{code}' is code, not a comment");
}
// Works in conditions and control flow
let input = "GET /api/users HTTP/1.1";
if input =~ re"^(GET|POST|PUT|DELETE) " {
    println("HTTP request line");
}

Actors

Actors are the core concurrency primitive. Each actor is an isolated state machine with a private mailbox. It processes one message at a time — no data races by design. Message handlers use receive fn; internal methods use fn.

Actors
actor Counter {
    let value: Int;
    receive fn increment(amount: Int) {
        value = value + amount;
        println(f"now {value}");
    }
    receive fn get_count() -> Int {
        value
    }
}
fn main() {
    let c = spawn Counter;
    c.increment(5);                      // fire-and-forget
    let n = await c.get_count();         // request-response
    println(n);
}

Lambda actors

Lightweight, inline actors for simple workers. They capture variables from the enclosing scope automatically.

Lambda actors
let factor = 3;
let multiplier = spawn (x: Int) => {
    println(x * factor);
};
multiplier <- 7;   // prints 21

Generators

Generator functions produce a sequence of values lazily using yield. Consume them with for loops or call .next() manually. Actors can expose streaming data with receive gen fn.

Generators
gen fn count_up() -> Int {
    yield 1;
    yield 2;
    yield 3;
}
fn main() {
    for x in count_up() {
        println(x);
    }
}
Actor generators
actor Fibonacci {
    let limit: Int;
    receive gen fn sequence() -> Int {
        var a = 0;
        var b = 1;
        while a < limit {
            yield a;
            let next = a + b;
            a = b;
            b = next;
        }
    }
}
fn main() {
    let fib = spawn Fibonacci(limit: 100);
    for await val in fib.sequence() {
        println(val);
    }
}

Supervisors

Supervisors manage actor lifecycles and automatic restart. Strategies follow Erlang/OTP patterns: one_for_one, one_for_all, rest_for_one.

Supervisors
supervisor AppSupervisor {
    child logger: Logger
        restart(permanent);
    child worker: WorkerActor
        restart(transient)
        budget(5, 60.seconds)
        strategy(one_for_one);
}

Wire types

Wire types define network-serializable data with stable field tags. They enforce forward and backward compatibility — field numbers must never be reused. The compiler generates _to_json, _from_json, _to_yaml, and _from_yaml functions for every wire type.

Wire types with JSON/YAML serialization
#[json(camelCase)]
#[yaml(snake_case)]
wire type UserEvent {
    user_id: u64    @1;
    name:    String @2;
    email:   String @3 optional;
    role:    Role   @4;
}
wire enum Role {
    Admin;
    Member;
    Guest;
}
// Generated: UserEvent_to_json, UserEvent_from_json,
//            UserEvent_to_yaml, UserEvent_from_yaml
let json = UserEvent_to_json(42, "Alice", "alice@example.com", Role::Member);
// → {"email":"alice@example.com","name":"Alice","role":1,"userId":42}
// Per-field key override:
wire type Event {
    internal_ref: String @1 json("ref") yaml("ref");
}