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/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,
@@ -114,9 +133,23 @@ export function createParser (spec, options = {}) {
114
133
  const startRule = rules[start]
115
134
  const firsts = ll1 ? analysis.firsts : null
116
135
 
136
+ // Declaration index of each token name, the same numbering `createLexer`
137
+ // assigns to the tokens it produces (both derive it from the same array in
138
+ // the same order). `consume` compares this instead of the string `type`
139
+ // where it can — an integer compare instead of a string compare, on the
140
+ // hottest path in the parser.
141
+ const tokenIds = new Map(lexer.tokenNames.map((name, id) => [name, id]))
142
+
117
143
  // The planner specializes the grammar into closures; the combinators'
118
144
  // `compile` hooks reach it through the engine.
119
- const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval })
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 })
120
153
  engine.__jlGrammar = { planner }
121
154
 
122
155
  // Source generation emits one JS function per rule with every node inlined,
@@ -125,7 +158,7 @@ export function createParser (spec, options = {}) {
125
158
  // lands on the generated code rather than a method dispatch.
126
159
  // Code generation needs `new Function`, so it is off whenever eval is.
127
160
  const codegen = compile && unsafeEval
128
- ? createCodegen({ planner, rules, firsts, memo, maxSteps })
161
+ ? createCodegen({ planner, rules, firsts, memo, maxSteps, tokenIds, memoSlots, inlineRules, idioms })
129
162
  : null
130
163
  const compiledStart = codegen && codegen.compile(start)
131
164
 
@@ -142,7 +175,7 @@ export function createParser (spec, options = {}) {
142
175
  * @param {string} [source] Original text, so `text` can slice exact spans.
143
176
  */
144
177
  function parseTokens (tokens, source, track = false) {
145
- const state = createState(tokens, rules, { memo, maxSteps, track })
178
+ const state = createState(tokens, rules, { memo, maxSteps, track, memoSlots: memoRules.length })
146
179
  state.firsts = firsts
147
180
  state.source = source
148
181
  state.positions = lexer.positions
@@ -185,6 +218,8 @@ export function createParser (spec, options = {}) {
185
218
  planner,
186
219
  codegen,
187
220
  execution: mode,
221
+ /** Rules memoized by `autoMemo`, for introspection. */
222
+ memoRules,
188
223
  compiled: Boolean(compiledStart),
189
224
  /** Rules whose `alt`s resolved to O(1) LL(1) dispatch, for introspection. */
190
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 } 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.
@@ -76,10 +55,12 @@ function mayReturnContext (logic) {
76
55
 
77
56
  /**
78
57
  * @param {{ engine: any, rules: Record<string, any>, firsts: Map<string, any>|null,
79
- * memo: boolean, maxSteps: number, unsafeEval?: boolean }} config
58
+ * memo: boolean, maxSteps: number, unsafeEval?: boolean,
59
+ * tokenIds?: Map<string, number>|null,
60
+ * memoSlots?: Map<string, number>, idioms?: boolean }} config
80
61
  */
81
62
  export function createPlanner (config) {
82
- const { engine, rules, firsts, memo, maxSteps, unsafeEval = true } = config
63
+ const { engine, rules, firsts, memo, maxSteps, unsafeEval = true, tokenIds = null, memoSlots = new Map(), idioms = true } = config
83
64
 
84
65
  /** @type {Map<string, { fn: ((state: any) => any) | null }>} */
85
66
  const slots = new Map()
@@ -227,9 +208,14 @@ export function createPlanner (config) {
227
208
  /** @returns {(state: any) => any} */
228
209
  const buildConsume = (arg) => {
229
210
  const name = typeof arg === 'string' ? arg : arg[0]
211
+ const id = tokenIds ? tokenIds.get(name) : undefined
212
+ // `token.id` is undefined only for a hand-built token that bypassed
213
+ // `createLexer` (parseTokens accepts any array shaped like a Token), in
214
+ // which case falling back to the string compare keeps that documented
215
+ // path working; every token this parser's own lexer produces has an id.
230
216
  return (state) => {
231
217
  const token = state.tokens[state.idx]
232
- if (token !== undefined && token.type === name) {
218
+ if (token !== undefined && (token.id === id || (token.id === undefined && token.type === name))) {
233
219
  state.idx++
234
220
  return token
235
221
  }
@@ -297,6 +283,25 @@ export function createPlanner (config) {
297
283
  }
298
284
  }
299
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
+
300
305
  /** @returns {(state: any) => any} */
301
306
  const buildInfixLeft = (arg) => {
302
307
  // A whole precedence level in one closure.
@@ -348,10 +353,11 @@ export function createPlanner (config) {
348
353
  // Terser than an `alt` of `consume`s, and a single set lookup.
349
354
  const names = Array.isArray(arg) ? arg : [arg]
350
355
  const set = new Set(names)
356
+ const idSet = tokenIds ? new Set(names.map((n) => tokenIds.get(n))) : null
351
357
  const count = names.length
352
358
  return (state) => {
353
359
  const token = state.tokens[state.idx]
354
- if (token !== undefined && set.has(token.type)) {
360
+ if (token !== undefined && (idSet ? (token.id !== undefined ? idSet.has(token.id) : set.has(token.type)) : set.has(token.type))) {
355
361
  state.idx++
356
362
  return token
357
363
  }
@@ -460,6 +466,35 @@ export function createPlanner (config) {
460
466
  }
461
467
  }
462
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
+
463
498
  /** @returns {(state: any) => any} */
464
499
  const buildPrefix = (arg) => {
465
500
  // Any number of prefix operators, folded right onto the operand:
@@ -560,6 +595,34 @@ export function createPlanner (config) {
560
595
  return value
561
596
  }
562
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
+ }
563
626
  return (state) => {
564
627
  tick(state)
565
628
  // See above: rule-stack bookkeeping is failure-path only.
@@ -657,6 +720,9 @@ export function createPlanner (config) {
657
720
  infixRight: buildInfixRight,
658
721
  as: buildAs,
659
722
  between: buildBetween,
723
+ flag: buildFlag,
724
+ pick: buildPick,
725
+ yields: buildYields,
660
726
  prefix: buildPrefix,
661
727
  postfix: buildPostfix,
662
728
  manySep: (arg) => planSeparated(arg, false),
@@ -683,7 +749,7 @@ export function createPlanner (config) {
683
749
  // A `seq` written in one of the recognized idioms is planned as the cheaper
684
750
  // combinator instead, so grammars written with the general primitives get
685
751
  // the same treatment as ones written with the specific ones.
686
- const idiom = recognizeSeqIdiom(children)
752
+ const idiom = idioms ? recognizeSeqIdiom(children) : null
687
753
  if (idiom !== null) return planUncached(idiom)
688
754
 
689
755
  // Step kinds. Terminals are inlined rather than dispatched to a closure:
@@ -700,6 +766,7 @@ export function createPlanner (config) {
700
766
  const kinds = []
701
767
  const names = [] // binding name, for the labelled kinds
702
768
  const tokens = [] // token type, for the terminal kinds
769
+ const tokenIdList = [] // token.id equivalent of `tokens`, for the terminal kinds
703
770
  const fns = []
704
771
  const actionLogic = [] // raw logic of each action child, for the reuse check
705
772
  let hasAction = false
@@ -724,11 +791,13 @@ export function createPlanner (config) {
724
791
  kinds.push(LABELLED_TERMINAL)
725
792
  names.push(name)
726
793
  tokens.push(terminal)
794
+ tokenIdList.push(tokenIds ? tokenIds.get(terminal) : undefined)
727
795
  fns.push(null)
728
796
  } else {
729
797
  kinds.push(LABELLED)
730
798
  names.push(name)
731
799
  tokens.push(null)
800
+ tokenIdList.push(undefined)
732
801
  fns.push(plan(inner))
733
802
  }
734
803
  continue
@@ -738,6 +807,7 @@ export function createPlanner (config) {
738
807
  kinds.push(ACTION)
739
808
  names.push(null)
740
809
  tokens.push(null)
810
+ tokenIdList.push(undefined)
741
811
  fns.push(planAction(child.action))
742
812
  actionLogic.push(child.action)
743
813
  hasAction = true
@@ -749,11 +819,13 @@ export function createPlanner (config) {
749
819
  kinds.push(TERMINAL)
750
820
  names.push(null)
751
821
  tokens.push(terminal)
822
+ tokenIdList.push(tokenIds ? tokenIds.get(terminal) : undefined)
752
823
  fns.push(null)
753
824
  } else {
754
825
  kinds.push(PARSER)
755
826
  names.push(null)
756
827
  tokens.push(null)
828
+ tokenIdList.push(undefined)
757
829
  fns.push(plan(child))
758
830
  }
759
831
  }
@@ -803,7 +875,7 @@ export function createPlanner (config) {
803
875
 
804
876
  if (kind === TERMINAL || kind === LABELLED_TERMINAL) {
805
877
  const token = state.tokens[state.idx]
806
- if (token !== undefined && token.type === tokens[i]) {
878
+ if (token !== undefined && (token.id === tokenIdList[i] || (token.id === undefined && token.type === tokens[i]))) {
807
879
  state.idx++
808
880
  value = token
809
881
  } else {
@@ -837,6 +909,15 @@ export function createPlanner (config) {
837
909
  function planAlt (branches) {
838
910
  if (!Array.isArray(branches)) throw new Error("'alt' expects an array of branches")
839
911
 
912
+ // A shared, possibly multi-token prefix across every branch is matched
913
+ // once here rather than once per branch that has it — see the comment on
914
+ // recognizeAltPrefixIdiom for the safety conditions.
915
+ const oneOf = idioms ? recognizeAltConsumeIdiom(branches) : null
916
+ if (oneOf !== null) return planUncached(oneOf)
917
+
918
+ const factored = idioms ? recognizeAltPrefixIdiom(branches) : null
919
+ if (factored !== null) return planUncached(factored)
920
+
840
921
  const planned = branches.map(plan)
841
922
  const table = firsts ? buildDispatch(branches, firsts) : null
842
923
 
@@ -850,8 +931,39 @@ export function createPlanner (config) {
850
931
  // internalized string is an inline-cached property load instead. The null
851
932
  // prototype also means a token type of `toString` or `constructor` cannot
852
933
  // find an inherited value.
934
+ //
935
+ // byId is the same table again, indexed by token.id instead of
936
+ // token.type — an array index instead of a property load. Used whenever
937
+ // a token has an id; byToken stays as the fallback for a hand-built
938
+ // token from parseTokens, which has none.
853
939
  const byToken = Object.create(null)
854
- for (const [token, branch] of table) byToken[token] = planned[branches.indexOf(branch)]
940
+ const byId = tokenIds ? [] : null
941
+ const groupClosures = new Map()
942
+ for (const [token, branch] of table) {
943
+ let closure = groupClosures.get(branch)
944
+ if (closure === undefined) {
945
+ closure = Array.isArray(branch)
946
+ ? (() => {
947
+ const group = branch.map((b) => planned[branches.indexOf(b)])
948
+ return (state) => {
949
+ const start = state.idx
950
+ for (let i = 0; i < group.length; i++) {
951
+ const value = group[i](state)
952
+ if (value !== FAIL) return value
953
+ state.idx = start
954
+ }
955
+ return FAIL
956
+ }
957
+ })()
958
+ : planned[branches.indexOf(branch)]
959
+ groupClosures.set(branch, closure)
960
+ }
961
+ byToken[token] = closure
962
+ if (byId) {
963
+ const id = tokenIds.get(token)
964
+ if (id !== undefined) byId[id] = closure
965
+ }
966
+ }
855
967
  const expectedTokens = [...table.keys()]
856
968
  const expectedCount = expectedTokens.length
857
969
 
@@ -859,7 +971,7 @@ export function createPlanner (config) {
859
971
  tick(state)
860
972
  const token = state.tokens[state.idx]
861
973
  if (token !== undefined) {
862
- const branch = byToken[token.type]
974
+ const branch = byId && token.id !== undefined ? byId[token.id] : byToken[token.type]
863
975
  if (branch !== undefined) return branch(state)
864
976
  }
865
977
  for (let i = 0; i < expectedCount; i++) expect(state, expectedTokens[i])
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'