small-python 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +52 -2
- package/index.d.ts +81 -0
- package/index.js +611 -0
- package/licenses/python.txt +277 -0
- package/licenses/unicode.txt +39 -0
- package/package.json +40 -4
- package/spy-worker.wasm +0 -0
- package/wire.js +230 -0
- package/worker.js +88 -0
package/index.js
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
// Run Small Python from JavaScript. Each Runtime runs the engine, compiled to
|
|
2
|
+
// WebAssembly, in a Worker thread of its own and speaks the worker protocol
|
|
3
|
+
// to it (docs/packaging/protocol.md, docs/packaging/javascript.md).
|
|
4
|
+
|
|
5
|
+
import * as wire from "./wire.js";
|
|
6
|
+
import { Complex, Tuple, Violation } from "./wire.js";
|
|
7
|
+
|
|
8
|
+
export { Complex };
|
|
9
|
+
|
|
10
|
+
const node = typeof process === "object" && process.versions?.node;
|
|
11
|
+
const NodeWorker = node ? (await import("node:worker_threads")).Worker : null;
|
|
12
|
+
|
|
13
|
+
const INITIAL_PAGES = 256; // spy-worker.wasm is linked with 16 MiB of initial memory
|
|
14
|
+
const MAX_PAGES = 65536;
|
|
15
|
+
const INBOX = 64 * 1024;
|
|
16
|
+
|
|
17
|
+
let compiled;
|
|
18
|
+
function compile() {
|
|
19
|
+
compiled ??= (async () => {
|
|
20
|
+
const url = new URL("./spy-worker.wasm", import.meta.url);
|
|
21
|
+
const bytes = node ? await (await import("node:fs/promises")).readFile(url) : await (await fetch(url)).arrayBuffer();
|
|
22
|
+
return WebAssembly.compile(bytes);
|
|
23
|
+
})();
|
|
24
|
+
return compiled;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** An engine exception that escaped. */
|
|
28
|
+
export class PythonError extends Error {
|
|
29
|
+
constructor(type, message, exception = null, filename = null, lineno = null, options) {
|
|
30
|
+
super(message, options);
|
|
31
|
+
this.name = "PythonError";
|
|
32
|
+
this.type = type;
|
|
33
|
+
this.exception = exception;
|
|
34
|
+
this.filename = filename;
|
|
35
|
+
this.lineno = lineno;
|
|
36
|
+
}
|
|
37
|
+
toString() { return this.message ? `${this.type}: ${this.message}` : String(this.type); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The worker was lost: it crashed, broke the protocol, or was terminated. */
|
|
41
|
+
export class WorkerError extends Error {
|
|
42
|
+
constructor(message, returncode = null) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = "WorkerError";
|
|
45
|
+
this.returncode = returncode;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** An operation ran past the runtime's timeout; the worker was terminated. */
|
|
50
|
+
export class Timeout extends WorkerError {
|
|
51
|
+
constructor(message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "Timeout";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const KEYWORDS = Symbol("small-python keywords");
|
|
58
|
+
|
|
59
|
+
/** Mark a host function as taking keyword arguments: fn(...args, kwargs). */
|
|
60
|
+
export function keywords(fn) {
|
|
61
|
+
if (typeof fn !== "function") throw new TypeError("keywords() needs a function");
|
|
62
|
+
const wrapped = function (...args) { return fn.apply(this, args); };
|
|
63
|
+
Object.defineProperty(wrapped, "name", { value: fn.name });
|
|
64
|
+
wrapped[KEYWORDS] = true;
|
|
65
|
+
return wrapped;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const CREATE = Symbol("create");
|
|
69
|
+
const released = new FinalizationRegistry(({ runtime, id }) => runtime._release(id));
|
|
70
|
+
|
|
71
|
+
export class Runtime {
|
|
72
|
+
/**
|
|
73
|
+
* Start a runtime and its Worker.
|
|
74
|
+
* @param {object} [options]
|
|
75
|
+
*/
|
|
76
|
+
static async create(options = {}) {
|
|
77
|
+
const runtime = new Runtime(CREATE, options);
|
|
78
|
+
await runtime._start(options.std ?? true);
|
|
79
|
+
return runtime;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
constructor(token, options) {
|
|
83
|
+
if (token !== CREATE) throw new TypeError("use Runtime.create()");
|
|
84
|
+
const { stepLimit = 0, memoryLimit = 0, timeout, output, loader, messageLimit = 64 * 2 ** 20 } = options;
|
|
85
|
+
for (const [name, limit] of [["stepLimit", stepLimit], ["memoryLimit", memoryLimit]])
|
|
86
|
+
if (!Number.isSafeInteger(limit) || limit < 0) throw new RangeError(`${name} must be an integer >= 0`);
|
|
87
|
+
if (!Number.isSafeInteger(messageLimit) || messageLimit < 1 || messageLimit > 0xffffffff)
|
|
88
|
+
throw new RangeError("messageLimit must be an integer from 1 to 2**32 - 1");
|
|
89
|
+
if (timeout !== undefined && !(timeout > 0)) throw new RangeError("timeout must be positive or undefined");
|
|
90
|
+
if ((output !== undefined && typeof output !== "function") || (loader !== undefined && typeof loader !== "function"))
|
|
91
|
+
throw new TypeError("output and loader must be functions or undefined");
|
|
92
|
+
this._stepLimit = stepLimit;
|
|
93
|
+
this._memoryLimit = memoryLimit;
|
|
94
|
+
this._timeout = timeout;
|
|
95
|
+
this._output = output;
|
|
96
|
+
this._loader = loader;
|
|
97
|
+
this._messageLimit = messageLimit;
|
|
98
|
+
this._functions = new Map(); // function id -> { fn, keywords }
|
|
99
|
+
this._functionIds = new Map(); // fn -> function id
|
|
100
|
+
this._released = []; // handle ids to send with the next message
|
|
101
|
+
this._failure = null; // { token, error } last raised into the engine
|
|
102
|
+
this._tokens = 0;
|
|
103
|
+
this._levels = [{ tail: Promise.resolve() }]; // one queue per callback depth
|
|
104
|
+
this._timer = null;
|
|
105
|
+
this._remaining = undefined;
|
|
106
|
+
this._line = "";
|
|
107
|
+
this._incoming = []; // chunks not yet framed
|
|
108
|
+
this._incomingLength = 0;
|
|
109
|
+
this._frames = [];
|
|
110
|
+
this._waiter = null;
|
|
111
|
+
this._lost = null;
|
|
112
|
+
this._closed = false;
|
|
113
|
+
this._lostSignal = new Promise((resolve) => { this._signalLost = resolve; });
|
|
114
|
+
this.version = null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async _start(std) {
|
|
118
|
+
if (typeof SharedArrayBuffer !== "function")
|
|
119
|
+
throw new WorkerError("SharedArrayBuffer is unavailable; a browser page must be cross-origin isolated");
|
|
120
|
+
const module = await compile();
|
|
121
|
+
this._inbox = new SharedArrayBuffer(8 + INBOX);
|
|
122
|
+
this._control = new Int32Array(this._inbox, 0, 2);
|
|
123
|
+
this._data = new Uint8Array(this._inbox, 8);
|
|
124
|
+
const url = Runtime._workerURL;
|
|
125
|
+
try {
|
|
126
|
+
this._worker = node ? new NodeWorker(url, { execArgv: [] }) : new Worker(url, { type: "module" });
|
|
127
|
+
} catch (e) {
|
|
128
|
+
throw new WorkerError(`cannot start the worker: ${e.message}`);
|
|
129
|
+
}
|
|
130
|
+
const onMessage = (data) => {
|
|
131
|
+
if (data instanceof Uint8Array) this._feed(data);
|
|
132
|
+
else if ("exit" in data) this._lose(new WorkerError(`the worker exited with status ${data.exit}`, data.exit));
|
|
133
|
+
else this._lose(new WorkerError(`the worker trapped: ${data.trap}`));
|
|
134
|
+
};
|
|
135
|
+
const onError = (e) => this._lose(new WorkerError(`the worker failed: ${e?.message ?? e}`));
|
|
136
|
+
if (node) {
|
|
137
|
+
this._worker.on("message", onMessage);
|
|
138
|
+
this._worker.on("error", onError);
|
|
139
|
+
this._worker.on("exit", (code) => this._lose(new WorkerError(`the worker exited with status ${code}`, code)));
|
|
140
|
+
} else {
|
|
141
|
+
this._worker.addEventListener("message", (event) => onMessage(event.data));
|
|
142
|
+
this._worker.addEventListener("error", onError);
|
|
143
|
+
}
|
|
144
|
+
const limit = this._memoryLimit ? INITIAL_PAGES + Math.ceil(this._memoryLimit * 1.25 / 65536) : MAX_PAGES;
|
|
145
|
+
this._worker.postMessage({ module, pages: INITIAL_PAGES, maximum: Math.min(limit, MAX_PAGES), inbox: this._inbox });
|
|
146
|
+
this.version = await this._run(wire.INIT, [{
|
|
147
|
+
std: Boolean(std), step_limit: this._stepLimit, memory_limit: this._memoryLimit,
|
|
148
|
+
message_limit: this._messageLimit,
|
|
149
|
+
}]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Scheduling: one operation at a time per level; a host callback opens a
|
|
153
|
+
// new level for the operations it makes.
|
|
154
|
+
|
|
155
|
+
_run(kind, fields) {
|
|
156
|
+
const level = this._levels.at(-1);
|
|
157
|
+
const result = level.tail.then(() => this._operate(kind, fields, level));
|
|
158
|
+
level.tail = result.catch(() => {});
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async _operate(kind, fields, level) {
|
|
163
|
+
if (this._lost) throw this._copyLost();
|
|
164
|
+
if (this._closed) throw new Error("the runtime is closed");
|
|
165
|
+
if (level !== this._levels.at(-1)) throw new Error("the runtime is serving a host function");
|
|
166
|
+
const top = this._levels.length === 1;
|
|
167
|
+
if (top) {
|
|
168
|
+
if (node) this._worker.ref();
|
|
169
|
+
if (this._timeout !== undefined) this._startTimer(this._timeout);
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return await this._request(kind, fields);
|
|
173
|
+
} finally {
|
|
174
|
+
if (top) {
|
|
175
|
+
this._stopTimer();
|
|
176
|
+
this._flushLine();
|
|
177
|
+
if (node && !this._lost && !this._closed) this._worker.unref();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async _request(kind, fields) {
|
|
183
|
+
const data = wire.message(kind, fields, (value) => this._reference(value));
|
|
184
|
+
if (data.length - 4 > this._messageLimit) throw new RangeError("message exceeds the limit");
|
|
185
|
+
let reader = null;
|
|
186
|
+
try {
|
|
187
|
+
await this._send(data);
|
|
188
|
+
reader = await this._awaitReply();
|
|
189
|
+
} finally {
|
|
190
|
+
if (!reader) this._lose(new WorkerError("the worker was terminated while it was running"));
|
|
191
|
+
}
|
|
192
|
+
const value = this._decode(reader, 1)[0];
|
|
193
|
+
if (reader.kind === wire.RAISE) {
|
|
194
|
+
this._checkError(value);
|
|
195
|
+
throw this._engineError(value);
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async _awaitReply() {
|
|
201
|
+
for (;;) {
|
|
202
|
+
const reader = await this._next();
|
|
203
|
+
if (reader.kind === wire.RESULT || reader.kind === wire.RAISE) return reader;
|
|
204
|
+
if (reader.kind !== wire.HOST && reader.kind !== wire.LOAD && reader.kind !== wire.OUTPUT)
|
|
205
|
+
throw this._violation(`unexpected message kind 0x${reader.kind.toString(16)}`);
|
|
206
|
+
await this._serve(reader);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The streams.
|
|
211
|
+
|
|
212
|
+
async _send(data) {
|
|
213
|
+
if (this._released.length) {
|
|
214
|
+
const ids = this._released.splice(0);
|
|
215
|
+
await this._write(wire.message(wire.RELEASE, [new Tuple(ids)], (value) => this._reference(value)));
|
|
216
|
+
}
|
|
217
|
+
await this._write(data);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async _write(data) {
|
|
221
|
+
for (let at = 0; at < data.length;) {
|
|
222
|
+
while (Atomics.load(this._control, 0) !== 0) {
|
|
223
|
+
const wait = Atomics.waitAsync(this._control, 0, 1);
|
|
224
|
+
if (wait.async) await Promise.race([wait.value, this._lostSignal]);
|
|
225
|
+
if (this._lost) throw this._lost;
|
|
226
|
+
}
|
|
227
|
+
const n = Math.min(INBOX, data.length - at);
|
|
228
|
+
this._data.set(data.subarray(at, at + n));
|
|
229
|
+
this._control[1] = n;
|
|
230
|
+
Atomics.store(this._control, 0, 1);
|
|
231
|
+
Atomics.notify(this._control, 0);
|
|
232
|
+
at += n;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_feed(chunk) {
|
|
237
|
+
if (this._lost) return;
|
|
238
|
+
this._incoming.push(chunk);
|
|
239
|
+
this._incomingLength += chunk.length;
|
|
240
|
+
while (this._incomingLength >= 4) {
|
|
241
|
+
const head = this._take(4, false);
|
|
242
|
+
const n = new DataView(head.buffer, head.byteOffset, 4).getUint32(0, true);
|
|
243
|
+
if (n < 1 || n > this._messageLimit) {
|
|
244
|
+
this._lose(this._violationError("message length out of range"));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (this._incomingLength < 4 + n) return;
|
|
248
|
+
this._take(4, true);
|
|
249
|
+
this._frames.push(new wire.Reader(this._take(n, true), (id) => this._obtain(id)));
|
|
250
|
+
}
|
|
251
|
+
if (this._waiter && this._frames.length) {
|
|
252
|
+
const { resolve } = this._waiter;
|
|
253
|
+
this._waiter = null;
|
|
254
|
+
resolve(this._frames.shift());
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_take(n, consume) {
|
|
259
|
+
const out = new Uint8Array(n);
|
|
260
|
+
let filled = 0, index = 0;
|
|
261
|
+
while (filled < n) {
|
|
262
|
+
const chunk = this._incoming[index];
|
|
263
|
+
const k = Math.min(n - filled, chunk.length);
|
|
264
|
+
out.set(chunk.subarray(0, k), filled);
|
|
265
|
+
filled += k;
|
|
266
|
+
if (consume) {
|
|
267
|
+
if (k === chunk.length) this._incoming.shift();
|
|
268
|
+
else this._incoming[0] = chunk.subarray(k);
|
|
269
|
+
} else index++;
|
|
270
|
+
}
|
|
271
|
+
if (consume) this._incomingLength -= n;
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
_next() {
|
|
276
|
+
if (this._frames.length) return Promise.resolve(this._frames.shift());
|
|
277
|
+
if (this._lost) return Promise.reject(this._lost);
|
|
278
|
+
return new Promise((resolve, reject) => { this._waiter = { resolve, reject }; });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_decode(reader, count) {
|
|
282
|
+
try {
|
|
283
|
+
const values = [];
|
|
284
|
+
for (let i = 0; i < count; i++) values.push(reader.value());
|
|
285
|
+
reader.finish();
|
|
286
|
+
return values;
|
|
287
|
+
} catch (e) {
|
|
288
|
+
if (e instanceof Violation) throw this._violation(e.message);
|
|
289
|
+
throw e;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Losing the worker (protocol.md section 8).
|
|
294
|
+
|
|
295
|
+
_violationError(what) { return new WorkerError(`the worker broke the protocol: ${what}`); }
|
|
296
|
+
_violation(what) { return this._lose(this._violationError(what)); }
|
|
297
|
+
|
|
298
|
+
_lose(error) {
|
|
299
|
+
if (this._lost || this._closed) return this._lost ?? error;
|
|
300
|
+
this._lost = error;
|
|
301
|
+
this._end();
|
|
302
|
+
if (this._waiter) {
|
|
303
|
+
this._waiter.reject(error);
|
|
304
|
+
this._waiter = null;
|
|
305
|
+
}
|
|
306
|
+
return error;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_end() {
|
|
310
|
+
this._stopTimer();
|
|
311
|
+
this._worker?.terminate();
|
|
312
|
+
this._signalLost();
|
|
313
|
+
this._functions.clear();
|
|
314
|
+
this._functionIds.clear();
|
|
315
|
+
this._released.length = 0;
|
|
316
|
+
this._failure = null;
|
|
317
|
+
this._frames.length = 0;
|
|
318
|
+
this._incoming.length = 0;
|
|
319
|
+
this._output = this._loader = undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_copyLost() {
|
|
323
|
+
const error = this._lost instanceof Timeout ? new Timeout(this._lost.message)
|
|
324
|
+
: new WorkerError(this._lost.message, this._lost.returncode);
|
|
325
|
+
return error;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_startTimer(ms) {
|
|
329
|
+
this._deadline = Date.now() + ms;
|
|
330
|
+
this._timer = setTimeout(() => {
|
|
331
|
+
this._lose(new Timeout(`the operation exceeded the timeout of ${this._timeout} ms`));
|
|
332
|
+
}, ms);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
_stopTimer() {
|
|
336
|
+
clearTimeout(this._timer);
|
|
337
|
+
this._timer = null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Values.
|
|
341
|
+
|
|
342
|
+
_reference(value) {
|
|
343
|
+
if (value instanceof PyObject) {
|
|
344
|
+
if (value._runtime !== this) throw new TypeError("the object belongs to another runtime");
|
|
345
|
+
if (value._id === null) throw new TypeError("the object was released");
|
|
346
|
+
return { handle: value._id };
|
|
347
|
+
}
|
|
348
|
+
if (value instanceof Named) return { fn: this._functionId(value.fn), name: value.name };
|
|
349
|
+
if (typeof value === "function") return { fn: this._functionId(value), name: value.name || "function" };
|
|
350
|
+
throw new TypeError(`cannot convert ${value?.constructor?.name ?? typeof value} to an engine value`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_functionId(fn) {
|
|
354
|
+
let id = this._functionIds.get(fn);
|
|
355
|
+
if (id === undefined) {
|
|
356
|
+
id = this._functions.size + 1;
|
|
357
|
+
this._functionIds.set(fn, id);
|
|
358
|
+
this._functions.set(id, fn);
|
|
359
|
+
}
|
|
360
|
+
return id;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_obtain(id) { return new PyObject(CREATE, this, id); }
|
|
364
|
+
|
|
365
|
+
_release(id) {
|
|
366
|
+
if (!this._lost && !this._closed) this._released.push(id);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Errors from the engine.
|
|
370
|
+
|
|
371
|
+
_checkError(error) {
|
|
372
|
+
const kinds = { type: "string", message: "string", filename: "string", line: "number", token: "number" };
|
|
373
|
+
const ok = error instanceof Map && error.size === 6 && Object.entries(kinds).every(([k, t]) => typeof error.get(k) === t)
|
|
374
|
+
&& (error.get("exception") === null || error.get("exception") instanceof PyObject);
|
|
375
|
+
if (!ok) throw this._violation("malformed error");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
_engineError(error) {
|
|
379
|
+
let cause;
|
|
380
|
+
if (this._failure && error.get("token") === this._failure.token) cause = this._failure.error;
|
|
381
|
+
this._failure = null;
|
|
382
|
+
return new PythonError(error.get("type"), error.get("message"), error.get("exception"), error.get("filename"),
|
|
383
|
+
error.get("line"), cause === undefined ? undefined : { cause });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Host callbacks (worker requests).
|
|
387
|
+
|
|
388
|
+
async _serve(reader) {
|
|
389
|
+
let run;
|
|
390
|
+
try {
|
|
391
|
+
if (reader.kind === wire.HOST) {
|
|
392
|
+
const [fid, args, kwargs] = this._decode(reader, 3);
|
|
393
|
+
const fn = this._functions.get(fid);
|
|
394
|
+
if (fn === undefined || !Array.isArray(args) || !(kwargs instanceof Map) || [...kwargs.keys()].some((k) => typeof k !== "string"))
|
|
395
|
+
throw this._violation("malformed host call");
|
|
396
|
+
run = () => {
|
|
397
|
+
if (fn[KEYWORDS]) return fn(...args, Object.assign(Object.create(null), Object.fromEntries(kwargs)));
|
|
398
|
+
if (kwargs.size) throw new TypeError(`${fn.name || "function"}() takes no keyword arguments`);
|
|
399
|
+
return fn(...args);
|
|
400
|
+
};
|
|
401
|
+
} else if (reader.kind === wire.LOAD) {
|
|
402
|
+
const [name] = this._decode(reader, 1);
|
|
403
|
+
if (typeof name !== "string") throw this._violation("malformed load");
|
|
404
|
+
run = () => this._load(name);
|
|
405
|
+
} else {
|
|
406
|
+
const [text] = this._decode(reader, 1);
|
|
407
|
+
if (typeof text !== "string") throw this._violation("malformed output");
|
|
408
|
+
run = () => this._writeOutput(text);
|
|
409
|
+
}
|
|
410
|
+
} catch (e) {
|
|
411
|
+
if (e instanceof WorkerError) throw e;
|
|
412
|
+
run = () => { throw e; }; // read whole but not convertible: the engine gets the error
|
|
413
|
+
}
|
|
414
|
+
const remaining = this._timer !== null ? this._deadline - Date.now() : null;
|
|
415
|
+
this._stopTimer();
|
|
416
|
+
this._levels.push({ tail: Promise.resolve() });
|
|
417
|
+
let reply;
|
|
418
|
+
try {
|
|
419
|
+
const result = await run();
|
|
420
|
+
reply = wire.message(wire.RESULT, [result], (value) => this._reference(value));
|
|
421
|
+
} catch (e) {
|
|
422
|
+
if (this._lost) throw this._lost;
|
|
423
|
+
reply = wire.message(wire.RAISE, [this._failureOf(e)], (value) => this._reference(value));
|
|
424
|
+
} finally {
|
|
425
|
+
this._levels.pop();
|
|
426
|
+
if (remaining !== null && !this._lost) this._startTimer(remaining);
|
|
427
|
+
}
|
|
428
|
+
if (this._lost) throw this._lost;
|
|
429
|
+
if (reply.length - 4 > this._messageLimit)
|
|
430
|
+
reply = wire.message(wire.RAISE, [this._failureOf(new RangeError("message exceeds the limit"))], (value) => this._reference(value));
|
|
431
|
+
await this._send(reply);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async _load(name) {
|
|
435
|
+
if (!this._loader) return null;
|
|
436
|
+
const source = await this._loader(name);
|
|
437
|
+
if (source !== null && source !== undefined && typeof source !== "string")
|
|
438
|
+
throw new TypeError("a loader must return a string or null");
|
|
439
|
+
return source ?? null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async _writeOutput(text) {
|
|
443
|
+
if (!this._output) {
|
|
444
|
+
const lines = (this._line + text).split("\n");
|
|
445
|
+
this._line = lines.pop();
|
|
446
|
+
for (const line of lines) console.log(line);
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
await this._output(text);
|
|
451
|
+
} catch (e) {
|
|
452
|
+
console.error("Exception ignored in the runtime's output callback:", e);
|
|
453
|
+
throw e;
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
_flushLine() {
|
|
459
|
+
if (this._line) console.log(this._line);
|
|
460
|
+
this._line = "";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
_failureOf(error) {
|
|
464
|
+
const token = ++this._tokens;
|
|
465
|
+
this._failure = { token, error };
|
|
466
|
+
if (error instanceof PythonError && error.exception instanceof PyObject && error.exception._runtime === this
|
|
467
|
+
&& error.exception._id !== null)
|
|
468
|
+
return { raise: error.exception, token };
|
|
469
|
+
const classes = [];
|
|
470
|
+
if (error instanceof Error) {
|
|
471
|
+
if (error.name) classes.push(error.name);
|
|
472
|
+
for (let proto = Object.getPrototypeOf(error); proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto))
|
|
473
|
+
if (proto.constructor?.name && !classes.includes(proto.constructor.name)) classes.push(proto.constructor.name);
|
|
474
|
+
}
|
|
475
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
476
|
+
return { classes: new Tuple(classes), args: new Tuple([message]), message, token };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// The public API.
|
|
480
|
+
|
|
481
|
+
_noCallback(what) {
|
|
482
|
+
if (this._levels.length > 1) throw new Error(`cannot ${what} while a host function of this runtime is running`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Run source and resolve to the value of its last expression statement. */
|
|
486
|
+
async eval(source, filename = "<string>") {
|
|
487
|
+
this._noCallback("evaluate source");
|
|
488
|
+
return this._run(wire.EVAL, [string(source), string(filename)]);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Run source for its effects. */
|
|
492
|
+
async exec(source, filename = "<string>") {
|
|
493
|
+
this._noCallback("evaluate source");
|
|
494
|
+
await this._run(wire.EXEC, [string(source), string(filename)]);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Call a global name or a PyObject with positional and keyword arguments. */
|
|
498
|
+
async call(target, args = [], kwargs = {}) {
|
|
499
|
+
return this._run(wire.CALL, [target, new Tuple([...args]), kwargs]);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async get(name) { return this._run(wire.GET, [string(name)]); }
|
|
503
|
+
|
|
504
|
+
async set(name, value) { await this._run(wire.SET, [string(name), value]); }
|
|
505
|
+
|
|
506
|
+
async delete(name) { await this._run(wire.DELETE, [string(name)]); }
|
|
507
|
+
|
|
508
|
+
/** Bind a function as the global name. */
|
|
509
|
+
async define(name, fn) {
|
|
510
|
+
if (typeof fn !== "function") throw new TypeError("define() needs a function");
|
|
511
|
+
await this._run(wire.SET, [string(name), new Named(fn, name)]);
|
|
512
|
+
return fn;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Make `import name` create a module from members, then source. */
|
|
516
|
+
async registerModule(name, members = null, source = null) {
|
|
517
|
+
if (source !== null && typeof source !== "string") throw new TypeError("source must be a string");
|
|
518
|
+
await this._run(wire.MODULE, [string(name), members, source]);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Run the engine's cycle collector; resolves to the number of values freed. */
|
|
522
|
+
async collect() { return this._run(wire.COLLECT, []); }
|
|
523
|
+
|
|
524
|
+
get closed() { return this._closed || this._lost !== null; }
|
|
525
|
+
|
|
526
|
+
/** End the Worker. Idempotent. */
|
|
527
|
+
close() {
|
|
528
|
+
if (this._closed) return;
|
|
529
|
+
if (this._levels.length > 1) throw new Error("cannot close the runtime while a host function of this runtime is running");
|
|
530
|
+
if (!this._lost) {
|
|
531
|
+
this._closed = true;
|
|
532
|
+
this._end();
|
|
533
|
+
if (this._waiter) {
|
|
534
|
+
this._waiter.reject(new Error("the runtime is closed"));
|
|
535
|
+
this._waiter = null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
this._closed = true;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
[Symbol.dispose]() { this.close(); }
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// The Worker script; tests point it at a fake.
|
|
545
|
+
Runtime._workerURL = new URL("./worker.js", import.meta.url);
|
|
546
|
+
|
|
547
|
+
/** A function passed in under a chosen name, as define() does. */
|
|
548
|
+
class Named {
|
|
549
|
+
constructor(fn, name) { this.fn = fn; this.name = name; }
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const COMPARE = { "<": 11, "<=": 12, "==": 13, "!=": 14, ">": 15, ">=": 16 };
|
|
553
|
+
|
|
554
|
+
/** An engine value held by the worker; every method is one message. */
|
|
555
|
+
export class PyObject {
|
|
556
|
+
constructor(token, runtime, id) {
|
|
557
|
+
if (token !== CREATE) throw new TypeError("PyObject values come from a Runtime");
|
|
558
|
+
this._runtime = runtime;
|
|
559
|
+
this._id = id;
|
|
560
|
+
released.register(this, { runtime, id }, this);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async _op(op, ...operands) {
|
|
564
|
+
if (this._id === null) throw new TypeError("the object was released");
|
|
565
|
+
return this._runtime._run(wire.OP, [op, this, ...operands]);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async get(name) { return this._op(1, string(name)); }
|
|
569
|
+
async set(name, value) { await this._op(2, string(name), value); }
|
|
570
|
+
getItem(key) { return this._op(3, key); }
|
|
571
|
+
async setItem(key, value) { await this._op(4, key, value); }
|
|
572
|
+
len() { return this._op(5); }
|
|
573
|
+
has(item) { return this._op(6, item); }
|
|
574
|
+
async call(args = [], kwargs = {}) { return this._runtime._run(wire.CALL, [this, new Tuple([...args]), kwargs]); }
|
|
575
|
+
bool() { return this._op(9); }
|
|
576
|
+
hash() { return this._op(10); }
|
|
577
|
+
async compare(op, other) {
|
|
578
|
+
if (!Object.hasOwn(COMPARE, op)) throw new RangeError(`unknown comparison ${op}`);
|
|
579
|
+
return this._op(COMPARE[op], other);
|
|
580
|
+
}
|
|
581
|
+
repr() { return this._op(17); }
|
|
582
|
+
str() { return this._op(18); }
|
|
583
|
+
dir() { return this._op(19); }
|
|
584
|
+
|
|
585
|
+
async *[Symbol.asyncIterator]() {
|
|
586
|
+
const iterator = await this._op(7);
|
|
587
|
+
for (;;) {
|
|
588
|
+
let value;
|
|
589
|
+
try {
|
|
590
|
+
value = await iterator._op(8);
|
|
591
|
+
} catch (e) {
|
|
592
|
+
if (e instanceof PythonError && e.type === "StopIteration") return;
|
|
593
|
+
throw e;
|
|
594
|
+
}
|
|
595
|
+
yield value;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Drop the worker's reference now. */
|
|
600
|
+
release() {
|
|
601
|
+
if (this._id === null) return;
|
|
602
|
+
released.unregister(this);
|
|
603
|
+
this._runtime._release(this._id);
|
|
604
|
+
this._id = null;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function string(value) {
|
|
609
|
+
if (typeof value !== "string") throw new TypeError(`expected a string, not ${typeof value}`);
|
|
610
|
+
return value;
|
|
611
|
+
}
|