@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/README.md CHANGED
@@ -1,58 +1,23 @@
1
1
  # @bablr/agast-vm
2
2
 
3
- The agAST VM provides consistency guarantees when with CSTML documents to parse or transform code. It has no language-specific functionality of any kind. Instead it acts as a streaming traversal engine for CSTML.
3
+ The agAST VM's purpose is to define what is valid agAST. For complete documentation, see the [agast-vm API reference](https://docs.bablr.org/reference/agast-vm).
4
4
 
5
- ## API
5
+ ## Usage
6
6
 
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`.
7
+ ```js
8
+ import { agast } from '@bablr/agast-vm';
9
+ import * as b from '@bablr/agast-helpers/builders';
8
10
 
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.
11
+ let vm = agast();
12
+ let step;
10
13
 
11
- The VM supports `branch()`, `accept()`, and `reject()` instructions, which allow a series of instructions to have their effects applied or discarded together in a kind of transaction.
14
+ let openTag = b.buildOpenNodeTag(b.tokenFlags, 'Token');
15
+ let closeTag = b.buildLiteralTag('OK');
16
+ let closeTag = b.buildCloseNodeTag();
12
17
 
13
- Finally the VM supports `bindAttribute(key, value)`. A node's attributes start unbound, and this command is used to give them values. Once all declared attributes for a node are bound, that node's full start tag is known and can be emitted.
18
+ step = vm.next(openTag);
19
+ step = vm.next(literalTag);
20
+ step = vm.next(closeTag);
14
21
 
15
- Here are the basic types used by the VM:
16
-
17
- ```ts
18
- type Token = OpenNodeTag | CloseNodeTag | Literal | Reference | Gap;
19
-
20
- type OpenNodeTag {
21
- type: 'OpenNodeTag',
22
- value: {
23
- flags: {
24
- token: boolean,
25
- trivia: boolean,
26
- escape: boolean
27
- },
28
- language: string | null,
29
- type: string | null, // null type indicates a fragment
30
- attributes: { [key: string]: boolean | number | string }
31
- }
32
- }
33
-
34
- type CloseNodeTag {
35
- type: 'CloseNodeTag',
36
- value: {
37
- language: string,
38
- type: string,
39
- }
40
- }
41
-
42
- type Literal {
43
- value: string
44
- }
45
-
46
- type Reference {
47
- type: 'Reference',
48
- value: {
49
- name: string,
50
- isArray: boolean
51
- }
52
- }
53
-
54
- type Gap {
55
- type: 'Gap',
56
- value: null,
57
- }
22
+ let node = step.value;
58
23
  ```
package/lib/evaluate.js CHANGED
@@ -1,18 +1,3 @@
1
- import { Coroutine } from '@bablr/coroutine';
2
- import {
3
- buildNullTag,
4
- buildGapTag,
5
- buildShiftTag,
6
- buildReferenceTag,
7
- buildLiteralTag,
8
- buildDoctypeTag,
9
- buildOpenNodeTag,
10
- buildCloseNodeTag,
11
- buildInitializerTag,
12
- } from '@bablr/agast-vm-helpers/internal-builders';
13
- import { getEmbeddedTag } from '@bablr/agast-vm-helpers/deembed';
14
- import { StreamIterable, getStreamIterator } from '@bablr/agast-helpers/stream';
15
- import { printExpression } from '@bablr/agast-helpers/print';
16
1
  import {
17
2
  DoctypeTag,
18
3
  OpenNodeTag,
@@ -23,186 +8,170 @@ import {
23
8
  NullTag,
24
9
  InitializerTag,
25
10
  LiteralTag,
11
+ AttributeDefinition,
12
+ BindingTag,
13
+ Property,
26
14
  } from '@bablr/agast-helpers/symbols';
27
15
  import { State } from './state.js';
28
- import { Context } from './context.js';
29
16
  import { facades } from './facades.js';
30
- import { add, TagPath } from '@bablr/agast-helpers/path';
31
- import { buildNullNode, defineAttribute } from '@bablr/agast-helpers/tree';
32
-
33
- export const agast = (strategy, options = {}) => {
34
- const ctx = Context.create();
35
- let s = State.from(ctx, options.expressions);
17
+ import { getFirstNode, TagPath } from '@bablr/agast-helpers/path';
18
+ import * as sumtree from '@bablr/agast-helpers/children';
19
+ import { NodeFacade } from './node.js';
36
20
 
37
- return new StreamIterable(__agast(ctx, s, strategy(facades.get(ctx), facades.get(s))));
38
- };
21
+ let { isArray } = Array;
39
22
 
40
- function* __agast(ctx, s, instructions) {
41
- const co = new Coroutine(getStreamIterator(instructions));
23
+ export const agast = (options = {}) => {
24
+ if (options.held?.properties) throw new Error();
42
25
 
43
- co.advance();
26
+ let state = State.from(options.held);
44
27
 
45
- for (;;) {
46
- if (co.current instanceof Promise) {
47
- co.current = yield co.current;
48
- }
28
+ let vm = __agast(state, options);
49
29
 
50
- if (co.done) break;
30
+ vm.next();
51
31
 
52
- const instr = co.value;
32
+ Object.freeze(options);
53
33
 
54
- let returnValue = undefined;
34
+ return Object.freeze({ options, state: facades.get(state), vm });
35
+ };
55
36
 
56
- const { verb, arguments: args = [] } = instr;
37
+ function* __agast(s) {
38
+ let tag = yield;
39
+ for (;;) {
40
+ let returnValue = null;
57
41
 
58
- switch (verb) {
59
- case 'advance': {
60
- const { 0: embeddedTags } = args;
42
+ if (s.resultPath && !s.path) {
43
+ throw new Error('Cannot advance after completion');
44
+ }
61
45
 
62
- const tag = getEmbeddedTag(embeddedTags);
46
+ if (
47
+ s.held &&
48
+ ![OpenNodeTag, ReferenceTag, InitializerTag, Property, BindingTag].includes(tag.type) &&
49
+ !(
50
+ (tag.type === Property &&
51
+ (sumtree.getSize(tag.value.node.children) <= 1 ||
52
+ getFirstNode(tag.value.node) === s.held.node)) ||
53
+ tag.value.node === s.held.node
54
+ )
55
+ ) {
56
+ throw new Error('Cannot advance while holding');
57
+ }
63
58
 
64
- if (
65
- s.held &&
66
- !(tag.type === OpenNodeTag || tag.type === ReferenceTag || tag.type === GapTag)
67
- ) {
68
- throw new Error('Cannot advance while holding');
59
+ switch (tag.type) {
60
+ case DoctypeTag: {
61
+ if (s.path) {
62
+ throw new Error();
69
63
  }
70
64
 
71
- switch (tag?.type || NullTag) {
72
- case DoctypeTag: {
73
- const { attributes } = tag.value;
65
+ s.advance(tag);
66
+ break;
67
+ }
74
68
 
75
- if (s.path.depth !== 0) {
76
- throw new Error();
77
- }
69
+ case LiteralTag: {
70
+ if (!s.node.flags.token) {
71
+ throw new Error('literals must occur inside tokens');
72
+ }
78
73
 
79
- returnValue = s.advance(buildDoctypeTag(attributes));
80
- break;
81
- }
74
+ if (s.held) {
75
+ throw new Error('Cannot consume input while hold register is full');
76
+ }
82
77
 
83
- case LiteralTag: {
84
- if (!s.node.flags.token) {
85
- throw new Error('literals must occur inside tokens');
86
- }
78
+ s.advance(tag);
79
+ break;
80
+ }
87
81
 
88
- if (s.held) {
89
- throw new Error('Cannot consume input while hold register is full');
90
- }
82
+ case ReferenceTag: {
83
+ const { type } = tag.value;
91
84
 
92
- returnValue = s.advance(buildLiteralTag(tag.value));
93
- break;
94
- }
85
+ if (s.node?.flags.token && type !== '@') {
86
+ throw new Error('A token node cannot contain a reference');
87
+ }
95
88
 
96
- case ReferenceTag: {
97
- const { name, isArray, flags } = tag.value;
89
+ s.advance(tag);
90
+ break;
91
+ }
98
92
 
99
- if (s.node?.flags.token && name !== '@') {
100
- throw new Error('A token node cannot contain a reference');
101
- }
93
+ case BindingTag: {
94
+ const { languagePath } = tag.value;
102
95
 
103
- returnValue = s.advance(buildReferenceTag(name, isArray, flags));
104
- break;
105
- }
96
+ if (languagePath && !isArray(languagePath)) throw new Error();
106
97
 
107
- case GapTag: {
108
- returnValue = s.advance(buildGapTag());
109
- break;
110
- }
98
+ s.advance(tag);
99
+ break;
100
+ }
111
101
 
112
- case NullTag: {
113
- returnValue = s.advance(buildNullTag());
114
- break;
115
- }
102
+ case NullTag:
103
+ case GapTag: {
104
+ s.advance(tag);
105
+ returnValue = new NodeFacade(s.resultPath.node);
106
+ break;
107
+ }
116
108
 
117
- case InitializerTag: {
118
- const { isArray } = tag.value;
119
- const { reference } = s;
109
+ case AttributeDefinition: {
110
+ if (s.atReference) throw new Error();
120
111
 
121
- if (reference?.type !== ReferenceTag) throw new Error();
112
+ s.advance(tag);
113
+ break;
114
+ }
122
115
 
123
- returnValue = s.advance(buildInitializerTag(isArray));
124
- break;
125
- }
116
+ case InitializerTag: {
117
+ if (!s.atReference) throw new Error();
126
118
 
127
- case ShiftTag: {
128
- const { index } = tag.value;
129
- if (s.resultPath.tag.type !== GapTag) throw new Error();
119
+ s.advance(tag);
120
+ break;
121
+ }
130
122
 
131
- let prevRef = s.resultPath.previousSibling;
123
+ case ShiftTag: {
124
+ const { index } = tag.value;
125
+ if (s.resultPath.tag.type !== Property) throw new Error();
132
126
 
133
- if (prevRef.tag.type === ShiftTag) {
134
- if (prevRef.tag.value.index + 1 !== index) throw new Error('bad shift index');
135
- }
127
+ let prevRef = s.resultPath.previousSibling.previousSibling;
136
128
 
137
- const refPath = TagPath.from(
138
- s.resultPath.path,
139
- s.resultPath.childrenIndex - (index - 1) * 2 - 1,
140
- );
129
+ if (prevRef.tag.type === ShiftTag) {
130
+ if (prevRef.tag.value.index + 1 !== index) throw new Error('bad shift index');
131
+ }
141
132
 
142
- if (refPath.tag.type !== ReferenceTag) throw new Error();
133
+ let refPath = TagPath.from(
134
+ s.resultPath.path,
135
+ s.resultPath.childrenIndex - (index - 1) * 3 - 2,
136
+ );
143
137
 
144
- if (!refPath.tag.value.flags.expression) {
145
- throw new Error();
146
- }
138
+ if (refPath.tag.type !== ReferenceTag) throw new Error();
147
139
 
148
- returnValue = s.advance(buildShiftTag(index));
149
- break;
150
- }
140
+ if (!refPath.tag.value.flags.expression) {
141
+ throw new Error();
142
+ }
151
143
 
152
- case OpenNodeTag: {
153
- const { flags, language, type, attributes } = tag.value;
144
+ s.advance(tag);
145
+ break;
146
+ }
154
147
 
155
- if (language && !language.startsWith('https://')) {
156
- throw new Error('Expected an absolute-language tag');
157
- }
148
+ case OpenNodeTag: {
149
+ s.advance(tag);
158
150
 
159
- returnValue = s.advance(buildOpenNodeTag(flags, language, type, attributes));
160
- break;
161
- }
151
+ returnValue = new NodeFacade(s.node);
152
+ break;
153
+ }
162
154
 
163
- case CloseNodeTag: {
164
- for (const { 0: key, 1: value } of Object.entries(s.node.properties)) {
165
- if (!Array.isArray(value)) {
166
- if (value.node === undefined) {
167
- add(s.node, value.reference, buildNullNode());
168
- }
169
- }
170
- }
171
-
172
- for (const { 0: key, 1: value } of Object.entries(s.node.attributes)) {
173
- if (value === undefined) {
174
- defineAttribute(s.node, key, null);
175
- }
176
- }
155
+ case CloseNodeTag: {
156
+ returnValue = new NodeFacade(s.node);
177
157
 
178
- returnValue = s.advance(buildCloseNodeTag());
179
- break;
180
- }
158
+ s.advance(tag);
159
+ break;
160
+ }
181
161
 
182
- default:
183
- throw new Error();
184
- }
162
+ case Property: {
163
+ let { node } = tag.value;
185
164
 
186
- yield returnValue;
165
+ s.advance(tag);
187
166
 
167
+ returnValue = new NodeFacade(node);
188
168
  break;
189
169
  }
190
170
 
191
- default: {
192
- throw new Error(`Unexpected call of {type: ${printExpression(verb)}}`);
193
- }
171
+ default:
172
+ throw new Error();
194
173
  }
195
174
 
196
- co.advance(returnValue);
175
+ tag = yield returnValue;
197
176
  }
198
-
199
- if (s.depth > 0) {
200
- throw new Error('Did not unwind state stack');
201
- }
202
-
203
- if (s.path) {
204
- throw new Error('Did not unwind path stack');
205
- }
206
-
207
- return s.resultPath.path.node;
208
177
  }
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { agast } from './evaluate.js';
2
2
 
3
- export { Context } from './context.js';
3
+ export { PathFacade, TagPathFacade } from './path.js';
4
+ export { NodeFacade } from './node.js';
package/lib/node.js ADDED
@@ -0,0 +1,176 @@
1
+ import {
2
+ buildPathSegment,
3
+ get,
4
+ getChildPropertyIndex,
5
+ getCloseTag,
6
+ getProperties,
7
+ getPropertyChildrenIndex,
8
+ } from '@bablr/agast-helpers/path';
9
+ import * as sumtree from '@bablr/agast-helpers/children';
10
+ import * as btree from '@bablr/agast-helpers/btree';
11
+ import { DoctypeTag, GapTag, NullTag, OpenNodeTag } from '@bablr/agast-helpers/symbols';
12
+ import { nodeFlags } from '@bablr/agast-helpers/builders';
13
+ import { nodeStates } from './state.js';
14
+
15
+ let { freeze, hasOwn } = Object;
16
+
17
+ export const nodes = new WeakMap();
18
+
19
+ export class NodeChildrenFacade {
20
+ constructor(node) {
21
+ nodes.set(this, node);
22
+ let tree = node.children;
23
+
24
+ if (!Object.isFrozen(tree)) throw new Error(); // just a sanity check right now
25
+
26
+ this.size = sumtree.getSize(tree);
27
+
28
+ freeze(this);
29
+ }
30
+
31
+ at(idx) {
32
+ return sumtree.getAt(idx, nodes.get(this).children);
33
+ }
34
+
35
+ [Symbol.iterator]() {
36
+ return sumtree.traverse(nodes.get(this).children);
37
+ }
38
+ }
39
+
40
+ export class ListFacade {
41
+ constructor(tree) {
42
+ nodes.set(this, tree);
43
+
44
+ this.size = btree.getSize(tree);
45
+
46
+ freeze(this);
47
+ }
48
+
49
+ at(idx) {
50
+ return btree.getAt(idx, nodes.get(this));
51
+ }
52
+
53
+ [Symbol.iterator]() {
54
+ return btree.traverse(nodes.get(this));
55
+ }
56
+ }
57
+
58
+ export class NodePropertiesFacade {
59
+ constructor(node) {
60
+ nodes.set(this, node);
61
+
62
+ freeze(this);
63
+ }
64
+
65
+ at(name, index = -1, shiftIndex = 0) {
66
+ let { properties } = nodes.get(this);
67
+
68
+ if (hasOwn(properties, name)) {
69
+ let prop = properties[name];
70
+
71
+ if (Array.isArray(prop.node) && index === -1) {
72
+ return new ListFacade(prop.node);
73
+ }
74
+
75
+ if (Array.isArray(prop.node)) {
76
+ prop = btree.getAt(index ?? -1, prop.node);
77
+ }
78
+
79
+ if (!prop) return index === -1 ? prop : null;
80
+ if (prop.reference.flags.expression) {
81
+ return NodeFacade.from(btree.getAt(shiftIndex + 1, prop.node)?.node);
82
+ } else {
83
+ return NodeFacade.from(prop.node);
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ has(name, index = -1) {
90
+ let { properties } = nodes.get(this);
91
+ if (!hasOwn(properties, name)) return false;
92
+
93
+ if (index >= 0 && !getProperties(name, index, properties)) {
94
+ return false;
95
+ }
96
+
97
+ return true;
98
+ }
99
+
100
+ referenceAt(name, index = -1) {
101
+ return getProperties(buildPathSegment(name, index), nodes.get(this).properties)?.reference;
102
+ }
103
+
104
+ [Symbol.iterator]() {
105
+ return Object.entries(nodes.get(this))[Symbol.iterator]();
106
+ }
107
+ }
108
+
109
+ export class NodeFacade {
110
+ static from(node) {
111
+ return node && new NodeFacade(node);
112
+ }
113
+
114
+ constructor(node) {
115
+ let sigilTag = sumtree.getAt(0, node.children);
116
+
117
+ if (sigilTag.type === DoctypeTag) {
118
+ sigilTag = sumtree.getAt(1, node.children);
119
+ }
120
+
121
+ this.sigilTag = sigilTag;
122
+
123
+ if ([NullTag, GapTag].includes(sigilTag.type)) {
124
+ this.flags = nodeFlags;
125
+ this.type = null;
126
+ } else if (sigilTag.type === OpenNodeTag) {
127
+ let { flags, type } = sigilTag.value;
128
+
129
+ this.flags = flags;
130
+ this.type = type;
131
+ } else {
132
+ throw new Error();
133
+ }
134
+
135
+ nodes.set(this, node);
136
+
137
+ freeze(this);
138
+ }
139
+
140
+ get children() {
141
+ return new NodeChildrenFacade(nodes.get(this));
142
+ }
143
+
144
+ get properties() {
145
+ return new NodePropertiesFacade(nodes.get(this));
146
+ }
147
+
148
+ get attributes() {
149
+ return nodes.get(this).attributes;
150
+ }
151
+
152
+ get undefinedAttributes() {
153
+ return nodeStates.get(nodes.get(this)).undefinedAttributes;
154
+ }
155
+
156
+ get node() {
157
+ let node = nodes.get(this);
158
+
159
+ return getCloseTag(node) || [NullTag, GapTag].includes(sumtree.getAt(0, node.children).type)
160
+ ? node
161
+ : undefined;
162
+ }
163
+
164
+ get(path, shiftIndex = null) {
165
+ if (shiftIndex !== null) throw new Error('bad get');
166
+ return NodeFacade.from(get(path, nodes.get(this)));
167
+ }
168
+
169
+ getChildPropertyIndex(childrenIndex) {
170
+ return getChildPropertyIndex(nodes.get(this), childrenIndex);
171
+ }
172
+
173
+ getPropertyChildrenIndex(type, name) {
174
+ return getPropertyChildrenIndex(nodes.get(this), type, name);
175
+ }
176
+ }