A fast PEG (Parsing Expression Grammar) parser and packrat virtual machine for Odin.
๐ Documentation Website โข ๐ Quick Start โข ๐ Benchmarks
--- Compiles grammars into bytecode, optimizes them, and executes them with selective memoization โ fast enough to parse real-world JavaScript files up to 545 KB.
| Metric | Result | vs Target |
|---|---|---|
| JIT Speedup | 29x faster than VM | Target: 5x โ |
| SIMD Throughput | 548 MB/s sustained / 2016 MB/s peak | Target: 1000 MB/s โ |
| AOT Compilation | ~1.2s compile to native binary | Target: <5s โ |
| Test Coverage | 150/150 passing | Target: 100% โ |
| vs Tree-sitter | ~30x faster (JIT throughput) | Target: Beat โ |
Key Optimizations:
- Multi-byte literal optimization: ~2.4x faster keyword matching
- SIMD class caching: Per-charset caching eliminates re-scanning
- AOT V2: Functional native code generation for basic grammars
- Zero-allocation:
match_prefix()for consecutive char literals
Ships with a full ES2025 JavaScript grammar (~304 rules) validated against 76 production files (~2.5 MB total) from Node.js, Express, Three.js, Lodash, Underscore, Prettier, D3, and more.
Pegasus is designed to be used as a shared collection. Symlink or copy the project into Odin's shared folder:
# From your Odin installation's shared directory
ln -s /path/to/pegasus pegasusThen import it in your code:
import pegasus "shared:pegasus"Define a grammar as a string using PEG syntax, then call match():
import pegasus "shared:pegasus"
import "shared:pegasus/log"
main :: proc() {
grammar := `
Expr <- Space SubOp
SubOp <- AddOp (Sub AddOp)*
AddOp <- DivOp (Add DivOp)*
DivOp <- MulOp (Div MulOp)*
MulOp <- PowOp (Mul PowOp)*
PowOp <- Term (Pow Term)*
Term <- Atom / OParen Expr CParen
Pow <- '^' Space
Mul <- '*' Space
Div <- '/' Space
Add <- '+' Space
Sub <- '-' Space
OParen <- '(' Space
CParen <- ')' Space
Atom <- Numeral
Numeral <- Number Space
Number <- [0-9]+
Space <- (' ' / '\t' / EndOfLine)*
EndOfLine <- '\r\n' / '\n' / '\r'
EndOfFile <- !.
`
subject := "23 + 2 +(1* 2) ^ 2"
is_match, pos, captures, errors := pegasus.match(grammar, subject)
if is_match {
log.success("matched til position", pos, "|", subject[:pos])
log.info(captures)
} else {
log.error("matched til position", pos, "|", subject[:pos])
for err in errors {
log.error(err.message)
}
}
}Pegasus includes a command-line tool for parsing files with auto-detected grammars.
# Build the CLI binary
task build # outputs to ./bin/pegasus
# Or install globally
task cli:install # copies to /usr/local/bin/pegasus| Command | Description |
|---|---|
pegasus <file> |
Parse file (auto-detect grammar) |
pegasus parse <file> |
Parse file explicitly |
pegasus parse <file> <grammar.peg> |
Parse with custom grammar |
pegasus version |
Show version |
pegasus help |
Show help |
| Extension | Grammar | Description |
|---|---|---|
| .js, .mjs, .cjs, .jsx | JavaScript | ES2025 (full grammar) |
| .json | JSON | JSON parser |
| .peg, .pegasus | PEG | PEG meta-grammar |
| .xml, .html, .htm | XML/HTML | Markup parser |
| .txt | Text | Generic text |
# Parse JavaScript (auto-detects from .js extension)
./bin/pegasus app.js
# Parse JSON
./bin/pegasus data.json
# Parse with custom grammar
./bin/pegasus parse custom.txt mygrammar.peg
# Show detailed output
./bin/pegasus parse script.peg
# Check version
./bin/pegasus version| Code | Meaning |
|---|---|
0 |
Success โ input matched |
1 |
Parse failed or invalid arguments |
2 |
Grammar error or file not found |
| Flag | Description |
|---|---|
--json, -j |
Output as JSON (capture tree) |
--tree, -t |
Show only the parse tree |
--no-code, -nc |
Hide source code snippets in tree output |
--treesitter, -ts |
Output Tree-sitter compatible JSON format |
Pegasus can output parse trees in a format compatible with Tree-sitter, enabling interoperability with existing tools and IDE integrations.
- Node name mapping: Pegasus rule names are automatically mapped to Tree-sitter type names
- Field labels: Semantic field names (
function,arguments,object,property) - Position encoding: Line/column positions for every node
- Pruned trees: Duplicate and wrapper nodes are removed for clean output
# Parse JavaScript and output Tree-sitter compatible JSON
./bin/pegasus app.js --treesitter
# Combine with JSON output for structured data
./bin/pegasus app.js --json --treesitter| Pegasus Rule | Tree-sitter Type |
|---|---|
Program |
program |
ExpressionStatement |
expression_statement |
CallExpression |
call_expression |
MemberExpression |
member_expression |
Identifier |
identifier |
StringLiteral |
string |
NumericLiteral |
number |
FunctionDeclaration |
function_declaration |
VariableStatement |
variable_declaration |
{
"rule": "program",
"start": [0, 0],
"end": [3, 0],
"children": [
{
"rule": "expression_statement",
"start": [0, 0],
"end": [0, 15],
"children": [
{
"rule": "call_expression",
"field": "expression",
"start": [0, 0],
"end": [0, 14],
"children": [
{
"rule": "member_expression",
"field": "function",
"start": [0, 0],
"end": [0, 11],
"children": [
{"rule": "identifier", "field": "object", "start": [0, 0], "end": [0, 7]},
{"rule": "property_identifier", "field": "property", "start": [0, 8], "end": [0, 11]}
]
},
{
"rule": "arguments",
"field": "arguments",
"start": [0, 11],
"end": [0, 14],
"children": [
{"rule": "string", "start": [0, 12], "end": [0, 13]}
]
}
]
}
]
}
]
}match :: proc(grammar: string, subject: string) -> (bool, int, ^memo.Capture, []ParseError)
Parameters:
grammarโ a PEG grammar string (see syntax reference below)subjectโ the input string to parse
Returns:
boolโ whether the grammar matched the subjectintโ how many characters were consumed^memo.Captureโ the capture tree (from{ }expressions in the grammar)[]ParseErrorโ any errors encountered during parsing
The flagship grammar at grammars/javascript.peg is a complete ES2025 PEG grammar โ ~800 lines, ~304 rules covering the full language:
- Modules โ
import/exportwith all specifier forms, ES2022 string literal module export names,import()with options arg,withandassertattribute clauses - Declarations โ
let,const,var,using,await using, classes with fields/static blocks/auto-accessors, generators, async generators - Expressions โ optional chaining (
?.), nullish coalescing (??), private fieldinchecks (#x in obj), tagged templates, destructuring assignment - Statements โ
for-of,for-in,for-await-of, labeled statements (including labeled function declarations),switch,try/catch/finally - Functions โ arrow functions, async arrows, default parameters, rest parameters, computed property names
- Literals โ template literals with nesting, regex literals, BigInt, numeric separators, legacy octal literals
- Classes โ decorators, static blocks, auto-accessors (
accessor x = 1), private fields/methods, computed names - Unicode โ non-ASCII identifiers via UTF-8 byte ranges (
cafรฉ,ฯ,ๆฅๆฌ่ช),\uXXXXand\u{XXXXX}escapes
The grammar has been tested against 76 production JavaScript files (~2.5 MB total, 1.6 KB โ 545 KB) from:
- Node.js internals โ
path,fs,net,http2,streams,crypto,child_process,repl,readline,url, CJS/ESM loaders,buffer,events,errors,zlib,dgram,worker,cluster,console,timers - Express.js โ router, app, response
- Lodash (545 KB), Three.js โ
WebGLRenderer(106 KB) - Underscore.js (68 KB), Prettier, Moment.js, D3, npm utilities
JavaScript's Automatic Semicolon Insertion (ASI) requires knowing whether a newline occurred between two tokens. Pegasus supports this through the built-in %begin_line assertion, which succeeds at the start of input or immediately after a newline character.
The grammar uses !%begin_line in the 9 restricted productions where ASI matters:
ReturnStatement <- RETURN (!%begin_line Expression)? EOS
ThrowStatement <- THROW !%begin_line Expression EOS
PostfixExpression <- LeftHandSideExpression (!%begin_line ('++' / '--') S)?
ArrowFunction <- ArrowParameters !%begin_line ARROW ConciseBody
YieldExpression <- YIELD (!%begin_line STAR? AssignmentExpression)?
# ... and continue, break, async arrow, async method
This means return\n42 correctly parses as return; followed by 42;, while return 42 parses as return 42;.
Three key optimizations keep parsing tractable for large files:
- Arrow parameter lookahead โ a balanced-paren scan before
=>avoids expensive speculativeFormalParametersparses on every(expr). ~38% faster. - Unified
LeftHandSideExpressionโ mergesCallExpression/OptionalExpression/NewExpressioninto a single rule with a suffix loop, eliminating exponential double-parse. ~42% faster. - Arena allocator โ
match()allocates into a virtual arena, avoiding per-node allocation overhead. Combined with in-place stack operations and a memo table cap (100K entries), this reduced memory from OOM to 67 MB on Lodash (545 KB) and improved Three.js (106 KB) from 3.4s to 0.06s (57x).
Selective memoization (threshold = 512) keeps memory usage reasonable.
| # | Gap | Severity | Notes |
|---|---|---|---|
| 1 | ASI over-permissive | Low | EOS fallback accepts code that should be a syntax error. Restricted productions (return, throw, ++/--) are handled correctly via %begin_line, but the general case is an approximation. Accepts invalid code but never rejects valid code. |
| 2 | import.source() |
Low | Source Phase Imports (Stage 3). Not yet in the spec. |
Grammars are defined as a set of rules. The first rule is the start rule.
RuleName <- Pattern
| Syntax | Description |
|---|---|
'abc' |
Literal string (single quotes) |
"abc" |
Literal string (double quotes) |
[a-z] |
Character class |
[^0-9] |
Negated character class |
. |
Any character |
| Syntax | Description |
|---|---|
A B |
Sequence โ match A then B |
A / B |
Ordered choice โ try A, if it fails try B |
A* |
Zero or more repetitions |
A+ |
One or more repetitions |
A? |
Optional (zero or one) |
!A |
Not predicate โ succeed if A fails, consume nothing |
&A |
And predicate โ succeed if A succeeds, consume nothing |
(A) |
Grouping |
{ A } |
Capture โ record the matched text |
Zero-width assertions that test position without consuming input:
| Syntax | Description |
|---|---|
%begin_line |
Matches at start of input or immediately after \n |
%end_line |
Matches at end of input or immediately before \n |
%begin_text |
Matches only at the very start of input |
%end_text |
Matches only at the very end of input |
%word_boundary |
Matches at a word/non-word boundary |
%no_word_boundary |
Matches where there is no word boundary |
These are useful for line-sensitive grammars. For example, the JavaScript grammar uses !%begin_line to detect newlines between tokens for ASI (Automatic Semicolon Insertion).
Lines starting with # are comments.
| File | Description |
|---|---|
grammars/javascript.peg |
Full ES2025 JavaScript โ ~304 rules, validated against 76 production files |
grammars/peg.peg |
PEG meta-grammar โ Pegasus can parse its own grammar format |
grammars/arith.peg |
Arithmetic expressions with operator precedence |
The examples/ directory contains standalone Odin packages that embed grammars and parse input programmatically:
examples/calculator/โ Arithmetic expressions with operator precedenceexamples/json/โ Full JSON parser (objects, arrays, strings, numbers, booleans, null)examples/javascript/โ JavaScript subset (variables, functions, control flow, expressions)
Run the full test suite (150 tests across all packages):
task ta # runs all tests with -thread-count:1 for determinismOther useful commands:
task t # run tests for lib only
task b # build
task r # run
task c # cleanRun performance benchmarks with optimized builds:
# Full performance summary
odin test lib -collection:pegasus=./lib -o:speed \
-define:BENCH=true \
-define:ODIN_TEST_NAMES=pegasus.bench_performance_summary
# SIMD span throughput test
odin test lib -collection:pegasus=./lib -o:speed \
-define:BENCH=true \
-define:ODIN_TEST_NAMES=pegasus.bench_digit_span_comparison
# Multi-byte literal optimization
odin test lib -collection:pegasus=./lib -o:speed \
-define:BENCH=true \
-define:ODIN_TEST_NAMES=pegasus.bench_multi_byte_literal_optimization
# AOT compilation test
odin test lib -collection:pegasus=./lib -o:speed \
-define:BENCH=true \
-define:ODIN_TEST_NAMES=pegasus.bench_aot_basicExpected Results:
- JIT speedup: ~29x faster than VM
- SIMD span: 548 MB/s sustained throughput
- Multi-byte literals: ~2.4x faster than single-char matching
- AOT compile time: ~1.2 seconds to native binary
๐ Full documentation website: https://dvrd.github.io/pegasus/
The documentation site includes:
- Getting Started guide with installation instructions
- Complete PEG syntax reference
- API documentation with examples
- Tutorials for building real parsers
- Performance benchmarks and optimization tips
- CLI tool documentation
To view the documentation locally:
# Option 1: Open directly in browser
open docs/index.html
# Option 2: Serve with Python
cd docs && python3 -m http.server 8080
# Then open http://localhost:8080
# Option 3: Use npx serve
npx serve docsTo enable GitHub Pages for your fork:
- Go to Settings โ Pages in your GitHub repository
- Under Build and deployment:
- Source: Deploy from a branch
- Branch:
main/root(ormain//docsfolder)
- Select Folder:
/docs(the documentation is in thedocs/directory) - Click Save
- Wait ~1 minute and visit
https://YOUR_USERNAME.github.io/pegasus/
๐ก Note: GitHub Pages serves the static files directly from
/docsfolder - no build process needed!
Pegasus works in four stages:
- Parse โ The PEG grammar string is parsed into an AST of grammar rules
- Check โ The checker validates rule references, detects left recursion, and annotates the AST
- Compile โ The AST is compiled into a bytecode
Program(a sequence ofInstructionvalues), then optimized (dead code elimination, jump threading) - Execute โ The VM runs the program against the subject string using a packrat memoization table with selective caching (only entries where the parser examined โฅ512 characters are memoized)
Key packages:
lib/โ Core: grammar parsing (grammar.odin), checker (checker.odin), compiler (compile.odin), optimizer (optimize.odin), VM (vm.odin), pattern matching (re.odin)lib/charset/โ Character set operations using Odinbit_setlib/input/โ Input abstraction over the subject stringlib/memo/โ Memoization table (tree-based), capture tree, parse tree nodeslib/log/โ Logging utilities
MIT