diff --git a/CHANGELOG.md b/CHANGELOG.md index 2000bc6d..7b7c976e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ ### Added +- **compat.pocketpy 2.2.0(新包)。** PocketPy 2:用 C11 写的小型可嵌入 Python 3.x 解释器(MIT,无依赖)。从标签归档 + 构建 `src/**/*.c`(即上游 CMake 的 POCKETPY_SRC;`_generated.c` 已随源码提供,无需 prebuild.py)为静态库, + 按上游 CMake 的默认做法作为一个 unity 翻译单元编译(生成的 `pocketpy_unity.c` 按 CMake 的 GLOB_RECURSE 顺序包含 84 个 + 源文件;源码依赖于此:NDEBUG 下 `PK_INLINE` 把头文件声明、其他文件调用的函数标为 `inline`,clang 在 MSVC ABI 下不输出 + 其定义,逐个编译会在 Windows 上留下未定义符号),`-std=gnu11`。配置取 `pocketpy/config.h` 给未定义任何宏的使用者 + 的取值(即使用者的 `` 所见):`PK_ENABLE_OS=1`、`PK_ENABLE_THREADS=0`(CMake 打开它只在其 unity 构建下 + 能编译:`time.c` 未包含声明 `c11_thrd__yield` 的头文件)、`PK_ENABLE_DLL=0`;另打开两项上游选项: + `PK_ENABLE_DETERMINISM=1` 与 `-ffp-contract=off`(同 CMake,浮点结果跨平台一致)、`PK_ENABLE_WATCHDOG=1` + (`py_watchdog_begin`/`py_watchdog_end` 在头文件中总是声明,只在打开时才有定义)。NDEBUG 随构建配置(release + 定义),库与使用者一致。`install()` 带匹配计数地应用上游 2.2.0 之后的修复 de8351ed:异常对象记录的回溯帧中只有 + 调试器才填写的 `locals`/`globals` 未初始化,而垃圾回收无条件标记它们,异常对象存活时回收会读到未初始化内存并可能崩溃 + (上游自己的 `tests/901_array2d_extra1.py` 按 build.sh 构建即崩溃);修复将二者置为 nil。Linux 链接 `-lm`,Windows 链接 `ws2_32` 并定义 `_CRT_SECURE_NO_WARNINGS`。Linux、macOS、 + Windows 与 `wasm32-emscripten` 使用同一组源文件与参数。使用者 `#include `。url 为上游标签归档(暂无 + CN 镜像);声明 `extensions.autoupdate`(`v${version}` 标签)。新增测试成员 `tests/examples/pocketpy`:版本与配置、 + exec/eval 与 int/float/str/bool 读回、dict 与 list 的遍历、绑定的 C 函数及其参数检查、语法错误/运行时错误/ + 抛出/无限递归均返回 false 并带消息且 VM 可继续、异常对象跨多次垃圾回收存活、watchdog 结束死循环、json/math/collections、确定性浮点值、 + 同一 VM 中重复执行、第二个 VM 与 `py_resetvm`。 - **mcpplibs.os 0.2.2。** 新增 `wasm32-emscripten` 目标:基于 Emscripten 的 C 库与 openkal-emscripten 0.6.0 的 wasm 模块,在 node 或页面中运行;新增 `platform::emscripten`、`architecture::wasm32` 与 `is_emscripten`,不定义平台模块。 openkal-emscripten 没有 `openkal.process`,启动程序的调用在链接时失败并给出 openkal 符号名;`interrupt` 报告不会收到 diff --git a/mcpp.toml b/mcpp.toml index 971bbaee..99ee4387 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -139,6 +139,7 @@ members = [ "tests/examples/sdl2", "tests/examples/sdl3", "tests/examples/box2d", + "tests/examples/pocketpy", "tests/examples/sheenbidi", "tests/examples/spdlog", "tests/examples/fontconfig", diff --git a/pkgs/c/compat.pocketpy.lua b/pkgs/c/compat.pocketpy.lua new file mode 100644 index 00000000..5fbe6000 --- /dev/null +++ b/pkgs/c/compat.pocketpy.lua @@ -0,0 +1,276 @@ +-- compat.pocketpy — PocketPy 2, a small embeddable Python 3.x interpreter in +-- C11 (MIT). +-- +-- Shape A (C-source compat): upstream's library target, built from the tag +-- archive as upstream's CMake builds it by default. A consumer writes +-- `#include ` and links the lib. +-- +-- WHAT IS COMPILED. Every src/**/*.c, which is exactly CMake's POCKETPY_SRC +-- (`file(GLOB_RECURSE … src/*.c)`): the compiler, the VM, the public C API +-- (src/public), the built-in types and the bundled modules (math, json, +-- pickle, random, time, os, array2d, vmath …). src/common/_generated.c, the +-- pure-Python part of the standard library as C strings, is checked in, so +-- upstream's prebuild.py does not need to run. Not compiled: src2/ (the +-- `main` REPL program and its tests), 3rd/ (the optional lz4, cute_png, +-- msgpack and periphery modules, each OFF in CMakeOptions.txt), and the +-- pybind11-style C++ layer under include/pybind11 (header-only, not built). +-- +-- ONE TRANSLATION UNIT, as upstream builds it: CMake's default is a unity +-- build of the whole of POCKETPY_SRC in one group (PK_BUILD_WITH_UNITY, +-- UNITY_BUILD_MODE GROUP), and the release's pocketpy.c is the same sources +-- amalgamated. The generated pocketpy_unity.c below includes the 84 files in +-- CMake's order (GLOB_RECURSE sorts them). The sources rely on it: with +-- NDEBUG, PK_INLINE marks functions the headers declare and other files call +-- (py_retval, py_peek, PyObject__userdata …) `inline`, whose definitions +-- clang does not emit for the MSVC ABI, so compiled one by one they leave +-- undefined symbols on Windows; and calls across files inline as upstream's +-- build inlines them. The file list is 2.2.0's: a new version's must be +-- checked against its src/. +-- +-- INCLUDE PATH. `*/include`, upstream's INTERFACE include directory +-- (`target_include_directories(pocketpy INTERFACE include)`): a consumer +-- includes , which defines PK_IS_PUBLIC_INCLUDE and so declares +-- the py_TValue layout. The sources include their private headers as +-- "pocketpy/…" from there, so it cannot be narrower; the private headers are +-- all under pocketpy/, and upstream's pybind11-style C++ layer is under +-- pybind11/, as a CMake consumer sees them. +-- +-- CONFIGURATION. The library is built with the PK_ENABLE_* values its own +-- include/pocketpy/config.h gives a consumer that defines none (the values a +-- consumer's reads, and those of upstream's single-file release +-- and of its build.sh / run_tests.sh builds), except for two upstream options +-- that are on here: +-- PK_ENABLE_OS=1 the os module, file import, the debugger +-- (config.h's default; CMake's too). +-- PK_ENABLE_THREADS=0 config.h's default (CMake's is on). Off, pkpy has +-- no ComputeThread and names are interned without a +-- lock: the same on every platform, the Web +-- included, where a program has no threads unless +-- built for shared memory (-pthread). +-- PK_ENABLE_DLL=0 config.h's default: no import of native modules +-- from shared libraries (dlopen / LoadLibrary). +-- PK_ENABLE_DETERMINISM=1 and -ffp-contract=off, as CMake builds it: float +-- operations give the same bits on every platform +-- (no contraction of a*b+c into an FMA; the math +-- module is upstream's own dmath either way). +-- PK_ENABLE_WATCHDOG=1 pocketpy.h declares py_watchdog_begin and +-- py_watchdog_end unconditionally, but they are +-- defined only with the watchdog on: on, a host can +-- end a runaway script with a TimeoutError. It costs +-- one comparison per bytecode while no watchdog runs. +-- PK_ENABLE_CUSTOM_SNAME=0, PK_ENABLE_MIMALLOC=0: off, as everywhere. +-- NDEBUG follows the build profile, as it follows CMake's build type upstream +-- (defined for every type but Debug): the release profile defines it for the +-- library and its consumer alike, which matters because pocketpy.h declares +-- py_callcfunc as a function without NDEBUG and as a macro with it. +-- +-- PLATFORMS. Linux (GCC, LLVM), macOS, Windows (clang targeting the MSVC ABI) +-- and wasm32-emscripten, the same translation unit and flags. Linux links libm for a +-- consumer linked by the C driver. Windows links ws2_32 (CMake, for the +-- debugger's socket) and defines _CRT_SECURE_NO_WARNINGS as CMake does. +-- +-- ONE UPSTREAM FIX, applied in install() with a checked match count: +-- pocketpy commit de8351ed (2026-09-06, after the 2.2.0 tag, on main since). +-- An exception keeps up to seven traceback frames, each with a `locals` and a +-- `globals` value that only the debugger fills in; the garbage collector +-- marks both unconditionally, so a collection while an exception object is +-- alive reads uninitialised memory as values, and crashes when it looks like +-- a pointer. Upstream's own tests/901_array2d_extra1.py crashes so, built as +-- build.sh builds it (GCC 16, -O2). The fix sets both to nil when a frame is +-- recorded, as upstream's main does. Nothing else is changed. +-- +-- NO CN MIRROR. The url is upstream's tag archive as a plain string, the +-- fallback docs/cn-mirror.md describes for a contributor without mcpp-res +-- write access; a maintainer can add the CN entry later, the sha256 stays. +-- +-- LICENSE. MIT (LICENSE, Copyright (c) 2026 blueloveTH). +package = { + spec = "1", + namespace = "compat", + name = "pocketpy", + description = "PocketPy 2.2.0, a small embeddable Python 3.x interpreter in C11 (MIT)", + licenses = {"MIT"}, + repo = "https://github.com/pocketpy/pocketpy", + type = "package", + + extensions = { + autoupdate = { + tag = "v${version}", + url = "https://github.com/pocketpy/pocketpy/archive/refs/tags/${tag}.tar.gz", + }, + }, + + xpm = { + linux = { + ["2.2.0"] = { + url = "https://github.com/pocketpy/pocketpy/archive/refs/tags/v2.2.0.tar.gz", + sha256 = "c67cb6a1422abe26114219d2e58441cbb3625bae6b25901d5b7f6f46b23ae8b3", + }, + }, + macosx = { + ["2.2.0"] = { + url = "https://github.com/pocketpy/pocketpy/archive/refs/tags/v2.2.0.tar.gz", + sha256 = "c67cb6a1422abe26114219d2e58441cbb3625bae6b25901d5b7f6f46b23ae8b3", + }, + }, + windows = { + ["2.2.0"] = { + url = "https://github.com/pocketpy/pocketpy/archive/refs/tags/v2.2.0.tar.gz", + sha256 = "c67cb6a1422abe26114219d2e58441cbb3625bae6b25901d5b7f6f46b23ae8b3", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "gnu11", + include_dirs = { "*/include" }, + sources = { "*/mcpp/pocketpy_unity.c" }, + generated_files = { + ["pocketpy-2.2.0/mcpp/pocketpy_unity.c"] = [==[ +/* compat.pocketpy: upstream's unity build (CMakeLists.txt, PK_BUILD_WITH_UNITY, + one UNITY_GROUP): every C source under src of 2.2.0, in the order of + CMake's GLOB_RECURSE. */ +#include "../src/bindings/py_array.c" +#include "../src/bindings/py_mappingproxy.c" +#include "../src/bindings/py_method.c" +#include "../src/bindings/py_number.c" +#include "../src/bindings/py_object.c" +#include "../src/bindings/py_property.c" +#include "../src/bindings/py_range.c" +#include "../src/bindings/py_str.c" +#include "../src/common/_generated.c" +#include "../src/common/algorithm.c" +#include "../src/common/chunkedvector.c" +#include "../src/common/dmath.c" +#include "../src/common/memorypool.c" +#include "../src/common/name.c" +#include "../src/common/serialize.c" +#include "../src/common/smallmap.c" +#include "../src/common/socket.c" +#include "../src/common/sourcedata.c" +#include "../src/common/sstream.c" +#include "../src/common/str.c" +#include "../src/common/threads.c" +#include "../src/common/vector.c" +#include "../src/compiler/compiler.c" +#include "../src/compiler/lexer.c" +#include "../src/debugger/core.c" +#include "../src/debugger/dap.c" +#include "../src/interpreter/ceval.c" +#include "../src/interpreter/dll.c" +#include "../src/interpreter/frame.c" +#include "../src/interpreter/generator.c" +#include "../src/interpreter/heap.c" +#include "../src/interpreter/line_profiler.c" +#include "../src/interpreter/objectpool.c" +#include "../src/interpreter/py_compile.c" +#include "../src/interpreter/typeinfo.c" +#include "../src/interpreter/vm.c" +#include "../src/interpreter/vmx.c" +#include "../src/modules/array2d.c" +#include "../src/modules/base64.c" +#include "../src/modules/builtins.c" +#include "../src/modules/colorcvt.c" +#include "../src/modules/conio.c" +#include "../src/modules/dis.c" +#include "../src/modules/easing.c" +#include "../src/modules/enum.c" +#include "../src/modules/gc.c" +#include "../src/modules/importlib.c" +#include "../src/modules/inspect.c" +#include "../src/modules/json.c" +#include "../src/modules/lz4.c" +#include "../src/modules/math.c" +#include "../src/modules/os.c" +#include "../src/modules/pickle.c" +#include "../src/modules/picoterm.c" +#include "../src/modules/pkpy.c" +#include "../src/modules/random.c" +#include "../src/modules/stdc.c" +#include "../src/modules/time.c" +#include "../src/modules/traceback.c" +#include "../src/modules/unicodedata.c" +#include "../src/modules/vmath.c" +#include "../src/objects/bintree.c" +#include "../src/objects/codeobject.c" +#include "../src/objects/codeobject_ser.c" +#include "../src/objects/container.c" +#include "../src/objects/namedict.c" +#include "../src/objects/object.c" +#include "../src/public/Bindings.c" +#include "../src/public/CodeExecution.c" +#include "../src/public/DictSlots.c" +#include "../src/public/FrameOps.c" +#include "../src/public/GlobalSetup.c" +#include "../src/public/Inspection.c" +#include "../src/public/ModuleSystem.c" +#include "../src/public/PyDict.c" +#include "../src/public/PyException.c" +#include "../src/public/PyList.c" +#include "../src/public/PySlice.c" +#include "../src/public/PyTuple.c" +#include "../src/public/PythonOps.c" +#include "../src/public/StackOps.c" +#include "../src/public/TypeSystem.c" +#include "../src/public/ValueCast.c" +#include "../src/public/ValueCreation.c" +]==], + }, + cflags = { + "-ffp-contract=off", + "-DPK_ENABLE_OS=1", + "-DPK_ENABLE_THREADS=0", + "-DPK_ENABLE_DLL=0", + "-DPK_ENABLE_DETERMINISM=1", + "-DPK_ENABLE_WATCHDOG=1", + "-DPK_ENABLE_CUSTOM_SNAME=0", + "-DPK_ENABLE_MIMALLOC=0", + }, + linux = { + ldflags = { "-lm" }, + }, + windows = { + cflags = { "-D_CRT_SECURE_NO_WARNINGS" }, + ldflags = { "-lws2_32" }, + }, + targets = { ["pocketpy"] = { kind = "lib" } }, + deps = { }, + }, +} + +import("xim.libxpkg.pkginfo") +import("xim.libxpkg.log") + +-- See the header note: upstream's fix for the uninitialised traceback-frame +-- values (de8351ed), then the unpacked tree moved into the install dir as its +-- single wrap level, so the `*/` globs keep matching (compat.libserial does +-- the same). +function install() + local wrap = "pocketpy-" .. pkginfo.version() + local source = path.join(wrap, "src/public/PyException.c") + local content = io.readfile(source) + if not content then + log.error("compat.pocketpy: cannot read %s", source) + return false + end + local before = " BaseExceptionFrame* frame_dump = c11_vector__emplace(&ud->stacktrace);\n" + local after = before + .. " // `locals` and `globals` are only filled in when the debugger is attached, but the GC\n" + .. " // walks them unconditionally; nil them out before anything below can allocate\n" + .. " py_newnil(&frame_dump->locals);\n" + .. " py_newnil(&frame_dump->globals);\n" + local patched, count = content:gsub(before:gsub("(%W)", "%%%1"), function() return after end) + if count ~= 1 then + log.error("compat.pocketpy: expected 1 match in %s, found %d", source, count) + return false + end + io.writefile(source, patched) + + local prefix = pkginfo.install_dir() + os.tryrm(prefix) + os.mkdir(prefix) + os.mv(wrap, path.join(prefix, wrap)) + return true +end diff --git a/tests/examples/pocketpy/mcpp.toml b/tests/examples/pocketpy/mcpp.toml new file mode 100644 index 00000000..90e7c311 --- /dev/null +++ b/tests/examples/pocketpy/mcpp.toml @@ -0,0 +1,9 @@ +# PocketPy test project: Python source run by the interpreter and its results +# read back through the C API, on Linux, macOS and Windows. +[package] +name = "pocketpy-tests" +version = "0.1.0" +standard = "c++23" + +[dependencies] +compat.pocketpy = "2.2.0" diff --git a/tests/examples/pocketpy/tests/pocketpy.cpp b/tests/examples/pocketpy/tests/pocketpy.cpp new file mode 100644 index 00000000..9bc4e390 --- /dev/null +++ b/tests/examples/pocketpy/tests/pocketpy.cpp @@ -0,0 +1,197 @@ +// compat.pocketpy — PocketPy 2.2.0: Python evaluated by the interpreter, +// checked by value through the C API. +// +// WHAT THIS ASSERTS AND WHY +// +// the version PK_VERSION and the interpreter's own sys.version +// exec and eval statements and expressions in the main module, the +// results read back as int, float, str and bool +// containers a dict and a list built in Python, walked from C++ +// native functions a C function bound into the main module and called +// from Python, with its argument checks raising TypeError +// errors a syntax error, a runtime error, an uncaught raise and +// runaway recursion: each returns false with a formatted +// message, and the VM goes on once it is cleared +// the watchdog PK_ENABLE_WATCHDOG=1: py_watchdog_begin ends an endless +// loop with a TimeoutError +// the configuration pkpy.configmacros: the PK_ENABLE_* the package sets +// upstream's fix exceptions kept alive across garbage collections +// (de8351ed, applied by install()): without it the +// collector reads their unset traceback values and +// crashes +// the library json and math (dmath), modules compiled in C, and the +// pure-Python ones from _generated.c (collections) +// determinism upstream's deterministic float test values, bit for bit +// several VMs a second VM's globals are its own; py_resetvm empties +// the current one; re-running code in one VM +// +// The expected values are written out by hand; none is computed by the code +// under test. + +#include + +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool ok, const char* what) { + std::printf("%-60s %s\n", what, ok ? "ok" : "FAILED"); + if (!ok) ++failures; +} + +// The pending exception as text, cleared. +std::string take_error() { + char* text = py_formatexc(); + std::string out = text ? text : ""; + py_free(text); + py_clearexc(nullptr); + return out; +} + +std::string eval_repr(const char* source) { + if (!py_eval(source, nullptr)) return "exception " + take_error(); + // py_retval() is not an argument: the call would overwrite it. + py_setreg(7, py_retval()); + if (!py_repr(py_getreg(7))) return "exception " + take_error(); + return py_tostr(py_retval()); +} + +bool add_ints(int argc, py_StackRef argv) { + PY_CHECK_ARGC(2); + PY_CHECK_ARG_TYPE(0, tp_int); + PY_CHECK_ARG_TYPE(1, tp_int); + py_newint(py_retval(), py_toint(py_arg(0)) + py_toint(py_arg(1))); + return true; +} + +int dict_sum = 0; +bool sum_values(py_Ref, py_Ref val, void*) { + dict_sum += static_cast(py_toint(val)); + return true; +} + +} // namespace + +int main() { + py_initialize(); + + check(std::strcmp(PK_VERSION, "2.2.0") == 0 && PK_VERSION_MAJOR == 2, "PK_VERSION is 2.2.0"); + check(eval_repr("__import__('sys').version") == "'2.2.0'", "sys.version is 2.2.0"); + + // The configuration the descriptor builds. + check(eval_repr("[__import__('pkpy').configmacros[k] for k in ('PK_ENABLE_OS', 'PK_ENABLE_THREADS', " + "'PK_ENABLE_DETERMINISM', 'PK_ENABLE_WATCHDOG')]") == "[1, 0, 1, 1]", + "PK_ENABLE_OS, _THREADS, _DETERMINISM, _WATCHDOG = 1, 0, 1, 1"); + + // exec, then eval in the same main module. + check(py_exec("x = 6 * 7\ns = 'py' + 'thon'\nf = 1 / 4\nb = x > 40", "", EXEC_MODE, nullptr), + "exec statements"); + check(py_eval("x", nullptr) && py_isint(py_retval()) && py_toint(py_retval()) == 42, "int read back"); + check(py_eval("f", nullptr) && py_isfloat(py_retval()) && py_tofloat(py_retval()) == 0.25, "float read back"); + check(py_eval("s", nullptr) && py_isstr(py_retval()) && std::string(py_tostr(py_retval())) == "python", + "str read back"); + check(py_eval("b", nullptr) && py_isbool(py_retval()) && py_tobool(py_retval()), "bool read back"); + // int is 64-bit (py_i64): no arbitrary precision. + check(eval_repr("2 ** 62") == "4611686018427387904", "int is py_i64"); + check(eval_repr("7 // -2") == "-4" && eval_repr("-7 % 3") == "2", + "integer floor division and modulo"); + + // Containers, walked from C++. + check(py_eval("{'a': 1, 'b': 2, 'c': 39}", nullptr) && py_isdict(py_retval()), "dict made"); + py_setreg(0, py_retval()); + dict_sum = 0; + check(py_dict_len(py_getreg(0)) == 3 && py_dict_apply(py_getreg(0), sum_values, nullptr) && dict_sum == 42, + "dict walked"); + check(py_dict_getitem_by_str(py_getreg(0), "c") == 1 && py_toint(py_retval()) == 39, "dict item by key"); + check(py_eval("[i * i for i in range(5)]", nullptr) && py_islist(py_retval()) && py_list_len(py_retval()) == 5 && + py_toint(py_list_getitem(py_retval(), 4)) == 16, + "list walked"); + + // A native function, called from Python. + py_bindfunc(py_getmodule("__main__"), "add_ints", add_ints); + check(eval_repr("add_ints(40, 2)") == "42", "native function called from Python"); + check(eval_repr("add_ints(1)").find("TypeError") != std::string::npos, "native function argument count checked"); + check(eval_repr("add_ints(1, 'a')").find("TypeError") != std::string::npos, "native function argument type checked"); + check(eval_repr("[add_ints(i, 1) for i in range(3)]") == "[1, 2, 3]", "native function in a comprehension"); + + // Errors: messages, and the VM goes on. + check(!py_exec("def broken(:\n pass", "", EXEC_MODE, nullptr) && + take_error().find("SyntaxError") != std::string::npos, + "a syntax error is reported"); + check(!py_exec("1 / 0", "", EXEC_MODE, nullptr) && + take_error().find("ZeroDivisionError") != std::string::npos, + "a runtime error is reported"); + check(!py_exec("raise ValueError('bad value 7')", "", EXEC_MODE, nullptr) && + take_error().find("bad value 7") != std::string::npos, + "a raised error carries its message"); + check(!py_exec("def r(n): return r(n + 1)\nr(0)", "", EXEC_MODE, nullptr) && + take_error().find("RecursionError") != std::string::npos, + "runaway recursion is a RecursionError"); + // The watchdog (PK_ENABLE_WATCHDOG=1): a runaway loop ends in TimeoutError. + py_watchdog_begin(50); + check(!py_exec("while True: pass", "", EXEC_MODE, nullptr) && + take_error().find("TimeoutError") != std::string::npos, + "the watchdog ends a runaway loop"); + py_watchdog_end(); + check(eval_repr("x + 1") == "43", "the VM goes on after errors"); + + // Exceptions alive across collections, after garbage has dirtied the heap. + check(py_exec("import gc\n" + "def deep(n):\n" + " if n == 0: raise ValueError('deep')\n" + " return deep(n - 1)\n" + "kept = []\n" + "for r in range(20):\n" + " junk = [str(i) * 50 for i in range(2000)]\n" + " junk = None\n" + " gc.collect()\n" + " for i in range(20):\n" + " try:\n" + " deep(10)\n" + " except ValueError as e:\n" + " kept.append(e)\n" + " gc.collect()\n", + "", EXEC_MODE, nullptr) && + eval_repr("len(kept)") == "400", + "exceptions survive garbage collections"); + + // The library: C modules and pure-Python ones. + check(eval_repr("__import__('json').dumps({'k': [1, 2.5, None, True]})") == "'{\"k\": [1, 2.5, null, true]}'", + "json.dumps"); + check(eval_repr("__import__('math').sqrt(2.25)") == "1.5", "math.sqrt"); + check(eval_repr("__import__('collections').deque([1, 2, 3]).pop()") == "3", "collections (pure Python)"); + + // Upstream's deterministic float values (tests/930_deterministic_float.py). + check(py_eval("__import__('math').sin(1.0)", nullptr) && py_tofloat(py_retval()) == 0.8414709848078965, + "math.sin(1.0), bit for bit"); + check(py_eval("__import__('math').exp(1.0)", nullptr) && py_tofloat(py_retval()) == 2.718281828459045, + "math.exp(1.0), bit for bit"); + + // Re-running code in the same VM: a function redefined replaces the old. + for (int i = 0; i < 3; ++i) py_exec("def version(): return 1", "", EXEC_MODE, nullptr); + py_exec("def version(): return 2", "", EXEC_MODE, nullptr); + check(eval_repr("version()") == "2", "code re-run in the same VM"); + + // A second VM has its own globals; resetting a VM empties it. + py_switchvm(1); + check(py_currentvm() == 1 && !py_eval("x", nullptr) && take_error().find("NameError") != std::string::npos, + "a second VM has its own globals"); + py_exec("y = 'second'", "", EXEC_MODE, nullptr); + py_switchvm(0); + check(eval_repr("x") == "42", "the first VM keeps its globals"); + py_resetvm(); + check(!py_eval("x", nullptr) && take_error().find("NameError") != std::string::npos, "py_resetvm empties the VM"); + check(eval_repr("add_ints").find("NameError") != std::string::npos, "py_resetvm drops bindings"); + py_switchvm(1); + check(eval_repr("y") == "'second'", "the second VM is untouched by the reset"); + + py_finalize(); + std::printf("%s\n", failures ? "FAILED" : "all passed"); + return failures ? 1 : 0; +}