typescript-virtual-container 1.2.9 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +0 -1
- package/README.md +141 -50
- package/biome.json +7 -0
- package/dist/SSHMimic/exec.d.ts.map +1 -1
- package/dist/SSHMimic/executor.d.ts.map +1 -1
- package/dist/SSHMimic/executor.js +32 -16
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +20 -6
- package/dist/VirtualFileSystem/binaryPack.d.ts.map +1 -1
- package/dist/VirtualFileSystem/binaryPack.js +29 -6
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +36 -13
- package/dist/VirtualPackageManager/index.d.ts.map +1 -1
- package/dist/VirtualPackageManager/index.js +192 -43
- package/dist/VirtualShell/index.d.ts +10 -4
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +18 -7
- package/dist/VirtualShell/shell.d.ts.map +1 -1
- package/dist/VirtualShell/shell.js +3 -1
- package/dist/VirtualShell/shellParser.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/commands/adduser.d.ts +6 -0
- package/dist/commands/adduser.d.ts.map +1 -1
- package/dist/commands/adduser.js +6 -0
- package/dist/commands/alias.d.ts +5 -0
- package/dist/commands/alias.d.ts.map +1 -1
- package/dist/commands/alias.js +5 -0
- package/dist/commands/apt.d.ts +5 -0
- package/dist/commands/apt.d.ts.map +1 -1
- package/dist/commands/apt.js +32 -9
- package/dist/commands/awk.d.ts +11 -0
- package/dist/commands/awk.d.ts.map +1 -1
- package/dist/commands/awk.js +15 -2
- package/dist/commands/base64.d.ts +5 -0
- package/dist/commands/base64.d.ts.map +1 -1
- package/dist/commands/base64.js +9 -1
- package/dist/commands/cat.d.ts +5 -0
- package/dist/commands/cat.d.ts.map +1 -1
- package/dist/commands/cat.js +10 -2
- package/dist/commands/cd.d.ts +5 -0
- package/dist/commands/cd.d.ts.map +1 -1
- package/dist/commands/cd.js +5 -0
- package/dist/commands/chmod.d.ts +5 -0
- package/dist/commands/chmod.d.ts.map +1 -1
- package/dist/commands/chmod.js +5 -0
- package/dist/commands/cp.d.ts +5 -0
- package/dist/commands/cp.d.ts.map +1 -1
- package/dist/commands/cp.js +5 -0
- package/dist/commands/curl.d.ts +5 -0
- package/dist/commands/curl.d.ts.map +1 -1
- package/dist/commands/curl.js +34 -6
- package/dist/commands/cut.d.ts +5 -0
- package/dist/commands/cut.d.ts.map +1 -1
- package/dist/commands/cut.js +8 -1
- package/dist/commands/date.d.ts +5 -0
- package/dist/commands/date.d.ts.map +1 -1
- package/dist/commands/date.js +7 -1
- package/dist/commands/declare.d.ts +3 -0
- package/dist/commands/declare.d.ts.map +1 -0
- package/dist/commands/declare.js +39 -0
- package/dist/commands/diff.d.ts +5 -0
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +5 -0
- package/dist/commands/dpkg.d.ts +5 -0
- package/dist/commands/dpkg.d.ts.map +1 -1
- package/dist/commands/dpkg.js +24 -7
- package/dist/commands/du.d.ts.map +1 -1
- package/dist/commands/du.js +8 -2
- package/dist/commands/echo.d.ts +5 -0
- package/dist/commands/echo.d.ts.map +1 -1
- package/dist/commands/echo.js +13 -4
- package/dist/commands/env.d.ts +5 -0
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +11 -1
- package/dist/commands/exit.d.ts +5 -0
- package/dist/commands/exit.d.ts.map +1 -1
- package/dist/commands/exit.js +12 -2
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +3 -1
- package/dist/commands/find.d.ts +5 -0
- package/dist/commands/find.d.ts.map +1 -1
- package/dist/commands/find.js +5 -0
- package/dist/commands/free.d.ts +5 -0
- package/dist/commands/free.d.ts.map +1 -1
- package/dist/commands/free.js +5 -0
- package/dist/commands/grep.d.ts +5 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +12 -2
- package/dist/commands/gzip.d.ts +5 -0
- package/dist/commands/gzip.d.ts.map +1 -1
- package/dist/commands/gzip.js +18 -2
- package/dist/commands/head.d.ts +5 -0
- package/dist/commands/head.d.ts.map +1 -1
- package/dist/commands/head.js +5 -0
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +98 -45
- package/dist/commands/history.d.ts +5 -0
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +5 -0
- package/dist/commands/hostname.d.ts +5 -0
- package/dist/commands/hostname.d.ts.map +1 -1
- package/dist/commands/hostname.js +5 -0
- package/dist/commands/id.d.ts.map +1 -1
- package/dist/commands/id.js +4 -1
- package/dist/commands/index.d.ts +2 -17
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -340
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +3 -1
- package/dist/commands/lsb-release.d.ts.map +1 -1
- package/dist/commands/lsb-release.js +8 -2
- package/dist/commands/nano.js +1 -1
- package/dist/commands/neofetch.js +1 -1
- package/dist/commands/node.d.ts +9 -0
- package/dist/commands/node.d.ts.map +1 -0
- package/dist/commands/node.js +316 -0
- package/dist/commands/npm.d.ts +19 -0
- package/dist/commands/npm.d.ts.map +1 -0
- package/dist/commands/npm.js +109 -0
- package/dist/commands/ping.d.ts.map +1 -1
- package/dist/commands/ping.js +3 -1
- package/dist/commands/printf.d.ts +3 -0
- package/dist/commands/printf.d.ts.map +1 -0
- package/dist/commands/printf.js +113 -0
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +4 -1
- package/dist/commands/python.d.ts +30 -0
- package/dist/commands/python.d.ts.map +1 -0
- package/dist/commands/python.js +2058 -0
- package/dist/commands/read.d.ts +3 -0
- package/dist/commands/read.d.ts.map +1 -0
- package/dist/commands/read.js +34 -0
- package/dist/commands/registry.d.ts +8 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +229 -0
- package/dist/commands/runtime.d.ts +6 -0
- package/dist/commands/runtime.d.ts.map +1 -0
- package/dist/commands/runtime.js +280 -0
- package/dist/commands/sed.d.ts.map +1 -1
- package/dist/commands/sed.js +11 -3
- package/dist/commands/set.d.ts.map +1 -1
- package/dist/commands/set.js +9 -3
- package/dist/commands/sh.d.ts.map +1 -1
- package/dist/commands/sh.js +57 -36
- package/dist/commands/shift.d.ts +5 -0
- package/dist/commands/shift.d.ts.map +1 -0
- package/dist/commands/shift.js +52 -0
- package/dist/commands/sleep.d.ts.map +1 -1
- package/dist/commands/sort.d.ts.map +1 -1
- package/dist/commands/sort.js +4 -2
- package/dist/commands/source.d.ts.map +1 -1
- package/dist/commands/source.js +5 -2
- package/dist/commands/sudo.js +1 -1
- package/dist/commands/tar.d.ts.map +1 -1
- package/dist/commands/tar.js +11 -3
- package/dist/commands/tee.d.ts.map +1 -1
- package/dist/commands/tee.js +8 -6
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +46 -24
- package/dist/commands/tr.d.ts.map +1 -1
- package/dist/commands/tr.js +3 -1
- package/dist/commands/true.d.ts +4 -0
- package/dist/commands/true.d.ts.map +1 -0
- package/dist/commands/true.js +14 -0
- package/dist/commands/type.d.ts.map +1 -1
- package/dist/commands/type.js +1 -1
- package/dist/commands/uname.d.ts.map +1 -1
- package/dist/commands/uname.js +4 -1
- package/dist/commands/uniq.d.ts.map +1 -1
- package/dist/commands/uptime.d.ts.map +1 -1
- package/dist/commands/uptime.js +4 -1
- package/dist/commands/wget.d.ts.map +1 -1
- package/dist/commands/wget.js +32 -7
- package/dist/commands/which.d.ts.map +1 -1
- package/dist/commands/xargs.d.ts.map +1 -1
- package/dist/commands/xargs.js +1 -1
- package/dist/index.d.ts +15 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -9
- package/dist/modules/linuxRootfs.d.ts +18 -1
- package/dist/modules/linuxRootfs.d.ts.map +1 -1
- package/dist/modules/linuxRootfs.js +160 -17
- package/dist/standalone-wo-sftp.d.ts +2 -0
- package/dist/standalone-wo-sftp.d.ts.map +1 -0
- package/dist/standalone-wo-sftp.js +30 -0
- package/dist/utils/expand.d.ts +50 -0
- package/dist/utils/expand.d.ts.map +1 -0
- package/dist/utils/expand.js +183 -0
- package/dist/utils/vfsDiff.d.ts +90 -0
- package/dist/utils/vfsDiff.d.ts.map +1 -0
- package/dist/utils/vfsDiff.js +177 -0
- package/package.json +2 -1
- package/src/SSHMimic/exec.ts +10 -1
- package/src/SSHMimic/executor.ts +104 -18
- package/src/SSHMimic/index.ts +49 -15
- package/src/VirtualFileSystem/binaryPack.ts +35 -8
- package/src/VirtualFileSystem/index.ts +78 -28
- package/src/VirtualPackageManager/index.ts +208 -49
- package/src/VirtualShell/index.ts +35 -7
- package/src/VirtualShell/shell.ts +23 -3
- package/src/VirtualShell/shellParser.ts +134 -36
- package/src/VirtualUserManager/index.ts +7 -2
- package/src/commands/adduser.ts +6 -0
- package/src/commands/alias.ts +5 -1
- package/src/commands/apt.ts +47 -17
- package/src/commands/awk.ts +20 -6
- package/src/commands/base64.ts +13 -2
- package/src/commands/cat.ts +13 -5
- package/src/commands/cd.ts +5 -0
- package/src/commands/chmod.ts +5 -0
- package/src/commands/cp.ts +5 -0
- package/src/commands/curl.ts +56 -12
- package/src/commands/cut.ts +8 -1
- package/src/commands/date.ts +7 -1
- package/src/commands/declare.ts +44 -0
- package/src/commands/diff.ts +17 -3
- package/src/commands/dpkg.ts +33 -11
- package/src/commands/du.ts +17 -5
- package/src/commands/echo.ts +22 -9
- package/src/commands/env.ts +11 -1
- package/src/commands/exit.ts +12 -2
- package/src/commands/export.ts +3 -1
- package/src/commands/find.ts +5 -0
- package/src/commands/free.ts +9 -2
- package/src/commands/grep.ts +12 -2
- package/src/commands/gzip.ts +28 -4
- package/src/commands/head.ts +5 -0
- package/src/commands/help.ts +121 -47
- package/src/commands/history.ts +7 -2
- package/src/commands/hostname.ts +5 -0
- package/src/commands/id.ts +4 -1
- package/src/commands/index.ts +9 -360
- package/src/commands/ls.ts +5 -3
- package/src/commands/lsb-release.ts +8 -2
- package/src/commands/nano.ts +1 -1
- package/src/commands/neofetch.ts +1 -1
- package/src/commands/node.ts +341 -0
- package/src/commands/npm.ts +132 -0
- package/src/commands/ping.ts +6 -2
- package/src/commands/printf.ts +112 -0
- package/src/commands/ps.ts +21 -9
- package/src/commands/python.ts +2229 -0
- package/src/commands/read.ts +41 -0
- package/src/commands/registry.ts +244 -0
- package/src/commands/runtime.ts +353 -0
- package/src/commands/sed.ts +27 -9
- package/src/commands/set.ts +9 -3
- package/src/commands/sh.ts +159 -55
- package/src/commands/shift.ts +53 -0
- package/src/commands/sleep.ts +2 -1
- package/src/commands/sort.ts +10 -6
- package/src/commands/source.ts +15 -3
- package/src/commands/sudo.ts +1 -1
- package/src/commands/tar.ts +28 -7
- package/src/commands/tee.ts +7 -1
- package/src/commands/test.ts +61 -26
- package/src/commands/tr.ts +3 -1
- package/src/commands/true.ts +17 -0
- package/src/commands/type.ts +6 -3
- package/src/commands/uname.ts +5 -1
- package/src/commands/uniq.ts +8 -2
- package/src/commands/uptime.ts +4 -1
- package/src/commands/wget.ts +51 -12
- package/src/commands/which.ts +5 -2
- package/src/commands/xargs.ts +11 -2
- package/src/index.ts +23 -24
- package/src/modules/linuxRootfs.ts +233 -30
- package/src/standalone-wo-sftp.ts +38 -0
- package/src/utils/expand.ts +238 -0
- package/src/utils/vfsDiff.ts +275 -0
- package/standalone-wo-sftp.js +507 -0
- package/standalone-wo-sftp.js.map +7 -0
- package/standalone.js +253 -191
- package/standalone.js.map +4 -4
- package/tests/bun-test-shim.ts +9 -1
- package/tests/command-helpers.test.ts +1 -5
- package/tests/new-features.test.ts +415 -5
- package/tests/parser-executor.test.ts +27 -27
- package/tests/sftp.test.ts +122 -42
- package/tests/users.test.ts +23 -5
- package/CHANGELOG.md +0 -150
|
@@ -0,0 +1,2058 @@
|
|
|
1
|
+
import { ifFlag } from "./command-helpers";
|
|
2
|
+
import { resolvePath } from "./helpers";
|
|
3
|
+
const VERSION = "Python 3.11.2";
|
|
4
|
+
const _VERSION_SHORT = "3.11.2";
|
|
5
|
+
const VERSION_INFO = "3.11.2 (default, Mar 13 2023, 12:18:29) [GCC 12.2.0]";
|
|
6
|
+
const NONE = { __pytype__: "none" };
|
|
7
|
+
function pyDict(entries = []) {
|
|
8
|
+
return { __pytype__: "dict", data: new Map(entries) };
|
|
9
|
+
}
|
|
10
|
+
function pyRange(start, stop, step = 1) {
|
|
11
|
+
return { __pytype__: "range", start, stop, step };
|
|
12
|
+
}
|
|
13
|
+
function isPyDict(v) {
|
|
14
|
+
return (!!v &&
|
|
15
|
+
typeof v === "object" &&
|
|
16
|
+
!Array.isArray(v) &&
|
|
17
|
+
v.__pytype__ === "dict");
|
|
18
|
+
}
|
|
19
|
+
function isPyRange(v) {
|
|
20
|
+
return (!!v &&
|
|
21
|
+
typeof v === "object" &&
|
|
22
|
+
!Array.isArray(v) &&
|
|
23
|
+
v.__pytype__ === "range");
|
|
24
|
+
}
|
|
25
|
+
function isPyFunc(v) {
|
|
26
|
+
return (!!v &&
|
|
27
|
+
typeof v === "object" &&
|
|
28
|
+
!Array.isArray(v) &&
|
|
29
|
+
v.__pytype__ === "func");
|
|
30
|
+
}
|
|
31
|
+
function isPyClass(v) {
|
|
32
|
+
return (!!v &&
|
|
33
|
+
typeof v === "object" &&
|
|
34
|
+
!Array.isArray(v) &&
|
|
35
|
+
v.__pytype__ === "class");
|
|
36
|
+
}
|
|
37
|
+
function isPyInstance(v) {
|
|
38
|
+
return (!!v &&
|
|
39
|
+
typeof v === "object" &&
|
|
40
|
+
!Array.isArray(v) &&
|
|
41
|
+
v.__pytype__ === "instance");
|
|
42
|
+
}
|
|
43
|
+
function isPyNone(v) {
|
|
44
|
+
return (!!v &&
|
|
45
|
+
typeof v === "object" &&
|
|
46
|
+
!Array.isArray(v) &&
|
|
47
|
+
v.__pytype__ === "none");
|
|
48
|
+
}
|
|
49
|
+
// ─── repr / str ───────────────────────────────────────────────────────────────
|
|
50
|
+
function pyRepr(v) {
|
|
51
|
+
if (v === null || isPyNone(v))
|
|
52
|
+
return "None";
|
|
53
|
+
if (v === true)
|
|
54
|
+
return "True";
|
|
55
|
+
if (v === false)
|
|
56
|
+
return "False";
|
|
57
|
+
if (typeof v === "number")
|
|
58
|
+
return Number.isInteger(v)
|
|
59
|
+
? String(v)
|
|
60
|
+
: v.toPrecision(12).replace(/\.?0+$/, "");
|
|
61
|
+
if (typeof v === "string")
|
|
62
|
+
return `'${v.replace(/'/g, "\\'")}'`;
|
|
63
|
+
if (Array.isArray(v))
|
|
64
|
+
return `[${v.map(pyRepr).join(", ")}]`;
|
|
65
|
+
if (isPyDict(v))
|
|
66
|
+
return `{${[...v.data.entries()].map(([k, val]) => `'${k}': ${pyRepr(val)}`).join(", ")}}`;
|
|
67
|
+
if (isPyRange(v))
|
|
68
|
+
return `range(${v.start}, ${v.stop}${v.step !== 1 ? `, ${v.step}` : ""})`;
|
|
69
|
+
if (isPyFunc(v))
|
|
70
|
+
return `<function ${v.name} at 0x...>`;
|
|
71
|
+
if (isPyClass(v))
|
|
72
|
+
return `<class '${v.name}'>`;
|
|
73
|
+
if (isPyInstance(v))
|
|
74
|
+
return `<${v.cls.name} object at 0x...>`;
|
|
75
|
+
return String(v);
|
|
76
|
+
}
|
|
77
|
+
function pyStr(v) {
|
|
78
|
+
if (v === null || isPyNone(v))
|
|
79
|
+
return "None";
|
|
80
|
+
if (v === true)
|
|
81
|
+
return "True";
|
|
82
|
+
if (v === false)
|
|
83
|
+
return "False";
|
|
84
|
+
if (typeof v === "number")
|
|
85
|
+
return Number.isInteger(v)
|
|
86
|
+
? String(v)
|
|
87
|
+
: v.toPrecision(12).replace(/\.?0+$/, "");
|
|
88
|
+
if (typeof v === "string")
|
|
89
|
+
return v;
|
|
90
|
+
if (Array.isArray(v))
|
|
91
|
+
return `[${v.map(pyRepr).join(", ")}]`;
|
|
92
|
+
if (isPyDict(v))
|
|
93
|
+
return `{${[...v.data.entries()].map(([k, val]) => `'${k}': ${pyRepr(val)}`).join(", ")}}`;
|
|
94
|
+
if (isPyRange(v))
|
|
95
|
+
return `range(${v.start}, ${v.stop}${v.step !== 1 ? `, ${v.step}` : ""})`;
|
|
96
|
+
return pyRepr(v);
|
|
97
|
+
}
|
|
98
|
+
function pyBool(v) {
|
|
99
|
+
if (v === null || isPyNone(v))
|
|
100
|
+
return false;
|
|
101
|
+
if (typeof v === "boolean")
|
|
102
|
+
return v;
|
|
103
|
+
if (typeof v === "number")
|
|
104
|
+
return v !== 0;
|
|
105
|
+
if (typeof v === "string")
|
|
106
|
+
return v.length > 0;
|
|
107
|
+
if (Array.isArray(v))
|
|
108
|
+
return v.length > 0;
|
|
109
|
+
if (isPyDict(v))
|
|
110
|
+
return v.data.size > 0;
|
|
111
|
+
if (isPyRange(v))
|
|
112
|
+
return pyRangeLength(v) > 0;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function pyRangeLength(r) {
|
|
116
|
+
if (r.step === 0)
|
|
117
|
+
return 0;
|
|
118
|
+
const n = Math.ceil((r.stop - r.start) / r.step);
|
|
119
|
+
return Math.max(0, n);
|
|
120
|
+
}
|
|
121
|
+
function pyRangeItems(r) {
|
|
122
|
+
const items = [];
|
|
123
|
+
for (let i = r.start; r.step > 0 ? i < r.stop : i > r.stop; i += r.step) {
|
|
124
|
+
items.push(i);
|
|
125
|
+
if (items.length > 10000)
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
return items;
|
|
129
|
+
}
|
|
130
|
+
function pyIter(v) {
|
|
131
|
+
if (Array.isArray(v))
|
|
132
|
+
return v;
|
|
133
|
+
if (typeof v === "string")
|
|
134
|
+
return [...v];
|
|
135
|
+
if (isPyRange(v))
|
|
136
|
+
return pyRangeItems(v);
|
|
137
|
+
if (isPyDict(v))
|
|
138
|
+
return [...v.data.keys()];
|
|
139
|
+
throw new PyError("TypeError", `'${pyTypeName(v)}' object is not iterable`);
|
|
140
|
+
}
|
|
141
|
+
function pyTypeName(v) {
|
|
142
|
+
if (v === null || isPyNone(v))
|
|
143
|
+
return "NoneType";
|
|
144
|
+
if (typeof v === "boolean")
|
|
145
|
+
return "bool";
|
|
146
|
+
if (typeof v === "number")
|
|
147
|
+
return Number.isInteger(v) ? "int" : "float";
|
|
148
|
+
if (typeof v === "string")
|
|
149
|
+
return "str";
|
|
150
|
+
if (Array.isArray(v))
|
|
151
|
+
return "list";
|
|
152
|
+
if (isPyDict(v))
|
|
153
|
+
return "dict";
|
|
154
|
+
if (isPyRange(v))
|
|
155
|
+
return "range";
|
|
156
|
+
if (isPyFunc(v))
|
|
157
|
+
return "function";
|
|
158
|
+
if (isPyClass(v))
|
|
159
|
+
return "type";
|
|
160
|
+
if (isPyInstance(v))
|
|
161
|
+
return v.cls.name;
|
|
162
|
+
return "object";
|
|
163
|
+
}
|
|
164
|
+
class PyError {
|
|
165
|
+
type;
|
|
166
|
+
message;
|
|
167
|
+
constructor(type, message) {
|
|
168
|
+
this.type = type;
|
|
169
|
+
this.message = message;
|
|
170
|
+
}
|
|
171
|
+
toString() {
|
|
172
|
+
return `${this.type}: ${this.message}`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
class ReturnSignal {
|
|
176
|
+
value;
|
|
177
|
+
constructor(value) {
|
|
178
|
+
this.value = value;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
class BreakSignal {
|
|
182
|
+
}
|
|
183
|
+
class ContinueSignal {
|
|
184
|
+
}
|
|
185
|
+
class ExitSignal {
|
|
186
|
+
code;
|
|
187
|
+
constructor(code) {
|
|
188
|
+
this.code = code;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function makeRootScope(cwd) {
|
|
192
|
+
const scope = new Map();
|
|
193
|
+
// Built-in modules (lazy)
|
|
194
|
+
const osModule = pyDict([
|
|
195
|
+
["sep", "/"],
|
|
196
|
+
["linesep", "\n"],
|
|
197
|
+
["curdir", "."],
|
|
198
|
+
["pardir", ".."],
|
|
199
|
+
]);
|
|
200
|
+
osModule.__methods__ = {
|
|
201
|
+
getcwd: () => cwd,
|
|
202
|
+
getenv: (k) => typeof k === "string" ? (process.env[k] ?? NONE) : NONE,
|
|
203
|
+
path: pyDict([
|
|
204
|
+
["join", NONE],
|
|
205
|
+
["exists", NONE],
|
|
206
|
+
["dirname", NONE],
|
|
207
|
+
["basename", NONE],
|
|
208
|
+
]),
|
|
209
|
+
listdir: () => [],
|
|
210
|
+
};
|
|
211
|
+
scope.set("__builtins__", NONE);
|
|
212
|
+
scope.set("__name__", "__main__");
|
|
213
|
+
scope.set("__cwd__", cwd);
|
|
214
|
+
return scope;
|
|
215
|
+
}
|
|
216
|
+
// ─── built-in modules ─────────────────────────────────────────────────────────
|
|
217
|
+
function makeOsModule(cwd) {
|
|
218
|
+
const path = pyDict([
|
|
219
|
+
["sep", "/"],
|
|
220
|
+
["curdir", "."],
|
|
221
|
+
]);
|
|
222
|
+
const os = pyDict([
|
|
223
|
+
["sep", "/"],
|
|
224
|
+
["linesep", "\n"],
|
|
225
|
+
["name", "posix"],
|
|
226
|
+
]);
|
|
227
|
+
// We'll handle method calls in callMethod
|
|
228
|
+
os._cwd = cwd;
|
|
229
|
+
path._cwd = cwd;
|
|
230
|
+
os.path = path;
|
|
231
|
+
return os;
|
|
232
|
+
}
|
|
233
|
+
function makeSysModule() {
|
|
234
|
+
return pyDict([
|
|
235
|
+
["version", VERSION_INFO],
|
|
236
|
+
[
|
|
237
|
+
"version_info",
|
|
238
|
+
pyDict([
|
|
239
|
+
["major", 3],
|
|
240
|
+
["minor", 11],
|
|
241
|
+
["micro", 2],
|
|
242
|
+
].map(([k, v]) => [k, v])),
|
|
243
|
+
],
|
|
244
|
+
["platform", "linux"],
|
|
245
|
+
["executable", "/usr/bin/python3"],
|
|
246
|
+
["prefix", "/usr"],
|
|
247
|
+
["path", ["/usr/lib/python3.11", "/usr/lib/python3.11/lib-dynload"]],
|
|
248
|
+
["argv", [""]],
|
|
249
|
+
["maxsize", 9007199254740991],
|
|
250
|
+
]);
|
|
251
|
+
}
|
|
252
|
+
function makeMathModule() {
|
|
253
|
+
return pyDict([
|
|
254
|
+
["pi", Math.PI],
|
|
255
|
+
["e", Math.E],
|
|
256
|
+
["tau", Math.PI * 2],
|
|
257
|
+
["inf", Infinity],
|
|
258
|
+
["nan", NaN],
|
|
259
|
+
["sqrt", NONE],
|
|
260
|
+
["floor", NONE],
|
|
261
|
+
["ceil", NONE],
|
|
262
|
+
["log", NONE],
|
|
263
|
+
["pow", NONE],
|
|
264
|
+
["sin", NONE],
|
|
265
|
+
["cos", NONE],
|
|
266
|
+
["tan", NONE],
|
|
267
|
+
["fabs", NONE],
|
|
268
|
+
["factorial", NONE],
|
|
269
|
+
]);
|
|
270
|
+
}
|
|
271
|
+
function makeJsonModule() {
|
|
272
|
+
return pyDict([
|
|
273
|
+
["dumps", NONE],
|
|
274
|
+
["loads", NONE],
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
function makeReModule() {
|
|
278
|
+
return pyDict([
|
|
279
|
+
["match", NONE],
|
|
280
|
+
["search", NONE],
|
|
281
|
+
["findall", NONE],
|
|
282
|
+
["sub", NONE],
|
|
283
|
+
["split", NONE],
|
|
284
|
+
["compile", NONE],
|
|
285
|
+
]);
|
|
286
|
+
}
|
|
287
|
+
const MODULE_FACTORIES = {
|
|
288
|
+
os: makeOsModule,
|
|
289
|
+
sys: () => makeSysModule(),
|
|
290
|
+
math: () => makeMathModule(),
|
|
291
|
+
json: () => makeJsonModule(),
|
|
292
|
+
re: () => makeReModule(),
|
|
293
|
+
random: () => pyDict([
|
|
294
|
+
["random", NONE],
|
|
295
|
+
["randint", NONE],
|
|
296
|
+
["choice", NONE],
|
|
297
|
+
["shuffle", NONE],
|
|
298
|
+
]),
|
|
299
|
+
time: () => pyDict([
|
|
300
|
+
["time", NONE],
|
|
301
|
+
["sleep", NONE],
|
|
302
|
+
["ctime", NONE],
|
|
303
|
+
]),
|
|
304
|
+
datetime: () => pyDict([
|
|
305
|
+
["datetime", NONE],
|
|
306
|
+
["date", NONE],
|
|
307
|
+
["timedelta", NONE],
|
|
308
|
+
]),
|
|
309
|
+
collections: () => pyDict([
|
|
310
|
+
["Counter", NONE],
|
|
311
|
+
["defaultdict", NONE],
|
|
312
|
+
["OrderedDict", NONE],
|
|
313
|
+
]),
|
|
314
|
+
itertools: () => pyDict([
|
|
315
|
+
["chain", NONE],
|
|
316
|
+
["product", NONE],
|
|
317
|
+
["combinations", NONE],
|
|
318
|
+
["permutations", NONE],
|
|
319
|
+
]),
|
|
320
|
+
functools: () => pyDict([
|
|
321
|
+
["reduce", NONE],
|
|
322
|
+
["partial", NONE],
|
|
323
|
+
["lru_cache", NONE],
|
|
324
|
+
]),
|
|
325
|
+
string: () => pyDict([
|
|
326
|
+
["ascii_letters", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"],
|
|
327
|
+
["digits", "0123456789"],
|
|
328
|
+
["punctuation", "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"],
|
|
329
|
+
]),
|
|
330
|
+
};
|
|
331
|
+
// ─── interpreter ─────────────────────────────────────────────────────────────
|
|
332
|
+
class Interpreter {
|
|
333
|
+
cwd;
|
|
334
|
+
output = [];
|
|
335
|
+
stderr = [];
|
|
336
|
+
modules = new Map();
|
|
337
|
+
constructor(cwd) {
|
|
338
|
+
this.cwd = cwd;
|
|
339
|
+
}
|
|
340
|
+
getOutput() {
|
|
341
|
+
return this.output.join("\n") + (this.output.length ? "\n" : "");
|
|
342
|
+
}
|
|
343
|
+
getStderr() {
|
|
344
|
+
return this.stderr.join("\n") + (this.stderr.length ? "\n" : "");
|
|
345
|
+
}
|
|
346
|
+
// ── tokenizer / parser helpers ──────────────────────────────────────────
|
|
347
|
+
splitArgs(s) {
|
|
348
|
+
// Split on commas respecting balanced parens, brackets, braces, quotes
|
|
349
|
+
const args = [];
|
|
350
|
+
let depth = 0, cur = "", inStr = false, strChar = "";
|
|
351
|
+
for (let i = 0; i < s.length; i++) {
|
|
352
|
+
const ch = s[i];
|
|
353
|
+
if (inStr) {
|
|
354
|
+
cur += ch;
|
|
355
|
+
if (ch === strChar && s[i - 1] !== "\\")
|
|
356
|
+
inStr = false;
|
|
357
|
+
}
|
|
358
|
+
else if (ch === '"' || ch === "'") {
|
|
359
|
+
inStr = true;
|
|
360
|
+
strChar = ch;
|
|
361
|
+
cur += ch;
|
|
362
|
+
}
|
|
363
|
+
else if ("([{".includes(ch)) {
|
|
364
|
+
depth++;
|
|
365
|
+
cur += ch;
|
|
366
|
+
}
|
|
367
|
+
else if (")]}".includes(ch)) {
|
|
368
|
+
depth--;
|
|
369
|
+
cur += ch;
|
|
370
|
+
}
|
|
371
|
+
else if (ch === "," && depth === 0) {
|
|
372
|
+
args.push(cur.trim());
|
|
373
|
+
cur = "";
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
cur += ch;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (cur.trim())
|
|
380
|
+
args.push(cur.trim());
|
|
381
|
+
return args;
|
|
382
|
+
}
|
|
383
|
+
// ── expression evaluator ─────────────────────────────────────────────────
|
|
384
|
+
pyEval(expr, scope) {
|
|
385
|
+
expr = expr.trim();
|
|
386
|
+
if (!expr)
|
|
387
|
+
return NONE;
|
|
388
|
+
// None True False literals
|
|
389
|
+
if (expr === "None")
|
|
390
|
+
return NONE;
|
|
391
|
+
if (expr === "True")
|
|
392
|
+
return true;
|
|
393
|
+
if (expr === "False")
|
|
394
|
+
return false;
|
|
395
|
+
if (expr === "...")
|
|
396
|
+
return NONE;
|
|
397
|
+
// Number literals
|
|
398
|
+
if (/^-?\d+$/.test(expr))
|
|
399
|
+
return parseInt(expr, 10);
|
|
400
|
+
if (/^-?\d+\.\d*$/.test(expr))
|
|
401
|
+
return parseFloat(expr);
|
|
402
|
+
if (/^0x[0-9a-fA-F]+$/.test(expr))
|
|
403
|
+
return parseInt(expr, 16);
|
|
404
|
+
if (/^0o[0-7]+$/.test(expr))
|
|
405
|
+
return parseInt(expr.slice(2), 8);
|
|
406
|
+
// String literals (single, double, triple)
|
|
407
|
+
if (/^('''[\s\S]*'''|"""[\s\S]*""")$/.test(expr)) {
|
|
408
|
+
return expr.slice(3, -3);
|
|
409
|
+
}
|
|
410
|
+
if (/^(['"])(.*)\1$/s.test(expr)) {
|
|
411
|
+
const inner = expr.slice(1, -1);
|
|
412
|
+
return inner
|
|
413
|
+
.replace(/\\n/g, "\n")
|
|
414
|
+
.replace(/\\t/g, "\t")
|
|
415
|
+
.replace(/\\r/g, "\r")
|
|
416
|
+
.replace(/\\\\/g, "\\")
|
|
417
|
+
.replace(/\\'/g, "'")
|
|
418
|
+
.replace(/\\"/g, '"');
|
|
419
|
+
}
|
|
420
|
+
// f-strings
|
|
421
|
+
const fMatch = expr.match(/^f(['"])([\s\S]*)\1$/);
|
|
422
|
+
if (fMatch) {
|
|
423
|
+
let result = fMatch[2];
|
|
424
|
+
result = result.replace(/\{([^{}]+)\}/g, (_, inner) => {
|
|
425
|
+
try {
|
|
426
|
+
return pyStr(this.pyEval(inner.trim(), scope));
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return `{${inner}}`;
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
// Byte strings b"..." — treat as string
|
|
435
|
+
const bMatch = expr.match(/^b(['"])(.*)\1$/s);
|
|
436
|
+
if (bMatch)
|
|
437
|
+
return bMatch[2];
|
|
438
|
+
// List literal [...]
|
|
439
|
+
if (expr.startsWith("[") && expr.endsWith("]")) {
|
|
440
|
+
const inner = expr.slice(1, -1).trim();
|
|
441
|
+
if (!inner)
|
|
442
|
+
return [];
|
|
443
|
+
// List comprehension
|
|
444
|
+
const compMatch = inner.match(/^(.+?)\s+for\s+(\w+)\s+in\s+(.+?)(?:\s+if\s+(.+))?$/);
|
|
445
|
+
if (compMatch) {
|
|
446
|
+
const [, itemExpr, varName, iterExpr, condExpr] = compMatch;
|
|
447
|
+
const iterable = pyIter(this.pyEval(iterExpr.trim(), scope));
|
|
448
|
+
const result = [];
|
|
449
|
+
for (const item of iterable) {
|
|
450
|
+
const inner2 = new Map(scope);
|
|
451
|
+
inner2.set(varName, item);
|
|
452
|
+
if (condExpr && !pyBool(this.pyEval(condExpr, inner2)))
|
|
453
|
+
continue;
|
|
454
|
+
result.push(this.pyEval(itemExpr.trim(), inner2));
|
|
455
|
+
}
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
return this.splitArgs(inner).map((a) => this.pyEval(a, scope));
|
|
459
|
+
}
|
|
460
|
+
// Tuple (...)
|
|
461
|
+
if (expr.startsWith("(") && expr.endsWith(")")) {
|
|
462
|
+
const inner = expr.slice(1, -1).trim();
|
|
463
|
+
if (!inner)
|
|
464
|
+
return [];
|
|
465
|
+
const parts = this.splitArgs(inner);
|
|
466
|
+
if (parts.length === 1 && !inner.endsWith(","))
|
|
467
|
+
return this.pyEval(parts[0], scope);
|
|
468
|
+
return parts.map((a) => this.pyEval(a, scope));
|
|
469
|
+
}
|
|
470
|
+
// Dict literal {...}
|
|
471
|
+
if (expr.startsWith("{") && expr.endsWith("}")) {
|
|
472
|
+
const inner = expr.slice(1, -1).trim();
|
|
473
|
+
if (!inner)
|
|
474
|
+
return pyDict();
|
|
475
|
+
const dict = pyDict();
|
|
476
|
+
for (const entry of this.splitArgs(inner)) {
|
|
477
|
+
const colonIdx = entry.indexOf(":");
|
|
478
|
+
if (colonIdx === -1)
|
|
479
|
+
continue;
|
|
480
|
+
const k = pyStr(this.pyEval(entry.slice(0, colonIdx).trim(), scope));
|
|
481
|
+
const v = this.pyEval(entry.slice(colonIdx + 1).trim(), scope);
|
|
482
|
+
dict.data.set(k, v);
|
|
483
|
+
}
|
|
484
|
+
return dict;
|
|
485
|
+
}
|
|
486
|
+
// not expr
|
|
487
|
+
const notMatch = expr.match(/^not\s+(.+)$/);
|
|
488
|
+
if (notMatch)
|
|
489
|
+
return !pyBool(this.pyEval(notMatch[1], scope));
|
|
490
|
+
// Binary operators (right-to-left scan at lowest depth)
|
|
491
|
+
const binaryOps = [
|
|
492
|
+
["or"],
|
|
493
|
+
["and"],
|
|
494
|
+
["in", "not in", "is not", "is", "==", "!=", "<=", ">=", "<", ">"],
|
|
495
|
+
["+", "-"],
|
|
496
|
+
["**"],
|
|
497
|
+
["*", "//", "/", "%"],
|
|
498
|
+
];
|
|
499
|
+
for (const ops of binaryOps) {
|
|
500
|
+
const result = this.tryBinaryOp(expr, ops, scope);
|
|
501
|
+
if (result !== undefined)
|
|
502
|
+
return result;
|
|
503
|
+
}
|
|
504
|
+
// Unary minus
|
|
505
|
+
if (expr.startsWith("-")) {
|
|
506
|
+
const val = this.pyEval(expr.slice(1), scope);
|
|
507
|
+
if (typeof val === "number")
|
|
508
|
+
return -val;
|
|
509
|
+
}
|
|
510
|
+
// Subscript: expr[key] or expr[start:stop]
|
|
511
|
+
if (process.env.PY_DEBUG)
|
|
512
|
+
console.error("eval:", JSON.stringify(expr));
|
|
513
|
+
if (expr.endsWith("]") && !expr.startsWith("[")) {
|
|
514
|
+
const bracketStart = this.findMatchingBracket(expr, "[");
|
|
515
|
+
if (bracketStart !== -1) {
|
|
516
|
+
const obj = this.pyEval(expr.slice(0, bracketStart), scope);
|
|
517
|
+
const key = expr.slice(bracketStart + 1, -1);
|
|
518
|
+
return this.subscript(obj, key, scope);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Function call: name(args...) — must come BEFORE dotMatch to avoid
|
|
522
|
+
// print('x'.upper()) being parsed as dotMatch("print('x'", "upper", "()")
|
|
523
|
+
const callMatch = expr.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*\(([\s\S]*)\)$/);
|
|
524
|
+
if (callMatch) {
|
|
525
|
+
const [, name, argsStr] = callMatch;
|
|
526
|
+
const callArgs = (argsStr?.trim() ? this.splitArgs(argsStr) : []).map((a) => this.pyEval(a, scope));
|
|
527
|
+
return this.callBuiltin(name, callArgs, scope);
|
|
528
|
+
}
|
|
529
|
+
// Attribute access / method call: expr.attr or expr.method(args)
|
|
530
|
+
// Uses a depth-aware scanner to find the rightmost dot at depth 0
|
|
531
|
+
const dotResult = this.findDotAccess(expr);
|
|
532
|
+
if (dotResult) {
|
|
533
|
+
const { objExpr, attr, callPart } = dotResult;
|
|
534
|
+
const obj = this.pyEval(objExpr, scope);
|
|
535
|
+
if (callPart !== undefined) {
|
|
536
|
+
const argsInner = callPart.slice(1, -1);
|
|
537
|
+
const callArgs = argsInner.trim()
|
|
538
|
+
? this.splitArgs(argsInner).map((a) => this.pyEval(a, scope))
|
|
539
|
+
: [];
|
|
540
|
+
return this.callMethod(obj, attr, callArgs, scope);
|
|
541
|
+
}
|
|
542
|
+
return this.getAttr(obj, attr, scope);
|
|
543
|
+
}
|
|
544
|
+
// Variable lookup
|
|
545
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(expr)) {
|
|
546
|
+
if (scope.has(expr))
|
|
547
|
+
return scope.get(expr);
|
|
548
|
+
// Check parent scopes (for closures)
|
|
549
|
+
throw new PyError("NameError", `name '${expr}' is not defined`);
|
|
550
|
+
}
|
|
551
|
+
// Dotted name lookup (module.attr)
|
|
552
|
+
if (/^[A-Za-z_][A-Za-z0-9_.]+$/.test(expr)) {
|
|
553
|
+
const parts = expr.split(".");
|
|
554
|
+
let val = scope.get(parts[0]) ??
|
|
555
|
+
(() => {
|
|
556
|
+
throw new PyError("NameError", `name '${parts[0]}' is not defined`);
|
|
557
|
+
})();
|
|
558
|
+
for (const part of parts.slice(1)) {
|
|
559
|
+
val = this.getAttr(val, part, scope);
|
|
560
|
+
}
|
|
561
|
+
return val;
|
|
562
|
+
}
|
|
563
|
+
return NONE;
|
|
564
|
+
}
|
|
565
|
+
findMatchingBracket(s, open) {
|
|
566
|
+
const close = open === "[" ? "]" : open === "(" ? ")" : "}";
|
|
567
|
+
let depth = 0;
|
|
568
|
+
for (let i = s.length - 1; i >= 0; i--) {
|
|
569
|
+
if (s[i] === close)
|
|
570
|
+
depth++;
|
|
571
|
+
if (s[i] === open) {
|
|
572
|
+
depth--;
|
|
573
|
+
if (depth === 0)
|
|
574
|
+
return i;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return -1;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Find the rightmost dot-attribute access at depth 0, respecting strings/parens.
|
|
581
|
+
* Returns {objExpr, attr, callPart} or null if not a dot-access expression.
|
|
582
|
+
*/
|
|
583
|
+
findDotAccess(expr) {
|
|
584
|
+
// Scan right to left for a dot at depth 0 (not inside strings/brackets)
|
|
585
|
+
let depth = 0, inStr = false, strChar = "";
|
|
586
|
+
for (let i = expr.length - 1; i > 0; i--) {
|
|
587
|
+
const ch = expr[i];
|
|
588
|
+
if (inStr) {
|
|
589
|
+
if (ch === strChar && expr[i - 1] !== "\\")
|
|
590
|
+
inStr = false;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (ch === '"' || ch === "'") {
|
|
594
|
+
inStr = true;
|
|
595
|
+
strChar = ch;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (")]}".includes(ch)) {
|
|
599
|
+
depth++;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if ("([{".includes(ch)) {
|
|
603
|
+
depth--;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (depth !== 0)
|
|
607
|
+
continue;
|
|
608
|
+
if (ch !== ".")
|
|
609
|
+
continue;
|
|
610
|
+
// Found a dot at depth 0
|
|
611
|
+
const objExpr = expr.slice(0, i).trim();
|
|
612
|
+
const rest = expr.slice(i + 1); // "attr" or "attr(args)"
|
|
613
|
+
const attrMatch = rest.match(/^(\w+)(\([\s\S]*\))?$/);
|
|
614
|
+
if (!attrMatch)
|
|
615
|
+
continue;
|
|
616
|
+
// Skip float literals like 1.5
|
|
617
|
+
if (/^-?\d+$/.test(objExpr))
|
|
618
|
+
continue;
|
|
619
|
+
return { objExpr, attr: attrMatch[1], callPart: attrMatch[2] };
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
tryBinaryOp(expr, ops, scope) {
|
|
624
|
+
let depth = 0, inStr = false, strChar = "";
|
|
625
|
+
for (let i = expr.length - 1; i >= 0; i--) {
|
|
626
|
+
const ch = expr[i];
|
|
627
|
+
if (inStr) {
|
|
628
|
+
if (ch === strChar && expr[i - 1] !== "\\")
|
|
629
|
+
inStr = false;
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (ch === '"' || ch === "'") {
|
|
633
|
+
inStr = true;
|
|
634
|
+
strChar = ch;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (")]}".includes(ch)) {
|
|
638
|
+
depth++;
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if ("([{".includes(ch)) {
|
|
642
|
+
depth--;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (depth !== 0)
|
|
646
|
+
continue;
|
|
647
|
+
for (const op of ops) {
|
|
648
|
+
if (expr.slice(i, i + op.length) === op) {
|
|
649
|
+
// Skip "*" if it's actually part of "**"
|
|
650
|
+
if (op === "*" && (expr[i + 1] === "*" || expr[i - 1] === "*"))
|
|
651
|
+
continue;
|
|
652
|
+
// Ensure it's a standalone operator (not part of identifier)
|
|
653
|
+
const before = expr[i - 1];
|
|
654
|
+
const after = expr[i + op.length];
|
|
655
|
+
const wordOp = /^[a-z]/.test(op);
|
|
656
|
+
if (wordOp) {
|
|
657
|
+
if (before && /\w/.test(before))
|
|
658
|
+
continue;
|
|
659
|
+
if (after && /\w/.test(after))
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const left = expr.slice(0, i).trim();
|
|
663
|
+
const right = expr.slice(i + op.length).trim();
|
|
664
|
+
if (!left || !right)
|
|
665
|
+
continue;
|
|
666
|
+
return this.applyBinaryOp(op, left, right, scope);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return undefined;
|
|
671
|
+
}
|
|
672
|
+
applyBinaryOp(op, leftExpr, rightExpr, scope) {
|
|
673
|
+
if (op === "and") {
|
|
674
|
+
const l = this.pyEval(leftExpr, scope);
|
|
675
|
+
return pyBool(l) ? this.pyEval(rightExpr, scope) : l;
|
|
676
|
+
}
|
|
677
|
+
if (op === "or") {
|
|
678
|
+
const l = this.pyEval(leftExpr, scope);
|
|
679
|
+
return pyBool(l) ? l : this.pyEval(rightExpr, scope);
|
|
680
|
+
}
|
|
681
|
+
const left = this.pyEval(leftExpr, scope);
|
|
682
|
+
const right = this.pyEval(rightExpr, scope);
|
|
683
|
+
switch (op) {
|
|
684
|
+
case "+":
|
|
685
|
+
if (typeof left === "string" && typeof right === "string")
|
|
686
|
+
return left + right;
|
|
687
|
+
if (Array.isArray(left) && Array.isArray(right))
|
|
688
|
+
return [...left, ...right];
|
|
689
|
+
return left + right;
|
|
690
|
+
case "-":
|
|
691
|
+
return left - right;
|
|
692
|
+
case "*":
|
|
693
|
+
if (typeof left === "string" && typeof right === "number")
|
|
694
|
+
return left.repeat(right);
|
|
695
|
+
if (Array.isArray(left) && typeof right === "number") {
|
|
696
|
+
const arr = [];
|
|
697
|
+
for (let i = 0; i < right; i++)
|
|
698
|
+
arr.push(...left);
|
|
699
|
+
return arr;
|
|
700
|
+
}
|
|
701
|
+
return left * right;
|
|
702
|
+
case "/": {
|
|
703
|
+
if (right === 0)
|
|
704
|
+
throw new PyError("ZeroDivisionError", "division by zero");
|
|
705
|
+
return left / right;
|
|
706
|
+
}
|
|
707
|
+
case "//": {
|
|
708
|
+
if (right === 0)
|
|
709
|
+
throw new PyError("ZeroDivisionError", "integer division or modulo by zero");
|
|
710
|
+
return Math.floor(left / right);
|
|
711
|
+
}
|
|
712
|
+
case "%": {
|
|
713
|
+
if (typeof left === "string")
|
|
714
|
+
return this.pyStringFormat(left, Array.isArray(right) ? right : [right]);
|
|
715
|
+
if (right === 0)
|
|
716
|
+
throw new PyError("ZeroDivisionError", "integer division or modulo by zero");
|
|
717
|
+
return left % right;
|
|
718
|
+
}
|
|
719
|
+
case "**":
|
|
720
|
+
return left ** right;
|
|
721
|
+
case "==":
|
|
722
|
+
return pyRepr(left) === pyRepr(right) || left === right;
|
|
723
|
+
case "!=":
|
|
724
|
+
return pyRepr(left) !== pyRepr(right) && left !== right;
|
|
725
|
+
case "<":
|
|
726
|
+
return left < right;
|
|
727
|
+
case "<=":
|
|
728
|
+
return left <= right;
|
|
729
|
+
case ">":
|
|
730
|
+
return left > right;
|
|
731
|
+
case ">=":
|
|
732
|
+
return left >= right;
|
|
733
|
+
case "in":
|
|
734
|
+
return this.pyIn(right, left);
|
|
735
|
+
case "not in":
|
|
736
|
+
return !this.pyIn(right, left);
|
|
737
|
+
case "is":
|
|
738
|
+
return (left === right ||
|
|
739
|
+
(isPyNone(left) && isPyNone(right)));
|
|
740
|
+
case "is not":
|
|
741
|
+
return !(left === right ||
|
|
742
|
+
(isPyNone(left) && isPyNone(right)));
|
|
743
|
+
}
|
|
744
|
+
return NONE;
|
|
745
|
+
}
|
|
746
|
+
pyIn(container, item) {
|
|
747
|
+
if (typeof container === "string")
|
|
748
|
+
return typeof item === "string" && container.includes(item);
|
|
749
|
+
if (Array.isArray(container))
|
|
750
|
+
return container.some((v) => pyRepr(v) === pyRepr(item));
|
|
751
|
+
if (isPyDict(container))
|
|
752
|
+
return container.data.has(pyStr(item));
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
subscript(obj, key, scope) {
|
|
756
|
+
// Slice
|
|
757
|
+
if (key.includes(":")) {
|
|
758
|
+
const parts = key.split(":").map((p) => p.trim());
|
|
759
|
+
const start = parts[0]
|
|
760
|
+
? this.pyEval(parts[0], scope)
|
|
761
|
+
: undefined;
|
|
762
|
+
const stop = parts[1]
|
|
763
|
+
? this.pyEval(parts[1], scope)
|
|
764
|
+
: undefined;
|
|
765
|
+
if (typeof obj === "string")
|
|
766
|
+
return obj.slice(start, stop);
|
|
767
|
+
if (Array.isArray(obj))
|
|
768
|
+
return obj.slice(start, stop);
|
|
769
|
+
return NONE;
|
|
770
|
+
}
|
|
771
|
+
const k = this.pyEval(key, scope);
|
|
772
|
+
if (Array.isArray(obj)) {
|
|
773
|
+
let idx = k;
|
|
774
|
+
if (idx < 0)
|
|
775
|
+
idx = obj.length + idx;
|
|
776
|
+
return obj[idx] ?? NONE;
|
|
777
|
+
}
|
|
778
|
+
if (typeof obj === "string") {
|
|
779
|
+
let idx = k;
|
|
780
|
+
if (idx < 0)
|
|
781
|
+
idx = obj.length + idx;
|
|
782
|
+
return obj[idx] ?? NONE;
|
|
783
|
+
}
|
|
784
|
+
if (isPyDict(obj))
|
|
785
|
+
return obj.data.get(pyStr(k)) ?? NONE;
|
|
786
|
+
throw new PyError("TypeError", `'${pyTypeName(obj)}' is not subscriptable`);
|
|
787
|
+
}
|
|
788
|
+
// ── attribute access ─────────────────────────────────────────────────────
|
|
789
|
+
getAttr(obj, attr, _scope) {
|
|
790
|
+
if (isPyDict(obj)) {
|
|
791
|
+
if (obj.data.has(attr))
|
|
792
|
+
return obj.data.get(attr);
|
|
793
|
+
// Special dict attributes
|
|
794
|
+
if (attr === "path" && obj.path)
|
|
795
|
+
return obj.path;
|
|
796
|
+
return NONE;
|
|
797
|
+
}
|
|
798
|
+
if (isPyInstance(obj))
|
|
799
|
+
return obj.attrs.get(attr) ?? NONE;
|
|
800
|
+
if (typeof obj === "string") {
|
|
801
|
+
// String attributes
|
|
802
|
+
const strMethods = {
|
|
803
|
+
__class__: { __pytype__: "class", name: "str" },
|
|
804
|
+
};
|
|
805
|
+
return strMethods[attr] ?? NONE;
|
|
806
|
+
}
|
|
807
|
+
return NONE;
|
|
808
|
+
}
|
|
809
|
+
// ── method calls ──────────────────────────────────────────────────────────
|
|
810
|
+
callMethod(obj, method, args, _scope) {
|
|
811
|
+
// String methods
|
|
812
|
+
if (typeof obj === "string") {
|
|
813
|
+
switch (method) {
|
|
814
|
+
case "upper":
|
|
815
|
+
return obj.toUpperCase();
|
|
816
|
+
case "lower":
|
|
817
|
+
return obj.toLowerCase();
|
|
818
|
+
case "strip":
|
|
819
|
+
return (args[0] ? obj.replace(new RegExp(`[${args[0]}]+`, "g"), "") : obj).trim();
|
|
820
|
+
case "lstrip":
|
|
821
|
+
return obj.trimStart();
|
|
822
|
+
case "rstrip":
|
|
823
|
+
return obj.trimEnd();
|
|
824
|
+
case "split":
|
|
825
|
+
return obj
|
|
826
|
+
.split(typeof args[0] === "string" ? args[0] : /\s+/)
|
|
827
|
+
.filter((s, i) => i > 0 || s !== "");
|
|
828
|
+
case "splitlines":
|
|
829
|
+
return obj.split("\n");
|
|
830
|
+
case "join":
|
|
831
|
+
return pyIter(args[0] ?? [])
|
|
832
|
+
.map(pyStr)
|
|
833
|
+
.join(obj);
|
|
834
|
+
case "replace":
|
|
835
|
+
return obj.replaceAll(pyStr(args[0] ?? ""), pyStr(args[1] ?? ""));
|
|
836
|
+
case "startswith":
|
|
837
|
+
return obj.startsWith(pyStr(args[0] ?? ""));
|
|
838
|
+
case "endswith":
|
|
839
|
+
return obj.endsWith(pyStr(args[0] ?? ""));
|
|
840
|
+
case "find":
|
|
841
|
+
return obj.indexOf(pyStr(args[0] ?? ""));
|
|
842
|
+
case "index": {
|
|
843
|
+
const i = obj.indexOf(pyStr(args[0] ?? ""));
|
|
844
|
+
if (i === -1)
|
|
845
|
+
throw new PyError("ValueError", "substring not found");
|
|
846
|
+
return i;
|
|
847
|
+
}
|
|
848
|
+
case "count":
|
|
849
|
+
return obj.split(pyStr(args[0] ?? "")).length - 1;
|
|
850
|
+
case "format":
|
|
851
|
+
return this.pyStringFormat(obj, args);
|
|
852
|
+
case "encode":
|
|
853
|
+
return obj; // bytes stub
|
|
854
|
+
case "decode":
|
|
855
|
+
return obj;
|
|
856
|
+
case "isdigit":
|
|
857
|
+
return /^\d+$/.test(obj);
|
|
858
|
+
case "isalpha":
|
|
859
|
+
return /^[a-zA-Z]+$/.test(obj);
|
|
860
|
+
case "isalnum":
|
|
861
|
+
return /^[a-zA-Z0-9]+$/.test(obj);
|
|
862
|
+
case "isspace":
|
|
863
|
+
return /^\s+$/.test(obj);
|
|
864
|
+
case "isupper":
|
|
865
|
+
return obj === obj.toUpperCase() && obj !== obj.toLowerCase();
|
|
866
|
+
case "islower":
|
|
867
|
+
return obj === obj.toLowerCase() && obj !== obj.toUpperCase();
|
|
868
|
+
case "center": {
|
|
869
|
+
const w = args[0] ?? 0;
|
|
870
|
+
const f = pyStr(args[1] ?? " ");
|
|
871
|
+
return obj.padStart(Math.floor((w + obj.length) / 2), f).padEnd(w, f);
|
|
872
|
+
}
|
|
873
|
+
case "ljust":
|
|
874
|
+
return obj.padEnd(args[0] ?? 0, pyStr(args[1] ?? " "));
|
|
875
|
+
case "rjust":
|
|
876
|
+
return obj.padStart(args[0] ?? 0, pyStr(args[1] ?? " "));
|
|
877
|
+
case "zfill":
|
|
878
|
+
return obj.padStart(args[0] ?? 0, "0");
|
|
879
|
+
case "title":
|
|
880
|
+
return obj.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
881
|
+
case "capitalize":
|
|
882
|
+
return obj[0]?.toUpperCase() + obj.slice(1).toLowerCase();
|
|
883
|
+
case "swapcase":
|
|
884
|
+
return [...obj]
|
|
885
|
+
.map((c) => c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase())
|
|
886
|
+
.join("");
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// List methods
|
|
890
|
+
if (Array.isArray(obj)) {
|
|
891
|
+
switch (method) {
|
|
892
|
+
case "append":
|
|
893
|
+
obj.push(args[0] ?? NONE);
|
|
894
|
+
return NONE;
|
|
895
|
+
case "extend":
|
|
896
|
+
for (const v of pyIter(args[0] ?? []))
|
|
897
|
+
obj.push(v);
|
|
898
|
+
return NONE;
|
|
899
|
+
case "insert":
|
|
900
|
+
obj.splice(args[0] ?? 0, 0, args[1] ?? NONE);
|
|
901
|
+
return NONE;
|
|
902
|
+
case "pop": {
|
|
903
|
+
const idx = args[0] !== undefined ? args[0] : -1;
|
|
904
|
+
const i = idx < 0 ? obj.length + idx : idx;
|
|
905
|
+
return obj.splice(i, 1)[0] ?? NONE;
|
|
906
|
+
}
|
|
907
|
+
case "remove": {
|
|
908
|
+
const i = obj.findIndex((v) => pyRepr(v) === pyRepr(args[0] ?? NONE));
|
|
909
|
+
if (i !== -1)
|
|
910
|
+
obj.splice(i, 1);
|
|
911
|
+
return NONE;
|
|
912
|
+
}
|
|
913
|
+
case "index": {
|
|
914
|
+
const i = obj.findIndex((v) => pyRepr(v) === pyRepr(args[0] ?? NONE));
|
|
915
|
+
if (i === -1)
|
|
916
|
+
throw new PyError("ValueError", "is not in list");
|
|
917
|
+
return i;
|
|
918
|
+
}
|
|
919
|
+
case "count":
|
|
920
|
+
return obj.filter((v) => pyRepr(v) === pyRepr(args[0] ?? NONE))
|
|
921
|
+
.length;
|
|
922
|
+
case "sort":
|
|
923
|
+
obj.sort((a, b) => typeof a === "number" && typeof b === "number"
|
|
924
|
+
? a - b
|
|
925
|
+
: pyStr(a).localeCompare(pyStr(b)));
|
|
926
|
+
return NONE;
|
|
927
|
+
case "reverse":
|
|
928
|
+
obj.reverse();
|
|
929
|
+
return NONE;
|
|
930
|
+
case "copy":
|
|
931
|
+
return [...obj];
|
|
932
|
+
case "clear":
|
|
933
|
+
obj.splice(0);
|
|
934
|
+
return NONE;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
// Dict methods
|
|
938
|
+
if (isPyDict(obj)) {
|
|
939
|
+
switch (method) {
|
|
940
|
+
case "keys":
|
|
941
|
+
return [...obj.data.keys()];
|
|
942
|
+
case "values":
|
|
943
|
+
return [...obj.data.values()];
|
|
944
|
+
case "items":
|
|
945
|
+
return [...obj.data.entries()].map(([k, v]) => [k, v]);
|
|
946
|
+
case "get":
|
|
947
|
+
return obj.data.get(pyStr(args[0] ?? "")) ?? args[1] ?? NONE;
|
|
948
|
+
case "update": {
|
|
949
|
+
if (isPyDict(args[0] ?? NONE))
|
|
950
|
+
for (const [k, v] of args[0].data)
|
|
951
|
+
obj.data.set(k, v);
|
|
952
|
+
return NONE;
|
|
953
|
+
}
|
|
954
|
+
case "pop": {
|
|
955
|
+
const k = pyStr(args[0] ?? "");
|
|
956
|
+
const v = obj.data.get(k) ?? args[1] ?? NONE;
|
|
957
|
+
obj.data.delete(k);
|
|
958
|
+
return v;
|
|
959
|
+
}
|
|
960
|
+
case "clear":
|
|
961
|
+
obj.data.clear();
|
|
962
|
+
return NONE;
|
|
963
|
+
case "copy":
|
|
964
|
+
return pyDict([...obj.data.entries()]);
|
|
965
|
+
case "setdefault": {
|
|
966
|
+
const k = pyStr(args[0] ?? "");
|
|
967
|
+
if (!obj.data.has(k))
|
|
968
|
+
obj.data.set(k, args[1] ?? NONE);
|
|
969
|
+
return obj.data.get(k) ?? NONE;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// os module methods
|
|
974
|
+
if (isPyDict(obj) &&
|
|
975
|
+
obj.data.has("name") &&
|
|
976
|
+
obj.data.get("name") === "posix") {
|
|
977
|
+
switch (method) {
|
|
978
|
+
case "getcwd":
|
|
979
|
+
return this.cwd;
|
|
980
|
+
case "getenv":
|
|
981
|
+
return typeof args[0] === "string"
|
|
982
|
+
? (process.env[args[0]] ?? args[1] ?? NONE)
|
|
983
|
+
: NONE;
|
|
984
|
+
case "listdir":
|
|
985
|
+
return [];
|
|
986
|
+
case "path":
|
|
987
|
+
return obj; // return self
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
// os.path methods
|
|
991
|
+
if (isPyDict(obj)) {
|
|
992
|
+
switch (method) {
|
|
993
|
+
case "join":
|
|
994
|
+
return args.map(pyStr).join("/").replace(/\/+/g, "/");
|
|
995
|
+
case "exists":
|
|
996
|
+
return false; // no real fs access
|
|
997
|
+
case "dirname": {
|
|
998
|
+
const p = pyStr(args[0] ?? "");
|
|
999
|
+
return p.split("/").slice(0, -1).join("/") || "/";
|
|
1000
|
+
}
|
|
1001
|
+
case "basename": {
|
|
1002
|
+
const p = pyStr(args[0] ?? "");
|
|
1003
|
+
return p.split("/").pop() ?? "";
|
|
1004
|
+
}
|
|
1005
|
+
case "abspath":
|
|
1006
|
+
return pyStr(args[0] ?? "");
|
|
1007
|
+
case "splitext": {
|
|
1008
|
+
const p = pyStr(args[0] ?? "");
|
|
1009
|
+
const d = p.lastIndexOf(".");
|
|
1010
|
+
return d > 0 ? [p.slice(0, d), p.slice(d)] : [p, ""];
|
|
1011
|
+
}
|
|
1012
|
+
case "isfile":
|
|
1013
|
+
return false;
|
|
1014
|
+
case "isdir":
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// sys module
|
|
1019
|
+
if (isPyDict(obj) &&
|
|
1020
|
+
obj.data.has("version") &&
|
|
1021
|
+
obj.data.get("version") === VERSION_INFO) {
|
|
1022
|
+
switch (method) {
|
|
1023
|
+
case "exit":
|
|
1024
|
+
throw new ExitSignal(args[0] ?? 0);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// math module
|
|
1028
|
+
if (isPyDict(obj)) {
|
|
1029
|
+
const mathFns = {
|
|
1030
|
+
sqrt: Math.sqrt,
|
|
1031
|
+
floor: Math.floor,
|
|
1032
|
+
ceil: Math.ceil,
|
|
1033
|
+
fabs: Math.abs,
|
|
1034
|
+
log: Math.log,
|
|
1035
|
+
log2: Math.log2,
|
|
1036
|
+
log10: Math.log10,
|
|
1037
|
+
sin: Math.sin,
|
|
1038
|
+
cos: Math.cos,
|
|
1039
|
+
tan: Math.tan,
|
|
1040
|
+
asin: Math.asin,
|
|
1041
|
+
acos: Math.acos,
|
|
1042
|
+
atan: Math.atan,
|
|
1043
|
+
atan2: Math.atan2,
|
|
1044
|
+
pow: Math.pow,
|
|
1045
|
+
exp: Math.exp,
|
|
1046
|
+
hypot: Math.hypot,
|
|
1047
|
+
};
|
|
1048
|
+
if (method in mathFns) {
|
|
1049
|
+
const fn = mathFns[method];
|
|
1050
|
+
return fn(...args.map((a) => a));
|
|
1051
|
+
}
|
|
1052
|
+
if (method === "factorial") {
|
|
1053
|
+
let n = args[0] ?? 0;
|
|
1054
|
+
let r = 1;
|
|
1055
|
+
while (n > 1) {
|
|
1056
|
+
r *= n--;
|
|
1057
|
+
}
|
|
1058
|
+
return r;
|
|
1059
|
+
}
|
|
1060
|
+
if (method === "gcd") {
|
|
1061
|
+
let a = Math.abs(args[0] ?? 0);
|
|
1062
|
+
let b = Math.abs(args[1] ?? 0);
|
|
1063
|
+
while (b) {
|
|
1064
|
+
[a, b] = [b, a % b];
|
|
1065
|
+
}
|
|
1066
|
+
return a;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
// json module
|
|
1070
|
+
if (isPyDict(obj)) {
|
|
1071
|
+
if (method === "dumps") {
|
|
1072
|
+
const opts = isPyDict(args[1] ?? NONE)
|
|
1073
|
+
? args[1]
|
|
1074
|
+
: undefined;
|
|
1075
|
+
const indent = opts ? opts.data.get("indent") : undefined;
|
|
1076
|
+
return JSON.stringify(this.pyToJs(args[0] ?? NONE), null, indent);
|
|
1077
|
+
}
|
|
1078
|
+
if (method === "loads") {
|
|
1079
|
+
return this.jsToPy(JSON.parse(pyStr(args[0] ?? "")));
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
// Instance method calls
|
|
1083
|
+
if (isPyInstance(obj)) {
|
|
1084
|
+
const fn = obj.attrs.get(method) ?? obj.cls.methods.get(method) ?? NONE;
|
|
1085
|
+
if (isPyFunc(fn)) {
|
|
1086
|
+
const callScope = new Map(fn.closure);
|
|
1087
|
+
callScope.set("self", obj);
|
|
1088
|
+
fn.params.slice(1).forEach((p, i) => callScope.set(p, args[i] ?? NONE));
|
|
1089
|
+
return this.execBlock(fn.body, callScope);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
throw new PyError("AttributeError", `'${pyTypeName(obj)}' object has no attribute '${method}'`);
|
|
1093
|
+
}
|
|
1094
|
+
pyStringFormat(fmt, args) {
|
|
1095
|
+
let i = 0;
|
|
1096
|
+
return fmt.replace(/%([diouxXeEfFgGcrs%])/g, (_, spec) => {
|
|
1097
|
+
if (spec === "%")
|
|
1098
|
+
return "%";
|
|
1099
|
+
const val = args[i++];
|
|
1100
|
+
switch (spec) {
|
|
1101
|
+
case "d":
|
|
1102
|
+
case "i":
|
|
1103
|
+
return String(Math.trunc(val));
|
|
1104
|
+
case "f":
|
|
1105
|
+
return val.toFixed(6);
|
|
1106
|
+
case "s":
|
|
1107
|
+
return pyStr(val ?? NONE);
|
|
1108
|
+
case "r":
|
|
1109
|
+
return pyRepr(val ?? NONE);
|
|
1110
|
+
default:
|
|
1111
|
+
return String(val);
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
pyToJs(v) {
|
|
1116
|
+
if (isPyNone(v))
|
|
1117
|
+
return null;
|
|
1118
|
+
if (isPyDict(v))
|
|
1119
|
+
return Object.fromEntries([...v.data.entries()].map(([k, val]) => [k, this.pyToJs(val)]));
|
|
1120
|
+
if (Array.isArray(v))
|
|
1121
|
+
return v.map((i) => this.pyToJs(i));
|
|
1122
|
+
return v;
|
|
1123
|
+
}
|
|
1124
|
+
jsToPy(v) {
|
|
1125
|
+
if (v === null || v === undefined)
|
|
1126
|
+
return NONE;
|
|
1127
|
+
if (typeof v === "boolean")
|
|
1128
|
+
return v;
|
|
1129
|
+
if (typeof v === "number")
|
|
1130
|
+
return v;
|
|
1131
|
+
if (typeof v === "string")
|
|
1132
|
+
return v;
|
|
1133
|
+
if (Array.isArray(v))
|
|
1134
|
+
return v.map((i) => this.jsToPy(i));
|
|
1135
|
+
if (typeof v === "object")
|
|
1136
|
+
return pyDict(Object.entries(v).map(([k, val]) => [
|
|
1137
|
+
k,
|
|
1138
|
+
this.jsToPy(val),
|
|
1139
|
+
]));
|
|
1140
|
+
return NONE;
|
|
1141
|
+
}
|
|
1142
|
+
// ── built-in functions ────────────────────────────────────────────────────
|
|
1143
|
+
callBuiltin(name, args, scope) {
|
|
1144
|
+
// User-defined functions
|
|
1145
|
+
if (scope.has(name)) {
|
|
1146
|
+
const fn = scope.get(name) ?? NONE;
|
|
1147
|
+
if (isPyFunc(fn))
|
|
1148
|
+
return this.callFunc(fn, args, scope);
|
|
1149
|
+
if (isPyClass(fn))
|
|
1150
|
+
return this.instantiate(fn, args, scope);
|
|
1151
|
+
return fn;
|
|
1152
|
+
}
|
|
1153
|
+
switch (name) {
|
|
1154
|
+
// Output
|
|
1155
|
+
case "print": {
|
|
1156
|
+
const sep = " ", end = "\n";
|
|
1157
|
+
this.output.push(args.map(pyStr).join(sep) + end.replace(/\\n/g, ""));
|
|
1158
|
+
return NONE;
|
|
1159
|
+
}
|
|
1160
|
+
case "input": {
|
|
1161
|
+
this.output.push(pyStr(args[0] ?? ""));
|
|
1162
|
+
return "";
|
|
1163
|
+
}
|
|
1164
|
+
// Type constructors
|
|
1165
|
+
case "int": {
|
|
1166
|
+
if (args.length === 0)
|
|
1167
|
+
return 0;
|
|
1168
|
+
const base = args[1] ?? 10;
|
|
1169
|
+
const n = parseInt(pyStr(args[0] ?? 0), base);
|
|
1170
|
+
return Number.isNaN(n)
|
|
1171
|
+
? (() => {
|
|
1172
|
+
throw new PyError("ValueError", `invalid literal for int()`);
|
|
1173
|
+
})()
|
|
1174
|
+
: n;
|
|
1175
|
+
}
|
|
1176
|
+
case "float": {
|
|
1177
|
+
if (args.length === 0)
|
|
1178
|
+
return 0.0;
|
|
1179
|
+
const f = parseFloat(pyStr(args[0] ?? 0));
|
|
1180
|
+
return Number.isNaN(f)
|
|
1181
|
+
? (() => {
|
|
1182
|
+
throw new PyError("ValueError", `could not convert to float`);
|
|
1183
|
+
})()
|
|
1184
|
+
: f;
|
|
1185
|
+
}
|
|
1186
|
+
case "str":
|
|
1187
|
+
return args.length === 0 ? "" : pyStr(args[0] ?? NONE);
|
|
1188
|
+
case "bool":
|
|
1189
|
+
return args.length === 0 ? false : pyBool(args[0] ?? NONE);
|
|
1190
|
+
case "list":
|
|
1191
|
+
return args.length === 0 ? [] : pyIter(args[0] ?? []);
|
|
1192
|
+
case "tuple":
|
|
1193
|
+
return args.length === 0 ? [] : pyIter(args[0] ?? []);
|
|
1194
|
+
case "set":
|
|
1195
|
+
return args.length === 0
|
|
1196
|
+
? []
|
|
1197
|
+
: [...new Set(pyIter(args[0] ?? []).map(pyRepr))].map((s) => {
|
|
1198
|
+
const v = pyIter(args[0] ?? []).find((item) => pyRepr(item) === s);
|
|
1199
|
+
return v ?? NONE;
|
|
1200
|
+
});
|
|
1201
|
+
case "dict":
|
|
1202
|
+
return args.length === 0
|
|
1203
|
+
? pyDict()
|
|
1204
|
+
: isPyDict(args[0] ?? NONE)
|
|
1205
|
+
? args[0]
|
|
1206
|
+
: pyDict();
|
|
1207
|
+
case "bytes":
|
|
1208
|
+
return typeof args[0] === "string" ? args[0] : pyStr(args[0] ?? "");
|
|
1209
|
+
case "bytearray":
|
|
1210
|
+
return args.length === 0 ? "" : pyStr(args[0] ?? "");
|
|
1211
|
+
// Type inspection
|
|
1212
|
+
case "type": {
|
|
1213
|
+
if (args.length === 1)
|
|
1214
|
+
return `<class '${pyTypeName(args[0] ?? NONE)}'>`;
|
|
1215
|
+
return NONE;
|
|
1216
|
+
}
|
|
1217
|
+
case "isinstance":
|
|
1218
|
+
return pyTypeName(args[0] ?? NONE) === pyStr(args[1] ?? "");
|
|
1219
|
+
case "issubclass":
|
|
1220
|
+
return false;
|
|
1221
|
+
case "callable":
|
|
1222
|
+
return isPyFunc(args[0] ?? NONE);
|
|
1223
|
+
case "hasattr":
|
|
1224
|
+
return isPyDict(args[0] ?? NONE)
|
|
1225
|
+
? args[0].data.has(pyStr(args[1] ?? ""))
|
|
1226
|
+
: false;
|
|
1227
|
+
case "getattr": {
|
|
1228
|
+
if (!isPyDict(args[0] ?? NONE))
|
|
1229
|
+
return args[2] ?? NONE;
|
|
1230
|
+
return (args[0].data.get(pyStr(args[1] ?? "")) ?? args[2] ?? NONE);
|
|
1231
|
+
}
|
|
1232
|
+
case "setattr": {
|
|
1233
|
+
if (isPyDict(args[0] ?? NONE))
|
|
1234
|
+
args[0].data.set(pyStr(args[1] ?? ""), args[2] ?? NONE);
|
|
1235
|
+
return NONE;
|
|
1236
|
+
}
|
|
1237
|
+
// Functional
|
|
1238
|
+
case "len": {
|
|
1239
|
+
const v = args[0] ?? NONE;
|
|
1240
|
+
if (typeof v === "string")
|
|
1241
|
+
return v.length;
|
|
1242
|
+
if (Array.isArray(v))
|
|
1243
|
+
return v.length;
|
|
1244
|
+
if (isPyDict(v))
|
|
1245
|
+
return v.data.size;
|
|
1246
|
+
if (isPyRange(v))
|
|
1247
|
+
return pyRangeLength(v);
|
|
1248
|
+
throw new PyError("TypeError", `object of type '${pyTypeName(v)}' has no len()`);
|
|
1249
|
+
}
|
|
1250
|
+
case "range": {
|
|
1251
|
+
if (args.length === 1)
|
|
1252
|
+
return pyRange(0, args[0]);
|
|
1253
|
+
if (args.length === 2)
|
|
1254
|
+
return pyRange(args[0], args[1]);
|
|
1255
|
+
return pyRange(args[0], args[1], args[2]);
|
|
1256
|
+
}
|
|
1257
|
+
case "enumerate": {
|
|
1258
|
+
const start = args[1] ?? 0;
|
|
1259
|
+
return pyIter(args[0] ?? []).map((v, i) => [i + start, v]);
|
|
1260
|
+
}
|
|
1261
|
+
case "zip": {
|
|
1262
|
+
const iters = args.map(pyIter);
|
|
1263
|
+
const len = Math.min(...iters.map((it) => it.length));
|
|
1264
|
+
return Array.from({ length: len }, (_, i) => iters.map((it) => it[i] ?? NONE));
|
|
1265
|
+
}
|
|
1266
|
+
case "map": {
|
|
1267
|
+
const fn = args[0] ?? NONE;
|
|
1268
|
+
return pyIter(args[1] ?? []).map((v) => isPyFunc(fn) ? this.callFunc(fn, [v], scope) : NONE);
|
|
1269
|
+
}
|
|
1270
|
+
case "filter": {
|
|
1271
|
+
const fn = args[0] ?? NONE;
|
|
1272
|
+
return pyIter(args[1] ?? []).filter((v) => isPyFunc(fn) ? pyBool(this.callFunc(fn, [v], scope)) : pyBool(v));
|
|
1273
|
+
}
|
|
1274
|
+
case "reduce": {
|
|
1275
|
+
const fn = args[0] ?? NONE;
|
|
1276
|
+
const items = pyIter(args[1] ?? []);
|
|
1277
|
+
if (items.length === 0)
|
|
1278
|
+
return args[2] ?? NONE;
|
|
1279
|
+
let acc = args[2] !== undefined ? args[2] : items[0];
|
|
1280
|
+
for (const item of args[2] !== undefined ? items : items.slice(1)) {
|
|
1281
|
+
acc = isPyFunc(fn)
|
|
1282
|
+
? this.callFunc(fn, [acc, item], scope)
|
|
1283
|
+
: NONE;
|
|
1284
|
+
}
|
|
1285
|
+
return acc;
|
|
1286
|
+
}
|
|
1287
|
+
case "sorted": {
|
|
1288
|
+
const items = [...pyIter(args[0] ?? [])];
|
|
1289
|
+
const sortArg1 = args[1] ?? NONE;
|
|
1290
|
+
const keyFn = isPyDict(sortArg1)
|
|
1291
|
+
? (sortArg1.data.get("key") ?? NONE)
|
|
1292
|
+
: sortArg1;
|
|
1293
|
+
items.sort((a, b) => {
|
|
1294
|
+
const ka = isPyFunc(keyFn)
|
|
1295
|
+
? this.callFunc(keyFn, [a], scope)
|
|
1296
|
+
: a;
|
|
1297
|
+
const kb = isPyFunc(keyFn)
|
|
1298
|
+
? this.callFunc(keyFn, [b], scope)
|
|
1299
|
+
: b;
|
|
1300
|
+
return typeof ka === "number" && typeof kb === "number"
|
|
1301
|
+
? ka - kb
|
|
1302
|
+
: pyStr(ka).localeCompare(pyStr(kb));
|
|
1303
|
+
});
|
|
1304
|
+
return items;
|
|
1305
|
+
}
|
|
1306
|
+
case "reversed":
|
|
1307
|
+
return [...pyIter(args[0] ?? [])].reverse();
|
|
1308
|
+
case "any":
|
|
1309
|
+
return pyIter(args[0] ?? []).some(pyBool);
|
|
1310
|
+
case "all":
|
|
1311
|
+
return pyIter(args[0] ?? []).every(pyBool);
|
|
1312
|
+
case "sum":
|
|
1313
|
+
return pyIter(args[0] ?? []).reduce((acc, v) => acc + v, (args[1] ?? 0));
|
|
1314
|
+
case "max": {
|
|
1315
|
+
const items = args.length === 1 ? pyIter(args[0] ?? []) : args;
|
|
1316
|
+
return items.reduce((a, b) => (a >= b ? a : b));
|
|
1317
|
+
}
|
|
1318
|
+
case "min": {
|
|
1319
|
+
const items = args.length === 1 ? pyIter(args[0] ?? []) : args;
|
|
1320
|
+
return items.reduce((a, b) => (a <= b ? a : b));
|
|
1321
|
+
}
|
|
1322
|
+
case "abs":
|
|
1323
|
+
return Math.abs(args[0] ?? 0);
|
|
1324
|
+
case "round":
|
|
1325
|
+
return args[1] !== undefined
|
|
1326
|
+
? parseFloat(args[0].toFixed(args[1]))
|
|
1327
|
+
: Math.round(args[0] ?? 0);
|
|
1328
|
+
case "divmod": {
|
|
1329
|
+
const a = args[0], b = args[1];
|
|
1330
|
+
return [Math.floor(a / b), a % b];
|
|
1331
|
+
}
|
|
1332
|
+
case "pow":
|
|
1333
|
+
return args[0] ** args[1];
|
|
1334
|
+
case "hex":
|
|
1335
|
+
return `0x${args[0].toString(16)}`;
|
|
1336
|
+
case "oct":
|
|
1337
|
+
return `0o${args[0].toString(8)}`;
|
|
1338
|
+
case "bin":
|
|
1339
|
+
return `0b${args[0].toString(2)}`;
|
|
1340
|
+
case "ord":
|
|
1341
|
+
return pyStr(args[0] ?? "").charCodeAt(0);
|
|
1342
|
+
case "chr":
|
|
1343
|
+
return String.fromCharCode(args[0] ?? 0);
|
|
1344
|
+
case "id":
|
|
1345
|
+
return Math.floor(Math.random() * 0xffffffff);
|
|
1346
|
+
case "hash":
|
|
1347
|
+
return typeof args[0] === "number"
|
|
1348
|
+
? args[0]
|
|
1349
|
+
: pyStr(args[0] ?? "")
|
|
1350
|
+
.split("")
|
|
1351
|
+
.reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0);
|
|
1352
|
+
// I/O
|
|
1353
|
+
case "open":
|
|
1354
|
+
throw new PyError("PermissionError", "open() not available in virtual runtime");
|
|
1355
|
+
case "repr":
|
|
1356
|
+
return pyRepr(args[0] ?? NONE);
|
|
1357
|
+
// Iteration helpers
|
|
1358
|
+
case "iter":
|
|
1359
|
+
return args[0] ?? NONE; // simplification
|
|
1360
|
+
case "next": {
|
|
1361
|
+
if (Array.isArray(args[0]) && args[0].length > 0)
|
|
1362
|
+
return args[0].shift();
|
|
1363
|
+
return (args[1] ??
|
|
1364
|
+
(() => {
|
|
1365
|
+
throw new PyError("StopIteration", "");
|
|
1366
|
+
})());
|
|
1367
|
+
}
|
|
1368
|
+
// vars/globals/locals
|
|
1369
|
+
case "vars":
|
|
1370
|
+
return pyDict([...scope.entries()].map(([k, v]) => [k, v]));
|
|
1371
|
+
case "globals":
|
|
1372
|
+
return pyDict([...scope.entries()].map(([k, v]) => [k, v]));
|
|
1373
|
+
case "locals":
|
|
1374
|
+
return pyDict([...scope.entries()].map(([k, v]) => [k, v]));
|
|
1375
|
+
case "dir": {
|
|
1376
|
+
if (args.length === 0)
|
|
1377
|
+
return [...scope.keys()];
|
|
1378
|
+
const obj = args[0] ?? NONE;
|
|
1379
|
+
if (typeof obj === "string")
|
|
1380
|
+
return [
|
|
1381
|
+
"upper",
|
|
1382
|
+
"lower",
|
|
1383
|
+
"strip",
|
|
1384
|
+
"split",
|
|
1385
|
+
"join",
|
|
1386
|
+
"replace",
|
|
1387
|
+
"find",
|
|
1388
|
+
"format",
|
|
1389
|
+
"encode",
|
|
1390
|
+
"startswith",
|
|
1391
|
+
"endswith",
|
|
1392
|
+
"count",
|
|
1393
|
+
"isdigit",
|
|
1394
|
+
"isalpha",
|
|
1395
|
+
"title",
|
|
1396
|
+
"capitalize",
|
|
1397
|
+
];
|
|
1398
|
+
if (Array.isArray(obj))
|
|
1399
|
+
return [
|
|
1400
|
+
"append",
|
|
1401
|
+
"extend",
|
|
1402
|
+
"insert",
|
|
1403
|
+
"pop",
|
|
1404
|
+
"remove",
|
|
1405
|
+
"index",
|
|
1406
|
+
"count",
|
|
1407
|
+
"sort",
|
|
1408
|
+
"reverse",
|
|
1409
|
+
"copy",
|
|
1410
|
+
"clear",
|
|
1411
|
+
];
|
|
1412
|
+
if (isPyDict(obj))
|
|
1413
|
+
return [
|
|
1414
|
+
"keys",
|
|
1415
|
+
"values",
|
|
1416
|
+
"items",
|
|
1417
|
+
"get",
|
|
1418
|
+
"update",
|
|
1419
|
+
"pop",
|
|
1420
|
+
"clear",
|
|
1421
|
+
"copy",
|
|
1422
|
+
"setdefault",
|
|
1423
|
+
];
|
|
1424
|
+
return [];
|
|
1425
|
+
}
|
|
1426
|
+
// Exception
|
|
1427
|
+
case "Exception":
|
|
1428
|
+
case "ValueError":
|
|
1429
|
+
case "TypeError":
|
|
1430
|
+
case "KeyError":
|
|
1431
|
+
case "IndexError":
|
|
1432
|
+
case "AttributeError":
|
|
1433
|
+
case "NameError":
|
|
1434
|
+
case "RuntimeError":
|
|
1435
|
+
case "StopIteration":
|
|
1436
|
+
case "NotImplementedError":
|
|
1437
|
+
case "OSError":
|
|
1438
|
+
case "IOError":
|
|
1439
|
+
throw new PyError(name, pyStr(args[0] ?? ""));
|
|
1440
|
+
// exec/eval
|
|
1441
|
+
case "exec": {
|
|
1442
|
+
this.execScript(pyStr(args[0] ?? ""), scope);
|
|
1443
|
+
return NONE;
|
|
1444
|
+
}
|
|
1445
|
+
case "eval":
|
|
1446
|
+
return this.pyEval(pyStr(args[0] ?? ""), scope);
|
|
1447
|
+
default:
|
|
1448
|
+
throw new PyError("NameError", `name '${name}' is not defined`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
callFunc(fn, args, _scope) {
|
|
1452
|
+
const callScope = new Map(fn.closure);
|
|
1453
|
+
fn.params.forEach((p, i) => {
|
|
1454
|
+
if (p.startsWith("*")) {
|
|
1455
|
+
callScope.set(p.slice(1), args.slice(i));
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
callScope.set(p, args[i] ?? NONE);
|
|
1459
|
+
});
|
|
1460
|
+
try {
|
|
1461
|
+
return this.execBlock(fn.body, callScope);
|
|
1462
|
+
}
|
|
1463
|
+
catch (e) {
|
|
1464
|
+
if (e instanceof ReturnSignal)
|
|
1465
|
+
return e.value;
|
|
1466
|
+
throw e;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
instantiate(cls, args, scope) {
|
|
1470
|
+
const inst = { __pytype__: "instance", cls, attrs: new Map() };
|
|
1471
|
+
const init = cls.methods.get("__init__");
|
|
1472
|
+
if (init)
|
|
1473
|
+
this.callMethod(inst, "__init__", args, scope);
|
|
1474
|
+
return inst;
|
|
1475
|
+
}
|
|
1476
|
+
// ── statement executor ────────────────────────────────────────────────────
|
|
1477
|
+
execScript(code, scope) {
|
|
1478
|
+
const lines = code.split("\n");
|
|
1479
|
+
this.execLines(lines, 0, scope);
|
|
1480
|
+
}
|
|
1481
|
+
execLines(lines, startIdx, scope) {
|
|
1482
|
+
let i = startIdx;
|
|
1483
|
+
while (i < lines.length) {
|
|
1484
|
+
const raw = lines[i];
|
|
1485
|
+
if (!raw.trim() || raw.trim().startsWith("#")) {
|
|
1486
|
+
i++;
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
i = this.execStatement(lines, i, scope);
|
|
1490
|
+
}
|
|
1491
|
+
return i;
|
|
1492
|
+
}
|
|
1493
|
+
execBlock(bodyLines, scope) {
|
|
1494
|
+
try {
|
|
1495
|
+
this.execLines(bodyLines, 0, scope);
|
|
1496
|
+
}
|
|
1497
|
+
catch (e) {
|
|
1498
|
+
if (e instanceof ReturnSignal)
|
|
1499
|
+
return e.value;
|
|
1500
|
+
throw e;
|
|
1501
|
+
}
|
|
1502
|
+
return NONE;
|
|
1503
|
+
}
|
|
1504
|
+
getIndent(line) {
|
|
1505
|
+
let n = 0;
|
|
1506
|
+
for (const ch of line) {
|
|
1507
|
+
if (ch === " ")
|
|
1508
|
+
n++;
|
|
1509
|
+
else if (ch === "\t")
|
|
1510
|
+
n += 4;
|
|
1511
|
+
else
|
|
1512
|
+
break;
|
|
1513
|
+
}
|
|
1514
|
+
return n;
|
|
1515
|
+
}
|
|
1516
|
+
collectBlock(lines, startIdx, baseIndent) {
|
|
1517
|
+
const block = [];
|
|
1518
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
1519
|
+
const l = lines[i];
|
|
1520
|
+
if (!l.trim()) {
|
|
1521
|
+
block.push("");
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
if (this.getIndent(l) <= baseIndent)
|
|
1525
|
+
break;
|
|
1526
|
+
block.push(l.slice(baseIndent + 4));
|
|
1527
|
+
}
|
|
1528
|
+
return block;
|
|
1529
|
+
}
|
|
1530
|
+
execStatement(lines, idx, scope) {
|
|
1531
|
+
const raw = lines[idx];
|
|
1532
|
+
const line = raw.trim();
|
|
1533
|
+
const indent = this.getIndent(raw);
|
|
1534
|
+
// pass
|
|
1535
|
+
if (line === "pass")
|
|
1536
|
+
return idx + 1;
|
|
1537
|
+
// break / continue
|
|
1538
|
+
if (line === "break") {
|
|
1539
|
+
throw new BreakSignal();
|
|
1540
|
+
}
|
|
1541
|
+
if (line === "continue") {
|
|
1542
|
+
throw new ContinueSignal();
|
|
1543
|
+
}
|
|
1544
|
+
// return
|
|
1545
|
+
const retMatch = line.match(/^return(?:\s+(.+))?$/);
|
|
1546
|
+
if (retMatch)
|
|
1547
|
+
throw new ReturnSignal(retMatch[1] ? this.pyEval(retMatch[1], scope) : NONE);
|
|
1548
|
+
// raise
|
|
1549
|
+
const raiseMatch = line.match(/^raise(?:\s+(.+))?$/);
|
|
1550
|
+
if (raiseMatch) {
|
|
1551
|
+
if (raiseMatch[1]) {
|
|
1552
|
+
const ex = this.pyEval(raiseMatch[1], scope);
|
|
1553
|
+
throw new PyError(typeof ex === "string" ? ex : pyTypeName(ex), pyStr(ex));
|
|
1554
|
+
}
|
|
1555
|
+
throw new PyError("RuntimeError", "");
|
|
1556
|
+
}
|
|
1557
|
+
// assert
|
|
1558
|
+
const assertMatch = line.match(/^assert\s+(.+?)(?:,\s*(.+))?$/);
|
|
1559
|
+
if (assertMatch) {
|
|
1560
|
+
if (!pyBool(this.pyEval(assertMatch[1], scope))) {
|
|
1561
|
+
throw new PyError("AssertionError", assertMatch[2] ? pyStr(this.pyEval(assertMatch[2], scope)) : "");
|
|
1562
|
+
}
|
|
1563
|
+
return idx + 1;
|
|
1564
|
+
}
|
|
1565
|
+
// del
|
|
1566
|
+
const delMatch = line.match(/^del\s+(.+)$/);
|
|
1567
|
+
if (delMatch) {
|
|
1568
|
+
scope.delete(delMatch[1].trim());
|
|
1569
|
+
return idx + 1;
|
|
1570
|
+
}
|
|
1571
|
+
// import / from
|
|
1572
|
+
const importMatch = line.match(/^import\s+(\w+)(?:\s+as\s+(\w+))?$/);
|
|
1573
|
+
if (importMatch) {
|
|
1574
|
+
const [, modName, alias] = importMatch;
|
|
1575
|
+
const factory = MODULE_FACTORIES[modName];
|
|
1576
|
+
if (factory) {
|
|
1577
|
+
const mod = factory(this.cwd);
|
|
1578
|
+
this.modules.set(modName, mod);
|
|
1579
|
+
scope.set(alias ?? modName, mod);
|
|
1580
|
+
}
|
|
1581
|
+
return idx + 1;
|
|
1582
|
+
}
|
|
1583
|
+
const fromMatch = line.match(/^from\s+(\w+)\s+import\s+(.+)$/);
|
|
1584
|
+
if (fromMatch) {
|
|
1585
|
+
const [, modName, imports] = fromMatch;
|
|
1586
|
+
const factory = MODULE_FACTORIES[modName];
|
|
1587
|
+
if (factory) {
|
|
1588
|
+
const mod = factory(this.cwd);
|
|
1589
|
+
if (imports?.trim() === "*") {
|
|
1590
|
+
for (const [k, v] of mod.data)
|
|
1591
|
+
scope.set(k, v);
|
|
1592
|
+
}
|
|
1593
|
+
else {
|
|
1594
|
+
for (const name of imports.split(",").map((s) => s.trim())) {
|
|
1595
|
+
scope.set(name, mod.data.get(name) ?? NONE);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return idx + 1;
|
|
1600
|
+
}
|
|
1601
|
+
// def
|
|
1602
|
+
const defMatch = line.match(/^def\s+(\w+)\s*\(([^)]*)\)\s*:$/);
|
|
1603
|
+
if (defMatch) {
|
|
1604
|
+
const [, fnName, paramsStr] = defMatch;
|
|
1605
|
+
const params = paramsStr
|
|
1606
|
+
.split(",")
|
|
1607
|
+
.map((p) => p.trim())
|
|
1608
|
+
.filter(Boolean);
|
|
1609
|
+
const body = this.collectBlock(lines, idx + 1, indent);
|
|
1610
|
+
const fn = {
|
|
1611
|
+
__pytype__: "func",
|
|
1612
|
+
name: fnName,
|
|
1613
|
+
params,
|
|
1614
|
+
body,
|
|
1615
|
+
closure: new Map(scope),
|
|
1616
|
+
};
|
|
1617
|
+
scope.set(fnName, fn);
|
|
1618
|
+
return idx + 1 + body.length;
|
|
1619
|
+
}
|
|
1620
|
+
// class
|
|
1621
|
+
const classMatch = line.match(/^class\s+(\w+)(?:\(([^)]*)\))?\s*:$/);
|
|
1622
|
+
if (classMatch) {
|
|
1623
|
+
const [, className, basesStr] = classMatch;
|
|
1624
|
+
const bases = basesStr ? basesStr.split(",").map((s) => s.trim()) : [];
|
|
1625
|
+
const body = this.collectBlock(lines, idx + 1, indent);
|
|
1626
|
+
const cls = {
|
|
1627
|
+
__pytype__: "class",
|
|
1628
|
+
name: className,
|
|
1629
|
+
methods: new Map(),
|
|
1630
|
+
bases,
|
|
1631
|
+
};
|
|
1632
|
+
// Parse method definitions from body
|
|
1633
|
+
let j = 0;
|
|
1634
|
+
while (j < body.length) {
|
|
1635
|
+
const bl = body[j].trim();
|
|
1636
|
+
const mMatch = bl.match(/^def\s+(\w+)\s*\(([^)]*)\)\s*:$/);
|
|
1637
|
+
if (mMatch) {
|
|
1638
|
+
const [, mName, mParams] = mMatch;
|
|
1639
|
+
const params = mParams
|
|
1640
|
+
.split(",")
|
|
1641
|
+
.map((p) => p.trim())
|
|
1642
|
+
.filter(Boolean);
|
|
1643
|
+
const mBody = this.collectBlock(body, j + 1, 0);
|
|
1644
|
+
cls.methods.set(mName, {
|
|
1645
|
+
__pytype__: "func",
|
|
1646
|
+
name: mName,
|
|
1647
|
+
params,
|
|
1648
|
+
body: mBody,
|
|
1649
|
+
closure: new Map(scope),
|
|
1650
|
+
});
|
|
1651
|
+
j += 1 + mBody.length;
|
|
1652
|
+
}
|
|
1653
|
+
else {
|
|
1654
|
+
j++;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
scope.set(className, cls);
|
|
1658
|
+
return idx + 1 + body.length;
|
|
1659
|
+
}
|
|
1660
|
+
// if / elif / else
|
|
1661
|
+
if (line.startsWith("if ") && line.endsWith(":")) {
|
|
1662
|
+
const cond = line.slice(3, -1).trim();
|
|
1663
|
+
const body = this.collectBlock(lines, idx + 1, indent);
|
|
1664
|
+
const _skip = body.length + 1;
|
|
1665
|
+
if (pyBool(this.pyEval(cond, scope))) {
|
|
1666
|
+
this.execBlock(body, new Map(scope).also?.((s) => {
|
|
1667
|
+
for (const [k, v] of scope)
|
|
1668
|
+
s.set(k, v);
|
|
1669
|
+
}) ?? scope);
|
|
1670
|
+
// Update scope from block (assignments)
|
|
1671
|
+
this.runBlockInScope(body, scope);
|
|
1672
|
+
// Skip elif/else
|
|
1673
|
+
let j = idx + 1 + body.length;
|
|
1674
|
+
while (j < lines.length) {
|
|
1675
|
+
const l = lines[j].trim();
|
|
1676
|
+
if (this.getIndent(lines[j]) < indent ||
|
|
1677
|
+
(!l.startsWith("elif") && !l.startsWith("else")))
|
|
1678
|
+
break;
|
|
1679
|
+
const bk = this.collectBlock(lines, j + 1, indent);
|
|
1680
|
+
j += 1 + bk.length;
|
|
1681
|
+
}
|
|
1682
|
+
return j;
|
|
1683
|
+
}
|
|
1684
|
+
// Check elif / else
|
|
1685
|
+
let j = idx + 1 + body.length;
|
|
1686
|
+
while (j < lines.length) {
|
|
1687
|
+
const el = lines[j];
|
|
1688
|
+
const elt = el.trim();
|
|
1689
|
+
if (this.getIndent(el) !== indent)
|
|
1690
|
+
break;
|
|
1691
|
+
const elifMatch = elt.match(/^elif\s+(.+):$/);
|
|
1692
|
+
if (elifMatch) {
|
|
1693
|
+
const eBody = this.collectBlock(lines, j + 1, indent);
|
|
1694
|
+
if (pyBool(this.pyEval(elifMatch[1], scope))) {
|
|
1695
|
+
this.runBlockInScope(eBody, scope);
|
|
1696
|
+
j += 1 + eBody.length;
|
|
1697
|
+
// Skip remaining elif/else
|
|
1698
|
+
while (j < lines.length) {
|
|
1699
|
+
const sl = lines[j].trim();
|
|
1700
|
+
if (this.getIndent(lines[j]) !== indent ||
|
|
1701
|
+
(!sl.startsWith("elif") && !sl.startsWith("else")))
|
|
1702
|
+
break;
|
|
1703
|
+
const sb = this.collectBlock(lines, j + 1, indent);
|
|
1704
|
+
j += 1 + sb.length;
|
|
1705
|
+
}
|
|
1706
|
+
return j;
|
|
1707
|
+
}
|
|
1708
|
+
j += 1 + eBody.length;
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
if (elt === "else:") {
|
|
1712
|
+
const eBody = this.collectBlock(lines, j + 1, indent);
|
|
1713
|
+
this.runBlockInScope(eBody, scope);
|
|
1714
|
+
return j + 1 + eBody.length;
|
|
1715
|
+
}
|
|
1716
|
+
break;
|
|
1717
|
+
}
|
|
1718
|
+
return j;
|
|
1719
|
+
}
|
|
1720
|
+
// for
|
|
1721
|
+
const forMatch = line.match(/^for\s+(.+?)\s+in\s+(.+?)\s*:$/);
|
|
1722
|
+
if (forMatch) {
|
|
1723
|
+
const [, target, iterExpr] = forMatch;
|
|
1724
|
+
const iterable = pyIter(this.pyEval(iterExpr.trim(), scope));
|
|
1725
|
+
const body = this.collectBlock(lines, idx + 1, indent);
|
|
1726
|
+
// Check for else clause
|
|
1727
|
+
let elseBody = [];
|
|
1728
|
+
let afterIdx = idx + 1 + body.length;
|
|
1729
|
+
if (afterIdx < lines.length && lines[afterIdx]?.trim() === "else:") {
|
|
1730
|
+
elseBody = this.collectBlock(lines, afterIdx + 1, indent);
|
|
1731
|
+
afterIdx += 1 + elseBody.length;
|
|
1732
|
+
}
|
|
1733
|
+
let broken = false;
|
|
1734
|
+
for (const item of iterable) {
|
|
1735
|
+
// Unpack
|
|
1736
|
+
if (target.includes(",")) {
|
|
1737
|
+
const targets = target.split(",").map((t) => t.trim());
|
|
1738
|
+
const items = Array.isArray(item) ? item : [item];
|
|
1739
|
+
targets.forEach((t, i) => scope.set(t, items[i] ?? NONE));
|
|
1740
|
+
}
|
|
1741
|
+
else {
|
|
1742
|
+
scope.set(target.trim(), item);
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
this.runBlockInScope(body, scope);
|
|
1746
|
+
}
|
|
1747
|
+
catch (e) {
|
|
1748
|
+
if (e instanceof BreakSignal) {
|
|
1749
|
+
broken = true;
|
|
1750
|
+
break;
|
|
1751
|
+
}
|
|
1752
|
+
if (e instanceof ContinueSignal)
|
|
1753
|
+
continue;
|
|
1754
|
+
throw e;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (!broken && elseBody.length)
|
|
1758
|
+
this.runBlockInScope(elseBody, scope);
|
|
1759
|
+
return afterIdx;
|
|
1760
|
+
}
|
|
1761
|
+
// while
|
|
1762
|
+
const whileMatch = line.match(/^while\s+(.+?)\s*:$/);
|
|
1763
|
+
if (whileMatch) {
|
|
1764
|
+
const cond = whileMatch[1];
|
|
1765
|
+
const body = this.collectBlock(lines, idx + 1, indent);
|
|
1766
|
+
let iterations = 0;
|
|
1767
|
+
while (pyBool(this.pyEval(cond, scope)) && iterations++ < 100000) {
|
|
1768
|
+
try {
|
|
1769
|
+
this.runBlockInScope(body, scope);
|
|
1770
|
+
}
|
|
1771
|
+
catch (e) {
|
|
1772
|
+
if (e instanceof BreakSignal)
|
|
1773
|
+
break;
|
|
1774
|
+
if (e instanceof ContinueSignal)
|
|
1775
|
+
continue;
|
|
1776
|
+
throw e;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return idx + 1 + body.length;
|
|
1780
|
+
}
|
|
1781
|
+
// try / except
|
|
1782
|
+
if (line === "try:") {
|
|
1783
|
+
const tryBody = this.collectBlock(lines, idx + 1, indent);
|
|
1784
|
+
let j = idx + 1 + tryBody.length;
|
|
1785
|
+
const exceptClauses = [];
|
|
1786
|
+
let finallyBody = [];
|
|
1787
|
+
let elseBody = [];
|
|
1788
|
+
while (j < lines.length) {
|
|
1789
|
+
const el = lines[j];
|
|
1790
|
+
const elt = el.trim();
|
|
1791
|
+
if (this.getIndent(el) !== indent)
|
|
1792
|
+
break;
|
|
1793
|
+
if (elt.startsWith("except")) {
|
|
1794
|
+
const excMatch = elt.match(/^except(?:\s+(\w+)(?:\s+as\s+(\w+))?)?\s*:$/);
|
|
1795
|
+
const excName = excMatch?.[1] ?? null;
|
|
1796
|
+
const excAlias = excMatch?.[2];
|
|
1797
|
+
const excBody = this.collectBlock(lines, j + 1, indent);
|
|
1798
|
+
exceptClauses.push({ exc: excName, body: excBody });
|
|
1799
|
+
if (excAlias)
|
|
1800
|
+
scope.set(excAlias, "");
|
|
1801
|
+
j += 1 + excBody.length;
|
|
1802
|
+
}
|
|
1803
|
+
else if (elt === "else:") {
|
|
1804
|
+
elseBody = this.collectBlock(lines, j + 1, indent);
|
|
1805
|
+
j += 1 + elseBody.length;
|
|
1806
|
+
}
|
|
1807
|
+
else if (elt === "finally:") {
|
|
1808
|
+
finallyBody = this.collectBlock(lines, j + 1, indent);
|
|
1809
|
+
j += 1 + finallyBody.length;
|
|
1810
|
+
}
|
|
1811
|
+
else
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
let _caughtErr = null;
|
|
1815
|
+
try {
|
|
1816
|
+
this.runBlockInScope(tryBody, scope);
|
|
1817
|
+
if (elseBody.length)
|
|
1818
|
+
this.runBlockInScope(elseBody, scope);
|
|
1819
|
+
}
|
|
1820
|
+
catch (e) {
|
|
1821
|
+
if (e instanceof PyError) {
|
|
1822
|
+
_caughtErr = e;
|
|
1823
|
+
let handled = false;
|
|
1824
|
+
for (const clause of exceptClauses) {
|
|
1825
|
+
if (clause.exc === null ||
|
|
1826
|
+
clause.exc === e.type ||
|
|
1827
|
+
clause.exc === "Exception") {
|
|
1828
|
+
this.runBlockInScope(clause.body, scope);
|
|
1829
|
+
handled = true;
|
|
1830
|
+
break;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (!handled)
|
|
1834
|
+
throw e;
|
|
1835
|
+
}
|
|
1836
|
+
else
|
|
1837
|
+
throw e;
|
|
1838
|
+
}
|
|
1839
|
+
finally {
|
|
1840
|
+
if (finallyBody.length)
|
|
1841
|
+
this.runBlockInScope(finallyBody, scope);
|
|
1842
|
+
}
|
|
1843
|
+
return j;
|
|
1844
|
+
}
|
|
1845
|
+
// with
|
|
1846
|
+
const withMatch = line.match(/^with\s+(.+?)\s+as\s+(\w+)\s*:$/);
|
|
1847
|
+
if (withMatch) {
|
|
1848
|
+
const body = this.collectBlock(lines, idx + 1, indent);
|
|
1849
|
+
scope.set(withMatch[2], NONE); // stub: just set to None
|
|
1850
|
+
this.runBlockInScope(body, scope);
|
|
1851
|
+
return idx + 1 + body.length;
|
|
1852
|
+
}
|
|
1853
|
+
// Augmented assignments: +=, -=, *=, /=, //=, %= **=
|
|
1854
|
+
const augMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*(\+=|-=|\*=|\/\/=|\/=|%=|\*\*=|&=|\|=)\s*(.+)$/);
|
|
1855
|
+
if (augMatch) {
|
|
1856
|
+
const [, name, op, rhsExpr] = augMatch;
|
|
1857
|
+
const lhs = scope.get(name) ?? 0;
|
|
1858
|
+
const rhs = this.pyEval(rhsExpr, scope);
|
|
1859
|
+
let result;
|
|
1860
|
+
switch (op) {
|
|
1861
|
+
case "+=":
|
|
1862
|
+
result =
|
|
1863
|
+
typeof lhs === "string"
|
|
1864
|
+
? lhs + pyStr(rhs)
|
|
1865
|
+
: lhs + rhs;
|
|
1866
|
+
break;
|
|
1867
|
+
case "-=":
|
|
1868
|
+
result = lhs - rhs;
|
|
1869
|
+
break;
|
|
1870
|
+
case "*=":
|
|
1871
|
+
result = lhs * rhs;
|
|
1872
|
+
break;
|
|
1873
|
+
case "/=":
|
|
1874
|
+
result = lhs / rhs;
|
|
1875
|
+
break;
|
|
1876
|
+
case "//=":
|
|
1877
|
+
result = Math.floor(lhs / rhs);
|
|
1878
|
+
break;
|
|
1879
|
+
case "%=":
|
|
1880
|
+
result = lhs % rhs;
|
|
1881
|
+
break;
|
|
1882
|
+
case "**=":
|
|
1883
|
+
result = lhs ** rhs;
|
|
1884
|
+
break;
|
|
1885
|
+
default:
|
|
1886
|
+
result = rhs;
|
|
1887
|
+
}
|
|
1888
|
+
scope.set(name, result);
|
|
1889
|
+
return idx + 1;
|
|
1890
|
+
}
|
|
1891
|
+
// Subscript assignment: obj[key] = val
|
|
1892
|
+
const subAssignMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\[(.+)\]\s*=\s*(.+)$/);
|
|
1893
|
+
if (subAssignMatch) {
|
|
1894
|
+
const [, name, key, valExpr] = subAssignMatch;
|
|
1895
|
+
const obj = scope.get(name) ?? NONE;
|
|
1896
|
+
const val = this.pyEval(valExpr, scope) ?? NONE;
|
|
1897
|
+
const k = this.pyEval(key, scope) ?? NONE;
|
|
1898
|
+
if (Array.isArray(obj))
|
|
1899
|
+
obj[k] = val;
|
|
1900
|
+
else if (isPyDict(obj))
|
|
1901
|
+
obj.data.set(pyStr(k), val);
|
|
1902
|
+
return idx + 1;
|
|
1903
|
+
}
|
|
1904
|
+
// Attribute assignment: obj.attr = val
|
|
1905
|
+
const attrAssignMatch = line.match(/^([A-Za-z_][A-Za-z0-9_.]+)\s*=\s*(.+)$/);
|
|
1906
|
+
if (attrAssignMatch) {
|
|
1907
|
+
const dotIdx = attrAssignMatch[1].lastIndexOf(".");
|
|
1908
|
+
if (dotIdx !== -1) {
|
|
1909
|
+
const objExpr = attrAssignMatch[1].slice(0, dotIdx);
|
|
1910
|
+
const attr = attrAssignMatch[1].slice(dotIdx + 1);
|
|
1911
|
+
const val = this.pyEval(attrAssignMatch[2], scope);
|
|
1912
|
+
const obj = this.pyEval(objExpr, scope);
|
|
1913
|
+
if (isPyDict(obj))
|
|
1914
|
+
obj.data.set(attr, val);
|
|
1915
|
+
else if (isPyInstance(obj))
|
|
1916
|
+
obj.attrs.set(attr, val);
|
|
1917
|
+
return idx + 1;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
// Tuple / multi-assignment: a, b = expr
|
|
1921
|
+
const multiAssignMatch = line.match(/^([A-Za-z_][A-Za-z0-9_,\s]*),\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$/);
|
|
1922
|
+
if (multiAssignMatch) {
|
|
1923
|
+
const rhs = this.pyEval(multiAssignMatch[3], scope);
|
|
1924
|
+
const targets = line
|
|
1925
|
+
.split("=")[0]
|
|
1926
|
+
.split(",")
|
|
1927
|
+
.map((s) => s.trim());
|
|
1928
|
+
const values = pyIter(rhs);
|
|
1929
|
+
targets.forEach((t, i) => scope.set(t, values[i] ?? NONE));
|
|
1930
|
+
return idx + 1;
|
|
1931
|
+
}
|
|
1932
|
+
// Simple assignment: name = expr (or name: type = expr)
|
|
1933
|
+
const assignMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=]+)?\s*=\s*(.+)$/);
|
|
1934
|
+
if (assignMatch) {
|
|
1935
|
+
const [, name, rhs] = assignMatch;
|
|
1936
|
+
scope.set(name, this.pyEval(rhs, scope));
|
|
1937
|
+
return idx + 1;
|
|
1938
|
+
}
|
|
1939
|
+
// Expression statement (function call, etc.)
|
|
1940
|
+
try {
|
|
1941
|
+
this.pyEval(line, scope);
|
|
1942
|
+
}
|
|
1943
|
+
catch (e) {
|
|
1944
|
+
if (e instanceof PyError || e instanceof ExitSignal)
|
|
1945
|
+
throw e;
|
|
1946
|
+
// Ignore eval errors for expression statements
|
|
1947
|
+
}
|
|
1948
|
+
return idx + 1;
|
|
1949
|
+
}
|
|
1950
|
+
runBlockInScope(body, scope) {
|
|
1951
|
+
this.execLines(body, 0, scope);
|
|
1952
|
+
}
|
|
1953
|
+
run(code) {
|
|
1954
|
+
const scope = makeRootScope(this.cwd);
|
|
1955
|
+
try {
|
|
1956
|
+
this.execScript(code, scope);
|
|
1957
|
+
}
|
|
1958
|
+
catch (e) {
|
|
1959
|
+
if (e instanceof ExitSignal)
|
|
1960
|
+
return {
|
|
1961
|
+
stdout: this.getOutput(),
|
|
1962
|
+
stderr: this.getStderr(),
|
|
1963
|
+
exitCode: e.code,
|
|
1964
|
+
};
|
|
1965
|
+
if (e instanceof PyError) {
|
|
1966
|
+
this.stderr.push(e.toString());
|
|
1967
|
+
return {
|
|
1968
|
+
stdout: this.getOutput(),
|
|
1969
|
+
stderr: this.getStderr(),
|
|
1970
|
+
exitCode: 1,
|
|
1971
|
+
};
|
|
1972
|
+
}
|
|
1973
|
+
if (e instanceof ReturnSignal)
|
|
1974
|
+
return {
|
|
1975
|
+
stdout: this.getOutput(),
|
|
1976
|
+
stderr: this.getStderr(),
|
|
1977
|
+
exitCode: 0,
|
|
1978
|
+
};
|
|
1979
|
+
this.stderr.push(`RuntimeError: ${e}`);
|
|
1980
|
+
return {
|
|
1981
|
+
stdout: this.getOutput(),
|
|
1982
|
+
stderr: this.getStderr(),
|
|
1983
|
+
exitCode: 1,
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
return { stdout: this.getOutput(), stderr: this.getStderr(), exitCode: 0 };
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
// ─── command ──────────────────────────────────────────────────────────────────
|
|
1990
|
+
/**
|
|
1991
|
+
* Virtual Python 3 interpreter command. Implements a small Python subset
|
|
1992
|
+
* for scripts and `-c` invocations. Requires `apt install python3` in the
|
|
1993
|
+
* virtual package manager to be available.
|
|
1994
|
+
* @category system
|
|
1995
|
+
* @params ["[--version] [-c <code>] [-V] [file]"]
|
|
1996
|
+
*/
|
|
1997
|
+
export const python3Command = {
|
|
1998
|
+
name: "python3",
|
|
1999
|
+
aliases: ["python"],
|
|
2000
|
+
description: "Python 3 interpreter (virtual)",
|
|
2001
|
+
category: "system",
|
|
2002
|
+
params: ["[--version] [-c <code>] [-V] [file]"],
|
|
2003
|
+
run: ({ args, shell, cwd }) => {
|
|
2004
|
+
// Require explicit installation via `apt install python3`
|
|
2005
|
+
if (!shell.packageManager.isInstalled("python3")) {
|
|
2006
|
+
return {
|
|
2007
|
+
stderr: "bash: python3: command not found\nHint: install it with: apt install python3\n",
|
|
2008
|
+
exitCode: 127,
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
if (ifFlag(args, ["--version", "-V"])) {
|
|
2012
|
+
return { stdout: `${VERSION}\n`, exitCode: 0 };
|
|
2013
|
+
}
|
|
2014
|
+
if (ifFlag(args, ["--version-full"])) {
|
|
2015
|
+
return { stdout: `${VERSION_INFO}\n`, exitCode: 0 };
|
|
2016
|
+
}
|
|
2017
|
+
const cIdx = args.indexOf("-c");
|
|
2018
|
+
if (cIdx !== -1) {
|
|
2019
|
+
const code = args[cIdx + 1];
|
|
2020
|
+
if (!code)
|
|
2021
|
+
return {
|
|
2022
|
+
stderr: "python3: -c requires a code argument\n",
|
|
2023
|
+
exitCode: 1,
|
|
2024
|
+
};
|
|
2025
|
+
// Handle \n as actual newlines
|
|
2026
|
+
const normalised = code.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
|
|
2027
|
+
const interp = new Interpreter(cwd);
|
|
2028
|
+
const { stdout, stderr, exitCode } = interp.run(normalised);
|
|
2029
|
+
return {
|
|
2030
|
+
stdout: stdout || undefined,
|
|
2031
|
+
stderr: stderr || undefined,
|
|
2032
|
+
exitCode,
|
|
2033
|
+
};
|
|
2034
|
+
}
|
|
2035
|
+
const file = args.find((a) => !a.startsWith("-"));
|
|
2036
|
+
if (file) {
|
|
2037
|
+
const filePath = resolvePath(cwd, file);
|
|
2038
|
+
if (!shell.vfs.exists(filePath)) {
|
|
2039
|
+
return {
|
|
2040
|
+
stderr: `python3: can't open file '${file}': [Errno 2] No such file or directory\n`,
|
|
2041
|
+
exitCode: 2,
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
const code = shell.vfs.readFile(filePath);
|
|
2045
|
+
const interp = new Interpreter(cwd);
|
|
2046
|
+
const { stdout, stderr, exitCode } = interp.run(code);
|
|
2047
|
+
return {
|
|
2048
|
+
stdout: stdout || undefined,
|
|
2049
|
+
stderr: stderr || undefined,
|
|
2050
|
+
exitCode,
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
return {
|
|
2054
|
+
stdout: `${VERSION_INFO}\nType "help", "copyright", "credits" or "license" for more information.\n>>> `,
|
|
2055
|
+
exitCode: 0,
|
|
2056
|
+
};
|
|
2057
|
+
},
|
|
2058
|
+
};
|