@bablr/agast-vm 0.1.3 → 0.3.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
@@ -11,7 +11,8 @@ function* ownTerminalsFor(range, nextTerminals, tagNodes) {
11
11
 
12
12
  for (let term = start; term && term !== pastEnd; term = nextTerminals.get(term)) {
13
13
  if (!(term === range[0] || term === range[1]) && term.type === 'OpenNodeTag') {
14
- term = nextTerminals.get(tagNodes.get(term).endTag);
14
+ term = tagNodes.get(term).closeTag;
15
+ continue;
15
16
  }
16
17
 
17
18
  yield term;
@@ -24,7 +25,7 @@ function* allTerminalsFor(range, nextTerminals) {
24
25
 
25
26
  const pastEnd = nextTerminals.get(end);
26
27
 
27
- for (let tag = start; tag !== pastEnd; tag = nextTerminals.get(tag)) {
28
+ for (let tag = start; tag && tag !== pastEnd; tag = nextTerminals.get(tag)) {
28
29
  yield tag;
29
30
  }
30
31
  }
@@ -70,6 +71,14 @@ export const ContextFacade = class AgastContextFacade {
70
71
  return actuals.get(this).nodeForTag(tag);
71
72
  }
72
73
 
74
+ pathForTag(tag) {
75
+ return actuals.get(this).pathForTag(tag);
76
+ }
77
+
78
+ nodeForPath(path) {
79
+ return actuals.get(this).nodeForPath(path);
80
+ }
81
+
73
82
  unbox(value) {
74
83
  return actuals.get(this).unbox(value);
75
84
  }
@@ -84,6 +93,8 @@ export const Context = class AgastContext {
84
93
  this.prevTerminals = new WeakMap();
85
94
  this.nextTerminals = new WeakMap();
86
95
  this.tagNodes = new WeakMap();
96
+ this.tagPaths = new WeakMap();
97
+ this.pathNodes = new WeakMap();
87
98
  this.unboxedValues = new WeakMap();
88
99
  this.facade = new ContextFacade();
89
100
 
@@ -91,7 +102,13 @@ export const Context = class AgastContext {
91
102
  }
92
103
 
93
104
  getProperty(result, name) {
94
- return this.tagNodes.get(result[0] || result).properties.get(name);
105
+ let startTag = result[0] || result;
106
+
107
+ if (startTag.type === 'Reference') {
108
+ startTag = this.nextTerminals.get(startTag);
109
+ }
110
+
111
+ return this.tagNodes.get(startTag).properties.get(name);
95
112
  }
96
113
 
97
114
  isEmpty(range) {
@@ -123,10 +140,26 @@ export const Context = class AgastContext {
123
140
  return ownTerminalsFor(range, this.prevTerminals, this.tagNodes);
124
141
  }
125
142
 
143
+ getPreviousTerminal(token) {
144
+ return this.prevTerminals.get(token);
145
+ }
146
+
147
+ getNextTerminal(token) {
148
+ return this.nextTerminals.get(token);
149
+ }
150
+
126
151
  nodeForTag(tag) {
127
152
  return this.tagNodes.get(tag);
128
153
  }
129
154
 
155
+ pathForTag(ref) {
156
+ return this.tagPaths.get(ref);
157
+ }
158
+
159
+ nodeForPath(path) {
160
+ return this.pathNodes.get(path);
161
+ }
162
+
130
163
  sourceTextFor(range) {
131
164
  return sourceTextFor(this.allTerminalsFor(range));
132
165
  }
package/lib/evaluate.js CHANGED
@@ -2,24 +2,26 @@ 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';
14
14
  import { printExpression } from '@bablr/agast-helpers/print';
15
15
  import { reifyExpression } from '@bablr/agast-vm-helpers';
16
- import { Path, Node } from './path.js';
16
+ import { Path } from './path.js';
17
+ import { Node } from './node.js';
17
18
  import { State } from './state.js';
18
19
  import { facades } from './facades.js';
19
20
 
20
- export const evaluate = (ctx, strategy) => new StreamIterable(__evaluate(ctx, strategy));
21
+ export const evaluate = (ctx, strategy, options) =>
22
+ new StreamIterable(__evaluate(ctx, strategy, options));
21
23
 
22
- const __evaluate = function* agast(ctx, strategy) {
24
+ const __evaluate = function* agast(ctx, strategy, options = {}) {
23
25
  let s = State.from(ctx);
24
26
 
25
27
  const co = new Coroutine(getStreamIterator(strategy(facades.get(ctx), facades.get(s))));
@@ -37,7 +39,7 @@ const __evaluate = function* agast(ctx, strategy) {
37
39
  const instr = reifyExpression(sourceInstr);
38
40
  let returnValue = undefined;
39
41
 
40
- const { verb } = instr;
42
+ const { verb, arguments: args = [] } = instr;
41
43
 
42
44
  switch (verb) {
43
45
  case 'branch': {
@@ -66,13 +68,24 @@ const __evaluate = function* agast(ctx, strategy) {
66
68
  }
67
69
 
68
70
  case 'advance': {
69
- const { arguments: { 0: terminal, 1: options } = [] } = instr;
71
+ const { 0: terminal, 1: options } = args;
72
+
73
+ if (
74
+ s.held &&
75
+ !(
76
+ terminal.type === 'OpenNodeTag' ||
77
+ terminal.type === 'Reference' ||
78
+ terminal.type === 'Gap'
79
+ )
80
+ ) {
81
+ throw new Error('Cannot advance while holding');
82
+ }
70
83
 
71
84
  switch (terminal?.type || 'Null') {
72
85
  case 'DoctypeTag': {
73
86
  const { attributes } = terminal.value;
74
87
  const doctypeTag = buildDoctypeTag(attributes);
75
- const rootPath = Path.from(ctx);
88
+ const rootPath = Path.from(ctx, doctypeTag);
76
89
 
77
90
  if (s.path) {
78
91
  throw new Error();
@@ -109,20 +122,21 @@ const __evaluate = function* agast(ctx, strategy) {
109
122
  const tag = buildReference(name, isArray);
110
123
 
111
124
  if (s.result.type === 'Reference') {
112
- throw new Error('double reference');
125
+ throw new Error('A reference must have a non-reference value');
113
126
  }
114
127
 
115
- if (!s.path.depth) {
128
+ if (s.node?.flags.token) {
116
129
  throw new Error();
117
130
  }
118
131
 
119
- if (s.node.flags.token) {
120
- throw new Error();
132
+ if (s.path.depth) {
133
+ s.node.resolver.consume(tag);
121
134
  }
122
135
 
123
- s.node.resolver.consume(tag);
124
-
125
136
  s.path = s.path.push(ctx, tag);
137
+ s.node = null;
138
+
139
+ ctx.tagPaths.set(tag, s.path);
126
140
 
127
141
  yield* s.emit(tag);
128
142
 
@@ -135,10 +149,15 @@ const __evaluate = function* agast(ctx, strategy) {
135
149
 
136
150
  if (reference?.type !== 'Reference') throw new Error();
137
151
 
138
- s.path = s.path.parent;
139
-
140
152
  const gapTag = buildGap();
141
153
 
154
+ s.held = null;
155
+
156
+ ctx.tagPaths.set(gapTag, s.path);
157
+
158
+ s.node = s.parentNode;
159
+ s.path = s.path.parent;
160
+
142
161
  yield* s.emit(gapTag);
143
162
 
144
163
  returnValue = gapTag;
@@ -150,33 +169,39 @@ const __evaluate = function* agast(ctx, strategy) {
150
169
 
151
170
  if (reference?.type !== 'Reference') throw new Error();
152
171
 
153
- s.path = s.path.parent;
154
-
155
172
  const null_ = buildNull();
156
173
 
174
+ ctx.tagPaths.set(null_, s.path);
175
+
176
+ s.node = s.parentNode;
177
+ s.path = s.path.parent;
178
+
157
179
  yield* s.emit(null_);
158
180
 
159
181
  returnValue = null_;
160
182
  break;
161
183
  }
162
184
 
163
- case 'OpenFragmentTag': {
164
- const openTag = buildFragmentOpenTag();
185
+ case 'Shift': {
186
+ const tag = buildShift();
165
187
 
166
- s.node = Node.from(openTag);
188
+ const finishedNode = ctx.nodeForTag(s.result);
189
+ const ref = ctx.getPreviousTerminal(finishedNode.openTag);
190
+ const finishedPath = ctx.pathForTag(ref);
167
191
 
168
- yield* s.emit(openTag);
192
+ ctx.pathNodes.set(finishedPath, null);
169
193
 
170
- returnValue = openTag;
171
- break;
172
- }
194
+ s.held = { node: finishedNode, path: finishedPath };
173
195
 
174
- case 'CloseFragmentTag': {
175
- const closeTag = buildFragmentCloseTag();
196
+ if (!finishedNode.openTag.value.flags.expression) {
197
+ throw new Error();
198
+ }
176
199
 
177
- yield* s.emit(closeTag);
200
+ s.path = finishedPath;
178
201
 
179
- returnValue = closeTag;
202
+ yield* s.emit(tag);
203
+
204
+ returnValue = tag;
180
205
  break;
181
206
  }
182
207
 
@@ -186,36 +211,42 @@ const __evaluate = function* agast(ctx, strategy) {
186
211
  const reference = s.result;
187
212
  const openTag = buildNodeOpenTag(flags, language, type, intrinsicValue, attributes);
188
213
 
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');
214
+ if (!type) {
215
+ s.node = Node.from(openTag);
216
+ ctx.pathNodes.set(s.path, s.node);
217
+ ctx.pathNodes.set(s.node, s.path);
218
+ ctx.tagNodes.set(openTag, s.node);
219
+ ctx.tagPaths.set(openTag, s.path);
220
+ } else {
221
+ if (!flags.trivia && !flags.escape) {
222
+ if (
223
+ reference.type !== 'Reference' &&
224
+ reference.type !== 'Shift' &&
225
+ reference.type !== 'OpenFragmentTag'
226
+ ) {
227
+ throw new Error('Invalid location for OpenNodeTag');
228
+ }
192
229
  }
193
- }
194
230
 
195
- const { flags: openFlags } = openTag.value;
196
-
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
- }
231
+ const newNode = new Node(openTag);
206
232
 
207
- const newNode = s.node.push(openTag);
233
+ newNode.unboundAttributes = new Set(unboundAttributes);
208
234
 
209
- newNode.unboundAttributes = new Set(unboundAttributes);
235
+ s.node = newNode;
236
+ if (flags.trivia || flags.escape) {
237
+ s.path = s.path.push(ctx, null);
238
+ }
210
239
 
211
- ctx.tagNodes.set(openTag, newNode);
240
+ ctx.pathNodes.set(newNode, s.path);
241
+ ctx.pathNodes.set(s.path, newNode);
242
+ ctx.tagNodes.set(openTag, newNode);
243
+ ctx.tagPaths.set(openTag, s.path);
212
244
 
213
- if (!intrinsicValue) {
214
- s.node = newNode;
215
- } else {
216
- s.path = s.path.parent;
245
+ if (intrinsicValue) {
246
+ newNode.closeTag = newNode.openTag;
247
+ s.node = s.parentNode;
248
+ s.path = s.path.parent;
217
249
 
218
- if (s.path.depth > 1) {
219
250
  const { properties } = s.node;
220
251
  const { name: refName, isArray } = reference.value;
221
252
 
@@ -241,24 +272,23 @@ const __evaluate = function* agast(ctx, strategy) {
241
272
  const { openTag } = s.node;
242
273
  const { flags, type: openType } = openTag.value;
243
274
 
244
- if (s.node.unboundAttributes?.size)
245
- throw new Error('Grammar failed to bind all attributes');
275
+ const closeTag = buildNodeCloseTag(type, language);
246
276
 
247
- if (!type) throw new Error(`CloseNodeTag must have type`);
277
+ if (openType) {
278
+ if (s.node.unboundAttributes?.size)
279
+ throw new Error('Grammar failed to bind all attributes');
248
280
 
249
- if (s.path.depth > 1 && type !== openType)
250
- throw new Error(
251
- `Grammar close {type: ${type}} did not match open {type: ${openType}}`,
252
- );
281
+ if (!type) throw new Error(`CloseNodeTag must have type`);
253
282
 
254
- const closeTag = buildNodeCloseTag(type, language);
283
+ if (type !== openType)
284
+ throw new Error(
285
+ `Grammar close {type: ${type}} did not match open {type: ${openType}}`,
286
+ );
255
287
 
256
- if (!flags.escape && !flags.trivia) {
257
- const { name: refName, isArray } = s.path.reference.value;
288
+ if (!flags.escape && !flags.trivia) {
289
+ const { name: refName, isArray } = s.path.reference.value;
258
290
 
259
- // is this right?
260
- if (s.path.depth > 2) {
261
- const { properties } = s.node.parent;
291
+ const { properties } = ctx.nodeForPath(s.path.parent);
262
292
 
263
293
  if (!isArray) {
264
294
  properties.set(refName, [openTag, closeTag]);
@@ -269,19 +299,14 @@ const __evaluate = function* agast(ctx, strategy) {
269
299
  properties.get(refName).push([openTag, closeTag]);
270
300
  }
271
301
  }
272
- }
273
-
274
- if (flags.expression) {
275
- s.expressionDepth--;
276
- }
277
302
 
278
- ctx.tagNodes.set(closeTag, s.node);
303
+ ctx.tagNodes.set(closeTag, s.node);
304
+ ctx.tagPaths.set(closeTag, s.path);
279
305
 
280
- s.node.closeTag = closeTag;
306
+ s.node.closeTag = closeTag;
281
307
 
282
- s.node = s.node.parent;
308
+ s.node = s.parentNode;
283
309
 
284
- if (!(flags.trivia || flags.escape)) {
285
310
  s.path = s.path.parent;
286
311
  }
287
312
 
@@ -298,18 +323,8 @@ const __evaluate = function* agast(ctx, strategy) {
298
323
  break;
299
324
  }
300
325
 
301
- case 'shift': {
302
- s.shift();
303
- break;
304
- }
305
-
306
- case 'unshift': {
307
- s.unshift();
308
- break;
309
- }
310
-
311
326
  case 'bindAttribute': {
312
- const { arguments: { 0: key, 1: value } = [] } = instr;
327
+ const { 0: key, 1: value } = args;
313
328
 
314
329
  const { unboundAttributes } = s.node;
315
330
 
@@ -317,8 +332,8 @@ const __evaluate = function* agast(ctx, strategy) {
317
332
  throw new Error('No unbound attribute to bind');
318
333
  }
319
334
 
320
- if (s.node.openTag.type === 'OpenFragmentTag') {
321
- throw new Error();
335
+ if (!s.node.openTag.value.type) {
336
+ throw new Error('Cannot bind attribute to fragment');
322
337
  }
323
338
 
324
339
  if (key === 'span') throw new Error('too late');
@@ -339,25 +354,25 @@ const __evaluate = function* agast(ctx, strategy) {
339
354
  if (value != null) {
340
355
  const { flags, language, type, intrinsicValue } = openTag.value;
341
356
  const attributes = { ...openTag.value.attributes, [key]: value };
342
- const newStartTag = buildNodeOpenTag(flags, language, type, intrinsicValue, attributes);
357
+ const newOpenTag = buildNodeOpenTag(flags, language, type, intrinsicValue, attributes);
343
358
 
344
- let startNext = ctx.nextTerminals.get(openTag);
359
+ let openNext = ctx.nextTerminals.get(openTag);
345
360
  let startPrev = ctx.prevTerminals.get(openTag);
346
361
 
347
- ctx.prevTerminals.set(newStartTag, startPrev);
348
- ctx.nextTerminals.set(startPrev, newStartTag);
362
+ ctx.prevTerminals.set(newOpenTag, startPrev);
363
+ ctx.nextTerminals.set(startPrev, newOpenTag);
349
364
 
350
- ctx.tagNodes.set(newStartTag, s.node);
365
+ ctx.tagNodes.set(newOpenTag, ctx.tagNodes.get(openTag));
351
366
 
352
- if (startNext) {
353
- ctx.nextTerminals.set(newStartTag, startNext);
354
- ctx.prevTerminals.set(startNext, newStartTag);
367
+ if (openNext) {
368
+ ctx.nextTerminals.set(newOpenTag, openNext);
369
+ ctx.prevTerminals.set(openNext, newOpenTag);
355
370
  } else {
356
371
  // could this terminal be stored anywhere else?
357
- s.result = newStartTag;
372
+ s.result = newOpenTag;
358
373
  }
359
374
 
360
- s.node.openTag = newStartTag;
375
+ s.node.openTag = newOpenTag;
361
376
  }
362
377
 
363
378
  if (!unboundAttributes.size) {
@@ -378,6 +393,13 @@ const __evaluate = function* agast(ctx, strategy) {
378
393
  break;
379
394
  }
380
395
 
396
+ case 'write': {
397
+ if (options.emitEffects) {
398
+ yield buildWriteEffect(args[0], args[1]);
399
+ }
400
+ break;
401
+ }
402
+
381
403
  default: {
382
404
  throw new Error(`Unexpected call of {type: ${printExpression(verb)}}`);
383
405
  }
package/lib/node.js ADDED
@@ -0,0 +1,99 @@
1
+ import { Resolver } from '@bablr/agast-helpers/tree';
2
+ import { facades, actuals } from './facades.js';
3
+
4
+ export const NodeFacade = class AgastNodeFacade {
5
+ constructor(path) {
6
+ facades.set(path, this);
7
+ }
8
+
9
+ get language() {
10
+ return actuals.get(this).language;
11
+ }
12
+
13
+ get type() {
14
+ return actuals.get(this).type;
15
+ }
16
+
17
+ get range() {
18
+ return actuals.get(this).range;
19
+ }
20
+
21
+ get openTag() {
22
+ return actuals.get(this).openTag;
23
+ }
24
+
25
+ get closeTag() {
26
+ return actuals.get(this).closeTag;
27
+ }
28
+
29
+ get flags() {
30
+ return actuals.get(this).flags;
31
+ }
32
+
33
+ get attributes() {
34
+ return actuals.get(this).attributes;
35
+ }
36
+ };
37
+
38
+ export const Node = class AgastNode {
39
+ static from(openTag) {
40
+ return new AgastNode(openTag);
41
+ }
42
+
43
+ constructor(
44
+ openTag,
45
+ closeTag = null,
46
+ properties = new Map(),
47
+ resolver = new Resolver(),
48
+ unboundAttributes = null,
49
+ ) {
50
+ this.openTag = openTag;
51
+ this.closeTag = closeTag;
52
+ this.properties = properties;
53
+ this.resolver = resolver;
54
+ this.unboundAttributes = unboundAttributes;
55
+ }
56
+
57
+ get language() {
58
+ return this.openTag.value?.language;
59
+ }
60
+
61
+ get type() {
62
+ return this.openTag.value?.type || null;
63
+ }
64
+
65
+ get flags() {
66
+ return this.openTag.value?.flags || {};
67
+ }
68
+
69
+ get attributes() {
70
+ return this.openTag.value?.attributes || {};
71
+ }
72
+
73
+ get range() {
74
+ return [this.openTag, this.closeTag];
75
+ }
76
+
77
+ branch() {
78
+ const { openTag, closeTag, properties, resolver, unboundAttributes } = this;
79
+
80
+ return new Node(
81
+ openTag,
82
+ closeTag,
83
+ new Map(properties), // there is probably a better way
84
+ resolver.branch(),
85
+ new Set(unboundAttributes),
86
+ );
87
+ }
88
+
89
+ accept(node) {
90
+ this.openTag = node.openTag;
91
+ this.closeTag = node.closeTag;
92
+ this.properties = node.properties;
93
+ this.unboundAttributes = node.unboundAttributes;
94
+
95
+ this.resolver.accept(node.resolver);
96
+
97
+ return this;
98
+ }
99
+ };
package/lib/path.js CHANGED
@@ -1,61 +1,7 @@
1
1
  import { WeakStackFrame } from '@bablr/weak-stack';
2
- import { Resolver } from '@bablr/agast-helpers/tree';
3
- import { findRight } from './utils/array.js';
2
+ import { skipToDepth, buildSkips } from './utils/skip.js';
4
3
  import { facades, actuals } from './facades.js';
5
4
 
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
- export const NodeFacade = class AgastNodeFacade {
14
- constructor(path) {
15
- facades.set(path, this);
16
- }
17
-
18
- get language() {
19
- return actuals.get(this).language;
20
- }
21
-
22
- get type() {
23
- return actuals.get(this).type;
24
- }
25
-
26
- get path() {
27
- return actuals.get(this).path;
28
- }
29
-
30
- get parent() {
31
- return facades.get(actuals.get(this).parent);
32
- }
33
-
34
- get range() {
35
- return actuals.get(this).range;
36
- }
37
-
38
- get openTag() {
39
- return actuals.get(this).openTag;
40
- }
41
-
42
- get closeTag() {
43
- return actuals.get(this).closeTag;
44
- }
45
-
46
- get depth() {
47
- return actuals.get(this).depth;
48
- }
49
-
50
- get flags() {
51
- return actuals.get(this).flags;
52
- }
53
-
54
- get attributes() {
55
- return actuals.get(this).attributes;
56
- }
57
- };
58
-
59
5
  export const PathFacade = class AgastPathFacade {
60
6
  constructor(path) {
61
7
  facades.set(path, this);
@@ -74,17 +20,7 @@ export const PathFacade = class AgastPathFacade {
74
20
  }
75
21
 
76
22
  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;
23
+ return facades.get(actuals.get(this).at(depth));
88
24
  }
89
25
 
90
26
  *parents(includeSelf = false) {
@@ -96,67 +32,6 @@ export const PathFacade = class AgastPathFacade {
96
32
  }
97
33
  };
98
34
 
99
- export const Node = class AgastNode extends WeakStackFrame {
100
- static from(openTag) {
101
- return AgastNode.create(openTag);
102
- }
103
-
104
- constructor(
105
- openTag,
106
- closeTag = null,
107
- properties = new Map(),
108
- resolver = new Resolver(),
109
- unboundAttributes = null,
110
- ) {
111
- super();
112
-
113
- this.openTag = openTag;
114
- this.closeTag = closeTag;
115
- this.properties = properties;
116
- this.resolver = resolver;
117
- this.unboundAttributes = unboundAttributes;
118
- }
119
-
120
- get language() {
121
- return this.openTag.value?.language;
122
- }
123
-
124
- get type() {
125
- return this.openTag.value?.type || Symbol.for('@bablr/fragment');
126
- }
127
-
128
- get flags() {
129
- return this.openTag.value?.flags || {};
130
- }
131
-
132
- get attributes() {
133
- return this.openTag.value?.attributes || {};
134
- }
135
-
136
- branch() {
137
- const { openTag, closeTag, properties, resolver, unboundAttributes } = this;
138
-
139
- return this.replace(
140
- openTag,
141
- closeTag,
142
- new Map(properties), // there is probably a better way
143
- resolver.branch(),
144
- new Set(unboundAttributes),
145
- );
146
- }
147
-
148
- accept(node) {
149
- this.openTag = node.openTag;
150
- this.closeTag = node.closeTag;
151
- this.properties = node.properties;
152
- this.unboundAttributes = node.unboundAttributes;
153
-
154
- this.resolver.accept(node.resolver);
155
-
156
- return this;
157
- }
158
- };
159
-
160
35
  export const Path = class AgastPath extends WeakStackFrame {
161
36
  static from(context, tag) {
162
37
  return Path.create(context, tag);
@@ -165,43 +40,28 @@ export const Path = class AgastPath extends WeakStackFrame {
165
40
  constructor(context, reference) {
166
41
  super();
167
42
 
168
- if (reference && reference.type !== 'Reference') {
43
+ if (reference && reference.type !== 'Reference' && reference.type !== 'DoctypeTag') {
169
44
  throw new Error('Invalid reference for path');
170
45
  }
171
46
 
172
47
  this.context = context;
173
48
  this.reference = reference;
174
49
 
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
- }
50
+ buildSkips(this);
189
51
 
190
52
  new PathFacade(this);
191
53
  }
192
54
 
193
- at(depth) {
194
- let parent = this;
55
+ get name() {
56
+ return this.reference?.value.name || '[anonymous]';
57
+ }
195
58
 
196
- if (depth > this.depth) throw new Error();
59
+ get isArray() {
60
+ return this.reference?.value.isArray || false;
61
+ }
197
62
 
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;
63
+ at(depth) {
64
+ return skipToDepth(depth, this);
205
65
  }
206
66
 
207
67
  *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) {
@@ -27,6 +28,10 @@ export const StateFacade = class AgastStateFacade {
27
28
  return actuals.get(this).node;
28
29
  }
29
30
 
31
+ get parentNode() {
32
+ return actuals.get(this).parentNode;
33
+ }
34
+
30
35
  get holding() {
31
36
  return actuals.get(this).holding;
32
37
  }
@@ -45,10 +50,9 @@ export const State = class AgastState extends WeakStackFrame {
45
50
  context,
46
51
  path = null,
47
52
  node = null,
48
- result = null,
53
+ result = buildBeginningOfStreamToken(),
49
54
  emitted = null,
50
55
  held = null,
51
- expressionDepth = 0,
52
56
  ) {
53
57
  super();
54
58
 
@@ -60,7 +64,6 @@ export const State = class AgastState extends WeakStackFrame {
60
64
  this.result = result;
61
65
  this.emitted = emitted;
62
66
  this.held = held;
63
- this.expressionDepth = expressionDepth;
64
67
 
65
68
  new StateFacade(this);
66
69
  }
@@ -73,41 +76,8 @@ export const State = class AgastState extends WeakStackFrame {
73
76
  return !!this.held;
74
77
  }
75
78
 
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
79
  *emit(terminal, suppressEmit) {
110
- const { prevTerminals, nextTerminals, tagNodes } = this.context;
80
+ const { prevTerminals, nextTerminals } = this.context;
111
81
 
112
82
  if (terminal) {
113
83
  if (prevTerminals.has(terminal)) {
@@ -118,13 +88,11 @@ export const State = class AgastState extends WeakStackFrame {
118
88
  this.result?.type === 'Reference' &&
119
89
  !['OpenNodeTag', 'Gap', 'Null'].includes(terminal.type)
120
90
  ) {
121
- throw new Error('Bad reference emit');
91
+ throw new Error(`${terminal.type} is not a valid reference target`);
122
92
  }
123
93
 
124
94
  prevTerminals.set(terminal, this.result);
125
- if (this.result) {
126
- nextTerminals.set(this.result, terminal);
127
- }
95
+ nextTerminals.set(this.result, terminal);
128
96
 
129
97
  this.result = terminal;
130
98
 
@@ -135,12 +103,16 @@ export const State = class AgastState extends WeakStackFrame {
135
103
  }
136
104
  }
137
105
 
138
- if (!this.depth && !this.expressionDepth && !suppressEmit) {
106
+ if (!this.depth && !suppressEmit) {
139
107
  let emittable = nextTerminals.get(this.emitted);
140
108
 
141
109
  while (
142
110
  emittable &&
143
- !(emittable.type === 'OpenNodeTag' && tagNodes.get(emittable).unboundAttributes?.size)
111
+ !(
112
+ emittable.type === 'OpenNodeTag' &&
113
+ emittable.value.type &&
114
+ this.context.nodeForTag(emittable).unboundAttributes?.size
115
+ )
144
116
  ) {
145
117
  yield emittable;
146
118
  this.emitted = emittable;
@@ -163,27 +135,42 @@ export const State = class AgastState extends WeakStackFrame {
163
135
  return !!this.parent;
164
136
  }
165
137
 
138
+ get parentNode() {
139
+ return this.ctx.nodeForPath(this.path.parent);
140
+ }
141
+
166
142
  branch() {
167
- const { context, path, node, result, emitted, held, expressionDepth } = this;
143
+ const { context, path, node, result, emitted, held } = this;
144
+
145
+ const newNode = node && node.branch();
146
+
147
+ if (newNode?.openTag) context.tagNodes.set(newNode.openTag, newNode);
148
+ if (newNode?.closeTag) context.tagNodes.set(newNode.closeTag, newNode);
168
149
 
169
- return this.push(context, path, node.branch(), result, emitted, held, expressionDepth);
150
+ return this.push(context, path, newNode, result, emitted, held);
170
151
  }
171
152
 
172
153
  accept() {
173
- const { parent } = this;
154
+ const { parent, context } = this;
174
155
 
175
156
  if (!parent) {
176
- throw new Error('accepted the root state');
157
+ return null;
158
+ }
159
+
160
+ if (this.node && parent.node) {
161
+ parent.node.accept(this.node, context);
162
+ } else {
163
+ parent.node = this.node;
177
164
  }
178
165
 
179
- parent.node.accept(this.node);
166
+ if (this.node?.openTag) context.tagNodes.set(parent.node.openTag, parent.node);
167
+ if (this.node?.closeTag) context.tagNodes.set(parent.node.closeTag, parent.node);
180
168
 
181
169
  // emitted isn't used here and probably doesn't need to be part of state
182
170
 
183
171
  parent.result = this.result;
184
172
  parent.held = this.held;
185
173
  parent.path = this.path;
186
- parent.expressionDepth = this.expressionDepth;
187
174
 
188
175
  return parent;
189
176
  }
@@ -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.3.0",
5
5
  "author": "Conrad Buck<conartist6@gmail.com>",
6
6
  "type": "module",
7
7
  "files": [
@@ -11,24 +11,19 @@
11
11
  ".": "./lib/index.js"
12
12
  },
13
13
  "sideEffects": false,
14
- "scripts": {
15
- "test": "node ./test/runner.js"
16
- },
17
14
  "dependencies": {
18
- "@bablr/agast-helpers": "0.1.5",
19
- "@bablr/agast-vm-helpers": "0.1.3",
15
+ "@bablr/agast-helpers": "0.2.0",
16
+ "@bablr/agast-vm-helpers": "0.2.0",
20
17
  "@bablr/coroutine": "0.1.0",
21
18
  "@bablr/weak-stack": "0.1.0"
22
19
  },
23
20
  "devDependencies": {
24
21
  "@bablr/agast-vm-strategy-passthrough": "github:bablr-lang/agast-vm-strategy-passthrough#2bd3a0c7311037af92c5b81941c79161499f6c9e",
25
22
  "@bablr/eslint-config-base": "github:bablr-lang/eslint-config-base#49f5952efed27f94ee9b94340eb1563c440bf64e",
26
- "@bablr/strategy_enhancer-debug-log": "0.1.1",
27
23
  "enhanced-resolve": "^5.12.0",
28
24
  "eslint": "^8.32.0",
29
25
  "eslint-import-resolver-enhanced-resolve": "^1.0.5",
30
26
  "eslint-plugin-import": "^2.27.5",
31
- "expect": "29.7.0",
32
27
  "iter-tools-es": "^7.3.1",
33
28
  "prettier": "^2.6.2"
34
29
  },