Benchmarks
Hyperlark’s parse time against other parsers in Python, Rust,
JavaScript/TypeScript and C. Most charts use one of two inputs: JSON with
many small objects and arrays (structure-heavy), or arithmetic expressions
(calc). Within a chart, every library reads the same input and produces the
same result: the JSON value or a digest of it (a sum over its numbers), or
the calc answer (method). Both of Hyperlark’s
parsers, LALR and Earley, are
charted. nom, lalrpop with logos, pest, bison and lemon compile the grammar
into the program at build time and are faster than Hyperlark, which builds
its tables when your program runs. In JavaScript, chevrotain is faster on
the structure-heavy JSON and on calc, peggy on calc, and nearley leads
Hyperlark’s Earley with the dynamic_complete lexer (TypeScript tab).
Alternatives says what each library is and when
to prefer it.
Results
Section titled “Results”The parse runs in native code; only your transformer callbacks run in Python. Hyperlark’s LALR is 11.1× faster than Lark’s on JSON and 10.1× on calc, and ahead of sly, the fastest pure-Python parser here.
Hyperlark’s LALR, Lark’s LALR and sly compute the result during the parse,
with no tree. For Hyperlark and Lark that is
Lark(grammar, parser="lalr", transformer=...)
(Transformers). The Earley rows and
parsimonious build a tree first. Lark’s Earley
(1096 ms on JSON,
1739 ms on calc) is off the scale; see
below.
All six, both inputs (ms):
| library | JSON → digest | calc → number |
|---|---|---|
| Hyperlark (LALR) | 4.74 | 7.02 |
| sly | 42.3 | 39.6 |
| lark (LALR) | 52.6 | 71.3 |
| Hyperlark (Earley) | 62.9 | 73.1 |
| parsimonious (PEG) | 194 | 125 |
| lark (Earley) | 1096 | 1739 |
Opt-in switches such as fast_scan are off.
Versions: lark 1.3.1, sly 0.5, CPython 3.14.2.
Building the parser
Section titled “Building the parser”These tables time building an LALR parser, Hyperlark against Lark. The
builds are warm, so a fresh process’s first build costs more on both sides.
Hyperlark builds its lexer’s scanners lazily, during the first parse, so the
constructor alone overstates its margin on python.lark: the first parse
there takes 6.47 ms, a warm one
0.73 ms. The second table adds that first
parse (8 to 13 KB input) to both sides.
Constructor only:
| Grammar (LALR) | Hyperlark | lark | ratio |
|---|---|---|---|
json.lark | 0.72 ms | 4.77 ms | 6.7× |
calc.lark | 0.79 ms | 4.35 ms | 5.5× |
tp_escaped_string.lark | 0.28 ms | 2.5 ms | 9.1× |
python.lark | 22.5 ms | 269 ms | 12.0× |
Constructor plus the first parse:
| Grammar (LALR) | Hyperlark | lark | ratio |
|---|---|---|---|
json.lark | 1.18 ms | 11.7 ms | 9.9× |
calc.lark | 1.53 ms | 12.3 ms | 8.0× |
tp_escaped_string.lark | 0.39 ms | 3.81 ms | 9.8× |
python.lark | 29 ms | 298 ms | 10.3× |
Earley vs Lark’s Earley
Section titled “Earley vs Lark’s Earley”Earley is the default parser in both libraries. Hyperlark’s is 17.4× faster than Lark’s on JSON and 23.8× on calc. If your grammar needs Earley, Hyperlark’s is slower than Lark’s LALR but close: 62.9 against 52.6 ms on JSON and 73.1 against 71.3 ms on calc.
On JSON, Hyperlark trails nom by 3.4×, lalrpop with logos by 2.2×, lalrpop with its built-in lexer by 1.6× and pest by 1.4×. It leads tree-sitter by 3.9× and chumsky by 4.3×.
Both inputs (ms). Blank: not run.
| library | JSON → value | calc → number |
|---|---|---|
| nom | 0.79 | 0.43 |
| lalrpop + logos | 1.2 | 0.85 |
| lalrpop (builtin lexer) | 1.66 | 4.83 |
| pest | 1.89 | |
| Hyperlark (LALR) | 2.66 | 2.36 |
| Hyperlark (LALR, tree first) | 2.68 | 3.07 |
| tree-sitter | 10.4 | |
| chumsky | 11.5 | |
| Hyperlark (Earley) | 21.7 | 28.3 |
Comparator versions, as declared: nom 7, pest 2, chumsky 0.9, logos 0.14, tree-sitter 0.24, lalrpop 0.22.
chevrotain, a pure-JS parser, leads Hyperlark by
1.2× on
the structure-heavy JSON and
1.3× on
calc, where peggy is ahead too (4.92 against
5.39 ms). Hyperlark trails on these inputs because
its parse runs in WASM and builds a JS value for every node. The Hyperlark
(LALR) bars use a transformer passed to Lark, which computes the value
with no JS tree (LALR only;
Transformers). Cold runs (a CLI, a
fresh page load) are not measured.
On the scan-heavy input (1 MB of long strings), Hyperlark is slightly ahead of chevrotain (2.62 against 3.07 ms):
All three inputs (ms; Node v22.22.1). Blank: not run.
| library | JSON → value (134 KB) | calc → number (85 KB) | scan-heavy JSON (1 MB) |
|---|---|---|---|
| JSON.parse | 0.83 | 0.28 | |
| chevrotain | 4.3 | 4.11 | 3.07 |
| peggy | 6.24 | 4.92 | 18.6 |
| Hyperlark (LALR) | 5.29 | 5.39 | 2.62 |
| Hyperlark (LALR, tree first) | 6.26 | 7.05 | 2.7 |
Hyperlark (Earley, dynamic) | 56.9 | 66.6 | |
Hyperlark (Earley, dynamic_complete) | 94 | ||
| nearley | 69.8 |
Comparator versions, as declared: chevrotain 11, peggy 4, nearley 2.20.
Earley vs nearley
Section titled “Earley vs nearley”nearley is an Earley parser for JavaScript, measured on calc only. Here it
runs without a lexer, on a character-level grammar, so it considers every
way of splitting the input into tokens. Hyperlark’s default Earley
lexer, dynamic, takes one match per terminal at each position, which is
cheaper and less general; the dynamic_complete lexer also tries the
shorter prefixes of each match
(choosing a lexer). So the bar to read
against nearley is dynamic_complete, and nearley leads it by
1.3×.
The Hyperlark Earley rows parse to a JS tree and transform it afterwards,
which costs little next to the Earley parse: with LALR on the same input, a
tree instead of a value adds
1.66 ms.
The C benchmark is JSON only; the binding is LALR only for now. bison and
lemon, which generate C at build time, lead Hyperlark by
2.3× and
4.0×; Hyperlark
leads tree-sitter by
4.5×.
All but tree-sitter compute the digest during the parse with no tree,
Hyperlark through lark_parse_fold (C header);
tree-sitter builds a tree and walks it.
| library | JSON → digest (ms) |
|---|---|
| lemon | 0.56 |
| flex + bison | 0.96 |
| Hyperlark (LALR) | 2.23 |
| tree-sitter | 10 |
How these were measured
Section titled “How these were measured”- One machine (cpu Apple · arch aarch64 · 13 cores · rustc 1.96.0). Compare bars within a chart; your hardware’s milliseconds will differ.
- Warm timings: the best of many runs after warm-up. The Rust figures are criterion medians; the first-parse figures under Building the parser are single cold runs.
- Each parser is built once, outside the timed loop, so the figures are parse time only. Building the parser, Hyperlark against Lark, is timed in the Python tab.
- Every library reads the same bytes and must produce the same result, or its row is not published. For JSON, Rust and JavaScript produce the value itself. Python and C produce a digest of it: the sum of its numbers, each truncated to an integer. For calc, every target produces the evaluated answer.
- Inputs:
- JSON, 134 KB: 240 generated values nested up to three deep (structure-heavy).
- calc, 85 KB: 720 arithmetic expressions, one per line, each parsed separately (720 small parses per run).
- JSON, 1 MB, for the WASM scan-heavy chart: 430 strings of about 2.5 KB, so the lexer does most of the work.
- Bars are milliseconds; shorter is faster. A library far off a chart’s scale is left off it and listed in that tab’s table.