@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 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 `StartFragmentTag`, `EndFragmentTag`, `StartNodeTag`, `EndNodeTag`, `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`.
16
8
 
17
- The VM requires the basic invariants of CSTML to be followed, for example that `Reference` must be followed by either a `StartNodeTag` or a `Gap`. In fact, `agast-vm` is the reference implementation of these invariants.
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 = StartFragmentTag | EndFragmentTag | StartNodeTag | EndNodeTag | Literal | Reference | Gap;
18
+ type Token = OpenNodeTag | CloseNodeTag | Literal | Reference | Gap;
27
19
 
28
- type StartFragmentTag {
29
- type: 'StartFragmentTag',
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 EndNodeTag {
58
- type: 'EndNodeTag',
59
- value: null
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
- 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* agastStrategy(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* 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 { arguments: { 0: terminal } = [] } = 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* agastStrategy(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,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 '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(s.path, 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
 
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 boundAttributes = Object.entries(attributes).filter((a) => a[1].type !== 'Gap');
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 (intrinsicValue && !flags.token) {
203
- throw new Error('intrinsic nodes must occur inside tokens');
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
- if (!flags.expression) {
207
- const { flags } = openTag.value;
218
+ const { flags: openFlags } = openTag.value;
208
219
 
209
- if (!(flags.trivia || flags.escape) && !s.path.depth) {
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
- const newNode = s.node.push(s.path, openTag);
226
+ const newNode = s.node.push(s.path, openTag);
219
227
 
220
- newNode.unboundAttributes = new Set(unboundAttributes.map((e) => e[0]));
228
+ newNode.unboundAttributes = new Set(unboundAttributes);
221
229
 
222
- ctx.tagNodes.set(openTag, newNode);
230
+ ctx.tagNodes.set(openTag, newNode);
223
231
 
224
- if (!flags.intrinsic) {
225
- s.node = newNode;
226
- } else {
227
- s.path = s.path.parent;
232
+ if (!intrinsicValue) {
233
+ s.node = newNode;
234
+ } else {
235
+ s.path = s.path.parent;
228
236
 
229
- if (s.path.depth > 1) {
230
- const { properties } = s.node;
231
- const { name: refName, isArray } = reference.value;
237
+ if (s.path.depth > 1) {
238
+ const { properties } = s.node;
239
+ const { name: refName, isArray } = reference.value;
232
240
 
233
- if (!isArray) {
234
- properties.set(refName, [openTag, openTag]);
235
- } else {
236
- if (!properties.has(refName)) {
237
- 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]);
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
- if (s.node.unboundAttributes?.size)
256
- throw new Error('Grammar failed to bind all attributes');
264
+ const closeTag = buildNodeCloseTag(type, language);
257
265
 
258
- 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');
259
269
 
260
- if (s.path.depth > 1 && type !== openType)
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
- 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
+ );
266
276
 
267
- if (!flags.escape && !flags.trivia) {
268
- const { name: refName, isArray } = s.path.reference.value;
277
+ if (!flags.escape && !flags.trivia) {
278
+ const { name: refName, isArray } = s.path.reference.value;
269
279
 
270
- if (s.path.depth > 2) {
271
- const { properties } = s.node.parent;
280
+ // is this right?
281
+ if (s.path.depth > 2) {
282
+ const { properties } = s.node.parent;
272
283
 
273
- if (!isArray) {
274
- properties.set(refName, [openTag, closeTag]);
275
- } else {
276
- if (!properties.has(refName)) {
277
- 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]);
278
291
  }
279
- properties.get(refName).push([openTag, closeTag]);
280
292
  }
281
293
  }
282
- }
283
294
 
284
- if (flags.expression) {
285
- s.expressionDepth--;
286
- }
295
+ ctx.tagNodes.set(closeTag, s.node);
287
296
 
288
- ctx.tagNodes.set(closeTag, s.node);
297
+ s.node.closeTag = closeTag;
289
298
 
290
- s.node.closeTag = closeTag;
299
+ s.node = s.node.parent;
291
300
 
292
- s.node = s.node.parent;
293
-
294
- if (!(flags.trivia || flags.escape)) {
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 { arguments: { 0: key, 1: value } = [] } = instr;
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 === 'OpenFragmentTag') {
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 { 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) {
@@ -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
- get context() {
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 || Symbol.for('@bablr/fragment');
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.push(
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
- let skipIdx = 0;
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
- let parent = this;
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 = 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);
@@ -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.expressionDepth = this.expressionDepth;
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.1.2",
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.1",
19
- "@bablr/agast-vm-helpers": "0.1.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
  },