Turn a short typed instruction into structured data, safely.
Below is a feature-flag rule, {new-checkout, beta, 25%, [eu, us], sticky}, read into a typed value. Rearrange everything after the flag's name: the result stays the same, because each word finds the one field that takes it. Then type a word that doesn't belong, like mars, and Segno points at it and says what it expected.
Pick a kind of rule, then type. The same notation means different things in different places: each place has a schema that says which fields it takes, and every error names what was expected.
[x], {done} and {yes} all mean the same state. That is kind per span and messy per document, so Segno reports every span written differently from the document's most common spelling — with a fix. One state mark per line:
These are the inputs that took other parser libraries from one second to over sixteen, at 2k, 8k and 32k characters. Each step is 4 times longer, so steady parsing takes about 4 times as long; a parser that blows up takes 16 times as long or more.
Underneath the notation is a grammar engine. Segno only builds a parser it can prove reads in one pass, and says no to a grammar that would need to guess. A grammar is a few rules built from seq, alt, many, opt, ref and character sets; node marks the pieces you want back. Edit the grammar or the text: it compiles and parses as you type. Pick a refused grammar to see the compiler say no.
What generate(grammar) writes: straight-line TypeScript, no library at runtime. The notation ships this way.
Lattice renders slide decks from Markdown, and its inline code spans carry directives. It had grown 27 hand-written grammars for them, with 21 sigils between them; it is moving every one onto this notation. Some of them, beside what each becomes — try any row in the editor above.
| Where | Before | Segno |
|---|
Segno beside the six parser libraries its first user evaluated, and beside the hand-written parsers it replaces — every candidate in its own process, on identical inputs, on one machine. Time per input on real spans, then the worst case on hostile input.
| Parser | Style | Bracket list | Inline span | Worst hostile case, 32k chars |
|---|---|---|---|---|
| Segno | LL(1), proven at compile time | 734 ns | 45 ns | 7 ms |
| Hand-written (Lattice) | recursive descent, by hand | 942 ns | 33 ns | 7 ms |
| Peggy | PEG | 6,577 ns | 882 ns | 7.3 s |
| Chevrotain | LL(k), tokens | 4,723 ns | 24,605 ns | 776 ms |
| Parsimmon | PEG combinators | 54,267 ns | 1,073 ns | did not finish |
| Nearley | Earley | 66,000 ns | 12,644 ns | did not finish |
| Ohm | PEG, memoized | 107,161 ns | 24,597 ns | did not finish |
| Lezer | LR | — | 8,670 ns | — |
Measured 2026-09-29, Node 22, on a shared cloud machine: absolute times move with the machine, the ratios carry. Bracket lists: the 64 in Lattice's decks, parsed into parts. Inline spans: all 4,645, 99% of them ordinary code. "Did not finish": a smaller input already took over half a second, so the next was stopped. Lezer's grammar set does not cover bracket lists. Segno's flat best case on nested brackets is its 31-level cap refusing the input, not a parse. Reproduce: npm run parser:bakeoff:versus.
This page runs the real library in your browser. Segno is Italian for sign or mark. Learn how Segno works, and how to use it →