@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,834 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `__ctx`: the WHOLE seam between the transformed program and the host.
|
|
3
|
+
*
|
|
4
|
+
* The transform emits a CLOSED function expression with zero free identifiers; the host
|
|
5
|
+
* evaluates it in a Compartment with ZERO endowments and passes this object as the call argument
|
|
6
|
+
* (the seam contract). So this file is the complete list of what a program can reach. Anything not
|
|
7
|
+
* here is not reachable, which is why the member list is a surface both sides hold each other to.
|
|
8
|
+
*
|
|
9
|
+
* Every law lives here, and every table it needs is IMPORTED. `library.ts` owns the curated method
|
|
10
|
+
* tables and the free builtins; `values.ts` owns freezing, birth-depth stamping and the crossing
|
|
11
|
+
* check; `keys.ts` owns key allocation; `journal.ts` owns the entries. Nothing in this file is a
|
|
12
|
+
* second copy of any of them: a copied table is a table that disagrees with the walker on its
|
|
13
|
+
* first edit, and the walker is the differential oracle.
|
|
14
|
+
*
|
|
15
|
+
* WHAT THIS FILE IS NOT: it is not the walker rewritten. The walker enforces these laws inline
|
|
16
|
+
* while it walks; here they are the host side of a native program's calls. The behaviours must be
|
|
17
|
+
* identical and the differential suite is what says so: same programs, walker and engine,
|
|
18
|
+
* identical journals (entry sequences and step keys, not merely output).
|
|
19
|
+
*/
|
|
20
|
+
import { RuntimeFault } from "../errors.js";
|
|
21
|
+
import { Cancelled, EffectError } from "../effects.js";
|
|
22
|
+
import { arrayMethods, builtins, numberMethods, stringMethods } from "../library.js";
|
|
23
|
+
import { NotCrossable, Prng, assertNoCode, birthDepth, born as stampBirth, deepFreeze, setOwn } from "../values.js";
|
|
24
|
+
import { digest } from "../keys.js";
|
|
25
|
+
import { currentFrame, withFrame } from "./frame.js";
|
|
26
|
+
import { dispatchPrimitive, freeConstructors, option, performScope, runScope } from "../perform.js";
|
|
27
|
+
import { PRIMITIVES } from "../primitives.js";
|
|
28
|
+
// ---- the coercion law, shared by every site that can reach host ToPrimitive --------------------
|
|
29
|
+
/**
|
|
30
|
+
* L4018, the one refusal that has to be spelled identically everywhere it applies.
|
|
31
|
+
*
|
|
32
|
+
* Converting reads `valueOf`/`toString` off the value, and a program closure stored there would run
|
|
33
|
+
* from host machinery with no frame. The walker measured every one of these holes before closing
|
|
34
|
+
* them; the engine inherits the closed shape rather than rediscovering it.
|
|
35
|
+
*/
|
|
36
|
+
function refuseCoercion(where, v) {
|
|
37
|
+
if (v !== null && (typeof v === "object" || typeof v === "function")) {
|
|
38
|
+
const kind = typeof v === "function" ? "a function" : Array.isArray(v) ? "an array" : "a record";
|
|
39
|
+
throw new RuntimeFault("L4018", `\`${where}\` cannot take ${kind}: there is no implicit conversion here, because converting would read \`valueOf\`/\`toString\` off the value — host machinery this language does not have. Convert explicitly: \`json.stringify(value)\` for text, or read the field you mean.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** A canonical array index (`"0"`, `"12"`), as a number, or nothing. */
|
|
43
|
+
function arrayIndex(prop) {
|
|
44
|
+
if (!/^(0|[1-9][0-9]*)$/.test(prop))
|
|
45
|
+
return undefined;
|
|
46
|
+
const n = Number(prop);
|
|
47
|
+
return n <= 4294967294 ? n : undefined;
|
|
48
|
+
}
|
|
49
|
+
// ---- the thenable gate -------------------------------------------------------------------------
|
|
50
|
+
/**
|
|
51
|
+
* An own CALLABLE `then` on a program value, refused where it is minted.
|
|
52
|
+
*
|
|
53
|
+
* Measured at 9dc154f8 (node v26.7.0, ses@2.3.0), inside the compartment, one shot per site:
|
|
54
|
+
* constructing such a record is harmless, but RETURNING it out of any async function assimilates
|
|
55
|
+
* it (`then(resolve, reject)` runs, with the host's settlement functions as its arguments), and so
|
|
56
|
+
* does awaiting it, and so does a function carrying an own `then`. Every transformed function body
|
|
57
|
+
* is async, so the hazard sits at every return of a program value, not merely at `await`.
|
|
58
|
+
*
|
|
59
|
+
* The refusal is scoped to a CALLABLE `then` because a non-callable one is legal and settles clean
|
|
60
|
+
* on the walker (`{ then: 1 }` runs to completion); refusing every own `then` would refuse a program
|
|
61
|
+
* the oracle accepts.
|
|
62
|
+
*
|
|
63
|
+
* THE CODE IS L4021, the walker's, since #657 gave the walker the same rule at its four record-member
|
|
64
|
+
* write sites (a literal, a spread, a rest pattern, `o.a = v`). Measured across every route a program
|
|
65
|
+
* can spell it, both engines now answer L4021 where the walker does and the same older code where it
|
|
66
|
+
* already agreed: a literal, a plain field write, a computed one and a spread are L4021 on both; a
|
|
67
|
+
* non-callable `then` completes on both; `then` on an ARRAY is L4014 on both, and on a string or a
|
|
68
|
+
* number L4010 on both. The one door with NO walker route behind it is `await`, which gates a value
|
|
69
|
+
* arriving from the HOST side of the seam - the walker has no such door because it has no seam. It
|
|
70
|
+
* carries L4021 too, so a program reads one code for one shape rather than learning which door
|
|
71
|
+
* it happened to come through.
|
|
72
|
+
*
|
|
73
|
+
* Two doors close the whole value graph, by induction: a callable `then` can only enter through a
|
|
74
|
+
* literal (`born`) or a field write (`set`): `json.parse` cannot spell a function, and spread,
|
|
75
|
+
* `merge` and the array methods only copy fields out of records that already passed a door. `await`
|
|
76
|
+
* is gated too, because a value can also arrive from the host side of the seam.
|
|
77
|
+
*
|
|
78
|
+
* THERE IS DELIBERATELY NO CHECK ON THE HOST'S RETURN PATH, and the reason is measured rather than
|
|
79
|
+
* argued. Such a check was asked for at `free`/`call`, on the strength of `merge({}, { then: f })`
|
|
80
|
+
* minting the shape on the WALKER. It cannot exist: every builtin and curated method in library.ts
|
|
81
|
+
* returns through its own `async` wrapper, so the assimilation happens INSIDE library.ts, one frame
|
|
82
|
+
* before any host code could inspect the result. Measured directly against `merge`: with a `then`
|
|
83
|
+
* that never resolves, the builtin's promise never settles at all (the program closure had already
|
|
84
|
+
* run); with a `then` that resolves, the record is silently REPLACED by whatever it resolves with.
|
|
85
|
+
* In both cases the value a return-path gate would examine is either never delivered or already
|
|
86
|
+
* substituted, so such a gate is unreachable code that no mutant can kill. The walker shape it was
|
|
87
|
+
* meant to cover is a walker defect (there is no birth gate there at all) and belongs to the filed
|
|
88
|
+
* issue; in the engine the literal never reaches `merge`, because `born` refuses it first, which is
|
|
89
|
+
* a cell, not an assertion.
|
|
90
|
+
*/
|
|
91
|
+
function hasCallableThen(v) {
|
|
92
|
+
if (v === null || (typeof v !== "object" && typeof v !== "function"))
|
|
93
|
+
return false;
|
|
94
|
+
if (!Object.prototype.hasOwnProperty.call(v, "then"))
|
|
95
|
+
return false;
|
|
96
|
+
return typeof v.then === "function";
|
|
97
|
+
}
|
|
98
|
+
function refuseThenable(v, where) {
|
|
99
|
+
if (!hasCallableThen(v))
|
|
100
|
+
return;
|
|
101
|
+
throw new RuntimeFault("L4021", `${where} carries an own callable \`then\`. To the host's promise machinery any object with a callable \`then\` is a promise waiting to be adopted, so the value would never arrive as the one this program built: its \`then\` runs with the machinery's own continuations, a \`then\` that throws or rejects escapes the run as an unhandled rejection with no owner and kills the host, and the await that adopted it never settles. Name the member something else.`);
|
|
102
|
+
}
|
|
103
|
+
// ---- the seam ----------------------------------------------------------------------------------
|
|
104
|
+
/**
|
|
105
|
+
* The seam, plus the one thing the HOST needs from it that the program must never see.
|
|
106
|
+
*
|
|
107
|
+
* `steps()` is the count charged against the step budget, which a `RunResult` reports so a host can
|
|
108
|
+
* see how close a program runs to the ceiling before the ceiling is what tells it. It is deliberately
|
|
109
|
+
* NOT a member of {@link EngineCtx}: everything on that interface is reachable from inside the
|
|
110
|
+
* compartment, and a program that can read its own fuel gauge can shape its behaviour around one.
|
|
111
|
+
*/
|
|
112
|
+
export function createEngine(run) {
|
|
113
|
+
const ctx = buildCtx(run);
|
|
114
|
+
return { ctx, steps: () => ctx[STEPS]() };
|
|
115
|
+
}
|
|
116
|
+
/** The seam alone. */
|
|
117
|
+
export function createCtx(run) {
|
|
118
|
+
return buildCtx(run);
|
|
119
|
+
}
|
|
120
|
+
/** Where the step count hides: a symbol the language cannot name, on the object the program holds. */
|
|
121
|
+
const STEPS = Symbol("cotal-lang engine steps");
|
|
122
|
+
function buildCtx(run) {
|
|
123
|
+
const prng = new Prng(run.pins.seed);
|
|
124
|
+
// The effect seam is ONE function over ONE table, shared with the walker (src/perform.ts). The
|
|
125
|
+
// engine holds no copy of it: a second set of hashed projections is a set that disagrees with the
|
|
126
|
+
// oracle on its first edit, and the journal is the contract the differential suite compares.
|
|
127
|
+
//
|
|
128
|
+
// THE CEILING IS A RUN BOUND, so the count starts where the run left off. Starting at 0 gives
|
|
129
|
+
// every activation a full allowance, and a runaway loop that crashed periodically never reaches
|
|
130
|
+
// the ceiling however much it performed against the world.
|
|
131
|
+
const host = {
|
|
132
|
+
journal: run.journal,
|
|
133
|
+
options: {
|
|
134
|
+
runId: run.runId,
|
|
135
|
+
handler: run.handler,
|
|
136
|
+
journal: run.journal,
|
|
137
|
+
pins: run.pins,
|
|
138
|
+
...(run.onLog !== undefined ? { onLog: run.onLog } : {}),
|
|
139
|
+
...(run.shouldStop !== undefined ? { shouldStop: run.shouldStop } : {}),
|
|
140
|
+
},
|
|
141
|
+
ceiling: run.pins.effectCeiling,
|
|
142
|
+
effectCount: run.journal.dispatchedEffects(),
|
|
143
|
+
};
|
|
144
|
+
/** May this frame write into this container? The value half of freeze-on-share, whole. */
|
|
145
|
+
const assertWritable = (target, frame) => {
|
|
146
|
+
if (Object.isFrozen(target)) {
|
|
147
|
+
throw new RuntimeFault("L2031", "this value crossed an effect boundary and is frozen: what crossed is what the journal recorded, so it cannot change afterwards. Build a new value instead: `{ ...record, field: value }` or `[...list, item]`.");
|
|
148
|
+
}
|
|
149
|
+
if (birthDepth(target) < frame.depth) {
|
|
150
|
+
throw new RuntimeFault("L2032", "this value was built outside this concurrent branch and is written inside it. Two branches writing one value is nondeterministic, and it is silent: live they write in completion order, on resume the recorded effects return instantly and they write in launch order, so the value differs and the run takes a path it never recorded. Build the value inside the branch and return it, and read it out of the combinator's result.");
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
// A LOG LINE IS DATA on this engine: a function anywhere inside a logged value is refused HERE, in
|
|
154
|
+
// the one sink every log line leaves through, before it reaches any transport - the in-process host
|
|
155
|
+
// callback or the worker thread, which cannot even clone one (measured, unguarded: the thread died
|
|
156
|
+
// on the host's DataCloneError with the emitted module body in its message). Held whether or not the
|
|
157
|
+
// host is listening, so a program's outcome never depends on who is watching; refused as the builtin
|
|
158
|
+
// refuses (L4016 naming the value and the path), catchable, the way json.stringify refuses. The
|
|
159
|
+
// WALKER is untouched on purpose: it replays every run recorded under language version 1, and a v1
|
|
160
|
+
// record that logs a builtin must replay as recorded. This is a rule of the engine, declared as a
|
|
161
|
+
// divergence in the differential suite.
|
|
162
|
+
const onLog = (line) => {
|
|
163
|
+
line.values.forEach((v, i) => {
|
|
164
|
+
try {
|
|
165
|
+
assertNoCode(v, `log: value ${i + 1}`);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
if (e instanceof NotCrossable)
|
|
169
|
+
throw new RuntimeFault("L4016", e.message);
|
|
170
|
+
throw e;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
run.onLog?.(line);
|
|
174
|
+
};
|
|
175
|
+
const libraryContext = {
|
|
176
|
+
runId: run.runId,
|
|
177
|
+
programHash: run.programHash,
|
|
178
|
+
startedAt: run.pins.startedAt,
|
|
179
|
+
prng,
|
|
180
|
+
onLog,
|
|
181
|
+
assertWritable,
|
|
182
|
+
};
|
|
183
|
+
const methods = {
|
|
184
|
+
array: arrayMethods(libraryContext),
|
|
185
|
+
string: stringMethods(),
|
|
186
|
+
number: numberMethods(),
|
|
187
|
+
};
|
|
188
|
+
const freeNames = new Map(builtins(libraryContext).map(([n, v]) => [n, v]));
|
|
189
|
+
// The two pure primitives and the four event constructors are free VALUES, not journalled effects
|
|
190
|
+
// (design §3A). They come from the SAME table the walker declares them from (perform.ts), wrapped
|
|
191
|
+
// into the internal `(frame, args)` convention here so one `free` serves every name: a second copy
|
|
192
|
+
// of the shapes is a second answer to what a handle or an event descriptor IS on the wire.
|
|
193
|
+
for (const [name, impl] of freeConstructors({ runId: run.runId, programHash: run.programHash, startedAt: run.pins.startedAt })) {
|
|
194
|
+
freeNames.set(name, ((_frame, a) => impl(a)));
|
|
195
|
+
}
|
|
196
|
+
// ---- the calling-convention adapter, both directions ----------------------------------------
|
|
197
|
+
//
|
|
198
|
+
// The walker's convention is `(frame, args)`, and library.ts calls every callback that way. The
|
|
199
|
+
// transform emits plain `async (...args)` closures and never a frame parameter. So the
|
|
200
|
+
// HOST adapts at every crossing, in both directions, and this section is the whole of it.
|
|
201
|
+
//
|
|
202
|
+
// THE INVARIANT: adaptation may never be observable from inside the program. A value
|
|
203
|
+
// the program hands to a library function and reads back must behave AND compare `===` as the one
|
|
204
|
+
// it handed in. Two things follow, and both were live defects before they were rules:
|
|
205
|
+
//
|
|
206
|
+
// * ONLY A POSITION THE LIBRARY CALLS IS ADAPTED. Adapting every function in an argument list
|
|
207
|
+
// rewrote the ones a mutating method STORES: `xs.push(f)` put the walker view in the array,
|
|
208
|
+
// and the program read back a value that neither called (`args is not iterable`) nor compared
|
|
209
|
+
// equal to the one it pushed.
|
|
210
|
+
// * AN ADAPTER IS MINTED ONCE PER VALUE. A fresh closure per read makes `map !== map`, which is
|
|
211
|
+
// false on the walker, where a builtin is one immutable binding for the whole run.
|
|
212
|
+
/**
|
|
213
|
+
* Where a library function calls one of its arguments, by qualified name.
|
|
214
|
+
*
|
|
215
|
+
* Derived from library.ts's `asCallable` sites: the eleven callback-taking array methods take
|
|
216
|
+
* theirs FIRST, the six higher-order builtins take theirs SECOND (the list is first). It is held
|
|
217
|
+
* to those sites BEHAVIOURALLY rather than by reading them: a cell probes every name in every
|
|
218
|
+
* table with a marker function and compares the set of positions the library actually calls to
|
|
219
|
+
* this one, so a table that grows a callback position reds here instead of drifting.
|
|
220
|
+
*/
|
|
221
|
+
const CALLBACK_ARG = new Map([
|
|
222
|
+
...["map", "filter", "find", "findIndex", "findLast", "findLastIndex", "some", "every", "forEach", "reduce", "flatMap"].map((n) => [`array.${n}`, 0]),
|
|
223
|
+
...["map", "filter", "find", "some", "every", "sort"].map((n) => [`builtin.${n}`, 1]),
|
|
224
|
+
]);
|
|
225
|
+
/** Host value -> the program's view of it, so a value has ONE view for the life of the run. */
|
|
226
|
+
const programView = new WeakMap();
|
|
227
|
+
/** A program view -> the host callable it wraps, so a round trip through the seam is the identity. */
|
|
228
|
+
const hostOf = new WeakMap();
|
|
229
|
+
/** Program closure -> the walker's view of it, for the same reason in the other direction. */
|
|
230
|
+
const walkerView = new WeakMap();
|
|
231
|
+
/** A program closure, as library.ts calls it. The frame travels explicitly, not by ambience. */
|
|
232
|
+
const toWalker = (fn) => {
|
|
233
|
+
const underlying = hostOf.get(fn);
|
|
234
|
+
if (underlying !== undefined)
|
|
235
|
+
return underlying;
|
|
236
|
+
const had = walkerView.get(fn);
|
|
237
|
+
if (had !== undefined)
|
|
238
|
+
return had;
|
|
239
|
+
const w = async (frame, args) => await withFrame(frame, () => fn(...args));
|
|
240
|
+
walkerView.set(fn, w);
|
|
241
|
+
return w;
|
|
242
|
+
};
|
|
243
|
+
/** Adapt the ONE argument this library function calls, and nothing else in the list. */
|
|
244
|
+
const adaptArgs = (key, args) => {
|
|
245
|
+
const at = CALLBACK_ARG.get(key);
|
|
246
|
+
if (at === undefined || typeof args[at] !== "function")
|
|
247
|
+
return args;
|
|
248
|
+
const out = args.slice();
|
|
249
|
+
out[at] = toWalker(args[at]);
|
|
250
|
+
return out;
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* A host value on its way OUT to the program: any `(frame, args)` callable in it becomes a plain
|
|
254
|
+
* closure that adapts its own arguments the same way the call form does. Only `json` needs the
|
|
255
|
+
* record walk, and it is walked rather than special-cased so a builtin that grows members later
|
|
256
|
+
* does not silently hand out the wrong convention.
|
|
257
|
+
*/
|
|
258
|
+
const toProgramValue = (name, v) => {
|
|
259
|
+
if (v === null || (typeof v !== "object" && typeof v !== "function"))
|
|
260
|
+
return v;
|
|
261
|
+
const had = programView.get(v);
|
|
262
|
+
if (had !== undefined)
|
|
263
|
+
return had;
|
|
264
|
+
let out;
|
|
265
|
+
if (typeof v === "function") {
|
|
266
|
+
const key = `builtin.${name}`;
|
|
267
|
+
const p = async (...args) => await v(currentFrame(), adaptArgs(key, args));
|
|
268
|
+
hostOf.set(p, v);
|
|
269
|
+
out = p;
|
|
270
|
+
}
|
|
271
|
+
else if (Array.isArray(v)) {
|
|
272
|
+
out = v;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const rec = {};
|
|
276
|
+
for (const [k, inner] of Object.entries(v))
|
|
277
|
+
setOwn(rec, k, toProgramValue(`${name}.${k}`, inner));
|
|
278
|
+
out = deepFreeze(rec);
|
|
279
|
+
}
|
|
280
|
+
programView.set(v, out);
|
|
281
|
+
return out;
|
|
282
|
+
};
|
|
283
|
+
// ---- the concurrency scopes ------------------------------------------------------------------
|
|
284
|
+
//
|
|
285
|
+
// The SAME two functions the walker calls, with the same arguments in the same order:
|
|
286
|
+
// `performScope` owns the journal entry and the replay, `runScope` owns what a scope means. None
|
|
287
|
+
// of a race's winner rule, a fanOut's key rule or a conclave's close lives on this side, because
|
|
288
|
+
// a second copy of that logic would be a second answer to what a scope IS.
|
|
289
|
+
//
|
|
290
|
+
// What this side owns is the two things the engine has that the walker does not: the CALLING
|
|
291
|
+
// CONVENTION (arms arrive as the program's own closures, and the scope machinery calls them
|
|
292
|
+
// `(frame, args)`) and the MISSING AST (a settled race's `branchDigest` is a function of the
|
|
293
|
+
// source, so it arrives as the call site's static payload instead).
|
|
294
|
+
/** An arm, as the scope machinery calls one. A non-function is passed through so the engine fails where the walker fails. */
|
|
295
|
+
const asArm = (v) => (typeof v === "function" ? toWalker(v) : v);
|
|
296
|
+
/** `parallel`/`race` take a record or an array OF ARMS; the other two take data in that position. */
|
|
297
|
+
const branchesOf = (name, first) => {
|
|
298
|
+
if (name !== "parallel" && name !== "race")
|
|
299
|
+
return first;
|
|
300
|
+
if (Array.isArray(first))
|
|
301
|
+
return first.map(asArm);
|
|
302
|
+
if (first === null || typeof first !== "object")
|
|
303
|
+
return first;
|
|
304
|
+
const out = {};
|
|
305
|
+
for (const [k, v] of Object.entries(first))
|
|
306
|
+
setOwn(out, k, asArm(v));
|
|
307
|
+
return out;
|
|
308
|
+
};
|
|
309
|
+
/** `fanOut`'s `key` is called by the scope machinery too. The copy never reaches the program. */
|
|
310
|
+
const bagWithKey = (bag) => {
|
|
311
|
+
const key = option(bag, "key");
|
|
312
|
+
if (typeof key !== "function")
|
|
313
|
+
return bag;
|
|
314
|
+
const out = {};
|
|
315
|
+
for (const [k, v] of Object.entries(bag))
|
|
316
|
+
setOwn(out, k, v);
|
|
317
|
+
setOwn(out, "key", asArm(key));
|
|
318
|
+
return out;
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* The second argument, DEFERRED - and it must be, measured on the walker rather than argued:
|
|
322
|
+
*
|
|
323
|
+
* fanOut(xs, await choose(), { name: "f", key }) journal ["fanOut:f", "sleep:warm"]
|
|
324
|
+
* conclave([a], await choose(), { name: "c" }) journal ["spawn:hire", "conclave:c", "sleep:warm"]
|
|
325
|
+
* fanOut(xs, fn, { name: "f", key: await choose() }) journal ["sleep:warm", "fanOut:f"]
|
|
326
|
+
*
|
|
327
|
+
* The body is evaluated INSIDE the scope, after its entry has begun; the options bag is evaluated
|
|
328
|
+
* before it. So the emitted call hands the body over as a thunk, for the same reason the optional
|
|
329
|
+
* call hands over its arguments as one: an argument that was already evaluated has already
|
|
330
|
+
* journalled its effects in the wrong place, and a resume would replay a step the walker's run
|
|
331
|
+
* never recorded. `parallel` and `race` have no deferred argument - their second is the bag.
|
|
332
|
+
*/
|
|
333
|
+
const deferredBody = (name, args) => async () => {
|
|
334
|
+
if (name === "parallel" || name === "race") {
|
|
335
|
+
throw new RuntimeFault("L1000", `\`${name}\` has no deferred argument; asking for one is an engine fault`);
|
|
336
|
+
}
|
|
337
|
+
const thunk = args[1];
|
|
338
|
+
if (typeof thunk !== "function") {
|
|
339
|
+
throw new RuntimeFault("L1000", `\`${name}\` takes its body UNEVALUATED, as a thunk: the walker evaluates it AFTER the scope's entry has begun (measured: an effect in that position journals inside the scope, one in the options bag journals before it), so a body handed over already evaluated has journalled its effects in the wrong place.`);
|
|
340
|
+
}
|
|
341
|
+
return asArm(await thunk());
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* The `branchDigest`, rebuilt from the call site's payload with the walker's own `digest`.
|
|
345
|
+
*
|
|
346
|
+
* The walker digests `[...losers].sort().map((n) => [n, bodies.get(n) ?? null])` over the arm
|
|
347
|
+
* bodies with positions stripped, and a name the site does not carry digests as `null`. An arm
|
|
348
|
+
* that was RENAMED is exactly the case this has to notice. Absent `branchDigests` means the arms
|
|
349
|
+
* were not written as an object literal at the call, which is where the walker also answers
|
|
350
|
+
* undefined, so the field's presence is the whole decision.
|
|
351
|
+
*/
|
|
352
|
+
const digesterFor = (site) => {
|
|
353
|
+
const bodies = site?.branchDigests;
|
|
354
|
+
if (bodies === undefined)
|
|
355
|
+
return undefined;
|
|
356
|
+
return (losers) => digest([...losers]
|
|
357
|
+
.sort()
|
|
358
|
+
.map((n) => [n, Object.prototype.hasOwnProperty.call(bodies, n) ? bodies[n] : null]));
|
|
359
|
+
};
|
|
360
|
+
const openScope = async (name, spec, args, site) => {
|
|
361
|
+
const frame = currentFrame();
|
|
362
|
+
const scopeKind = name;
|
|
363
|
+
const first = args[0];
|
|
364
|
+
const bag = args[spec.optionsAt];
|
|
365
|
+
const scopeName = option(bag, "name") ?? null;
|
|
366
|
+
// Allocated HERE, synchronously, exactly as the walker allocates it: the occurrence is what
|
|
367
|
+
// makes two textually identical scopes different steps, and a counter read after an await is a
|
|
368
|
+
// counter two scopes can race for.
|
|
369
|
+
const occurrence = frame.keys.nextScope(scopeKind, scopeName);
|
|
370
|
+
const scopeKey = frame.keys.scopeKey(scopeKind, scopeName, occurrence);
|
|
371
|
+
// `conclave` is the one scope whose identity includes a SUBJECT: the members are what the
|
|
372
|
+
// sub-team IS, so editing the member list diverges rather than resuming into a different room.
|
|
373
|
+
const subject = spec.hashesSubject
|
|
374
|
+
? {
|
|
375
|
+
members: first.map((m) => m.agent),
|
|
376
|
+
channel: option(bag, "channel") ?? null,
|
|
377
|
+
}
|
|
378
|
+
: undefined;
|
|
379
|
+
return await performScope(host, scopeKey, frame, async (ctx, only) => await runScope(host, name, scopeKind, scopeName, occurrence, branchesOf(name, first), deferredBody(name, args), bagWithKey(bag), frame, ctx, only), subject,
|
|
380
|
+
// `race` alone, as on the walker: `parallel` and `fanOut` have no losers, and a `conclave`
|
|
381
|
+
// cannot be walked into at all.
|
|
382
|
+
name === "race" ? digesterFor(site) : undefined);
|
|
383
|
+
};
|
|
384
|
+
// ---- fuel ------------------------------------------------------------------------------------
|
|
385
|
+
//
|
|
386
|
+
// The unit CHANGES from the walker's: the walker charges one dispatch per node it walks, the
|
|
387
|
+
// engine charges one transformed-site hit. That is languageVersion 2's pin-unit change, and it is
|
|
388
|
+
// why L4013 is a FIRST-PARTY cell rather than a differential one: journals are unaffected,
|
|
389
|
+
// because steps are never recorded, but the budget fires at different points on the two engines.
|
|
390
|
+
let steps = 0;
|
|
391
|
+
let nextYield = run.pins.yieldEvery;
|
|
392
|
+
const breathe = async (frame) => {
|
|
393
|
+
// The MACROTASK queue, not a microtask. A program that only ever yields microtasks starves the
|
|
394
|
+
// host completely: a watchdog's setTimeout never fires, and the run takes down the timer plane
|
|
395
|
+
// and every other run on the host with it.
|
|
396
|
+
await new Promise((resolve) => {
|
|
397
|
+
setTimeout(resolve, 0);
|
|
398
|
+
});
|
|
399
|
+
// The cut, and only the cut: an arm that can no longer win is abandoned here. An arm that could
|
|
400
|
+
// still win keeps running its pure work, so a live race is decided by the recorded clocks and
|
|
401
|
+
// declaration order rather than by how many steps a tail happens to take against `yieldEvery`.
|
|
402
|
+
if (frame.signal.cutPure)
|
|
403
|
+
throw new Cancelled(frame.signal.reason ?? "cancelled");
|
|
404
|
+
};
|
|
405
|
+
const fuel = () => {
|
|
406
|
+
steps += 1;
|
|
407
|
+
if (steps > run.pins.stepBudget) {
|
|
408
|
+
throw new RuntimeFault("L4013", `this walk has taken more than ${run.pins.stepBudget} interpreter steps without finishing, which means a loop that performs no effect is not terminating. The effect ceiling cannot see such a loop, because it performs nothing to count. Add an exit condition, or raise stepBudget if the program legitimately does this much work. (stepBudget bounds ONE WALK, not the run: steps are not recorded, so a resume cannot recover a count the way the effect ceiling can.)`);
|
|
409
|
+
}
|
|
410
|
+
// Returns nothing on the common path, so the emitted `await __ctx.fuel()` costs a bare microtask
|
|
411
|
+
// rather than an allocated promise.
|
|
412
|
+
if (steps < nextYield)
|
|
413
|
+
return;
|
|
414
|
+
nextYield = steps + run.pins.yieldEvery;
|
|
415
|
+
return breathe(currentFrame());
|
|
416
|
+
};
|
|
417
|
+
// ---- members ---------------------------------------------------------------------------------
|
|
418
|
+
/** The property key a member expression names, as JavaScript would spell it. */
|
|
419
|
+
const keyOf = (k) => {
|
|
420
|
+
if (typeof k === "string")
|
|
421
|
+
return k;
|
|
422
|
+
refuseCoercion("[...]", k);
|
|
423
|
+
return String(k);
|
|
424
|
+
};
|
|
425
|
+
const methodOf = (table, receiver, prop, kind, asCallee) => {
|
|
426
|
+
const m = table[prop];
|
|
427
|
+
if (m === undefined) {
|
|
428
|
+
throw new RuntimeFault("L4014", `\`${prop}\` is not a member of ${kind}. The members are: length, an index, ${Object.keys(table).join(", ")}.`);
|
|
429
|
+
}
|
|
430
|
+
// A method is looked up AT THE CALL and exists nowhere else, a declared difference from
|
|
431
|
+
// JavaScript, where `xs.map` is a value. Handing one out gives `xs.map !== xs.map` and an
|
|
432
|
+
// extracted `push` that writes to a receiver strict JavaScript would refuse.
|
|
433
|
+
if (!asCallee) {
|
|
434
|
+
throw new RuntimeFault("L4020", `\`${prop}\` is a method of ${kind}, and a method is not a value here: it is looked up at the call, so it cannot be extracted, compared, or passed. Call it — \`.${prop}(...)\` — or wrap it: \`(...args) => value.${prop}(...args)\`.`);
|
|
435
|
+
}
|
|
436
|
+
return async (frame, args) => await m(frame, receiver, args);
|
|
437
|
+
};
|
|
438
|
+
const memberOf = (obj, prop, asCallee) => {
|
|
439
|
+
switch (typeof obj) {
|
|
440
|
+
case "string": {
|
|
441
|
+
if (prop === "length")
|
|
442
|
+
return obj.length;
|
|
443
|
+
const i = arrayIndex(prop);
|
|
444
|
+
if (i !== undefined)
|
|
445
|
+
return obj[i];
|
|
446
|
+
return methodOf(methods.string, obj, prop, "a string", asCallee);
|
|
447
|
+
}
|
|
448
|
+
case "number":
|
|
449
|
+
return methodOf(methods.number, obj, prop, "a number", asCallee);
|
|
450
|
+
case "object": {
|
|
451
|
+
if (obj === null)
|
|
452
|
+
throw new RuntimeFault("L4010", `cannot read \`${prop}\` of null`);
|
|
453
|
+
if (Array.isArray(obj)) {
|
|
454
|
+
if (prop === "length")
|
|
455
|
+
return obj.length;
|
|
456
|
+
const i = arrayIndex(prop);
|
|
457
|
+
if (i !== undefined)
|
|
458
|
+
return obj[i];
|
|
459
|
+
return methodOf(methods.array, obj, prop, "an array", asCallee);
|
|
460
|
+
}
|
|
461
|
+
// A record answers its OWN fields and `undefined` for anything else, so no host prototype is
|
|
462
|
+
// ever reached: `o.constructor`, `o.toString` and `o.hasOwnProperty` are all `undefined`.
|
|
463
|
+
return Object.prototype.hasOwnProperty.call(obj, prop) ? obj[prop] : undefined;
|
|
464
|
+
}
|
|
465
|
+
case "undefined":
|
|
466
|
+
throw new RuntimeFault("L4010", `cannot read \`${prop}\` of undefined`);
|
|
467
|
+
default:
|
|
468
|
+
throw new RuntimeFault("L4014", `\`${prop}\` is not a member: a ${typeof obj} has no members`);
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
/** Which curated table a receiver answers from, and the first half of a member's qualified name. */
|
|
472
|
+
const tableKind = (obj) => typeof obj === "string" ? "string" : typeof obj === "number" ? "number" : Array.isArray(obj) ? "array" : undefined;
|
|
473
|
+
/**
|
|
474
|
+
* The same lookup as {@link memberOf}, keeping WHERE the member came from.
|
|
475
|
+
*
|
|
476
|
+
* The two paths are told apart by the NAME, never by what the name answered. Deciding on
|
|
477
|
+
* `typeof v === "function"` reads right and is wrong: an array element can itself be a program
|
|
478
|
+
* closure, so `fs[0]("a")` classified as a curated method and was called `(frame, ["a"])`, with
|
|
479
|
+
* the frame handed to the program as its first argument, which is the hazard this whole section
|
|
480
|
+
* exists to close. Measured on `const fs = [(x) => x]` before the fix.
|
|
481
|
+
*/
|
|
482
|
+
const lookup = (obj, prop, asCallee) => {
|
|
483
|
+
const kind = tableKind(obj);
|
|
484
|
+
if (kind === undefined)
|
|
485
|
+
return { from: "own", value: memberOf(obj, prop, asCallee) };
|
|
486
|
+
if (typeof obj === "string" || Array.isArray(obj)) {
|
|
487
|
+
if (prop === "length" || arrayIndex(prop) !== undefined)
|
|
488
|
+
return { from: "own", value: memberOf(obj, prop, asCallee) };
|
|
489
|
+
}
|
|
490
|
+
return { from: "table", fn: memberOf(obj, prop, asCallee), key: `${kind}.${prop}` };
|
|
491
|
+
};
|
|
492
|
+
/**
|
|
493
|
+
* THE CELL TEST, in ONE place because both doors ask the same question.
|
|
494
|
+
*
|
|
495
|
+
* A binding the transform turned into a cell exists for its whole block, and the cell record is
|
|
496
|
+
* hoisted to the top of that block so the closures capturing it have something to close over. What
|
|
497
|
+
* decides whether the DECLARATION has run is whether the key is THERE - `hasOwn`, never truthiness,
|
|
498
|
+
* because a binding initialised to `undefined` has run and a truthiness test would refuse it.
|
|
499
|
+
*
|
|
500
|
+
* Written once rather than at each door: a duplicated guard is one whose mutant can be defeated by
|
|
501
|
+
* copying it, and the read and the write differ in their SENTENCE, not in this question.
|
|
502
|
+
*/
|
|
503
|
+
const declarationHasRun = (cell, prop) => cell !== null && typeof cell === "object" && Object.prototype.hasOwnProperty.call(cell, prop);
|
|
504
|
+
const ctx = {
|
|
505
|
+
fuel,
|
|
506
|
+
get(o, k, binding) {
|
|
507
|
+
const prop = keyOf(k);
|
|
508
|
+
// THE CELL DOOR. A binding the transform turned into a cell exists for its whole block, and
|
|
509
|
+
// the cell record is hoisted to the top of that block so the closures capturing it have
|
|
510
|
+
// something to close over. What decides whether the DECLARATION has run is whether the key is
|
|
511
|
+
// there - `hasOwn`, never truthiness, because a binding initialised to `undefined` has run.
|
|
512
|
+
// The walker's own words, so a program cannot tell which engine refused it.
|
|
513
|
+
if (binding !== undefined && !declarationHasRun(o, prop)) {
|
|
514
|
+
throw new RuntimeFault("L2004", `${binding} is used before its declaration was reached: the binding exists for the whole block, but it holds no value until the \`let\`/\`const\` line runs. Call this function after the declaration, or move the declaration up.`);
|
|
515
|
+
}
|
|
516
|
+
return memberOf(o, prop, false);
|
|
517
|
+
},
|
|
518
|
+
set(o, k, v, binding) {
|
|
519
|
+
const prop = keyOf(k);
|
|
520
|
+
// THE OTHER CELL DOOR, and it is a SEPARATE refusal rather than the read's reused: the walker
|
|
521
|
+
// answers a different sentence for a write, and a program that could tell the two engines apart
|
|
522
|
+
// by which sentence it caught would be a divergence dressed as a message. Both are catchable,
|
|
523
|
+
// both leave the binding uninitialised, and the declaration still initialises it when reached.
|
|
524
|
+
// Emitted only on an assignment - the declaration's own `set` passes three arguments - so a
|
|
525
|
+
// cell being written for the first time by its `let` line never comes through here.
|
|
526
|
+
if (binding !== undefined && !declarationHasRun(o, prop)) {
|
|
527
|
+
throw new RuntimeFault("L2004", `${binding} is assigned before its declaration was reached: the binding exists for the whole block, but it holds no value until the \`let\`/\`const\` line runs.`);
|
|
528
|
+
}
|
|
529
|
+
if (o === null || o === undefined || typeof o !== "object") {
|
|
530
|
+
throw new RuntimeFault("L4010", `cannot write \`${prop}\` of ${o === null ? "null" : typeof o === "undefined" ? "undefined" : `a ${typeof o}`}`);
|
|
531
|
+
}
|
|
532
|
+
assertWritable(o, currentFrame());
|
|
533
|
+
if (Array.isArray(o)) {
|
|
534
|
+
if (prop === "length") {
|
|
535
|
+
// `xs.length = n` truncates, as in JavaScript. A LONGER length is refused: JavaScript
|
|
536
|
+
// would fill the gap with holes, and a hole is a value class this language does not have.
|
|
537
|
+
if (typeof v !== "number" || !Number.isInteger(v) || v < 0 || v > o.length) {
|
|
538
|
+
throw new RuntimeFault("L4017", `\`length\` can only be set to an integer between 0 and the array's current length (${o.length}), got ${typeof v === "number" ? v : typeof v}: a longer length would create holes, which this language does not have; push the elements instead`);
|
|
539
|
+
}
|
|
540
|
+
o.length = v;
|
|
541
|
+
return v;
|
|
542
|
+
}
|
|
543
|
+
const i = arrayIndex(prop);
|
|
544
|
+
if (i === undefined) {
|
|
545
|
+
throw new RuntimeFault("L4014", `\`${prop}\` is not a member of an array: an array takes an index or \`length\``);
|
|
546
|
+
}
|
|
547
|
+
if (i > o.length) {
|
|
548
|
+
throw new RuntimeFault("L4019", `index ${i} is past the end of this array (length ${o.length}), and JavaScript would fill the gap with holes, which this language does not have. Write at an existing index, at the length to append, or use \`push\`.`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else if (prop === "__proto__") {
|
|
552
|
+
throw new RuntimeFault("L4014", "`__proto__` names an object's prototype, and there are no prototypes here");
|
|
553
|
+
}
|
|
554
|
+
// THE SECOND DOOR OF THE THENABLE GATE, and it is LAST on purpose. A record can acquire a
|
|
555
|
+
// callable `then` after birth, and a computed key reaches it past any static spelling
|
|
556
|
+
// (`x["th" + "en"] = f`). But it applies to a RECORD and to nothing else, because the walker's
|
|
557
|
+
// order is: what kind of thing is being written (L4010), is it frozen (L2031), does that kind
|
|
558
|
+
// have this member (L4014/L4017/L4019) - and only then what the value is. MEASURED on the
|
|
559
|
+
// oracle: `keys({a:1}).then = () => 1` is L4014 "`then` is not a member of an array", the same
|
|
560
|
+
// answer as `.foo`, and `"x".then = f` and `(1).then = f` are L4010. Refusing L4018 here first
|
|
561
|
+
// answered the value's rule for a receiver that never had the member, which is a different
|
|
562
|
+
// sentence for the same program. On a record BOTH now refuse L4021: #657 gave the walker the
|
|
563
|
+
// rule at its record-member write sites, so what was the last declared divergence of this
|
|
564
|
+
// reordering is a corpus row the differential can compare.
|
|
565
|
+
if (prop === "then")
|
|
566
|
+
refuseThenable({ then: v }, "this field write");
|
|
567
|
+
setOwn(o, prop, v);
|
|
568
|
+
return v;
|
|
569
|
+
},
|
|
570
|
+
async call(o, k, args, optional, chain) {
|
|
571
|
+
// THE LOOKUP HAPPENS FIRST, AND `?.` DOES NOT SOFTEN IT. Measured on the walker: `xs.nope?.()`
|
|
572
|
+
// is L4014 exactly as `xs.nope()` is, and a member that resolves to a non-function is L4011.
|
|
573
|
+
// The only thing an optional call guards is a member that is null or undefined.
|
|
574
|
+
const found = lookup(o, keyOf(k), true);
|
|
575
|
+
// AN OPTIONAL CALL EVALUATES NO ARGUMENT WHEN IT SHORT-CIRCUITS, which is why the optional
|
|
576
|
+
// form takes a thunk: the transform evaluates arguments before it can call anything, so an
|
|
577
|
+
// array here would already have run them. Measured: the walker's short-circuit journalled
|
|
578
|
+
// nothing where the same argument on a present method journalled a `sleep`.
|
|
579
|
+
// A continuation without a short-circuit to guard is an emitter mistake: an ordinary call's
|
|
580
|
+
// chain is written natively, because nothing in it depends on a decision only the host made.
|
|
581
|
+
if (chain !== undefined && optional !== true) {
|
|
582
|
+
throw new RuntimeFault("L1000", "a call continuation belongs to an OPTIONAL call: there is nothing else for it to be skipped by");
|
|
583
|
+
}
|
|
584
|
+
if (optional === true && typeof args !== "function") {
|
|
585
|
+
throw new RuntimeFault("L1000", "an optional call must be handed its arguments as a thunk: it may not evaluate them at all, and an array is a list that has already been evaluated");
|
|
586
|
+
}
|
|
587
|
+
// Nothing runs: not the arguments, and not the rest of the chain. Measured on the walker, the
|
|
588
|
+
// short-circuit swallows a deep chain (`o.z?.().x.y`) and a trailing call alike.
|
|
589
|
+
if (found.from === "own" && (found.value === null || found.value === undefined) && optional === true) {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
// AWAITED, because the thunk is `async`: every argument the transform emits may itself contain
|
|
593
|
+
// an `await`, so a sync arrow could not hold one. Measured without the await: an ordinary
|
|
594
|
+
// `o.m?.(1)` died on `Spread syntax requires ...iterable`, and `xs.map?.(f)` reached the
|
|
595
|
+
// curated method with a Promise where its argument list should be.
|
|
596
|
+
const list = typeof args === "function" ? await args() : args;
|
|
597
|
+
let answer;
|
|
598
|
+
if (found.from === "table") {
|
|
599
|
+
// A curated method: the walker's convention, and the ONE argument this method calls is
|
|
600
|
+
// adapted into it on the way in. Every other argument crosses untouched: see the invariant.
|
|
601
|
+
answer = await found.fn(currentFrame(), adaptArgs(found.key, list));
|
|
602
|
+
}
|
|
603
|
+
else if (typeof found.value !== "function") {
|
|
604
|
+
throw new RuntimeFault("L4011", "this value is not a function, so it cannot be called");
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
// An own field holds a program-convention closure. Adapting here would pass the frame in as
|
|
608
|
+
// the first argument and shift every real one along by a position.
|
|
609
|
+
answer = await found.value(...list);
|
|
610
|
+
}
|
|
611
|
+
return chain === undefined ? answer : await chain(answer);
|
|
612
|
+
},
|
|
613
|
+
born(v) {
|
|
614
|
+
refuseThenable(v, "this value");
|
|
615
|
+
return stampBirth(v, currentFrame().depth);
|
|
616
|
+
},
|
|
617
|
+
async effect(name, args, site) {
|
|
618
|
+
const spec = PRIMITIVES[name];
|
|
619
|
+
if (spec === undefined)
|
|
620
|
+
throw new RuntimeFault("L2001", `${name} is not a primitive`);
|
|
621
|
+
if (spec.opensScope)
|
|
622
|
+
return await openScope(name, spec, args, site);
|
|
623
|
+
return await dispatchPrimitive(host, name, args, currentFrame());
|
|
624
|
+
},
|
|
625
|
+
free(name, args) {
|
|
626
|
+
const v = freeNames.get(name);
|
|
627
|
+
if (v === undefined && !freeNames.has(name)) {
|
|
628
|
+
throw new RuntimeFault("L2001", `${name} is not a builtin`);
|
|
629
|
+
}
|
|
630
|
+
// Read as a VALUE when no arguments are given: a builtin is a binding in this language, so
|
|
631
|
+
// `map(xs, upper)` and `const f = trim` both need one, and what leaves has to speak the
|
|
632
|
+
// program's convention or a native call would pass `(x)` where the library expects
|
|
633
|
+
// `(frame, [x])`.
|
|
634
|
+
if (args === undefined)
|
|
635
|
+
return toProgramValue(name, v);
|
|
636
|
+
if (typeof v !== "function") {
|
|
637
|
+
throw new RuntimeFault("L4011", `\`${name}\` is not a function, so it cannot be called`);
|
|
638
|
+
}
|
|
639
|
+
return v(currentFrame(), adaptArgs(`builtin.${name}`, args));
|
|
640
|
+
},
|
|
641
|
+
async await(v) {
|
|
642
|
+
// BEFORE the await, which is the whole point: once `await` has the value, the host has
|
|
643
|
+
// already called its `then`.
|
|
644
|
+
refuseThenable(v, "this awaited value");
|
|
645
|
+
return await v;
|
|
646
|
+
},
|
|
647
|
+
template(parts, values) {
|
|
648
|
+
let out = "";
|
|
649
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
650
|
+
out += parts[i] ?? "";
|
|
651
|
+
if (i < values.length) {
|
|
652
|
+
const v = values[i];
|
|
653
|
+
refuseCoercion("${...}", v);
|
|
654
|
+
out += String(v);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return out;
|
|
658
|
+
},
|
|
659
|
+
binary(op, l, r) {
|
|
660
|
+
const a = l;
|
|
661
|
+
const b = r;
|
|
662
|
+
switch (op) {
|
|
663
|
+
case "===":
|
|
664
|
+
return l === r;
|
|
665
|
+
case "!==":
|
|
666
|
+
return l !== r;
|
|
667
|
+
default:
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
refuseCoercion(op, l);
|
|
671
|
+
refuseCoercion(op, r);
|
|
672
|
+
switch (op) {
|
|
673
|
+
case "<":
|
|
674
|
+
return a < b;
|
|
675
|
+
case "<=":
|
|
676
|
+
return a <= b;
|
|
677
|
+
case ">":
|
|
678
|
+
return a > b;
|
|
679
|
+
case ">=":
|
|
680
|
+
return a >= b;
|
|
681
|
+
case "+":
|
|
682
|
+
return a + b;
|
|
683
|
+
case "-":
|
|
684
|
+
return a - b;
|
|
685
|
+
case "*":
|
|
686
|
+
return a * b;
|
|
687
|
+
case "/":
|
|
688
|
+
return a / b;
|
|
689
|
+
case "%":
|
|
690
|
+
return a % b;
|
|
691
|
+
case "**":
|
|
692
|
+
return a ** b;
|
|
693
|
+
case "&":
|
|
694
|
+
return a & b;
|
|
695
|
+
case "|":
|
|
696
|
+
return a | b;
|
|
697
|
+
case "^":
|
|
698
|
+
return a ^ b;
|
|
699
|
+
case "<<":
|
|
700
|
+
return a << b;
|
|
701
|
+
case ">>":
|
|
702
|
+
return a >> b;
|
|
703
|
+
case ">>>":
|
|
704
|
+
return a >>> b;
|
|
705
|
+
default:
|
|
706
|
+
throw new RuntimeFault("L1000", `unsupported operator ${op}`);
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
unary(op, v) {
|
|
710
|
+
switch (op) {
|
|
711
|
+
case "!":
|
|
712
|
+
return !v;
|
|
713
|
+
case "typeof":
|
|
714
|
+
return typeof v;
|
|
715
|
+
case "-":
|
|
716
|
+
refuseCoercion("-", v);
|
|
717
|
+
return -v;
|
|
718
|
+
case "+":
|
|
719
|
+
refuseCoercion("+", v);
|
|
720
|
+
return +v;
|
|
721
|
+
case "~":
|
|
722
|
+
refuseCoercion("~", v);
|
|
723
|
+
return ~v;
|
|
724
|
+
case "update":
|
|
725
|
+
// `x++`, `x--` and their compound cousins, on the slow path only: the transform emits a
|
|
726
|
+
// native increment when it can see the operand is a number. A DECLARED DIVERGENCE: the
|
|
727
|
+
// walker reads the operand through `Number(...)`, so `"5"++` answers 6 and a
|
|
728
|
+
// record settles as NaN, while `o + 1` and `x += 1` refuse on the very same values. That
|
|
729
|
+
// is the silent-coercion class, filed against the walker as issue #646, and it is not
|
|
730
|
+
// being built into the new engine for fidelity's sake.
|
|
731
|
+
if (typeof v !== "number") {
|
|
732
|
+
throw new RuntimeFault("L4018", `\`++\` and \`--\` count, and ${v === null ? "null" : Array.isArray(v) ? "an array" : `a ${typeof v}`} is not a number, so there is nothing to count. Nothing is converted for you here: parse it first (\`number(value)\`), or hold the counter in a number.`);
|
|
733
|
+
}
|
|
734
|
+
return v;
|
|
735
|
+
default:
|
|
736
|
+
throw new RuntimeFault("L1000", `unsupported unary operator ${String(op)}`);
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
iter(v) {
|
|
740
|
+
if (Array.isArray(v))
|
|
741
|
+
return v;
|
|
742
|
+
if (typeof v === "string")
|
|
743
|
+
return [...v];
|
|
744
|
+
throw new RuntimeFault("L4015", `${v === null ? "null" : typeof v === "object" ? "a record" : typeof v} is not iterable: only arrays and strings can be spread or looped over. For a record, iterate \`keys(record)\` or \`entries(record)\`.`);
|
|
745
|
+
},
|
|
746
|
+
callee(v) {
|
|
747
|
+
// Member 14, the last of the seam. The transform emits it behind a `typeof` so a real call
|
|
748
|
+
// stays a native call; this is only the refusal, and it is the walker's own words because the
|
|
749
|
+
// differential suite compares the message, not merely the code.
|
|
750
|
+
if (typeof v !== "function") {
|
|
751
|
+
throw new RuntimeFault("L4011", `this value is not a function, so it cannot be called`);
|
|
752
|
+
}
|
|
753
|
+
return v;
|
|
754
|
+
},
|
|
755
|
+
caught(e) {
|
|
756
|
+
// The run's continuation is forfeit for these, and that includes its cleanup: the world-side
|
|
757
|
+
// recovery belongs to the driver and the journal, not to the program that just lost the right
|
|
758
|
+
// to run. They are recognised by CLASS, which is why the emitted catch has to ask rather than
|
|
759
|
+
// test a shape the program could forge.
|
|
760
|
+
if (isUncatchable(e))
|
|
761
|
+
throw e;
|
|
762
|
+
// A NATIVE ReferenceError IS THE ENGINE'S FAULT, NOT THE PROGRAM'S. The emitted module is
|
|
763
|
+
// closed over the seam with ZERO free identifiers, so nothing in it can name a binding that
|
|
764
|
+
// does not exist: a ReferenceError here can only be a TDZ read the transform's classifier
|
|
765
|
+
// missed, or an emitter temporary used before it was bound. Converting it would hand the
|
|
766
|
+
// program a `{code: "L4000"}` for a compiler bug, and MAPPING it to L2004 by reading its
|
|
767
|
+
// message would be worse - it would make the two indistinguishable exactly where they must not
|
|
768
|
+
// be. So it refuses loudly and uncatchably, with the original message carried along.
|
|
769
|
+
if (e instanceof ReferenceError)
|
|
770
|
+
throw new EngineFault(e);
|
|
771
|
+
return toProgramError(e);
|
|
772
|
+
},
|
|
773
|
+
};
|
|
774
|
+
return Object.defineProperty(ctx, STEPS, {
|
|
775
|
+
value: () => steps,
|
|
776
|
+
enumerable: false,
|
|
777
|
+
writable: false,
|
|
778
|
+
configurable: false,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
// ---- the classes a program may not catch, and the value one that it may -------------------------
|
|
782
|
+
/**
|
|
783
|
+
* The engine broke its own contract, and no program may catch that.
|
|
784
|
+
*
|
|
785
|
+
* The one thing that reaches this today is a native `ReferenceError`. The emitted module is a closed
|
|
786
|
+
* expression with zero free identifiers, so nothing the program wrote can name a binding that is not
|
|
787
|
+
* there; a ReferenceError therefore means a TDZ read the transform's classifier missed or an emitter
|
|
788
|
+
* temporary read before it was bound. It is not the program's error, it is not converted to one, and
|
|
789
|
+
* it is never mapped to L2004 by reading its message - that mapping would make a compiler bug
|
|
790
|
+
* indistinguishable from the language rule it imitates.
|
|
791
|
+
*/
|
|
792
|
+
export class EngineFault extends Error {
|
|
793
|
+
cause;
|
|
794
|
+
constructor(cause) {
|
|
795
|
+
super(`the engine broke its own contract: ${cause?.message ?? String(cause)}. The emitted module has zero free identifiers, so this can only be a binding the transform did not classify as a cell, or an emitter temporary read before it was bound. It is not the program's error and is not converted into one.`);
|
|
796
|
+
this.cause = cause;
|
|
797
|
+
this.name = "EngineFault";
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Recognised by class rather than by shape.
|
|
802
|
+
*
|
|
803
|
+
* `Cancelled` and `RunReleased` come from effects.ts, `JournalAppendRejected` from journal.ts. The
|
|
804
|
+
* three the walker adds (`RunDivergence`, `ScopeBranchMissing`, `UnwalkableScope`) are exported by
|
|
805
|
+
* interpret.ts, and importing them here would pull the whole walker into the engine's module graph
|
|
806
|
+
* for three constructors. They are matched by `name` instead, which is a deliberate, narrow
|
|
807
|
+
* exception to "recognise by class": the names are the classes' own, they are set in the
|
|
808
|
+
* constructors, and a program cannot mint an Error subclass to forge one (there is no `Error` in the
|
|
809
|
+
* language). The differential suite carries a cell per class.
|
|
810
|
+
*/
|
|
811
|
+
const UNCATCHABLE_NAMES = new Set([
|
|
812
|
+
"EngineFault",
|
|
813
|
+
"Cancelled",
|
|
814
|
+
"JournalAppendRejected",
|
|
815
|
+
"RunReleased",
|
|
816
|
+
"RunDivergence",
|
|
817
|
+
"ScopeBranchMissing",
|
|
818
|
+
"UnwalkableScope",
|
|
819
|
+
]);
|
|
820
|
+
function isUncatchable(e) {
|
|
821
|
+
return e instanceof Error && UNCATCHABLE_NAMES.has(e.name);
|
|
822
|
+
}
|
|
823
|
+
/** What the catch parameter binds to: a frozen record with a code, never the host's own object. */
|
|
824
|
+
function toProgramError(e) {
|
|
825
|
+
if (e instanceof EffectError) {
|
|
826
|
+
return deepFreeze({ code: e.code, kind: e.kind, message: e.message, ...(e.detail !== undefined ? { detail: e.detail } : {}) });
|
|
827
|
+
}
|
|
828
|
+
if (e instanceof RuntimeFault)
|
|
829
|
+
return deepFreeze({ code: e.code, kind: "runtime", message: e.message });
|
|
830
|
+
if (e instanceof Error)
|
|
831
|
+
return deepFreeze({ code: "L4000", kind: "host", message: e.message });
|
|
832
|
+
return e;
|
|
833
|
+
}
|
|
834
|
+
//# sourceMappingURL=ctx.js.map
|