grammatik 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/bin/grammatik.js +8 -0
- package/dist/cjs/index.js +1637 -139
- package/dist/cjs/runtime.js +642 -34
- package/dist/esm/index.js +1637 -139
- package/dist/esm/runtime.js +642 -34
- package/dist/types/index.d.ts +36 -0
- package/dist/types/runtime.d.ts +4 -3
- package/package.json +3 -2
- package/src/analyze.js +225 -18
- package/src/codegen.js +218 -27
- package/src/definition.js +5 -0
- package/src/emit.js +41 -7
- package/src/lexer.d.ts +43 -2
- package/src/lexer.js +573 -44
- package/src/lexgen.js +451 -0
- package/src/memoplan.js +220 -0
- package/src/methods.js +1 -1
- package/src/parser.js +38 -3
- package/src/plan.js +142 -30
- package/src/runtime.js +8 -6
- package/src/scan.js +422 -0
- package/src/state.js +33 -1
package/README.md
CHANGED
|
@@ -121,9 +121,12 @@ Invalid grammar:
|
|
|
121
121
|
| `many` | `{"many": p}` | Zero or more. |
|
|
122
122
|
| `many1` | `{"many1": p}` | One or more. |
|
|
123
123
|
| `option` | `{"option": p}` | Zero or one; yields `null` when absent. |
|
|
124
|
+
| `flag` | `{"flag": p}` | Zero or one; yields `true` when `p` matched and `false` when it did not. Replaces `label(negated, option(consume(Not)))` plus `{"!==": [{"val": "negated"}, null]}` in the action. Written by hand: the same generated code as `option`, so it is for readability, not speed, and nothing rewrites to it (that would mean rewriting the action as well). |
|
|
124
125
|
| `manySep` | `{"manySep": {"rule": p, "sep": "Comma", "trailing": false}}` | Separated list. |
|
|
125
126
|
| `many1Sep` | as above | Separated list, at least one. |
|
|
126
127
|
| `between` | `{"between": {"open": p, "body": p, "close": p}}` | Bracketed; yields the body. |
|
|
128
|
+
| `pick` | `{"pick": {"parsers": [p, ...], "index": 1}}` | The parsers in order; yields the one at `index`. `between` with any number on either side, e.g. `pick(1, consume(Where), subrule(Expr))`. |
|
|
129
|
+
| `yields` | `{"yields": {"parsers": [p, ...], "value": "ASC"}}` | The parsers in order; yields a constant (string, number, boolean or null). `yields(consume(Asc), "ASC")`. |
|
|
127
130
|
| `infixLeft` | `{"infixLeft": {"operand": p, "operator": p, "combine": logic}}` | One left-associative precedence level; `combine` sees `left`/`op`/`right`. |
|
|
128
131
|
| `infixRight` | as above | Right-associative: `a ^ b ^ c` is `a ^ (b ^ c)`. |
|
|
129
132
|
| `prefix` | `{"prefix": {"operator": p, "operand": p, "combine": logic}}` | Prefix operators, folded right; `combine` sees `op`/`operand`. |
|
|
@@ -291,6 +294,9 @@ A parser exposes `parse(text)`, `parseTokens(tokens, source?)`, `tokenize(text)`
|
|
|
291
294
|
| `execution` | `'generated'` | `'generated'` (a JS function per rule) or `'interpreted'` (closures, no code generation). |
|
|
292
295
|
| `positions` | `'full'` | `'full'` records line and column per token; `'offset'` records byte offsets only and skips scanning for newlines. |
|
|
293
296
|
| `memo` | `false` | Packrat memoization on `(rule, position)`. Changes the complexity class on grammars that backtrack; pure overhead on grammars that do not. |
|
|
297
|
+
| `autoMemo` | `true` | Memoize only the rules that several colliding branches of an `alt` would each re-parse (listed in `parser.memoRules`). Where `memo` costs ~2x everywhere, this costs nothing on a grammar with no such rule, and removes the exponential cost of nesting when there is one: in the SQL example `WHERE ((((((( x IS NULL )))))))` went from 255 ms to ~15 µs. Ignored when `memo` is on. |
|
|
298
|
+
| `inlineRules` | `true` (`false` for `emitModule`) | In generated code, write single-caller operator levels (`infixLeft`, `infixRight`, `prefix`, `postfix`) into their caller instead of calling them, up to six per function (past that V8 stops optimizing the function). 2–3x on a bare precedence chain but only ~1.02–1.04x on the formula and SQL examples, and it makes the code bigger (the formula module grows 32% minified), so a parser built at run time has it on and an emitted file, which someone ships, leaves it off. No effect on `execution: 'interpreted'`. |
|
|
299
|
+
| `idioms` | `true` | Recognize common spellings and run them as the cheaper combinator they are equivalent to: `seq(open, label(n, p), close, action({val:n}))` → `between`; `seq(prefix…, label(n, p), action({val:n}))` → `pick`; `seq(p…, action(<constant>))` → `yields`; `label` + `action` → `as`; `alt(consume(A), consume(B), …)` → `oneOf` (1.3–1.8x on its own for the 2–7 token operator sets grammars have); a shared leading run of `alt` branches is factored out. Neither `pick` nor `yields` allocates the bindings object a `seq` does. Off runs the grammar exactly as written. |
|
|
294
300
|
| `strict` | `true` | Require the whole token stream to be consumed. |
|
|
295
301
|
| `ll1` | `true` | Use FIRST-set dispatch for `alt`. Off forces ordered backtracking. |
|
|
296
302
|
| `validate` | `true` | Throw `GrammarError` on left recursion and bad references. |
|
|
@@ -327,6 +333,15 @@ The file holds the same rule functions `createParser` would build at run time, a
|
|
|
327
333
|
source. The grammar was analyzed when the file was written, so importing it does
|
|
328
334
|
no grammar work and calls no `new Function`.
|
|
329
335
|
|
|
336
|
+
The lexer is written out as source too. A table-driven lexer would need an engine
|
|
337
|
+
at run time — regex analysis, scanner selection, dispatch tables — in every
|
|
338
|
+
process that loads the file; the generated one carries only the code its grammar
|
|
339
|
+
needs. For the JSON parity grammar that is a 9.6 KB bundle against 24.0 KB (3.1
|
|
340
|
+
against 8.4 KB gzipped) with the engine, loads about 2.5x faster, and lexes
|
|
341
|
+
1.4x faster (`positions: 'offset'`). It produces exactly the tokens and errors
|
|
342
|
+
`createLexer` does; `test/lexgen.test.js` holds it to that on every example
|
|
343
|
+
grammar. `--lexer runtime` keeps the old behaviour.
|
|
344
|
+
|
|
330
345
|
Pass `--methods ./methods.js` and the emitter compiles the semantic actions to
|
|
331
346
|
source as well. When every action compiles, the `json-logic-engine` import is
|
|
332
347
|
dropped and the file's only dependency is `grammatik/runtime`. Actions that
|
|
@@ -423,6 +438,7 @@ would show up in its size immediately.
|
|
|
423
438
|
|
|
424
439
|
```
|
|
425
440
|
src/lexer.js Token definitions to a sticky-regex lexer with first-char dispatch
|
|
441
|
+
src/lexgen.js The same lexer written out as source, for emitted parsers
|
|
426
442
|
src/firstchars.js Static analysis of regex sources, for that dispatch
|
|
427
443
|
src/analyze.js FIRST sets, nullability, left recursion, idiom recognition
|
|
428
444
|
src/plan.js Specializes a grammar into closures
|
package/bin/grammatik.js
CHANGED
|
@@ -44,7 +44,11 @@ const USAGE = `Usage: grammatik generate <grammar.gram|grammar.json> [options]
|
|
|
44
44
|
--format <format> esm (default) | cjs
|
|
45
45
|
--execution <mode> generated (default) | interpreted
|
|
46
46
|
--positions <mode> full (default) | offset
|
|
47
|
+
--lexer <mode> generated (default) | runtime
|
|
48
|
+
generated writes the lexer into the file as source;
|
|
49
|
+
runtime keeps it as a table the runtime interprets
|
|
47
50
|
--memo Enable packrat memoization
|
|
51
|
+
--no-auto-memo Do not memoize shared-prefix rules automatically
|
|
48
52
|
--no-ll1 Force ordered backtracking
|
|
49
53
|
--max-steps <n> Runaway guard (0 disables)
|
|
50
54
|
--name <name> Name recorded in the file header
|
|
@@ -69,11 +73,13 @@ for (let i = 1; i < args.length; i++) {
|
|
|
69
73
|
else if (arg === '--format') flags.format = args[++i]
|
|
70
74
|
else if (arg === '--execution') flags.execution = args[++i]
|
|
71
75
|
else if (arg === '--positions') flags.positions = args[++i]
|
|
76
|
+
else if (arg === '--lexer') flags.lexer = args[++i]
|
|
72
77
|
else if (arg === '--max-steps') flags.maxSteps = Number(args[++i])
|
|
73
78
|
else if (arg === '--name') flags.name = args[++i]
|
|
74
79
|
else if (arg === '--methods') flags.methods = args[++i]
|
|
75
80
|
else if (arg === '--memo') flags.memo = true
|
|
76
81
|
else if (arg === '--no-ll1') flags.ll1 = false
|
|
82
|
+
else if (arg === '--no-auto-memo') flags.autoMemo = false
|
|
77
83
|
else if (arg.startsWith('-')) { console.error(`unknown option ${arg}\n\n${USAGE}`); process.exit(1) }
|
|
78
84
|
else input = arg
|
|
79
85
|
}
|
|
@@ -98,7 +104,9 @@ const output = emitModule(grammar, {
|
|
|
98
104
|
format: flags.format,
|
|
99
105
|
execution: flags.execution,
|
|
100
106
|
positions: flags.positions,
|
|
107
|
+
lexer: flags.lexer,
|
|
101
108
|
memo: flags.memo,
|
|
109
|
+
autoMemo: flags.autoMemo,
|
|
102
110
|
ll1: flags.ll1,
|
|
103
111
|
maxSteps: flags.maxSteps
|
|
104
112
|
})
|