Skip to content

PARG language tour

The syntax on one page, with real examples. The friendly reference is thelanguage guide; the normative semanticswin where any summary disagrees.

Grammar skeleton

grammar Demo version "1.0.0" {
  # rules, one per line — a rule body ends at the newline
  digit = %x30-39
  number = 1*digit
}

entry identifier: number
  • # starts a comment to end of line; ## is a doc comment that attaches to the following rule and ships in the artifact (LSP hovers). One rule per line (newlines are significant) — except in brace bodies, where newlines are whitespace.
  • A case-insensitive literal (%i"iso") captures the raw input form: IEEE 42 captures "IEEE", not "ieee".
  • grammar NAME version "X.Y.Z" { wraps the rules; entry NAME: rule declares an artifact entry point — multiple entries share one rule set.
  • Keywords (grammar version as bindings preprocess entry table_lookup from_table column) cannot name or reference rules.

Terminals

"abc"      exact literal (case-sensitive)
%i"abc"    case-insensitive literal
%x41       one byte 0x41
%x41-5A    one byte in the range 0x41-5A
%x0D.0A    a byte sequence: 0x0D then 0x0A

String escapes: \", \\, \n, \t, \r.

Operators

a b        sequence (juxtaposition)
a / b      ordered choice — first match wins
[ x ]      optional (maybe)
( x )      grouping
*x         zero or more          1*x   one or more
2*4x       two to four           4x    exactly four
!x         negative lookahead    &x    positive lookahead
!<x        negative behind-guard &<x   positive behind-guard
x as name  capture — names the parse result

PEG semantics throughout: choice is ordered, repetition is greedy. A repetition prefix binds one primary — 3*(a b) is three-to-four repetitions of the group; parenthesize composites.

Captures

dash number as part    # captures "number" only
*( item ) as items     # captures the whole repeated run
*( ( item ) as each )  # names each iteration's item

Flattening follows parslet's CanFlatten exactly (including its data-loss rules for mixed repetitions) — verified byte-for-byte against parslet 2.0.0. The stable idiom: capture every structural rule so each iteration yields one hash. The normative laws are V0–V4 in the semantics document.

Behind-guards

emph = ( !< ALPHA "*" 1*( !star ANY ) "*" ) as emph

!<x / &<x inspect the text behind the position and consume nothing. A character-class body (or builtin class reference) compiles to an end-anchored flanking match; a case-sensitive literal to a fixed byte window; anything else is a compile error — an arbitrary rule has no bounded behind-window.

Rule bodies: the brace form

## Publisher code: case-insensitive tag; raw input is captured.
publisher = {
  %i"iso" / %i"ieee"
}

## ISO-style identifier: publisher, one space, a number.
iso_identifier = { publisher as publisher space number as number }

A rule body is normally newline-terminated — one line, one rule. The brace form makes it self-delimiting: the body extends to the matching}, and inside the braces newlines and comments are whitespace. Both forms compile to the same atom stream — engines never see the difference. Prefer the brace form for anything that wraps; the opening brace may also sit on the next line after the equals sign.

# RFC 5234 core rules are predeclared — no boilerplate:
octet_number = 2*DIGIT "." 3*DIGIT      # DIGIT, ALPHA, DQUOTE, WSP, ...
local_win = LWSP / CRLF                 # LWSP and CRLF inject as real rules
digit7 = "7"                            # a local DIGIT shadows the core rule

The RFC 5234 core rules are predeclared in every grammar —ALPHA BIT CHAR CR CRLF CTL DIGIT DQUOTE HEXDIG HTAB LF OCTET SP VCHAR WSP— so grammars imported from ABNF keep their vocabulary. A local rule of the same name deliberately shadows the core rule (the lint flags it as style guidance).

Character classes

A class is a hex range — %x5f, %x21-2f — or an alternation of ranges:(%x21-2f / %x3a-40 / %x5b-60 / %x7b-7e).[ … ] is not a class — it is the optional form, and brackets whose elements are all classes fail compilation with the alternation spelling in the message.

Skip & trivia

grammar Lml version "2.0.0" {
  skip = trivia
  trivia = 1*( ( 1*" " ) / line_comment / block_comment )
  comment = "(*" *( comment / ( !"*)" ANY ) ) "*)"
  ...
}

One declaration per grammar: an optional match of the skip rule is injected before every terminal, and entries take leading and trailing skips. Trivia is capture-free, and failures inside injected trivia never surface — deepest-failure positions point at real content. The skip rule must be non-nullable and may only reference rules; those rules (and everything they reference) build without injection, so recursive trivia like nested block comments work.

skip = trivia capture: comments
trivia = 1*( ( 1*" " ) / line_comment / block_comment )

capture: comments attaches comment-shaped trivia to the capture it precedes under comments: — comments become data without dedicated capture rules. Whitespace is never recorded; trailing trivia attaches to the enclosing entry capture.

atomic word = ( 1*( ALPHA ) )

Injection happens inside repetition runs too, so word = 1*( ALPHA )under a whitespace skip matches "a b" as one word. Token-shaped rules opt out with atomic: the body builds without injection and matches contiguously.

Left recursion

expr = expr "*" expr
     / expr "+" expr
     / number

Direct left recursion is rewritten at compile time (the ANTLR 4 model) into left-folding precedence tiers: alternatives in source order bind tightest-first, non-recursive alternatives are the primary. The artifact carries ordinary atoms, so every engine folds identically. Indirect left recursion is a compile error.

Resync (recovery)

grammar RecoveryResync version "1.0.0" {
  pair = ( 1*( %x61-7A / %x30-39 ) ) as k "=" ( 1*( %x30-39 ) ) as v ";"
  document = 1*( pair )

  # recovery policy: junk is any run of non-";" bytes ending at a ";"
  resync document = 1*( %x00-3A / %x3C-FF ) ";"
  entry main: document
}

A resync declaration is a recovery policy, not part of the match relation: at a failure point under document, the engine consumes each pattern match as one annotated error span and resumes iterations after it. Onparse_recovering the answer carries the deepest position, the partial tree, and the exact spans — byte-identically on every engine. See it live in theplayground's error-recovery grammar.

In-file tests

accept "iso 12345"                      # the input parses
reject "iso"                            # the input does not parse
capture "ieee 42" { publisher: "ieee", number: "42" }
example "iso 12345" { publisher: "iso", number: 12345 }
shape "a b" [ { w: "a" }, { w: "b" } ]
  • accept / reject pin parseability.
  • capture pins the raw capture tree (strings compare by text).
  • example pins the bound attribute hash — bindings applied.
  • shape pins the canonical whole tree — the only phase that can observe repetition flattening and sequence merge laws.
test main {
  accept "AB2020"
  reject "AB20"
  capture "AB2020" { pub: "AB" }
  shape "aab" [ { x: "a" }, { x: "a" }, { y: "b" } ]
}

Tests run at compile time (a failing test fails the build) and ride the artifact, so every engine replays them.

Bindings & tables

bindings iso_identifier {
  publisher -> publisher (string)
  year -> year (integer, 0..1)
  stage -> stage (string) preprocess: stage_code
  supplement_no -> supplements[].number (integer, 0..*)
}

preprocess stage_code {
  table_lookup stages abbr -> code
}

stage_abbr = alt from_table "stages" column "abbr"

Bindings map captures to typed model fields (string,integer, float, boolean; cardinalities1, 0..1, 1..*, 0..*) with one array-grouping level. Preprocess steps are declarative data operations (table_lookup in v1). alt from_table expands a table column into literal alternatives, sorted longest-first.

Style guide

# short named rules; the brace form for anything that wraps
iso_identifier = { publisher space number }

# tests beside the grammar they pin
test identifier {
  accept "iso 12345"
  reject "iso"
}

# bindings beside the captures they consume
bindings iso_identifier {
  publisher -> publisher (string)
}

## doc comments ride the artifact as LSP hovers
## Serial number: digits only, no separators.
serial = 1*DIGIT
  • Short named rules; the brace form for anything that wraps.
  • Tests live beside the grammar they pin; bindings beside the captures they consume.
  • ## doc comments on every entry-reachable rule — the artifact ships them.
  • Grammar-wide: one rule style per file; the lints flag heavy mixing.

The lint

Under PEG semantics order is decisive, so the compiler runs first-set analysis on every alternative: shadowed branches ("iso" / "iso/iec"), empty shadowing, duplicates, and left recursion are build failures; order-dependent branch pairs are warnings recorded in the artifact for consumer CI. Foreign grammars can come in as PARG text:Parsanol::PARG::Import.import(:abnf | :antlr | :ebnf | :pest, text) — ABNF and ANTLR grammars import into PARG text, with everything the scoped importers cannot express rejected with position instead of dropped.

Next: break an input on purpose in theplayground, or read thenormative semantics.