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.
@@ -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;