I’d been sketching a machine keyword for months. Every actor I’ve written that handles more than two message types ends up with some combination of status fields and boolean flags, and the compiler sees none of it — just assignments. No state graph, no transition validation, no way to tell you that state Connecting can reach Disconnected but never Authenticated.
The machine declaration
Five new keywords landed together: machine, state, event, on, when. The idea is that a machine is a named state graph that lives inside an actor, with explicit states, events that trigger transitions, and guards that constrain when transitions are valid:
actor Connection {
addr: String;
retries: i32;
machine Lifecycle {
state Idle;
state Connecting;
state Connected;
state Disconnected;
event connect(host: String);
event disconnect;
event heartbeat_timeout;
on connect: Idle -> Connecting;
on connect: Disconnected -> Connecting
when retries < 5;
on disconnect: Connected -> Disconnected;
on heartbeat_timeout: Connected -> Disconnected;
}
}The on declarations are transition rules — on connect: Idle -> Connecting says “when the connect event arrives and the machine is in Idle, transition to Connecting.” The when clause is a guard: the transition only fires if the expression evaluates to true. Guards can reference actor fields directly — or with this.field when disambiguation is needed — which ties the machine’s behaviour to the actor’s actual state.
This is different from how most languages handle state machines. You don’t build a state machine by instantiating a builder or calling methods on a library type. The machine is a declaration — the compiler parses it, type-checks the guards, and (eventually) validates that the state graph is well-formed. Unreachable states, missing transitions for an event, guards that reference nonexistent fields — these are all compile-time errors, not runtime surprises.
First-class because actors need it
Actors without state machines eventually become state machines anyway, just bad ones. Match arms that check if is_connected, subtle bugs where a message arrives in a state nobody planned for, status fields that accumulate until you’re maintaining a hand-rolled FSM without any of the guarantees. (The “connected but not authenticated” problem shows up in roughly 100% of network actors.)
Making machines a language construct means the compiler can do exhaustiveness checking — if state Connecting doesn’t handle event disconnect, you hear about it at compile time. The state graph is right there in the AST, so tooling can render it without reverse-engineering your logic. And eventually the machine declaration should generate an optimized dispatch table directly.
I keep saying “eventually” because the honest status is: parsing and type checking work, codegen doesn’t. The parser builds a full MachineDecl AST node with states, events, transitions, and guard expressions. The type checker validates that guards type-check against the actor’s fields, that states and events referenced in transitions actually exist, and that transition rules don’t conflict. But the C++ MLIR codegen doesn’t emit anything for machines yet — they’re parsed, validated, and silently ignored during code generation. The runtime dispatch is still manual match on message types.
Duration literals
This one had been on the list since the spec went to v0.9.0. Hew now has duration literal suffixes: ns, us, ms, s, m, h. Each compiles to an i64 representing nanoseconds:
let timeout = 30s; // 30_000_000_000 nanoseconds
let interval = 100ms; // 100_000_000 nanoseconds
let precision = 50us; // 50_000 nanoseconds
let tick = 16ms; // 16_000_000 nanoseconds
let ttl = 2h; // 7_200_000_000_000 nanosecondsThe lexer handles the suffix — it sees an integer literal followed by one of the six suffixes and produces a DurationLiteral token with the value already converted to nanoseconds. No runtime conversion, no library call. 30s and 30_000_000_000ns produce the same i64. The type system doesn’t yet have a distinct Duration type — it’s just i64 all the way through. That’s a deliberate deferral. A proper Duration type with overflow checking and unit-safe arithmetic is planned, but shipping nanosecond-valued i64 literals now means people can write sleep(100ms) instead of sleep(100_000_000) immediately.
The suffix us is microseconds (not the Unicode “micro sign” because ASCII-only identifiers are a hill I will die on). Minutes are m and milliseconds are ms, which is unambiguous because a bare m after an integer isn’t a valid identifier start in any other context.
Duration literals interact naturally with the actor runtime. Mailbox overflow policies, supervision restart windows, heartbeat intervals — everything in the runtime that takes a time value takes nanoseconds as i64. So the literals just work:
actor Heartbeat {
interval: i64;
machine Health {
state Alive;
state Suspect;
state Dead;
event tick;
event timeout;
on timeout: Alive -> Suspect;
on timeout: Suspect -> Dead;
on tick: Suspect -> Alive;
}
}
fn main() {
let hb = spawn Heartbeat(interval: 5s);
}The hew machine CLI
hew machine is a subcommand that operates on machine declarations without compiling the full program. hew machine list shows all machines declared in a file. hew machine check validates the state graph — reachability, completeness, guard conflicts. hew machine dot exports a Graphviz (the graph visualization tool) dot file of the state graph, which you can pipe to dot -Tsvg for a visual diagram.
The validation in hew machine check is stricter than the type checker. The type checker validates syntax and types; hew machine check does graph analysis. It checks that every state is reachable from the initial state (first declared state), that every event is handled in at least one state, and that no two transitions from the same state on the same event have overlapping guards. That last one is the useful one — guards like when retries < 5 and when retries < 10 on the same state and event overlap, and the machine’s behaviour would depend on evaluation order.
Parsing, type checking, and the hew machine CLI all work on real files today. MLIR codegen hasn’t started, and the runtime doesn’t know about machine states yet — no state-aware scheduling, no automatic transition dispatch, no machine state in crash dumps. Once codegen lands, the machine declaration will be the dispatch logic, and writing a match arm by hand will be the escape hatch.