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/runtime.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The surface a generated parser module needs at run time.
|
|
6
|
+
*
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { FAIL, expect, createState, buildParseError, ParseError } from './state.js'
|
|
15
|
+
export { createLexer, LexError } from './lexer.js'
|
|
16
|
+
export { createValueMethods } from './values.js'
|
package/src/state.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Records that `expectation` would have been accepted at the cursor, keeping
|
|
3
|
+
* only the *furthest* position — the alternative that got deepest is almost
|
|
4
|
+
* always the one the author meant.
|
|
5
|
+
*
|
|
6
|
+
* Tracking is off during the first parse. `expect` fires on every failed token
|
|
7
|
+
* match, so the bookkeeping (acceptable tokens, rule stack) is most of the work
|
|
8
|
+
* in a backtracking grammar — slicing the rule stack per advance measured ~50x.
|
|
9
|
+
* On failure `parseTokens` re-runs with `track` on and builds the message from
|
|
10
|
+
* that; the parser is deterministic, so it lands in the same place.
|
|
11
|
+
*
|
|
12
|
+
* @param {ParserState} state
|
|
13
|
+
* @param {string} expectation
|
|
14
|
+
*/
|
|
15
|
+
export function expect(state: ParserState, expectation: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* @param {ParserState} state
|
|
18
|
+
* @returns {ParseError}
|
|
19
|
+
*/
|
|
20
|
+
export function buildParseError(state: ParserState): ParseError;
|
|
21
|
+
/**
|
|
22
|
+
* The sentinel returned by any parser that did not match. A symbol, so a rule
|
|
23
|
+
* stays free to legitimately produce `null`/`undefined`, and nothing in JSON
|
|
24
|
+
* Logic can forge it.
|
|
25
|
+
*/
|
|
26
|
+
export const FAIL: unique symbol;
|
|
27
|
+
export function createState(tokens: import("./lexer.js").Token[], grammar: Record<string, any>, options?: {
|
|
28
|
+
memo?: boolean;
|
|
29
|
+
maxSteps?: number;
|
|
30
|
+
track?: boolean;
|
|
31
|
+
}): ParserState;
|
|
32
|
+
export class ParseError extends Error {
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} message
|
|
35
|
+
* @param {{ token: import('./lexer.js').Token | null, expected: string[], ruleStack: string[] }} details
|
|
36
|
+
*/
|
|
37
|
+
constructor(message: string, details: {
|
|
38
|
+
token: import("./lexer.js").Token | null;
|
|
39
|
+
expected: string[];
|
|
40
|
+
ruleStack: string[];
|
|
41
|
+
});
|
|
42
|
+
token: import("./lexer.js").Token;
|
|
43
|
+
expected: string[];
|
|
44
|
+
ruleStack: string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One mutable object threaded through the whole parse as the JSON Logic
|
|
48
|
+
* *context*. Mutation rather than a rebuilt state per step is what keeps this
|
|
49
|
+
* fast: `{"val": "idx"}` reads the cursor with no allocation, and backtracking
|
|
50
|
+
* is a single integer assignment.
|
|
51
|
+
*/
|
|
52
|
+
export type ParserState = {
|
|
53
|
+
tokens: import("./lexer.js").Token[];
|
|
54
|
+
/**
|
|
55
|
+
* Cursor into `tokens`.
|
|
56
|
+
*/
|
|
57
|
+
idx: number;
|
|
58
|
+
/**
|
|
59
|
+
* Furthest cursor reached, for error reporting.
|
|
60
|
+
*/
|
|
61
|
+
maxIdx: number;
|
|
62
|
+
/**
|
|
63
|
+
* Token types acceptable at `maxIdx`.
|
|
64
|
+
*/
|
|
65
|
+
expected: string[];
|
|
66
|
+
/**
|
|
67
|
+
* Rule name -> JSON Logic.
|
|
68
|
+
*/
|
|
69
|
+
grammar: Record<string, any>;
|
|
70
|
+
/**
|
|
71
|
+
* Packrat cache, when enabled.
|
|
72
|
+
*/
|
|
73
|
+
memo: Map<string, any> | null;
|
|
74
|
+
/**
|
|
75
|
+
* Rule-name stack, written by index, never popped.
|
|
76
|
+
*/
|
|
77
|
+
ruleNames: string[];
|
|
78
|
+
/**
|
|
79
|
+
* Live depth within `ruleNames`.
|
|
80
|
+
*/
|
|
81
|
+
ruleDepth: number;
|
|
82
|
+
/**
|
|
83
|
+
* Snapshot taken at the furthest failure.
|
|
84
|
+
*/
|
|
85
|
+
failureStack: string[] | null;
|
|
86
|
+
/**
|
|
87
|
+
* Whether to record error-reporting detail.
|
|
88
|
+
*/
|
|
89
|
+
track: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Combinator invocations, for the runaway guard.
|
|
92
|
+
*/
|
|
93
|
+
steps: number;
|
|
94
|
+
/**
|
|
95
|
+
* 0 disables the guard.
|
|
96
|
+
*/
|
|
97
|
+
maxSteps: number;
|
|
98
|
+
};
|
package/src/state.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The sentinel returned by any parser that did not match. A symbol, so a rule
|
|
6
|
+
* stays free to legitimately produce `null`/`undefined`, and nothing in JSON
|
|
7
|
+
* Logic can forge it.
|
|
8
|
+
*/
|
|
9
|
+
export const FAIL = Symbol('grammatik.fail')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One mutable object threaded through the whole parse as the JSON Logic
|
|
13
|
+
* *context*. Mutation rather than a rebuilt state per step is what keeps this
|
|
14
|
+
* fast: `{"val": "idx"}` reads the cursor with no allocation, and backtracking
|
|
15
|
+
* is a single integer assignment.
|
|
16
|
+
*
|
|
17
|
+
* @typedef {object} ParserState
|
|
18
|
+
* @property {import('./lexer.js').Token[]} tokens
|
|
19
|
+
* @property {number} idx Cursor into `tokens`.
|
|
20
|
+
* @property {number} maxIdx Furthest cursor reached, for error reporting.
|
|
21
|
+
* @property {string[]} expected Token types acceptable at `maxIdx`.
|
|
22
|
+
* @property {Record<string, any>} grammar Rule name -> JSON Logic.
|
|
23
|
+
* @property {Map<string, any>|null} memo Packrat cache, when enabled.
|
|
24
|
+
* @property {string[]} ruleNames Rule-name stack, written by index, never popped.
|
|
25
|
+
* @property {number} ruleDepth Live depth within `ruleNames`.
|
|
26
|
+
* @property {string[]|null} failureStack Snapshot taken at the furthest failure.
|
|
27
|
+
* @property {boolean} track Whether to record error-reporting detail.
|
|
28
|
+
* @property {number} steps Combinator invocations, for the runaway guard.
|
|
29
|
+
* @property {number} maxSteps 0 disables the guard.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {import('./lexer.js').Token[]} tokens
|
|
34
|
+
* @param {Record<string, any>} grammar
|
|
35
|
+
* @param {{ memo?: boolean, maxSteps?: number, track?: boolean }} [options]
|
|
36
|
+
* @returns {ParserState}
|
|
37
|
+
*/
|
|
38
|
+
export const createState = (tokens, grammar, options = {}) => ({
|
|
39
|
+
tokens,
|
|
40
|
+
idx: 0,
|
|
41
|
+
maxIdx: 0,
|
|
42
|
+
expected: [],
|
|
43
|
+
grammar,
|
|
44
|
+
memo: options.memo ? new Map() : null,
|
|
45
|
+
// Entering a rule is one array store rather than an object allocation, and
|
|
46
|
+
// the stack is snapshotted only on a furthest failure — by the time an error
|
|
47
|
+
// is built it would otherwise have unwound to just the start rule.
|
|
48
|
+
ruleNames: [],
|
|
49
|
+
ruleDepth: 0,
|
|
50
|
+
failureStack: null,
|
|
51
|
+
track: options.track === true,
|
|
52
|
+
steps: 0,
|
|
53
|
+
maxSteps: options.maxSteps || 0
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Records that `expectation` would have been accepted at the cursor, keeping
|
|
58
|
+
* only the *furthest* position — the alternative that got deepest is almost
|
|
59
|
+
* always the one the author meant.
|
|
60
|
+
*
|
|
61
|
+
* Tracking is off during the first parse. `expect` fires on every failed token
|
|
62
|
+
* match, so the bookkeeping (acceptable tokens, rule stack) is most of the work
|
|
63
|
+
* in a backtracking grammar — slicing the rule stack per advance measured ~50x.
|
|
64
|
+
* On failure `parseTokens` re-runs with `track` on and builds the message from
|
|
65
|
+
* that; the parser is deterministic, so it lands in the same place.
|
|
66
|
+
*
|
|
67
|
+
* @param {ParserState} state
|
|
68
|
+
* @param {string} expectation
|
|
69
|
+
*/
|
|
70
|
+
export function expect (state, expectation) {
|
|
71
|
+
if (state.idx > state.maxIdx) {
|
|
72
|
+
state.maxIdx = state.idx
|
|
73
|
+
if (!state.track) return
|
|
74
|
+
// An array, not a Set: a handful of names, so a linear scan beats hashing.
|
|
75
|
+
state.expected = []
|
|
76
|
+
state.failureStack = state.ruleNames.slice(0, state.ruleDepth)
|
|
77
|
+
}
|
|
78
|
+
if (!state.track || state.idx !== state.maxIdx) return
|
|
79
|
+
if (!state.expected.includes(expectation)) state.expected.push(expectation)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class ParseError extends Error {
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} message
|
|
85
|
+
* @param {{ token: import('./lexer.js').Token | null, expected: string[], ruleStack: string[] }} details
|
|
86
|
+
*/
|
|
87
|
+
constructor (message, details) {
|
|
88
|
+
super(message)
|
|
89
|
+
this.name = 'ParseError'
|
|
90
|
+
this.token = details.token
|
|
91
|
+
this.expected = details.expected
|
|
92
|
+
this.ruleStack = details.ruleStack
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Chevrotain-flavoured message built from the furthest failure. */
|
|
97
|
+
const describeExpected = (expected) =>
|
|
98
|
+
expected.length === 0 ? 'nothing'
|
|
99
|
+
: expected.length === 1 ? expected[0]
|
|
100
|
+
: `one of [${expected.join(', ')}]`
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {ParserState} state
|
|
104
|
+
* @returns {ParseError}
|
|
105
|
+
*/
|
|
106
|
+
export function buildParseError (state) {
|
|
107
|
+
const token = state.tokens[state.maxIdx] || null
|
|
108
|
+
const expected = [...state.expected].sort()
|
|
109
|
+
const ruleStack = state.failureStack || state.ruleNames.slice(0, state.ruleDepth)
|
|
110
|
+
|
|
111
|
+
// Without line tracking there is no line to name, so report the offset.
|
|
112
|
+
const at = (t) => state.positions === 'offset'
|
|
113
|
+
? `offset ${t.start}`
|
|
114
|
+
: `line ${t.line} column ${t.col}`
|
|
115
|
+
const found = token ? `--> ${token.image} <-- (${token.type}) at ${at(token)}` : 'end of input'
|
|
116
|
+
|
|
117
|
+
return new ParseError(
|
|
118
|
+
`Expecting ${describeExpected(expected)} but found ${found}`,
|
|
119
|
+
{ token, expected, ruleStack }
|
|
120
|
+
)
|
|
121
|
+
}
|
package/src/values.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JSON Logic methods available inside semantic actions, beyond the engine's own.
|
|
6
|
+
*
|
|
7
|
+
* Deliberately a module of its own with no imports. It is needed in four places
|
|
8
|
+
* — `createParser`'s engine, the emitter's compile engine, and the engine a
|
|
9
|
+
* generated module builds when it still has one — and the last of those reaches
|
|
10
|
+
* it through `grammatik/runtime`, which must not grow a dependency on the
|
|
11
|
+
* planner or the analyzer to get here.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `obj` — build an object whose keys are computed.
|
|
16
|
+
*
|
|
17
|
+
* {"obj": [{"val": "op"}, [{"val": "left"}, {"val": "right"}]]}
|
|
18
|
+
* -> {"+": [1, 2]} when `op` is "+"
|
|
19
|
+
*
|
|
20
|
+
* This is the one thing a parser action does constantly that JSON Logic could
|
|
21
|
+
* not express. Building a node whose *type* comes from the input — `binary`,
|
|
22
|
+
* `func`, `call`, every "wrap this in an operator" helper in every grammar here
|
|
23
|
+
* — was the single most common reason to drop into JavaScript, and none of those
|
|
24
|
+
* helpers were doing anything a data form could not.
|
|
25
|
+
*
|
|
26
|
+
* Arguments are flat key/value pairs, so `{"obj": [k, v]}` is the common case and
|
|
27
|
+
* `{"obj": [k1, v1, k2, v2]}` builds two entries. `eachKey` already covers static
|
|
28
|
+
* keys; this covers the rest.
|
|
29
|
+
*/
|
|
30
|
+
const badArity = (length) =>
|
|
31
|
+
new Error(`obj expects an even number of arguments (key, value, ...), received ${length}`)
|
|
32
|
+
|
|
33
|
+
const objectFrom = (args) => {
|
|
34
|
+
if (!Array.isArray(args) || args.length === 0 || args.length % 2 !== 0) {
|
|
35
|
+
throw badArity(Array.isArray(args) ? args.length : 1)
|
|
36
|
+
}
|
|
37
|
+
const result = {}
|
|
38
|
+
for (let i = 0; i < args.length; i += 2) result[args[i]] = args[i + 1]
|
|
39
|
+
return result
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Emits `({[k]: v, [k]: v})` with each key and value compiled in place, so the
|
|
44
|
+
* whole thing is one object literal with no call into the engine.
|
|
45
|
+
*
|
|
46
|
+
* This hook is not an optimization. Without it the emitter cannot turn an action
|
|
47
|
+
* using `obj` into source, so the generated parser would fall back to shipping
|
|
48
|
+
* the action as data and carrying json-logic-engine — a new operator that
|
|
49
|
+
* quietly cost a grammar its engine-free build would be worse than no operator.
|
|
50
|
+
*/
|
|
51
|
+
const compileObject = (args, buildState) => {
|
|
52
|
+
if (!Array.isArray(args) || args.length === 0 || args.length % 2 !== 0) return false
|
|
53
|
+
const strings = ['({[']
|
|
54
|
+
const items = []
|
|
55
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
56
|
+
items.push(args[i])
|
|
57
|
+
strings.push(']: ')
|
|
58
|
+
items.push(args[i + 1])
|
|
59
|
+
strings.push(i + 2 < args.length ? ', [' : '})')
|
|
60
|
+
}
|
|
61
|
+
return buildState.compile(strings, ...items)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @returns {Record<string, any>} */
|
|
65
|
+
export const createValueMethods = () => ({
|
|
66
|
+
obj: { method: objectFrom, compile: compileObject }
|
|
67
|
+
})
|