@bablr/agast-vm 0.3.0 → 0.4.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/lib/context.js CHANGED
@@ -1,23 +1,15 @@
1
1
  import { reifyExpressionShallow, reifyExpression } from '@bablr/agast-vm-helpers';
2
- import { getCooked, sourceTextFor } from '@bablr/agast-helpers/stream';
2
+ import {
3
+ getCooked as getCookedFromTree,
4
+ sourceTextFor as sourceTextForTree,
5
+ } from '@bablr/agast-helpers/tree';
6
+ import {
7
+ getCooked as getCookedFromStream,
8
+ sourceTextFor as sourceTextForStream,
9
+ } from '@bablr/agast-helpers/stream';
3
10
  import { facades, actuals } from './facades.js';
4
11
 
5
- function* ownTerminalsFor(range, nextTerminals, tagNodes) {
6
- if (!range) return;
7
-
8
- const { 0: start, 1: end } = range;
9
-
10
- const pastEnd = nextTerminals.get(end);
11
-
12
- for (let term = start; term && term !== pastEnd; term = nextTerminals.get(term)) {
13
- if (!(term === range[0] || term === range[1]) && term.type === 'OpenNodeTag') {
14
- term = tagNodes.get(term).closeTag;
15
- continue;
16
- }
17
-
18
- yield term;
19
- }
20
- }
12
+ const { isArray } = Array;
21
13
 
22
14
  function* allTerminalsFor(range, nextTerminals) {
23
15
  if (!range) return;
@@ -39,10 +31,6 @@ export const ContextFacade = class AgastContextFacade {
39
31
  return actuals.get(this).nextTerminals.get(token);
40
32
  }
41
33
 
42
- ownTerminalsFor(range) {
43
- return actuals.get(this).ownTerminalsFor(range);
44
- }
45
-
46
34
  allTerminalsFor(range) {
47
35
  return actuals.get(this).allTerminalsFor(range);
48
36
  }
@@ -55,10 +43,6 @@ export const ContextFacade = class AgastContextFacade {
55
43
  return actuals.get(this).reifyExpression(range);
56
44
  }
57
45
 
58
- getProperty(node, name) {
59
- return actuals.get(this).getProperty(node, name);
60
- }
61
-
62
46
  sourceTextFor(range) {
63
47
  return actuals.get(this).sourceTextFor(range);
64
48
  }
@@ -67,18 +51,6 @@ export const ContextFacade = class AgastContextFacade {
67
51
  return actuals.get(this).buildRange(terminals);
68
52
  }
69
53
 
70
- nodeForTag(tag) {
71
- return actuals.get(this).nodeForTag(tag);
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
-
82
54
  unbox(value) {
83
55
  return actuals.get(this).unbox(value);
84
56
  }
@@ -92,25 +64,12 @@ export const Context = class AgastContext {
92
64
  constructor() {
93
65
  this.prevTerminals = new WeakMap();
94
66
  this.nextTerminals = new WeakMap();
95
- this.tagNodes = new WeakMap();
96
- this.tagPaths = new WeakMap();
97
- this.pathNodes = new WeakMap();
98
67
  this.unboxedValues = new WeakMap();
99
68
  this.facade = new ContextFacade();
100
69
 
101
70
  facades.set(this, this.facade);
102
71
  }
103
72
 
104
- getProperty(result, 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);
112
- }
113
-
114
73
  isEmpty(range) {
115
74
  const { path, parent } = this;
116
75
 
@@ -129,15 +88,7 @@ export const Context = class AgastContext {
129
88
  }
130
89
 
131
90
  allTerminalsReverseFor(range) {
132
- return allTerminalsFor(range, this.prevTerminals);
133
- }
134
-
135
- ownTerminalsFor(range) {
136
- return ownTerminalsFor(range, this.nextTerminals, this.tagNodes);
137
- }
138
-
139
- ownTerminalsReverseFor(range) {
140
- return ownTerminalsFor(range, this.prevTerminals, this.tagNodes);
91
+ return allTerminalsFor([...range].reverse(), this.prevTerminals);
141
92
  }
142
93
 
143
94
  getPreviousTerminal(token) {
@@ -148,20 +99,10 @@ export const Context = class AgastContext {
148
99
  return this.nextTerminals.get(token);
149
100
  }
150
101
 
151
- nodeForTag(tag) {
152
- return this.tagNodes.get(tag);
153
- }
154
-
155
- pathForTag(ref) {
156
- return this.tagPaths.get(ref);
157
- }
158
-
159
- nodeForPath(path) {
160
- return this.pathNodes.get(path);
161
- }
162
-
163
- sourceTextFor(range) {
164
- return sourceTextFor(this.allTerminalsFor(range));
102
+ sourceTextFor(nodeOrRange) {
103
+ return isArray(nodeOrRange)
104
+ ? sourceTextForStream(this.allTerminalsFor(nodeOrRange))
105
+ : sourceTextForTree(nodeOrRange);
165
106
  }
166
107
 
167
108
  buildRange(terminals) {
@@ -197,7 +138,9 @@ export const Context = class AgastContext {
197
138
  return reifyExpression(value);
198
139
  }
199
140
 
200
- getCooked(range) {
201
- return getCooked(this.ownTerminalsFor(range));
141
+ getCooked(nodeOrRange) {
142
+ return isArray(nodeOrRange)
143
+ ? getCookedFromStream(this.allTerminalsFor(nodeOrRange))
144
+ : getCookedFromTree(nodeOrRange);
202
145
  }
203
146
  };
package/lib/evaluate.js CHANGED
@@ -9,12 +9,11 @@ import {
9
9
  buildDoctypeTag,
10
10
  buildNodeOpenTag,
11
11
  buildNodeCloseTag,
12
- } from '@bablr/agast-helpers/builders';
12
+ } from '@bablr/agast-vm-helpers/internal-builders';
13
+ import { deembedExpression } from '@bablr/agast-vm-helpers/deembed';
13
14
  import { StreamIterable, getStreamIterator } from '@bablr/agast-helpers/stream';
14
15
  import { printExpression } from '@bablr/agast-helpers/print';
15
- import { reifyExpression } from '@bablr/agast-vm-helpers';
16
- import { Path } from './path.js';
17
- import { Node } from './node.js';
16
+ import { getRange, getOpenTag } from '@bablr/agast-helpers/tree';
18
17
  import { State } from './state.js';
19
18
  import { facades } from './facades.js';
20
19
 
@@ -22,7 +21,7 @@ export const evaluate = (ctx, strategy, options) =>
22
21
  new StreamIterable(__evaluate(ctx, strategy, options));
23
22
 
24
23
  const __evaluate = function* agast(ctx, strategy, options = {}) {
25
- let s = State.from(ctx);
24
+ let s = State.from(ctx, options.expressions);
26
25
 
27
26
  const co = new Coroutine(getStreamIterator(strategy(facades.get(ctx), facades.get(s))));
28
27
 
@@ -35,8 +34,8 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
35
34
 
36
35
  if (co.done) break;
37
36
 
38
- const sourceInstr = co.value;
39
- const instr = reifyExpression(sourceInstr);
37
+ const instr = co.value;
38
+
40
39
  let returnValue = undefined;
41
40
 
42
41
  const { verb, arguments: args = [] } = instr;
@@ -68,7 +67,9 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
68
67
  }
69
68
 
70
69
  case 'advance': {
71
- const { 0: terminal, 1: options } = args;
70
+ const { 0: embeddedTerminal, 1: options } = args;
71
+
72
+ const terminal = embeddedTerminal.value;
72
73
 
73
74
  if (
74
75
  s.held &&
@@ -84,24 +85,16 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
84
85
  switch (terminal?.type || 'Null') {
85
86
  case 'DoctypeTag': {
86
87
  const { attributes } = terminal.value;
87
- const doctypeTag = buildDoctypeTag(attributes);
88
- const rootPath = Path.from(ctx, doctypeTag);
89
88
 
90
89
  if (s.path) {
91
90
  throw new Error();
92
91
  }
93
92
 
94
- s.path = rootPath;
95
-
96
- yield* s.emit(doctypeTag);
97
-
98
- returnValue = doctypeTag;
93
+ returnValue = s.advance(buildDoctypeTag(attributes));
99
94
  break;
100
95
  }
101
96
 
102
97
  case 'Literal': {
103
- const literal = buildLiteral(terminal.value);
104
-
105
98
  if (!s.node.flags.token) {
106
99
  throw new Error('literals must occur inside tokens');
107
100
  }
@@ -110,37 +103,22 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
110
103
  throw new Error('Cannot consume input while hold register is full');
111
104
  }
112
105
 
113
- yield* s.emit(literal);
114
-
115
- returnValue = literal;
106
+ returnValue = s.advance(buildLiteral(terminal.value));
116
107
  break;
117
108
  }
118
109
 
119
110
  case 'Reference': {
120
111
  const { name, isArray } = terminal.value;
121
112
 
122
- const tag = buildReference(name, isArray);
123
-
124
113
  if (s.result.type === 'Reference') {
125
114
  throw new Error('A reference must have a non-reference value');
126
115
  }
127
116
 
128
117
  if (s.node?.flags.token) {
129
- throw new Error();
130
- }
131
-
132
- if (s.path.depth) {
133
- s.node.resolver.consume(tag);
118
+ throw new Error('A token node cannot contain a reference');
134
119
  }
135
120
 
136
- s.path = s.path.push(ctx, tag);
137
- s.node = null;
138
-
139
- ctx.tagPaths.set(tag, s.path);
140
-
141
- yield* s.emit(tag);
142
-
143
- returnValue = tag;
121
+ returnValue = s.advance(buildReference(name, isArray));
144
122
  break;
145
123
  }
146
124
 
@@ -149,18 +127,7 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
149
127
 
150
128
  if (reference?.type !== 'Reference') throw new Error();
151
129
 
152
- const gapTag = buildGap();
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
-
161
- yield* s.emit(gapTag);
162
-
163
- returnValue = gapTag;
130
+ returnValue = s.advance(buildGap());
164
131
  break;
165
132
  }
166
133
 
@@ -169,150 +136,39 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
169
136
 
170
137
  if (reference?.type !== 'Reference') throw new Error();
171
138
 
172
- const null_ = buildNull();
173
-
174
- ctx.tagPaths.set(null_, s.path);
175
-
176
- s.node = s.parentNode;
177
- s.path = s.path.parent;
178
-
179
- yield* s.emit(null_);
180
-
181
- returnValue = null_;
139
+ returnValue = s.advance(buildNull());
182
140
  break;
183
141
  }
184
142
 
185
143
  case 'Shift': {
186
- const tag = buildShift();
187
-
188
- const finishedNode = ctx.nodeForTag(s.result);
189
- const ref = ctx.getPreviousTerminal(finishedNode.openTag);
190
- const finishedPath = ctx.pathForTag(ref);
191
-
192
- ctx.pathNodes.set(finishedPath, null);
144
+ const finishedNode = s.nodeForTag(s.result);
193
145
 
194
- s.held = { node: finishedNode, path: finishedPath };
195
-
196
- if (!finishedNode.openTag.value.flags.expression) {
146
+ if (!getOpenTag(finishedNode).value.flags.expression) {
197
147
  throw new Error();
198
148
  }
199
149
 
200
- s.path = finishedPath;
201
-
202
- yield* s.emit(tag);
203
-
204
- returnValue = tag;
150
+ returnValue = s.advance(buildShift());
205
151
  break;
206
152
  }
207
153
 
208
154
  case 'OpenNodeTag': {
209
- const { flags, language, type, intrinsicValue, attributes } = terminal.value;
210
- const { unboundAttributes } = options || {};
211
- const reference = s.result;
212
- const openTag = buildNodeOpenTag(flags, language, type, intrinsicValue, attributes);
213
-
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
- }
229
- }
230
-
231
- const newNode = new Node(openTag);
232
-
233
- newNode.unboundAttributes = new Set(unboundAttributes);
234
-
235
- s.node = newNode;
236
- if (flags.trivia || flags.escape) {
237
- s.path = s.path.push(ctx, null);
238
- }
239
-
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);
244
-
245
- if (intrinsicValue) {
246
- newNode.closeTag = newNode.openTag;
247
- s.node = s.parentNode;
248
- s.path = s.path.parent;
249
-
250
- const { properties } = s.node;
251
- const { name: refName, isArray } = reference.value;
252
-
253
- if (!isArray) {
254
- properties.set(refName, [openTag, openTag]);
255
- } else {
256
- if (!properties.has(refName)) {
257
- properties.set(refName, []);
258
- }
259
- properties.get(refName).push([openTag, openTag]);
260
- }
261
- }
262
- }
155
+ const { flags, language, type, attributes } = terminal.value;
263
156
 
264
- yield* s.emit(openTag);
157
+ if (language && !language.startsWith('https://')) {
158
+ throw new Error('Expected an absolute-language tag');
159
+ }
265
160
 
266
- returnValue = openTag;
161
+ returnValue = s.advance(
162
+ buildNodeOpenTag(flags, language, type, attributes),
163
+ deembedExpression(options),
164
+ );
267
165
  break;
268
166
  }
269
167
 
270
168
  case 'CloseNodeTag': {
271
169
  const { type, language } = terminal.value;
272
- const { openTag } = s.node;
273
- const { flags, type: openType } = openTag.value;
274
-
275
- const closeTag = buildNodeCloseTag(type, language);
276
-
277
- if (openType) {
278
- if (s.node.unboundAttributes?.size)
279
- throw new Error('Grammar failed to bind all attributes');
280
-
281
- if (!type) throw new Error(`CloseNodeTag must have type`);
282
-
283
- if (type !== openType)
284
- throw new Error(
285
- `Grammar close {type: ${type}} did not match open {type: ${openType}}`,
286
- );
287
-
288
- if (!flags.escape && !flags.trivia) {
289
- const { name: refName, isArray } = s.path.reference.value;
290
-
291
- const { properties } = ctx.nodeForPath(s.path.parent);
292
170
 
293
- if (!isArray) {
294
- properties.set(refName, [openTag, closeTag]);
295
- } else {
296
- if (!properties.has(refName)) {
297
- properties.set(refName, []);
298
- }
299
- properties.get(refName).push([openTag, closeTag]);
300
- }
301
- }
302
-
303
- ctx.tagNodes.set(closeTag, s.node);
304
- ctx.tagPaths.set(closeTag, s.path);
305
-
306
- s.node.closeTag = closeTag;
307
-
308
- s.node = s.parentNode;
309
-
310
- s.path = s.path.parent;
311
- }
312
-
313
- yield* s.emit(closeTag, flags.expression);
314
-
315
- returnValue = closeTag;
171
+ returnValue = s.advance(buildNodeCloseTag(type, language));
316
172
  break;
317
173
  }
318
174
 
@@ -320,19 +176,21 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
320
176
  throw new Error();
321
177
  }
322
178
 
179
+ yield* s.emit();
180
+
323
181
  break;
324
182
  }
325
183
 
326
184
  case 'bindAttribute': {
327
185
  const { 0: key, 1: value } = args;
328
186
 
329
- const { unboundAttributes } = s.node;
187
+ const { unboundAttributes } = s;
330
188
 
331
189
  if (!unboundAttributes || !unboundAttributes.has(key)) {
332
190
  throw new Error('No unbound attribute to bind');
333
191
  }
334
192
 
335
- if (!s.node.openTag.value.type) {
193
+ if (!s.node.type) {
336
194
  throw new Error('Cannot bind attribute to fragment');
337
195
  }
338
196
 
@@ -349,12 +207,12 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
349
207
 
350
208
  unboundAttributes.delete(key);
351
209
 
352
- const { openTag } = s.node;
210
+ const openTag = s.node.children[0];
353
211
 
354
212
  if (value != null) {
355
- const { flags, language, type, intrinsicValue } = openTag.value;
213
+ const { flags, language, type } = openTag.value;
356
214
  const attributes = { ...openTag.value.attributes, [key]: value };
357
- const newOpenTag = buildNodeOpenTag(flags, language, type, intrinsicValue, attributes);
215
+ const newOpenTag = buildNodeOpenTag(flags, language, type, attributes);
358
216
 
359
217
  let openNext = ctx.nextTerminals.get(openTag);
360
218
  let startPrev = ctx.prevTerminals.get(openTag);
@@ -362,7 +220,13 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
362
220
  ctx.prevTerminals.set(newOpenTag, startPrev);
363
221
  ctx.nextTerminals.set(startPrev, newOpenTag);
364
222
 
365
- ctx.tagNodes.set(newOpenTag, ctx.tagNodes.get(openTag));
223
+ if (s.node !== s.tagNodes.get(openTag)) throw new Error();
224
+ if (s.path !== s.tagPaths.get(openTag)) throw new Error();
225
+
226
+ s.node.attributes = attributes;
227
+
228
+ s.tagNodes.set(newOpenTag, s.node);
229
+ s.tagPaths.set(newOpenTag, s.path);
366
230
 
367
231
  if (openNext) {
368
232
  ctx.nextTerminals.set(newOpenTag, openNext);
@@ -372,14 +236,14 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
372
236
  s.result = newOpenTag;
373
237
  }
374
238
 
375
- s.node.openTag = newOpenTag;
239
+ s.node.children[0] = newOpenTag;
376
240
  }
377
241
 
378
242
  if (!unboundAttributes.size) {
379
243
  yield* s.emit();
380
244
  }
381
245
 
382
- returnValue = s.node.openTag;
246
+ returnValue = getRange(s.node);
383
247
  break;
384
248
  }
385
249
 
@@ -408,8 +272,6 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
408
272
  co.advance(returnValue);
409
273
  }
410
274
 
411
- s.path = s.path.parent;
412
-
413
275
  if (s.depth > 0) {
414
276
  throw new Error('Did not unwind state stack');
415
277
  }
@@ -417,4 +279,6 @@ const __evaluate = function* agast(ctx, strategy, options = {}) {
417
279
  if (s.path?.depth > 0) {
418
280
  throw new Error('Did not unwind path stack');
419
281
  }
282
+
283
+ return s.nodeForTag(s.result);
420
284
  };
package/lib/node.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Resolver } from '@bablr/agast-helpers/tree';
1
+ import { getRange, getOpenTag, getCloseTag } from '@bablr/agast-helpers/tree';
2
2
  import { facades, actuals } from './facades.js';
3
3
 
4
4
  export const NodeFacade = class AgastNodeFacade {
@@ -15,15 +15,15 @@ export const NodeFacade = class AgastNodeFacade {
15
15
  }
16
16
 
17
17
  get range() {
18
- return actuals.get(this).range;
18
+ return getRange(actuals.get(this));
19
19
  }
20
20
 
21
21
  get openTag() {
22
- return actuals.get(this).openTag;
22
+ return getOpenTag(actuals.get(this));
23
23
  }
24
24
 
25
25
  get closeTag() {
26
- return actuals.get(this).closeTag;
26
+ return getCloseTag(actuals.get(this));
27
27
  }
28
28
 
29
29
  get flags() {
@@ -34,66 +34,3 @@ export const NodeFacade = class AgastNodeFacade {
34
34
  return actuals.get(this).attributes;
35
35
  }
36
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/state.js CHANGED
@@ -1,7 +1,58 @@
1
+ import emptyStack from '@iter-tools/imm-stack';
1
2
  import { WeakStackFrame } from '@bablr/weak-stack';
2
- import { startsDocument } from '@bablr/agast-helpers/stream';
3
+ import {
4
+ Resolver,
5
+ add,
6
+ createNode,
7
+ getOpenTag,
8
+ getCloseTag,
9
+ branchNode,
10
+ acceptNode,
11
+ finalizeNode,
12
+ getRoot,
13
+ } from '@bablr/agast-helpers/tree';
14
+ import {
15
+ buildBeginningOfStreamToken,
16
+ buildEmbeddedNode,
17
+ nodeFlags,
18
+ } from '@bablr/agast-vm-helpers/internal-builders';
19
+ import * as sym from '@bablr/agast-helpers/symbols';
3
20
  import { facades, actuals } from './facades.js';
4
- import { buildBeginningOfStreamToken } from '@bablr/agast-helpers/builders';
21
+ import { Path } from './path.js';
22
+
23
+ const { hasOwn } = Object;
24
+
25
+ const arrayLast = (arr) => arr[arr.length - 1];
26
+
27
+ const createNodeWithState = (startTag, options = {}) => {
28
+ const { unboundAttributes } = options;
29
+ const node = createNode(startTag);
30
+ nodeStates.set(node, {
31
+ resolver: new Resolver(node),
32
+ unboundAttributes: new Set(unboundAttributes || []),
33
+ });
34
+ return node;
35
+ };
36
+
37
+ const symbolTypeFor = (type) => {
38
+ // prettier-ignore
39
+ switch (type) {
40
+ case 'Null': return sym.null;
41
+ case 'Gap': return sym.gap;
42
+ default: throw new Error();
43
+ }
44
+ };
45
+
46
+ const buildStubNode = (tag) => {
47
+ return {
48
+ flags: nodeFlags,
49
+ language: null,
50
+ type: symbolTypeFor(tag.type),
51
+ children: [tag],
52
+ properties: {},
53
+ attributes: {},
54
+ };
55
+ };
5
56
 
6
57
  export const StateFacade = class AgastStateFacade {
7
58
  constructor(state) {
@@ -43,41 +94,96 @@ export const StateFacade = class AgastStateFacade {
43
94
  get ctx() {
44
95
  return this.context;
45
96
  }
97
+
98
+ nodeForPath(path) {
99
+ return actuals.get(this).nodeForPath(path);
100
+ }
101
+
102
+ pathForTag(tag) {
103
+ return actuals.get(this).pathForTag(tag);
104
+ }
105
+
106
+ nodeForTag(tag) {
107
+ return actuals.get(this).nodeForTag(tag);
108
+ }
46
109
  };
47
110
 
111
+ export const nodeStates = new WeakMap();
112
+
48
113
  export const State = class AgastState extends WeakStackFrame {
49
114
  constructor(
50
115
  context,
116
+ expressions = emptyStack,
51
117
  path = null,
52
118
  node = null,
53
119
  result = buildBeginningOfStreamToken(),
54
120
  emitted = null,
55
121
  held = null,
122
+ internalContext = {
123
+ pathNodes: new WeakMap(),
124
+ tagPaths: new WeakMap(),
125
+ tagNodes: new WeakMap(),
126
+ },
56
127
  ) {
57
128
  super();
58
129
 
59
130
  if (!context) throw new Error('invalid args to tagState');
60
131
 
61
132
  this.context = context;
133
+ this.expressions = expressions;
62
134
  this.path = path;
63
135
  this.node = node;
64
136
  this.result = result;
65
137
  this.emitted = emitted;
66
138
  this.held = held;
139
+ this.internalContext = internalContext;
67
140
 
68
141
  new StateFacade(this);
69
142
  }
70
143
 
71
- static from(context) {
72
- return State.create(context, null);
144
+ static from(context, expressions = []) {
145
+ return State.create(context, emptyStack.push(...[...expressions].reverse()));
146
+ }
147
+
148
+ get pathNodes() {
149
+ return this.internalContext.pathNodes;
150
+ }
151
+
152
+ get tagPaths() {
153
+ return this.internalContext.tagPaths;
154
+ }
155
+
156
+ get tagNodes() {
157
+ return this.internalContext.tagNodes;
158
+ }
159
+
160
+ get unboundAttributes() {
161
+ return nodeStates.get(this.node).unboundAttributes;
162
+ }
163
+
164
+ get resolver() {
165
+ return nodeStates.get(this.node).resolver;
73
166
  }
74
167
 
75
168
  get holding() {
76
169
  return !!this.held;
77
170
  }
78
171
 
79
- *emit(terminal, suppressEmit) {
80
- const { prevTerminals, nextTerminals } = this.context;
172
+ nodeForPath(path) {
173
+ return this.pathNodes.get(path);
174
+ }
175
+
176
+ pathForTag(ref) {
177
+ return this.tagPaths.get(ref);
178
+ }
179
+
180
+ nodeForTag(tag) {
181
+ return this.tagNodes.get(tag);
182
+ }
183
+
184
+ advance(terminal, options = {}) {
185
+ const ctx = this.context;
186
+ const { prevTerminals, nextTerminals } = ctx;
81
187
 
82
188
  if (terminal) {
83
189
  if (prevTerminals.has(terminal)) {
@@ -94,24 +200,203 @@ export const State = class AgastState extends WeakStackFrame {
94
200
  prevTerminals.set(terminal, this.result);
95
201
  nextTerminals.set(this.result, terminal);
96
202
 
97
- this.result = terminal;
203
+ switch (terminal.type) {
204
+ case 'DoctypeTag': {
205
+ this.path = Path.from(ctx, terminal);
206
+
207
+ this.tagPaths.set(terminal, this.path);
208
+ break;
209
+ }
210
+
211
+ case 'OpenNodeTag': {
212
+ const openTag = terminal;
213
+ const { type, flags } = terminal.value;
214
+ this.node = createNodeWithState(terminal, options);
215
+
216
+ const reference = this.result;
217
+
218
+ this.node.children.push(terminal);
219
+
220
+ if (!type) {
221
+ this.node.attributes = this.result.value.attributes;
222
+ } else {
223
+ if (!flags.trivia && !flags.escape) {
224
+ if (
225
+ reference.type !== 'Reference' &&
226
+ reference.type !== 'Shift' &&
227
+ reference.type !== 'OpenNodeTag' &&
228
+ !reference.value.type
229
+ ) {
230
+ throw new Error('Invalid location for OpenNodeTag');
231
+ }
232
+ } else {
233
+ this.path = this.path.push(ctx, null);
234
+ }
235
+ }
236
+
237
+ this.pathNodes.set(this.node, this.path);
238
+ this.pathNodes.set(this.path, this.node);
239
+
240
+ this.tagNodes.set(openTag, this.node);
241
+ this.tagPaths.set(openTag, this.path);
242
+ break;
243
+ }
244
+
245
+ case 'CloseNodeTag': {
246
+ const openTag = this.node.children[0];
247
+ const { flags, type: openType } = openTag.value;
248
+ const closeTag = terminal;
249
+ const { type } = closeTag.value;
250
+
251
+ this.node.children.push(terminal);
252
+
253
+ if (openType) {
254
+ if (this.node.unboundAttributes?.size)
255
+ throw new Error('Grammar failed to bind all attributes');
256
+
257
+ if (!type) throw new Error(`CloseNodeTag must have type`);
98
258
 
99
- if (!this.emitted) {
100
- if (!startsDocument(terminal)) throw new Error();
101
- this.emitted = terminal;
102
- yield terminal;
259
+ if (type !== openType)
260
+ throw new Error(
261
+ `Grammar close {type: ${type}} did not match open {type: ${openType}}`,
262
+ );
263
+
264
+ if (!flags.escape && !flags.trivia) {
265
+ const { name: refName, isArray } = this.path.reference.value;
266
+
267
+ const { properties } = this.parentNode;
268
+
269
+ if (!isArray) {
270
+ properties[refName] = this.node;
271
+ } else {
272
+ if (!hasOwn(properties, refName)) {
273
+ properties[refName] = [];
274
+ }
275
+ properties[refName].push(this.node);
276
+ }
277
+ } else {
278
+ this.parentNode.children.push(buildEmbeddedNode(this.node));
279
+ }
280
+ }
281
+
282
+ this.tagNodes.set(closeTag, this.node);
283
+ this.tagPaths.set(closeTag, this.path);
284
+
285
+ finalizeNode(this.node);
286
+
287
+ this.node = this.parentNode;
288
+ this.path = this.path.parent;
289
+ break;
290
+ }
291
+
292
+ case 'Reference': {
293
+ if (this.path.depth) {
294
+ nodeStates.get(this.node).resolver.consume(terminal);
295
+ }
296
+
297
+ this.node.children.push(terminal);
298
+
299
+ this.path = this.path.push(ctx, terminal);
300
+
301
+ this.tagPaths.set(terminal, this.path);
302
+ break;
303
+ }
304
+
305
+ case 'Gap': {
306
+ this.tagPaths.set(terminal, this.path);
307
+
308
+ let target;
309
+ let ref = arrayLast(this.node.children);
310
+
311
+ if (ref.type !== 'Reference') throw new Error();
312
+
313
+ if (this.held) {
314
+ target = this.held.node;
315
+
316
+ this.held = null;
317
+ } else if (this.expressions.size) {
318
+ const expression = this.expressions.value;
319
+ target = getRoot(expression);
320
+ this.expressions = this.expressions.pop();
321
+ } else {
322
+ target = buildStubNode(terminal);
323
+ }
324
+
325
+ this.pathNodes.set(this.pathForTag(ref), target);
326
+ add(this.node, ref, target);
327
+
328
+ this.path = this.path.parent;
329
+ break;
330
+ }
331
+
332
+ case 'Null': {
333
+ this.tagPaths.set(terminal, this.path);
334
+
335
+ const { properties } = this.node;
336
+ const { isArray, name } = this.result.value;
337
+
338
+ const newNode = buildStubNode(terminal);
339
+
340
+ if (!hasOwn(properties, name)) {
341
+ // TODO is this behavior right
342
+ properties[name] = isArray ? [] : newNode;
343
+ }
344
+
345
+ this.pathNodes.set(this.path, newNode);
346
+
347
+ this.path = this.path.parent;
348
+ break;
349
+ }
350
+
351
+ case 'Shift': {
352
+ const finishedNode = this.nodeForTag(this.result);
353
+ const ref = ctx.getPreviousTerminal(getOpenTag(finishedNode));
354
+ const finishedPath = this.pathForTag(ref);
355
+ const { properties } = this.node;
356
+
357
+ this.pathNodes.set(finishedPath, null);
358
+
359
+ this.held = { node: finishedNode, path: finishedPath };
360
+
361
+ let node = properties[ref.value.name];
362
+
363
+ if (ref.value.isArray) {
364
+ node = arrayLast(node);
365
+ properties[ref.value.name].pop();
366
+ } else {
367
+ properties[ref.value.name] = null;
368
+ }
369
+
370
+ this.path = finishedPath;
371
+ break;
372
+ }
373
+
374
+ case 'Literal': {
375
+ this.node.children.push(terminal);
376
+ break;
377
+ }
378
+
379
+ default:
380
+ throw new Error();
103
381
  }
104
382
  }
105
383
 
106
- if (!this.depth && !suppressEmit) {
107
- let emittable = nextTerminals.get(this.emitted);
384
+ this.result = terminal;
385
+
386
+ return terminal;
387
+ }
388
+
389
+ *emit() {
390
+ const { nextTerminals } = this.context;
391
+ if (!this.depth) {
392
+ let emittable = this.emitted ? nextTerminals.get(this.emitted) : this.result;
108
393
 
109
394
  while (
110
395
  emittable &&
111
396
  !(
112
397
  emittable.type === 'OpenNodeTag' &&
113
398
  emittable.value.type &&
114
- this.context.nodeForTag(emittable).unboundAttributes?.size
399
+ nodeStates.get(this.nodeForTag(emittable)).unboundAttributes?.size
115
400
  )
116
401
  ) {
117
402
  yield emittable;
@@ -119,8 +404,6 @@ export const State = class AgastState extends WeakStackFrame {
119
404
  emittable = nextTerminals.get(this.emitted);
120
405
  }
121
406
  }
122
-
123
- return terminal;
124
407
  }
125
408
 
126
409
  get ctx() {
@@ -136,38 +419,47 @@ export const State = class AgastState extends WeakStackFrame {
136
419
  }
137
420
 
138
421
  get parentNode() {
139
- return this.ctx.nodeForPath(this.path.parent);
422
+ return this.pathNodes.get(this.path.parent);
140
423
  }
141
424
 
142
425
  branch() {
143
- const { context, path, node, result, emitted, held } = this;
426
+ const { context, expressions, path, node, result, emitted, held, internalContext } = this;
427
+ const { pathNodes } = internalContext;
428
+
429
+ const newNode = node && branchNode(node);
430
+
431
+ const nodeState = nodeStates.get(node);
144
432
 
145
- const newNode = node && node.branch();
433
+ pathNodes.set(path, newNode);
146
434
 
147
- if (newNode?.openTag) context.tagNodes.set(newNode.openTag, newNode);
148
- if (newNode?.closeTag) context.tagNodes.set(newNode.closeTag, newNode);
435
+ nodeStates.set(newNode, { ...nodeState, resolver: nodeState.resolver.branch() });
149
436
 
150
- return this.push(context, path, newNode, result, emitted, held);
437
+ const nodeOpen = getOpenTag(node);
438
+ const nodeClose = getCloseTag(node);
439
+ if (nodeOpen) this.tagNodes.set(nodeOpen, newNode);
440
+ if (nodeClose) this.tagNodes.set(nodeClose, newNode);
441
+
442
+ return this.push(context, expressions, path, newNode, result, emitted, held, internalContext);
151
443
  }
152
444
 
153
445
  accept() {
154
- const { parent, context } = this;
446
+ const { parent } = this;
155
447
 
156
448
  if (!parent) {
157
449
  return null;
158
450
  }
159
451
 
160
452
  if (this.node && parent.node) {
161
- parent.node.accept(this.node, context);
453
+ acceptNode(parent.node, this.node);
454
+ const nodeState = nodeStates.get(this.node);
455
+ Object.assign(nodeStates.get(parent.node), nodeState);
162
456
  } else {
163
457
  parent.node = this.node;
164
458
  }
165
459
 
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);
168
-
169
460
  // emitted isn't used here and probably doesn't need to be part of state
170
461
 
462
+ parent.expressions = this.expressions;
171
463
  parent.result = this.result;
172
464
  parent.held = this.held;
173
465
  parent.path = this.path;
@@ -176,12 +468,17 @@ export const State = class AgastState extends WeakStackFrame {
176
468
  }
177
469
 
178
470
  reject() {
179
- const { parent, context } = this;
471
+ const { parent, context, pathNodes, tagNodes } = this;
180
472
 
181
473
  if (!parent) throw new Error('rejected root state');
182
474
 
183
475
  context.nextTerminals.delete(parent.result);
184
476
 
477
+ pathNodes.set(parent.path, parent.node);
478
+
479
+ if (getOpenTag(parent.node)) tagNodes.set(getOpenTag(parent.node), parent.node);
480
+ if (getCloseTag(parent.node)) tagNodes.set(getCloseTag(parent.node), parent.node);
481
+
185
482
  return parent;
186
483
  }
187
484
  };
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.3.0",
4
+ "version": "0.4.0",
5
5
  "author": "Conrad Buck<conartist6@gmail.com>",
6
6
  "type": "module",
7
7
  "files": [
@@ -12,10 +12,11 @@
12
12
  },
13
13
  "sideEffects": false,
14
14
  "dependencies": {
15
- "@bablr/agast-helpers": "0.2.0",
16
- "@bablr/agast-vm-helpers": "0.2.0",
15
+ "@bablr/agast-helpers": "0.3.0",
16
+ "@bablr/agast-vm-helpers": "0.3.0",
17
17
  "@bablr/coroutine": "0.1.0",
18
- "@bablr/weak-stack": "0.1.0"
18
+ "@bablr/weak-stack": "0.1.0",
19
+ "@iter-tools/imm-stack": "1.1.0"
19
20
  },
20
21
  "devDependencies": {
21
22
  "@bablr/agast-vm-strategy-passthrough": "github:bablr-lang/agast-vm-strategy-passthrough#2bd3a0c7311037af92c5b81941c79161499f6c9e",