@bablr/agast-vm 0.1.2 → 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 +13 -33
- package/lib/context.js +8 -2
- package/lib/evaluate.js +112 -107
- package/lib/path.js +24 -49
- package/lib/state.js +14 -46
- package/lib/utils/skip.js +39 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,19 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
The agAST VM provides consistency guarantees when with CSTML documents to parse or transform code. It has no language-specific functionality of any kind. Instead it acts as a streaming traversal engine for CSTML.
|
|
4
4
|
|
|
5
|
-
## Why
|
|
6
|
-
|
|
7
|
-
The goal of this project is to transition editors towards being a lot more like web browsers. You can have many of them, and they can be written in a variety of languages (though many share internals). You can even have a terminal browser like Lynx that does presentation very differently, yet it is still possible (if not trivial) to write a website once that will run on all (er, most) web browsers.
|
|
8
|
-
|
|
9
|
-
If the parallel is not immediately obvious, try thinking about it this way: a webapp is really more or less a set of automated tools for editing a DOM tree. As programmers we have all these amazing web libraries and frameworks that can exist because at the end of the day everything comes down to editing a shared DOM tree. There's a great explanation of those dynamics here: https://glazkov.com/2024/01/02/the-protocol-force/
|
|
10
|
-
|
|
11
|
-
If a code-DOM existed and was shared by all IDEs there would spring up a rich ecosystem of tools for accomplishing many kinds of tree alterations. For example it could become common for library authors to publish codemods that could help any project upgrade past breaking changes in its APIs!
|
|
12
|
-
|
|
13
5
|
## API
|
|
14
6
|
|
|
15
|
-
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`.
|
|
16
8
|
|
|
17
|
-
The VM requires the basic invariants of CSTML to be followed, for example that `Reference` must be followed by either a `
|
|
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.
|
|
18
10
|
|
|
19
11
|
The VM supports `branch()`, `accept()`, and `reject()` instructions, which allow a series of instructions to have their effects applied or discarded together in a kind of transaction.
|
|
20
12
|
|
|
@@ -23,40 +15,28 @@ Finally the VM supports `bindAttribute(key, value)`. A node's attributes start u
|
|
|
23
15
|
Here are the basic types used by the VM:
|
|
24
16
|
|
|
25
17
|
```ts
|
|
26
|
-
type Token =
|
|
18
|
+
type Token = OpenNodeTag | CloseNodeTag | Literal | Reference | Gap;
|
|
27
19
|
|
|
28
|
-
type
|
|
29
|
-
type: '
|
|
30
|
-
value: {
|
|
31
|
-
flags: {
|
|
32
|
-
trivia: boolean
|
|
33
|
-
},
|
|
34
|
-
language: string,
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type EndFragmentTag {
|
|
39
|
-
type: 'EndFragmentTag',
|
|
40
|
-
value: null
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
type StartNodeTag {
|
|
44
|
-
type: 'StartNodeTag',
|
|
20
|
+
type OpenNodeTag {
|
|
21
|
+
type: 'OpenNodeTag',
|
|
45
22
|
value: {
|
|
46
23
|
flags: {
|
|
47
24
|
token: boolean,
|
|
48
25
|
trivia: boolean,
|
|
49
26
|
escape: boolean
|
|
50
27
|
},
|
|
51
|
-
language: string,
|
|
52
|
-
type: string,
|
|
28
|
+
language: string | null,
|
|
29
|
+
type: string | null, // null type indicates a fragment
|
|
53
30
|
attributes: { [key: string]: boolean | number | string }
|
|
54
31
|
}
|
|
55
32
|
}
|
|
56
33
|
|
|
57
|
-
type
|
|
58
|
-
type: '
|
|
59
|
-
value:
|
|
34
|
+
type CloseNodeTag {
|
|
35
|
+
type: 'CloseNodeTag',
|
|
36
|
+
value: {
|
|
37
|
+
language: string,
|
|
38
|
+
type: string,
|
|
39
|
+
}
|
|
60
40
|
}
|
|
61
41
|
|
|
62
42
|
type Literal {
|
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*
|
|
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* agastStrategy(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* agastStrategy(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* agastStrategy(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,104 +151,101 @@ const __evaluate = function* agastStrategy(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(s.path, 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
|
|
|
183
198
|
case 'OpenNodeTag': {
|
|
184
199
|
const { flags, language, type, intrinsicValue, attributes } = terminal.value;
|
|
200
|
+
const { unboundAttributes } = options || {};
|
|
185
201
|
const reference = s.result;
|
|
186
|
-
const
|
|
187
|
-
const unboundAttributes = Object.entries(attributes).filter((a) => a[1].type === 'Gap');
|
|
188
|
-
const openTag = buildNodeOpenTag(
|
|
189
|
-
flags,
|
|
190
|
-
language,
|
|
191
|
-
type,
|
|
192
|
-
intrinsicValue,
|
|
193
|
-
Object.fromEntries(boundAttributes),
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
if (!flags.trivia && !flags.escape && s.path.depth > 1) {
|
|
197
|
-
if (reference.type !== 'Reference' && reference.type !== 'OpenFragmentTag') {
|
|
198
|
-
throw new Error('Invalid location for OpenNodeTag');
|
|
199
|
-
}
|
|
200
|
-
}
|
|
202
|
+
const openTag = buildNodeOpenTag(flags, language, type, intrinsicValue, attributes);
|
|
201
203
|
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
}
|
|
216
|
+
}
|
|
205
217
|
|
|
206
|
-
|
|
207
|
-
const { flags } = openTag.value;
|
|
218
|
+
const { flags: openFlags } = openTag.value;
|
|
208
219
|
|
|
209
|
-
if (!(
|
|
220
|
+
if (!(openFlags.trivia || openFlags.escape) && !s.path.depth) {
|
|
210
221
|
const tag = buildReference('root', false);
|
|
211
222
|
s.path = s.path.push(ctx, tag);
|
|
212
223
|
s.node.resolver.consume(tag);
|
|
213
224
|
}
|
|
214
|
-
} else if (!intrinsicValue) {
|
|
215
|
-
s.expressionDepth++;
|
|
216
|
-
}
|
|
217
225
|
|
|
218
|
-
|
|
226
|
+
const newNode = s.node.push(s.path, openTag);
|
|
219
227
|
|
|
220
|
-
|
|
228
|
+
newNode.unboundAttributes = new Set(unboundAttributes);
|
|
221
229
|
|
|
222
|
-
|
|
230
|
+
ctx.tagNodes.set(openTag, newNode);
|
|
223
231
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
232
|
+
if (!intrinsicValue) {
|
|
233
|
+
s.node = newNode;
|
|
234
|
+
} else {
|
|
235
|
+
s.path = s.path.parent;
|
|
228
236
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
237
|
+
if (s.path.depth > 1) {
|
|
238
|
+
const { properties } = s.node;
|
|
239
|
+
const { name: refName, isArray } = reference.value;
|
|
232
240
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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]);
|
|
238
248
|
}
|
|
239
|
-
properties.get(refName).push([openTag, openTag]);
|
|
240
249
|
}
|
|
241
250
|
}
|
|
242
251
|
}
|
|
@@ -252,47 +261,46 @@ const __evaluate = function* agastStrategy(ctx, strategy) {
|
|
|
252
261
|
const { openTag } = s.node;
|
|
253
262
|
const { flags, type: openType } = openTag.value;
|
|
254
263
|
|
|
255
|
-
|
|
256
|
-
throw new Error('Grammar failed to bind all attributes');
|
|
264
|
+
const closeTag = buildNodeCloseTag(type, language);
|
|
257
265
|
|
|
258
|
-
if (
|
|
266
|
+
if (openType) {
|
|
267
|
+
if (s.node.unboundAttributes?.size)
|
|
268
|
+
throw new Error('Grammar failed to bind all attributes');
|
|
259
269
|
|
|
260
|
-
|
|
261
|
-
throw new Error(
|
|
262
|
-
`Grammar close {type: ${type}} did not match open {type: ${openType}}`,
|
|
263
|
-
);
|
|
270
|
+
if (!type) throw new Error(`CloseNodeTag must have type`);
|
|
264
271
|
|
|
265
|
-
|
|
272
|
+
if (s.path.depth > 1 && type !== openType)
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Grammar close {type: ${type}} did not match open {type: ${openType}}`,
|
|
275
|
+
);
|
|
266
276
|
|
|
267
|
-
|
|
268
|
-
|
|
277
|
+
if (!flags.escape && !flags.trivia) {
|
|
278
|
+
const { name: refName, isArray } = s.path.reference.value;
|
|
269
279
|
|
|
270
|
-
|
|
271
|
-
|
|
280
|
+
// is this right?
|
|
281
|
+
if (s.path.depth > 2) {
|
|
282
|
+
const { properties } = s.node.parent;
|
|
272
283
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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]);
|
|
278
291
|
}
|
|
279
|
-
properties.get(refName).push([openTag, closeTag]);
|
|
280
292
|
}
|
|
281
293
|
}
|
|
282
|
-
}
|
|
283
294
|
|
|
284
|
-
|
|
285
|
-
s.expressionDepth--;
|
|
286
|
-
}
|
|
295
|
+
ctx.tagNodes.set(closeTag, s.node);
|
|
287
296
|
|
|
288
|
-
|
|
297
|
+
s.node.closeTag = closeTag;
|
|
289
298
|
|
|
290
|
-
|
|
299
|
+
s.node = s.node.parent;
|
|
291
300
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
s.path = s.path.parent;
|
|
301
|
+
if (!(flags.trivia || flags.escape)) {
|
|
302
|
+
s.path = s.path.parent;
|
|
303
|
+
}
|
|
296
304
|
}
|
|
297
305
|
|
|
298
306
|
yield* s.emit(closeTag, flags.expression);
|
|
@@ -308,18 +316,8 @@ const __evaluate = function* agastStrategy(ctx, strategy) {
|
|
|
308
316
|
break;
|
|
309
317
|
}
|
|
310
318
|
|
|
311
|
-
case 'shift': {
|
|
312
|
-
s.shift();
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
case 'unshift': {
|
|
317
|
-
s.unshift();
|
|
318
|
-
break;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
319
|
case 'bindAttribute': {
|
|
322
|
-
const {
|
|
320
|
+
const { 0: key, 1: value } = args;
|
|
323
321
|
|
|
324
322
|
const { unboundAttributes } = s.node;
|
|
325
323
|
|
|
@@ -327,8 +325,8 @@ const __evaluate = function* agastStrategy(ctx, strategy) {
|
|
|
327
325
|
throw new Error('No unbound attribute to bind');
|
|
328
326
|
}
|
|
329
327
|
|
|
330
|
-
if (s.node.openTag.type
|
|
331
|
-
throw new Error();
|
|
328
|
+
if (!s.node.openTag.value.type) {
|
|
329
|
+
throw new Error('Cannot bind attribute to fragment');
|
|
332
330
|
}
|
|
333
331
|
|
|
334
332
|
if (key === 'span') throw new Error('too late');
|
|
@@ -388,6 +386,13 @@ const __evaluate = function* agastStrategy(ctx, strategy) {
|
|
|
388
386
|
break;
|
|
389
387
|
}
|
|
390
388
|
|
|
389
|
+
case 'write': {
|
|
390
|
+
if (options.emitEffects) {
|
|
391
|
+
yield buildWriteEffect(args[0], args[1]);
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
|
|
391
396
|
default: {
|
|
392
397
|
throw new Error(`Unexpected call of {type: ${printExpression(verb)}}`);
|
|
393
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) {
|
|
@@ -117,10 +104,8 @@ export const Node = class AgastNode extends WeakStackFrame {
|
|
|
117
104
|
this.properties = properties;
|
|
118
105
|
this.resolver = resolver;
|
|
119
106
|
this.unboundAttributes = unboundAttributes;
|
|
120
|
-
}
|
|
121
107
|
|
|
122
|
-
|
|
123
|
-
return this.path.context;
|
|
108
|
+
buildSkips(this);
|
|
124
109
|
}
|
|
125
110
|
|
|
126
111
|
get language() {
|
|
@@ -128,7 +113,7 @@ export const Node = class AgastNode extends WeakStackFrame {
|
|
|
128
113
|
}
|
|
129
114
|
|
|
130
115
|
get type() {
|
|
131
|
-
return this.openTag.value?.type ||
|
|
116
|
+
return this.openTag.value?.type || null;
|
|
132
117
|
}
|
|
133
118
|
|
|
134
119
|
get flags() {
|
|
@@ -139,10 +124,22 @@ export const Node = class AgastNode extends WeakStackFrame {
|
|
|
139
124
|
return this.openTag.value?.attributes || {};
|
|
140
125
|
}
|
|
141
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
|
+
|
|
142
139
|
branch() {
|
|
143
140
|
const { path, openTag, closeTag, properties, resolver, unboundAttributes } = this;
|
|
144
141
|
|
|
145
|
-
return this.
|
|
142
|
+
return this.replace(
|
|
146
143
|
path,
|
|
147
144
|
openTag,
|
|
148
145
|
closeTag,
|
|
@@ -153,6 +150,7 @@ export const Node = class AgastNode extends WeakStackFrame {
|
|
|
153
150
|
}
|
|
154
151
|
|
|
155
152
|
accept(node) {
|
|
153
|
+
this.path = node.path;
|
|
156
154
|
this.openTag = node.openTag;
|
|
157
155
|
this.closeTag = node.closeTag;
|
|
158
156
|
this.properties = node.properties;
|
|
@@ -179,36 +177,13 @@ export const Path = class AgastPath extends WeakStackFrame {
|
|
|
179
177
|
this.context = context;
|
|
180
178
|
this.reference = reference;
|
|
181
179
|
|
|
182
|
-
|
|
183
|
-
let skipAmount = skipAmounts[skipIdx];
|
|
184
|
-
let skips;
|
|
185
|
-
while ((this.depth & skipAmount) === skipAmount) {
|
|
186
|
-
if (!skips) {
|
|
187
|
-
skips = [];
|
|
188
|
-
skipsByPath.set(this, skips);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
skips[skipIdx] = this.at(this.depth - skipAmount);
|
|
192
|
-
|
|
193
|
-
skipIdx++;
|
|
194
|
-
skipAmount = skipAmounts[skipIdx];
|
|
195
|
-
}
|
|
180
|
+
buildSkips(this);
|
|
196
181
|
|
|
197
182
|
new PathFacade(this);
|
|
198
183
|
}
|
|
199
184
|
|
|
200
185
|
at(depth) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (depth > this.depth) throw new Error();
|
|
204
|
-
|
|
205
|
-
let d = this.depth;
|
|
206
|
-
for (; d > depth; ) {
|
|
207
|
-
const skips = skipsByPath.get(this);
|
|
208
|
-
parent = (skips && findRight(skips, (skip) => d - skip > depth)) || parent.parent;
|
|
209
|
-
d = parent.depth;
|
|
210
|
-
}
|
|
211
|
-
return parent;
|
|
186
|
+
return skipToDepth(depth, this);
|
|
212
187
|
}
|
|
213
188
|
|
|
214
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);
|
|
@@ -182,7 +150,7 @@ export const State = class AgastState extends WeakStackFrame {
|
|
|
182
150
|
|
|
183
151
|
parent.result = this.result;
|
|
184
152
|
parent.held = this.held;
|
|
185
|
-
parent.
|
|
153
|
+
parent.path = this.path;
|
|
186
154
|
|
|
187
155
|
return parent;
|
|
188
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
|
},
|