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