@bablr/boot 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,434 @@
1
+ const { ref, lit, trivia, esc } = require('@bablr/boot-helpers/types');
2
+ const escapeRegex = require('escape-string-regexp');
3
+ const arrayLast = require('iter-tools-es/methods/array-last');
4
+ const isString = require('iter-tools-es/methods/is-string');
5
+ const isObject = require('iter-tools-es/methods/is-object');
6
+ const sym = require('./symbols.js');
7
+ const { Match } = require('./match.js');
8
+ const { set, isRegex, isArray, parsePath, getPrototypeOf, buildNode } = require('./utils.js');
9
+
10
+ class TemplateParser {
11
+ constructor(rootLanguage, quasis, expressions) {
12
+ this.rootLanguage = rootLanguage;
13
+ this.spans = [];
14
+ this.quasis = quasis;
15
+ this.expressions = expressions;
16
+ this.quasiIdx = 0;
17
+ this.expressionIdx = 0;
18
+ this.idx = 0;
19
+ this.type = null;
20
+ this.m = null;
21
+ }
22
+
23
+ get quasi() {
24
+ return this.quasis[this.quasiIdx];
25
+ }
26
+
27
+ get expression() {
28
+ return this.expressions[this.expressionIdx];
29
+ }
30
+
31
+ get expressionsDone() {
32
+ return this.expressionIdx >= this.expressions.length;
33
+ }
34
+
35
+ get atExpression() {
36
+ return !this.slicedQuasi.length && !this.expressionsDone;
37
+ }
38
+
39
+ get done() {
40
+ return !this.guardedSlicedQuasi.length && !this.atExpression;
41
+ }
42
+
43
+ get quasisDone() {
44
+ return this.quasiIdx >= this.quasis.length;
45
+ }
46
+
47
+ get path() {
48
+ return this.m?.path;
49
+ }
50
+
51
+ get node() {
52
+ return this.path.node;
53
+ }
54
+
55
+ get language() {
56
+ return this.m.resolvedLanguage;
57
+ }
58
+
59
+ get grammar() {
60
+ return this.m.grammar;
61
+ }
62
+
63
+ get matchIsNode() {
64
+ return this.language.covers.get(sym.node).has(this.m.type) && !this.matchIsCover;
65
+ }
66
+
67
+ get matchIsCover() {
68
+ return this.language.covers.has(this.m.type);
69
+ }
70
+
71
+ get matchIsFragment() {
72
+ return this.language.covers.get(sym.fragment)?.has(this.m.type);
73
+ }
74
+
75
+ get span() {
76
+ return arrayLast(this.spans);
77
+ }
78
+
79
+ get chr() {
80
+ return this.quasi[this.idx];
81
+ }
82
+
83
+ get slicedQuasi() {
84
+ const { idx, quasi } = this;
85
+ return quasi.slice(idx);
86
+ }
87
+
88
+ get guardedSlicedQuasi() {
89
+ const { span, slicedQuasi } = this;
90
+ const { guard } = span;
91
+
92
+ if (!guard) return slicedQuasi;
93
+
94
+ const pat = new RegExp(escapeRegex(guard), 'y');
95
+ const res = pat.exec(slicedQuasi);
96
+
97
+ return res ? slicedQuasi.slice(0, pat.lastIndex - res[0].length) : slicedQuasi;
98
+ }
99
+
100
+ matchSticky(pattern, attrs) {
101
+ const { slicedQuasi, guardedSlicedQuasi } = this;
102
+ const { balancer } = attrs;
103
+
104
+ const source = balancer ? slicedQuasi : guardedSlicedQuasi;
105
+
106
+ if (isString(pattern)) {
107
+ return source.startsWith(pattern) ? pattern : null;
108
+ } else if (isRegex(pattern)) {
109
+ if (!pattern.sticky) throw new Error('be sticky!');
110
+ pattern.lastIndex = 0;
111
+
112
+ const result = pattern.exec(source);
113
+
114
+ return result ? result[0] : null;
115
+ } else {
116
+ throw new Error(`Unknown pattern type`);
117
+ }
118
+ }
119
+
120
+ eval(id, attrs = {}, props = {}) {
121
+ const parentMatch = this.m;
122
+ const parentPath = this.path?.node ? this.path : this.path?.parent;
123
+ const { type } = id;
124
+
125
+ if (parentMatch && this.matchIsNode) {
126
+ if (this.matchIsCover && Object.keys(attrs).length) {
127
+ throw new Error('Attrs cannot be passed from inside covers');
128
+ }
129
+ }
130
+
131
+ if (parentMatch) {
132
+ this.m = parentMatch.generate(id, attrs);
133
+ } else {
134
+ this.m = Match.from(this.rootLanguage, id, attrs);
135
+ }
136
+
137
+ const { covers } = this.language;
138
+ const isNode = this.matchIsNode;
139
+ const isCover = this.matchIsCover;
140
+ const isFragment = this.matchIsFragment;
141
+ const isEmbedded = this.language !== this.m.parent?.resolvedLanguage;
142
+ const { path, grammar } = this;
143
+
144
+ if (!type) throw new Error('eval requires a type');
145
+
146
+ if (parentPath?.node && this.atExpression && (isNode || isCover || isFragment)) {
147
+ const { quasisDone } = this;
148
+
149
+ if (quasisDone) throw new Error('there must be more quasis than expressions');
150
+
151
+ const result = this.expression;
152
+
153
+ this.expressionIdx++;
154
+ this.quasiIdx++;
155
+ this.idx = 0;
156
+
157
+ if (parentPath?.node && isFragment) {
158
+ const { properties, children } = parentPath.node;
159
+
160
+ if (result) {
161
+ children.push(...result.children);
162
+
163
+ for (const { 0: key, 1: property } of Object.entries(result.properties)) {
164
+ if (isArray(property)) {
165
+ for (const value of property) {
166
+ set(properties, `[${key}]`, value);
167
+ }
168
+ } else {
169
+ set(properties, key, property);
170
+ }
171
+ }
172
+ }
173
+ } else if (parentPath?.node && (isNode || covers.has(type))) {
174
+ const { properties, children } = parentPath.node;
175
+ const { pathName } = parsePath(this.m.attrs.path);
176
+
177
+ children.push({ ...ref(pathName), id });
178
+
179
+ set(properties, this.m.attrs.path, result);
180
+ }
181
+ } else {
182
+ if (isEmbedded) {
183
+ this.spans.push({ type: 'Bare', guard: null });
184
+ }
185
+
186
+ const result = getPrototypeOf(grammar)[type].call(grammar, this, props);
187
+
188
+ if (isEmbedded) {
189
+ this.spans.pop();
190
+ }
191
+
192
+ if (isNode) {
193
+ const { node } = this.path;
194
+ if (result?.attrs) {
195
+ node.attributes = result.attrs;
196
+ }
197
+ if (parentPath?.node && !covers.has(type)) {
198
+ const { pathName } = parsePath(this.m.attrs.path);
199
+
200
+ parentPath.node.children.push(ref(pathName));
201
+
202
+ set(parentPath.node.properties, this.m.attrs.path, node);
203
+ }
204
+ }
205
+ }
206
+
207
+ this.m = this.m.parent;
208
+
209
+ if (this.path?.node) {
210
+ const isTerminal = (child) => ['Literal', 'Escape'].includes(child.type);
211
+
212
+ const { children } = this.path.node;
213
+
214
+ if (children.find(isTerminal) && !children.every(isTerminal)) {
215
+ throw new Error('strings must be wrapped in nodes');
216
+ }
217
+ }
218
+
219
+ return path.node;
220
+ }
221
+
222
+ updateSpans(attrs) {
223
+ const { startSpan, endSpan, balanced, balancer } = attrs;
224
+ if (endSpan || balancer) {
225
+ if (!this.span.guard) {
226
+ throw new Error('Only balanced spans can be closed with endSpan');
227
+ }
228
+ this.popSpan();
229
+ }
230
+ if (startSpan || balanced) {
231
+ const type = startSpan || this.span.type;
232
+ this.pushSpan({ type, guard: balanced });
233
+ }
234
+ }
235
+
236
+ buildId(id) {
237
+ let type;
238
+ let language = this.language.name;
239
+
240
+ if (id.includes(':')) {
241
+ ({ 0: language, 1: type } = id.split(':'));
242
+ } else {
243
+ type = id;
244
+ }
245
+
246
+ return { type, language };
247
+ }
248
+
249
+ eatProduction(id, attrs = {}, props = {}) {
250
+ return this.eval(this.buildId(id), attrs, props);
251
+ }
252
+
253
+ eatHeldProduction(type, attrs) {
254
+ const { children, properties } = this.node;
255
+
256
+ if (!this.held) {
257
+ throw new Error();
258
+ }
259
+
260
+ const { held } = this;
261
+
262
+ this.held = null;
263
+
264
+ children.push(ref(attrs.path));
265
+ set(properties, attrs.path, held);
266
+
267
+ return held;
268
+ }
269
+
270
+ shiftProduction(id, attrs = {}, props = {}) {
271
+ const { children, properties } = this.node;
272
+ // don't push a new path onto the stack
273
+
274
+ // get the most recently produced node and detach it from its parent
275
+
276
+ const lastChild = arrayLast(children);
277
+
278
+ if (lastChild.type !== 'Reference') {
279
+ throw new Error();
280
+ }
281
+
282
+ const path = lastChild.value;
283
+ const pathIsArray = isArray(properties[path]);
284
+
285
+ this.held = pathIsArray ? arrayLast(properties[path]) : properties[path];
286
+
287
+ children.pop();
288
+
289
+ if (pathIsArray) {
290
+ properties[path].pop();
291
+ } else {
292
+ properties[path] = null;
293
+ }
294
+
295
+ return this.eval(this.buildId(id), attrs, props);
296
+ }
297
+
298
+ eat(pattern, type, attrs) {
299
+ if (!isString(type)) throw new Error('Cannot eat anonymous token');
300
+ if (!isObject(attrs) || !attrs.path) throw new Error('a node must have a path');
301
+
302
+ const result = this.matchSticky(pattern, attrs, this);
303
+
304
+ if (!result) throw new Error('miniparser: parsing failed');
305
+
306
+ this.idx += result.length;
307
+
308
+ this.updateSpans(attrs);
309
+
310
+ set(this.node.properties, attrs.path, buildNode(this.buildId(type), [lit(result)]));
311
+
312
+ this.node.children.push(ref(parsePath(attrs.path).pathName));
313
+
314
+ return result;
315
+ }
316
+
317
+ // matchLiteral would be a better name
318
+ match(pattern, attrs = {}) {
319
+ return this.matchSticky(pattern, attrs, this);
320
+ }
321
+
322
+ eatMatch(pattern, type, attrs) {
323
+ if (!isString(type)) throw new Error('Cannot eatMatch anonymous token');
324
+ if (!isObject(attrs) || !attrs.path) throw new Error('a node must have a path');
325
+
326
+ let result;
327
+ if (this.atExpression) {
328
+ } else {
329
+ }
330
+
331
+ result = this.matchSticky(pattern, attrs, this);
332
+
333
+ if (result) {
334
+ this.updateSpans(attrs);
335
+
336
+ this.idx += result.length;
337
+
338
+ set(this.node.properties, attrs.path, buildNode(this.buildId(type), [lit(result)]));
339
+
340
+ this.node.children.push(ref(parsePath(attrs.path).pathName));
341
+ }
342
+ return result;
343
+ }
344
+
345
+ eatTrivia(pattern) {
346
+ const result = this.matchSticky(pattern, {}, this);
347
+
348
+ if (!result) throw new Error('miniparser: parsing failed');
349
+
350
+ this.idx += result.length;
351
+
352
+ this.node.children.push(trivia(result));
353
+
354
+ return result;
355
+ }
356
+
357
+ eatMatchTrivia(pattern) {
358
+ const result = this.matchSticky(pattern, {}, this);
359
+
360
+ if (result) {
361
+ this.idx += result.length;
362
+
363
+ this.node.children.push(trivia(result));
364
+ }
365
+
366
+ return result;
367
+ }
368
+
369
+ eatEscape(pattern) {
370
+ const result = this.matchSticky(pattern, {}, this);
371
+
372
+ if (!result) throw new Error('miniparser: parsing failed');
373
+
374
+ this.idx += result.length;
375
+
376
+ this.node.children.push(esc(result, this.language.cookEscape(result, this.span)));
377
+
378
+ return result;
379
+ }
380
+
381
+ eatMatchEscape(pattern) {
382
+ const result = this.matchSticky(pattern, {}, this);
383
+
384
+ if (result) {
385
+ this.idx += result.length;
386
+
387
+ this.node.children.push(esc(result, this.language.cookEscape(result, this.span)));
388
+ }
389
+
390
+ return result;
391
+ }
392
+
393
+ eatLiteral(pattern) {
394
+ const result = this.matchSticky(pattern, {}, this);
395
+
396
+ if (!result) throw new Error('miniparser: parsing failed');
397
+
398
+ this.idx += result.length;
399
+
400
+ this.node.children.push(lit(result));
401
+
402
+ return result;
403
+ }
404
+
405
+ eatMatchLiteral(pattern) {
406
+ const result = this.matchSticky(pattern, {}, this);
407
+
408
+ if (result) {
409
+ this.idx += result.length;
410
+
411
+ this.node.children.push(lit(result));
412
+ }
413
+
414
+ return result;
415
+ }
416
+
417
+ pushSpan(span) {
418
+ this.spans.push(span);
419
+ }
420
+
421
+ popSpan() {
422
+ if (!this.spans.length) {
423
+ throw new Error('no span to pop');
424
+ }
425
+ this.spans.pop();
426
+ }
427
+
428
+ replaceSpan(span) {
429
+ this.spans.pop();
430
+ this.spans.push(span);
431
+ }
432
+ }
433
+
434
+ module.exports = { TemplateParser };
package/lib/path.js ADDED
@@ -0,0 +1,47 @@
1
+ const buildNode = (id) => {
2
+ const { language, type } = id;
3
+
4
+ return {
5
+ language,
6
+ type,
7
+ attributes: {},
8
+ children: [],
9
+ properties: {},
10
+ };
11
+ };
12
+
13
+ class Path {
14
+ constructor(id, attributes, parent = null) {
15
+ this.id = id;
16
+ this.attributes = attributes;
17
+ this.parent = parent;
18
+
19
+ this.node = null;
20
+ }
21
+
22
+ get parentProperty() {
23
+ return this.attributes.path;
24
+ }
25
+
26
+ get attrs() {
27
+ return this.attributes;
28
+ }
29
+
30
+ get language() {
31
+ return this.id.language;
32
+ }
33
+
34
+ get type() {
35
+ return this.id.type;
36
+ }
37
+
38
+ generate(id, attrs) {
39
+ return new Path(id, attrs, this);
40
+ }
41
+
42
+ static from(id, attrs = {}) {
43
+ return new Path(id, attrs);
44
+ }
45
+ }
46
+
47
+ module.exports = { Path, buildNode };
package/lib/symbols.js ADDED
@@ -0,0 +1,4 @@
1
+ const node = Symbol.for('@bablr/node');
2
+ const fragment = Symbol.for('@bablr/fragment');
3
+
4
+ module.exports = { node, fragment };
package/lib/utils.js ADDED
@@ -0,0 +1,119 @@
1
+ const every = require('iter-tools-es/methods/every');
2
+ const isArray = require('iter-tools-es/methods/is-array');
3
+ const isString = require('iter-tools-es/methods/is-string');
4
+ const { parsePath, stripPathBraces } = require('@bablr/boot-helpers/path');
5
+
6
+ const { hasOwn, getPrototypeOf, getOwnPropertySymbols } = Object;
7
+ const isSymbol = (value) => typeof value === 'symbol';
8
+ const isType = (value) => isString(value) || isSymbol(value);
9
+ const isRegex = (val) => val instanceof RegExp;
10
+
11
+ const objectEntries = (obj) => {
12
+ return {
13
+ *[Symbol.iterator]() {
14
+ for (let key in obj) if (hasOwn(obj, key)) yield [key, obj[key]];
15
+ const symTypes = getOwnPropertySymbols(obj);
16
+ for (const type of symTypes) {
17
+ yield [type, obj[type]];
18
+ }
19
+ },
20
+ };
21
+ };
22
+
23
+ const explodeSubtypes = (aliases, exploded, types) => {
24
+ for (const type of types) {
25
+ const explodedTypes = aliases.get(type);
26
+ if (explodedTypes) {
27
+ for (const explodedType of explodedTypes) {
28
+ exploded.add(explodedType);
29
+ const subtypes = aliases.get(explodedType);
30
+ if (subtypes) {
31
+ explodeSubtypes(aliases, exploded, subtypes);
32
+ }
33
+ }
34
+ }
35
+ }
36
+ };
37
+
38
+ const buildCovers = (rawAliases) => {
39
+ const aliases = new Map();
40
+
41
+ for (const alias of objectEntries(rawAliases)) {
42
+ if (!isType(alias[0])) throw new Error('alias[0] key must be a string or symbol');
43
+ if (!isArray(alias[1])) throw new Error('alias[1] must be an array');
44
+ if (!every(isType, alias[1])) throw new Error('alias[1] values must be strings or symbols');
45
+
46
+ aliases.set(alias[0], new Set(alias[1]));
47
+ }
48
+
49
+ for (const [type, types] of aliases.entries()) {
50
+ explodeSubtypes(aliases, aliases.get(type), types);
51
+ }
52
+
53
+ return new Map(aliases);
54
+ };
55
+
56
+ const set = (obj, path, value) => {
57
+ const { pathIsArray, pathName } = parsePath(path);
58
+
59
+ if (pathIsArray) {
60
+ if (!obj[pathName]) {
61
+ obj[pathName] = [];
62
+ }
63
+
64
+ if (!isArray(obj[pathName])) throw new Error('bad array value');
65
+
66
+ obj[pathName].push(value);
67
+ } else {
68
+ if (hasOwn(obj, pathName)) {
69
+ throw new Error('duplicate child name');
70
+ }
71
+ obj[pathName] = value;
72
+ }
73
+ };
74
+
75
+ const resolveDependentLanguage = (language, name) => {
76
+ if (name === undefined) {
77
+ return language;
78
+ }
79
+
80
+ const resolved = name === language.name ? language : language.dependencies[name];
81
+
82
+ if (!resolved) {
83
+ throw new Error(`Cannot resolve {name: ${name}} from {name: ${language.name}}`);
84
+ }
85
+
86
+ return resolved;
87
+ };
88
+
89
+ const buildNode = (id, children, properties = {}, attributes = {}) => {
90
+ const { language, type } = id;
91
+ return { language, type, children, properties, attributes, gap: undefined };
92
+ };
93
+
94
+ const buildId = (value) => {
95
+ if (isString(value)) {
96
+ const { 0: language, 1: type } = value.split(':');
97
+ return type ? { language, type } : { language: undefined, type: language };
98
+ } else {
99
+ return value;
100
+ }
101
+ };
102
+
103
+ const id = (...args) => {
104
+ return buildId(String.raw(...args));
105
+ };
106
+
107
+ module.exports = {
108
+ parsePath,
109
+ stripPathBraces,
110
+ buildCovers,
111
+ set,
112
+ resolveDependentLanguage,
113
+ isArray,
114
+ isRegex,
115
+ getPrototypeOf,
116
+ buildNode,
117
+ buildId,
118
+ id,
119
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@bablr/boot",
3
+ "version": "0.1.0",
4
+ "description": "Compile-time tools for bootstrapping BABLR VM",
5
+ "engines": {
6
+ "node": ">=12.0.0"
7
+ },
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./shorthand.macro": "./shorthand.macro.js"
11
+ },
12
+ "files": [
13
+ "lib/**/*.js"
14
+ ],
15
+ "sideEffects": false,
16
+ "dependencies": {
17
+ "@babel/helper-module-imports": "^7.22.15",
18
+ "@babel/template": "^7.22.15",
19
+ "@babel/types": "7.23.0",
20
+ "@bablr/boot-helpers": "github:bablr-lang/boot-helpers#1551598bc7257481089d06839eeec2e26885bbb2",
21
+ "escape-string-regexp": "4.0.0",
22
+ "iter-tools-es": "^7.5.3"
23
+ },
24
+ "devDependencies": {
25
+ "@babel/cli": "^7.23.0",
26
+ "@babel/core": "^7.23.0",
27
+ "@babel/plugin-proposal-decorators": "^7.23.2",
28
+ "@babel/plugin-transform-runtime": "^7.23.2",
29
+ "@bablr/eslint-config-base": "github:bablr-lang/eslint-config-base#a49dbe6861a82d96864ee89fbf0ec1844a8a5e8e",
30
+ "@bablr/helpers": "github:bablr-lang/helpers#8ef702024395635c9a05fe4c9f803a48f87d902c",
31
+ "babel-plugin-macros": "^3.1.0",
32
+ "enhanced-resolve": "^5.12.0",
33
+ "eslint": "^8.47.0",
34
+ "eslint-import-resolver-enhanced-resolve": "^1.0.5",
35
+ "eslint-plugin-import": "^2.27.5",
36
+ "expect": "^29.6.2",
37
+ "prettier": "^2.0.5"
38
+ },
39
+ "repository": "git@github.com:bablr-lang/boot.git",
40
+ "homepage": "https://github.com/bablr-lang/boot",
41
+ "author": "Conrad Buck <conartist6@gmail.com>",
42
+ "license": "MIT"
43
+ }