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/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
+ })