import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { test } from "node:test"; import { build } from "esbuild"; const compiled = await build({ entryPoints: [fileURLToPath(new URL("../src/player/python.ts", import.meta.url))], bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" }); const { Python, BlockingCall, userBuiltins } = await import(`data:text/javascript;base64,${Buffer.from(compiled.outputFiles[0].text).toString("base64")}`); const ir = JSON.parse(readFileSync(new URL("./fixtures/waiting.ir.json", import.meta.url), "utf8")); function runtime() { const store = Object.create(null); const builtins = { len: (x) => x.length, range: (n) => Array.from({ length: n }, (_, i) => i), ask: (prompt) => new BlockingCall("ask", [prompt]) }; const py = new Python({ get: (n) => (n in store ? store[n] : builtins[n]), has: (n) => n in store || n in builtins, set: (n, v) => { store[n] = v; }, del: (n) => { delete store[n]; }, }); Object.assign(builtins, userBuiltins(py)); py.blockingNames = new Set(["ask", "greet", "helper", "twice", "Q"]); return { py, store }; } /** Drive a generator like the engine does: answers come from a list, errors can be thrown back in. */ function drive(gen, answers) { const asked = []; let step = gen.next(); while (!step.done) { asked.push(step.value.args[0]); const answer = answers.shift(); step = answer instanceof Error ? gen.throw(answer) : gen.next(answer); } return asked; } test("code that waits for the player runs through loops, try, functions, methods and expressions", () => { const { py, store } = runtime(); const asked = drive(py.execG(ir), ["a", "bob", "1", "x", "5", "init", "p", "q", "yes"]); assert.deepEqual(asked, ["name?", "again", "v", "v", "v", "init", "x", "y", "c"]); assert.equal(store.result, "hi bob!"); assert.equal(store.t, 6, "int('x') raised ValueError inside try and the loop went on"); assert.equal(store.q.attrs.v, "init"); assert.equal(store.r2, "pq"); assert.equal(store.cond, "a"); assert.equal(store.plain, 4); }); test("mayBlock only marks what can really wait; plain code stays on the fast path", () => { const { py } = runtime(); assert.equal(py.mayBlock(ir[0]), false, "a definition does not run when it is defined"); assert.equal(py.mayBlock(ir[ir.length - 1]), false); assert.equal(py.mayBlock(ir.find((s) => s.op === "set" && s.t.id === "result")), true); }); test("a waiting call outside the generator interpreter is an explicit error, not a silent None", () => { const { py } = runtime(); assert.throws(() => py.eval({ k: "call", f: { k: "name", id: "ask" }, a: [{ k: "const", v: "x" }], kw: {} }), /ask\(\) здесь вызвать нельзя/); }); test("the interpreter scope is set aside while the player decides", () => { const { py } = runtime(); const gen = py.execG(ir); gen.next(); // stops inside greet() at ask("name?") assert.deepEqual(py.locals, [], "other code (screens) does not see the half-run function's variables"); assert.equal(py.fn, null); assert.equal(gen.next("zed").value.args[0], "v", "'zed' is long enough, so greet() ends and helper() asks next"); });