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>
1067 lines
40 KiB
TypeScript
1067 lines
40 KiB
TypeScript
// 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;
|
||
},
|
||
};
|
||
}
|