grammatik 0.1.0
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/LICENSE +21 -0
- package/README.md +443 -0
- package/bin/grammatik.js +115 -0
- package/dist/cjs/index.js +3010 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/runtime.js +639 -0
- package/dist/esm/index.js +2959 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/runtime.js +605 -0
- package/dist/types/index.d.ts +214 -0
- package/dist/types/runtime.d.ts +40 -0
- package/package.json +65 -0
- package/src/analyze.d.ts +78 -0
- package/src/analyze.js +602 -0
- package/src/codegen.d.ts +36 -0
- package/src/codegen.js +635 -0
- package/src/definition.d.ts +133 -0
- package/src/definition.js +291 -0
- package/src/emit.d.ts +23 -0
- package/src/emit.js +374 -0
- package/src/firstchars.d.ts +6 -0
- package/src/firstchars.js +353 -0
- package/src/index.d.ts +7 -0
- package/src/index.js +15 -0
- package/src/lexer.d.ts +96 -0
- package/src/lexer.js +358 -0
- package/src/methods.d.ts +20 -0
- package/src/methods.js +61 -0
- package/src/methodsblock.js +197 -0
- package/src/parser.d.ts +130 -0
- package/src/parser.js +194 -0
- package/src/plan.d.ts +29 -0
- package/src/plan.js +938 -0
- package/src/runtime.js +16 -0
- package/src/state.d.ts +98 -0
- package/src/state.js +121 -0
- package/src/values.js +67 -0
package/src/parser.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a parser from a JSON grammar.
|
|
3
|
+
*
|
|
4
|
+
* @param {GrammarSpec} spec
|
|
5
|
+
* @param {ParserOptions} [options]
|
|
6
|
+
*/
|
|
7
|
+
export function createParser(spec: GrammarSpec, options?: ParserOptions): {
|
|
8
|
+
parse: (text: string) => any;
|
|
9
|
+
parseTokens: (tokens: import("./lexer.js").Token[], source?: string, track?: boolean) => any;
|
|
10
|
+
tokenize: (text: string) => Token[];
|
|
11
|
+
lexer: {
|
|
12
|
+
tokenize: (text: string) => Token[];
|
|
13
|
+
tokenNames: string[];
|
|
14
|
+
positions: string;
|
|
15
|
+
};
|
|
16
|
+
engine: LogicEngine;
|
|
17
|
+
analysis: {
|
|
18
|
+
firsts: Map<string, First>;
|
|
19
|
+
errors: string[];
|
|
20
|
+
leftRecursive: string[];
|
|
21
|
+
};
|
|
22
|
+
planner: {
|
|
23
|
+
plan: (node: any) => (state: any) => any;
|
|
24
|
+
planOp: (op: string, args: any) => (state: any) => any;
|
|
25
|
+
planAction: (logic: any) => (bindings: any, state: any) => any;
|
|
26
|
+
planRule: (name: any) => {
|
|
27
|
+
fn: ((state: any) => any) | null;
|
|
28
|
+
};
|
|
29
|
+
declareSync: typeof import("./plan.js").declareSync;
|
|
30
|
+
};
|
|
31
|
+
codegen: {
|
|
32
|
+
compile: (start: string) => (state: any) => any;
|
|
33
|
+
functionFor: (name: any) => any;
|
|
34
|
+
sourceFor: (name: any) => string;
|
|
35
|
+
ruleIndex: Map<any, any>;
|
|
36
|
+
RULES: any[];
|
|
37
|
+
SOURCES: any[];
|
|
38
|
+
ORIGINS: any[];
|
|
39
|
+
guard: boolean;
|
|
40
|
+
};
|
|
41
|
+
execution: "generated" | "interpreted";
|
|
42
|
+
compiled: boolean;
|
|
43
|
+
/** Rules whose `alt`s resolved to O(1) LL(1) dispatch, for introspection. */
|
|
44
|
+
readonly firsts: Map<string, import("./analyze.js").First>;
|
|
45
|
+
};
|
|
46
|
+
export class GrammarError extends Error {
|
|
47
|
+
/** @param {string[]} problems */
|
|
48
|
+
constructor(problems: string[]);
|
|
49
|
+
problems: string[];
|
|
50
|
+
}
|
|
51
|
+
export type GrammarSpec = {
|
|
52
|
+
/**
|
|
53
|
+
* Token definitions for the lexer.
|
|
54
|
+
*/
|
|
55
|
+
tokens: import("./lexer.js").TokenDef[];
|
|
56
|
+
/**
|
|
57
|
+
* Rule name -> JSON Logic parser.
|
|
58
|
+
*/
|
|
59
|
+
rules: Record<string, any>;
|
|
60
|
+
/**
|
|
61
|
+
* Entry rule.
|
|
62
|
+
*/
|
|
63
|
+
start: string;
|
|
64
|
+
};
|
|
65
|
+
export type ParserOptions = {
|
|
66
|
+
/**
|
|
67
|
+
* Enable packrat memoization. Costs a Map
|
|
68
|
+
* write per rule invocation; pays for itself
|
|
69
|
+
* only on grammars that backtrack heavily.
|
|
70
|
+
*/
|
|
71
|
+
memo?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Require the whole token stream to be consumed.
|
|
74
|
+
*/
|
|
75
|
+
strict?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Throw on grammar errors (left recursion, bad refs).
|
|
78
|
+
*/
|
|
79
|
+
validate?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Runaway guard; 0 disables.
|
|
82
|
+
*/
|
|
83
|
+
maxSteps?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Compile the grammar through json-logic-engine's
|
|
86
|
+
* compiler into a tree of specialized closures.
|
|
87
|
+
* Off falls back to interpreting the JSON Logic,
|
|
88
|
+
* which is slower but easier to step through.
|
|
89
|
+
*/
|
|
90
|
+
compile?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Use FIRST-set dispatch for `alt`. Turning it
|
|
93
|
+
* off forces ordered backtracking everywhere,
|
|
94
|
+
* which parses the same language, just slower.
|
|
95
|
+
* Mostly useful for benchmarking and debugging.
|
|
96
|
+
*/
|
|
97
|
+
ll1?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Legacy alias: `false` selects `'interpreted'`.
|
|
100
|
+
*/
|
|
101
|
+
unsafeEval?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* How the grammar is run.
|
|
104
|
+
*
|
|
105
|
+
* - `generated` one JavaScript function per rule, semantic actions compiled
|
|
106
|
+
* by the engine. The default: ~2.5x faster to parse, ~0.5 ms
|
|
107
|
+
* more to construct.
|
|
108
|
+
* - `interpreted` rules as planner closures, actions walked by the engine. No
|
|
109
|
+
* `eval` or `new Function` anywhere, which is what a strict
|
|
110
|
+
* Content-Security-Policy (no `unsafe-eval`) requires, and
|
|
111
|
+
* cheaper to construct — so it also wins when a grammar is
|
|
112
|
+
* built to parse one short input and then discarded.
|
|
113
|
+
*/
|
|
114
|
+
execution?: "generated" | "interpreted";
|
|
115
|
+
/**
|
|
116
|
+
* `full` records line and column
|
|
117
|
+
* on every token; `offset` records only
|
|
118
|
+
* byte offsets and skips scanning the
|
|
119
|
+
* input for newlines.
|
|
120
|
+
*/
|
|
121
|
+
positions?: "full" | "offset";
|
|
122
|
+
/**
|
|
123
|
+
* Extra JSON Logic methods for semantic actions.
|
|
124
|
+
*/
|
|
125
|
+
methods?: Record<string, any>;
|
|
126
|
+
};
|
|
127
|
+
import { LogicEngine } from 'json-logic-engine';
|
|
128
|
+
import { FAIL } from './state.js';
|
|
129
|
+
import { ParseError } from './state.js';
|
|
130
|
+
export { FAIL, ParseError };
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { LogicEngine } from 'json-logic-engine'
|
|
5
|
+
|
|
6
|
+
import { createLexer } from './lexer.js'
|
|
7
|
+
import { createMethods } from './methods.js'
|
|
8
|
+
import { createValueMethods } from './values.js'
|
|
9
|
+
import { analyze } from './analyze.js'
|
|
10
|
+
import { createPlanner } from './plan.js'
|
|
11
|
+
import { createCodegen } from './codegen.js'
|
|
12
|
+
import { FAIL, createState, buildParseError, ParseError } from './state.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} GrammarSpec
|
|
16
|
+
* @property {import('./lexer.js').TokenDef[]} tokens Token definitions for the lexer.
|
|
17
|
+
* @property {Record<string, any>} rules Rule name -> JSON Logic parser.
|
|
18
|
+
* @property {string} start Entry rule.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} ParserOptions
|
|
23
|
+
* @property {boolean} [memo=false] Enable packrat memoization. Costs a Map
|
|
24
|
+
* write per rule invocation; pays for itself
|
|
25
|
+
* only on grammars that backtrack heavily.
|
|
26
|
+
* @property {boolean} [strict=true] Require the whole token stream to be consumed.
|
|
27
|
+
* @property {boolean} [validate=true] Throw on grammar errors (left recursion, bad refs).
|
|
28
|
+
* @property {number} [maxSteps=0] Runaway guard; 0 disables.
|
|
29
|
+
* @property {boolean} [compile=true] Compile the grammar through json-logic-engine's
|
|
30
|
+
* compiler into a tree of specialized closures.
|
|
31
|
+
* Off falls back to interpreting the JSON Logic,
|
|
32
|
+
* which is slower but easier to step through.
|
|
33
|
+
* @property {boolean} [ll1=true] Use FIRST-set dispatch for `alt`. Turning it
|
|
34
|
+
* off forces ordered backtracking everywhere,
|
|
35
|
+
* which parses the same language, just slower.
|
|
36
|
+
* Mostly useful for benchmarking and debugging.
|
|
37
|
+
* @property {boolean} [compile] Legacy alias: `false` selects `'interpreted'`.
|
|
38
|
+
* @property {boolean} [unsafeEval] Legacy alias: `false` selects `'interpreted'`.
|
|
39
|
+
* @property {'generated'|'interpreted'} [execution='generated']
|
|
40
|
+
* How the grammar is run.
|
|
41
|
+
*
|
|
42
|
+
* - `generated` one JavaScript function per rule, semantic actions compiled
|
|
43
|
+
* by the engine. The default: ~2.5x faster to parse, ~0.5 ms
|
|
44
|
+
* more to construct.
|
|
45
|
+
* - `interpreted` rules as planner closures, actions walked by the engine. No
|
|
46
|
+
* `eval` or `new Function` anywhere, which is what a strict
|
|
47
|
+
* Content-Security-Policy (no `unsafe-eval`) requires, and
|
|
48
|
+
* cheaper to construct — so it also wins when a grammar is
|
|
49
|
+
* built to parse one short input and then discarded.
|
|
50
|
+
* @property {'full'|'offset'} [positions='full'] `full` records line and column
|
|
51
|
+
* on every token; `offset` records only
|
|
52
|
+
* byte offsets and skips scanning the
|
|
53
|
+
* input for newlines.
|
|
54
|
+
* @property {Record<string, any>} [methods] Extra JSON Logic methods for semantic actions.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
const EXECUTION_MODES = new Set(['generated', 'interpreted'])
|
|
58
|
+
|
|
59
|
+
export class GrammarError extends Error {
|
|
60
|
+
/** @param {string[]} problems */
|
|
61
|
+
constructor (problems) {
|
|
62
|
+
super(`Invalid grammar:\n - ${problems.join('\n - ')}`)
|
|
63
|
+
this.name = 'GrammarError'
|
|
64
|
+
this.problems = problems
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Builds a parser from a JSON grammar.
|
|
70
|
+
*
|
|
71
|
+
* @param {GrammarSpec} spec
|
|
72
|
+
* @param {ParserOptions} [options]
|
|
73
|
+
*/
|
|
74
|
+
export function createParser (spec, options = {}) {
|
|
75
|
+
const { tokens: tokenDefs, rules, start } = spec
|
|
76
|
+
const {
|
|
77
|
+
memo = false,
|
|
78
|
+
strict = true,
|
|
79
|
+
validate = true,
|
|
80
|
+
maxSteps = 0,
|
|
81
|
+
ll1 = true,
|
|
82
|
+
execution = 'generated',
|
|
83
|
+
positions = 'full',
|
|
84
|
+
methods: extraMethods
|
|
85
|
+
} = options
|
|
86
|
+
|
|
87
|
+
// `compile` and `unsafeEval` were the booleans this replaced. Either one set
|
|
88
|
+
// to false selects the interpreter; `execution` wins when both are given.
|
|
89
|
+
const mode = options.execution !== undefined
|
|
90
|
+
? execution
|
|
91
|
+
: (options.compile === false || options.unsafeEval === false) ? 'interpreted' : 'generated'
|
|
92
|
+
|
|
93
|
+
if (!EXECUTION_MODES.has(mode)) {
|
|
94
|
+
throw new Error(`createParser: \`execution\` must be one of ${[...EXECUTION_MODES].join(', ')}, got ${JSON.stringify(mode)}`)
|
|
95
|
+
}
|
|
96
|
+
const compile = mode === 'generated'
|
|
97
|
+
const unsafeEval = mode === 'generated'
|
|
98
|
+
|
|
99
|
+
if (!rules || typeof rules !== 'object') throw new Error('createParser: `rules` must be an object of rule name -> logic')
|
|
100
|
+
if (!start) throw new Error('createParser: `start` is required')
|
|
101
|
+
if (!(start in rules)) throw new Error(`createParser: start rule '${start}' is not defined in rules`)
|
|
102
|
+
|
|
103
|
+
const lexer = createLexer(tokenDefs, { positions })
|
|
104
|
+
|
|
105
|
+
const analysis = analyze(rules, { tokenNames: lexer.tokenNames })
|
|
106
|
+
if (validate && analysis.errors.length > 0) throw new GrammarError(analysis.errors)
|
|
107
|
+
|
|
108
|
+
const engine = new LogicEngine()
|
|
109
|
+
for (const [name, method] of Object.entries(createMethods())) engine.addMethod(name, method)
|
|
110
|
+
// Value methods before the caller's, so a grammar can override one.
|
|
111
|
+
for (const [name, method] of Object.entries(createValueMethods())) engine.addMethod(name, method)
|
|
112
|
+
for (const [name, method] of Object.entries(extraMethods || {})) engine.addMethod(name, method)
|
|
113
|
+
|
|
114
|
+
const startRule = rules[start]
|
|
115
|
+
const firsts = ll1 ? analysis.firsts : null
|
|
116
|
+
|
|
117
|
+
// The planner specializes the grammar into closures; the combinators'
|
|
118
|
+
// `compile` hooks reach it through the engine.
|
|
119
|
+
const planner = createPlanner({ engine, rules, firsts, memo, maxSteps, unsafeEval })
|
|
120
|
+
engine.__jlGrammar = { planner }
|
|
121
|
+
|
|
122
|
+
// Source generation emits one JS function per rule with every node inlined,
|
|
123
|
+
// so only rule boundaries remain calls. Generated functions are published into
|
|
124
|
+
// the engine's `optimizedMap` too, so reaching a rule through `engine.run`
|
|
125
|
+
// lands on the generated code rather than a method dispatch.
|
|
126
|
+
// Code generation needs `new Function`, so it is off whenever eval is.
|
|
127
|
+
const codegen = compile && unsafeEval
|
|
128
|
+
? createCodegen({ planner, rules, firsts, memo, maxSteps })
|
|
129
|
+
: null
|
|
130
|
+
const compiledStart = codegen && codegen.compile(start)
|
|
131
|
+
|
|
132
|
+
if (codegen && !engine.disableInterpretedOptimization) {
|
|
133
|
+
for (const name of Object.keys(rules)) {
|
|
134
|
+
const generated = codegen.functionFor(name)
|
|
135
|
+
if (generated) engine.optimizedMap.set(rules[name], generated)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parses a token stream that you lexed yourself.
|
|
141
|
+
* @param {import('./lexer.js').Token[]} tokens
|
|
142
|
+
* @param {string} [source] Original text, so `text` can slice exact spans.
|
|
143
|
+
*/
|
|
144
|
+
function parseTokens (tokens, source, track = false) {
|
|
145
|
+
const state = createState(tokens, rules, { memo, maxSteps, track })
|
|
146
|
+
state.firsts = firsts
|
|
147
|
+
state.source = source
|
|
148
|
+
state.positions = lexer.positions
|
|
149
|
+
state.ruleNames[0] = start
|
|
150
|
+
state.ruleDepth = 1
|
|
151
|
+
|
|
152
|
+
const result = compiledStart ? compiledStart(state) : engine.run(startRule, state, { above: [] })
|
|
153
|
+
const trailing = strict && state.idx < tokens.length
|
|
154
|
+
if (result !== FAIL && !trailing) return result
|
|
155
|
+
|
|
156
|
+
// Re-run with error bookkeeping on, so the message can name what was
|
|
157
|
+
// expected and where. Only failures pay for it.
|
|
158
|
+
if (!track) return parseTokens(tokens, source, true)
|
|
159
|
+
|
|
160
|
+
// A parse that succeeded but stopped early: the furthest failure is a better
|
|
161
|
+
// error site than the cursor, so prefer it when it is ahead.
|
|
162
|
+
if (trailing && state.maxIdx < state.idx) {
|
|
163
|
+
state.maxIdx = state.idx
|
|
164
|
+
state.expected = ['end of input']
|
|
165
|
+
state.failureStack = state.ruleNames.slice(0, state.ruleDepth)
|
|
166
|
+
}
|
|
167
|
+
throw buildParseError(state)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Lexes and parses text.
|
|
172
|
+
* @param {string} text
|
|
173
|
+
*/
|
|
174
|
+
function parse (text) {
|
|
175
|
+
return parseTokens(lexer.tokenize(text), text)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
parse,
|
|
180
|
+
parseTokens,
|
|
181
|
+
tokenize: lexer.tokenize,
|
|
182
|
+
lexer,
|
|
183
|
+
engine,
|
|
184
|
+
analysis,
|
|
185
|
+
planner,
|
|
186
|
+
codegen,
|
|
187
|
+
execution: mode,
|
|
188
|
+
compiled: Boolean(compiledStart),
|
|
189
|
+
/** Rules whose `alt`s resolved to O(1) LL(1) dispatch, for introspection. */
|
|
190
|
+
get firsts () { return analysis.firsts }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { FAIL, ParseError }
|
package/src/plan.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{ engine: any, rules: Record<string, any>, firsts: Map<string, any>|null,
|
|
3
|
+
* memo: boolean, maxSteps: number, unsafeEval?: boolean }} config
|
|
4
|
+
*/
|
|
5
|
+
export function createPlanner(config: {
|
|
6
|
+
engine: any;
|
|
7
|
+
rules: Record<string, any>;
|
|
8
|
+
firsts: Map<string, any> | null;
|
|
9
|
+
memo: boolean;
|
|
10
|
+
maxSteps: number;
|
|
11
|
+
unsafeEval?: boolean;
|
|
12
|
+
}): {
|
|
13
|
+
plan: (node: any) => (state: any) => any;
|
|
14
|
+
planOp: (op: string, args: any) => (state: any) => any;
|
|
15
|
+
planAction: (logic: any) => (bindings: any, state: any) => any;
|
|
16
|
+
planRule: (name: any) => {
|
|
17
|
+
fn: ((state: any) => any) | null;
|
|
18
|
+
};
|
|
19
|
+
/** Wraps a plan so json-logic-engine's compiler can embed it. */
|
|
20
|
+
declareSync: typeof declareSync;
|
|
21
|
+
};
|
|
22
|
+
/** Marks a closure as synchronous so the engine's compiler inlines it directly. */
|
|
23
|
+
export function declareSync(fn: any): any;
|
|
24
|
+
/**
|
|
25
|
+
* True when the logic reads from an outer scope (`{val: [[-1], ...]}` or
|
|
26
|
+
* `{var: "../x"}`). The engine's compiler does not thread `above` into built
|
|
27
|
+
* functions, so such actions must stay interpreted.
|
|
28
|
+
*/
|
|
29
|
+
export function usesOuterScope(logic: any): boolean;
|