@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 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 `OpenFragmentTag`, `CloseFragmentTag`, `OpenNodeTag`, `CloseNodeTag`, `Literal`, `Reference`, or `Gap`.
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 = OpenFragmentTag | CloseFragmentTag | OpenNodeTag | CloseNodeTag | Literal | Reference | Gap;
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
- return this.tagNodes.get(result[0] || result).properties.get(name);
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) => new StreamIterable(__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 { arguments: { 0: terminal, 1: options } = [] } = instr;
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('double reference');
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 'Null': {
149
- const reference = s.result;
162
+ case 'Shift': {
163
+ const { tagNodes, prevTerminals } = ctx;
164
+ const tag = buildShift();
150
165
 
151
- if (reference?.type !== 'Reference') throw new Error();
166
+ const finishedNode = tagNodes.get(s.result);
167
+ const finishedPath = finishedNode.path;
152
168
 
153
- s.path = s.path.parent;
169
+ s.held = { node: finishedNode, path: finishedPath };
154
170
 
155
- const null_ = buildNull();
171
+ if (!finishedNode.openTag.value.flags.expression) {
172
+ throw new Error();
173
+ }
156
174
 
157
- yield* s.emit(null_);
175
+ s.path = finishedPath;
158
176
 
159
- returnValue = null_;
177
+ yield* s.emit(tag);
178
+
179
+ returnValue = tag;
160
180
  break;
161
181
  }
162
182
 
163
- case 'OpenFragmentTag': {
164
- const openTag = buildFragmentOpenTag();
165
-
166
- s.node = Node.from(openTag);
183
+ case 'Null': {
184
+ const reference = s.result;
167
185
 
168
- yield* s.emit(openTag);
186
+ if (reference?.type !== 'Reference') throw new Error();
169
187
 
170
- returnValue = openTag;
171
- break;
172
- }
188
+ s.path = s.path.parent;
173
189
 
174
- case 'CloseFragmentTag': {
175
- const closeTag = buildFragmentCloseTag();
190
+ const null_ = buildNull();
176
191
 
177
- yield* s.emit(closeTag);
192
+ yield* s.emit(null_);
178
193
 
179
- returnValue = closeTag;
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 (!flags.trivia && !flags.escape && s.path.depth > 1) {
190
- if (reference.type !== 'Reference' && reference.type !== 'OpenFragmentTag') {
191
- throw new Error('Invalid location for OpenNodeTag');
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
- const { flags: openFlags } = openTag.value;
218
+ const { flags: openFlags } = openTag.value;
196
219
 
197
- if (!(openFlags.trivia || openFlags.escape) && !s.path.depth) {
198
- const tag = buildReference('root', false);
199
- s.path = s.path.push(ctx, tag);
200
- s.node.resolver.consume(tag);
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
- const newNode = s.node.push(openTag);
226
+ const newNode = s.node.push(s.path, openTag);
208
227
 
209
- newNode.unboundAttributes = new Set(unboundAttributes);
228
+ newNode.unboundAttributes = new Set(unboundAttributes);
210
229
 
211
- ctx.tagNodes.set(openTag, newNode);
230
+ ctx.tagNodes.set(openTag, newNode);
212
231
 
213
- if (!intrinsicValue) {
214
- s.node = newNode;
215
- } else {
216
- s.path = s.path.parent;
232
+ if (!intrinsicValue) {
233
+ s.node = newNode;
234
+ } else {
235
+ s.path = s.path.parent;
217
236
 
218
- if (s.path.depth > 1) {
219
- const { properties } = s.node;
220
- const { name: refName, isArray } = reference.value;
237
+ if (s.path.depth > 1) {
238
+ const { properties } = s.node;
239
+ const { name: refName, isArray } = reference.value;
221
240
 
222
- if (!isArray) {
223
- properties.set(refName, [openTag, openTag]);
224
- } else {
225
- if (!properties.has(refName)) {
226
- properties.set(refName, []);
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
- if (s.node.unboundAttributes?.size)
245
- throw new Error('Grammar failed to bind all attributes');
264
+ const closeTag = buildNodeCloseTag(type, language);
246
265
 
247
- if (!type) throw new Error(`CloseNodeTag must have type`);
266
+ if (openType) {
267
+ if (s.node.unboundAttributes?.size)
268
+ throw new Error('Grammar failed to bind all attributes');
248
269
 
249
- if (s.path.depth > 1 && type !== openType)
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
- const closeTag = buildNodeCloseTag(type, language);
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
- if (!flags.escape && !flags.trivia) {
257
- const { name: refName, isArray } = s.path.reference.value;
277
+ if (!flags.escape && !flags.trivia) {
278
+ const { name: refName, isArray } = s.path.reference.value;
258
279
 
259
- // is this right?
260
- if (s.path.depth > 2) {
261
- const { properties } = s.node.parent;
280
+ // is this right?
281
+ if (s.path.depth > 2) {
282
+ const { properties } = s.node.parent;
262
283
 
263
- if (!isArray) {
264
- properties.set(refName, [openTag, closeTag]);
265
- } else {
266
- if (!properties.has(refName)) {
267
- properties.set(refName, []);
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
- if (flags.expression) {
275
- s.expressionDepth--;
276
- }
295
+ ctx.tagNodes.set(closeTag, s.node);
277
296
 
278
- ctx.tagNodes.set(closeTag, s.node);
297
+ s.node.closeTag = closeTag;
279
298
 
280
- s.node.closeTag = closeTag;
299
+ s.node = s.node.parent;
281
300
 
282
- s.node = s.node.parent;
283
-
284
- if (!(flags.trivia || flags.escape)) {
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 { arguments: { 0: key, 1: value } = [] } = instr;
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 === 'OpenFragmentTag') {
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 { findRight } from './utils/array.js';
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
- let parent = this;
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 || Symbol.for('@bablr/fragment');
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
- let skipIdx = 0;
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
- let parent = this;
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 = null,
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('Bad reference emit');
87
+ throw new Error(`${terminal.type} is not a valid reference target`);
122
88
  }
123
89
 
124
90
  prevTerminals.set(terminal, this.result);
125
- if (this.result) {
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 && !this.expressionDepth && !suppressEmit) {
102
+ if (!this.depth && !suppressEmit) {
139
103
  let emittable = nextTerminals.get(this.emitted);
140
104
 
141
105
  while (
142
106
  emittable &&
143
- !(emittable.type === 'OpenNodeTag' && tagNodes.get(emittable).unboundAttributes?.size)
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, expressionDepth } = this;
135
+ const { context, path, node, result, emitted, held } = this;
168
136
 
169
- return this.push(context, path, node.branch(), result, emitted, held, expressionDepth);
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
- throw new Error('accepted the root state');
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.1.3",
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.5",
19
- "@bablr/agast-vm-helpers": "0.1.3",
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
  },