@bablr/agast-vm 0.1.3 → 0.2.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/README.md +4 -14
- package/lib/context.js +8 -2
- package/lib/evaluate.js +113 -98
- package/lib/path.js +30 -48
- package/lib/state.js +13 -46
- package/lib/utils/skip.js +39 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The agAST VM provides consistency guarantees when with CSTML documents to parse
|
|
|
4
4
|
|
|
5
5
|
## API
|
|
6
6
|
|
|
7
|
-
The VM responds to several instructions, but its primary API is `advance(token)`, where `token` may be a `
|
|
7
|
+
The VM responds to several instructions, but its primary API is `advance(token)`, where `token` may be a `OpenNodeTag`, `CloseNodeTag`, `Literal`, `Reference`, or `Gap`.
|
|
8
8
|
|
|
9
9
|
The VM requires the basic invariants of CSTML to be followed, for example that `Reference` must be followed by either a `OpenNodeTag` or a `Gap`. In fact, `agast-vm` is the reference implementation of these invariants.
|
|
10
10
|
|
|
@@ -15,17 +15,7 @@ Finally the VM supports `bindAttribute(key, value)`. A node's attributes start u
|
|
|
15
15
|
Here are the basic types used by the VM:
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
type Token =
|
|
19
|
-
|
|
20
|
-
type OpenFragmentTag {
|
|
21
|
-
type: 'OpenFragmentTag',
|
|
22
|
-
value: null
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type CloseFragmentTag {
|
|
26
|
-
type: 'CloseFragmentTag',
|
|
27
|
-
value: null
|
|
28
|
-
}
|
|
18
|
+
type Token = OpenNodeTag | CloseNodeTag | Literal | Reference | Gap;
|
|
29
19
|
|
|
30
20
|
type OpenNodeTag {
|
|
31
21
|
type: 'OpenNodeTag',
|
|
@@ -35,8 +25,8 @@ type OpenNodeTag {
|
|
|
35
25
|
trivia: boolean,
|
|
36
26
|
escape: boolean
|
|
37
27
|
},
|
|
38
|
-
language: string,
|
|
39
|
-
type: string,
|
|
28
|
+
language: string | null,
|
|
29
|
+
type: string | null, // null type indicates a fragment
|
|
40
30
|
attributes: { [key: string]: boolean | number | string }
|
|
41
31
|
}
|
|
42
32
|
}
|
package/lib/context.js
CHANGED
|
@@ -24,7 +24,7 @@ function* allTerminalsFor(range, nextTerminals) {
|
|
|
24
24
|
|
|
25
25
|
const pastEnd = nextTerminals.get(end);
|
|
26
26
|
|
|
27
|
-
for (let tag = start; tag !== pastEnd; tag = nextTerminals.get(tag)) {
|
|
27
|
+
for (let tag = start; tag && tag !== pastEnd; tag = nextTerminals.get(tag)) {
|
|
28
28
|
yield tag;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -91,7 +91,13 @@ export const Context = class AgastContext {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
getProperty(result, name) {
|
|
94
|
-
|
|
94
|
+
let startTag = result[0] || result;
|
|
95
|
+
|
|
96
|
+
if (startTag.type === 'Reference') {
|
|
97
|
+
startTag = this.nextTerminals.get(startTag);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return this.tagNodes.get(startTag).properties.get(name);
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
isEmpty(range) {
|
package/lib/evaluate.js
CHANGED
|
@@ -2,12 +2,12 @@ import { Coroutine } from '@bablr/coroutine';
|
|
|
2
2
|
import {
|
|
3
3
|
buildNull,
|
|
4
4
|
buildGap,
|
|
5
|
+
buildShift,
|
|
5
6
|
buildReference,
|
|
6
7
|
buildLiteral,
|
|
8
|
+
buildWriteEffect,
|
|
7
9
|
buildDoctypeTag,
|
|
8
10
|
buildNodeOpenTag,
|
|
9
|
-
buildFragmentOpenTag,
|
|
10
|
-
buildFragmentCloseTag,
|
|
11
11
|
buildNodeCloseTag,
|
|
12
12
|
} from '@bablr/agast-helpers/builders';
|
|
13
13
|
import { StreamIterable, getStreamIterator } from '@bablr/agast-helpers/stream';
|
|
@@ -17,9 +17,10 @@ import { Path, Node } from './path.js';
|
|
|
17
17
|
import { State } from './state.js';
|
|
18
18
|
import { facades } from './facades.js';
|
|
19
19
|
|
|
20
|
-
export const evaluate = (ctx, strategy) =>
|
|
20
|
+
export const evaluate = (ctx, strategy, options) =>
|
|
21
|
+
new StreamIterable(__evaluate(ctx, strategy, options));
|
|
21
22
|
|
|
22
|
-
const __evaluate = function* agast(ctx, strategy) {
|
|
23
|
+
const __evaluate = function* agast(ctx, strategy, options = {}) {
|
|
23
24
|
let s = State.from(ctx);
|
|
24
25
|
|
|
25
26
|
const co = new Coroutine(getStreamIterator(strategy(facades.get(ctx), facades.get(s))));
|
|
@@ -37,7 +38,7 @@ const __evaluate = function* agast(ctx, strategy) {
|
|
|
37
38
|
const instr = reifyExpression(sourceInstr);
|
|
38
39
|
let returnValue = undefined;
|
|
39
40
|
|
|
40
|
-
const { verb } = instr;
|
|
41
|
+
const { verb, arguments: args = [] } = instr;
|
|
41
42
|
|
|
42
43
|
switch (verb) {
|
|
43
44
|
case 'branch': {
|
|
@@ -66,7 +67,18 @@ const __evaluate = function* agast(ctx, strategy) {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
case 'advance': {
|
|
69
|
-
const {
|
|
70
|
+
const { 0: terminal, 1: options } = args;
|
|
71
|
+
|
|
72
|
+
if (
|
|
73
|
+
s.held &&
|
|
74
|
+
!(
|
|
75
|
+
terminal.type === 'OpenNodeTag' ||
|
|
76
|
+
terminal.type === 'Reference' ||
|
|
77
|
+
terminal.type === 'Gap'
|
|
78
|
+
)
|
|
79
|
+
) {
|
|
80
|
+
throw new Error('Cannot advance while holding');
|
|
81
|
+
}
|
|
70
82
|
|
|
71
83
|
switch (terminal?.type || 'Null') {
|
|
72
84
|
case 'DoctypeTag': {
|
|
@@ -109,7 +121,7 @@ const __evaluate = function* agast(ctx, strategy) {
|
|
|
109
121
|
const tag = buildReference(name, isArray);
|
|
110
122
|
|
|
111
123
|
if (s.result.type === 'Reference') {
|
|
112
|
-
throw new Error('
|
|
124
|
+
throw new Error('A reference must have a non-reference value');
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
if (!s.path.depth) {
|
|
@@ -139,44 +151,47 @@ const __evaluate = function* agast(ctx, strategy) {
|
|
|
139
151
|
|
|
140
152
|
const gapTag = buildGap();
|
|
141
153
|
|
|
154
|
+
s.held = null;
|
|
155
|
+
|
|
142
156
|
yield* s.emit(gapTag);
|
|
143
157
|
|
|
144
158
|
returnValue = gapTag;
|
|
145
159
|
break;
|
|
146
160
|
}
|
|
147
161
|
|
|
148
|
-
case '
|
|
149
|
-
const
|
|
162
|
+
case 'Shift': {
|
|
163
|
+
const { tagNodes, prevTerminals } = ctx;
|
|
164
|
+
const tag = buildShift();
|
|
150
165
|
|
|
151
|
-
|
|
166
|
+
const finishedNode = tagNodes.get(s.result);
|
|
167
|
+
const finishedPath = finishedNode.path;
|
|
152
168
|
|
|
153
|
-
s.
|
|
169
|
+
s.held = { node: finishedNode, path: finishedPath };
|
|
154
170
|
|
|
155
|
-
|
|
171
|
+
if (!finishedNode.openTag.value.flags.expression) {
|
|
172
|
+
throw new Error();
|
|
173
|
+
}
|
|
156
174
|
|
|
157
|
-
|
|
175
|
+
s.path = finishedPath;
|
|
158
176
|
|
|
159
|
-
|
|
177
|
+
yield* s.emit(tag);
|
|
178
|
+
|
|
179
|
+
returnValue = tag;
|
|
160
180
|
break;
|
|
161
181
|
}
|
|
162
182
|
|
|
163
|
-
case '
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
s.node = Node.from(openTag);
|
|
183
|
+
case 'Null': {
|
|
184
|
+
const reference = s.result;
|
|
167
185
|
|
|
168
|
-
|
|
186
|
+
if (reference?.type !== 'Reference') throw new Error();
|
|
169
187
|
|
|
170
|
-
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
188
|
+
s.path = s.path.parent;
|
|
173
189
|
|
|
174
|
-
|
|
175
|
-
const closeTag = buildFragmentCloseTag();
|
|
190
|
+
const null_ = buildNull();
|
|
176
191
|
|
|
177
|
-
yield* s.emit(
|
|
192
|
+
yield* s.emit(null_);
|
|
178
193
|
|
|
179
|
-
returnValue =
|
|
194
|
+
returnValue = null_;
|
|
180
195
|
break;
|
|
181
196
|
}
|
|
182
197
|
|
|
@@ -186,46 +201,51 @@ const __evaluate = function* agast(ctx, strategy) {
|
|
|
186
201
|
const reference = s.result;
|
|
187
202
|
const openTag = buildNodeOpenTag(flags, language, type, intrinsicValue, attributes);
|
|
188
203
|
|
|
189
|
-
if (!
|
|
190
|
-
|
|
191
|
-
|
|
204
|
+
if (!type) {
|
|
205
|
+
s.node = Node.from(s.path, openTag);
|
|
206
|
+
ctx.tagNodes.set(openTag, s.node);
|
|
207
|
+
} else {
|
|
208
|
+
if (!flags.trivia && !flags.escape && s.node.type) {
|
|
209
|
+
if (
|
|
210
|
+
reference.type !== 'Reference' &&
|
|
211
|
+
reference.type !== 'Shift' &&
|
|
212
|
+
reference.type !== 'OpenFragmentTag'
|
|
213
|
+
) {
|
|
214
|
+
throw new Error('Invalid location for OpenNodeTag');
|
|
215
|
+
}
|
|
192
216
|
}
|
|
193
|
-
}
|
|
194
217
|
|
|
195
|
-
|
|
218
|
+
const { flags: openFlags } = openTag.value;
|
|
196
219
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (flags.expression && !flags.intrinsic) {
|
|
204
|
-
s.expressionDepth++;
|
|
205
|
-
}
|
|
220
|
+
if (!(openFlags.trivia || openFlags.escape) && !s.path.depth) {
|
|
221
|
+
const tag = buildReference('root', false);
|
|
222
|
+
s.path = s.path.push(ctx, tag);
|
|
223
|
+
s.node.resolver.consume(tag);
|
|
224
|
+
}
|
|
206
225
|
|
|
207
|
-
|
|
226
|
+
const newNode = s.node.push(s.path, openTag);
|
|
208
227
|
|
|
209
|
-
|
|
228
|
+
newNode.unboundAttributes = new Set(unboundAttributes);
|
|
210
229
|
|
|
211
|
-
|
|
230
|
+
ctx.tagNodes.set(openTag, newNode);
|
|
212
231
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
232
|
+
if (!intrinsicValue) {
|
|
233
|
+
s.node = newNode;
|
|
234
|
+
} else {
|
|
235
|
+
s.path = s.path.parent;
|
|
217
236
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
237
|
+
if (s.path.depth > 1) {
|
|
238
|
+
const { properties } = s.node;
|
|
239
|
+
const { name: refName, isArray } = reference.value;
|
|
221
240
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
241
|
+
if (!isArray) {
|
|
242
|
+
properties.set(refName, [openTag, openTag]);
|
|
243
|
+
} else {
|
|
244
|
+
if (!properties.has(refName)) {
|
|
245
|
+
properties.set(refName, []);
|
|
246
|
+
}
|
|
247
|
+
properties.get(refName).push([openTag, openTag]);
|
|
227
248
|
}
|
|
228
|
-
properties.get(refName).push([openTag, openTag]);
|
|
229
249
|
}
|
|
230
250
|
}
|
|
231
251
|
}
|
|
@@ -241,48 +261,46 @@ const __evaluate = function* agast(ctx, strategy) {
|
|
|
241
261
|
const { openTag } = s.node;
|
|
242
262
|
const { flags, type: openType } = openTag.value;
|
|
243
263
|
|
|
244
|
-
|
|
245
|
-
throw new Error('Grammar failed to bind all attributes');
|
|
264
|
+
const closeTag = buildNodeCloseTag(type, language);
|
|
246
265
|
|
|
247
|
-
if (
|
|
266
|
+
if (openType) {
|
|
267
|
+
if (s.node.unboundAttributes?.size)
|
|
268
|
+
throw new Error('Grammar failed to bind all attributes');
|
|
248
269
|
|
|
249
|
-
|
|
250
|
-
throw new Error(
|
|
251
|
-
`Grammar close {type: ${type}} did not match open {type: ${openType}}`,
|
|
252
|
-
);
|
|
270
|
+
if (!type) throw new Error(`CloseNodeTag must have type`);
|
|
253
271
|
|
|
254
|
-
|
|
272
|
+
if (s.path.depth > 1 && type !== openType)
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Grammar close {type: ${type}} did not match open {type: ${openType}}`,
|
|
275
|
+
);
|
|
255
276
|
|
|
256
|
-
|
|
257
|
-
|
|
277
|
+
if (!flags.escape && !flags.trivia) {
|
|
278
|
+
const { name: refName, isArray } = s.path.reference.value;
|
|
258
279
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
280
|
+
// is this right?
|
|
281
|
+
if (s.path.depth > 2) {
|
|
282
|
+
const { properties } = s.node.parent;
|
|
262
283
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
284
|
+
if (!isArray) {
|
|
285
|
+
properties.set(refName, [openTag, closeTag]);
|
|
286
|
+
} else {
|
|
287
|
+
if (!properties.has(refName)) {
|
|
288
|
+
properties.set(refName, []);
|
|
289
|
+
}
|
|
290
|
+
properties.get(refName).push([openTag, closeTag]);
|
|
268
291
|
}
|
|
269
|
-
properties.get(refName).push([openTag, closeTag]);
|
|
270
292
|
}
|
|
271
293
|
}
|
|
272
|
-
}
|
|
273
294
|
|
|
274
|
-
|
|
275
|
-
s.expressionDepth--;
|
|
276
|
-
}
|
|
295
|
+
ctx.tagNodes.set(closeTag, s.node);
|
|
277
296
|
|
|
278
|
-
|
|
297
|
+
s.node.closeTag = closeTag;
|
|
279
298
|
|
|
280
|
-
|
|
299
|
+
s.node = s.node.parent;
|
|
281
300
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
s.path = s.path.parent;
|
|
301
|
+
if (!(flags.trivia || flags.escape)) {
|
|
302
|
+
s.path = s.path.parent;
|
|
303
|
+
}
|
|
286
304
|
}
|
|
287
305
|
|
|
288
306
|
yield* s.emit(closeTag, flags.expression);
|
|
@@ -298,18 +316,8 @@ const __evaluate = function* agast(ctx, strategy) {
|
|
|
298
316
|
break;
|
|
299
317
|
}
|
|
300
318
|
|
|
301
|
-
case 'shift': {
|
|
302
|
-
s.shift();
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
case 'unshift': {
|
|
307
|
-
s.unshift();
|
|
308
|
-
break;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
319
|
case 'bindAttribute': {
|
|
312
|
-
const {
|
|
320
|
+
const { 0: key, 1: value } = args;
|
|
313
321
|
|
|
314
322
|
const { unboundAttributes } = s.node;
|
|
315
323
|
|
|
@@ -317,8 +325,8 @@ const __evaluate = function* agast(ctx, strategy) {
|
|
|
317
325
|
throw new Error('No unbound attribute to bind');
|
|
318
326
|
}
|
|
319
327
|
|
|
320
|
-
if (s.node.openTag.type
|
|
321
|
-
throw new Error();
|
|
328
|
+
if (!s.node.openTag.value.type) {
|
|
329
|
+
throw new Error('Cannot bind attribute to fragment');
|
|
322
330
|
}
|
|
323
331
|
|
|
324
332
|
if (key === 'span') throw new Error('too late');
|
|
@@ -378,6 +386,13 @@ const __evaluate = function* agast(ctx, strategy) {
|
|
|
378
386
|
break;
|
|
379
387
|
}
|
|
380
388
|
|
|
389
|
+
case 'write': {
|
|
390
|
+
if (options.emitEffects) {
|
|
391
|
+
yield buildWriteEffect(args[0], args[1]);
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
|
|
381
396
|
default: {
|
|
382
397
|
throw new Error(`Unexpected call of {type: ${printExpression(verb)}}`);
|
|
383
398
|
}
|
package/lib/path.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { WeakStackFrame } from '@bablr/weak-stack';
|
|
2
2
|
import { Resolver } from '@bablr/agast-helpers/tree';
|
|
3
|
-
import {
|
|
3
|
+
import { skipToDepth, buildSkips } from './utils/skip.js';
|
|
4
4
|
import { facades, actuals } from './facades.js';
|
|
5
5
|
|
|
6
|
-
const skipLevels = 3;
|
|
7
|
-
const skipShiftExponentGrowth = 4;
|
|
8
|
-
const skipAmounts = new Array(skipLevels)
|
|
9
|
-
.fill(null)
|
|
10
|
-
.map((_, i) => 2 >> (i * skipShiftExponentGrowth));
|
|
11
|
-
const skipsByPath = new WeakMap();
|
|
12
|
-
|
|
13
6
|
export const NodeFacade = class AgastNodeFacade {
|
|
14
7
|
constructor(path) {
|
|
15
8
|
facades.set(path, this);
|
|
@@ -54,6 +47,10 @@ export const NodeFacade = class AgastNodeFacade {
|
|
|
54
47
|
get attributes() {
|
|
55
48
|
return actuals.get(this).attributes;
|
|
56
49
|
}
|
|
50
|
+
|
|
51
|
+
at(depth) {
|
|
52
|
+
return facades.get(actuals.get(this).at(depth));
|
|
53
|
+
}
|
|
57
54
|
};
|
|
58
55
|
|
|
59
56
|
export const PathFacade = class AgastPathFacade {
|
|
@@ -74,17 +71,7 @@ export const PathFacade = class AgastPathFacade {
|
|
|
74
71
|
}
|
|
75
72
|
|
|
76
73
|
at(depth) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (depth > this.depth) throw new Error();
|
|
80
|
-
|
|
81
|
-
let d = actuals.get(parent).depth;
|
|
82
|
-
for (; d > depth; ) {
|
|
83
|
-
const skips = skipsByPath.get(actuals.get(this));
|
|
84
|
-
parent = (skips && findRight(skips, (skip) => d - skip > depth)) || parent.parent;
|
|
85
|
-
d = actuals.get(parent).depth;
|
|
86
|
-
}
|
|
87
|
-
return parent;
|
|
74
|
+
return facades.get(actuals.get(this).at(depth));
|
|
88
75
|
}
|
|
89
76
|
|
|
90
77
|
*parents(includeSelf = false) {
|
|
@@ -97,11 +84,12 @@ export const PathFacade = class AgastPathFacade {
|
|
|
97
84
|
};
|
|
98
85
|
|
|
99
86
|
export const Node = class AgastNode extends WeakStackFrame {
|
|
100
|
-
static from(openTag) {
|
|
101
|
-
return AgastNode.create(openTag);
|
|
87
|
+
static from(path, openTag) {
|
|
88
|
+
return AgastNode.create(path, openTag);
|
|
102
89
|
}
|
|
103
90
|
|
|
104
91
|
constructor(
|
|
92
|
+
path,
|
|
105
93
|
openTag,
|
|
106
94
|
closeTag = null,
|
|
107
95
|
properties = new Map(),
|
|
@@ -110,11 +98,14 @@ export const Node = class AgastNode extends WeakStackFrame {
|
|
|
110
98
|
) {
|
|
111
99
|
super();
|
|
112
100
|
|
|
101
|
+
this.path = path;
|
|
113
102
|
this.openTag = openTag;
|
|
114
103
|
this.closeTag = closeTag;
|
|
115
104
|
this.properties = properties;
|
|
116
105
|
this.resolver = resolver;
|
|
117
106
|
this.unboundAttributes = unboundAttributes;
|
|
107
|
+
|
|
108
|
+
buildSkips(this);
|
|
118
109
|
}
|
|
119
110
|
|
|
120
111
|
get language() {
|
|
@@ -122,7 +113,7 @@ export const Node = class AgastNode extends WeakStackFrame {
|
|
|
122
113
|
}
|
|
123
114
|
|
|
124
115
|
get type() {
|
|
125
|
-
return this.openTag.value?.type ||
|
|
116
|
+
return this.openTag.value?.type || null;
|
|
126
117
|
}
|
|
127
118
|
|
|
128
119
|
get flags() {
|
|
@@ -133,10 +124,23 @@ export const Node = class AgastNode extends WeakStackFrame {
|
|
|
133
124
|
return this.openTag.value?.attributes || {};
|
|
134
125
|
}
|
|
135
126
|
|
|
127
|
+
at(depth) {
|
|
128
|
+
return skipToDepth(depth, this);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
*parents(includeSelf = false) {
|
|
132
|
+
if (includeSelf) yield this;
|
|
133
|
+
let parent = this;
|
|
134
|
+
while ((parent = parent.parent)) {
|
|
135
|
+
yield parent;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
136
139
|
branch() {
|
|
137
|
-
const { openTag, closeTag, properties, resolver, unboundAttributes } = this;
|
|
140
|
+
const { path, openTag, closeTag, properties, resolver, unboundAttributes } = this;
|
|
138
141
|
|
|
139
142
|
return this.replace(
|
|
143
|
+
path,
|
|
140
144
|
openTag,
|
|
141
145
|
closeTag,
|
|
142
146
|
new Map(properties), // there is probably a better way
|
|
@@ -146,6 +150,7 @@ export const Node = class AgastNode extends WeakStackFrame {
|
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
accept(node) {
|
|
153
|
+
this.path = node.path;
|
|
149
154
|
this.openTag = node.openTag;
|
|
150
155
|
this.closeTag = node.closeTag;
|
|
151
156
|
this.properties = node.properties;
|
|
@@ -172,36 +177,13 @@ export const Path = class AgastPath extends WeakStackFrame {
|
|
|
172
177
|
this.context = context;
|
|
173
178
|
this.reference = reference;
|
|
174
179
|
|
|
175
|
-
|
|
176
|
-
let skipAmount = skipAmounts[skipIdx];
|
|
177
|
-
let skips;
|
|
178
|
-
while ((this.depth & skipAmount) === skipAmount) {
|
|
179
|
-
if (!skips) {
|
|
180
|
-
skips = [];
|
|
181
|
-
skipsByPath.set(this, skips);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
skips[skipIdx] = this.at(this.depth - skipAmount);
|
|
185
|
-
|
|
186
|
-
skipIdx++;
|
|
187
|
-
skipAmount = skipAmounts[skipIdx];
|
|
188
|
-
}
|
|
180
|
+
buildSkips(this);
|
|
189
181
|
|
|
190
182
|
new PathFacade(this);
|
|
191
183
|
}
|
|
192
184
|
|
|
193
185
|
at(depth) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (depth > this.depth) throw new Error();
|
|
197
|
-
|
|
198
|
-
let d = this.depth;
|
|
199
|
-
for (; d > depth; ) {
|
|
200
|
-
const skips = skipsByPath.get(this);
|
|
201
|
-
parent = (skips && findRight(skips, (skip) => d - skip > depth)) || parent.parent;
|
|
202
|
-
d = parent.depth;
|
|
203
|
-
}
|
|
204
|
-
return parent;
|
|
186
|
+
return skipToDepth(depth, this);
|
|
205
187
|
}
|
|
206
188
|
|
|
207
189
|
*parents(includeSelf = false) {
|
package/lib/state.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { WeakStackFrame } from '@bablr/weak-stack';
|
|
2
2
|
import { startsDocument } from '@bablr/agast-helpers/stream';
|
|
3
3
|
import { facades, actuals } from './facades.js';
|
|
4
|
+
import { buildBeginningOfStreamToken } from '@bablr/agast-helpers/builders';
|
|
4
5
|
|
|
5
6
|
export const StateFacade = class AgastStateFacade {
|
|
6
7
|
constructor(state) {
|
|
@@ -45,10 +46,9 @@ export const State = class AgastState extends WeakStackFrame {
|
|
|
45
46
|
context,
|
|
46
47
|
path = null,
|
|
47
48
|
node = null,
|
|
48
|
-
result =
|
|
49
|
+
result = buildBeginningOfStreamToken(),
|
|
49
50
|
emitted = null,
|
|
50
51
|
held = null,
|
|
51
|
-
expressionDepth = 0,
|
|
52
52
|
) {
|
|
53
53
|
super();
|
|
54
54
|
|
|
@@ -60,7 +60,6 @@ export const State = class AgastState extends WeakStackFrame {
|
|
|
60
60
|
this.result = result;
|
|
61
61
|
this.emitted = emitted;
|
|
62
62
|
this.held = held;
|
|
63
|
-
this.expressionDepth = expressionDepth;
|
|
64
63
|
|
|
65
64
|
new StateFacade(this);
|
|
66
65
|
}
|
|
@@ -73,39 +72,6 @@ export const State = class AgastState extends WeakStackFrame {
|
|
|
73
72
|
return !!this.held;
|
|
74
73
|
}
|
|
75
74
|
|
|
76
|
-
shift() {
|
|
77
|
-
const { tagNodes, prevTerminals, nextTerminals } = this.context;
|
|
78
|
-
|
|
79
|
-
const finishedNode = tagNodes.get(this.result);
|
|
80
|
-
|
|
81
|
-
if (!finishedNode.openTag.value.flags.expression) {
|
|
82
|
-
throw new Error();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
this.result = prevTerminals.get(finishedNode.openTag);
|
|
86
|
-
|
|
87
|
-
nextTerminals.delete(this.result);
|
|
88
|
-
|
|
89
|
-
this.held = finishedNode;
|
|
90
|
-
|
|
91
|
-
// put the first expression node into the holding register
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
unshift() {
|
|
95
|
-
const { tagNodes, prevTerminals, nextTerminals } = this.context;
|
|
96
|
-
|
|
97
|
-
if (!this.held) {
|
|
98
|
-
throw new Error('cannot unshift when no expression is in the holding register');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
nextTerminals.set(this.result, this.held.openTag);
|
|
102
|
-
prevTerminals.set(this.held.openTag, this.result);
|
|
103
|
-
|
|
104
|
-
this.result = this.held.closeTag;
|
|
105
|
-
|
|
106
|
-
this.held = null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
75
|
*emit(terminal, suppressEmit) {
|
|
110
76
|
const { prevTerminals, nextTerminals, tagNodes } = this.context;
|
|
111
77
|
|
|
@@ -118,13 +84,11 @@ export const State = class AgastState extends WeakStackFrame {
|
|
|
118
84
|
this.result?.type === 'Reference' &&
|
|
119
85
|
!['OpenNodeTag', 'Gap', 'Null'].includes(terminal.type)
|
|
120
86
|
) {
|
|
121
|
-
throw new Error(
|
|
87
|
+
throw new Error(`${terminal.type} is not a valid reference target`);
|
|
122
88
|
}
|
|
123
89
|
|
|
124
90
|
prevTerminals.set(terminal, this.result);
|
|
125
|
-
|
|
126
|
-
nextTerminals.set(this.result, terminal);
|
|
127
|
-
}
|
|
91
|
+
nextTerminals.set(this.result, terminal);
|
|
128
92
|
|
|
129
93
|
this.result = terminal;
|
|
130
94
|
|
|
@@ -135,12 +99,16 @@ export const State = class AgastState extends WeakStackFrame {
|
|
|
135
99
|
}
|
|
136
100
|
}
|
|
137
101
|
|
|
138
|
-
if (!this.depth && !
|
|
102
|
+
if (!this.depth && !suppressEmit) {
|
|
139
103
|
let emittable = nextTerminals.get(this.emitted);
|
|
140
104
|
|
|
141
105
|
while (
|
|
142
106
|
emittable &&
|
|
143
|
-
!(
|
|
107
|
+
!(
|
|
108
|
+
emittable.type === 'OpenNodeTag' &&
|
|
109
|
+
emittable.value.type &&
|
|
110
|
+
tagNodes.get(emittable).unboundAttributes?.size
|
|
111
|
+
)
|
|
144
112
|
) {
|
|
145
113
|
yield emittable;
|
|
146
114
|
this.emitted = emittable;
|
|
@@ -164,16 +132,16 @@ export const State = class AgastState extends WeakStackFrame {
|
|
|
164
132
|
}
|
|
165
133
|
|
|
166
134
|
branch() {
|
|
167
|
-
const { context, path, node, result, emitted, held
|
|
135
|
+
const { context, path, node, result, emitted, held } = this;
|
|
168
136
|
|
|
169
|
-
return this.push(context, path, node.branch(), result, emitted, held
|
|
137
|
+
return this.push(context, path, node.branch(), result, emitted, held);
|
|
170
138
|
}
|
|
171
139
|
|
|
172
140
|
accept() {
|
|
173
141
|
const { parent } = this;
|
|
174
142
|
|
|
175
143
|
if (!parent) {
|
|
176
|
-
|
|
144
|
+
return null;
|
|
177
145
|
}
|
|
178
146
|
|
|
179
147
|
parent.node.accept(this.node);
|
|
@@ -183,7 +151,6 @@ export const State = class AgastState extends WeakStackFrame {
|
|
|
183
151
|
parent.result = this.result;
|
|
184
152
|
parent.held = this.held;
|
|
185
153
|
parent.path = this.path;
|
|
186
|
-
parent.expressionDepth = this.expressionDepth;
|
|
187
154
|
|
|
188
155
|
return parent;
|
|
189
156
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { findRight } from './array.js';
|
|
2
|
+
|
|
3
|
+
const skipLevels = 3;
|
|
4
|
+
const skipShiftExponentGrowth = 4;
|
|
5
|
+
const skipAmounts = new Array(skipLevels)
|
|
6
|
+
.fill(null)
|
|
7
|
+
.map((_, i) => 2 >> (i * skipShiftExponentGrowth));
|
|
8
|
+
const skipsByFrame = new WeakMap();
|
|
9
|
+
|
|
10
|
+
export const buildSkips = (frame) => {
|
|
11
|
+
let skipIdx = 0;
|
|
12
|
+
let skipAmount = skipAmounts[skipIdx];
|
|
13
|
+
let skips;
|
|
14
|
+
while ((frame.depth & skipAmount) === skipAmount) {
|
|
15
|
+
if (!skips) {
|
|
16
|
+
skips = [];
|
|
17
|
+
skipsByFrame.set(frame, skips);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
skips[skipIdx] = frame.at(frame.depth - skipAmount);
|
|
21
|
+
|
|
22
|
+
skipIdx++;
|
|
23
|
+
skipAmount = skipAmounts[skipIdx];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const skipToDepth = (depth, frame) => {
|
|
28
|
+
let parent = frame;
|
|
29
|
+
|
|
30
|
+
if (depth > frame.depth) throw new Error();
|
|
31
|
+
|
|
32
|
+
let d = frame.depth;
|
|
33
|
+
for (; d > depth; ) {
|
|
34
|
+
const skips = skipsByFrame.get(frame);
|
|
35
|
+
parent = (skips && findRight(skips, (skip) => d - skip > depth)) || parent.parent;
|
|
36
|
+
d = parent.depth;
|
|
37
|
+
}
|
|
38
|
+
return parent;
|
|
39
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bablr/agast-vm",
|
|
3
3
|
"description": "A VM providing DOM-like guarantees about agAST trees",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"author": "Conrad Buck<conartist6@gmail.com>",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"test": "node ./test/runner.js"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@bablr/agast-helpers": "0.1.
|
|
19
|
-
"@bablr/agast-vm-helpers": "0.1.
|
|
18
|
+
"@bablr/agast-helpers": "0.1.6",
|
|
19
|
+
"@bablr/agast-vm-helpers": "0.1.5",
|
|
20
20
|
"@bablr/coroutine": "0.1.0",
|
|
21
21
|
"@bablr/weak-stack": "0.1.0"
|
|
22
22
|
},
|