Two sites, one repository, one deploy. Both are served from spec.raku.online (GitHub Pages, custom domain), and in both every example runs live in your browser via the same WebAssembly build of Raku++ that powers the raku.online playground.
| Site | URL | What it is |
|---|---|---|
| Spec | / |
The readable feature guide — one page per feature, prose plus runnable examples. 129 pages. |
| Rules | /rules/ |
The exhaustive reference: every operator filed by precedence level, and every type with its hierarchy, signatures and return types. 536 pages. |
Where Roast is a conformance test suite and docs.raku.org documents the library, these sites pin down last-mile behaviour: what a construct does, what it returns, and where this implementation and Rakudo part company.
Every generator here is written in Raku and run by rakupp; the sites are
previewed with rakus, the Raku++ static file server. Building the spec is
itself an act of dogfooding the interpreter it documents.
Nothing on either site is retyped from somewhere else. Three sources, each answering a different question — and one source deliberately not used.
The official documentation (a local Raku/doc checkout) is the authority on
what the language contains.
doc/Language/operators.rakudoccarries the precedence ladder as a table — 26 levels, tightest to loosest, each with its associativity — and is organised by that ladder: every operator is documented under a=head1 <Level> precedencesection. So an operator's precedence comes from the document's own structure rather than from anyone's inference.doc/Type/*.rakudocgives each type its=TITLE/=SUBTITLE, its declaration line (class Int is Cool does Real { }— the hierarchy), and for every routine a=head2heading, an indented block of signatures including-->return types, and a prose statement of what it does.- Both carry examples annotated with the output they are expected to produce:
say 1 + 1; # OUTPUT: «2».
Raku++ itself, probed. Each construct is fed to the interpreter in a minimal
use and classified by whether it parses. A type error still proves the spelling
is known; only a parse error does not. That is what the "Raku++ parses this" chip
and the gap badge mean — measured, never asserted.
Rakudo, as an oracle. Not read — run. Every example is executed by it, and its answer is treated as what the language actually does.
An earlier version of tools/inventory.raku parsed
rakudo/src/Perl6/Grammar.nqp. It was seductive — the grammar has exact
precedence letters (y= … b=) and internal flags (iffy, thunky, fiddly)
that no documentation mentions, and it declares constructs the docs never
describe.
That is precisely the problem. Those are facts about one implementation's parser, not about Raku, and publishing them would quietly turn this into a description of Rakudo's internals — a wrong path to be led down while believing you are documenting a language.
The cost is visible and accepted: the operator inventory went from 292 constructs to 181 when the grammar was dropped. The remaining 181 are the ones the language documents.
A rule ships only when its declared output is what both engines produce.
rakupp rules.raku --verify # every example on rakupp
rakupp rules.raku --verify --oracle=raku # …and on Rakudo, which must agreebuild.raku adds a third gate — running every example through the node-target
build of raku.js under Bun — so a browser-only divergence also fails the build.
./verify.sh runs all of them and aborts on any drift.
Where the two engines genuinely differ, the page says so in a static block showing both, rather than claiming either is the language.
Run in this order; each writes a committed data file under src/data/, so a
site build needs neither a Rakudo checkout nor the doc sources.
| Tool | Reads | Writes | Purpose |
|---|---|---|---|
tools/inventory.raku |
doc operators.rakudoc, rakupp probe |
inventory.raku |
every documented operator, its precedence level, whether Raku++ parses it |
tools/matrix.raku |
inventory, both engines | matrix.raku |
the behaviour matrix — each operator against a battery of operand types |
tools/divergences.raku |
matrix | DIVERGENCES.md |
operator disagreements, grouped by root cause |
tools/typedoc.raku |
doc Type/*.rakudoc, type-graph.txt |
typedoc.raku |
355 types, 2009 routines, their signatures and examples |
tools/typerun.raku |
typedoc, both engines | typerun.raku |
every documented example, run three ways |
tools/conformance.raku |
typerun | CONFORMANCE.md |
the Raku++ work list |
tools/snapshot.raku |
typerun, matrix, inventory | history.jsonl |
appends one record per run, so the trend survives regeneration |
rakupp tools/inventory.raku --doc=/path/to/doc --rakupp=/path/to/rakupp
rakupp tools/matrix.raku --rakupp=/path/to/rakupp --oracle=raku
rakupp tools/typedoc.raku --doc=/path/to/doc
rakupp tools/typerun.raku --rakupp=/path/to/rakupp --oracle=raku
rakupp tools/snapshot.raku --rakupp=/path/to/rakupp --oracle=rakuNote that the run is not perfectly deterministic: two consecutive runs over identical data have differed by roughly 1-2% of examples, because some documentation examples print members of unordered collections or report timings. A movement of a handful of examples between snapshots is noise; only tens of examples are signal.
Run snapshot.raku last, after any regeneration. It appends one self-contained
JSON object to src/data/history.jsonl — aggregate verdict counts, matrix
totals, operator counts, both engine versions, and per-type counts — and never
rewrites an earlier line. /rules/divergences/ draws a static chart from it;
the file is plain JSON Lines, so an interactive timeline can read it directly
later without the data having to be reconstructed.
typerun executes arbitrary documentation examples, so it runs them in a
sandbox directory — some of them write files — under a fork-and-alarm guard,
because a few block on input.
Each documentation example carries the output the docs assert. Running it gives two more answers, and the information is in how the three relate:
| Verdict | Meaning |
|---|---|
ok |
documentation, Rakudo and Raku++ all agree |
rakupp-differs |
doc and Rakudo agree — Raku++ is wrong |
doc-drift |
both engines agree — the documentation is stale |
rakudo-differs |
doc and Raku++ agree, Rakudo does not — usually a stale doc that Raku++ was implemented from, inheriting its error |
all-differ |
three answers; needs a human |
not-runnable |
neither engine runs it standalone |
The last two classes are why this is not a two-way comparison — and
rakudo-differs is the one where no source can be trusted by default. It can
mean a stale doc that Raku++ was implemented from, or a Rakudo bug the
documentation predates, and the two look identical from the outside.
The asinh case shows why that matters. The documentation and Raku++ both give
1.asinh as 0.881373587019543; Rakudo gives 0.8813735870195429. Working it
out: asinh(1) is ln(1+√2) = 0.88137358701954302523…, whose correctly-rounded
double has 0.881373587019543 as its shortest round-tripping representation. So
Raku++ is right and Rakudo is one ulp low — resolving this by "follow Rakudo"
would have made Raku++ less accurate.
Such cases are therefore examined one at a time and the finding recorded in
src/rules/adjudications.raku, keyed by type and the example's first line. The
site prints the ruling and its reasoning on the example itself; anything not yet
examined is shown as such rather than silently resolved.
The comparison exists to be acted on, so the interesting figure is how ok
moves. Both columns are the same corpus measured by the same tools; the current
numbers are always in CONFORMANCE.md.
| Verdict | 2026-07-25 (first run) | 2026-07-26 (Raku++ v1.2.0) |
|---|---|---|
ok — all three agree |
596 | 835 |
rakupp-differs — Raku++ is wrong |
471 | 237 |
all-differ — needs a human |
176 | 164 |
doc-drift — the docs are stale |
104 | 112 |
not-runnable |
88 | 88 |
rakudo-differs — Rakudo is the odd one out |
16 | 15 |
Two cautions when reading a delta. rakupp-differs is not a clean progress
metric on its own: fixing Raku++ reshuffles rows between all-differ,
doc-drift and rakupp-differs as the three answers realign — track ok. And
the count carries a few points of run-to-run jitter, because Rakudo randomises
hash iteration order per process, so every example whose output gists a Hash or
a QuantHash flips between ok and rakudo-differs between runs. A move of ±5
with a flat Roast gate is noise.
build.raku the Spec generator -> out/
rules.raku the Rules generator -> out/rules/
verify.sh build both + all three checks (pre-push gate)
tools/ the extractors above
src/
site.raku Spec config
theme/ base.css, rules.css, spec.js, rules.js, search.js
pages/<cat>/*.md Spec pages
rules/
site.raku Rules config: topics, and the precedence ladder
adjudications.raku hand-written rulings on disagreements
pages/<topic>/*.md hand-written rule pages
data/*.raku generated, committed — the site build reads only these
out/ generated site (git-ignored)
---
title: Integer literals
status: full # full | partial | divergent | ni
order: 10
summary: One-line teaser shown in nav cards and the page header.
---
Naming the construct pulls in its precedence, associativity and section automatically — an author never restates a machine fact.
---
title: Concatenation
sym: "~"
cat: infix
status: written
summary: One line shown in the nav and under the title.
---
Then ## Rules, ## Traps and ## Errors; each ### inside them becomes a
numbered, anchored, citable rule — R1, T2, E1.
Any construct with no hand-written page still gets one, pre-filled with what was
extracted and badged Skeleton, and /rules/coverage/ counts written against
total — so a missing rule is visible rather than absent.
```raku is a runnable editor; follow it with ```output to declare the
expected output, which --verify then enforces. Options: run (auto-run),
stdin="…", rows="8". Also syntax and text for static blocks, and bad
for code that is meant to fail — shown, never run, never verified.
rakupp build.raku # the Spec -> out/
rakupp rules.raku # the Rules -> out/rules/
rakupp path/to/rakus.raku 8080 out # preview at http://127.0.0.1:8080/spec.raku.online is GitHub Pages with a custom domain. Push to main and
.github/workflows/pages.yml rebuilds: it installs the latest released rakupp,
runs build.raku --clean, then rules.raku. The Rules step is
continue-on-error, so a released interpreter that cannot build the generator
degrades to the Spec alone rather than staling the whole site.
There is no other deploy path. An earlier deploy.sh mirrored out/ to a server
doc root over sshfs; that tree serves nothing and the script is gone.
The workflow does not verify anything — it only builds. So verification is a local pre-push gate:
./verify.sh # build both sites, run all three checks, publish nothingIt reads RAKUPP, ORACLE and WASM from a git-ignored .verify.env (or the
older .deploy.env) and fails on any example drift. Run it before pushing.
The engine is not copied here — pages load https://raku.online/raku.js,
reusing the interpreter and cache of the
raku.online playground.