Language Specification
A practical guide to current Hew syntax and semantics.
This guide introduces the current language with examples checked by the compiler. The released language specification defines the detailed contracts. The grammar explorer provides a generated syntax view for tooling.
How to read the grammar
The canonical grammar is written in a compact EBNF style. Use the examples and notes in each section for intent, then use the grammar for exact syntax.
| Form | Meaning | Example |
|---|---|---|
"literal" | Exact token text | "actor" |
RuleName | Reference to another grammar rule | expression |
A | B | Choose one alternative | "type" | "enum" |
A? | Optional item | Visibility? |
A* / A+ | Zero-or-more / one-or-more | Attribute* |
{ A } | Repeated item in ISO EBNF sections | { Stmt } |
The generated syntax view omits precedence, lexer metadata and type rules. Use the compiler to check a program and the language specification for its semantics.
Syntax map
Hew is centered on typed values, actors, structured concurrency, explicit wire formats, and a small set of expression forms. Current syntax highlights include type aliases, suspending function calls, generator functions, receive handlers, state machines, f-strings, byte strings, char and regex literals, map literals, and declarative supervisor policies.
| Area | Use it for | Starting rules |
|---|---|---|
| Items | Top-level declarations in a module | source_file |
| Types | Named data, enums, functions, collections, actor refs | struct_declaration, _type |
| Actors | Message-driven state and request/response handlers | actor_declaration |
| Concurrency | Scoped tasks, select and concurrent calls | scope_expression, select_expression |
| Machines | Finite-state models with guarded transitions | machine_declaration, machine_transition |
| Interchange | Stable serialized schemas | attribute, struct_field |
Programs and modules
A .hew file is a module. Top-level items may be annotated and can be private, public, or package-visible. Imports support dotted module paths, explicit grouped symbols, and string imports for external/module-loader integrations.
import std.net;
pub type Packet {
id: u64;
payload: Vec<u8>;
ttl: u8;
}Declarations
Hew separates compile-time constants, value bindings, mutable bindings, named types, functions and generator functions. Any function may suspend when it calls a suspending operation. let accepts patterns, which keeps destructuring consistent with match.
import std.math;
const DEFAULT_LIMIT: i64 = 100;
type Point { x: f64; y: f64; }
enum Shape {
Circle(f64);
Rectangle { width: f64; height: f64; }
Unit;
}
fn distance(a: Point, b: Point) -> f64 {
math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y))
}
fn describe(id: u64) -> string {
f"user-{id}"
}
gen fn ids() -> i64 {
var next: i64 = 0;
loop {
yield next;
next += 1;
}
}Types and traits
Types are statically known and inferred bidirectionally. Built-ins cover numeric scalars (i8–i64, u8–u64, f32/f64), bool, char, string, bytes, duration, tuples, arrays, slices, function types, raw pointers, trait objects, actor references, tasks, generators, streams, sinks, and collection types.
| Category | Examples | Notes |
|---|---|---|
| Fixed-width numerics | i8…i64, u8…u64, f32, f64 | The int/uint aliases were removed in v0.5 — use i64/u64. |
| Actor-safe references | A | Sendable handle used to message an actor. |
| Shared references | Rc<T>, Weak<T> | Strong and weak ownership within one actor. |
| Async data | Task<T>, Stream<T>, Sink<T> | Used by scopes, generators, and actor APIs. |
Traits and implementations
Traits define behavior contracts and associated types. Implementations bind a trait to a concrete type. Marker traits such as Send, Frozen, and Copy are central to actor-boundary safety.
trait Summary {
fn summarize(it: Self) -> string;
}
type Article {
title: string;
body: string;
}
impl Summary for Article {
fn summarize(it: Article) -> string {
f"{it.title}: {it.body}"
}
}Expressions and operators
Most control forms are expressions. Operator precedence is encoded in the grammar from timeout expressions down through postfix calls, indexing, field access, and error propagation. Actor sends are method calls (actor.handler(msg)), not an operator.
| High to low | Operators/forms | Meaning |
|---|---|---|
| Postfix | ?, .field, (args), [index] | Error propagation, member access, calls, indexing |
| Unary | !, -, ~, await | Negation, bitwise not, await |
| Math and bitwise | * / % + - << >> & ^ | | Arithmetic, shifts, bit operations |
| Comparison | .., ..=, <, >, ==, is | Ranges, ordering, equality, type/pattern test |
| Logical | &&, || | Boolean composition |
| Concurrency | | after | Timeout combinator on an awaited expression |
Closures and spawning
Closures use pipe syntax: |params| body. The arrow closure form (params) => body was removed. Lambda actors are written actor |params| { body }.
import std.iter;
actor Cache {
receive fn lookup(key: string) -> string { "" }
}
// A select deadline can bound an ordinary actor call.
actor Client {
let cache: Cache;
receive fn fetch(key: string) -> string {
select {
reply from cache.lookup(key) => reply handle e { "unavailable" },
after 500ms => "timeout",
}
}
}
fn main() {
let numbers: Vec<i64> = Vec.new();
let doubled = iter.map(numbers.into_iter(), |n: i64| n * 2);
let result: Vec<i64> = iter.collect(doubled);
_ = result;
let worker = actor |msg: string| {
println(f"received {msg}");
};
worker("hello").expect("worker stopped");
stop(worker);
stopped(worker);
}Statements and control flow
Blocks can end with an expression. if, if let, and match can be used as statements or expressions, and loops support labels with @label. defer registers cleanup work for scope exit.
type Item { done: bool; }
fn main() {
let count = 0;
let label = if count == 0 { "empty" } else { "ready" };
_ = label;
let items: Vec<Item> = [Item { done: true }];
var found: Option<Item> = .None;
@scan: for item in items {
if item.done {
found = .Some(item);
break @scan;
}
}
println(found.is_some());
}
enum IoError { NotFound; }
fn with_file(_path: string) -> Result<string, IoError> {
// defer runs exactly once at scope exit
defer {
// cleanup here
};
Result.Err(IoError.NotFound)
}Pattern matching
Patterns appear in match arms, for bindings, and let destructuring. They cover wildcards, literals, bindings, constructor patterns, struct patterns, tuple patterns, and or-patterns.
fn use_val(v: i64) { println(v); }
type Point { x: f64; y: f64; }
fn main() {
let result: Result<Option<i64>, string> = Result.Ok(Option.Some(42));
match result {
.Ok(.Some(value)) => use_val(value),
.Ok(.None) => println("empty"),
.Err(e) => println(f"error: {e}"),
}
let cursor = Point { x: 1.0, y: 2.0 };
let Point { x, y } = cursor;
println(f"{x} {y}");
}Actors and supervisors
Actors own their mutable state and communicate by messages. All actors are isolated by definition; there is no separate isolated keyword. receive fn declares handlers, and handlers with return types are request/response calls.
actor Counter {
var count: i64 = 0;
mailbox 256 overflow block;
receive fn increment(n: i64) {
count += n;
}
receive fn get() -> i64 {
count
}
}
fn main() {
let counter = spawn Counter { count: 0 };
counter.increment(10).expect("actor stopped");
match counter.get() {
.Ok(current) => println(current),
.Err(_) => println("ask failed"),
}
}Supervisors
Supervisors describe restart topology. The strategy and the restart budget (intensity: N within <window>) are supervisor fields, while each child chooses a restart policy with restart:.
actor DatabaseActor {
var shard: i64 = 0;
receive fn query(sql: string) -> string { "" }
}
actor CacheActor {
var capacity: i64 = 0;
receive fn get(key: string) -> string { "" }
}
actor MetricsActor {
var port: i64 = 0;
receive fn emit(name: string) { println(name); }
}
supervisor AppSupervisor {
strategy: one_for_one;
intensity: 5 within 30s;
child db: DatabaseActor { shard: 1 } restart: permanent;
child cache: CacheActor { capacity: 1024 } restart: transient;
child metrics: MetricsActor { port: 9090 } restart: temporary;
}Structured concurrency
scope creates a boundary that joins all child work before it exits. Inside it, let name = fork expr; runs a child task bound to name, and scope within duration { ... } bounds the work with a deadline. select waits for the first branch and cancels the rest. await fork (a(), b()) starts both computations and collects their results. Ordinary function and actor calls suspend automatically when their implementation needs to wait.
fn load_user(id: u64) { println(f"user-{id}"); }
fn load_posts(id: u64) { println(f"posts-{id}"); }
actor Cache { receive fn get(key: string) -> string { "" } }
actor Origin { receive fn get(key: string) -> string { "" } }
actor ProfileService { receive fn profile(id: u64) -> string { "" } }
actor StatsService { receive fn stats(id: u64) -> string { "" } }
// scope + fork: parallel tasks inside an actor handler
actor Service {
receive fn handle(id: u64) {
scope {
let user_t = fork load_user(id);
let posts_t = fork load_posts(id);
await user_t;
await posts_t;
}
}
}
// select: race actor replies against a timeout (inside an actor handler)
actor Resolver {
let cache: Cache;
let origin: Origin;
receive fn resolve(key: string) -> string {
let key2 = key.clone();
let chosen = select {
cached from cache.get(key) => cached handle e { "unavailable" },
fresh from origin.get(key2) => fresh handle e { "unavailable" },
after 100ms => "fallback",
};
chosen
}
}
// join: wait for all branches (inside an actor handler)
actor Aggregator {
let profile_svc: ProfileService;
let stats_svc: StatsService;
receive fn aggregate(id: u64) {
let (profile, stats) = await fork (
profile_svc.profile(id),
stats_svc.stats(id),
);
println(f"{profile.expect("profile service stopped")} {stats.expect("stats service stopped")}");
}
}State machines
Machine declarations model finite-state workflows as named states, events, guarded transitions, and optional default handling. They are useful for protocols and lifecycle-heavy services where the valid transition set should be visible in the type-level source.
machine Connection {
events {
Connect { peer: string; }
Disconnect;
}
state Closed;
state Open { peer: string; }
on Connect: Closed => Open when event.peer.len() > 0 { peer: event.peer }
on Disconnect: Open => Closed ;
default { state }
}Wire types
Wire types define stable serialized data. Field numbers are compatibility contracts: never reuse a removed tag; reserve it. Per-field JSON/YAML names override type-level naming attributes.
#[wire]
#[serial(case = "camelCase")]
type User {
name: string @1;
email: string @2;
age: Option<u32> @3 optional;
role: i32 @4 since 2;
#[serial(key = "id")]
user_id: u64 @5;
reserved @6, @7;
}FFI and unsafe code
FFI is explicit and unsafe by default. Declare imported functions in an extern block, then put validation and resource handling in safe Hew wrappers.
extern "C" {
fn malloc(size: usize) -> *mut u8;
fn free(ptr: *mut u8);
fn printf(fmt: *const u8 ..) -> i32;
}
fn allocate(size: usize) -> *mut u8 {
unsafe { malloc(size) }
}Literals and lexical rules
Integer literals support decimal, hexadecimal, octal, and binary forms with underscores. String forms include normal strings, raw strings, byte strings, character literals, f-strings with expression interpolation, and regex literals with re"...".
fn main() {
let dec = 1_000_000;
let hex = 0xFF;
let oct = 0o755;
let bin = 0b1010;
let timeout: duration = 500ms;
let raw = r"no escapes here";
let bytes = b"hello";
let newline = '
';
let count = 42;
let message = f"count is {count + 1}";
let byte_count = bytes.len();
_ = (dec, hex, oct, bin, timeout, raw, byte_count, newline, message);
}Generated syntax grammar
Explore the grammar and railroad diagrams, or download the generated EBNF.