traitMailer:fnsend!(&mutself, to:string, body:string) ->void# The signature says it all: this sends mail and reads the clock.fnwelcome!(email:string) ->void$Mailer+Clock:
mailer :=$.use(Mailer)
mailer.send!(email, "Welcome, joined at ${$.use(Clock).now()}")
See it run
One claim per card, the lines that prove it, and the full example one click away in the playground.
Effects you can review
The signature lists everything the function may touch: mail and the clock.
fnwelcome!(email:string) ->void$Mailer+Clock:# ← sends mail, reads the clock
mailer :=$.use(Mailer)
mailer.send!(email, "Welcome at ${$.use(Clock).now()}")
Elsewhere, only the body tells you it reads the clock or sends mail.
A test installs a fake mailer and a fixed clock for one block.
it("welcome mails the user at a fixed time, no mocks"):letmut outbox =Outbox { sent: [] }
$.with(Mailer=&mut outbox, Clock=epoch()):# ← fakes, for this block onlywelcome!("ada@example.com")
assert_equal(outbox.sent[0], "ada@example.com|Welcome at 1970-01-01T00:00:00Z", reason="body")
all! runs both calls at once and joins the results.
fnpage!(id:i32) ->string$Clock:let (profile, orders) =all!(fetch_profile(id), fetch_orders(id)) # ← both at once"${profile.name} has ${orders.len()} orders"
Without requirements, a function that sends mail and reads the clock looks
like any other, and a test must patch globals or pull in a mocking library to
check it. With them, the signature lists what the function touches, and the
test installs a fake mailer and a fixed clock for one block. Forgetting a
provider is a missing-requirement compile error, not a runtime surprise.
use std.time.{Clock, ManualClock, Timestamp}
traitMailer:fnsend!(&mutself, to:string, body:string) ->void# The signature says it all: this sends mail and reads the clock.fnwelcome!(email:string) ->void$Mailer+Clock:
mailer :=$.use(Mailer)
mailer.send!(email, "Welcome, joined at ${$.use(Clock).now()}")
structOutbox:
sent:List[string]
implMailerforOutbox:fnsend!(&mutself, to:string, body:string) ->void:self.sent.push("$to: $body")
tests:use std.testing.assert_equal
it("welcome sends one mail at the fixed time"):letmut outbox =Outbox { sent: [] }
clock :=ManualClock::new(Timestamp::from_unix_milliseconds(0))
$.with(Mailer=&mut outbox, Clock=clock):welcome!("ada@example.com")
assert_equal(outbox.sent, ["ada@example.com: Welcome, joined at 1970-01-01T00:00:00Z"], reason="one mail")
Run it with hd test. The playground has more
examples that each show one feature at work: property tests, typed errors,
exhaustive match, and all!.
Attach typed facts to declarations, and derive behavior from a type's
structure. @derive(Trait) generates an ordinary implementation from the
trait's template. A template is plain hd over the compiler's view of the
type's members, and it reads the facts it understands. Serialization,
equality, debug output, property-test generators, and error types all work
this way. So can your own library traits. There are no macros and no
codegen step: facts are typed values, and templates are checked like any
other code.
@derive(Serialize, Deserialize, Table)
structUser:@column("user_id")
id:i64
email:stringpubfnmain() ->void$Console:println("SELECT ${User::columns()} FROM users") # SELECT user_id, email FROM users
text :=encode(User { id:7, email:"ada@example.com" })
println(text) # {"id":7,"email":"ada@example.com"}
Serialize and Deserialize come from std.serde; JSON reads them, and so
does the host boundary. Table is a library trait. Its template describes
the members and reads the column fact, which only Table cares about:
use std.json.encode
use std.serde.{Serialize, Deserialize}
use std.structure.{Structure, Describer, Field, Variant}
structColumn:
name:stringfncolumn(name:string) ->Column:Column { name: name }
traitTable:fncolumns() ->stringimpl[T] TableforTbyStructure:fncolumns() ->string:letmut list =ColumnList { names:"" }
_ :=T::describe(&mut list)
list.names
structColumnList:
names:stringimpl[S] Describer[S] forColumnList:typeError=neverfnvariant(&mutself, v:Variant[S]) ->Result[void, never]:
.Ok(())
fnmember[F](&mutself, h:Field[S, F]) ->Result[void, never]:
name :=match h.info.facts.find::[Column]():
.Some(column) => column.name
.None=> h.info.name
separator :=ifself.names =="":""else:", "self.names ="${self.names}$separator$name"
.Ok(())
See Annotations for facts, derivation
blocks, and @error.
Serialization, schemas, property tests, and fake data from your types.
Typed facts and structural derive generate them in plain hd. No macros, no
codegen.
Effects you can review. A signature shows every capability a function
can touch, and tests swap providers instead of patching globals.
Sandboxed. WebAssembly, with host access only through requirements.
hd is judged by three pillars for agentic development, compared side by
side rather than folded into one score
(goals).
Correctness is a gate, not a pillar.
Cheap for one agent to get right. Elapsed time and tokens per
correct solution, and compile time is agent wait time. Every signature
is explicit, so the checker skips dependency bodies and an agent reads a
header, never a body. Checking is incremental, with a per-module cache
and early cutoff, and std is checked in advance. The targets: an
edit-to-check of a 10k-line package in 50 ms at the median, a cold check
in 1 s, an edit-to-test in 300 ms, and near-linear time on the
pathological cases. Measured on 2026-10-17: edit-to-check takes 25 ms
at the median (p95 27 ms), a cold check 111 ms, and edit-to-test 110 ms,
all within target. On the pathological cases, 7 of 10 stay within 2 s and
200 MB at 4x (the slowest, large-enum-match, takes 338 ms), but
large-enum-match grows with exponent 2.42 against 1.3, and
deep-blocks, deep-parens, and method-chain fail (nesting limit,
stack overflow). Diagnostics are compact, and hd check --format json
has a field for exact-edit fix-its that hd fix applies, but no corpus
mistake offers one yet. Output is deterministic. Tests sit in the same
file under tests:, and providers turn a fake clock or mailer into one
line.
Many agents on one machine. Check results and compiled code are
cached by content and shared across processes, and std is checked in
advance and built into the binary. A program reaches the host only
through the capabilities it is granted, with a time limit, so agents can
run untrusted code side by side.
Artifacts that are correct, then fast. The portable conformance
suite passes on two engines, V8 and wasmtime, with the same behaviour.
Time, randomness and I/O sit behind capabilities, so a test can fix
them. Property tests shrink failures, and only reachable code is
compiled into the Wasm module.
The goal metrics are scripts under bench/metrics
(pnpm run metrics). Daily results
show the targets by pillar, changes since the previous run, and archived
measurements. Timing changes of 10% or less are treated as runner noise.
hd is installed from source for now. You need the Rust toolchain
(rustup; the repository pins its version in
rust-toolchain.toml), git (used to fetch dependencies), and Node.js
(programs run on Node by default).
git clone https://github.com/HerringtonDarkholme/hd-lang
cd hd-lang
cargo install --path crates/hd_cli
This puts hd in ~/.cargo/bin. To run programs without Node, build with
the wasmtime engine, cargo install --path crates/hd_cli --features wasmtime,
and choose it with hd --engine wasmtime ... or HD_ENGINE=wasmtime.
hd build takes the same choice and tunes its Wasm for that engine.
Read the guide, take the
tour, and run hd help for the
commands (The hd Command). The
specification holds the exact rules, and the standard
library is in lib/std. To work on the compiler, see
crates/README.md. docs/ indexes the
compiler design, the open design questions, and the audit logs.