@bablr/agast-vm 0.1.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/LICENSE +21 -0
- package/README.md +78 -0
- package/lib/context.js +170 -0
- package/lib/evaluate.js +422 -0
- package/lib/facades.js +3 -0
- package/lib/index.js +3 -0
- package/lib/path.js +221 -0
- package/lib/state.js +199 -0
- package/lib/symbols.js +1 -0
- package/lib/utils/array.js +7 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Conrad Buck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @bablr/agast-vm
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
The goal of this project is to transition editors towards being a lot more like web browsers. You can have many of them, and they can be written in a variety of languages (though many share internals). You can even have a terminal browser like Lynx that does presentation very differently, yet it is still possible (if not trivial) to write a website once that will run on all (er, most) web browsers.
|
|
8
|
+
|
|
9
|
+
If the parallel is not immediately obvious, try thinking about it this way: a webapp is really more or less a set of automated tools for editing a DOM tree. As programmers we have all these amazing web libraries and frameworks that can exist because at the end of the day everything comes down to editing a shared DOM tree. There's a great explanation of those dynamics here: https://glazkov.com/2024/01/02/the-protocol-force/
|
|
10
|
+
|
|
11
|
+
If a code-DOM existed and was shared by all IDEs there would spring up a rich ecosystem of tools for accomplishing many kinds of tree alterations. For example it could become common for library authors to publish codemods that could help any project upgrade past breaking changes in its APIs!
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
The VM responds to several instructions, but its primary API is `advance(token)`, where `token` may be a `StartFragmentTag`, `EndFragmentTag`, `StartNodeTag`, `EndNodeTag`, `Literal`, `Reference`, or `Gap`.
|
|
16
|
+
|
|
17
|
+
The VM requires the basic invariants of CSTML to be followed, for example that `Reference` must be followed by either a `StartNodeTag` or a `Gap`. In fact, `agast-vm` is the reference implementation of these invariants.
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
Here are the basic types used by the VM:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
type Token = StartFragmentTag | EndFragmentTag | StartNodeTag | EndNodeTag | Literal | Reference | Gap;
|
|
27
|
+
|
|
28
|
+
type StartFragmentTag {
|
|
29
|
+
type: 'StartFragmentTag',
|
|
30
|
+
value: {
|
|
31
|
+
flags: {
|
|
32
|
+
trivia: boolean
|
|
33
|
+
},
|
|
34
|
+
language: string,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type EndFragmentTag {
|
|
39
|
+
type: 'EndFragmentTag',
|
|
40
|
+
value: null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type StartNodeTag {
|
|
44
|
+
type: 'StartNodeTag',
|
|
45
|
+
value: {
|
|
46
|
+
flags: {
|
|
47
|
+
token: boolean,
|
|
48
|
+
trivia: boolean,
|
|
49
|
+
escape: boolean
|
|
50
|
+
},
|
|
51
|
+
language: string,
|
|
52
|
+
type: string,
|
|
53
|
+
attributes: { [key: string]: boolean | number | string }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type EndNodeTag {
|
|
58
|
+
type: 'EndNodeTag',
|
|
59
|
+
value: null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type Literal {
|
|
63
|
+
value: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Reference {
|
|
67
|
+
type: 'Reference',
|
|
68
|
+
value: {
|
|
69
|
+
name: string,
|
|
70
|
+
isArray: boolean
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type Gap {
|
|
75
|
+
type: 'Gap',
|
|
76
|
+
value: null,
|
|
77
|
+
}
|
|
78
|
+
```
|
package/lib/context.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { reifyExpressionShallow, reifyExpression } from '@bablr/agast-vm-helpers';
|
|
2
|
+
import { getCooked, sourceTextFor } from '@bablr/agast-helpers/stream';
|
|
3
|
+
import { facades, actuals } from './facades.js';
|
|
4
|
+
|
|
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
|
+
}
|
|
20
|
+
|
|
21
|
+
function* allTerminalsFor(range, nextTerminals) {
|
|
22
|
+
if (!range) return;
|
|
23
|
+
const { 0: start, 1: end } = range;
|
|
24
|
+
|
|
25
|
+
const pastEnd = nextTerminals.get(end);
|
|
26
|
+
|
|
27
|
+
for (let tag = start; tag !== pastEnd; tag = nextTerminals.get(tag)) {
|
|
28
|
+
yield tag;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const ContextFacade = class AgastContextFacade {
|
|
33
|
+
getPreviousTerminal(token) {
|
|
34
|
+
return actuals.get(this).prevTerminals.get(token);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getNextTerminal(token) {
|
|
38
|
+
return actuals.get(this).nextTerminals.get(token);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ownTerminalsFor(range) {
|
|
42
|
+
return actuals.get(this).ownTerminalsFor(range);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
allTerminalsFor(range) {
|
|
46
|
+
return actuals.get(this).allTerminalsFor(range);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getCooked(range) {
|
|
50
|
+
return actuals.get(this).getCooked(range);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
reifyExpression(range) {
|
|
54
|
+
return actuals.get(this).reifyExpression(range);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getProperty(node, name) {
|
|
58
|
+
return actuals.get(this).getProperty(node, name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
sourceTextFor(range) {
|
|
62
|
+
return actuals.get(this).sourceTextFor(range);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
buildRange(terminals) {
|
|
66
|
+
return actuals.get(this).buildRange(terminals);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
nodeForTag(tag) {
|
|
70
|
+
return actuals.get(this).nodeForTag(tag);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
unbox(value) {
|
|
74
|
+
return actuals.get(this).unbox(value);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const Context = class AgastContext {
|
|
79
|
+
static create() {
|
|
80
|
+
return new Context();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
constructor() {
|
|
84
|
+
this.prevTerminals = new WeakMap();
|
|
85
|
+
this.nextTerminals = new WeakMap();
|
|
86
|
+
this.tagNodes = new WeakMap();
|
|
87
|
+
this.unboxedValues = new WeakMap();
|
|
88
|
+
this.facade = new ContextFacade();
|
|
89
|
+
|
|
90
|
+
facades.set(this, this.facade);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getProperty(result, name) {
|
|
94
|
+
return this.tagNodes.get(result[0] || result).properties.get(name);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
isEmpty(range) {
|
|
98
|
+
const { path, parent } = this;
|
|
99
|
+
|
|
100
|
+
if (range[0]?.type === 'OpenNodeTag' && path !== parent.path) {
|
|
101
|
+
const nextTag = this.nextTerminals.get(range[0]);
|
|
102
|
+
if (!nextTag || nextTag.type === 'CloseNodeTag') {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
return range[0] === range[1];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
allTerminalsFor(range) {
|
|
111
|
+
return allTerminalsFor(range, this.nextTerminals);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
allTerminalsReverseFor(range) {
|
|
115
|
+
return allTerminalsFor(range, this.prevTerminals);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ownTerminalsFor(range) {
|
|
119
|
+
return ownTerminalsFor(range, this.nextTerminals, this.tagNodes);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ownTerminalsReverseFor(range) {
|
|
123
|
+
return ownTerminalsFor(range, this.prevTerminals, this.tagNodes);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
nodeForTag(tag) {
|
|
127
|
+
return this.tagNodes.get(tag);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
sourceTextFor(range) {
|
|
131
|
+
return sourceTextFor(this.allTerminalsFor(range));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
buildRange(terminals) {
|
|
135
|
+
const { prevTerminals, nextTerminals } = this;
|
|
136
|
+
|
|
137
|
+
let start, end;
|
|
138
|
+
for (const terminal of terminals) {
|
|
139
|
+
if (prevTerminals.has(terminal) || nextTerminals.has(terminal)) {
|
|
140
|
+
throw new Error('buildRange must not overwrite linkages');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (end) {
|
|
144
|
+
prevTerminals.set(terminal, end);
|
|
145
|
+
nextTerminals.set(end, terminal);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
start = start || terminal;
|
|
149
|
+
end = terminal || end;
|
|
150
|
+
}
|
|
151
|
+
return start ? [start, end] : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
unbox(value) {
|
|
155
|
+
const { unboxedValues } = this;
|
|
156
|
+
if (!unboxedValues.has(value)) {
|
|
157
|
+
unboxedValues.set(value, reifyExpressionShallow(value));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return unboxedValues.get(value);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
reifyExpression(value) {
|
|
164
|
+
return reifyExpression(value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getCooked(range) {
|
|
168
|
+
return getCooked(this.ownTerminalsFor(range));
|
|
169
|
+
}
|
|
170
|
+
};
|
package/lib/evaluate.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { Coroutine } from '@bablr/coroutine';
|
|
2
|
+
import {
|
|
3
|
+
buildNull,
|
|
4
|
+
buildGap,
|
|
5
|
+
buildReference,
|
|
6
|
+
buildLiteral,
|
|
7
|
+
buildDoctypeTag,
|
|
8
|
+
buildNodeOpenTag,
|
|
9
|
+
buildFragmentOpenTag,
|
|
10
|
+
buildFragmentCloseTag,
|
|
11
|
+
buildNodeCloseTag,
|
|
12
|
+
} from '@bablr/agast-helpers/builders';
|
|
13
|
+
import { StreamIterable, getStreamIterator } from '@bablr/agast-helpers/stream';
|
|
14
|
+
import { printExpression } from '@bablr/agast-helpers/print';
|
|
15
|
+
import { reifyExpression } from '@bablr/agast-vm-helpers';
|
|
16
|
+
import { Path, Node } from './path.js';
|
|
17
|
+
import { State } from './state.js';
|
|
18
|
+
import { facades } from './facades.js';
|
|
19
|
+
|
|
20
|
+
export const evaluate = (ctx, strategy) => new StreamIterable(__evaluate(ctx, strategy));
|
|
21
|
+
|
|
22
|
+
const __evaluate = function* agastStrategy(ctx, strategy) {
|
|
23
|
+
let s = State.from(ctx);
|
|
24
|
+
|
|
25
|
+
const co = new Coroutine(getStreamIterator(strategy(facades.get(ctx), facades.get(s))));
|
|
26
|
+
|
|
27
|
+
co.advance();
|
|
28
|
+
|
|
29
|
+
for (;;) {
|
|
30
|
+
if (co.current instanceof Promise) {
|
|
31
|
+
co.current = yield co.current;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (co.done) break;
|
|
35
|
+
|
|
36
|
+
const sourceInstr = co.value;
|
|
37
|
+
const instr = reifyExpression(sourceInstr);
|
|
38
|
+
let returnValue = undefined;
|
|
39
|
+
|
|
40
|
+
const { verb } = instr;
|
|
41
|
+
|
|
42
|
+
switch (verb) {
|
|
43
|
+
case 'branch': {
|
|
44
|
+
s = s.branch();
|
|
45
|
+
|
|
46
|
+
returnValue = facades.get(s);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case 'accept': {
|
|
51
|
+
s = s.accept();
|
|
52
|
+
|
|
53
|
+
if (s.depth === 0) {
|
|
54
|
+
yield* s.emit();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
returnValue = facades.get(s);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case 'reject': {
|
|
62
|
+
const rejectedState = s;
|
|
63
|
+
|
|
64
|
+
s = s.reject();
|
|
65
|
+
|
|
66
|
+
if (rejectedState.path.depth > s.path.depth) {
|
|
67
|
+
const lowPath = rejectedState.path.at(
|
|
68
|
+
Math.min(s.path.depth + 1, rejectedState.path.depth),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const { name, isArray } = lowPath.reference?.value || {};
|
|
72
|
+
|
|
73
|
+
if (!s.node.resolver.counters.has(name) && !lowPath.openTag?.value.flags.trivia) {
|
|
74
|
+
yield* s.emit(buildReference(name, isArray));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (s.result.type === 'Reference') {
|
|
78
|
+
yield* s.emit(buildNull());
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
returnValue = facades.get(s);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case 'advance': {
|
|
87
|
+
const { arguments: { 0: terminal } = [] } = instr;
|
|
88
|
+
|
|
89
|
+
switch (terminal?.type || 'Null') {
|
|
90
|
+
case 'DoctypeTag': {
|
|
91
|
+
const { attributes } = terminal.value;
|
|
92
|
+
const doctypeTag = buildDoctypeTag(attributes);
|
|
93
|
+
const rootPath = Path.from(ctx);
|
|
94
|
+
|
|
95
|
+
if (s.path) {
|
|
96
|
+
throw new Error();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
s.path = rootPath;
|
|
100
|
+
|
|
101
|
+
yield* s.emit(doctypeTag);
|
|
102
|
+
|
|
103
|
+
returnValue = doctypeTag;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'Literal': {
|
|
108
|
+
const literal = buildLiteral(terminal.value);
|
|
109
|
+
|
|
110
|
+
if (!s.node.flags.token) {
|
|
111
|
+
throw new Error('literals must occur inside tokens');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (s.held) {
|
|
115
|
+
throw new Error('Cannot consume input while hold register is full');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
yield* s.emit(literal);
|
|
119
|
+
|
|
120
|
+
returnValue = literal;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case 'Reference': {
|
|
125
|
+
const { name, isArray } = terminal.value;
|
|
126
|
+
|
|
127
|
+
const tag = buildReference(name, isArray);
|
|
128
|
+
|
|
129
|
+
if (s.result.type === 'Reference') {
|
|
130
|
+
throw new Error('double reference');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!s.path.depth) {
|
|
134
|
+
throw new Error();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (s.node.flags.token) {
|
|
138
|
+
throw new Error();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
s.node.resolver.consume(tag);
|
|
142
|
+
|
|
143
|
+
s.path = s.path.push(ctx, tag);
|
|
144
|
+
|
|
145
|
+
yield* s.emit(tag);
|
|
146
|
+
|
|
147
|
+
returnValue = tag;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case 'Gap': {
|
|
152
|
+
const reference = s.result;
|
|
153
|
+
|
|
154
|
+
if (reference?.type !== 'Reference') throw new Error();
|
|
155
|
+
|
|
156
|
+
s.path = s.path.parent;
|
|
157
|
+
|
|
158
|
+
const gapTag = buildGap();
|
|
159
|
+
|
|
160
|
+
yield* s.emit(gapTag);
|
|
161
|
+
|
|
162
|
+
returnValue = gapTag;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case 'Null': {
|
|
167
|
+
const reference = s.result;
|
|
168
|
+
|
|
169
|
+
if (reference?.type !== 'Reference') throw new Error();
|
|
170
|
+
|
|
171
|
+
s.path = s.path.parent;
|
|
172
|
+
|
|
173
|
+
const null_ = buildNull();
|
|
174
|
+
|
|
175
|
+
yield* s.emit(null_);
|
|
176
|
+
|
|
177
|
+
returnValue = null_;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case 'OpenFragmentTag': {
|
|
182
|
+
const openTag = buildFragmentOpenTag();
|
|
183
|
+
|
|
184
|
+
s.node = Node.from(s.path, openTag);
|
|
185
|
+
|
|
186
|
+
yield* s.emit(openTag);
|
|
187
|
+
|
|
188
|
+
returnValue = openTag;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case 'CloseFragmentTag': {
|
|
193
|
+
const closeTag = buildFragmentCloseTag();
|
|
194
|
+
|
|
195
|
+
yield* s.emit(closeTag);
|
|
196
|
+
|
|
197
|
+
returnValue = closeTag;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case 'OpenNodeTag': {
|
|
202
|
+
const { flags, language, type, intrinsicValue, attributes } = terminal.value;
|
|
203
|
+
const reference = s.result;
|
|
204
|
+
const boundAttributes = Object.entries(attributes).filter((a) => a[1].type !== 'Gap');
|
|
205
|
+
const unboundAttributes = Object.entries(attributes).filter((a) => a[1].type === 'Gap');
|
|
206
|
+
const openTag = buildNodeOpenTag(
|
|
207
|
+
flags,
|
|
208
|
+
language,
|
|
209
|
+
type,
|
|
210
|
+
intrinsicValue,
|
|
211
|
+
Object.fromEntries(boundAttributes),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (!flags.trivia && !flags.escape && s.path.depth > 1) {
|
|
215
|
+
if (reference.type !== 'Reference' && reference.type !== 'OpenFragmentTag') {
|
|
216
|
+
throw new Error('Invalid location for OpenNodeTag');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!flags.expression) {
|
|
221
|
+
const { flags } = openTag.value;
|
|
222
|
+
|
|
223
|
+
if (!(flags.trivia || flags.escape) && !s.path.depth) {
|
|
224
|
+
const tag = buildReference('root', false);
|
|
225
|
+
s.path = s.path.push(ctx, tag);
|
|
226
|
+
s.node.resolver.consume(tag);
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
s.expressionDepth++;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const newNode = s.node.push(s.path, openTag);
|
|
233
|
+
|
|
234
|
+
newNode.unboundAttributes = new Set(unboundAttributes.map((e) => e[0]));
|
|
235
|
+
|
|
236
|
+
ctx.tagNodes.set(openTag, newNode);
|
|
237
|
+
|
|
238
|
+
if (!flags.intrinsic) {
|
|
239
|
+
s.node = newNode;
|
|
240
|
+
} else {
|
|
241
|
+
s.path = s.path.parent;
|
|
242
|
+
|
|
243
|
+
if (s.path.depth > 1) {
|
|
244
|
+
const { properties } = s.node;
|
|
245
|
+
const { name: refName, isArray } = reference.value;
|
|
246
|
+
|
|
247
|
+
if (!isArray) {
|
|
248
|
+
properties.set(refName, [openTag, openTag]);
|
|
249
|
+
} else {
|
|
250
|
+
if (!properties.has(refName)) {
|
|
251
|
+
properties.set(refName, []);
|
|
252
|
+
}
|
|
253
|
+
properties.get(refName).push([openTag, openTag]);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
yield* s.emit(openTag);
|
|
259
|
+
|
|
260
|
+
returnValue = openTag;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case 'CloseNodeTag': {
|
|
265
|
+
const { type, language } = terminal.value;
|
|
266
|
+
const { openTag } = s.node;
|
|
267
|
+
const { flags, type: openType } = openTag.value;
|
|
268
|
+
|
|
269
|
+
if (s.node.unboundAttributes?.size)
|
|
270
|
+
throw new Error('Grammar failed to bind all attributes');
|
|
271
|
+
|
|
272
|
+
if (!type) throw new Error(`CloseNodeTag must have type`);
|
|
273
|
+
|
|
274
|
+
if (s.path.depth > 1 && type !== openType)
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Grammar close {type: ${type}} did not match open {type: ${openType}}`,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const closeTag = buildNodeCloseTag(type, language);
|
|
280
|
+
|
|
281
|
+
if (!flags.escape && !flags.trivia) {
|
|
282
|
+
const { name: refName, isArray } = s.path.reference.value;
|
|
283
|
+
|
|
284
|
+
if (s.path.depth > 2) {
|
|
285
|
+
const { properties } = s.node.parent;
|
|
286
|
+
|
|
287
|
+
if (!isArray) {
|
|
288
|
+
properties.set(refName, [openTag, closeTag]);
|
|
289
|
+
} else {
|
|
290
|
+
if (!properties.has(refName)) {
|
|
291
|
+
properties.set(refName, []);
|
|
292
|
+
}
|
|
293
|
+
properties.get(refName).push([openTag, closeTag]);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (flags.expression) {
|
|
299
|
+
s.expressionDepth--;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
ctx.tagNodes.set(closeTag, s.node);
|
|
303
|
+
|
|
304
|
+
s.node.closeTag = closeTag;
|
|
305
|
+
|
|
306
|
+
s.node = s.node.parent;
|
|
307
|
+
|
|
308
|
+
if (!(flags.trivia || flags.escape)) {
|
|
309
|
+
s.path = s.path.parent;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
yield* s.emit(closeTag, flags.expression);
|
|
313
|
+
|
|
314
|
+
returnValue = closeTag;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
default:
|
|
319
|
+
throw new Error();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
case 'shift': {
|
|
326
|
+
s.shift();
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
case 'unshift': {
|
|
331
|
+
s.unshift();
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
case 'bindAttribute': {
|
|
336
|
+
const { arguments: { 0: key, 1: value } = [] } = instr;
|
|
337
|
+
|
|
338
|
+
const { unboundAttributes } = s.node;
|
|
339
|
+
|
|
340
|
+
if (!unboundAttributes || !unboundAttributes.has(key)) {
|
|
341
|
+
throw new Error('No unbound attribute to bind');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (s.node.openTag.type === 'OpenFragmentTag') {
|
|
345
|
+
throw new Error();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (key === 'span') throw new Error('too late');
|
|
349
|
+
|
|
350
|
+
if (key === 'balancedSpan') {
|
|
351
|
+
throw new Error('not implemented');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// if (stateIsDifferent) {
|
|
355
|
+
// // we can't allow effects to cross state branches
|
|
356
|
+
// throw new Error();
|
|
357
|
+
// }
|
|
358
|
+
|
|
359
|
+
unboundAttributes.delete(key);
|
|
360
|
+
|
|
361
|
+
const { openTag } = s.node;
|
|
362
|
+
|
|
363
|
+
if (value != null) {
|
|
364
|
+
const { flags, language, type, intrinsicValue } = openTag.value;
|
|
365
|
+
const attributes = { ...openTag.value.attributes, [key]: value };
|
|
366
|
+
const newStartTag = buildNodeOpenTag(flags, language, type, intrinsicValue, attributes);
|
|
367
|
+
|
|
368
|
+
let startNext = ctx.nextTerminals.get(openTag);
|
|
369
|
+
let startPrev = ctx.prevTerminals.get(openTag);
|
|
370
|
+
|
|
371
|
+
ctx.prevTerminals.set(newStartTag, startPrev);
|
|
372
|
+
ctx.nextTerminals.set(startPrev, newStartTag);
|
|
373
|
+
|
|
374
|
+
ctx.tagNodes.set(newStartTag, s.node);
|
|
375
|
+
|
|
376
|
+
if (startNext) {
|
|
377
|
+
ctx.nextTerminals.set(newStartTag, startNext);
|
|
378
|
+
ctx.prevTerminals.set(startNext, newStartTag);
|
|
379
|
+
} else {
|
|
380
|
+
// could this terminal be stored anywhere else?
|
|
381
|
+
s.result = newStartTag;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
s.node.openTag = newStartTag;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!unboundAttributes.size) {
|
|
388
|
+
yield* s.emit();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
returnValue = s.node.openTag;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
case 'getState': {
|
|
396
|
+
returnValue = facades.get(s);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
case 'getContext': {
|
|
401
|
+
returnValue = facades.get(ctx);
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
default: {
|
|
406
|
+
throw new Error(`Unexpected call of {type: ${printExpression(verb)}}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
co.advance(returnValue);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
s.path = s.path.parent;
|
|
414
|
+
|
|
415
|
+
if (s.depth > 0) {
|
|
416
|
+
throw new Error('Did not unwind state stack');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (s.path?.depth > 0) {
|
|
420
|
+
throw new Error('Did not unwind path stack');
|
|
421
|
+
}
|
|
422
|
+
};
|
package/lib/facades.js
ADDED
package/lib/index.js
ADDED
package/lib/path.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { WeakStackFrame } from '@bablr/weak-stack';
|
|
2
|
+
import { Resolver } from '@bablr/agast-helpers/tree';
|
|
3
|
+
import { findRight } from './utils/array.js';
|
|
4
|
+
import { facades, actuals } from './facades.js';
|
|
5
|
+
|
|
6
|
+
const skipLevels = 3;
|
|
7
|
+
const skipShiftExponentGrowth = 4;
|
|
8
|
+
const skipAmounts = new Array(skipLevels)
|
|
9
|
+
.fill(null)
|
|
10
|
+
.map((_, i) => 2 >> (i * skipShiftExponentGrowth));
|
|
11
|
+
const skipsByPath = new WeakMap();
|
|
12
|
+
|
|
13
|
+
export const NodeFacade = class AgastNodeFacade {
|
|
14
|
+
constructor(path) {
|
|
15
|
+
facades.set(path, this);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get language() {
|
|
19
|
+
return actuals.get(this).language;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get type() {
|
|
23
|
+
return actuals.get(this).type;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get path() {
|
|
27
|
+
return actuals.get(this).path;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get parent() {
|
|
31
|
+
return facades.get(actuals.get(this).parent);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get range() {
|
|
35
|
+
return actuals.get(this).range;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get openTag() {
|
|
39
|
+
return actuals.get(this).openTag;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get closeTag() {
|
|
43
|
+
return actuals.get(this).closeTag;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get depth() {
|
|
47
|
+
return actuals.get(this).depth;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get flags() {
|
|
51
|
+
return actuals.get(this).flags;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get attributes() {
|
|
55
|
+
return actuals.get(this).attributes;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const PathFacade = class AgastPathFacade {
|
|
60
|
+
constructor(path) {
|
|
61
|
+
facades.set(path, this);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get reference() {
|
|
65
|
+
return actuals.get(this).reference;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get parent() {
|
|
69
|
+
return facades.get(actuals.get(this).parent);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get depth() {
|
|
73
|
+
return actuals.get(this).depth;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
at(depth) {
|
|
77
|
+
let parent = this;
|
|
78
|
+
|
|
79
|
+
if (depth > this.depth) throw new Error();
|
|
80
|
+
|
|
81
|
+
let d = actuals.get(parent).depth;
|
|
82
|
+
for (; d > depth; ) {
|
|
83
|
+
const skips = skipsByPath.get(actuals.get(this));
|
|
84
|
+
parent = (skips && findRight(skips, (skip) => d - skip > depth)) || parent.parent;
|
|
85
|
+
d = actuals.get(parent).depth;
|
|
86
|
+
}
|
|
87
|
+
return parent;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
*parents(includeSelf = false) {
|
|
91
|
+
if (includeSelf) yield this;
|
|
92
|
+
let parent = this;
|
|
93
|
+
while ((parent = parent.parent)) {
|
|
94
|
+
yield parent;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const Node = class AgastNode extends WeakStackFrame {
|
|
100
|
+
static from(path, openTag) {
|
|
101
|
+
return AgastNode.create(path, openTag);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
constructor(
|
|
105
|
+
path,
|
|
106
|
+
openTag,
|
|
107
|
+
closeTag = null,
|
|
108
|
+
properties = new Map(),
|
|
109
|
+
resolver = new Resolver(),
|
|
110
|
+
unboundAttributes = null,
|
|
111
|
+
) {
|
|
112
|
+
super();
|
|
113
|
+
|
|
114
|
+
this.path = path;
|
|
115
|
+
this.openTag = openTag;
|
|
116
|
+
this.closeTag = closeTag;
|
|
117
|
+
this.properties = properties;
|
|
118
|
+
this.resolver = resolver;
|
|
119
|
+
this.unboundAttributes = unboundAttributes;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get context() {
|
|
123
|
+
return this.path.context;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get language() {
|
|
127
|
+
return this.openTag.value?.language;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
get type() {
|
|
131
|
+
return this.openTag.value?.type || Symbol.for('@bablr/fragment');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get flags() {
|
|
135
|
+
return this.openTag.value?.flags || {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get attributes() {
|
|
139
|
+
return this.openTag.value?.attributes || {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
branch() {
|
|
143
|
+
const { path, openTag, closeTag, properties, resolver, unboundAttributes } = this;
|
|
144
|
+
|
|
145
|
+
return this.push(
|
|
146
|
+
path,
|
|
147
|
+
openTag,
|
|
148
|
+
closeTag,
|
|
149
|
+
new Map(properties), // there is probably a better way
|
|
150
|
+
resolver.branch(),
|
|
151
|
+
new Set(unboundAttributes),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
accept(node) {
|
|
156
|
+
this.openTag = node.openTag;
|
|
157
|
+
this.closeTag = node.closeTag;
|
|
158
|
+
this.properties = node.properties;
|
|
159
|
+
this.unboundAttributes = node.unboundAttributes;
|
|
160
|
+
|
|
161
|
+
this.resolver.accept(node.resolver);
|
|
162
|
+
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const Path = class AgastPath extends WeakStackFrame {
|
|
168
|
+
static from(context, tag) {
|
|
169
|
+
return Path.create(context, tag);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
constructor(context, reference) {
|
|
173
|
+
super();
|
|
174
|
+
|
|
175
|
+
if (reference && reference.type !== 'Reference') {
|
|
176
|
+
throw new Error('Invalid reference for path');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.context = context;
|
|
180
|
+
this.reference = reference;
|
|
181
|
+
|
|
182
|
+
let skipIdx = 0;
|
|
183
|
+
let skipAmount = skipAmounts[skipIdx];
|
|
184
|
+
let skips;
|
|
185
|
+
while ((this.depth & skipAmount) === skipAmount) {
|
|
186
|
+
if (!skips) {
|
|
187
|
+
skips = [];
|
|
188
|
+
skipsByPath.set(this, skips);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
skips[skipIdx] = this.at(this.depth - skipAmount);
|
|
192
|
+
|
|
193
|
+
skipIdx++;
|
|
194
|
+
skipAmount = skipAmounts[skipIdx];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
new PathFacade(this);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
at(depth) {
|
|
201
|
+
let parent = this;
|
|
202
|
+
|
|
203
|
+
if (depth > this.depth) throw new Error();
|
|
204
|
+
|
|
205
|
+
let d = this.depth;
|
|
206
|
+
for (; d > depth; ) {
|
|
207
|
+
const skips = skipsByPath.get(this);
|
|
208
|
+
parent = (skips && findRight(skips, (skip) => d - skip > depth)) || parent.parent;
|
|
209
|
+
d = parent.depth;
|
|
210
|
+
}
|
|
211
|
+
return parent;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
*parents(includeSelf = false) {
|
|
215
|
+
if (includeSelf) yield this;
|
|
216
|
+
let parent = this;
|
|
217
|
+
while ((parent = parent.parent)) {
|
|
218
|
+
yield parent;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
package/lib/state.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { WeakStackFrame } from '@bablr/weak-stack';
|
|
2
|
+
import { startsDocument } from '@bablr/agast-helpers/stream';
|
|
3
|
+
import { facades, actuals } from './facades.js';
|
|
4
|
+
|
|
5
|
+
export const StateFacade = class AgastStateFacade {
|
|
6
|
+
constructor(state) {
|
|
7
|
+
facades.set(state, this);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static from(context) {
|
|
11
|
+
return State.from(actuals.get(context));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get result() {
|
|
15
|
+
return actuals.get(this).result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get context() {
|
|
19
|
+
return facades.get(actuals.get(this).context);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get path() {
|
|
23
|
+
return facades.get(actuals.get(this).path);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get node() {
|
|
27
|
+
return actuals.get(this).node;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get holding() {
|
|
31
|
+
return actuals.get(this).holding;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get depth() {
|
|
35
|
+
return actuals.get(this).depth;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get ctx() {
|
|
39
|
+
return this.context;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const State = class AgastState extends WeakStackFrame {
|
|
44
|
+
constructor(
|
|
45
|
+
context,
|
|
46
|
+
path = null,
|
|
47
|
+
node = null,
|
|
48
|
+
result = null,
|
|
49
|
+
emitted = null,
|
|
50
|
+
held = null,
|
|
51
|
+
expressionDepth = 0,
|
|
52
|
+
) {
|
|
53
|
+
super();
|
|
54
|
+
|
|
55
|
+
if (!context) throw new Error('invalid args to tagState');
|
|
56
|
+
|
|
57
|
+
this.context = context;
|
|
58
|
+
this.path = path;
|
|
59
|
+
this.node = node;
|
|
60
|
+
this.result = result;
|
|
61
|
+
this.emitted = emitted;
|
|
62
|
+
this.held = held;
|
|
63
|
+
this.expressionDepth = expressionDepth;
|
|
64
|
+
|
|
65
|
+
new StateFacade(this);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static from(context) {
|
|
69
|
+
return State.create(context, null);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get holding() {
|
|
73
|
+
return !!this.held;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
shift() {
|
|
77
|
+
const { tagNodes, prevTerminals, nextTerminals } = this.context;
|
|
78
|
+
|
|
79
|
+
const finishedNode = tagNodes.get(this.result);
|
|
80
|
+
|
|
81
|
+
if (!finishedNode.openTag.value.flags.expression) {
|
|
82
|
+
throw new Error();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.result = prevTerminals.get(finishedNode.openTag);
|
|
86
|
+
|
|
87
|
+
nextTerminals.delete(this.result);
|
|
88
|
+
|
|
89
|
+
this.held = finishedNode;
|
|
90
|
+
|
|
91
|
+
// put the first expression node into the holding register
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
unshift() {
|
|
95
|
+
const { tagNodes, prevTerminals, nextTerminals } = this.context;
|
|
96
|
+
|
|
97
|
+
if (!this.held) {
|
|
98
|
+
throw new Error('cannot unshift when no expression is in the holding register');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
nextTerminals.set(this.result, this.held.openTag);
|
|
102
|
+
prevTerminals.set(this.held.openTag, this.result);
|
|
103
|
+
|
|
104
|
+
this.result = this.held.closeTag;
|
|
105
|
+
|
|
106
|
+
this.held = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
*emit(terminal, suppressEmit) {
|
|
110
|
+
const { prevTerminals, nextTerminals, tagNodes } = this.context;
|
|
111
|
+
|
|
112
|
+
if (terminal) {
|
|
113
|
+
if (prevTerminals.has(terminal)) {
|
|
114
|
+
throw new Error('Double emit');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
this.result?.type === 'Reference' &&
|
|
119
|
+
!['OpenNodeTag', 'Gap', 'Null'].includes(terminal.type)
|
|
120
|
+
) {
|
|
121
|
+
throw new Error('Bad reference emit');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
prevTerminals.set(terminal, this.result);
|
|
125
|
+
if (this.result) {
|
|
126
|
+
nextTerminals.set(this.result, terminal);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.result = terminal;
|
|
130
|
+
|
|
131
|
+
if (!this.emitted) {
|
|
132
|
+
if (!startsDocument(terminal)) throw new Error();
|
|
133
|
+
this.emitted = terminal;
|
|
134
|
+
yield terminal;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!this.depth && !this.expressionDepth && !suppressEmit) {
|
|
139
|
+
let emittable = nextTerminals.get(this.emitted);
|
|
140
|
+
|
|
141
|
+
while (
|
|
142
|
+
emittable &&
|
|
143
|
+
!(emittable.type === 'OpenNodeTag' && tagNodes.get(emittable).unboundAttributes?.size)
|
|
144
|
+
) {
|
|
145
|
+
yield emittable;
|
|
146
|
+
this.emitted = emittable;
|
|
147
|
+
emittable = nextTerminals.get(this.emitted);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return terminal;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get ctx() {
|
|
155
|
+
return this.context;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get isGap() {
|
|
159
|
+
return this.tag.type === 'NodeGapTag';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get speculative() {
|
|
163
|
+
return !!this.parent;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
branch() {
|
|
167
|
+
const { context, path, node, result, emitted, held, expressionDepth } = this;
|
|
168
|
+
|
|
169
|
+
return this.push(context, path, node.branch(), result, emitted, held, expressionDepth);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
accept() {
|
|
173
|
+
const { parent } = this;
|
|
174
|
+
|
|
175
|
+
if (!parent) {
|
|
176
|
+
throw new Error('accepted the root state');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
parent.node.accept(this.node);
|
|
180
|
+
|
|
181
|
+
// emitted isn't used here and probably doesn't need to be part of state
|
|
182
|
+
|
|
183
|
+
parent.result = this.result;
|
|
184
|
+
parent.held = this.held;
|
|
185
|
+
parent.expressionDepth = this.expressionDepth;
|
|
186
|
+
|
|
187
|
+
return parent;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
reject() {
|
|
191
|
+
const { parent, context } = this;
|
|
192
|
+
|
|
193
|
+
if (!parent) throw new Error('rejected root state');
|
|
194
|
+
|
|
195
|
+
context.nextTerminals.delete(parent.result);
|
|
196
|
+
|
|
197
|
+
return parent;
|
|
198
|
+
}
|
|
199
|
+
};
|
package/lib/symbols.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const unbound = Symbol.for('@bablr/unbound');
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bablr/agast-vm",
|
|
3
|
+
"description": "A VM providing DOM-like guarantees about agAST trees",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"author": "Conrad Buck<conartist6@gmail.com>",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node ./test/runner.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@bablr/agast-helpers": "0.1.0",
|
|
19
|
+
"@bablr/agast-vm-helpers": "0.1.0",
|
|
20
|
+
"@bablr/coroutine": "0.1.0",
|
|
21
|
+
"@bablr/weak-stack": "0.1.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@bablr/agast-vm-strategy-passthrough": "github:bablr-lang/agast-vm-strategy-passthrough#2bd3a0c7311037af92c5b81941c79161499f6c9e",
|
|
25
|
+
"@bablr/eslint-config-base": "github:bablr-lang/eslint-config-base#49f5952efed27f94ee9b94340eb1563c440bf64e",
|
|
26
|
+
"@bablr/strategy_enhancer-debug-log": "0.1.0",
|
|
27
|
+
"enhanced-resolve": "^5.12.0",
|
|
28
|
+
"eslint": "^8.32.0",
|
|
29
|
+
"eslint-import-resolver-enhanced-resolve": "^1.0.5",
|
|
30
|
+
"eslint-plugin-import": "^2.27.5",
|
|
31
|
+
"expect": "29.7.0",
|
|
32
|
+
"iter-tools-es": "^7.3.1",
|
|
33
|
+
"prettier": "^2.6.2"
|
|
34
|
+
},
|
|
35
|
+
"repository": "github:bablr-lang/agast-vm",
|
|
36
|
+
"homepage": "https://github.com/bablr-lang/agast-vm",
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|