Skip to main content

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.

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

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

String interpolation
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.

if / else
fn main() {
    let count = 5;
    let status = if count > 0 { "active" } else { "idle" };
    println(status);
}
match
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}"),
    }
}
Loops
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.

Structs and enums
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.

Declaring a machine
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.

Using a machine
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.

State data, event payloads, and guards
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.

Result and ?
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.

Regex literals and matching (illustrative — regex match codegen 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.

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

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

Generators
gen fn count_up() -> i64 {
    yield 1;
    yield 2;
    yield 3;
}
 
fn main() {
    for x in count_up() {
        println(x);
    }
}
Parameterized generator
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.

Supervisors
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 types with JSON/YAML serialization
#[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;
}