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/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
- const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval, tokenIds })
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 }} config
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 factored = recognizeAltPrefixIdiom(branches)
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 lexer, the parse
9
- * state and error construction — but not the analyzer, the planner, or the code
10
- * generator, none of which it can ever use. The grammar was already analyzed
11
- * when the file was written.
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, LexError } from './lexer.js'
16
+ export { FAIL, expect, createState, buildParseError, ParseError, LexError } from './state.js'
17
+ export { createLexer } from './lexer.js'
16
18
  export { createValueMethods } from './values.js'