@bablr/agast-vm 0.8.0 → 0.9.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/state.js CHANGED
@@ -1,20 +1,25 @@
1
- import isArray from 'iter-tools-es/methods/is-array';
2
- import { WeakStackFrame } from '@bablr/weak-stack';
3
1
  import {
4
- add,
2
+ addProperty,
5
3
  finalizeNode,
6
- getRoot,
7
4
  buildStubNode,
8
5
  getRange,
9
- buildNullTag,
10
- treeFromStream,
11
6
  createNode,
12
- buildReferenceTag,
7
+ buildNullNode,
8
+ buildOpenNodeTag,
9
+ shiftProperty,
10
+ buildProperty,
11
+ add,
12
+ buildBindingTag,
13
+ buildAttributeDefinition,
13
14
  } from '@bablr/agast-helpers/tree';
14
- import * as sumtree from '@bablr/agast-helpers/sumtree';
15
+ import * as sumtree from '@bablr/agast-helpers/children';
16
+ import * as btree from '@bablr/agast-helpers/btree';
15
17
  import {
16
18
  getInitializerChildrenIndex,
17
- getPropertyChildrenIndex,
19
+ getOpenTag,
20
+ getOriginalFirstNode,
21
+ isGapNode,
22
+ isStubNode,
18
23
  referencesAreEqual,
19
24
  TagPath,
20
25
  } from '@bablr/agast-helpers/path';
@@ -28,52 +33,85 @@ import {
28
33
  NullTag,
29
34
  InitializerTag,
30
35
  LiteralTag,
36
+ AttributeDefinition,
37
+ BindingTag,
38
+ Property,
31
39
  } from '@bablr/agast-helpers/symbols';
32
40
  import { facades, actuals } from './facades.js';
33
- import { Path } from './path.js';
34
- import { Coroutine } from '@bablr/coroutine';
41
+ import { Path, PathFacade, TagPathFacade } from './path.js';
42
+ import { get, has, immSet, isObject } from '@bablr/agast-helpers/object';
43
+ import { NodeFacade } from './node.js';
35
44
 
45
+ const { isArray } = Array;
36
46
  const { hasOwn } = Object;
37
47
 
48
+ const defineAttribute = (node, tag) => {
49
+ if (tag.type !== AttributeDefinition) throw new Error();
50
+
51
+ let { path, value } = tag.value;
52
+ let openTag = getOpenTag(node);
53
+ let { attributes } = node;
54
+
55
+ if (!has(attributes, path) && get(attributes, path) !== undefined)
56
+ throw new Error('Can only define undefined attributes');
57
+
58
+ if (value === undefined) throw new Error('cannot define attribute to undefined');
59
+
60
+ let { flags, type } = openTag.value;
61
+ attributes = immSet(attributes, path, value);
62
+ let newOpenTag = buildOpenNodeTag(flags, type, attributes);
63
+
64
+ node.attributes = attributes;
65
+
66
+ node.children = sumtree.replaceAt(0, node.children, newOpenTag);
67
+ node.children = sumtree.push(node.children, tag);
68
+ };
69
+
38
70
  export const StateFacade = class AgastStateFacade {
39
71
  constructor(state) {
40
72
  facades.set(state, this);
41
73
  }
42
74
 
43
- static from(context) {
44
- return State.from(actuals.get(context));
75
+ static create() {
76
+ return State.create();
45
77
  }
46
78
 
47
79
  get resultPath() {
48
- return actuals.get(this).resultPath;
80
+ let { resultPath } = actuals.get(this);
81
+ return resultPath && new TagPathFacade(resultPath);
49
82
  }
50
83
 
51
- get reference() {
52
- return actuals.get(this).reference;
84
+ get referenceTag() {
85
+ return actuals.get(this).referenceTag;
53
86
  }
54
87
 
55
- get referencePath() {
56
- return actuals.get(this).referencePath;
88
+ get referenceTagPath() {
89
+ let { referenceTagPath } = actuals.get(this);
90
+ return referenceTagPath && new TagPathFacade(referenceTagPath);
57
91
  }
58
92
 
59
- get unshiftedReferencePath() {
60
- return actuals.get(this).unshiftedReferencePath;
93
+ get path() {
94
+ let { path } = actuals.get(this);
95
+ return path && new PathFacade(path);
61
96
  }
62
97
 
63
- get context() {
64
- return facades.get(actuals.get(this).context);
98
+ get node() {
99
+ let { node } = actuals.get(this);
100
+ return node && new NodeFacade(node);
65
101
  }
66
102
 
67
- get path() {
68
- return actuals.get(this).path;
103
+ get done() {
104
+ return actuals.get(this).done;
69
105
  }
70
106
 
71
- get node() {
72
- return actuals.get(this).node;
107
+ get parentNode() {
108
+ let { parentNode } = actuals.get(this);
109
+ return parentNode && new NodeFacade(parentNode);
73
110
  }
74
111
 
75
- get parentNode() {
76
- return actuals.get(this).parentNode;
112
+ get held() {
113
+ let { held } = actuals.get(this);
114
+ return held && new NodeFacade(held.node);
77
115
  }
78
116
 
79
117
  get holding() {
@@ -84,10 +122,6 @@ export const StateFacade = class AgastStateFacade {
84
122
  return actuals.get(this).depth;
85
123
  }
86
124
 
87
- get ctx() {
88
- return this.context;
89
- }
90
-
91
125
  get parent() {
92
126
  return facades.get(actuals.get(this).parent);
93
127
  }
@@ -95,33 +129,24 @@ export const StateFacade = class AgastStateFacade {
95
129
 
96
130
  export const nodeStates = new WeakMap();
97
131
 
98
- export const State = class AgastState extends WeakStackFrame {
99
- constructor(
100
- parent,
101
- context,
102
- expressions = [],
103
- path = Path.from(createNode()),
104
- held = null,
105
- resultPath = null,
106
- ) {
107
- super(parent);
108
-
109
- if (!context || !path || !expressions) throw new Error('invalid args to tagState');
110
-
111
- this.context = context;
112
- this.expressions = new Coroutine(expressions[Symbol.iterator]());
113
- this.path = path;
132
+ export const State = class AgastState {
133
+ static create() {
134
+ return new State();
135
+ }
136
+
137
+ constructor(held = null, path = null, resultPath = null) {
114
138
  this.held = held;
139
+ this.path = path;
115
140
  this.resultPath = resultPath;
116
- this.referencePath = null;
117
-
118
- if (!this.node) throw new Error();
141
+ this.done = false;
142
+ this.referenceTagPath = null;
143
+ this.doctype = null;
119
144
 
120
145
  new StateFacade(this);
121
146
  }
122
147
 
123
- static from(context, expressions = []) {
124
- return State.create(context, expressions);
148
+ static from(held) {
149
+ return new State(held);
125
150
  }
126
151
 
127
152
  get holding() {
@@ -129,33 +154,48 @@ export const State = class AgastState extends WeakStackFrame {
129
154
  }
130
155
 
131
156
  get atReference() {
132
- return !![ReferenceTag, ShiftTag].includes(this.resultPath?.tag.type);
157
+ return [ReferenceTag, ShiftTag].includes(this.resultPath?.tag.type);
158
+ }
159
+
160
+ get atBinding() {
161
+ return this.resultPath?.tag.type === BindingTag;
133
162
  }
134
163
 
135
164
  advance(tag) {
136
165
  if (!tag) throw new Error();
137
166
 
167
+ if (this.done) throw new Error();
168
+
138
169
  let targetPath = this.path;
139
170
 
140
171
  switch (tag.type) {
141
172
  case DoctypeTag: {
142
- let { path, node } = this;
173
+ let { path } = this;
143
174
 
144
- if (this.resultPath) throw new Error('invalid location for doctype');
175
+ if (this.resultPath || this.docType) throw new Error('invalid location for doctype');
145
176
 
146
- node.children = sumtree.push(node.children, tag);
147
- node.attributes = tag.value.attributes;
177
+ this.doctype = tag;
148
178
 
149
179
  targetPath = path;
150
180
  break;
151
181
  }
152
182
 
153
183
  case ReferenceTag: {
154
- let { name, isArray, flags } = tag.value;
184
+ let { type, isArray, name, flags } = tag.value;
155
185
 
156
186
  if (this.atReference) throw new Error('invalid location for reference');
157
- if (!name) throw new Error('missing reference name');
158
- if (!/#|@|[.]|[a-zA-Z]+/.test(name)) throw new Error('invalid reference name');
187
+ if (!name && !type) throw new Error();
188
+ if (type && !['.', '#', '@'].includes(type)) throw new Error();
189
+ if (['#', '@'].includes(type) && isArray) throw new Error();
190
+
191
+ if (this.held && sumtree.getAt(2, this.node.children)?.type === InitializerTag) {
192
+ if (
193
+ !referencesAreEqual(sumtree.getAt(1, this.node.children), tag) ||
194
+ sumtree.getSize(this.node.children) > 3
195
+ ) {
196
+ throw new Error();
197
+ }
198
+ }
159
199
 
160
200
  if (this.node.properties[name] && this.node.properties[name]?.node === undefined) {
161
201
  let existingReferenceIndex = getInitializerChildrenIndex(this.node, tag);
@@ -171,42 +211,102 @@ export const State = class AgastState extends WeakStackFrame {
171
211
  throw new Error('gap reference in gapless node');
172
212
  }
173
213
 
174
- this.referencePath = TagPath.from(this.path, -1);
214
+ this.referenceTagPath = TagPath.from(this.path, -1);
175
215
 
176
216
  break;
177
217
  }
178
218
 
219
+ case BindingTag: {
220
+ if (!this.atReference && this.parent) {
221
+ throw new Error('Invalid location for BindingTag');
222
+ }
223
+
224
+ this.node.children = sumtree.push(this.node.children, tag);
225
+ break;
226
+ }
227
+
179
228
  case OpenNodeTag: {
180
229
  let parentNode = this.node;
181
230
 
182
231
  targetPath = this.path;
183
232
 
184
- if (!this.atReference && this.parent) {
233
+ if (!this.atBinding && this.parent) {
185
234
  throw new Error('Invalid location for OpenNodeTag');
186
235
  }
187
236
 
188
- if (!this.path.depth) {
189
- this.node.flags = tag.value.flags;
190
- this.node.children = sumtree.push(this.node.children, tag);
191
- this.node.type = tag.value.type;
192
- this.node.language = tag.value.language;
193
- this.node.attributes = tag.value.attributes || {};
237
+ let bindingTag = parentNode && sumtree.getAt(-1, parentNode.children);
238
+
239
+ if (bindingTag && !bindingTag.value.languagePath) throw new Error();
240
+
241
+ let node = createNode(tag);
242
+
243
+ if (!this.path) {
244
+ this.path = Path.create(node);
245
+
246
+ if (!tag.value.type && this.doctype) {
247
+ node.children = sumtree.push(node.children, this.doctype);
248
+ }
194
249
  } else {
195
- let node = createNode(tag);
196
250
  this.path = this.path.push(node, sumtree.getSize(parentNode.children) - 2);
197
- add(parentNode, this.reference, node);
251
+ add(parentNode, this.referenceTag, node, bindingTag);
252
+ }
253
+
254
+ node.children = sumtree.push(node.children, tag);
255
+
256
+ let undefinedAttributes = 0;
257
+
258
+ let queue = [this.node.attributes];
259
+ while (queue.length) {
260
+ for (let value of Object.values(queue[queue.length - 1])) {
261
+ if (value === undefined) {
262
+ undefinedAttributes++;
263
+ } else if (isArray(value) || isObject(value)) {
264
+ queue.push(value);
265
+ }
266
+ }
267
+ queue.pop();
198
268
  }
199
269
 
270
+ nodeStates.set(this.node, { undefinedAttributes });
271
+
200
272
  break;
201
273
  }
202
274
 
203
275
  case CloseNodeTag: {
204
276
  if (this.atReference) throw new Error('invalid location for close tag');
205
277
 
278
+ if (this.node.type) {
279
+ for (const value of Object.values(this.node.properties)) {
280
+ if (!Array.isArray(value) && value.node === undefined) {
281
+ addProperty(
282
+ this.node,
283
+ buildProperty(value.reference, buildBindingTag().value, buildNullNode()),
284
+ );
285
+ }
286
+ }
287
+ }
288
+
289
+ let path = [];
290
+ let queue = [this.node.attributes];
291
+ while (queue.length) {
292
+ for (const { 0: key, 1: value } of Object.entries(queue[queue.length - 1])) {
293
+ if (value === undefined) {
294
+ path.push(key);
295
+ defineAttribute(this.node, buildAttributeDefinition(path, null));
296
+ path.pop(key);
297
+ } else if (isArray(value) || isObject(value)) {
298
+ queue.push(value);
299
+ path.push(key);
300
+ }
301
+ }
302
+ queue.pop();
303
+ }
304
+
206
305
  this.node.children = sumtree.push(this.node.children, tag);
207
306
 
208
- if (this.node.undefinedAttributes?.size)
209
- throw new Error('Grammar failed to bind all attributes');
307
+ if (!this.path) {
308
+ this.done = true;
309
+ }
210
310
 
211
311
  finalizeNode(this.node);
212
312
 
@@ -214,46 +314,43 @@ export const State = class AgastState extends WeakStackFrame {
214
314
  break;
215
315
  }
216
316
 
217
- case GapTag: {
317
+ case Property: {
218
318
  let target;
319
+ let property = tag.value;
320
+ let { node } = this.path;
219
321
  let lastTagPath = TagPath.from(this.path, -1);
220
322
 
221
- if (!this.atReference) {
222
- throw new Error('Invalid location for GapTag');
323
+ if (!this.atBinding && !isStubNode(property.node)) {
324
+ throw new Error('Invalid location for Property');
223
325
  }
224
326
 
225
- if (this.held && lastTagPath.tag.type !== ShiftTag) {
226
- target = this.held.node;
227
-
228
- this.held = null;
229
- } else {
230
- this.expressions.advance();
231
-
232
- if (!this.expressions.done) {
233
- const expression = this.expressions.value;
234
-
235
- if (isArray(expression)) {
236
- throw new Error('Invalid array interpolation');
237
- }
238
-
239
- if (expression == null) {
240
- target = treeFromStream(buildNullTag());
241
- } else {
242
- target = getRoot(expression);
243
- }
327
+ if (this.held) {
328
+ if (isGapNode(property.node)) {
329
+ property = buildProperty(property.reference, this.held.binding, this.held.node);
244
330
  } else {
245
- if (!this.node.flags.hasGap) throw new Error('Node must allow gaps');
246
-
247
- target = buildStubNode(tag);
331
+ let { height } = lastTagPath.previousSibling.tag.value;
332
+ let matchesHeld =
333
+ isGapNode(property.node) ||
334
+ this.held.node === property.node ||
335
+ this.held.node === getOriginalFirstNode(property.node, (height ?? 1) - 1);
336
+ if (!matchesHeld) {
337
+ throw new Error();
338
+ }
248
339
  }
340
+ }
341
+
342
+ if (this.held && lastTagPath.previousSibling.tag.type !== ShiftTag) {
343
+ target = this.held.node;
344
+ } else {
345
+ // if (!this.node.flags.hasGap) throw new Error('Node must allow gaps');
249
346
 
250
- this.held = null;
347
+ target = buildStubNode(tag);
251
348
  }
252
349
 
253
350
  const range = getRange(target);
254
351
 
255
352
  this.resultPath = range ? range[1] : this.resultPath;
256
- this.referencePath = null;
353
+ this.referenceTagPath = null;
257
354
 
258
355
  let lastRefPath = [ShiftTag, ReferenceTag].includes(lastTagPath.tag.type)
259
356
  ? lastTagPath
@@ -261,80 +358,88 @@ export const State = class AgastState extends WeakStackFrame {
261
358
 
262
359
  if (![ShiftTag, ReferenceTag].includes(lastRefPath.tag.type)) throw new Error();
263
360
 
264
- let refTag = lastRefPath.tag;
265
-
266
- if (refTag.type === ShiftTag) {
267
- let refIndex = lastRefPath.childrenIndex - lastRefPath.tag.value.index * 2;
268
- refTag = sumtree.getAt(refIndex, this.node.children);
269
- }
270
-
271
- if (refTag) {
272
- add(
273
- this.path.node,
274
- refTag,
275
- target,
276
- lastRefPath.tag.type === ShiftTag
277
- ? lastRefPath.tag.value.index
278
- : refTag.value.flags.expression
279
- ? 0
280
- : null,
281
- );
361
+ if (lastRefPath.tag.type === ShiftTag) {
362
+ shiftProperty(node, property);
363
+ } else {
364
+ addProperty(node, property);
282
365
  }
283
366
 
367
+ this.held = null;
284
368
  break;
285
369
  }
286
370
 
371
+ case GapTag:
287
372
  case NullTag: {
288
- const { node: parentNode, reference } = this;
373
+ const { node: parentNode, referenceTag } = this;
374
+
375
+ if (this.held) throw new Error();
289
376
 
290
377
  if (!this.atReference && this.parent) {
291
- throw new Error('Invalid location for OpenNodeTag');
378
+ throw new Error('Invalid location for NullTag');
292
379
  }
293
380
 
294
381
  let stubNode = buildStubNode(tag);
295
382
 
296
- add(parentNode, reference, stubNode);
383
+ add(parentNode, referenceTag, stubNode);
384
+
385
+ targetPath = this.path.push(stubNode, sumtree.getSize(this.node.children) - 3);
386
+ break;
387
+ }
388
+
389
+ case AttributeDefinition: {
390
+ if (this.held) throw new Error('invalid place for an atrribute binding');
391
+
392
+ let nodeState = nodeStates.get(this.node);
393
+
394
+ nodeState.undefinedAttributes--;
395
+
396
+ // add undefined attributes from value
397
+
398
+ defineAttribute(this.node, tag);
297
399
 
298
- targetPath = this.path.push(stubNode, sumtree.getSize(this.node.children) - 2);
299
400
  break;
300
401
  }
301
402
 
302
403
  case InitializerTag: {
303
404
  const { isArray } = tag.value;
304
- const { node, reference } = this;
305
- const { name } = reference.value;
405
+ const { node, referenceTag } = this;
406
+ const { name } = referenceTag.value;
306
407
 
307
408
  if (!this.atReference && this.parent) {
308
- throw new Error('Invalid location for OpenNodeTag');
409
+ throw new Error('Invalid location for InitializerTag');
410
+ }
411
+
412
+ if (this.held && sumtree.getAt(2, node.children)?.type === InitializerTag) {
413
+ throw new Error();
309
414
  }
310
415
 
311
- if (node.properties[name] && node.properties[name]?.node === undefined) {
416
+ if (
417
+ hasOwn(node.properties, name) &&
418
+ (isArray
419
+ ? btree.getSize(node.properties[name])
420
+ : node.properties[name]?.node !== undefined)
421
+ ) {
312
422
  throw new Error();
313
423
  }
314
424
 
315
- add(node, reference, isArray ? [] : undefined);
425
+ add(node, referenceTag, isArray ? [] : undefined);
316
426
  break;
317
427
  }
318
428
 
319
429
  case ShiftTag: {
320
- if (this.atReference) throw new Error('invalid location for shift');
321
-
322
- const finishedPath = this.resultPath.innerPath;
323
- const finishedNode = finishedPath.node;
324
- const ref = TagPath.from(
325
- this.resultPath,
326
- this.resultPath.childrenIndex - tag.value.index * 2 + 1,
327
- ).tag;
430
+ if (this.resultPath.tag.type !== Property) throw new Error('invalid location for shift');
328
431
 
329
- this.held = { node: finishedNode, path: finishedPath };
432
+ let heldProperty = this.resultPath.tag.value;
330
433
 
331
- if (!ref.value.name) throw new Error();
434
+ if (!this.held) {
435
+ this.held = heldProperty;
436
+ }
332
437
 
333
- if (!ref.value.flags.expression) throw new Error();
438
+ if (!heldProperty.reference.flags.expression) throw new Error();
334
439
 
335
440
  this.node.children = sumtree.push(this.node.children, tag);
336
441
 
337
- this.referencePath = TagPath.from(this.path, -1);
442
+ this.referenceTagPath = TagPath.from(this.path, -1);
338
443
 
339
444
  // this.path = finishedPath;
340
445
  targetPath = this.path;
@@ -351,15 +456,11 @@ export const State = class AgastState extends WeakStackFrame {
351
456
  throw new Error();
352
457
  }
353
458
 
354
- this.resultPath = TagPath.from(targetPath, -1);
459
+ this.resultPath = targetPath && TagPath.from(targetPath, -1);
355
460
 
356
461
  return tag;
357
462
  }
358
463
 
359
- get ctx() {
360
- return this.context;
361
- }
362
-
363
464
  get isGap() {
364
465
  return this.tag.type === GapTag;
365
466
  }
@@ -372,8 +473,17 @@ export const State = class AgastState extends WeakStackFrame {
372
473
  return this.path.parent.node;
373
474
  }
374
475
 
375
- get reference() {
376
- return this.referencePath.tag;
476
+ get referenceTag() {
477
+ return this.referenceTagPath?.tag;
478
+ }
479
+
480
+ get bindingPath() {
481
+ let path = this.referenceTagPath.nextSibling;
482
+ return path.tag.type === BindingTag ? path : null;
483
+ }
484
+
485
+ get binding() {
486
+ return this.bindingPath?.tag;
377
487
  }
378
488
 
379
489
  get node() {
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.8.0",
4
+ "version": "0.9.0",
5
5
  "author": "Conrad Buck<conartist6@gmail.com>",
6
6
  "type": "module",
7
7
  "files": [
@@ -12,11 +12,8 @@
12
12
  },
13
13
  "sideEffects": false,
14
14
  "dependencies": {
15
- "@bablr/agast-helpers": "0.7.0",
16
- "@bablr/agast-vm-helpers": "0.7.0",
17
- "@bablr/coroutine": "0.1.0",
18
- "@bablr/weak-stack": "1.0.0",
19
- "@iter-tools/imm-stack": "1.1.0"
15
+ "@bablr/agast-helpers": "0.8.0",
16
+ "@bablr/agast-vm-helpers": "0.8.0"
20
17
  },
21
18
  "devDependencies": {
22
19
  "@bablr/eslint-config-base": "github:bablr-lang/eslint-config-base#49f5952efed27f94ee9b94340eb1563c440bf64e",