typescript-virtual-container 1.5.3 → 1.5.5

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