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,133 @@
1
+ export function createMetaMethods(): {};
2
+ export const definitionTokens: ({
3
+ name: any;
4
+ literal: any;
5
+ longerAlt: string;
6
+ } | {
7
+ name: string;
8
+ pattern: string;
9
+ skip: boolean;
10
+ literal?: undefined;
11
+ } | {
12
+ name: string;
13
+ pattern: string;
14
+ skip?: undefined;
15
+ literal?: undefined;
16
+ } | {
17
+ name: string;
18
+ literal: string;
19
+ pattern?: undefined;
20
+ skip?: undefined;
21
+ })[];
22
+ export namespace definitionRules {
23
+ export namespace Document {
24
+ export { items as seq };
25
+ }
26
+ export namespace Declaration {
27
+ export { items as alt };
28
+ }
29
+ export namespace TokenDeclaration { }
30
+ export namespace TokenKind { }
31
+ export namespace TokenFlag { }
32
+ export namespace RuleDeclaration { }
33
+ export namespace ParserExpression {
34
+ export { name as subrule };
35
+ }
36
+ export namespace Call { }
37
+ export namespace CallArguments { }
38
+ export namespace CallArgument { }
39
+ export namespace JsonValue { }
40
+ export namespace JsonObject { }
41
+ export namespace JsonPair { }
42
+ export namespace JsonArray { }
43
+ export { identifier as IdentifierValue };
44
+ export { stringValue as StringValue };
45
+ export namespace NumberValue { }
46
+ }
47
+ export namespace definitionGrammar {
48
+ export { definitionTokens as tokens };
49
+ export { definitionRules as rules };
50
+ export let start: string;
51
+ }
52
+ /** Constructor names the language understands. */
53
+ export const CONSTRUCTOR_NAMES: string[];
54
+ export function createDefinitionParser(options?: {}): {
55
+ parse: (text: string) => any;
56
+ parseTokens: (tokens: import("./lexer.js").Token[], source?: string, track?: boolean) => any;
57
+ tokenize: (text: string) => Token[];
58
+ lexer: {
59
+ tokenize: (text: string) => Token[];
60
+ tokenNames: string[];
61
+ positions: string;
62
+ };
63
+ engine: import("json-logic-engine").LogicEngine;
64
+ analysis: {
65
+ firsts: Map<string, First>;
66
+ errors: string[];
67
+ leftRecursive: string[];
68
+ };
69
+ planner: {
70
+ plan: (node: any) => (state: any) => any;
71
+ planOp: (op: string, args: any) => (state: any) => any;
72
+ planAction: (logic: any) => (bindings: any, state: any) => any;
73
+ planRule: (name: any) => {
74
+ fn: ((state: any) => any) | null;
75
+ };
76
+ declareSync: typeof import("./plan.js").declareSync;
77
+ };
78
+ codegen: {
79
+ compile: (start: string) => (state: any) => any;
80
+ functionFor: (name: any) => any;
81
+ sourceFor: (name: any) => string;
82
+ ruleIndex: Map<any, any>;
83
+ RULES: any[];
84
+ SOURCES: any[];
85
+ ORIGINS: any[];
86
+ guard: boolean;
87
+ };
88
+ execution: "generated" | "interpreted";
89
+ compiled: boolean;
90
+ readonly firsts: Map<string, import("./analyze.js").First>;
91
+ };
92
+ export function parseDefinition(source: string, options?: object): any;
93
+ export function createParserFromSource(source: string, options?: object): {
94
+ parse: (text: string) => any;
95
+ parseTokens: (tokens: import("./lexer.js").Token[], source?: string, track?: boolean) => any;
96
+ tokenize: (text: string) => Token[];
97
+ lexer: {
98
+ tokenize: (text: string) => Token[];
99
+ tokenNames: string[];
100
+ positions: string;
101
+ };
102
+ engine: import("json-logic-engine").LogicEngine;
103
+ analysis: {
104
+ firsts: Map<string, First>;
105
+ errors: string[];
106
+ leftRecursive: string[];
107
+ };
108
+ planner: {
109
+ plan: (node: any) => (state: any) => any;
110
+ planOp: (op: string, args: any) => (state: any) => any;
111
+ planAction: (logic: any) => (bindings: any, state: any) => any;
112
+ planRule: (name: any) => {
113
+ fn: ((state: any) => any) | null;
114
+ };
115
+ declareSync: typeof import("./plan.js").declareSync;
116
+ };
117
+ codegen: {
118
+ compile: (start: string) => (state: any) => any;
119
+ functionFor: (name: any) => any;
120
+ sourceFor: (name: any) => string;
121
+ ruleIndex: Map<any, any>;
122
+ RULES: any[];
123
+ SOURCES: any[];
124
+ ORIGINS: any[];
125
+ guard: boolean;
126
+ };
127
+ execution: "generated" | "interpreted";
128
+ compiled: boolean;
129
+ readonly firsts: Map<string, import("./analyze.js").First>;
130
+ };
131
+ declare namespace identifier { }
132
+ declare namespace stringValue { }
133
+ export {};
@@ -0,0 +1,291 @@
1
+ import { createParser } from './parser.js'
2
+ import { splitMethodsBlock, evaluateMethodsBlock } from './methodsblock.js'
3
+
4
+ /**
5
+ * The grammar-definition language: a small surface syntax for the same grammars
6
+ * `createParser` takes as JSON. `examples/*.gram` are written in it.
7
+ *
8
+ * It is defined by a grammar in this library's own format and parsed by this
9
+ * library — the bootstrap is the point. `test/meta-formula.test.js` asserts that
10
+ * parsing `formula.gram` produces a structure deep-equal to the hand-authored
11
+ * grammar object, so the two spellings cannot drift.
12
+ */
13
+
14
+ const consume = (name) => ({ consume: name })
15
+ const subrule = (name) => ({ subrule: name })
16
+ const label = (name, parser) => ({ label: [name, parser] })
17
+ const seq = (...items) => ({ seq: items })
18
+ const alt = (...items) => ({ alt: items })
19
+ const many = (item) => ({ many: item })
20
+ const action = (logic) => ({ action: logic })
21
+ const call = (name, value) => ({ [`meta.${name}`]: value })
22
+ const keyword = (name, literal) => ({ name, literal, longerAlt: 'Identifier' })
23
+
24
+ export const definitionTokens = [
25
+ { name: 'Whitespace', pattern: '[ \\t\\r\\n]+', skip: true },
26
+ { name: 'LineComment', pattern: '//[^\\r\\n]*', skip: true },
27
+ keyword('GrammarKeyword', 'grammar'),
28
+ keyword('StartKeyword', 'start'),
29
+ keyword('TokenKeyword', 'token'),
30
+ keyword('RuleKeyword', 'rule'),
31
+ keyword('PatternKeyword', 'pattern'),
32
+ keyword('LiteralKeyword', 'literal'),
33
+ keyword('SkipKeyword', 'skip'),
34
+ keyword('IgnoreCaseKeyword', 'ignoreCase'),
35
+ keyword('LongerAltKeyword', 'longerAlt'),
36
+ keyword('ModeKeyword', 'mode'),
37
+ keyword('PushModeKeyword', 'pushMode'),
38
+ keyword('PopModeKeyword', 'popMode'),
39
+ keyword('TrueKeyword', 'true'),
40
+ keyword('FalseKeyword', 'false'),
41
+ keyword('NullKeyword', 'null'),
42
+ { name: 'String', pattern: '"(?:\\\\.|[^"\\\\])*"' },
43
+ { name: 'Number', pattern: '-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?' },
44
+ { name: 'Identifier', pattern: '[A-Za-z_][A-Za-z0-9_]*' },
45
+ { name: 'LParen', literal: '(' },
46
+ { name: 'RParen', literal: ')' },
47
+ { name: 'LBrace', literal: '{' },
48
+ { name: 'RBrace', literal: '}' },
49
+ { name: 'LBracket', literal: '[' },
50
+ { name: 'RBracket', literal: ']' },
51
+ { name: 'Comma', literal: ',' },
52
+ { name: 'Colon', literal: ':' },
53
+ { name: 'Equals', literal: '=' },
54
+ { name: 'Semicolon', literal: ';' }
55
+ ]
56
+
57
+ const identifier = seq(label('token', consume('Identifier')), action({ val: ['token', 'image'] }))
58
+ const stringValue = seq(label('token', consume('String')), action(call('string', { val: ['token', 'image'] })))
59
+
60
+ export const definitionRules = {
61
+ Document: seq(
62
+ consume('GrammarKeyword'), label('name', subrule('IdentifierValue')), consume('Semicolon'),
63
+ consume('StartKeyword'), label('start', subrule('IdentifierValue')), consume('Semicolon'),
64
+ label('declarations', many(subrule('Declaration'))),
65
+ action(call('document', [{ val: 'name' }, { val: 'start' }, { val: 'declarations' }]))
66
+ ),
67
+ Declaration: alt(subrule('TokenDeclaration'), subrule('RuleDeclaration')),
68
+ TokenDeclaration: seq(
69
+ consume('TokenKeyword'), label('name', subrule('IdentifierValue')),
70
+ label('kind', subrule('TokenKind')), label('flags', many(subrule('TokenFlag'))), consume('Semicolon'),
71
+ action(call('token', [{ val: 'name' }, { val: 'kind' }, { val: 'flags' }]))
72
+ ),
73
+ TokenKind: alt(
74
+ seq(consume('PatternKeyword'), label('source', subrule('StringValue')), action(['pattern', { val: 'source' }])),
75
+ seq(consume('LiteralKeyword'), label('source', subrule('StringValue')), action(['literal', { val: 'source' }]))
76
+ ),
77
+ TokenFlag: alt(
78
+ seq(consume('SkipKeyword'), action(['skip', true])),
79
+ seq(consume('IgnoreCaseKeyword'), action(['ignoreCase', true])),
80
+ seq(consume('LongerAltKeyword'), label('name', subrule('IdentifierValue')), action(['longerAlt', { val: 'name' }])),
81
+ // `mode` may be repeated, for a token that serves several modes.
82
+ seq(consume('ModeKeyword'), label('name', subrule('IdentifierValue')), action(['mode', { val: 'name' }])),
83
+ seq(consume('PushModeKeyword'), label('name', subrule('IdentifierValue')), action(['pushMode', { val: 'name' }])),
84
+ seq(consume('PopModeKeyword'), action(['popMode', true]))
85
+ ),
86
+ RuleDeclaration: seq(
87
+ consume('RuleKeyword'), label('name', subrule('IdentifierValue')), consume('Equals'),
88
+ label('parser', subrule('ParserExpression')), consume('Semicolon'),
89
+ action(call('rule', [{ val: 'name' }, { val: 'parser' }]))
90
+ ),
91
+ ParserExpression: subrule('Call'),
92
+ Call: seq(
93
+ label('name', subrule('IdentifierValue')), consume('LParen'),
94
+ label('args', subrule('CallArguments')), consume('RParen'),
95
+ action(call('call', [{ val: 'name' }, { val: 'args' }]))
96
+ ),
97
+ CallArguments: alt(
98
+ seq({ lookahead: consume('RParen') }, action([])),
99
+ seq(
100
+ label('head', subrule('CallArgument')),
101
+ label('tail', many(seq(consume('Comma'), label('value', subrule('CallArgument')), action({ val: 'value' })))),
102
+ action(call('list', [{ val: 'head' }, { val: 'tail' }]))
103
+ )
104
+ ),
105
+ CallArgument: alt(subrule('Call'), subrule('JsonValue'), subrule('IdentifierValue')),
106
+ JsonValue: alt(
107
+ subrule('JsonObject'), subrule('JsonArray'), subrule('StringValue'), subrule('NumberValue'),
108
+ seq(consume('TrueKeyword'), action(true)),
109
+ seq(consume('FalseKeyword'), action(false)),
110
+ seq(consume('NullKeyword'), action(null))
111
+ ),
112
+ JsonObject: alt(
113
+ seq(consume('LBrace'), consume('RBrace'), action(call('object', []))),
114
+ seq(
115
+ consume('LBrace'), label('head', subrule('JsonPair')),
116
+ label('tail', many(seq(consume('Comma'), label('pair', subrule('JsonPair')), action({ val: 'pair' })))),
117
+ consume('RBrace'), action(call('object', [{ val: 'head' }, { val: 'tail' }]))
118
+ )
119
+ ),
120
+ JsonPair: seq(
121
+ label('key', subrule('StringValue')), consume('Colon'), label('value', subrule('JsonValue')),
122
+ action([{ val: 'key' }, { val: 'value' }])
123
+ ),
124
+ JsonArray: alt(
125
+ seq(consume('LBracket'), consume('RBracket'), action([])),
126
+ seq(
127
+ consume('LBracket'), label('head', subrule('JsonValue')),
128
+ label('tail', many(seq(consume('Comma'), label('value', subrule('JsonValue')), action({ val: 'value' })))),
129
+ consume('RBracket'), action(call('list', [{ val: 'head' }, { val: 'tail' }]))
130
+ )
131
+ ),
132
+ IdentifierValue: identifier,
133
+ StringValue: stringValue,
134
+ NumberValue: seq(label('token', consume('Number')), action(call('number', { val: ['token', 'image'] })))
135
+ }
136
+
137
+ export const definitionGrammar = { tokens: definitionTokens, rules: definitionRules, start: 'Document' }
138
+
139
+ function assertArity(name, args, minimum, maximum = minimum) {
140
+ if (args.length < minimum || args.length > maximum) {
141
+ const expected = minimum === maximum ? String(minimum) : `${minimum}-${maximum}`
142
+ throw new Error(`${name} expects ${expected} argument(s), received ${args.length}`)
143
+ }
144
+ }
145
+
146
+ export function createMetaMethods () {
147
+ const methods = {
148
+ string: (image) => JSON.parse(image),
149
+ number: (image) => Number(image),
150
+ list: ([head, tail]) => {
151
+ const output = new Array(tail.length + 1)
152
+ output[0] = head
153
+ for (let i = 0; i < tail.length; i++) output[i + 1] = tail[i]
154
+ return output
155
+ },
156
+ object: (value) => {
157
+ const output = {}
158
+ if (value.length === 0) return output
159
+ const head = value[0]
160
+ const tail = value[1]
161
+ output[head[0]] = head[1]
162
+ for (let i = 0; i < tail.length; i++) output[tail[i][0]] = tail[i][1]
163
+ return output
164
+ },
165
+ token: ([name, kind, flags]) => {
166
+ const value = { name, [kind[0]]: kind[1] }
167
+ // `mode` is the one repeatable flag: one occurrence stays `mode`, several
168
+ // collapse to `modes`, which is what a token serving two modes looks like.
169
+ const modes = []
170
+ for (let i = 0; i < flags.length; i++) {
171
+ if (flags[i][0] === 'mode') modes.push(flags[i][1])
172
+ else value[flags[i][0]] = flags[i][1]
173
+ }
174
+ if (modes.length === 1) value.mode = modes[0]
175
+ else if (modes.length > 1) value.modes = modes
176
+ return ['token', value]
177
+ },
178
+ rule: ([name, parser]) => ['rule', name, parser],
179
+ document: ([name, start, declarations]) => {
180
+ const tokens = []
181
+ const rules = {}
182
+ for (let i = 0; i < declarations.length; i++) {
183
+ const declaration = declarations[i]
184
+ if (declaration[0] === 'token') tokens.push(declaration[1])
185
+ else rules[declaration[1]] = declaration[2]
186
+ }
187
+ return { name, tokens, rules, start }
188
+ },
189
+ call: ([name, args]) => {
190
+ const build = CONSTRUCTORS[name]
191
+ if (!build) throw new Error(`Unknown parser constructor ${name}`)
192
+ if (args.length < build.arity[0] || args.length > build.arity[1]) {
193
+ const [min, max] = build.arity
194
+ const expected = min === max ? String(min) : `${min}-${max}`
195
+ throw new Error(`${name} expects ${expected} argument(s), received ${args.length}`)
196
+ }
197
+ return build.of(args)
198
+ }
199
+ }
200
+
201
+ const descriptors = {}
202
+ for (const key of Object.keys(methods)) {
203
+ descriptors[`meta.${key}`] = { method: methods[key], optimizeUnary: true }
204
+ }
205
+ return descriptors
206
+ }
207
+
208
+ /**
209
+ * How each constructor in the language maps onto a grammar node.
210
+ *
211
+ * Table-driven so the language cannot fall behind the operator set: adding a
212
+ * combinator means adding a line here, and `test/definition.test.js` asserts
213
+ * every operator in `OPERATORS` has one.
214
+ */
215
+ const VARIADIC = [0, Infinity]
216
+
217
+ /** `action(...)` is accepted where raw logic is wanted; unwrap it. */
218
+ const logicOf = (node) => (node && node.action !== undefined ? node.action : node)
219
+
220
+ const CONSTRUCTORS = {
221
+ seq: { arity: VARIADIC, of: (args) => ({ seq: args }) },
222
+ alt: { arity: VARIADIC, of: (args) => ({ alt: args }) },
223
+ oneOf: { arity: VARIADIC, of: (args) => ({ oneOf: args }) },
224
+
225
+ consume: { arity: [1, 1], of: ([a]) => ({ consume: a }) },
226
+ subrule: { arity: [1, 1], of: ([a]) => ({ subrule: a }) },
227
+ many: { arity: [1, 1], of: ([a]) => ({ many: a }) },
228
+ many1: { arity: [1, 1], of: ([a]) => ({ many1: a }) },
229
+ option: { arity: [1, 1], of: ([a]) => ({ option: a }) },
230
+ action: { arity: [1, 1], of: ([a]) => ({ action: a }) },
231
+ lookahead: { arity: [1, 1], of: ([a]) => ({ lookahead: a }) },
232
+ negLookahead: { arity: [1, 1], of: ([a]) => ({ negLookahead: a }) },
233
+ text: { arity: [1, 1], of: ([a]) => ({ text: a }) },
234
+
235
+ eof: { arity: [0, 0], of: () => ({ eof: null }) },
236
+ epsilon: { arity: [0, 0], of: () => ({ epsilon: null }) },
237
+
238
+ label: { arity: [2, 2], of: ([a, b]) => ({ label: [a, b] }) },
239
+ as: { arity: [2, 2], of: ([a, b]) => ({ as: [a, logicOf(b)] }) },
240
+ manySep: { arity: [2, 2], of: ([a, b]) => ({ manySep: { rule: a, sep: b } }) },
241
+ many1Sep: { arity: [2, 2], of: ([a, b]) => ({ many1Sep: { rule: a, sep: b } }) },
242
+
243
+ between: { arity: [3, 3], of: ([a, b, c]) => ({ between: { open: a, body: b, close: c } }) },
244
+ infixLeft: { arity: [3, 3], of: ([a, b, c]) => ({ infixLeft: { operand: a, operator: b, combine: logicOf(c) } }) },
245
+ infixRight: { arity: [3, 3], of: ([a, b, c]) => ({ infixRight: { operand: a, operator: b, combine: logicOf(c) } }) },
246
+ prefix: { arity: [3, 3], of: ([a, b, c]) => ({ prefix: { operator: a, operand: b, combine: logicOf(c) } }) },
247
+ postfix: { arity: [3, 3], of: ([a, b, c]) => ({ postfix: { operand: a, suffix: b, combine: logicOf(c) } }) }
248
+ }
249
+
250
+ /** Constructor names the language understands. */
251
+ export const CONSTRUCTOR_NAMES = Object.keys(CONSTRUCTORS)
252
+
253
+ /** A parser for the grammar-definition language itself. */
254
+ export const createDefinitionParser = (options = {}) =>
255
+ createParser(definitionGrammar, { ...options, methods: createMetaMethods() })
256
+
257
+ /**
258
+ * Parses definition source into a grammar object — plain data, safe to
259
+ * `JSON.stringify` and hand to `createParser`.
260
+ *
261
+ * A trailing `methods { ... }` block is separated out before the language sees
262
+ * the source and carried on the result as `methodsBlock`, an opaque string. The
263
+ * grammar itself is unchanged by its presence.
264
+ *
265
+ * @param {string} source
266
+ * @param {object} [options] Passed to the definition parser itself.
267
+ */
268
+ export function parseDefinition (source, options = {}) {
269
+ const { grammar: text, block } = splitMethodsBlock(source)
270
+ const grammar = createDefinitionParser(options).parse(text)
271
+ if (block !== null) grammar.methodsBlock = block
272
+ return grammar
273
+ }
274
+
275
+ /**
276
+ * Definition source straight to a working parser.
277
+ *
278
+ * A `methods` block is evaluated here, which needs `new Function`. Explicit
279
+ * `options.methods` win over it, and a block that imports has to go through
280
+ * `emitModule` or the CLI instead — see `evaluateMethodsBlock`.
281
+ *
282
+ * @param {string} source
283
+ * @param {object} [options] Passed to `createParser` for the resulting grammar.
284
+ */
285
+ export function createParserFromSource (source, options = {}) {
286
+ const grammar = parseDefinition(source, { execution: options.execution })
287
+ if (grammar.methodsBlock === undefined || options.methods) return createParser(grammar, options)
288
+
289
+ const { methods } = evaluateMethodsBlock(grammar.methodsBlock)
290
+ return createParser(grammar, { ...options, methods })
291
+ }
package/src/emit.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @param {import('./parser.js').GrammarSpec} spec
3
+ * @param {{ memo?: boolean, strict?: boolean, ll1?: boolean, maxSteps?: number,
4
+ * positions?: 'full'|'offset', execution?: 'generated'|'interpreted',
5
+ * format?: 'esm'|'cjs', validate?: boolean, moduleName?: string,
6
+ * runtimeSpecifier?: string, engineSpecifier?: string,
7
+ * methods?: Record<string, any> }} [options]
8
+ * @returns {string} JavaScript module source
9
+ */
10
+ export function emitModule(spec: import("./parser.js").GrammarSpec, options?: {
11
+ memo?: boolean;
12
+ strict?: boolean;
13
+ ll1?: boolean;
14
+ maxSteps?: number;
15
+ positions?: "full" | "offset";
16
+ execution?: "generated" | "interpreted";
17
+ format?: "esm" | "cjs";
18
+ validate?: boolean;
19
+ moduleName?: string;
20
+ runtimeSpecifier?: string;
21
+ engineSpecifier?: string;
22
+ methods?: Record<string, any>;
23
+ }): string;