Skip to content

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

1
Write

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.

2
Compile

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.

3
Artifact

A parsanol-tree/v2 envelope: the portable grammar JSON, bindings, embedded tests, the original source — and a sha256 checksum over the canonicalized content.

4
Consume

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
# 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_identifier
compile once, parse anywhere
require "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:

TypeScript / JavaScript
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

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