// A small evaluator for the Python subset emitted by the converter. // Python values map to JS: None→null, list/tuple→Array, dict→PyDict, str/num/bool as is. import type { Expr, PyStmt } from "./types"; import { evaluateNani } from "./naninovel"; export class PyDict { map = new Map(); constructor(entries?: Iterable<[any, any]>) { if (entries) for (const [k, v] of entries) this.map.set(key(k), v); } get(k: any) { return this.map.get(key(k)); } set(k: any, v: any) { this.map.set(key(k), v); } has(k: any) { return this.map.has(key(k)); } toJSON() { return { __dict__: [...this.map.entries()] }; } } // tuples used as dict keys must compare by value function key(k: any) { return Array.isArray(k) ? JSON.stringify(k) : k; } export class JumpSignal { constructor(public kind: "jump" | "call", public target: string) {} } class BreakSignal {} class ContinueSignal {} class ReturnSignal { constructor(public value: any) {} } export type Builtin = ((...args: any[]) => any) & { __py?: true }; // --- user-defined classes ------------------------------------------------------- // `class` statements of the game become PyClass objects. Instances keep their data in // `attrs`, so a plain JSON round trip (saves) restores them given the class registry. const SUPPORTED_DUNDERS = new Set(["__init__", "__str__", "__repr__"]); const MAX_CALL_DEPTH = 150; export class PyClass { attrs = new Map(); constructor(public name: string, public bases: PyClass[]) {} toJSON() { return undefined; // classes are rebuilt by the game's init code, never saved } mro(): PyClass[] { const out: PyClass[] = []; const visit = (c: PyClass) => { if (out.includes(c)) return; out.push(c); c.bases.forEach(visit); }; visit(this); return out; } find(name: string): { value: any; owner: PyClass } | undefined { for (const c of this.mro()) if (c.attrs.has(name)) return { value: c.attrs.get(name), owner: c }; return undefined; } } export class PyInstance { constructor(public cls: PyClass, public attrs: Record = Object.create(null)) {} toJSON() { return { __inst__: this.cls.name, a: this.attrs }; } } class SuperProxy { constructor(public owner: PyClass, public self: any) {} } /** staticmethod / classmethod / property wrapper stored in a class body. */ class PyDescriptor { constructor(public kind: "static" | "class" | "property", public fn: any, public setter?: any) {} } export const OBJECT = new PyClass("object", []); const classRegistry = new Map(); /** Builtin exception hierarchy; ``args`` holds the constructor arguments. */ export const BASE_EXCEPTION = new PyClass("BaseException", [OBJECT]); BASE_EXCEPTION.attrs.set("__init__", (self: PyInstance, ...args: any[]) => { const last = args[args.length - 1]; self.attrs.args = last && typeof last === "object" && !Array.isArray(last) && Object.hasOwn(last, "__kw") ? args.slice(0, -1) : args; return null; }); BASE_EXCEPTION.attrs.set("__str__", (self: PyInstance) => { const args = self.attrs.args; return Array.isArray(args) && args.length ? (args.length === 1 ? pyStr(args[0]) : pyStr(args)) : ""; }); for (const method of BASE_EXCEPTION.attrs.values()) method.__method = true; export const EXCEPTIONS = new Map([["BaseException", BASE_EXCEPTION]]); for (const [name, parent] of [ ["Exception", "BaseException"], ["ArithmeticError", "Exception"], ["ZeroDivisionError", "ArithmeticError"], ["OverflowError", "ArithmeticError"], ["AssertionError", "Exception"], ["AttributeError", "Exception"], ["ImportError", "Exception"], ["LookupError", "Exception"], ["IndexError", "LookupError"], ["KeyError", "LookupError"], ["NameError", "Exception"], ["OSError", "Exception"], ["IOError", "OSError"], ["EnvironmentError", "OSError"], ["RuntimeError", "Exception"], ["NotImplementedError", "RuntimeError"], ["RecursionError", "RuntimeError"], ["StopIteration", "Exception"], ["SyntaxError", "Exception"], ["TypeError", "Exception"], ["ValueError", "Exception"], ["UnicodeError", "ValueError"], ] as const) { EXCEPTIONS.set(name, new PyClass(name, [EXCEPTIONS.get(parent)!])); } for (const cls of EXCEPTIONS.values()) classRegistry.set(cls.name, cls); class PyException extends Error { constructor(public instance: PyInstance) { super(`${instance.cls.name}: ${instanceText(instance)}`); } } type FnDef = { name: string; params: any[]; body?: PyStmt[]; expr?: Expr }; type FnCtx = { vars: Map; globals: Set; nonlocals: Set; owner?: PyClass; self?: any; isClassBody: boolean; index: number; }; let activePython: Python | null = null; function instanceText(v: PyInstance): string { for (const name of ["__str__", "__repr__"]) { const found = v.cls.find(name); if (found && typeof found.value === "function" && activePython) return pyStr(found.value(v)); } return `<${v.cls.name} object>`; } export function truthy(v: any): boolean { if (v === null || v === undefined || v === false || v === 0 || v === "") return false; if (Array.isArray(v)) return v.length > 0; if (v instanceof PyDict) return v.map.size > 0; return true; } export function pyEq(a: any, b: any): boolean { if (a === b) return true; if (a === null || b === null || a === undefined || b === undefined) return a == b; if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((x, i) => pyEq(x, b[i])); if (typeof a === "boolean" || typeof b === "boolean") return Number(a) === Number(b); return false; } export function pyStr(v: any): string { if (v === null || v === undefined) return "None"; if (v === true) return "True"; if (v === false) return "False"; if (typeof v === "number") return Number.isInteger(v) ? String(v) : String(v); if (Array.isArray(v)) return "[" + v.map(pyRepr).join(", ") + "]"; if (v instanceof PyInstance) return instanceText(v); if (v instanceof PyDict) return "{" + [...v.map.entries()].map(([k, x]) => `${pyRepr(k)}: ${pyRepr(x)}`).join(", ") + "}"; return String(v); } function pyRepr(v: any): string { return typeof v === "string" ? `'${v}'` : pyStr(v); } function pyMod(a: number, b: number) { return ((a % b) + b) % b; } function percentFormat(fmt: string, args: any): string { const list = Array.isArray(args) ? args : [args]; let i = 0; return fmt.replace(/%(\([^)]*\))?[-+ 0#]*\d*(\.\d+)?([sdifr%])/g, (m, name, prec, conv) => { if (conv === "%") return "%"; const v = name && args instanceof PyDict ? args.get(name.slice(1, -1)) : list[i++]; if (conv === "d" || conv === "i") return String(Math.trunc(Number(v))); if (conv === "f") return Number(v).toFixed(prec ? Number(prec.slice(1)) : 6); void m; return pyStr(v); }); } export function binop(op: string, a: any, b: any): any { // objects (colour matrices) may define their own arithmetic if (a && typeof a === "object" && typeof a.__binop__ === "function") return a.__binop__(op, b); switch (op) { case "+": if (Array.isArray(a) && Array.isArray(b)) return [...a, ...b]; if (typeof a === "string" || typeof b === "string") { if (typeof a !== typeof b) throw new TypeError("str + не-str"); return a + b; } return a + b; case "-": return a - b; case "*": if (typeof a === "string" && typeof b === "number") return a.repeat(Math.max(0, b)); if (Array.isArray(a) && typeof b === "number") return Array.from({ length: b }, () => a).flat(); return a * b; case "/": return a / b; case "//": return Math.floor(a / b); case "%": if (typeof a === "string") return percentFormat(a, b); return pyMod(a, b); case "**": return a ** b; case "&": return a & b; case "|": return a | b; case "^": return a ^ b; case "<<": return a << b; case ">>": return a >> b; } throw new Error(`оператор ${op}`); } function contains(container: any, item: any): boolean { if (typeof container === "string") return container.includes(item); if (Array.isArray(container)) return container.some((x) => pyEq(x, item)); if (container instanceof PyDict) return container.has(item); if (container && typeof container === "object") return item in container; return false; } function compare(op: string, a: any, b: any): boolean { switch (op) { case "==": return pyEq(a, b); case "!=": return !pyEq(a, b); case "<": return a < b; case "<=": return a <= b; case ">": return a > b; case ">=": return a >= b; case "in": return contains(b, a); case "not in": return !contains(b, a); case "is": return a === b || (a == null && b == null); case "is not": return !(a === b || (a == null && b == null)); } throw new Error(`сравнение ${op}`); } function index(len: number, i: number) { if (!Number.isInteger(i)) throw new Error("Индекс последовательности должен быть целым числом"); return i < 0 ? len + i : i; } function safeProperty(name: any): boolean { if (typeof name !== "string") return typeof name === "number" && Number.isFinite(name); return !name.startsWith("__") && !["constructor", "prototype", "caller", "callee", "arguments"].includes(name); } function methodOf(obj: any, name: string): any { if (Array.isArray(obj)) { const m: Record = { append: (x) => void obj.push(x), extend: (xs) => void obj.push(...xs), insert: (i, x) => void obj.splice(i, 0, x), pop: (i = -1) => obj.splice(index(obj.length, i), 1)[0], remove: (x) => { const i = obj.findIndex((y) => pyEq(y, x)); if (i < 0) throw new Error("list.remove: нет элемента"); obj.splice(i, 1); }, index: (x) => obj.findIndex((y) => pyEq(y, x)), count: (x) => obj.filter((y) => pyEq(y, x)).length, reverse: () => void obj.reverse(), sort: (...a: any[]) => { const kw = a.length && a[a.length - 1]?.__kw ? a[a.length - 1].__kw : {}; const key = typeof kw.key === "function" ? kw.key : (x: any) => x; const sorted = obj.map((x) => [key(x), x]).sort((p, q) => (p[0] < q[0] ? -1 : p[0] > q[0] ? 1 : 0)).map((p) => p[1]); if (truthy(kw.reverse)) sorted.reverse(); obj.splice(0, obj.length, ...sorted); }, clear: () => void obj.splice(0), copy: () => [...obj], add: (x) => void (contains(obj, x) || obj.push(x)), // sets are lists here discard: (x) => { const i = obj.findIndex((y) => pyEq(y, x)); if (i >= 0) obj.splice(i, 1); }, }; return Object.hasOwn(m, name) ? m[name] : undefined; } if (obj instanceof PyDict) { const m: Record = { get: (k, d = null) => (obj.has(k) ? obj.get(k) : d), keys: () => [...obj.map.keys()], values: () => [...obj.map.values()], items: () => [...obj.map.entries()], pop: (k, d = null) => { const v = obj.has(k) ? obj.get(k) : d; obj.map.delete(key(k)); return v; }, setdefault: (k, d = null) => { if (!obj.has(k)) obj.set(k, d); return obj.get(k); }, update: (other: PyDict) => other.map.forEach((v, k) => obj.map.set(k, v)), has_key: (k) => obj.has(k), iteritems: () => [...obj.map.entries()], iterkeys: () => [...obj.map.keys()], itervalues: () => [...obj.map.values()], clear: () => obj.map.clear(), copy: () => new PyDict(obj.map.entries()), }; return Object.hasOwn(m, name) ? m[name] : undefined; } if (typeof obj === "string") { const m: Record = { lower: () => obj.toLowerCase(), upper: () => obj.toUpperCase(), capitalize: () => obj.charAt(0).toUpperCase() + obj.slice(1).toLowerCase(), title: () => obj.replace(/\w\S*/g, (w) => w[0].toUpperCase() + w.slice(1).toLowerCase()), strip: () => obj.trim(), lstrip: () => obj.trimStart(), rstrip: () => obj.trimEnd(), startswith: (p) => obj.startsWith(p), endswith: (p) => obj.endsWith(p), replace: (a, b) => obj.split(a).join(b), split: (sep?: string) => (sep ? obj.split(sep) : obj.trim().split(/\s+/)), join: (xs) => xs.map(pyStr).join(obj), format: (...args) => { let i = 0; return obj.replace(/\{(\d*)\}/g, (_, n) => pyStr(args[n === "" ? i++ : Number(n)])); }, count: (s) => obj.split(s).length - 1, find: (s) => obj.indexOf(s), zfill: (n: number) => obj.padStart(n, "0"), ljust: (n: number, ch = " ") => obj.padEnd(n, ch), rjust: (n: number, ch = " ") => obj.padStart(n, ch), center: (n: number, ch = " ") => { const total = Math.max(0, n - obj.length); const left = Math.floor(total / 2); return ch.repeat(left) + obj + ch.repeat(total - left); }, partition: (sep: string) => { const i = obj.indexOf(sep); return i < 0 ? [obj, "", ""] : [obj.slice(0, i), sep, obj.slice(i + sep.length)]; }, rpartition: (sep: string) => { const i = obj.lastIndexOf(sep); return i < 0 ? ["", "", obj] : [obj.slice(0, i), sep, obj.slice(i + sep.length)]; }, rsplit: (sep?: string) => (sep ? obj.split(sep) : obj.trim().split(/\s+/)), splitlines: () => obj.split(/\r?\n/), isupper: () => obj !== obj.toLowerCase() && obj === obj.toUpperCase(), islower: () => obj !== obj.toUpperCase() && obj === obj.toLowerCase(), isspace: () => obj.length > 0 && /^\s+$/.test(obj), isalnum: () => /^[\p{L}\p{N}]+$/u.test(obj), swapcase: () => [...obj].map((c) => (c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase())).join(""), index: (x: string) => { const i = obj.indexOf(x); if (i < 0) throw new Error("substring not found"); return i; }, rfind: (x: string) => obj.lastIndexOf(x), isdigit: () => /^\d+$/.test(obj), isalpha: () => /^\p{L}+$/u.test(obj), }; return Object.hasOwn(m, name) ? m[name] : undefined; } return undefined; } export interface Scope { get(name: string): any; has(name: string): boolean; set(name: string, value: any): void; del(name: string): void; } export class Python { locals: Map[] = []; warned = new Set(); fn: FnCtx | null = null; depth = 0; /** > 0 while a try block runs: lookups that normally yield None raise Python exceptions. */ strict = 0; handling: PyInstance[] = []; constructor(public scope: Scope) { activePython = this; } warnOnce(msg: string) { if (!this.warned.has(msg)) { this.warned.add(msg); console.warn("[pgvn]", msg); } } lookup(name: string): any { if (!safeProperty(name)) return null; for (let i = this.locals.length - 1; i >= 0; i--) if (this.locals[i].has(name)) return this.locals[i].get(name); if (this.scope.has(name)) return this.scope.get(name); if (this.strict) this.raise("NameError", `имя ${name} не определено`); this.warnOnce(`неизвестное имя ${name}`); return null; } eval(e: Expr | null | undefined): any { if (!e) return null; switch (e.k) { case "nani": return evaluateNani(e.ast, (name) => safeProperty(name) && this.scope.has(name) ? this.scope.get(name) : undefined); case "const": return e.v; case "name": return this.lookup(e.id); case "attr": { const o = this.eval(e.o); if (o instanceof PyInstance || o instanceof PyClass || o instanceof SuperProxy) { if (!safeProperty(e.a) && !SUPPORTED_DUNDERS.has(e.a)) return null; const v = this.attrOf(o, e.a); if (v === undefined) this.warnOnce(`у ${o instanceof PyInstance ? o.cls.name : "класса"} нет атрибута ${e.a}`); return v === undefined ? null : v; } if (!safeProperty(e.a) || typeof o === "function") return null; if (o === null || o === undefined) { if (this.strict) this.raise("AttributeError", `у None нет атрибута ${e.a}`); this.warnOnce(`атрибут ${e.a} у None`); return null; } const m = methodOf(o, e.a); if (m) return m; const v = o instanceof PyDict ? o.get(e.a) : o[e.a]; // methods of host objects (datetime, regex matches) need their object if (typeof v === "function" && !(o instanceof PyDict) && !v.__py && Object.getPrototypeOf(o) !== Object.prototype) return v.bind(o); return v === undefined ? null : v; } case "sub": { const o = this.eval(e.o); const i = this.eval(e.i); if (o instanceof PyDict) { if (!o.has(i) && typeof (o as any).missing === "function") return (o as any).missing(i); // defaultdict if (this.strict && !o.has(i)) this.raise("KeyError", pyStr(i)); return o.has(i) ? o.get(i) : null; } if (!safeProperty(i) || typeof o === "function") return null; if (this.strict && (Array.isArray(o) || typeof o === "string")) { const at = index(o.length, i); if (at < 0 || at >= o.length) this.raise("IndexError", "индекс вне диапазона"); } if (Array.isArray(o) || typeof o === "string") return o[index(o.length, i)] ?? null; return o?.[i] ?? null; } case "slice": { const o = this.eval(e.o); const lo = e.lo ? this.eval(e.lo) : undefined; const hi = e.hi ? this.eval(e.hi) : undefined; return o.slice(lo, hi); } case "bin": { const left = this.eval(e.l); const right = this.eval(e.r); if (this.strict && right === 0 && (e.op === "/" || e.op === "//" || e.op === "%")) this.raise("ZeroDivisionError", "деление на ноль"); return binop(e.op, left, right); } case "bool": { let v: any = null; for (const x of e.v) { v = this.eval(x); if (e.op === "and" ? !truthy(v) : truthy(v)) return v; } return v; } case "unary": { const v = this.eval(e.v); if (e.op === "not") return !truthy(v); if (e.op === "-") return -v; if (e.op === "~") return ~v; return +v; } case "cmp": { let left = this.eval(e.l); for (let i = 0; i < e.ops.length; i++) { const right = this.eval(e.r[i]); if (!compare(e.ops[i], left, right)) return false; left = right; } return true; } case "if": return truthy(this.eval(e.c)) ? this.eval(e.t) : this.eval(e.e); case "list": case "tuple": return e.v.map((x: Expr) => this.eval(x)); case "dict": return new PyDict(e.v.map(([k, v]: [Expr, Expr]) => [this.eval(k), this.eval(v)])); case "fmt": return e.v.map((p: any) => (typeof p === "string" ? p : pyStr(this.eval(p)))).join(""); case "comp": { const gens = e.gens ?? [{ t: e.t, it: e.it, if: e.if }]; const isDict = e.v !== undefined && e.v !== null; const out: any[] = []; const dict = new PyDict(); const frame = new Map(); this.locals.push(frame); const run = (level: number) => { if (level === gens.length) { if (isDict) dict.set(this.eval(e.e), this.eval(e.v)); else out.push(this.eval(e.e)); return; } const gen = gens[level]; for (const item of [...iterate(this.eval(gen.it))]) { this.bind(gen.t, item, frame); if (gen.if.every((c: Expr) => truthy(this.eval(c)))) run(level + 1); } }; try { run(0); } finally { this.locals.pop(); } return isDict ? dict : out; } case "lambda": { const closure = this.fn?.isClassBody ? this.locals.slice(0, this.fn.index) : this.locals.slice(); return this.makeFunction({ name: "", params: e.params, expr: e.body }, closure, e.params.map((p: any) => (p.d ? this.eval(p.d) : undefined))); } case "star": this.warnOnce("*выражение вне вызова"); return null; case "call": { const isSuper = e.f.k === "name" && e.f.id === "super" && !this.locals.some((l) => l.has("super")); if (isSuper) return this.makeSuper(); const f = this.eval(e.f); const args: any[] = []; for (const x of e.a as Expr[]) { if (x.k === "star") args.push(...iterate(this.eval(x.v))); else args.push(this.eval(x)); } const kw: Record = {}; for (const [k, v] of Object.entries(e.kw)) kw[k] = this.eval(v as Expr); for (const x of (e.ks ?? []) as Expr[]) { const m = this.eval(x); if (m instanceof PyDict) for (const [k, v] of m.map.entries()) kw[String(k)] = v; } const call = Object.keys(kw).length ? [...args, { __kw: kw }] : args; if (f instanceof PyClass) return this.instantiate(f, call); if (typeof f !== "function") { this.warnOnce(`вызов не-функции (${e.f.id ?? e.f.a ?? "?"})`); return null; } return f(...call); } } this.warnOnce(`неизвестное выражение ${e.k}`); return null; } bind(t: Expr, value: any, frame?: Map) { switch (t.k) { case "name": if (!safeProperty(t.id)) throw new Error("Служебное имя недоступно"); if (frame) frame.set(t.id, value); else this.bindName(t.id, value); return; case "tuple": { const items = [...iterate(value)]; t.v.forEach((sub: Expr, i: number) => this.bind(sub, items[i], frame)); return; } case "attr": { const o = this.eval(t.o); if (!safeProperty(t.a) || typeof o === "function") throw new Error("Служебный атрибут недоступен"); if (o instanceof PyInstance) { const found = o.cls.find(t.a); if (found?.value instanceof PyDescriptor && found.value.kind === "property") { if (!found.value.setter) throw new Error(`Свойство ${t.a} доступно только для чтения`); found.value.setter(o, value); } else o.attrs[t.a] = value; } else if (o instanceof PyClass) o.attrs.set(t.a, value); else if (o instanceof PyDict) o.set(t.a, value); else if (o) o[t.a] = value; return; } case "sub": { const o = this.eval(t.o); const i = this.eval(t.i); if (o instanceof PyDict) o.set(i, value); else if (!safeProperty(i) || typeof o === "function") throw new Error("Служебный ключ недоступен"); else if (Array.isArray(o)) o[index(o.length, i)] = value; else if (o) o[i] = value; return; } } this.warnOnce(`присваивание в ${t.k}`); } exec(stmts: PyStmt[]) { let guard = 0; for (const s of stmts) { switch (s.op) { case "set": this.bind(s.t, this.eval(s.v)); break; case "aug": this.bind(s.t, binop(s.o, this.eval(s.t), this.eval(s.v))); break; case "eval": this.eval(s.e); break; case "if": this.exec(truthy(this.eval(s.c)) ? s.then : s.else); break; case "while": { let broke = false; while (truthy(this.eval(s.c))) { if (++guard > 100000) throw new Error("бесконечный цикл в python"); if (this.loopBody(s.body)) { broke = true; break; } } if (!broke && s.else?.length) this.exec(s.else); break; } case "for": { let broke = false; for (const item of [...iterate(this.eval(s.it))]) { this.bind(s.t, item); if (this.loopBody(s.body)) { broke = true; break; } } if (!broke && s.else?.length) this.exec(s.else); break; } case "try": this.execTry(s); break; case "raise": this.execRaise(s); break; case "assert": if (!truthy(this.eval(s.c))) this.raise("AssertionError", s.m ? pyStr(this.eval(s.m)) : ""); break; case "del": if (s.t.k === "name") { if (this.fn && !this.fn.globals.has(s.t.id)) this.fn.vars.delete(s.t.id); else this.scope.del(s.t.id); } else if (s.t.k === "sub") { const o = this.eval(s.t.o); const i = this.eval(s.t.i); if (o instanceof PyDict) o.map.delete(key(i)); else if (Array.isArray(o)) o.splice(index(o.length, i), 1); } break; case "def": this.defineFunction(s); break; case "class": this.defineClass(s); break; case "return": if (!this.fn) throw new Error("return вне функции"); throw new ReturnSignal(s.v ? this.eval(s.v) : null); case "global": for (const n of s.names) this.fn?.globals.add(n); break; case "nonlocal": for (const n of s.names) this.fn?.nonlocals.add(n); break; case "break": throw new BreakSignal(); case "continue": throw new ContinueSignal(); } } } // --- exceptions --------------------------------------------------------------------- /** Throw a builtin Python exception, e.g. ``raise("KeyError", "x")``. */ raise(name: string, message: string): never { const cls = EXCEPTIONS.get(name) ?? EXCEPTIONS.get("Exception")!; throw new PyException(this.instantiate(cls, message === "" ? [] : [message])); } private execRaise(s: PyStmt) { if (!s.v) { const current = this.handling[this.handling.length - 1]; if (!current) this.raise("RuntimeError", "нет исключения для повторного вызова"); throw new PyException(current); } const v = this.eval(s.v); if (v instanceof PyInstance && v.cls.mro().includes(BASE_EXCEPTION)) throw new PyException(v); if (v instanceof PyClass && v.mro().includes(BASE_EXCEPTION)) throw new PyException(this.instantiate(v, [])); this.raise("TypeError", "можно вызвать только исключение"); } /** Python exception for anything thrown while running a try body, or null for control flow. */ private caught(err: unknown): PyInstance | null { if (err instanceof PyException) return err.instance; if (err instanceof BreakSignal || err instanceof ContinueSignal || err instanceof ReturnSignal || err instanceof JumpSignal) return null; if (err instanceof TypeError) return this.instantiate(EXCEPTIONS.get("TypeError")!, [err.message]); if (err instanceof Error) return this.instantiate(EXCEPTIONS.get("RuntimeError")!, [err.message]); return null; } private execTry(s: PyStmt) { try { let handled = false; this.strict++; try { this.exec(s.body); } catch (err) { this.strict--; const exception = this.caught(err); const handler = exception && s.handlers.find((h: any) => { if (h.types === null) return true; const kinds = this.eval(h.types); return (Array.isArray(kinds) ? kinds : [kinds]).some((k: any) => k instanceof PyClass && exception.cls.mro().includes(k)); }); if (!exception || !handler) throw err; handled = true; if (handler.name) this.bindName(handler.name, exception); this.handling.push(exception); try { this.exec(handler.body); } finally { this.handling.pop(); } return; } this.strict--; if (!handled && s.else?.length) this.exec(s.else); } finally { if (s.final?.length) this.exec(s.final); } } // --- functions and classes --------------------------------------------------------- /** Assign a name in the scope the running code sees: function local, global or enclosing. */ bindName(name: string, value: any) { const f = this.fn; if (!f || f.globals.has(name)) return void this.scope.set(name, value); if (f.nonlocals.has(name)) { for (let i = this.locals.length - 2; i >= 0; i--) { if (this.locals[i].has(name)) return void this.locals[i].set(name, value); } throw new Error(`nonlocal ${name}: переменная не найдена`); } f.vars.set(name, value); } makeFunction(def: FnDef, closure: Map[], defaults: any[]): Builtin { const fn: any = (...args: any[]) => this.callUser(fn, args); fn.__py = true; fn.__def = def; fn.__closure = closure; fn.__defaults = defaults; return fn; } private callUser(fn: any, given: any[]): any { const def: FnDef = fn.__def; let args = given; let kw: Record = {}; const last = args[args.length - 1]; if (last && typeof last === "object" && !Array.isArray(last) && Object.hasOwn(last, "__kw")) { kw = last.__kw; args = args.slice(0, -1); } const frame = new Map(); const named = new Set(); let position = 0; def.params.forEach((p: any, i: number) => { if (p.kind === "p") { named.add(p.n); if (position < args.length) { if (Object.hasOwn(kw, p.n)) throw new TypeError(`${def.name}(): параметр ${p.n} задан дважды`); frame.set(p.n, args[position++]); } else if (Object.hasOwn(kw, p.n)) frame.set(p.n, kw[p.n]); else if (p.d) frame.set(p.n, fn.__defaults[i]); else throw new TypeError(`${def.name}(): не хватает аргумента ${p.n}`); } else if (p.kind === "k") { named.add(p.n); if (Object.hasOwn(kw, p.n)) frame.set(p.n, kw[p.n]); else if (p.d) frame.set(p.n, fn.__defaults[i]); else throw new TypeError(`${def.name}(): не хватает именованного аргумента ${p.n}`); } else if (p.kind === "*") { frame.set(p.n, args.slice(position)); position = args.length; } }); if (position < args.length) throw new TypeError(`${def.name}(): слишком много аргументов`); const rest = Object.keys(kw).filter((k) => !named.has(k)); const kwParam = def.params.find((p: any) => p.kind === "**"); if (kwParam) frame.set(kwParam.n, new PyDict(rest.map((k) => [k, kw[k]]))); else if (rest.length) throw new TypeError(`${def.name}(): неожиданный аргумент ${rest[0]}`); if (this.depth >= MAX_CALL_DEPTH) throw new Error("Слишком глубокая рекурсия в python"); const previousLocals = this.locals; const previousFn = this.fn; this.depth++; this.locals = [...fn.__closure, frame]; this.fn = { vars: frame, globals: new Set(), nonlocals: new Set(), owner: fn.__owner, self: def.params.length ? frame.get(def.params[0].n) : undefined, isClassBody: false, index: 0, }; try { if (def.expr) return this.eval(def.expr); try { this.exec(def.body ?? []); } catch (err) { if (err instanceof ReturnSignal) return err.value; throw err; } return null; } finally { this.depth--; this.locals = previousLocals; this.fn = previousFn; } } private defineFunction(s: PyStmt) { const closure = this.fn?.isClassBody ? this.locals.slice(0, this.fn.index) : this.locals.slice(); let value: any = this.makeFunction({ name: s.name, params: s.params, body: s.body }, closure, s.params.map((p: any) => (p.d ? this.eval(p.d) : undefined))); for (const deco of [...(s.decos ?? [])].reverse()) { if (deco.k === "name") { const kind = { staticmethod: "static", classmethod: "class", property: "property" }[deco.id as string]; value = new PyDescriptor(kind as any, value); } else { const base = this.eval(deco.o); if (!(base instanceof PyDescriptor) || base.kind !== "property") throw new Error("Неверный декоратор setter"); value = new PyDescriptor("property", base.fn, value); } } this.defineName(s.name, value); } /** Like bindName, but for names produced by def/class (may be dunder methods). */ private defineName(name: string, value: any) { if (this.fn?.isClassBody) this.fn.vars.set(name, value); else this.bindName(name, value); } private defineClass(s: PyStmt) { const bases: PyClass[] = s.bases.map((b: Expr) => { const base = this.eval(b); if (!(base instanceof PyClass)) throw new Error(`Класс ${s.name}: неподдерживаемый базовый класс`); return base; }); const cls = new PyClass(s.name, bases.length ? bases : [OBJECT]); const previousFn = this.fn; const index = this.locals.length; this.locals.push(cls.attrs); this.fn = { vars: cls.attrs, globals: new Set(), nonlocals: new Set(), isClassBody: true, index }; try { this.exec(s.body); } finally { this.locals.splice(index); this.fn = previousFn; } for (const value of cls.attrs.values()) { const fns = value instanceof PyDescriptor ? [value.fn, value.setter] : [value]; for (const f of fns) if (typeof f === "function" && f.__def && !f.__owner) f.__owner = cls; } classRegistry.set(cls.name, cls); this.defineName(s.name, cls); } private makeSuper() { const ctx = this.fn; if (!ctx?.owner) throw new Error("super() вне метода класса"); return new SuperProxy(ctx.owner, ctx.self); } instantiate(cls: PyClass, call: any[]): PyInstance { const instance = new PyInstance(cls); const init = cls.find("__init__"); if (init && typeof init.value === "function") init.value(instance, ...call); else if (call.length) throw new TypeError(`${cls.name}() не принимает аргументов`); return instance; } /** Attribute of an instance, class or super() proxy; undefined when missing. */ attrOf(o: PyInstance | PyClass | SuperProxy, name: string): any { if (o instanceof PyInstance) { if (Object.hasOwn(o.attrs, name)) return o.attrs[name]; const found = o.cls.find(name); return found ? this.member(found.value, o, o.cls) : undefined; } if (o instanceof SuperProxy) { for (const base of o.owner.bases) { const found = base.find(name); if (found) return this.member(found.value, o.self, base); } return name === "__init__" ? () => null : undefined; } const found = o.find(name); if (!found) return undefined; const value = found.value; if (value instanceof PyDescriptor) { if (value.kind === "class") return (...a: any[]) => value.fn(o, ...a); return value.kind === "static" ? value.fn : null; } return value; } private member(value: any, self: any, cls: PyClass): any { if (value instanceof PyDescriptor) { if (value.kind === "property") return value.fn(self); if (value.kind === "static") return value.fn; return (...a: any[]) => value.fn(cls, ...a); } if (typeof value === "function" && (value.__def || value.__method)) return (...a: any[]) => value(self, ...a); return value; } /** Runs a loop body; returns true on break. */ private loopBody(body: PyStmt[]): boolean { try { this.exec(body); } catch (err) { if (err instanceof BreakSignal) return true; if (err instanceof ContinueSignal) return false; throw err; } return false; } } export function* iterate(v: any): Iterable { if (Array.isArray(v) || typeof v === "string") yield* v; else if (v instanceof PyDict) yield* v.map.keys(); else if (v && typeof v[Symbol.iterator] === "function") yield* v; } /** JSON (de)serialisation of the store, keeping PyDict instances. */ export function encodeValue(v: any): any { return JSON.parse(JSON.stringify(v, (_k, x) => (typeof x === "function" ? undefined : x))); } export function decodeValue(v: any): any { if (Array.isArray(v)) return v.map(decodeValue); if (v && typeof v === "object") { if (Array.isArray(v.__dict__)) return new PyDict(v.__dict__.map(([k, x]: [any, any]) => [k, decodeValue(x)])); if (typeof v.__inst__ === "string" && v.a && typeof v.a === "object") { const attrs: Record = Object.create(null); for (const [k, x] of Object.entries(v.a)) attrs[k] = decodeValue(x); const cls = classRegistry.get(v.__inst__); return cls ? new PyInstance(cls, attrs) : attrs; // class gone: keep the data as a record } const out: Record = {}; for (const [k, x] of Object.entries(v)) out[k] = decodeValue(x); return out; } return v; } /** Builtins that need the interpreter: type checks, attribute access, callbacks. */ export function userBuiltins(py: Python): Record { const split = (args: any[]) => { const last = args[args.length - 1]; return last && typeof last === "object" && !Array.isArray(last) && Object.hasOwn(last, "__kw") ? { pos: args.slice(0, -1), kw: last.__kw as Record } : { pos: args, kw: {} }; }; const typeOf = (name: string) => py.scope.get(name); const isa = (o: any, c: any): boolean => { if (Array.isArray(c)) return c.some((x) => isa(o, x)); if (c instanceof PyClass) return c === OBJECT || (o instanceof PyInstance && o.cls.mro().includes(c)); if (c === typeOf("int") || c === typeOf("float")) return typeof o === "number"; if (c === typeOf("str")) return typeof o === "string"; if (c === typeOf("bool")) return typeof o === "boolean"; if (c === typeOf("list") || c === typeOf("tuple") || c === typeOf("set")) return Array.isArray(o); if (c === typeOf("dict")) return o instanceof PyDict; return false; }; const get = (o: any, name: string): any => { if (typeof name !== "string" || (!safeProperty(name) && !SUPPORTED_DUNDERS.has(name))) return undefined; if (o instanceof PyInstance || o instanceof PyClass) return py.attrOf(o, name); if (o instanceof PyDict) return o.has(name) ? o.get(name) : undefined; if (o === null || o === undefined || typeof o === "function") return undefined; return methodOf(o, name) ?? (Object.hasOwn(Object(o), name) ? o[name] : undefined); }; const less = (a: any, b: any) => (a < b ? -1 : a > b ? 1 : 0); return { ...Object.fromEntries(EXCEPTIONS), int: (x: any = 0, base?: number) => { if (typeof x === "string") { const text = x.trim(); const ok = base ? new RegExp(`^[+-]?[0-9a-z]+$`, "i").test(text) : /^[+-]?\d+$/.test(text); const n = ok ? parseInt(text, base ?? 10) : NaN; if (Number.isNaN(n)) py.raise("ValueError", `недопустимое значение для int(): '${x}'`); return n; } const n = Math.trunc(Number(x)); if (!Number.isFinite(n)) py.raise("ValueError", "недопустимое значение для int()"); return n; }, float: (x: any = 0) => { const n = typeof x === "string" ? Number(x.trim()) : Number(x); if (Number.isNaN(n) || (typeof x === "string" && x.trim() === "")) py.raise("ValueError", `недопустимое значение для float(): '${x}'`); return n; }, object: OBJECT, python_object: OBJECT, NoRollback: OBJECT, isinstance: (o: any, c: any) => isa(o, c), hasattr: (o: any, name: string) => get(o, name) !== undefined, getattr: (...a: any[]) => { const v = get(a[0], a[1]); if (v !== undefined) return v; if (a.length > 2) return a[2]; throw new Error(`нет атрибута ${a[1]}`); }, setattr: (o: any, name: string, value: any) => { if (!safeProperty(name)) throw new Error("Служебный атрибут недоступен"); py.bind({ k: "attr", o: { k: "const", v: o }, a: name }, value); }, callable: (x: any) => typeof x === "function" || x instanceof PyClass, map: (f: any, xs: any) => [...iterate(xs)].map((x) => f(x)), filter: (f: any, xs: any) => [...iterate(xs)].filter((x) => (f === null ? truthy(x) : truthy(f(x)))), sorted: (...a: any[]) => { const { pos, kw } = split(a); const key = kw.key ?? ((x: any) => x); const out = [...iterate(pos[0])].map((x) => [key(x), x]).sort((p, q) => less(p[0], q[0])).map((p) => p[1]); return truthy(kw.reverse) ? out.reverse() : out; }, }; }