grammatik 0.1.1 → 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 +1471 -104
- package/dist/cjs/runtime.js +635 -25
- package/dist/esm/index.js +1471 -104
- package/dist/esm/runtime.js +635 -25
- package/dist/types/index.d.ts +36 -0
- package/dist/types/runtime.d.ts +4 -3
- package/package.json +1 -1
- package/src/analyze.js +114 -4
- package/src/codegen.js +147 -11
- package/src/definition.js +5 -0
- package/src/emit.js +29 -7
- package/src/lexer.d.ts +43 -2
- package/src/lexer.js +535 -28
- package/src/lexgen.js +451 -0
- package/src/memoplan.js +220 -0
- package/src/methods.js +1 -1
- package/src/parser.js +31 -3
- package/src/plan.js +88 -26
- package/src/runtime.js +8 -6
- package/src/scan.js +422 -0
- package/src/state.js +33 -1
package/src/parser.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createValueMethods } from './values.js'
|
|
|
9
9
|
import { analyze } from './analyze.js'
|
|
10
10
|
import { createPlanner } from './plan.js'
|
|
11
11
|
import { createCodegen } from './codegen.js'
|
|
12
|
+
import { selectMemoRules } from './memoplan.js'
|
|
12
13
|
import { FAIL, createState, buildParseError, ParseError } from './state.js'
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -23,6 +24,21 @@ import { FAIL, createState, buildParseError, ParseError } from './state.js'
|
|
|
23
24
|
* @property {boolean} [memo=false] Enable packrat memoization. Costs a Map
|
|
24
25
|
* write per rule invocation; pays for itself
|
|
25
26
|
* only on grammars that backtrack heavily.
|
|
27
|
+
* @property {boolean} [autoMemo=true] Memoize just the rules that would otherwise be
|
|
28
|
+
* re-parsed by several colliding branches of an
|
|
29
|
+
* `alt` (see `selectMemoRules`). Unlike `memo` it
|
|
30
|
+
* costs nothing on a grammar with no such rule,
|
|
31
|
+
* and it is ignored when `memo` is on.
|
|
32
|
+
* @property {boolean} [inlineRules=true] In generated code, write single-caller operator
|
|
33
|
+
* levels (`infixLeft`, `infixRight`, `prefix`, `postfix`)
|
|
34
|
+
* into their caller instead of calling them. Costs code
|
|
35
|
+
* size, which is why `emitModule` defaults it off.
|
|
36
|
+
* @property {boolean} [idioms=true] Recognize common spellings (`seq(open, label, close, action)`,
|
|
37
|
+
* a prefix plus one labelled result, a constant action, an
|
|
38
|
+
* `alt` of bare `consume`s, a shared leading run of `alt`
|
|
39
|
+
* branches) and run them as the
|
|
40
|
+
* cheaper combinator they are equivalent to. Off runs the
|
|
41
|
+
* grammar exactly as written; for debugging and benchmarking.
|
|
26
42
|
* @property {boolean} [strict=true] Require the whole token stream to be consumed.
|
|
27
43
|
* @property {boolean} [validate=true] Throw on grammar errors (left recursion, bad refs).
|
|
28
44
|
* @property {number} [maxSteps=0] Runaway guard; 0 disables.
|
|
@@ -75,6 +91,9 @@ export function createParser (spec, options = {}) {
|
|
|
75
91
|
const { tokens: tokenDefs, rules, start } = spec
|
|
76
92
|
const {
|
|
77
93
|
memo = false,
|
|
94
|
+
autoMemo = true,
|
|
95
|
+
inlineRules = true,
|
|
96
|
+
idioms = true,
|
|
78
97
|
strict = true,
|
|
79
98
|
validate = true,
|
|
80
99
|
maxSteps = 0,
|
|
@@ -123,7 +142,14 @@ export function createParser (spec, options = {}) {
|
|
|
123
142
|
|
|
124
143
|
// The planner specializes the grammar into closures; the combinators'
|
|
125
144
|
// `compile` hooks reach it through the engine.
|
|
126
|
-
|
|
145
|
+
// Which rules to remember per position. Global `memo` already remembers all of
|
|
146
|
+
// them, and `validate: false` can hand us a grammar the analysis did not accept.
|
|
147
|
+
const memoRules = autoMemo && !memo && analysis.errors.length === 0
|
|
148
|
+
? selectMemoRules(rules, analysis.firsts, { ll1 })
|
|
149
|
+
: []
|
|
150
|
+
const memoSlots = new Map(memoRules.map((name, slot) => [name, slot]))
|
|
151
|
+
|
|
152
|
+
const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval, tokenIds, memoSlots, idioms })
|
|
127
153
|
engine.__jlGrammar = { planner }
|
|
128
154
|
|
|
129
155
|
// Source generation emits one JS function per rule with every node inlined,
|
|
@@ -132,7 +158,7 @@ export function createParser (spec, options = {}) {
|
|
|
132
158
|
// lands on the generated code rather than a method dispatch.
|
|
133
159
|
// Code generation needs `new Function`, so it is off whenever eval is.
|
|
134
160
|
const codegen = compile && unsafeEval
|
|
135
|
-
? createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds })
|
|
161
|
+
? createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds, memoSlots, inlineRules, idioms })
|
|
136
162
|
: null
|
|
137
163
|
const compiledStart = codegen && codegen.compile(start)
|
|
138
164
|
|
|
@@ -149,7 +175,7 @@ export function createParser (spec, options = {}) {
|
|
|
149
175
|
* @param {string} [source] Original text, so `text` can slice exact spans.
|
|
150
176
|
*/
|
|
151
177
|
function parseTokens (tokens, source, track = false) {
|
|
152
|
-
const state = createState(tokens, rules, { memo, maxSteps, track })
|
|
178
|
+
const state = createState(tokens, rules, { memo, maxSteps, track, memoSlots: memoRules.length })
|
|
153
179
|
state.firsts = firsts
|
|
154
180
|
state.source = source
|
|
155
181
|
state.positions = lexer.positions
|
|
@@ -192,6 +218,8 @@ export function createParser (spec, options = {}) {
|
|
|
192
218
|
planner,
|
|
193
219
|
codegen,
|
|
194
220
|
execution: mode,
|
|
221
|
+
/** Rules memoized by `autoMemo`, for introspection. */
|
|
222
|
+
memoRules,
|
|
195
223
|
compiled: Boolean(compiledStart),
|
|
196
224
|
/** Rules whose `alt`s resolved to O(1) LL(1) dispatch, for introspection. */
|
|
197
225
|
get firsts () { return analysis.firsts }
|
package/src/plan.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
4
|
import { FAIL, expect } from './state.js'
|
|
5
|
-
import { buildDispatch, opOf, recognizeSeqIdiom, recognizeAltPrefixIdiom } from './analyze.js'
|
|
5
|
+
import { buildDispatch, opOf, recognizeSeqIdiom, recognizeAltPrefixIdiom, recognizeAltConsumeIdiom, usesOuterScope } from './analyze.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The specializing planner.
|
|
@@ -31,27 +31,6 @@ function declareSync (fn) {
|
|
|
31
31
|
return fn
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
* True when the logic reads from an outer scope (`{val: [[-1], ...]}` or
|
|
36
|
-
* `{var: "../x"}`). The engine's compiler does not thread `above` into built
|
|
37
|
-
* functions, so such actions must stay interpreted.
|
|
38
|
-
*/
|
|
39
|
-
function usesOuterScope (logic) {
|
|
40
|
-
if (Array.isArray(logic)) return logic.some(usesOuterScope)
|
|
41
|
-
if (!logic || typeof logic !== 'object') return false
|
|
42
|
-
|
|
43
|
-
for (const key of Object.keys(logic)) {
|
|
44
|
-
const arg = logic[key]
|
|
45
|
-
if (key === 'val' && Array.isArray(arg) && Array.isArray(arg[0])) return true
|
|
46
|
-
if (key === 'var') {
|
|
47
|
-
if (typeof arg === 'string' && arg.includes('../')) return true
|
|
48
|
-
if (Array.isArray(arg) && typeof arg[0] === 'string' && arg[0].includes('../')) return true
|
|
49
|
-
}
|
|
50
|
-
if (usesOuterScope(arg)) return true
|
|
51
|
-
}
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
|
|
55
34
|
/**
|
|
56
35
|
* True when the logic could return the *context object itself* rather than a
|
|
57
36
|
* value derived from it — `{"var": ""}` and `{"val": []}` both do.
|
|
@@ -77,10 +56,11 @@ function mayReturnContext (logic) {
|
|
|
77
56
|
/**
|
|
78
57
|
* @param {{ engine: any, rules: Record<string, any>, firsts: Map<string, any>|null,
|
|
79
58
|
* memo: boolean, maxSteps: number, unsafeEval?: boolean,
|
|
80
|
-
* tokenIds?: Map<string, number>|null
|
|
59
|
+
* tokenIds?: Map<string, number>|null,
|
|
60
|
+
* memoSlots?: Map<string, number>, idioms?: boolean }} config
|
|
81
61
|
*/
|
|
82
62
|
export function createPlanner (config) {
|
|
83
|
-
const { engine, rules, firsts, memo, maxSteps, unsafeEval = true, tokenIds = null } = config
|
|
63
|
+
const { engine, rules, firsts, memo, maxSteps, unsafeEval = true, tokenIds = null, memoSlots = new Map(), idioms = true } = config
|
|
84
64
|
|
|
85
65
|
/** @type {Map<string, { fn: ((state: any) => any) | null }>} */
|
|
86
66
|
const slots = new Map()
|
|
@@ -303,6 +283,25 @@ export function createPlanner (config) {
|
|
|
303
283
|
}
|
|
304
284
|
}
|
|
305
285
|
|
|
286
|
+
/**
|
|
287
|
+
* `{"flag": p}` — `true` when `p` matched, `false` when it did not; never fails.
|
|
288
|
+
* `option` with the answer already turned into a boolean, for the common
|
|
289
|
+
* `label(negated, option(consume(Not)))` followed by `{"!==": [negated, null]}`.
|
|
290
|
+
*
|
|
291
|
+
* @returns {(state: any) => any}
|
|
292
|
+
*/
|
|
293
|
+
const buildFlag = (arg) => {
|
|
294
|
+
const child = plan(arg)
|
|
295
|
+
return (state) => {
|
|
296
|
+
const start = state.idx
|
|
297
|
+
if (child(state) === FAIL) {
|
|
298
|
+
state.idx = start
|
|
299
|
+
return false
|
|
300
|
+
}
|
|
301
|
+
return true
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
306
305
|
/** @returns {(state: any) => any} */
|
|
307
306
|
const buildInfixLeft = (arg) => {
|
|
308
307
|
// A whole precedence level in one closure.
|
|
@@ -467,6 +466,35 @@ export function createPlanner (config) {
|
|
|
467
466
|
}
|
|
468
467
|
}
|
|
469
468
|
|
|
469
|
+
/**
|
|
470
|
+
* `{"pick": {parsers, index}}` and `{"yields": {parsers, value}}` run their parsers
|
|
471
|
+
* in order and give back one of the results or a constant. They are what a `seq`
|
|
472
|
+
* with a single labelled child and `action({val: label})`, or with a constant
|
|
473
|
+
* action, is rewritten to: neither needs the bindings object a `seq` allocates.
|
|
474
|
+
*
|
|
475
|
+
* @returns {(state: any) => any}
|
|
476
|
+
*/
|
|
477
|
+
const buildSequence = (arg, pickIndex, constant) => {
|
|
478
|
+
const fns = arg.parsers.map(plan)
|
|
479
|
+
const count = fns.length
|
|
480
|
+
return (state) => {
|
|
481
|
+
tick(state)
|
|
482
|
+
const start = state.idx
|
|
483
|
+
let picked
|
|
484
|
+
for (let i = 0; i < count; i++) {
|
|
485
|
+
const value = fns[i](state)
|
|
486
|
+
if (value === FAIL) {
|
|
487
|
+
state.idx = start
|
|
488
|
+
return FAIL
|
|
489
|
+
}
|
|
490
|
+
if (i === pickIndex) picked = value
|
|
491
|
+
}
|
|
492
|
+
return pickIndex === -1 ? constant : picked
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const buildPick = (arg) => buildSequence(arg, arg.index, undefined)
|
|
496
|
+
const buildYields = (arg) => buildSequence(arg, -1, arg.value)
|
|
497
|
+
|
|
470
498
|
/** @returns {(state: any) => any} */
|
|
471
499
|
const buildPrefix = (arg) => {
|
|
472
500
|
// Any number of prefix operators, folded right onto the operand:
|
|
@@ -567,6 +595,34 @@ export function createPlanner (config) {
|
|
|
567
595
|
return value
|
|
568
596
|
}
|
|
569
597
|
}
|
|
598
|
+
// A rule chosen by `selectMemoRules` remembers its result per position, so
|
|
599
|
+
// the branches of an `alt` that all start with it parse it once between
|
|
600
|
+
// them. Global `memo` already covers every rule, hence the order here.
|
|
601
|
+
const memoSlot = memoSlots.get(name)
|
|
602
|
+
if (memoSlot !== undefined) {
|
|
603
|
+
return (state) => {
|
|
604
|
+
tick(state)
|
|
605
|
+
const table = state.rm[memoSlot]
|
|
606
|
+
const at = state.idx
|
|
607
|
+
const end = table.ends[at]
|
|
608
|
+
if (end !== 0) {
|
|
609
|
+
state.idx = end - 1
|
|
610
|
+
return table.vals[at]
|
|
611
|
+
}
|
|
612
|
+
let depth = 0
|
|
613
|
+
if (state.track) {
|
|
614
|
+
depth = state.ruleDepth
|
|
615
|
+
state.ruleNames[depth] = name
|
|
616
|
+
state.ruleDepth = depth + 1
|
|
617
|
+
}
|
|
618
|
+
const value = slot.fn(state)
|
|
619
|
+
if (state.track) state.ruleDepth = depth
|
|
620
|
+
if (value === FAIL) state.idx = at
|
|
621
|
+
table.ends[at] = state.idx + 1
|
|
622
|
+
table.vals[at] = value
|
|
623
|
+
return value
|
|
624
|
+
}
|
|
625
|
+
}
|
|
570
626
|
return (state) => {
|
|
571
627
|
tick(state)
|
|
572
628
|
// See above: rule-stack bookkeeping is failure-path only.
|
|
@@ -664,6 +720,9 @@ export function createPlanner (config) {
|
|
|
664
720
|
infixRight: buildInfixRight,
|
|
665
721
|
as: buildAs,
|
|
666
722
|
between: buildBetween,
|
|
723
|
+
flag: buildFlag,
|
|
724
|
+
pick: buildPick,
|
|
725
|
+
yields: buildYields,
|
|
667
726
|
prefix: buildPrefix,
|
|
668
727
|
postfix: buildPostfix,
|
|
669
728
|
manySep: (arg) => planSeparated(arg, false),
|
|
@@ -690,7 +749,7 @@ export function createPlanner (config) {
|
|
|
690
749
|
// A `seq` written in one of the recognized idioms is planned as the cheaper
|
|
691
750
|
// combinator instead, so grammars written with the general primitives get
|
|
692
751
|
// the same treatment as ones written with the specific ones.
|
|
693
|
-
const idiom = recognizeSeqIdiom(children)
|
|
752
|
+
const idiom = idioms ? recognizeSeqIdiom(children) : null
|
|
694
753
|
if (idiom !== null) return planUncached(idiom)
|
|
695
754
|
|
|
696
755
|
// Step kinds. Terminals are inlined rather than dispatched to a closure:
|
|
@@ -853,7 +912,10 @@ export function createPlanner (config) {
|
|
|
853
912
|
// A shared, possibly multi-token prefix across every branch is matched
|
|
854
913
|
// once here rather than once per branch that has it — see the comment on
|
|
855
914
|
// recognizeAltPrefixIdiom for the safety conditions.
|
|
856
|
-
const
|
|
915
|
+
const oneOf = idioms ? recognizeAltConsumeIdiom(branches) : null
|
|
916
|
+
if (oneOf !== null) return planUncached(oneOf)
|
|
917
|
+
|
|
918
|
+
const factored = idioms ? recognizeAltPrefixIdiom(branches) : null
|
|
857
919
|
if (factored !== null) return planUncached(factored)
|
|
858
920
|
|
|
859
921
|
const planned = branches.map(plan)
|
package/src/runtime.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* The surface a generated parser module needs at run time.
|
|
6
6
|
*
|
|
7
7
|
* `emitModule` writes files that import from `grammatik/runtime` rather than
|
|
8
|
-
* from the main entry point, so a generated parser pulls in the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* from the main entry point, so a generated parser pulls in the parse state and
|
|
9
|
+
* error construction — and not the analyzer, the planner or the code generator,
|
|
10
|
+
* none of which it can ever use. The grammar was already analyzed when the file
|
|
11
|
+
* was written, and so, by default, was its lexer (see `lexgen.js`): the file
|
|
12
|
+
* carries that as source and needs only `LexError` from here. `createLexer` is
|
|
13
|
+
* exported for files emitted with `lexer: 'runtime'`.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
export { FAIL, expect, createState, buildParseError, ParseError } from './state.js'
|
|
15
|
-
export { createLexer
|
|
16
|
+
export { FAIL, expect, createState, buildParseError, ParseError, LexError } from './state.js'
|
|
17
|
+
export { createLexer } from './lexer.js'
|
|
16
18
|
export { createValueMethods } from './values.js'
|