@cotal-ai/lang 0.24.0 → 0.26.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 +11 -0
- package/dist/engine/bridge.d.ts +71 -0
- package/dist/engine/bridge.d.ts.map +1 -0
- package/dist/engine/bridge.js +277 -0
- package/dist/engine/bridge.js.map +1 -0
- package/dist/engine/ctx.d.ts +140 -0
- package/dist/engine/ctx.d.ts.map +1 -0
- package/dist/engine/ctx.js +834 -0
- package/dist/engine/ctx.js.map +1 -0
- package/dist/engine/frame.d.ts +69 -0
- package/dist/engine/frame.d.ts.map +1 -0
- package/dist/engine/frame.js +105 -0
- package/dist/engine/frame.js.map +1 -0
- package/dist/engine/host.d.ts +77 -0
- package/dist/engine/host.d.ts.map +1 -0
- package/dist/engine/host.js +134 -0
- package/dist/engine/host.js.map +1 -0
- package/dist/engine/worker-entry.d.ts +26 -0
- package/dist/engine/worker-entry.d.ts.map +1 -0
- package/dist/engine/worker-entry.js +175 -0
- package/dist/engine/worker-entry.js.map +1 -0
- package/dist/engine/worker.d.ts +156 -0
- package/dist/engine/worker.d.ts.map +1 -0
- package/dist/engine/worker.js +123 -0
- package/dist/engine/worker.js.map +1 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +97 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/interpret.d.ts +7 -38
- package/dist/interpret.d.ts.map +1 -1
- package/dist/interpret.js +45 -1003
- package/dist/interpret.js.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +75 -1
- package/dist/journal.js.map +1 -1
- package/dist/library.d.ts.map +1 -1
- package/dist/library.js +13 -1
- package/dist/library.js.map +1 -1
- package/dist/perform.d.ts +138 -0
- package/dist/perform.d.ts.map +1 -0
- package/dist/perform.js +1052 -0
- package/dist/perform.js.map +1 -0
- package/dist/pins.d.ts +28 -8
- package/dist/pins.d.ts.map +1 -1
- package/dist/pins.js +31 -11
- package/dist/pins.js.map +1 -1
- package/dist/sim.d.ts +15 -1
- package/dist/sim.d.ts.map +1 -1
- package/dist/sim.js.map +1 -1
- package/dist/transform/emit.d.ts +23 -0
- package/dist/transform/emit.d.ts.map +1 -0
- package/dist/transform/emit.js +934 -0
- package/dist/transform/emit.js.map +1 -0
- package/dist/transform/index.d.ts +34 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +31 -0
- package/dist/transform/index.js.map +1 -0
- package/dist/transform/scope.d.ts +58 -0
- package/dist/transform/scope.d.ts.map +1 -0
- package/dist/transform/scope.js +500 -0
- package/dist/transform/scope.js.map +1 -0
- package/dist/transform/seam.d.ts +78 -0
- package/dist/transform/seam.d.ts.map +1 -0
- package/dist/transform/seam.js +111 -0
- package/dist/transform/seam.js.map +1 -0
- package/dist/values.d.ts +15 -0
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +79 -0
- package/dist/values.js.map +1 -1
- package/package.json +9 -4
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The emitter: one rule per admitted node type, and every rule that carries a law routes it through
|
|
3
|
+
* the seam rather than restating it.
|
|
4
|
+
*
|
|
5
|
+
* The walk order here is the walker's walk order, deliberately and line by line: a journal's step
|
|
6
|
+
* keys are occurrence counters allocated at the call, so two engines agree on a journal only if they
|
|
7
|
+
* agree on the ORDER effects are reached in. Where this file departs from the shape of
|
|
8
|
+
* `interpret.ts` it is because native JavaScript already has the meaning (block scoping, `switch`
|
|
9
|
+
* selection, `try`/`finally` completions) and re-deriving it would be a second implementation to
|
|
10
|
+
* keep in step.
|
|
11
|
+
*/
|
|
12
|
+
import { BUILTINS, EVENT_CONSTRUCTORS, PRIMITIVES, PURE_PRIMITIVES, VALUE_NAMES } from "../primitives.js";
|
|
13
|
+
import { analyze } from "./scope.js";
|
|
14
|
+
import { stripPositions } from "../interpret.js";
|
|
15
|
+
import { pickNames } from "./seam.js";
|
|
16
|
+
/** Free names that are VALUES as well as callees: the walker declares each as a binding (`installGlobals`). */
|
|
17
|
+
const FREE_VALUES = new Set([
|
|
18
|
+
...BUILTINS,
|
|
19
|
+
...Object.keys(EVENT_CONSTRUCTORS),
|
|
20
|
+
...Object.keys(PURE_PRIMITIVES),
|
|
21
|
+
]);
|
|
22
|
+
const q = (s) => JSON.stringify(s);
|
|
23
|
+
/** `undefined` is a GLOBAL binding; the emitted module has no unbound references, so it spells the value. */
|
|
24
|
+
const VOID = "void 0";
|
|
25
|
+
/** Every identifier the source spells, so the emitter's own names can be picked around them. */
|
|
26
|
+
function identifiers(node, out) {
|
|
27
|
+
if (Array.isArray(node)) {
|
|
28
|
+
for (const n of node)
|
|
29
|
+
identifiers(n, out);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (node === null || typeof node !== "object")
|
|
33
|
+
return;
|
|
34
|
+
const n = node;
|
|
35
|
+
if (n.type === "Identifier" && typeof n.name === "string")
|
|
36
|
+
out.add(n.name);
|
|
37
|
+
for (const [k, v] of Object.entries(n)) {
|
|
38
|
+
if (k === "start" || k === "end" || k === "loc" || k === "range")
|
|
39
|
+
continue;
|
|
40
|
+
identifiers(v, out);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Every Identifier a declaration pattern binds, in source order. */
|
|
44
|
+
function declaredNames(pattern, out = []) {
|
|
45
|
+
switch (pattern.type) {
|
|
46
|
+
case "Identifier":
|
|
47
|
+
out.push(pattern);
|
|
48
|
+
break;
|
|
49
|
+
case "AssignmentPattern":
|
|
50
|
+
declaredNames(pattern.left, out);
|
|
51
|
+
break;
|
|
52
|
+
case "RestElement":
|
|
53
|
+
declaredNames(pattern.argument, out);
|
|
54
|
+
break;
|
|
55
|
+
case "ObjectPattern":
|
|
56
|
+
for (const p of pattern.properties ?? []) {
|
|
57
|
+
declaredNames((p.type === "RestElement" ? p.argument : p.value), out);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
case "ArrayPattern":
|
|
61
|
+
for (const el of pattern.elements ?? [])
|
|
62
|
+
if (el !== null && el !== undefined)
|
|
63
|
+
declaredNames(el, out);
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
class Emitter {
|
|
71
|
+
a;
|
|
72
|
+
n;
|
|
73
|
+
sites = new Map();
|
|
74
|
+
proposed = new Set();
|
|
75
|
+
tempTop = 0;
|
|
76
|
+
tempMax = 0;
|
|
77
|
+
labels = 0;
|
|
78
|
+
conts = 0;
|
|
79
|
+
/** Cells whose record was created at the top of a block, so the declaration only writes into it. */
|
|
80
|
+
hoisted = new Set();
|
|
81
|
+
/** The innermost loop's break and continue labels, and the innermost switch's break label. */
|
|
82
|
+
breakTo = null;
|
|
83
|
+
continueTo = null;
|
|
84
|
+
constructor(a, n) {
|
|
85
|
+
this.a = a;
|
|
86
|
+
this.n = n;
|
|
87
|
+
}
|
|
88
|
+
// ---- bookkeeping ------------------------------------------------------------------------------
|
|
89
|
+
seam(member, args) {
|
|
90
|
+
this.sites.set(member, (this.sites.get(member) ?? 0) + 1);
|
|
91
|
+
return `${this.n.ctx}.${member}(${args})`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Reach a member that is SURFACED but not yet in the contract. Empty once `callee` was granted as
|
|
95
|
+
* member 14, and kept because it is the only route into `TransformMeta.proposed`: a member added
|
|
96
|
+
* here shows up as measured debt, where one added straight to {@link seam} is caught instead by
|
|
97
|
+
* the suite's unruled-member check, loudly, but after the fact.
|
|
98
|
+
*/
|
|
99
|
+
propose(member, args) {
|
|
100
|
+
this.proposed.add(member);
|
|
101
|
+
return this.seam(member, args);
|
|
102
|
+
}
|
|
103
|
+
temp() {
|
|
104
|
+
const name = `${this.n.temp}${this.tempTop}`;
|
|
105
|
+
this.tempTop += 1;
|
|
106
|
+
if (this.tempTop > this.tempMax)
|
|
107
|
+
this.tempMax = this.tempTop;
|
|
108
|
+
return name;
|
|
109
|
+
}
|
|
110
|
+
/** A temp's life is its own expression, so a sibling may reuse the slot; a parent's stays live. */
|
|
111
|
+
scoped(fn) {
|
|
112
|
+
const save = this.tempTop;
|
|
113
|
+
const r = fn();
|
|
114
|
+
this.tempTop = save;
|
|
115
|
+
return r;
|
|
116
|
+
}
|
|
117
|
+
label(kind) {
|
|
118
|
+
this.labels += 1;
|
|
119
|
+
return `${this.n.label}${kind}${this.labels}`;
|
|
120
|
+
}
|
|
121
|
+
// ---- names ------------------------------------------------------------------------------------
|
|
122
|
+
binding(node) {
|
|
123
|
+
return this.a.bindingOf.get(node);
|
|
124
|
+
}
|
|
125
|
+
/** Read a name: a native binding, a cell's field, or a free name. */
|
|
126
|
+
readName(node) {
|
|
127
|
+
const b = this.binding(node);
|
|
128
|
+
const name = node.name;
|
|
129
|
+
// A CELL READ CARRIES THE BINDING'S NAME (the dead-zone rule). An absent own `v` is a read
|
|
130
|
+
// before the declaration ran, and the host answers L2004 for the binding it names: the code the
|
|
131
|
+
// walker gives, and one a program can catch and read, where a native binding gives a host
|
|
132
|
+
// ReferenceError that `caught` can only report as L4000/host.
|
|
133
|
+
if (b !== undefined)
|
|
134
|
+
return b.cell ? this.seam("get", `${name}, ${q("v")}, ${q(b.name)}`) : name;
|
|
135
|
+
if (VALUE_NAMES.includes(name))
|
|
136
|
+
return VOID;
|
|
137
|
+
// A builtin is a BINDING in this language, so it has to be readable as a value: `const f = trim`,
|
|
138
|
+
// `map(xs, upper)`, and `json.stringify(x)` (whose callee's object is the free name `json`).
|
|
139
|
+
// The contract pins the read form as `free(name)` with the arguments OMITTED - the host answers
|
|
140
|
+
// it in the program's convention, so a native call on what comes back is correct.
|
|
141
|
+
if (FREE_VALUES.has(name))
|
|
142
|
+
return this.seam("free", q(name));
|
|
143
|
+
// An effect primitive is not a binding the walker ever declares: `const t = turn` is admitted by
|
|
144
|
+
// the validator and answers L2001 at run time (measured at 9dc154f8). The host answers the same
|
|
145
|
+
// way for a name that is not a free builtin, so the reference is emitted as the call that asks.
|
|
146
|
+
if (PRIMITIVES[name] !== undefined)
|
|
147
|
+
return `(await ${this.seam("free", `${q(name)}, []`)})`;
|
|
148
|
+
throw new Error(`transform: ${name} resolves to nothing, which a validated program cannot contain`);
|
|
149
|
+
}
|
|
150
|
+
// ---- the module -------------------------------------------------------------------------------
|
|
151
|
+
module(program) {
|
|
152
|
+
const body = this.block(program, true);
|
|
153
|
+
const decls = this.tempMax === 0 ? "" : `let ${Array.from({ length: this.tempMax }, (_, i) => `${this.n.temp}${i}`).join(", ")};\n`;
|
|
154
|
+
return `(${this.n.ctx}) => async () => {\n${decls}${this.fuel()}${body}}\n`;
|
|
155
|
+
}
|
|
156
|
+
fuel() {
|
|
157
|
+
return `await ${this.seam("fuel", "")};\n`;
|
|
158
|
+
}
|
|
159
|
+
// ---- statements -------------------------------------------------------------------------------
|
|
160
|
+
/** A block's statements. `bare` emits them without braces (the program body, a function body). */
|
|
161
|
+
block(node, bare = false) {
|
|
162
|
+
const stmts = node.body ?? [];
|
|
163
|
+
// THE CELLS FIRST: a closure written before the declaration can read the binding, so the record
|
|
164
|
+
// has to exist before that closure is made. See {@link hoistCells}.
|
|
165
|
+
const head = this.hoistCells(stmts);
|
|
166
|
+
const body = stmts.map((s) => this.stmt(s)).join("");
|
|
167
|
+
return bare ? head + body : `{\n${head}${body}}\n`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The cell RECORDS a statement list declares, created empty at its top.
|
|
171
|
+
*
|
|
172
|
+
* The dead-zone rule's shape: `born({})` at the top of the block, `set(cell, "v", init)` where
|
|
173
|
+
* the declaration is, and every read through `get(cell, "v", name)`. The record must exist before
|
|
174
|
+
* the closures that capture it, and its `v` must be ABSENT until the declaration runs. That
|
|
175
|
+
* absence IS the dead zone, and it is what lets the host answer L2004 by name instead of a
|
|
176
|
+
* native ReferenceError. `v: undefined` is a present key, so the host asks `hasOwn`, not truth.
|
|
177
|
+
*
|
|
178
|
+
* A `for (let ...)` head is not here: its carrier gives each iteration its own record, and a
|
|
179
|
+
* binding declared in a loop head cannot be read before that head has run.
|
|
180
|
+
*/
|
|
181
|
+
hoistCells(stmts) {
|
|
182
|
+
let out = "";
|
|
183
|
+
for (const s of stmts) {
|
|
184
|
+
if (s.type !== "VariableDeclaration")
|
|
185
|
+
continue;
|
|
186
|
+
for (const d of s.declarations ?? []) {
|
|
187
|
+
for (const id of declaredNames(d.id)) {
|
|
188
|
+
const b = this.binding(id);
|
|
189
|
+
// ONLY THE DEAD-ZONE CLASS. A binding that is a cell for the write rule alone cannot be
|
|
190
|
+
// read before its declaration, so its record is still built where it is declared, with
|
|
191
|
+
// its value already in it: one seam call rather than two on a path that runs.
|
|
192
|
+
if (b?.deadZone !== true || this.hoisted.has(b))
|
|
193
|
+
continue;
|
|
194
|
+
this.hoisted.add(b);
|
|
195
|
+
out += `const ${id.name} = ${this.seam("born", "{}")};\n`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
stmt(node) {
|
|
202
|
+
return this.scoped(() => this.statement(node));
|
|
203
|
+
}
|
|
204
|
+
statement(node) {
|
|
205
|
+
switch (node.type) {
|
|
206
|
+
case "ExpressionStatement": {
|
|
207
|
+
const e = node.expression;
|
|
208
|
+
// A destructuring assignment as a STATEMENT needs no value, so it emits as statements rather
|
|
209
|
+
// than as the async IIFE the expression form needs.
|
|
210
|
+
if (e.type === "AssignmentExpression" && (e.left.type === "ObjectPattern" || e.left.type === "ArrayPattern")) {
|
|
211
|
+
const t = this.temp();
|
|
212
|
+
return `${t} = ${this.expr(e.right)};\n${this.bindPattern(e.left, t, "assign")}`;
|
|
213
|
+
}
|
|
214
|
+
return `${this.expr(e)};\n`;
|
|
215
|
+
}
|
|
216
|
+
case "VariableDeclaration": {
|
|
217
|
+
const kind = node.kind === "let" ? "let" : "const";
|
|
218
|
+
let out = "";
|
|
219
|
+
for (const d of node.declarations ?? []) {
|
|
220
|
+
const init = d.init === null || d.init === undefined ? VOID : this.expr(d.init);
|
|
221
|
+
out += this.bindPattern(d.id, init, kind);
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
case "FunctionDeclaration":
|
|
226
|
+
return `${this.fn(node, node.id !== null && node.id !== undefined ? node.id.name : null)}\n`;
|
|
227
|
+
case "BlockStatement":
|
|
228
|
+
return this.block(node);
|
|
229
|
+
case "IfStatement": {
|
|
230
|
+
const alt = node.alternate === null || node.alternate === undefined ? "" : ` else ${this.wrap(node.alternate)}`;
|
|
231
|
+
return `if (${this.expr(node.test)}) ${this.wrap(node.consequent)}${alt}\n`;
|
|
232
|
+
}
|
|
233
|
+
case "WhileStatement": {
|
|
234
|
+
const b = this.label("b");
|
|
235
|
+
const c = this.label("c");
|
|
236
|
+
const test = this.expr(node.test);
|
|
237
|
+
const body = this.loopBody(node.body, b, c);
|
|
238
|
+
return `${b}: for (;;) {\n${this.fuel()}if (!(${test})) break ${b};\n${c}: {\n${body}}\n}\n`;
|
|
239
|
+
}
|
|
240
|
+
case "ForStatement":
|
|
241
|
+
return this.forStatement(node);
|
|
242
|
+
case "ForOfStatement":
|
|
243
|
+
return this.forOfStatement(node);
|
|
244
|
+
case "ReturnStatement":
|
|
245
|
+
return `return ${node.argument === null || node.argument === undefined ? VOID : this.expr(node.argument)};\n`;
|
|
246
|
+
case "BreakStatement":
|
|
247
|
+
if (this.breakTo === null)
|
|
248
|
+
throw new Error("transform: `break` outside a loop or switch");
|
|
249
|
+
return `break ${this.breakTo};\n`;
|
|
250
|
+
case "ContinueStatement":
|
|
251
|
+
if (this.continueTo === null)
|
|
252
|
+
throw new Error("transform: `continue` outside a loop");
|
|
253
|
+
return `break ${this.continueTo};\n`;
|
|
254
|
+
case "ThrowStatement":
|
|
255
|
+
return `throw ${this.expr(node.argument)};\n`;
|
|
256
|
+
case "TryStatement":
|
|
257
|
+
return this.tryStatement(node);
|
|
258
|
+
case "SwitchStatement":
|
|
259
|
+
return this.switchStatement(node);
|
|
260
|
+
case "EmptyStatement":
|
|
261
|
+
return ";\n";
|
|
262
|
+
default:
|
|
263
|
+
throw new Error(`transform: no rule for statement ${node.type}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** A statement in a position that must hold exactly one: `if (x) y;` becomes `if (x) { y; }`. */
|
|
267
|
+
wrap(node) {
|
|
268
|
+
return node.type === "BlockStatement" ? this.block(node) : `{\n${this.stmt(node)}}`;
|
|
269
|
+
}
|
|
270
|
+
/** A loop body, with `break`/`continue` aimed at this loop's labels. */
|
|
271
|
+
loopBody(node, breakTo, continueTo) {
|
|
272
|
+
const b = this.breakTo;
|
|
273
|
+
const c = this.continueTo;
|
|
274
|
+
this.breakTo = breakTo;
|
|
275
|
+
this.continueTo = continueTo;
|
|
276
|
+
const out = node.type === "BlockStatement" ? this.block(node, true) : this.stmt(node);
|
|
277
|
+
this.breakTo = b;
|
|
278
|
+
this.continueTo = c;
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* `for`, in the shape the specification and the walker both give it.
|
|
283
|
+
*
|
|
284
|
+
* A `for (let ...)` head gives EACH ITERATION its own binding, and the copy happens after the body
|
|
285
|
+
* and before the update, so a closure made in one iteration keeps that iteration's value, and the
|
|
286
|
+
* update applies to the NEXT iteration's binding. Native `for (let ...)` does exactly this, but a
|
|
287
|
+
* cell (see scope.ts) is a record, not a binding, and copying a reference per iteration would let
|
|
288
|
+
* every closure watch the counter move. The carrier form below reproduces the walker's order for
|
|
289
|
+
* both, so one shape covers cells and natives alike.
|
|
290
|
+
*/
|
|
291
|
+
forStatement(node) {
|
|
292
|
+
const b = this.label("b");
|
|
293
|
+
const c = this.label("c");
|
|
294
|
+
const init = node.init;
|
|
295
|
+
const perIteration = init !== null && init !== undefined && init.type === "VariableDeclaration" && init.kind === "let";
|
|
296
|
+
if (!perIteration) {
|
|
297
|
+
let head = "";
|
|
298
|
+
if (init !== null && init !== undefined)
|
|
299
|
+
head = init.type === "VariableDeclaration" ? this.stmt(init) : `${this.expr(init)};\n`;
|
|
300
|
+
const test = node.test === null || node.test === undefined ? "true" : this.expr(node.test);
|
|
301
|
+
const body = this.loopBody(node.body, b, c);
|
|
302
|
+
const update = node.update === null || node.update === undefined ? "" : `${this.expr(node.update)};\n`;
|
|
303
|
+
return `{\n${head}${b}: for (;;) {\n${this.fuel()}if (!(${test})) break ${b};\n${c}: {\n${body}}\n${update}}\n}\n`;
|
|
304
|
+
}
|
|
305
|
+
// THE CARRIERS ARE ALLOCATED FIRST, before any expression inside the loop is emitted. They are
|
|
306
|
+
// the one place a temporary outlives its own expression (they hold the binding's value ACROSS
|
|
307
|
+
// an iteration), so an expression emitted before them would take their slots and the loop would
|
|
308
|
+
// overwrite its own counter. Measured, before this order: `for (let i = 0; i < 3; i = i + 1)`
|
|
309
|
+
// never terminated and the run died on the step budget.
|
|
310
|
+
const first = this.temp();
|
|
311
|
+
const decls = init.declarations ?? [];
|
|
312
|
+
const carriers = [];
|
|
313
|
+
for (const d of decls) {
|
|
314
|
+
const id = d.id;
|
|
315
|
+
if (id.type !== "Identifier")
|
|
316
|
+
throw new Error("transform: a per-iteration `for (let ...)` head takes a name, not a pattern");
|
|
317
|
+
carriers.push({ carrier: this.temp(), id });
|
|
318
|
+
}
|
|
319
|
+
let head = `${first} = true;\n`;
|
|
320
|
+
for (let i = 0; i < decls.length; i += 1) {
|
|
321
|
+
const d = decls[i];
|
|
322
|
+
head += `${carriers[i].carrier} = ${d.init === null || d.init === undefined ? VOID : this.expr(d.init)};\n`;
|
|
323
|
+
}
|
|
324
|
+
let open = "";
|
|
325
|
+
let close = "";
|
|
326
|
+
for (const { carrier, id } of carriers) {
|
|
327
|
+
open += this.bindPattern(id, carrier, "let");
|
|
328
|
+
close += `${carrier} = ${this.readName(id)};\n`;
|
|
329
|
+
}
|
|
330
|
+
// The update runs on the NEXT iteration's binding, which is where the specification and the
|
|
331
|
+
// walker both put it: the per-iteration copy happens after the body and before the increment.
|
|
332
|
+
const update = node.update === null || node.update === undefined ? "" : `${this.expr(node.update)};\n`;
|
|
333
|
+
const test = node.test === null || node.test === undefined ? "true" : this.expr(node.test);
|
|
334
|
+
const body = this.loopBody(node.body, b, c);
|
|
335
|
+
return (`{\n${head}${b}: for (;;) {\n${open}` +
|
|
336
|
+
`if (!${first}) {\n${update}}\n${first} = false;\n` +
|
|
337
|
+
`${this.fuel()}if (!(${test})) break ${b};\n${c}: {\n${body}}\n${close}}\n}\n`);
|
|
338
|
+
}
|
|
339
|
+
forOfStatement(node) {
|
|
340
|
+
const b = this.label("b");
|
|
341
|
+
const c = this.label("c");
|
|
342
|
+
const items = this.temp();
|
|
343
|
+
const item = this.temp();
|
|
344
|
+
const right = this.expr(node.right);
|
|
345
|
+
const left = node.left;
|
|
346
|
+
const target = left.type === "VariableDeclaration" ? left.declarations[0].id : left;
|
|
347
|
+
const mode = left.type === "VariableDeclaration" ? (left.kind === "let" ? "let" : "const") : "assign";
|
|
348
|
+
const bind = this.bindPattern(target, item, mode);
|
|
349
|
+
const body = this.loopBody(node.body, b, c);
|
|
350
|
+
return (`{\nconst ${items} = ${this.seam("iter", right)};\n${b}: for (const ${item} of ${items}) {\n` +
|
|
351
|
+
`${this.fuel()}${bind}${c}: {\n${body}}\n}\n}\n`);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* `try`, with JavaScript's own completion semantics (`try { return 1 } finally { return 2 }` is 2)
|
|
355
|
+
* and the catch parameter bound the way the walker binds it.
|
|
356
|
+
*
|
|
357
|
+
* `__ctx.caught` is required at the head of every catch clause and does both halves: it rethrows
|
|
358
|
+
* the six classes no program may catch, and otherwise answers the frozen `{code, kind, message}`
|
|
359
|
+
* record the walker hands a catch. A native binding of the raw host value diverges on the first
|
|
360
|
+
* program that reads `e.code`.
|
|
361
|
+
*/
|
|
362
|
+
tryStatement(node) {
|
|
363
|
+
let out = `try ${this.block(node.block)}`;
|
|
364
|
+
const handler = node.handler;
|
|
365
|
+
if (handler !== null && handler !== undefined) {
|
|
366
|
+
const raw = this.temp();
|
|
367
|
+
const caught = this.seam("caught", raw);
|
|
368
|
+
const param = handler.param;
|
|
369
|
+
const bind = param === null || param === undefined ? `${caught};\n` : this.bindPattern(param, caught, "const");
|
|
370
|
+
out += `catch (${raw}) {\n${bind}${this.block(handler.body, true)}}\n`;
|
|
371
|
+
}
|
|
372
|
+
if (node.finalizer !== null && node.finalizer !== undefined)
|
|
373
|
+
out += `finally ${this.block(node.finalizer)}`;
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* `switch`, native.
|
|
378
|
+
*
|
|
379
|
+
* The walker reproduces JavaScript's selection by hand (tests in source order, `default` skipped
|
|
380
|
+
* during matching and entered only when nothing matched, then fall-through from the selected
|
|
381
|
+
* clause) because a walker has to. Emitting a native `switch` is that same meaning, not an
|
|
382
|
+
* approximation of it, and a hand-rolled two-pass form here would be a second implementation of a
|
|
383
|
+
* rule the engine already has.
|
|
384
|
+
*/
|
|
385
|
+
switchStatement(node) {
|
|
386
|
+
const b = this.label("b");
|
|
387
|
+
const save = this.breakTo;
|
|
388
|
+
this.breakTo = b;
|
|
389
|
+
// A SWITCH'S CASES SHARE ONE BLOCK SCOPE, and no statement inside it runs before the jump, so a
|
|
390
|
+
// cell declared in a case is created in a block AROUND the switch instead of at its top. That
|
|
391
|
+
// block holds nothing else, so it scopes exactly as the switch's own block does.
|
|
392
|
+
const cells = this.hoistCells((node.cases ?? []).flatMap((c) => c.consequent ?? []));
|
|
393
|
+
const disc = this.expr(node.discriminant);
|
|
394
|
+
let out = `${b}: switch (${disc}) {\n`;
|
|
395
|
+
for (const c of node.cases ?? []) {
|
|
396
|
+
out += c.test === null || c.test === undefined ? "default:\n" : `case ${this.expr(c.test)}:\n`;
|
|
397
|
+
for (const s of c.consequent ?? [])
|
|
398
|
+
out += this.stmt(s);
|
|
399
|
+
}
|
|
400
|
+
this.breakTo = save;
|
|
401
|
+
return cells === "" ? `${out}}\n` : `{\n${cells}${out}}\n}\n`;
|
|
402
|
+
}
|
|
403
|
+
// ---- bindings ---------------------------------------------------------------------------------
|
|
404
|
+
/**
|
|
405
|
+
* Write a cell's field: `set(cell, "v", value)`, with the binding NAME where the write can land
|
|
406
|
+
* in the binding's dead zone.
|
|
407
|
+
*
|
|
408
|
+
* The name is passed on every write to a hoisted (dead-zone) cell and on NO other, which is what
|
|
409
|
+
* the host reads as "refuse if the declaration has not run". The declaration's own initializing
|
|
410
|
+
* write is the one that ends the dead zone, so it never carries it, and passing it there would make
|
|
411
|
+
* a binding refuse its own initialisation.
|
|
412
|
+
*/
|
|
413
|
+
cellWrite(b, target, value) {
|
|
414
|
+
const named = b !== undefined && b.deadZone;
|
|
415
|
+
return this.seam("set", `${target}, ${q("v")}, ${value}${named ? `, ${q(b.name)}` : ""}`);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Bind a pattern, declaring (`const`/`let`) or assigning.
|
|
419
|
+
*
|
|
420
|
+
* Native destructuring is NOT used: `const { a } = o` reaches the prototype chain, and the
|
|
421
|
+
* walker's `bindPattern` reads own fields only, through `memberOf`. So every field read goes
|
|
422
|
+
* through `__ctx.get` and every list read through `__ctx.iter`, which is also what keeps L4014 and
|
|
423
|
+
* L4015 where the walker put them.
|
|
424
|
+
*/
|
|
425
|
+
bindPattern(pattern, valueCode, mode) {
|
|
426
|
+
switch (pattern.type) {
|
|
427
|
+
case "Identifier": {
|
|
428
|
+
const b = this.binding(pattern);
|
|
429
|
+
const name = pattern.name;
|
|
430
|
+
if (mode === "assign") {
|
|
431
|
+
return b?.cell === true ? `${this.cellWrite(b, name, valueCode)};\n` : `${name} = ${valueCode};\n`;
|
|
432
|
+
}
|
|
433
|
+
// A hoisted cell's record already exists (see hoistCells): the declaration is the write that
|
|
434
|
+
// ends its dead zone. A cell that was NOT hoisted is a loop head's carrier, which builds a
|
|
435
|
+
// fresh record per iteration.
|
|
436
|
+
if (b?.cell === true && this.hoisted.has(b))
|
|
437
|
+
return `${this.seam("set", `${name}, ${q("v")}, ${valueCode}`)};\n`;
|
|
438
|
+
if (b?.cell === true)
|
|
439
|
+
return `const ${name} = ${this.seam("born", `{ v: ${valueCode} }`)};\n`;
|
|
440
|
+
return `${mode === "let" ? "let" : "const"} ${name} = ${valueCode};\n`;
|
|
441
|
+
}
|
|
442
|
+
case "MemberExpression": {
|
|
443
|
+
// Only in `assign` mode: `[o.a, o.b] = pair`.
|
|
444
|
+
const obj = this.temp();
|
|
445
|
+
const key = this.temp();
|
|
446
|
+
return `${obj} = ${this.expr(pattern.object)};\n${key} = ${this.memberKey(pattern)};\n${this.seam("set", `${obj}, ${key}, ${valueCode}`)};\n`;
|
|
447
|
+
}
|
|
448
|
+
case "AssignmentPattern": {
|
|
449
|
+
const t = this.temp();
|
|
450
|
+
return `${t} = ${valueCode};\n` + this.bindPattern(pattern.left, `(${t} === ${VOID} ? ${this.expr(pattern.right)} : ${t})`, mode);
|
|
451
|
+
}
|
|
452
|
+
case "ObjectPattern": {
|
|
453
|
+
const s = this.temp();
|
|
454
|
+
// The walker refuses a null or undefined subject with L4010 before it reads a field, and an
|
|
455
|
+
// EMPTY pattern refuses too, so the guard is not the first field read. `__ctx.get` raises
|
|
456
|
+
// L4010 on exactly these two values; the text differs (a declared divergence, code parity is
|
|
457
|
+
// the contract).
|
|
458
|
+
let out = `${s} = ${valueCode};\nif (${s} === null || ${s} === ${VOID}) ${this.seam("get", `${s}, ${q("")}`)};\n`;
|
|
459
|
+
const taken = [];
|
|
460
|
+
for (const p of pattern.properties ?? []) {
|
|
461
|
+
if (p.type === "RestElement") {
|
|
462
|
+
const rest = this.temp();
|
|
463
|
+
out += `${rest} = { ...${s} };\n`;
|
|
464
|
+
for (const k of taken)
|
|
465
|
+
out += `delete ${rest}[${k}];\n`;
|
|
466
|
+
out += this.bindPattern(p.argument, this.seam("born", rest), mode);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const key = this.propertyKey(p);
|
|
470
|
+
taken.push(key);
|
|
471
|
+
out += this.bindPattern(p.value, this.seam("get", `${s}, ${key}`), mode);
|
|
472
|
+
}
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
case "ArrayPattern": {
|
|
476
|
+
const s = this.temp();
|
|
477
|
+
let out = `${s} = ${valueCode};\nif (${s} === null || ${s} === ${VOID}) ${this.seam("get", `${s}, ${q("")}`)};\n`;
|
|
478
|
+
const items = this.temp();
|
|
479
|
+
out += `${items} = ${this.seam("iter", s)};\n`;
|
|
480
|
+
const els = pattern.elements ?? [];
|
|
481
|
+
for (let i = 0; i < els.length; i += 1) {
|
|
482
|
+
const el = els[i];
|
|
483
|
+
if (el === null || el === undefined)
|
|
484
|
+
continue;
|
|
485
|
+
if (el.type === "RestElement") {
|
|
486
|
+
out += this.bindPattern(el.argument, this.seam("born", `${items}.slice(${i})`), mode);
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
out += this.bindPattern(el, `${items}[${i}]`, mode);
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
default:
|
|
494
|
+
throw new Error(`transform: no rule for binding pattern ${pattern.type}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/** A property key in a pattern or an object literal, as a string expression. */
|
|
498
|
+
propertyKey(p) {
|
|
499
|
+
const key = p.key;
|
|
500
|
+
if (p.computed === true)
|
|
501
|
+
return this.expr(key);
|
|
502
|
+
return key.type === "Identifier" ? q(key.name) : q(String(key.value));
|
|
503
|
+
}
|
|
504
|
+
/** A member expression's key: the spelled name, or the computed value the host holds to L4018. */
|
|
505
|
+
memberKey(node) {
|
|
506
|
+
if (node.computed !== true)
|
|
507
|
+
return q(node.property.name);
|
|
508
|
+
return this.expr(node.property);
|
|
509
|
+
}
|
|
510
|
+
// ---- functions --------------------------------------------------------------------------------
|
|
511
|
+
/**
|
|
512
|
+
* A function.
|
|
513
|
+
*
|
|
514
|
+
* Parameters arrive as one rest array and are bound in the body, in order, exactly as the walker's
|
|
515
|
+
* `makeFunction` binds them, which is what makes a default, a pattern and a rest parameter one
|
|
516
|
+
* rule rather than three. Nothing observes a function's arity here: a function has no members.
|
|
517
|
+
*/
|
|
518
|
+
fn(node, name) {
|
|
519
|
+
const saveTop = this.tempTop;
|
|
520
|
+
const saveMax = this.tempMax;
|
|
521
|
+
const saveBreak = this.breakTo;
|
|
522
|
+
const saveContinue = this.continueTo;
|
|
523
|
+
this.tempTop = 0;
|
|
524
|
+
this.tempMax = 0;
|
|
525
|
+
this.breakTo = null;
|
|
526
|
+
this.continueTo = null;
|
|
527
|
+
const args = `${this.n.temp}a`;
|
|
528
|
+
let binds = "";
|
|
529
|
+
const params = node.params ?? [];
|
|
530
|
+
for (let i = 0; i < params.length; i += 1) {
|
|
531
|
+
const p = params[i];
|
|
532
|
+
if (p.type === "RestElement") {
|
|
533
|
+
binds += this.bindPattern(p.argument, this.seam("born", `${args}.slice(${i})`), "let");
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
binds += this.bindPattern(p, `${args}[${i}]`, "let");
|
|
537
|
+
}
|
|
538
|
+
const body = node.body;
|
|
539
|
+
const inner = body.type === "BlockStatement" ? this.block(body, true) : `return ${this.expr(body)};\n`;
|
|
540
|
+
const decls = this.tempMax === 0 ? "" : `let ${Array.from({ length: this.tempMax }, (_, i) => `${this.n.temp}${i}`).join(", ")};\n`;
|
|
541
|
+
const head = name === null ? `async (...${args}) => ` : `async function ${name}(...${args}) `;
|
|
542
|
+
const out = `${head}{\n${decls}${this.fuel()}${binds}${inner}}`;
|
|
543
|
+
this.tempTop = saveTop;
|
|
544
|
+
this.tempMax = saveMax;
|
|
545
|
+
this.breakTo = saveBreak;
|
|
546
|
+
this.continueTo = saveContinue;
|
|
547
|
+
return out;
|
|
548
|
+
}
|
|
549
|
+
// ---- expressions ------------------------------------------------------------------------------
|
|
550
|
+
expr(node) {
|
|
551
|
+
return this.scoped(() => this.expression(node));
|
|
552
|
+
}
|
|
553
|
+
expression(node) {
|
|
554
|
+
switch (node.type) {
|
|
555
|
+
case "Literal":
|
|
556
|
+
return this.literal(node);
|
|
557
|
+
case "Identifier":
|
|
558
|
+
return this.readName(node);
|
|
559
|
+
case "TemplateLiteral": {
|
|
560
|
+
const quasis = node.quasis.map((qu) => q((qu.value.cooked)));
|
|
561
|
+
const exprs = node.expressions.map((e) => this.expr(e));
|
|
562
|
+
return this.seam("template", `[${quasis.join(", ")}], [${exprs.join(", ")}]`);
|
|
563
|
+
}
|
|
564
|
+
case "ArrayExpression": {
|
|
565
|
+
const parts = (node.elements ?? []).map((el) => {
|
|
566
|
+
if (el === null || el === undefined)
|
|
567
|
+
throw new Error("transform: an array hole is not a value this language has");
|
|
568
|
+
return el.type === "SpreadElement" ? `...${this.seam("iter", this.expr(el.argument))}` : this.expr(el);
|
|
569
|
+
});
|
|
570
|
+
return this.seam("born", `[${parts.join(", ")}]`);
|
|
571
|
+
}
|
|
572
|
+
case "ObjectExpression": {
|
|
573
|
+
// Every key is emitted COMPUTED. A literal `__proto__:` key sets a prototype in JavaScript
|
|
574
|
+
// and names an own field in this language; the validator refuses the literal spelling
|
|
575
|
+
// (L1028, measured), and a computed key never reaches the prototype at all, so the emitted
|
|
576
|
+
// form cannot express the hazard whatever the validator does later.
|
|
577
|
+
const parts = (node.properties ?? []).map((p) => p.type === "SpreadElement" ? `...${this.expr(p.argument)}` : `[${this.propertyKey(p)}]: ${this.expr(p.value)}`);
|
|
578
|
+
return this.seam("born", `{ ${parts.join(", ")} }`);
|
|
579
|
+
}
|
|
580
|
+
case "MemberExpression":
|
|
581
|
+
case "ChainExpression":
|
|
582
|
+
case "CallExpression":
|
|
583
|
+
return this.chain(node);
|
|
584
|
+
case "UnaryExpression": {
|
|
585
|
+
const op = node.operator;
|
|
586
|
+
const v = this.expr(node.argument);
|
|
587
|
+
if (op === "!")
|
|
588
|
+
return `(!${v})`;
|
|
589
|
+
if (op === "typeof")
|
|
590
|
+
return `(typeof ${v})`;
|
|
591
|
+
const t = this.temp();
|
|
592
|
+
return `((${t} = ${v}), typeof ${t} === "number" ? ${op}${t} : ${this.seam("unary", `${q(op)}, ${t}`)})`;
|
|
593
|
+
}
|
|
594
|
+
case "UpdateExpression":
|
|
595
|
+
return this.update(node);
|
|
596
|
+
case "BinaryExpression": {
|
|
597
|
+
const op = node.operator;
|
|
598
|
+
const l = this.expr(node.left);
|
|
599
|
+
// `===`/`!==` are taken before the coercion refusal in the walker, so they are native here.
|
|
600
|
+
if (op === "===" || op === "!==")
|
|
601
|
+
return `(${l} ${op} ${this.expr(node.right)})`;
|
|
602
|
+
// THE OPERAND TEMPORARIES ARE ALLOCATED BEFORE THE RIGHT SIDE IS EMITTED. A temporary's life
|
|
603
|
+
// is its own expression, so a sibling may reuse the slot, but `a` outlives the right
|
|
604
|
+
// operand's evaluation, and emitting the right side first let it reuse `a`'s slot and
|
|
605
|
+
// overwrite the left value in place. Measured: `(5 + 1) + (3 + 4)` answered 10 for 13.
|
|
606
|
+
const a = this.temp();
|
|
607
|
+
const b = this.temp();
|
|
608
|
+
const r = this.expr(node.right);
|
|
609
|
+
// BOTH operands are assigned before the test. A `&&` between the assignments would
|
|
610
|
+
// short-circuit past the right-hand one, so its effects would vanish and the host leg would
|
|
611
|
+
// refuse a stale value (the engine host caught this in the design sketch).
|
|
612
|
+
const native = `${a} ${op} ${b}`;
|
|
613
|
+
const fast = STRING_SAFE.has(op)
|
|
614
|
+
? `(typeof ${a} === "number" && typeof ${b} === "number") || (typeof ${a} === "string" && typeof ${b} === "string")`
|
|
615
|
+
: `typeof ${a} === "number" && typeof ${b} === "number"`;
|
|
616
|
+
return `((${a} = ${l}), (${b} = ${r}), (${fast}) ? (${native}) : ${this.seam("binary", `${q(op)}, ${a}, ${b}`)})`;
|
|
617
|
+
}
|
|
618
|
+
case "LogicalExpression":
|
|
619
|
+
return `(${this.expr(node.left)} ${node.operator} ${this.expr(node.right)})`;
|
|
620
|
+
case "ConditionalExpression":
|
|
621
|
+
return `(${this.expr(node.test)} ? ${this.expr(node.consequent)} : ${this.expr(node.alternate)})`;
|
|
622
|
+
case "AssignmentExpression":
|
|
623
|
+
return this.assign(node);
|
|
624
|
+
case "AwaitExpression":
|
|
625
|
+
return `(await ${this.seam("fuel", "")}, await ${this.seam("await", this.expr(node.argument))})`;
|
|
626
|
+
case "ArrowFunctionExpression":
|
|
627
|
+
return this.fn(node, null);
|
|
628
|
+
case "FunctionExpression": {
|
|
629
|
+
// A named function expression sees its own name. Emitting the name on the expression is what
|
|
630
|
+
// binds it, exactly as the walker declares it into the call's environment.
|
|
631
|
+
const id = node.id;
|
|
632
|
+
if (id === null || id === undefined)
|
|
633
|
+
return this.fn(node, null);
|
|
634
|
+
return `(${this.fnNamedExpression(node, id.name)})`;
|
|
635
|
+
}
|
|
636
|
+
default:
|
|
637
|
+
throw new Error(`transform: no rule for expression ${node.type}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
fnNamedExpression(node, name) {
|
|
641
|
+
const decl = this.fn(node, name);
|
|
642
|
+
return decl.replace(/^async function /, "async function ");
|
|
643
|
+
}
|
|
644
|
+
literal(node) {
|
|
645
|
+
if (node.bigint !== undefined)
|
|
646
|
+
throw new Error("transform: a bigint literal is not in this language");
|
|
647
|
+
if (node.regex !== undefined)
|
|
648
|
+
throw new Error("transform: a regular expression literal is not in this language");
|
|
649
|
+
const v = node.value;
|
|
650
|
+
if (typeof v === "string")
|
|
651
|
+
return q(v);
|
|
652
|
+
if (v === null)
|
|
653
|
+
return "null";
|
|
654
|
+
return String(node.raw ?? String(v));
|
|
655
|
+
}
|
|
656
|
+
/** `x++`, `--o.count`: JavaScript's meaning, with the read charged through `Number` as the walker charges it. */
|
|
657
|
+
/**
|
|
658
|
+
* `x++`'s operand, coerced through the seam's `update` selector.
|
|
659
|
+
*
|
|
660
|
+
* The walker reads the old value with a bare `Number(...)` and NO refusal: `o.c++` on a record is
|
|
661
|
+
* NaN there, while `-o.c` on the same record is L4018. The transform reproduces that and declines
|
|
662
|
+
* to rebuild it: silent coercion is the class the language exists to refuse, so `update` refuses a
|
|
663
|
+
* non-number operand and the walker's answer is a DECLARED divergence (issue 646) with its own
|
|
664
|
+
* cells, not a fidelity target. A number never reaches the host: the fast path keeps every counter
|
|
665
|
+
* native, which is what makes the numeric corpus identical on both arms.
|
|
666
|
+
*/
|
|
667
|
+
toNumber(code) {
|
|
668
|
+
const t = this.temp();
|
|
669
|
+
return `((${t} = ${code}), typeof ${t} === "number" ? ${t} : ${this.seam("unary", `${q("update")}, ${t}`)})`;
|
|
670
|
+
}
|
|
671
|
+
update(node) {
|
|
672
|
+
const delta = node.operator === "++" ? "+ 1" : "- 1";
|
|
673
|
+
const prefix = node.prefix === true;
|
|
674
|
+
const arg = node.argument;
|
|
675
|
+
const old = this.temp();
|
|
676
|
+
const next = this.temp();
|
|
677
|
+
if (arg.type === "Identifier") {
|
|
678
|
+
const read = this.readName(arg);
|
|
679
|
+
const write = (value) => {
|
|
680
|
+
const b = this.binding(arg);
|
|
681
|
+
return b?.cell === true ? this.cellWrite(b, arg.name, value) : `(${arg.name} = ${value})`;
|
|
682
|
+
};
|
|
683
|
+
return `((${old} = ${this.toNumber(read)}), (${next} = ${old} ${delta}), ${write(next)}, ${prefix ? next : old})`;
|
|
684
|
+
}
|
|
685
|
+
const obj = this.temp();
|
|
686
|
+
const key = this.temp();
|
|
687
|
+
return (`((${obj} = ${this.expr(arg.object)}), (${key} = ${this.memberKey(arg)}), ` +
|
|
688
|
+
`(${old} = ${this.toNumber(this.seam("get", `${obj}, ${key}`))}), (${next} = ${old} ${delta}), ` +
|
|
689
|
+
`${this.seam("set", `${obj}, ${key}, ${next}`)}, ${prefix ? next : old})`);
|
|
690
|
+
}
|
|
691
|
+
assign(node) {
|
|
692
|
+
const op = node.operator;
|
|
693
|
+
const left = node.left;
|
|
694
|
+
if (left.type === "ObjectPattern" || left.type === "ArrayPattern") {
|
|
695
|
+
// The value of a destructuring assignment is its right-hand side; as an expression it needs a
|
|
696
|
+
// statement body, so it becomes one. As a STATEMENT it never reaches here.
|
|
697
|
+
const t = this.temp();
|
|
698
|
+
return `(await (async () => {\n${t} = ${this.expr(node.right)};\n${this.bindPattern(left, t, "assign")}return ${t};\n})())`;
|
|
699
|
+
}
|
|
700
|
+
if (left.type === "Identifier") {
|
|
701
|
+
const b = this.binding(left);
|
|
702
|
+
const name = left.name;
|
|
703
|
+
const write = (value) => b?.cell === true ? this.cellWrite(b, name, value) : `(${name} = ${value})`;
|
|
704
|
+
const t = this.temp();
|
|
705
|
+
// The READ is taken only where the operator needs one. Taking it unconditionally emitted a
|
|
706
|
+
// `get` a plain `=` never uses, and charged the site count for it.
|
|
707
|
+
if (op === "=")
|
|
708
|
+
return `((${t} = ${this.expr(node.right)}), ${write(t)}, ${t})`;
|
|
709
|
+
if (op === "&&=" || op === "||=" || op === "??=")
|
|
710
|
+
return this.logicalAssign(op, this.readName(left), write, node.right, t);
|
|
711
|
+
const cur = this.temp();
|
|
712
|
+
return `((${cur} = ${this.readName(left)}), (${t} = ${this.binaryOf(op.slice(0, -1), cur, () => this.expr(node.right))}), ${write(t)}, ${t})`;
|
|
713
|
+
}
|
|
714
|
+
const obj = this.temp();
|
|
715
|
+
const key = this.temp();
|
|
716
|
+
const head = `(${obj} = ${this.expr(left.object)}), (${key} = ${this.memberKey(left)})`;
|
|
717
|
+
const write = (value) => this.seam("set", `${obj}, ${key}, ${value}`);
|
|
718
|
+
const t = this.temp();
|
|
719
|
+
if (op === "=")
|
|
720
|
+
return `(${head}, (${t} = ${this.expr(node.right)}), ${write(t)}, ${t})`;
|
|
721
|
+
const read = () => this.seam("get", `${obj}, ${key}`);
|
|
722
|
+
if (op === "&&=" || op === "||=" || op === "??=")
|
|
723
|
+
return `(${head}, ${this.logicalAssign(op, read(), write, node.right, t)})`;
|
|
724
|
+
const cur = this.temp();
|
|
725
|
+
return `(${head}, (${cur} = ${read()}), (${t} = ${this.binaryOf(op.slice(0, -1), cur, () => this.expr(node.right))}), ${write(t)}, ${t})`;
|
|
726
|
+
}
|
|
727
|
+
/** `&&=`, `||=`, `??=`: the right side is evaluated, and the write happens, only when it proceeds. */
|
|
728
|
+
logicalAssign(op, read, write, right, t) {
|
|
729
|
+
const cur = this.temp();
|
|
730
|
+
const guard = op === "&&=" ? `${cur}` : op === "||=" ? `!${cur}` : `${cur} === null || ${cur} === ${VOID}`;
|
|
731
|
+
return `((${cur} = ${read}), (${guard}) ? ((${t} = ${this.expr(right)}), ${write(t)}, ${t}) : ${cur})`;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* A binary operation over an already-emitted left operand and a right one this emits.
|
|
735
|
+
*
|
|
736
|
+
* The right side is a thunk for the reason the `BinaryExpression` case gives: `a` has to be
|
|
737
|
+
* allocated before the right operand is emitted, or the right operand reuses `a`'s slot and
|
|
738
|
+
* overwrites the left value between the assignment and the test.
|
|
739
|
+
*/
|
|
740
|
+
binaryOf(op, leftCode, right) {
|
|
741
|
+
if (op === "===" || op === "!==")
|
|
742
|
+
return `(${leftCode} ${op} ${right()})`;
|
|
743
|
+
const a = this.temp();
|
|
744
|
+
const b = this.temp();
|
|
745
|
+
const rightCode = right();
|
|
746
|
+
const fast = STRING_SAFE.has(op)
|
|
747
|
+
? `(typeof ${a} === "number" && typeof ${b} === "number") || (typeof ${a} === "string" && typeof ${b} === "string")`
|
|
748
|
+
: `typeof ${a} === "number" && typeof ${b} === "number"`;
|
|
749
|
+
return `((${a} = ${leftCode}), (${b} = ${rightCode}), (${fast}) ? (${a} ${op} ${b}) : ${this.seam("binary", `${q(op)}, ${a}, ${b}`)})`;
|
|
750
|
+
}
|
|
751
|
+
// ---- chains, calls ----------------------------------------------------------------------------
|
|
752
|
+
/**
|
|
753
|
+
* A member chain, with `?.` short-circuiting the WHOLE chain and nothing after it evaluated.
|
|
754
|
+
*
|
|
755
|
+
* Each optional link stores its object in a temp inside the guard, so the value is computed once:
|
|
756
|
+
* the guards run in order, and the body reads the last temp rather than re-deriving the links.
|
|
757
|
+
*/
|
|
758
|
+
chain(node) {
|
|
759
|
+
const guards = [];
|
|
760
|
+
const body = this.chainLink(node.type === "ChainExpression" ? node.expression : node, guards, undefined);
|
|
761
|
+
return this.short(body, guards);
|
|
762
|
+
}
|
|
763
|
+
/** A body under the guards in force where it is written: nullish at any of them and nothing after runs. */
|
|
764
|
+
short(body, guards) {
|
|
765
|
+
if (guards.length === 0)
|
|
766
|
+
return body;
|
|
767
|
+
return `((${guards.join(") || (")}) ? ${VOID} : ${body})`;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* One link of a chain, with everything written AFTER it handed down as {@link Rest}.
|
|
771
|
+
*
|
|
772
|
+
* The chain is emitted from the inside out (the innermost value first, each link wrapping what it
|
|
773
|
+
* produced) but the OPTIONAL CALL needs the opposite direction. `o.m?.()` short-circuits on
|
|
774
|
+
* whether the member is nullish, and the seam resolves a method name and calls it in one step, the
|
|
775
|
+
* one place a method name may be resolved at all, so the host makes that decision and the
|
|
776
|
+
* transform never sees the answer. What the host cannot then tell the transform is WHETHER it
|
|
777
|
+
* short-circuited: measured on the walker, `o.z?.().x` on an absent member is undefined while
|
|
778
|
+
* `o.m?.().x` on a member that RETURNS undefined is L4010, and a guard on the returned value
|
|
779
|
+
* answers undefined for both, so it would drop a refusal in silence. The seam's fifth argument to
|
|
780
|
+
* `call` hands the rest of the chain to the call that made the decision instead.
|
|
781
|
+
*
|
|
782
|
+
* So `rest` travels down to every link, and each one applies it to its own value, except the
|
|
783
|
+
* optional call, which compiles it into a closure the host applies or skips.
|
|
784
|
+
*/
|
|
785
|
+
chainLink(node, guards, rest) {
|
|
786
|
+
const done = (code, g) => (rest === undefined ? code : rest(code, g));
|
|
787
|
+
if (node.type === "MemberExpression") {
|
|
788
|
+
return this.chainLink(node.object, guards, (objCode, g) => {
|
|
789
|
+
const obj = node.optional === true ? this.guard(objCode, g) : objCode;
|
|
790
|
+
return done(this.seam("get", `${obj}, ${this.memberKey(node)}`), g);
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
if (node.type === "CallExpression") {
|
|
794
|
+
const callee = node.callee;
|
|
795
|
+
// A primitive is dispatched by NAME, never by value: the validator forbids shadowing one, so a
|
|
796
|
+
// call spelled `turn` is always the effect. It is emitted BEFORE the ordinary argument list is
|
|
797
|
+
// built, because a primitive that defers its body hands that argument over differently.
|
|
798
|
+
if (callee.type === "Identifier" && this.binding(callee) === undefined && PRIMITIVES[callee.name] !== undefined) {
|
|
799
|
+
const name = callee.name;
|
|
800
|
+
return done(`(await ${this.seam("effect", `${q(name)}, [${this.effectArgs(name, node)}], ${this.site(name, node)}`)})`, guards);
|
|
801
|
+
}
|
|
802
|
+
const args = this.args(node);
|
|
803
|
+
if (callee.type === "Identifier" && this.binding(callee) === undefined && FREE_VALUES.has(callee.name)) {
|
|
804
|
+
return done(`(await ${this.seam("free", `${q(callee.name)}, [${args}]`)})`, guards);
|
|
805
|
+
}
|
|
806
|
+
if (callee.type === "MemberExpression") {
|
|
807
|
+
return this.chainLink(callee.object, guards, (objCode, g) => {
|
|
808
|
+
const obj = callee.optional === true ? this.guard(objCode, g) : objCode;
|
|
809
|
+
const key = this.memberKey(callee);
|
|
810
|
+
// The one place a method NAME may be resolved: at the call. That is what lets `get` refuse
|
|
811
|
+
// the same name everywhere else (L4020). An ordinary call's chain is written natively,
|
|
812
|
+
// and nothing in it waits on a decision only the host made.
|
|
813
|
+
if (node.optional !== true)
|
|
814
|
+
return done(`(await ${this.seam("call", `${obj}, ${key}, [${args}]`)})`, g);
|
|
815
|
+
// AND THE OPTIONAL FORM HANDS ITS ARGUMENTS OVER UNEVALUATED. The walker checks the member
|
|
816
|
+
// BEFORE it evaluates the argument list, so `o.m?.(await sleep("1s"))` on an absent member
|
|
817
|
+
// journals nothing while the same argument on a present method journals a sleep (the
|
|
818
|
+
// engine host measured both). An emitted array has already run them, and a resume would
|
|
819
|
+
// replay a step the walker's run never recorded, so it is a thunk, and `async` because an
|
|
820
|
+
// argument may await. The ordinary form is unchanged and still hands over a plain array.
|
|
821
|
+
const cont = rest === undefined ? "" : `, ${this.continuation(rest)}`;
|
|
822
|
+
return `(await ${this.seam("call", `${obj}, ${key}, async () => [${args}], true${cont}`)})`;
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
return this.chainLink(callee, guards, (fnCode, g) => {
|
|
826
|
+
const fn = node.optional === true ? this.guard(fnCode, g) : fnCode;
|
|
827
|
+
const f = this.temp();
|
|
828
|
+
// L4011 at a non-function callee, behind a `typeof` so a call to a real function never leaves
|
|
829
|
+
// the compartment. `const f = 1; f()` is admitted by the validator (measured).
|
|
830
|
+
return done(`(await ((${f} = ${fn}), typeof ${f} === "function" ? ${f} : ${this.seam("callee", f)})(${args}))`, g);
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
return done(this.expr(node), guards);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* The rest of a chain, as the closure the seam's fifth argument to `call` takes.
|
|
837
|
+
*
|
|
838
|
+
* Its guards are its OWN: a link after the optional call is guarded only where the call answered,
|
|
839
|
+
* because a short-circuit already skipped everything here. `async` because a continuation may hold
|
|
840
|
+
* another call, and the host awaits what it applies.
|
|
841
|
+
*/
|
|
842
|
+
continuation(rest) {
|
|
843
|
+
const value = `${this.n.temp}c${this.conts}`;
|
|
844
|
+
this.conts += 1;
|
|
845
|
+
const guards = [];
|
|
846
|
+
const body = rest(value, guards);
|
|
847
|
+
return `async (${value}) => ${this.short(body, guards)}`;
|
|
848
|
+
}
|
|
849
|
+
guard(code, guards) {
|
|
850
|
+
const t = this.temp();
|
|
851
|
+
guards.push(`(${t} = ${code}) === null || ${t} === ${VOID}`);
|
|
852
|
+
return t;
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* A primitive's arguments, with the DEFERRED BODY handed over unevaluated.
|
|
856
|
+
*
|
|
857
|
+
* `fanOut` and `conclave` take their body at index 1, and the walker evaluates it INSIDE the
|
|
858
|
+
* scope, after the entry has begun. Measured on the oracle: the same awaited effect journals
|
|
859
|
+
* after the scope entry in the body position and before it in the options bag. A body handed over
|
|
860
|
+
* already evaluated has journalled its effects in the wrong place, and a resume would replay a
|
|
861
|
+
* step the walker's run never recorded; it is the same rule as the optional call's arguments, and
|
|
862
|
+
* the host refuses a body that is not a thunk (L1000).
|
|
863
|
+
*
|
|
864
|
+
* WHICH primitives defer comes from the table (a scope-opener whose options sit at 2) rather
|
|
865
|
+
* than from a list of names spelled here, so a fifth combinator cannot arrive with its body
|
|
866
|
+
* silently eager.
|
|
867
|
+
*/
|
|
868
|
+
effectArgs(name, node) {
|
|
869
|
+
const spec = PRIMITIVES[name];
|
|
870
|
+
const defers = spec !== undefined && spec.opensScope && spec.optionsAt === 2;
|
|
871
|
+
return (node.arguments ?? [])
|
|
872
|
+
.map((a, i) => {
|
|
873
|
+
const code = a.type === "SpreadElement" ? `...${this.seam("iter", this.expr(a.argument))}` : this.expr(a);
|
|
874
|
+
return defers && i === 1 ? `async () => (${code})` : code;
|
|
875
|
+
})
|
|
876
|
+
.join(", ");
|
|
877
|
+
}
|
|
878
|
+
args(node) {
|
|
879
|
+
return (node.arguments ?? [])
|
|
880
|
+
.map((a) => (a.type === "SpreadElement" ? `...${this.seam("iter", this.expr(a.argument))}` : this.expr(a)))
|
|
881
|
+
.join(", ");
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* The static per-call-site payload an effect carries.
|
|
885
|
+
*
|
|
886
|
+
* Only `race` needs one today: its journal entry holds a `branchDigest` over the LOSING arms'
|
|
887
|
+
* source, which the walker computes from the AST at run time. The engine has no AST then, so
|
|
888
|
+
* without this the two journals cannot be byte-identical for any race that settled.
|
|
889
|
+
*/
|
|
890
|
+
/**
|
|
891
|
+
* The static payload a call site carries, because the engine has no AST at run time.
|
|
892
|
+
*
|
|
893
|
+
* A settled `race` journals a `branchDigest` over the arms it will never walk into, and that
|
|
894
|
+
* digest is a function of the SOURCE. The walker computes it from the object literal in hand;
|
|
895
|
+
* the engine has to be handed the same material, so the branch bodies travel here with their
|
|
896
|
+
* positions stripped by `interpret.ts`'s OWN function, imported and never copied, because a second
|
|
897
|
+
* implementation of "what the code IS" is a second answer to whether a resumed run diverged.
|
|
898
|
+
*
|
|
899
|
+
* WHAT TRAVELS IS THE STRIPPED BODY, NOT A PER-BRANCH DIGEST. The walker hashes one array of
|
|
900
|
+
* `[loserName, body]` pairs over the loser set, which is only known at run time; per-branch
|
|
901
|
+
* digests would make the host hash a hash and produce a different byte string for a journal entry
|
|
902
|
+
* that has to be identical. Only losers are ever digested, but which branches lose is the run's
|
|
903
|
+
* answer, so every branch travels.
|
|
904
|
+
*/
|
|
905
|
+
site(name, node) {
|
|
906
|
+
if (name !== "race")
|
|
907
|
+
return "{}";
|
|
908
|
+
const branches = (node.arguments ?? [])[0];
|
|
909
|
+
if (branches === undefined || branches.type !== "ObjectExpression")
|
|
910
|
+
return "{}";
|
|
911
|
+
const bodies = {};
|
|
912
|
+
for (const p of branches.properties ?? []) {
|
|
913
|
+
const key = p.key;
|
|
914
|
+
const named = key?.name ?? key?.value;
|
|
915
|
+
if (named !== undefined)
|
|
916
|
+
bodies[named] = stripPositions(p.value);
|
|
917
|
+
}
|
|
918
|
+
return `{ branchDigests: ${JSON.stringify(bodies)} }`;
|
|
919
|
+
}
|
|
920
|
+
result() {
|
|
921
|
+
return { sites: Object.fromEntries([...this.sites].sort()), proposed: [...this.proposed].sort() };
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
/** Operators whose native meaning is the walker's for two strings as well as two numbers. */
|
|
925
|
+
const STRING_SAFE = new Set(["+", "<", "<=", ">", ">="]);
|
|
926
|
+
export function emit(program) {
|
|
927
|
+
const ids = new Set();
|
|
928
|
+
identifiers(program, ids);
|
|
929
|
+
const names = pickNames(ids);
|
|
930
|
+
const e = new Emitter(analyze(program), names);
|
|
931
|
+
const module = e.module(program);
|
|
932
|
+
return { module, names, ...e.result() };
|
|
933
|
+
}
|
|
934
|
+
//# sourceMappingURL=emit.js.map
|