PARG — the grammar language
PARG is how you write a parser for parsanol: one text file describing the grammar, compiled once into a checksummed artifact that Ruby, Rust, WebAssembly, and TypeScript all consume — building byte-identical trees and recovering from broken input identically.
Write once → compile → verify → parse everywhere. The grammar becomes data with a checksum, not code pinned to one language.
The lifecycle
One .parg text file: rules in an ABNF-flavored style, entries, bindings, in-file tests. The compiler lints it — shadowed alternatives and order-dependence are build failures, not runtime surprises.
Parsanol::PARG::Compiler bakes the grammar into portable atoms, applies compile-time rewrites (skip injection, left-recursion tiers, resync policies), and runs every in-file test. A failing test fails the build.
A parsanol-tree/v2 envelope: the portable grammar JSON, bindings, embedded tests, the original source — and a sha256 checksum over the canonicalized content.
Every runtime loads the same file, verifies the checksum, and parses identically: PargArtifact (Ruby), the parsanol-rs crate, PargArtifactJs (@parsanol/wasm), PargRuntime (TypeScript).
Quick start
A grammar, its bindings, and an entry point — then compile it and parse. This is the whole workflow:
# demo.parg
grammar Demo version "1.0.0" {
skip = trivia
trivia = 1*" "
digit = %x30-39
number = 1*digit
publisher = %i"iso" / %i"ieee"
origin = publisher as publisher
serial = number as number
iso_identifier = origin serial
}
bindings iso_identifier {
publisher -> publisher (string)
number -> number (integer)
}
test identifier {
accept "iso 12345"
accept "IEEE 42"
reject "iso"
capture "ieee 42" { publisher: "ieee", number: "42" }
}
entry identifier: iso_identifierrequire "parsanol"
require "json"
document = Parsanol::PARG::Parser.new(File.read("demo.parg")).parse
result = Parsanol::PARG::Compiler.compile(document)
File.write("demo.artifact.json", JSON.generate(result.envelope))
artifact = Parsanol::PARG::Artifact.load("demo.artifact.json")
artifact.parse_and_bind("identifier", "iso 12345")
# => { publisher: "iso", number: 12345 }The same artifact parses the same input on the Rust side (PargArtifact) and in the browser (PargArtifactJs from@parsanol/wasm) — try theplayground, which runs the shipped wasm binary.
TypeScript, JavaScript, and the browser
The compiled artifact runs natively in TypeScript and JavaScript through@parsanol/wasm — the published wasm build this site's playground runs. Load it, and the checksum is verified before any parsing:
import init, { PargArtifactJs } from "@parsanol/wasm";
await init();
// Checksum verified before any parsing
const parg = new PargArtifactJs(artifactJson);
const bound = parg.parseAndBind("identifier", "iso 12345");
// => { publisher: "iso", number: 12345 }
// Recovery envelope: deepest position, partial tree, resync spans
const envelope = JSON.parse(parg.parseShapeRecovering("identifier", input));
// Generated binding-requirements types for your model
const ts = parg.schemaTypescript();The artifact exposes its whole surface to JavaScript:parseAndBind for one-step parsing and binding,parseShape and parseShapeRecovering for the raw parsanol-shape tree and the recovery envelope (deepest position, partial tree, declared resync spans), applyBindings for two-step binding,runTests for the embedded suite, and schemaTypescript(), which generates the binding-requirements types for your model from the artifact. The playground is itself a TypeScript consumer of the same binary — its recovery demo runs through parseShapeRecovering.
What the artifact guarantees
- Integrity — the sha256 checksum over the canonicalized envelope is verified on load; a mismatch fails loudly, never silently mis-parses.
- Portability — the grammar section is the exact wire format every engine registers; one serialization, four runtimes.
- Self-containment — the original .parg text rides inside, so a runtime can rebuild atoms without the source file.
- Behavior pinned — the in-file tests travel with the artifact; consumers replay them (this site's playground badge does exactly that).
- Versioned — capture renames are major, new alternatives minor, literal fixes patch; model-side-only changes move a separate
binding_version.
What the language has
skip = trivia injects whitespace/comment consumption before every terminal; captured trivia rides with the next capture under comments:. atomic rules opt out for token-shaped bodies.
resync <rule> = <pattern> bakes a deterministic skip policy into the artifact: failures return the deepest position, the partial tree, and the exact spans consumed so parsing resumed.
expr = expr "*" expr / INT is rewritten at compile time into left-folding precedence tiers — alternatives in source order bind tightest-first — and every engine runs the identical fold.
!<x and &<x inspect the text behind the position without consuming — the flank guards for intraword constraints like constrained emphasis.
accept / reject / capture / example / shape assertions run at compile time and ride the artifact, so every engine replays the same expectations — that is the conformance corpus.
alt from_table builds literal alternatives from a table at compile time; table_lookup preprocesses bound values; bindings map captures to typed model fields.
Runtime boundaries, stated plainly
Grammars using runtime state (state/set/switchatoms or custom Ruby classes) are Ruby-tier by contract: the Rust and wasm engines reject such artifacts loudly at the boundary instead of mis-parsing them. Everything else on this page runs on every engine — and theengine comparison shows how that is enforced.
Normative documents
- parg-language-guide.md — the friendly edition (this page distills it).
- parg-semantics.md — the normative match relation, flatten laws, trivia rewrite, and the corpus coverage matrix.
- The language tour — the syntax on one page, with examples.