Files
pgames-manager/frontend/src/player/python.ts
T
mrleo1nidandClaude Sonnet 5.5 ea03cebf6a feat: ATL pictures/contains/function/time, colour matrices and more Python for Ren'Py
ATL
- Image blocks become timelines: picture steps with dissolve, parallel, contains,
  finite repeat, time, function f(trans, st, at), calls of other transforms with
  arguments, on hover/idle events for buttons. Animations stop when their element
  leaves the page
- Pictures defined by an expression (get_face_path("base")) are drawn at show time
- Purely graphical hints (shader, mesh, gl_*, u_*) are accepted and dropped

Colour
- matrixcolor and im.matrix.* as 5x5 matrices combined with `*`, applied through SVG
  filters; im.MatrixColor/Scale/Crop/Blur/Composite and Text/Image as values

Python
- match statement, re/copy/time/datetime/defaultdict/functools.partial, more
  str/dict methods, Character(...) created by code
- narrator("..."), Character calls, renpy.say and renpy.show/hide with expressions in
  Python blocks become script nodes
- Audit follows reachability: functions and classes nothing refers to (custom
  statement lexers, unused tools) no longer block an import

Docs: README and ARCHITECTURE now describe a multi-engine library (GameMaker and
QSP planned). Tests for the new runtime pieces.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 14:05:14 +03:00

1067 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<any, any>();
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<string, any>();
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<string, any> = 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<string, PyClass>();
/** 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<string, PyClass>([["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<string, any>;
globals: Set<string>;
nonlocals: Set<string>;
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<string, Builtin> = {
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<string, Builtin> = {
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<string, Builtin> = {
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<string, any>[] = [];
warned = new Set<string>();
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<string, any>();
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: "<lambda>", 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<string, any> = {};
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<string, any>) {
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<string, any>[], 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<string, any> = {};
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<string, any>();
const named = new Set<string>();
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<any> {
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<string, any> = 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<string, any> = {};
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<string, any> {
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<string, any> } : { 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;
},
};
}