Changes
Added
- Record update. A record literal may end in
..base:
{ beats: st.beats + 1, ..st }takes the listed fields from the literal
and every other field fromst, and hasst's type, so an update can
neither add (C0010) nor remove fields.{ ..base }alone is an error
(C0060), not a copy; the base is written once, last (P0008). Lowers to
an Erlang map update. The demos' actor states are now record aliases
read by field and rebuilt with..st. - Type aliases.
type alias Name<params> = Tnames a type expression
— a record, a tuple, a function type — and every use expands to it, so
an alias has no identity: it is transparent to unification, the wire
check, the audit format, the IR, and codegen. A tool's argument record
and the handlers that implement it now name the shape once (the demos
do:LogArgsand friends). A recursive alias is C0059 andopaqueon
an alias is P0007;pub type aliasexports it and an importer sees the
expansion. - Sequencing with
;.a; brunsafor its effects and evaluates
tob; it is sugar forlet _ = a in b, except thatamust be()
(C0058 otherwise), so a result is never dropped by accident. Right-
associative and looser than every operator; bodies remain single
expressions. The demos and guides use it for effect sequencing. Optionis predeclared.Option<a> = Some(a) | Noneis seeded like
Next, soSome(1)and amatchoverSome/Noneneed no declaration;
a usertype Option<a> = …still shadows it.Liststays a bare type
name until list literals and patterns exist.- Irrefutable patterns in
let. The binder is a pattern:
let Config(clock, period) = config in …,let (a, b) = pair in …, and
let _ = e in …all work, replacing the one-armmatchidiom; a_
binder emits_ = Exprin Erlang, so sequencing an effect no longer
needs an invented name. A plain
name still generalises; a destructuring pattern binds monomorphically and
must cover every value of the bound type — a refutable one (Some(x), a
literal) is a compile error (C0057). Lowers to a one-arm match, so the IR
and codegen are unchanged.
Changed
- Actor handlers and
initno longer carry return types. A handler's
outcome is alwaysNext<State>andinitalways returns the state, both
fixed by the actor'sstate:field, so the grammar drops the slot:
handle Tick, st ! {…} = …andinit: fn(c: Cfg) ! {…} = …. Writing a
→ …there is now a parse error (P0006) with a removal hint. The IR
pretty-printer prints the new form; demos, fixtures, and docs follow. - Built-in effect heads need no declaration.
Tool,Send,Await,
Spawn,Schedule, andExnare pre-declared likeInstall,
Supervise,Stand, andClock, so a module no longer opens with
effect Tool<t>before its rows may name a tool call or a message send.
A declaration of a built-in head is accepted with a warning (C0056) and
otherwise ignored; user heads (effect Audit<t>) are declared as before.
The IR pretty-printer no longer synthesises declarations for built-in
heads. The demos, fixtures, and docs drop the redundant lines.
Install
Download the archive for your platform below and extract it; each one
carries three binaries:
hird— the compiler CLI (check,build,run,emit-ast,emit-effect-graph)hird-lsp— the language server (diagnostics, hover, go-to-definition)hird-mcp— the MCP server exposing compiler introspection to LLM agents
Or build from source with Rust 1.97+:
cargo install --git https://github.com/no-materials/hird hird-cliCompiling and running Hirð programs (hird build, hird run) needs
Erlang/OTP on PATH; hird check works without it.
See the README to get started.