From 5657989e27f4250ee82628b799a743792442b59c Mon Sep 17 00:00:00 2001
From: lightovernight <119399319+lightovernight@users.noreply.github.com>
Date: Wed, 10 Sep 2025 10:14:29 +0800
Subject: [PATCH 01/16] Gsoc 2025 debugger (#396)
* fix an fatal error
* fix bug : rerefrence invaild
* implement better exception support
* fix bug : might crash when eval executes
---
src/debugger/dap.c | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/debugger/dap.c b/src/debugger/dap.c
index e006b2df..811b40ae 100644
--- a/src/debugger/dap.c
+++ b/src/debugger/dap.c
@@ -175,6 +175,7 @@ void c11_dap_handle_evaluate(py_Ref arguments, c11_sbuf* buffer) {
// [eval, nil, expression, globals, locals]
// vectorcall would pop the above 5 items
// so we don't need to pop them manually
+ py_StackRef p0 = py_peek(0);
py_Ref py_eval = py_pushtmp();
py_pushnil();
py_Ref expression = py_pushtmp();
@@ -188,9 +189,14 @@ void c11_dap_handle_evaluate(py_Ref arguments, c11_sbuf* buffer) {
c11_sbuf__write_cstr(buffer, "\"body\":");
if(!ok) {
result = py_formatexc();
+ py_clearexc(p0);
} else {
- py_str(py_retval());
- result = c11_strdup(py_tostr(py_retval()));
+ py_Ref py_result = py_pushtmp();
+ py_assign(py_result, py_retval());
+ py_str(py_result);
+ py_assign(py_result, py_retval());
+ result = c11_strdup(py_tostr(py_result));
+ py_pop();
}
c11_sv result_sv = {.data = result, .size = strlen(result)};
From 8f6ab9f4367319f708ebe1d0cba44ec39b748e92 Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Thu, 11 Sep 2025 18:14:15 +0800
Subject: [PATCH 02/16] add `time.perf_counter`
---
src/modules/time.c | 45 +++++++++++++++++++++++++--------------------
1 file changed, 25 insertions(+), 20 deletions(-)
diff --git a/src/modules/time.c b/src/modules/time.c
index 118901ba..bed35ec6 100644
--- a/src/modules/time.c
+++ b/src/modules/time.c
@@ -5,24 +5,22 @@
#define NANOS_PER_SEC 1000000000
#ifndef __circle__
- int64_t time_ns() {
- struct timespec tms;
- #ifdef CLOCK_REALTIME
- clock_gettime(CLOCK_REALTIME, &tms);
- #else
- /* The C11 way */
- timespec_get(&tms, TIME_UTC);
- #endif
- /* seconds, multiplied with 1 billion */
- int64_t nanos = tms.tv_sec * (int64_t)NANOS_PER_SEC;
- /* Add full nanoseconds */
- nanos += tms.tv_nsec;
- return nanos;
- }
+int64_t time_ns() {
+ struct timespec tms;
+#ifdef CLOCK_REALTIME
+ clock_gettime(CLOCK_REALTIME, &tms);
#else
- int64_t time_ns() {
- return 0;
- }
+ /* The C11 way */
+ timespec_get(&tms, TIME_UTC);
+#endif
+ /* seconds, multiplied with 1 billion */
+ int64_t nanos = tms.tv_sec * (int64_t)NANOS_PER_SEC;
+ /* Add full nanoseconds */
+ nanos += tms.tv_nsec;
+ return nanos;
+}
+#else
+int64_t time_ns() { return 0; }
#endif
static bool time_time(int argc, py_Ref argv) {
@@ -39,15 +37,21 @@ static bool time_time_ns(int argc, py_Ref argv) {
return true;
}
+static bool time_perf_counter(int argc, py_Ref argv) {
+ PY_CHECK_ARGC(0);
+ py_newfloat(py_retval(), (double)clock() / CLOCKS_PER_SEC);
+ return true;
+}
+
static bool time_sleep(int argc, py_Ref argv) {
PY_CHECK_ARGC(1);
py_f64 secs;
if(!py_castfloat(argv, &secs)) return false;
- int64_t start = time_ns();
- const int64_t end = start + secs * 1000000000;
+ clock_t start = clock();
+ const clock_t end = start + (clock_t)(secs * CLOCKS_PER_SEC);
while(true) {
- int64_t now = time_ns();
+ clock_t now = clock();
if(now >= end) break;
}
py_newnone(py_retval());
@@ -101,6 +105,7 @@ void pk__add_module_time() {
py_bindfunc(mod, "time", time_time);
py_bindfunc(mod, "time_ns", time_time_ns);
+ py_bindfunc(mod, "perf_counter", time_perf_counter);
py_bindfunc(mod, "sleep", time_sleep);
py_bindfunc(mod, "localtime", time_localtime);
}
From 985bc299983ed832876b295be9398ea3774b4a33 Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Tue, 16 Sep 2025 19:26:43 +0800
Subject: [PATCH 03/16] add `TypedDict`
---
python/typing.py | 2 ++
src/common/_generated.c | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/python/typing.py b/python/typing.py
index 4f1cf1d4..de322a0b 100644
--- a/python/typing.py
+++ b/python/typing.py
@@ -52,3 +52,5 @@ final = lambda x: x
# exhaustiveness checking
assert_never = lambda x: x
+
+TypedDict = dict
diff --git a/src/common/_generated.c b/src/common/_generated.c
index 75316f7b..52af051c 100644
--- a/src/common/_generated.c
+++ b/src/common/_generated.c
@@ -11,7 +11,7 @@ const char kPythonLibs_functools[] = "class cache:\n def __init__(self, f):\n
const char kPythonLibs_heapq[] = "# Heap queue algorithm (a.k.a. priority queue)\ndef heappush(heap, item):\n \"\"\"Push item onto heap, maintaining the heap invariant.\"\"\"\n heap.append(item)\n _siftdown(heap, 0, len(heap)-1)\n\ndef heappop(heap):\n \"\"\"Pop the smallest item off the heap, maintaining the heap invariant.\"\"\"\n lastelt = heap.pop() # raises appropriate IndexError if heap is empty\n if heap:\n returnitem = heap[0]\n heap[0] = lastelt\n _siftup(heap, 0)\n return returnitem\n return lastelt\n\ndef heapreplace(heap, item):\n \"\"\"Pop and return the current smallest value, and add the new item.\n\n This is more efficient than heappop() followed by heappush(), and can be\n more appropriate when using a fixed-size heap. Note that the value\n returned may be larger than item! That constrains reasonable uses of\n this routine unless written as part of a conditional replacement:\n\n if item > heap[0]:\n item = heapreplace(heap, item)\n \"\"\"\n returnitem = heap[0] # raises appropriate IndexError if heap is empty\n heap[0] = item\n _siftup(heap, 0)\n return returnitem\n\ndef heappushpop(heap, item):\n \"\"\"Fast version of a heappush followed by a heappop.\"\"\"\n if heap and heap[0] < item:\n item, heap[0] = heap[0], item\n _siftup(heap, 0)\n return item\n\ndef heapify(x):\n \"\"\"Transform list into a heap, in-place, in O(len(x)) time.\"\"\"\n n = len(x)\n # Transform bottom-up. The largest index there's any point to looking at\n # is the largest with a child index in-range, so must have 2*i + 1 < n,\n # or i < (n-1)/2. If n is even = 2*j, this is (2*j-1)/2 = j-1/2 so\n # j-1 is the largest, which is n//2 - 1. If n is odd = 2*j+1, this is\n # (2*j+1-1)/2 = j so j-1 is the largest, and that's again n//2-1.\n for i in reversed(range(n//2)):\n _siftup(x, i)\n\n# 'heap' is a heap at all indices >= startpos, except possibly for pos. pos\n# is the index of a leaf with a possibly out-of-order value. Restore the\n# heap invariant.\ndef _siftdown(heap, startpos, pos):\n newitem = heap[pos]\n # Follow the path to the root, moving parents down until finding a place\n # newitem fits.\n while pos > startpos:\n parentpos = (pos - 1) >> 1\n parent = heap[parentpos]\n if newitem < parent:\n heap[pos] = parent\n pos = parentpos\n continue\n break\n heap[pos] = newitem\n\ndef _siftup(heap, pos):\n endpos = len(heap)\n startpos = pos\n newitem = heap[pos]\n # Bubble up the smaller child until hitting a leaf.\n childpos = 2*pos + 1 # leftmost child position\n while childpos < endpos:\n # Set childpos to index of smaller child.\n rightpos = childpos + 1\n if rightpos < endpos and not heap[childpos] < heap[rightpos]:\n childpos = rightpos\n # Move the smaller child up.\n heap[pos] = heap[childpos]\n pos = childpos\n childpos = 2*pos + 1\n # The leaf at pos is empty now. Put newitem there, and bubble it up\n # to its final resting place (by sifting its parents down).\n heap[pos] = newitem\n _siftdown(heap, startpos, pos)";
const char kPythonLibs_linalg[] = "from vmath import *";
const char kPythonLibs_operator[] = "# https://docs.python.org/3/library/operator.html#mapping-operators-to-functions\n\ndef le(a, b): return a <= b\ndef lt(a, b): return a < b\ndef ge(a, b): return a >= b\ndef gt(a, b): return a > b\ndef eq(a, b): return a == b\ndef ne(a, b): return a != b\n\ndef and_(a, b): return a & b\ndef or_(a, b): return a | b\ndef xor(a, b): return a ^ b\ndef invert(a): return ~a\ndef lshift(a, b): return a << b\ndef rshift(a, b): return a >> b\n\ndef is_(a, b): return a is b\ndef is_not(a, b): return a is not b\ndef not_(a): return not a\ndef truth(a): return bool(a)\ndef contains(a, b): return b in a\n\ndef add(a, b): return a + b\ndef sub(a, b): return a - b\ndef mul(a, b): return a * b\ndef truediv(a, b): return a / b\ndef floordiv(a, b): return a // b\ndef mod(a, b): return a % b\ndef pow(a, b): return a ** b\ndef neg(a): return -a\ndef matmul(a, b): return a @ b\n\ndef getitem(a, b): return a[b]\ndef setitem(a, b, c): a[b] = c\ndef delitem(a, b): del a[b]\n\ndef iadd(a, b): a += b; return a\ndef isub(a, b): a -= b; return a\ndef imul(a, b): a *= b; return a\ndef itruediv(a, b): a /= b; return a\ndef ifloordiv(a, b): a //= b; return a\ndef imod(a, b): a %= b; return a\n# def ipow(a, b): a **= b; return a\n# def imatmul(a, b): a @= b; return a\ndef iand(a, b): a &= b; return a\ndef ior(a, b): a |= b; return a\ndef ixor(a, b): a ^= b; return a\ndef ilshift(a, b): a <<= b; return a\ndef irshift(a, b): a >>= b; return a\n";
-const char kPythonLibs_typing[] = "class _Placeholder:\n def __init__(self, *args, **kwargs):\n pass\n def __getitem__(self, *args):\n return self\n def __call__(self, *args, **kwargs):\n return self\n def __and__(self, other):\n return self\n def __or__(self, other):\n return self\n def __xor__(self, other):\n return self\n\n\n_PLACEHOLDER = _Placeholder()\n\nSequence = _PLACEHOLDER\nList = _PLACEHOLDER\nDict = _PLACEHOLDER\nTuple = _PLACEHOLDER\nSet = _PLACEHOLDER\nAny = _PLACEHOLDER\nUnion = _PLACEHOLDER\nOptional = _PLACEHOLDER\nCallable = _PLACEHOLDER\nType = _PLACEHOLDER\nTypeAlias = _PLACEHOLDER\nNewType = _PLACEHOLDER\n\nLiteral = _PLACEHOLDER\nLiteralString = _PLACEHOLDER\n\nIterable = _PLACEHOLDER\nGenerator = _PLACEHOLDER\nIterator = _PLACEHOLDER\n\nHashable = _PLACEHOLDER\n\nTypeVar = _PLACEHOLDER\nSelf = _PLACEHOLDER\n\nProtocol = object\nGeneric = object\nNever = object\n\nTYPE_CHECKING = False\n\n# decorators\noverload = lambda x: x\nfinal = lambda x: x\n\n# exhaustiveness checking\nassert_never = lambda x: x\n";
+const char kPythonLibs_typing[] = "class _Placeholder:\n def __init__(self, *args, **kwargs):\n pass\n def __getitem__(self, *args):\n return self\n def __call__(self, *args, **kwargs):\n return self\n def __and__(self, other):\n return self\n def __or__(self, other):\n return self\n def __xor__(self, other):\n return self\n\n\n_PLACEHOLDER = _Placeholder()\n\nSequence = _PLACEHOLDER\nList = _PLACEHOLDER\nDict = _PLACEHOLDER\nTuple = _PLACEHOLDER\nSet = _PLACEHOLDER\nAny = _PLACEHOLDER\nUnion = _PLACEHOLDER\nOptional = _PLACEHOLDER\nCallable = _PLACEHOLDER\nType = _PLACEHOLDER\nTypeAlias = _PLACEHOLDER\nNewType = _PLACEHOLDER\n\nLiteral = _PLACEHOLDER\nLiteralString = _PLACEHOLDER\n\nIterable = _PLACEHOLDER\nGenerator = _PLACEHOLDER\nIterator = _PLACEHOLDER\n\nHashable = _PLACEHOLDER\n\nTypeVar = _PLACEHOLDER\nSelf = _PLACEHOLDER\n\nProtocol = object\nGeneric = object\nNever = object\n\nTYPE_CHECKING = False\n\n# decorators\noverload = lambda x: x\nfinal = lambda x: x\n\n# exhaustiveness checking\nassert_never = lambda x: x\n\nTypedDict = dict\n";
const char* load_kPythonLib(const char* name) {
if (strchr(name, '.') != NULL) return NULL;
From 6f7df0c3af28a2990fc22b300477f69d6eaf1bc0 Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Wed, 17 Sep 2025 02:00:55 +0800
Subject: [PATCH 04/16] Update ceval.c
---
src/interpreter/ceval.c | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/interpreter/ceval.c b/src/interpreter/ceval.c
index 492ca795..fe506325 100644
--- a/src/interpreter/ceval.c
+++ b/src/interpreter/ceval.c
@@ -11,7 +11,7 @@
#include
#include
-static bool stack_format_object(VM* self, c11_sv spec);
+static bool format_object(VM* self, py_Ref val, c11_sv spec);
#define DISPATCH() \
do { \
@@ -1191,7 +1191,7 @@ __NEXT_STEP:
//////////////////
case OP_FORMAT_STRING: {
py_Ref spec = c11__at(py_TValue, &frame->co->consts, byte.arg);
- bool ok = stack_format_object(self, py_tosv(spec));
+ bool ok = format_object(self, TOP(), py_tosv(spec));
if(!ok) goto __ERROR;
DISPATCH();
}
@@ -1298,10 +1298,9 @@ bool pk_stack_binaryop(VM* self, py_Name op, py_Name rop) {
rhs_t);
}
-static bool stack_format_object(VM* self, c11_sv spec) {
+static bool format_object(VM* self, py_Ref val, c11_sv spec) {
// format TOS via `spec` inplace
// spec: '!r:.2f', '.2f'
- py_StackRef val = TOP();
if(spec.size == 0) return py_str(val);
if(spec.data[0] == '!') {
From 62491dd99ada29f54cef655653fe8d9683f5db6e Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Wed, 17 Sep 2025 14:10:26 +0800
Subject: [PATCH 05/16] reimpl `str.format`
---
include/pocketpy/interpreter/vm.h | 2 +
python/builtins.py | 113 ------------------------------
src/bindings/py_str.c | 96 +++++++++++++++++++++++++
src/common/_generated.c | 2 +-
src/interpreter/ceval.c | 8 +--
tests/04_str.py | 4 +-
6 files changed, 104 insertions(+), 121 deletions(-)
diff --git a/include/pocketpy/interpreter/vm.h b/include/pocketpy/interpreter/vm.h
index 8cde83d4..b231cfc4 100644
--- a/include/pocketpy/interpreter/vm.h
+++ b/include/pocketpy/interpreter/vm.h
@@ -125,6 +125,8 @@ bool pk_stack_binaryop(VM* self, py_Name op, py_Name rop);
void pk_print_stack(VM* self, py_Frame* frame, Bytecode byte);
+bool pk_format_object(VM* self, py_Ref val, c11_sv spec);
+
// type registration
void pk_object__register();
void pk_number__register();
diff --git a/python/builtins.py b/python/builtins.py
index 84ab2a23..d19dd307 100644
--- a/python/builtins.py
+++ b/python/builtins.py
@@ -81,119 +81,6 @@ def sorted(iterable, key=None, reverse=False):
a.sort(key=key, reverse=reverse)
return a
-##### str #####
-def __format_string(self: str, *args, **kwargs) -> str:
- def tokenizeString(s: str):
- tokens = []
- L, R = 0,0
-
- mode = None
- curArg = 0
- # lookingForKword = False
-
- while(R
c11_string* pk_tostr(py_Ref self) {
assert(self->type == tp_str);
@@ -394,6 +396,99 @@ static bool str_encode(int argc, py_Ref argv) {
return true;
}
+static bool str_format(int argc, py_Ref argv) {
+ c11_sv self = py_tosv(argv);
+ py_Ref args = argv + 1;
+ int64_t auto_field_index = -1;
+ bool manual_field_used = false;
+ const char* p_begin = self.data;
+ const char* p_end = self.data + self.size;
+ const char* p = p_begin;
+ c11_sbuf buf;
+ c11_sbuf__ctor(&buf);
+ while(p < p_end) {
+ if(*p == '{') {
+ if((p + 1) < p_end && p[1] == '{') {
+ // '{{' -> '{'
+ c11_sbuf__write_char(&buf, '{');
+ p += 2;
+ } else {
+ if((p + 1) >= p_end) {
+ return ValueError("single '{' encountered in format string");
+ }
+ p++;
+ // parse field
+ c11_sv field = {p, 0};
+ while(p < p_end && *p != '}' && *p != ':') {
+ p++;
+ }
+ if(p < p_end) field.size = p - field.data;
+ // parse spec
+ c11_sv spec = {p, 0};
+ if(*p == ':') {
+ while(p < p_end && *p != '}') {
+ p++;
+ }
+ if(p < p_end) spec.size = p - spec.data;
+ }
+ if(p < p_end) {
+ c11__rtassert(*p == '}');
+ } else {
+ return ValueError("expected '}' before end of string");
+ }
+ // parse auto field
+ int64_t arg_index;
+ if(field.size > 0) { // {0}
+ if(auto_field_index >= 0) {
+ return ValueError(
+ "cannot switch from automatic field numbering to manual field specification");
+ }
+ IntParsingResult res = c11__parse_uint(field, &arg_index, 10);
+ if(res != IntParsing_SUCCESS) {
+ return ValueError("only integer field name is supported");
+ }
+ manual_field_used = true;
+ } else { // {}
+ if(manual_field_used) {
+ return ValueError(
+ "cannot switch from manual field specification to automatic field numbering");
+ }
+ auto_field_index++;
+ arg_index = auto_field_index;
+ }
+ // do format
+ if(arg_index < 0 || arg_index >= (argc - 1)) {
+ return IndexError("replacement index %i out of range for positional args tuple",
+ arg_index);
+ }
+ bool ok = pk_format_object(pk_current_vm, &args[arg_index], spec);
+ if(!ok) {
+ c11_sbuf__dtor(&buf);
+ return false;
+ }
+ // append to buf
+ c11__rtassert(py_isstr(py_retval()));
+ c11_sv formatted = py_tosv(py_retval());
+ c11_sbuf__write_sv(&buf, formatted);
+ p++; // skip '}'
+ }
+ } else if(*p == '}') {
+ if((p + 1) < p_end && p[1] == '}') {
+ // '}}' -> '}'
+ c11_sbuf__write_char(&buf, '}');
+ p += 2;
+ } else {
+ return ValueError("single '}' encountered in format string");
+ }
+ } else {
+ c11_sbuf__write_char(&buf, *p);
+ p++;
+ }
+ }
+ c11_sbuf__py_submit(&buf, py_retval());
+ return true;
+}
+
py_Type pk_str__register() {
py_Type type = pk_newtype("str", tp_object, NULL, NULL, false, true);
// no need to dtor because the memory is controlled by the object
@@ -434,6 +529,7 @@ py_Type pk_str__register() {
py_bindmethod(tp_str, "find", str_find);
py_bindmethod(tp_str, "index", str_index);
py_bindmethod(tp_str, "encode", str_encode);
+ py_bindmethod(tp_str, "format", str_format);
return type;
}
diff --git a/src/common/_generated.c b/src/common/_generated.c
index 52af051c..0916472b 100644
--- a/src/common/_generated.c
+++ b/src/common/_generated.c
@@ -2,7 +2,7 @@
#include "pocketpy/common/_generated.h"
#include
const char kPythonLibs_bisect[] = "\"\"\"Bisection algorithms.\"\"\"\n\ndef insort_right(a, x, lo=0, hi=None):\n \"\"\"Insert item x in list a, and keep it sorted assuming a is sorted.\n\n If x is already in a, insert it to the right of the rightmost x.\n\n Optional args lo (default 0) and hi (default len(a)) bound the\n slice of a to be searched.\n \"\"\"\n\n lo = bisect_right(a, x, lo, hi)\n a.insert(lo, x)\n\ndef bisect_right(a, x, lo=0, hi=None):\n \"\"\"Return the index where to insert item x in list a, assuming a is sorted.\n\n The return value i is such that all e in a[:i] have e <= x, and all e in\n a[i:] have e > x. So if x already appears in the list, a.insert(x) will\n insert just after the rightmost x already there.\n\n Optional args lo (default 0) and hi (default len(a)) bound the\n slice of a to be searched.\n \"\"\"\n\n if lo < 0:\n raise ValueError('lo must be non-negative')\n if hi is None:\n hi = len(a)\n while lo < hi:\n mid = (lo+hi)//2\n if x < a[mid]: hi = mid\n else: lo = mid+1\n return lo\n\ndef insort_left(a, x, lo=0, hi=None):\n \"\"\"Insert item x in list a, and keep it sorted assuming a is sorted.\n\n If x is already in a, insert it to the left of the leftmost x.\n\n Optional args lo (default 0) and hi (default len(a)) bound the\n slice of a to be searched.\n \"\"\"\n\n lo = bisect_left(a, x, lo, hi)\n a.insert(lo, x)\n\n\ndef bisect_left(a, x, lo=0, hi=None):\n \"\"\"Return the index where to insert item x in list a, assuming a is sorted.\n\n The return value i is such that all e in a[:i] have e < x, and all e in\n a[i:] have e >= x. So if x already appears in the list, a.insert(x) will\n insert just before the leftmost x already there.\n\n Optional args lo (default 0) and hi (default len(a)) bound the\n slice of a to be searched.\n \"\"\"\n\n if lo < 0:\n raise ValueError('lo must be non-negative')\n if hi is None:\n hi = len(a)\n while lo < hi:\n mid = (lo+hi)//2\n if a[mid] < x: lo = mid+1\n else: hi = mid\n return lo\n\n# Create aliases\nbisect = bisect_right\ninsort = insort_right\n";
-const char kPythonLibs_builtins[] = "def all(iterable):\n for i in iterable:\n if not i:\n return False\n return True\n\ndef any(iterable):\n for i in iterable:\n if i:\n return True\n return False\n\ndef enumerate(iterable, start=0):\n n = start\n for elem in iterable:\n yield n, elem\n n += 1\n\ndef __minmax_reduce(op, args):\n if len(args) == 2: # min(1, 2)\n return args[0] if op(args[0], args[1]) else args[1]\n if len(args) == 0: # min()\n raise TypeError('expected 1 arguments, got 0')\n if len(args) == 1: # min([1, 2, 3, 4]) -> min(1, 2, 3, 4)\n args = args[0]\n args = iter(args)\n try:\n res = next(args)\n except StopIteration:\n raise ValueError('args is an empty sequence')\n while True:\n try:\n i = next(args)\n except StopIteration:\n break\n if op(i, res):\n res = i\n return res\n\ndef min(*args, key=None):\n key = key or (lambda x: x)\n return __minmax_reduce(lambda x,y: key(x)key(y), args)\n\ndef sum(iterable):\n res = 0\n for i in iterable:\n res += i\n return res\n\ndef map(f, iterable):\n for i in iterable:\n yield f(i)\n\ndef filter(f, iterable):\n for i in iterable:\n if f(i):\n yield i\n\ndef zip(a, b):\n a = iter(a)\n b = iter(b)\n while True:\n try:\n ai = next(a)\n bi = next(b)\n except StopIteration:\n break\n yield ai, bi\n\ndef reversed(iterable):\n a = list(iterable)\n a.reverse()\n return a\n\ndef sorted(iterable, key=None, reverse=False):\n a = list(iterable)\n a.sort(key=key, reverse=reverse)\n return a\n\n##### str #####\ndef __format_string(self: str, *args, **kwargs) -> str:\n def tokenizeString(s: str):\n tokens = []\n L, R = 0,0\n \n mode = None\n curArg = 0\n # lookingForKword = False\n \n while(R list[str]:\n tp_module = type(__import__('math'))\n if isinstance(obj, tp_module):\n return [k for k, _ in obj.__dict__.items()]\n names = set()\n if not isinstance(obj, type):\n obj_d = obj.__dict__\n if obj_d is not None:\n names.update([k for k, _ in obj_d.items()])\n cls = type(obj)\n else:\n cls = obj\n while cls is not None:\n names.update([k for k, _ in cls.__dict__.items()])\n cls = cls.__base__\n return sorted(list(names))\n\nclass set:\n def __init__(self, iterable=None):\n iterable = iterable or []\n self._a = {}\n self.update(iterable)\n\n def add(self, elem):\n self._a[elem] = None\n \n def discard(self, elem):\n self._a.pop(elem, None)\n\n def remove(self, elem):\n del self._a[elem]\n \n def clear(self):\n self._a.clear()\n\n def update(self, other):\n for elem in other:\n self.add(elem)\n\n def __len__(self):\n return len(self._a)\n \n def copy(self):\n return set(self._a.keys())\n \n def __and__(self, other):\n return {elem for elem in self if elem in other}\n\n def __sub__(self, other):\n return {elem for elem in self if elem not in other}\n \n def __or__(self, other):\n ret = self.copy()\n ret.update(other)\n return ret\n\n def __xor__(self, other): \n _0 = self - other\n _1 = other - self\n return _0 | _1\n\n def union(self, other):\n return self | other\n\n def intersection(self, other):\n return self & other\n\n def difference(self, other):\n return self - other\n\n def symmetric_difference(self, other): \n return self ^ other\n \n def __eq__(self, other):\n if not isinstance(other, set):\n return NotImplemented\n return len(self ^ other) == 0\n \n def __ne__(self, other):\n if not isinstance(other, set):\n return NotImplemented\n return len(self ^ other) != 0\n\n def isdisjoint(self, other):\n return len(self & other) == 0\n \n def issubset(self, other):\n return len(self - other) == 0\n \n def issuperset(self, other):\n return len(other - self) == 0\n\n def __contains__(self, elem):\n return elem in self._a\n \n def __repr__(self):\n if len(self) == 0:\n return 'set()'\n return '{'+ ', '.join([repr(i) for i in self._a.keys()]) + '}'\n \n def __iter__(self):\n return iter(self._a.keys())";
+const char kPythonLibs_builtins[] = "def all(iterable):\n for i in iterable:\n if not i:\n return False\n return True\n\ndef any(iterable):\n for i in iterable:\n if i:\n return True\n return False\n\ndef enumerate(iterable, start=0):\n n = start\n for elem in iterable:\n yield n, elem\n n += 1\n\ndef __minmax_reduce(op, args):\n if len(args) == 2: # min(1, 2)\n return args[0] if op(args[0], args[1]) else args[1]\n if len(args) == 0: # min()\n raise TypeError('expected 1 arguments, got 0')\n if len(args) == 1: # min([1, 2, 3, 4]) -> min(1, 2, 3, 4)\n args = args[0]\n args = iter(args)\n try:\n res = next(args)\n except StopIteration:\n raise ValueError('args is an empty sequence')\n while True:\n try:\n i = next(args)\n except StopIteration:\n break\n if op(i, res):\n res = i\n return res\n\ndef min(*args, key=None):\n key = key or (lambda x: x)\n return __minmax_reduce(lambda x,y: key(x)key(y), args)\n\ndef sum(iterable):\n res = 0\n for i in iterable:\n res += i\n return res\n\ndef map(f, iterable):\n for i in iterable:\n yield f(i)\n\ndef filter(f, iterable):\n for i in iterable:\n if f(i):\n yield i\n\ndef zip(a, b):\n a = iter(a)\n b = iter(b)\n while True:\n try:\n ai = next(a)\n bi = next(b)\n except StopIteration:\n break\n yield ai, bi\n\ndef reversed(iterable):\n a = list(iterable)\n a.reverse()\n return a\n\ndef sorted(iterable, key=None, reverse=False):\n a = list(iterable)\n a.sort(key=key, reverse=reverse)\n return a\n\n\ndef help(obj):\n if hasattr(obj, '__func__'):\n obj = obj.__func__\n # print(obj.__signature__)\n if obj.__doc__:\n print(obj.__doc__)\n\ndef complex(real, imag=0):\n import cmath\n return cmath.complex(real, imag) # type: ignore\n\ndef dir(obj) -> list[str]:\n tp_module = type(__import__('math'))\n if isinstance(obj, tp_module):\n return [k for k, _ in obj.__dict__.items()]\n names = set()\n if not isinstance(obj, type):\n obj_d = obj.__dict__\n if obj_d is not None:\n names.update([k for k, _ in obj_d.items()])\n cls = type(obj)\n else:\n cls = obj\n while cls is not None:\n names.update([k for k, _ in cls.__dict__.items()])\n cls = cls.__base__\n return sorted(list(names))\n\nclass set:\n def __init__(self, iterable=None):\n iterable = iterable or []\n self._a = {}\n self.update(iterable)\n\n def add(self, elem):\n self._a[elem] = None\n \n def discard(self, elem):\n self._a.pop(elem, None)\n\n def remove(self, elem):\n del self._a[elem]\n \n def clear(self):\n self._a.clear()\n\n def update(self, other):\n for elem in other:\n self.add(elem)\n\n def __len__(self):\n return len(self._a)\n \n def copy(self):\n return set(self._a.keys())\n \n def __and__(self, other):\n return {elem for elem in self if elem in other}\n\n def __sub__(self, other):\n return {elem for elem in self if elem not in other}\n \n def __or__(self, other):\n ret = self.copy()\n ret.update(other)\n return ret\n\n def __xor__(self, other): \n _0 = self - other\n _1 = other - self\n return _0 | _1\n\n def union(self, other):\n return self | other\n\n def intersection(self, other):\n return self & other\n\n def difference(self, other):\n return self - other\n\n def symmetric_difference(self, other): \n return self ^ other\n \n def __eq__(self, other):\n if not isinstance(other, set):\n return NotImplemented\n return len(self ^ other) == 0\n \n def __ne__(self, other):\n if not isinstance(other, set):\n return NotImplemented\n return len(self ^ other) != 0\n\n def isdisjoint(self, other):\n return len(self & other) == 0\n \n def issubset(self, other):\n return len(self - other) == 0\n \n def issuperset(self, other):\n return len(other - self) == 0\n\n def __contains__(self, elem):\n return elem in self._a\n \n def __repr__(self):\n if len(self) == 0:\n return 'set()'\n return '{'+ ', '.join([repr(i) for i in self._a.keys()]) + '}'\n \n def __iter__(self):\n return iter(self._a.keys())";
const char kPythonLibs_cmath[] = "import math\n\nclass complex:\n def __init__(self, real, imag=0):\n self._real = float(real)\n self._imag = float(imag)\n\n @property\n def real(self):\n return self._real\n \n @property\n def imag(self):\n return self._imag\n\n def conjugate(self):\n return complex(self.real, -self.imag)\n \n def __repr__(self):\n s = ['(', str(self.real)]\n s.append('-' if self.imag < 0 else '+')\n s.append(str(abs(self.imag)))\n s.append('j)')\n return ''.join(s)\n \n def __eq__(self, other):\n if type(other) is complex:\n return self.real == other.real and self.imag == other.imag\n if type(other) in (int, float):\n return self.real == other and self.imag == 0\n return NotImplemented\n \n def __ne__(self, other):\n res = self == other\n if res is NotImplemented:\n return res\n return not res\n \n def __add__(self, other):\n if type(other) is complex:\n return complex(self.real + other.real, self.imag + other.imag)\n if type(other) in (int, float):\n return complex(self.real + other, self.imag)\n return NotImplemented\n \n def __radd__(self, other):\n return self.__add__(other)\n \n def __sub__(self, other):\n if type(other) is complex:\n return complex(self.real - other.real, self.imag - other.imag)\n if type(other) in (int, float):\n return complex(self.real - other, self.imag)\n return NotImplemented\n \n def __rsub__(self, other):\n if type(other) is complex:\n return complex(other.real - self.real, other.imag - self.imag)\n if type(other) in (int, float):\n return complex(other - self.real, -self.imag)\n return NotImplemented\n \n def __mul__(self, other):\n if type(other) is complex:\n return complex(self.real * other.real - self.imag * other.imag,\n self.real * other.imag + self.imag * other.real)\n if type(other) in (int, float):\n return complex(self.real * other, self.imag * other)\n return NotImplemented\n \n def __rmul__(self, other):\n return self.__mul__(other)\n \n def __truediv__(self, other):\n if type(other) is complex:\n denominator = other.real ** 2 + other.imag ** 2\n real_part = (self.real * other.real + self.imag * other.imag) / denominator\n imag_part = (self.imag * other.real - self.real * other.imag) / denominator\n return complex(real_part, imag_part)\n if type(other) in (int, float):\n return complex(self.real / other, self.imag / other)\n return NotImplemented\n \n def __pow__(self, other: int | float):\n if type(other) in (int, float):\n return complex(self.__abs__() ** other * math.cos(other * phase(self)),\n self.__abs__() ** other * math.sin(other * phase(self)))\n return NotImplemented\n \n def __abs__(self) -> float:\n return math.sqrt(self.real ** 2 + self.imag ** 2)\n\n def __neg__(self):\n return complex(-self.real, -self.imag)\n \n def __hash__(self):\n return hash((self.real, self.imag))\n\n\n# Conversions to and from polar coordinates\n\ndef phase(z: complex):\n return math.atan2(z.imag, z.real)\n\ndef polar(z: complex):\n return z.__abs__(), phase(z)\n\ndef rect(r: float, phi: float):\n return r * math.cos(phi) + r * math.sin(phi) * 1j\n\n# Power and logarithmic functions\n\ndef exp(z: complex):\n return math.exp(z.real) * rect(1, z.imag)\n\ndef log(z: complex, base=2.718281828459045):\n return math.log(z.__abs__(), base) + phase(z) * 1j\n\ndef log10(z: complex):\n return log(z, 10)\n\ndef sqrt(z: complex):\n return z ** 0.5\n\n# Trigonometric functions\n\ndef acos(z: complex):\n return -1j * log(z + sqrt(z * z - 1))\n\ndef asin(z: complex):\n return -1j * log(1j * z + sqrt(1 - z * z))\n\ndef atan(z: complex):\n return 1j / 2 * log((1 - 1j * z) / (1 + 1j * z))\n\ndef cos(z: complex):\n return (exp(z) + exp(-z)) / 2\n\ndef sin(z: complex):\n return (exp(z) - exp(-z)) / (2 * 1j)\n\ndef tan(z: complex):\n return sin(z) / cos(z)\n\n# Hyperbolic functions\n\ndef acosh(z: complex):\n return log(z + sqrt(z * z - 1))\n\ndef asinh(z: complex):\n return log(z + sqrt(z * z + 1))\n\ndef atanh(z: complex):\n return 1 / 2 * log((1 + z) / (1 - z))\n\ndef cosh(z: complex):\n return (exp(z) + exp(-z)) / 2\n\ndef sinh(z: complex):\n return (exp(z) - exp(-z)) / 2\n\ndef tanh(z: complex):\n return sinh(z) / cosh(z)\n\n# Classification functions\n\ndef isfinite(z: complex):\n return math.isfinite(z.real) and math.isfinite(z.imag)\n\ndef isinf(z: complex):\n return math.isinf(z.real) or math.isinf(z.imag)\n\ndef isnan(z: complex):\n return math.isnan(z.real) or math.isnan(z.imag)\n\ndef isclose(a: complex, b: complex):\n return math.isclose(a.real, b.real) and math.isclose(a.imag, b.imag)\n\n# Constants\n\npi = math.pi\ne = math.e\ntau = 2 * pi\ninf = math.inf\ninfj = complex(0, inf)\nnan = math.nan\nnanj = complex(0, nan)\n";
const char kPythonLibs_collections[] = "from typing import TypeVar, Iterable\n\ndef Counter[T](iterable: Iterable[T]):\n a: dict[T, int] = {}\n for x in iterable:\n if x in a:\n a[x] += 1\n else:\n a[x] = 1\n return a\n\n\nclass defaultdict(dict):\n def __init__(self, default_factory, *args):\n super().__init__(*args)\n self.default_factory = default_factory\n\n def __missing__(self, key):\n self[key] = self.default_factory()\n return self[key]\n\n def __repr__(self) -> str:\n return f\"defaultdict({self.default_factory}, {super().__repr__()})\"\n\n def copy(self):\n return defaultdict(self.default_factory, self)\n\n\nclass deque[T]:\n _head: int\n _tail: int\n _maxlen: int | None\n _capacity: int\n _data: list[T]\n\n def __init__(self, iterable: Iterable[T] = None, maxlen: int | None = None):\n if maxlen is not None:\n assert maxlen > 0\n\n self._head = 0\n self._tail = 0\n self._maxlen = maxlen\n self._capacity = 8 if maxlen is None else maxlen + 1\n self._data = [None] * self._capacity # type: ignore\n\n if iterable is not None:\n self.extend(iterable)\n\n @property\n def maxlen(self) -> int | None:\n return self._maxlen\n\n def __resize_2x(self):\n backup = list(self)\n self._capacity *= 2\n self._head = 0\n self._tail = len(backup)\n self._data.clear()\n self._data.extend(backup)\n self._data.extend([None] * (self._capacity - len(backup)))\n\n def append(self, x: T):\n if (self._tail + 1) % self._capacity == self._head:\n if self._maxlen is None:\n self.__resize_2x()\n else:\n self.popleft()\n self._data[self._tail] = x\n self._tail = (self._tail + 1) % self._capacity\n\n def appendleft(self, x: T):\n if (self._tail + 1) % self._capacity == self._head:\n if self._maxlen is None:\n self.__resize_2x()\n else:\n self.pop()\n self._head = (self._head - 1) % self._capacity\n self._data[self._head] = x\n\n def copy(self):\n return deque(self, maxlen=self.maxlen)\n \n def count(self, x: T) -> int:\n n = 0\n for item in self:\n if item == x:\n n += 1\n return n\n \n def extend(self, iterable: Iterable[T]):\n for x in iterable:\n self.append(x)\n\n def extendleft(self, iterable: Iterable[T]):\n for x in iterable:\n self.appendleft(x)\n \n def pop(self) -> T:\n if self._head == self._tail:\n raise IndexError(\"pop from an empty deque\")\n self._tail = (self._tail - 1) % self._capacity\n x = self._data[self._tail]\n self._data[self._tail] = None\n return x\n \n def popleft(self) -> T:\n if self._head == self._tail:\n raise IndexError(\"pop from an empty deque\")\n x = self._data[self._head]\n self._data[self._head] = None\n self._head = (self._head + 1) % self._capacity\n return x\n \n def clear(self):\n i = self._head\n while i != self._tail:\n self._data[i] = None # type: ignore\n i = (i + 1) % self._capacity\n self._head = 0\n self._tail = 0\n\n def rotate(self, n: int = 1):\n if len(self) == 0:\n return\n if n > 0:\n n = n % len(self)\n for _ in range(n):\n self.appendleft(self.pop())\n elif n < 0:\n n = -n % len(self)\n for _ in range(n):\n self.append(self.popleft())\n\n def __len__(self) -> int:\n return (self._tail - self._head) % self._capacity\n\n def __contains__(self, x: object) -> bool:\n for item in self:\n if item == x:\n return True\n return False\n \n def __iter__(self):\n i = self._head\n while i != self._tail:\n yield self._data[i]\n i = (i + 1) % self._capacity\n\n def __eq__(self, other: object) -> bool:\n if not isinstance(other, deque):\n return NotImplemented\n if len(self) != len(other):\n return False\n for x, y in zip(self, other):\n if x != y:\n return False\n return True\n \n def __ne__(self, other: object) -> bool:\n if not isinstance(other, deque):\n return NotImplemented\n return not self == other\n \n def __repr__(self) -> str:\n if self.maxlen is None:\n return f\"deque({list(self)!r})\"\n return f\"deque({list(self)!r}, maxlen={self.maxlen})\"\n\n";
const char kPythonLibs_dataclasses[] = "def _get_annotations(cls: type):\n inherits = []\n while cls is not object:\n inherits.append(cls)\n cls = cls.__base__\n inherits.reverse()\n res = {}\n for cls in inherits:\n res.update(cls.__annotations__)\n return res.keys()\n\ndef _wrapped__init__(self, *args, **kwargs):\n cls = type(self)\n cls_d = cls.__dict__\n fields = _get_annotations(cls)\n i = 0 # index into args\n for field in fields:\n if field in kwargs:\n setattr(self, field, kwargs.pop(field))\n else:\n if i < len(args):\n setattr(self, field, args[i])\n i += 1\n elif field in cls_d: # has default value\n setattr(self, field, cls_d[field])\n else:\n raise TypeError(f\"{cls.__name__} missing required argument {field!r}\")\n if len(args) > i:\n raise TypeError(f\"{cls.__name__} takes {len(fields)} positional arguments but {len(args)} were given\")\n if len(kwargs) > 0:\n raise TypeError(f\"{cls.__name__} got an unexpected keyword argument {next(iter(kwargs))!r}\")\n\ndef _wrapped__repr__(self):\n fields = _get_annotations(type(self))\n obj_d = self.__dict__\n args: list = [f\"{field}={obj_d[field]!r}\" for field in fields]\n return f\"{type(self).__name__}({', '.join(args)})\"\n\ndef _wrapped__eq__(self, other):\n if type(self) is not type(other):\n return False\n fields = _get_annotations(type(self))\n for field in fields:\n if getattr(self, field) != getattr(other, field):\n return False\n return True\n\ndef _wrapped__ne__(self, other):\n return not self.__eq__(other)\n\ndef dataclass(cls: type):\n assert type(cls) is type\n cls_d = cls.__dict__\n if '__init__' not in cls_d:\n cls.__init__ = _wrapped__init__\n if '__repr__' not in cls_d:\n cls.__repr__ = _wrapped__repr__\n if '__eq__' not in cls_d:\n cls.__eq__ = _wrapped__eq__\n if '__ne__' not in cls_d:\n cls.__ne__ = _wrapped__ne__\n fields = _get_annotations(cls)\n has_default = False\n for field in fields:\n if field in cls_d:\n has_default = True\n else:\n if has_default:\n raise TypeError(f\"non-default argument {field!r} follows default argument\")\n return cls\n\ndef asdict(obj) -> dict:\n fields = _get_annotations(type(obj))\n obj_d = obj.__dict__\n return {field: obj_d[field] for field in fields}";
diff --git a/src/interpreter/ceval.c b/src/interpreter/ceval.c
index fe506325..ee00febf 100644
--- a/src/interpreter/ceval.c
+++ b/src/interpreter/ceval.c
@@ -11,8 +11,6 @@
#include
#include
-static bool format_object(VM* self, py_Ref val, c11_sv spec);
-
#define DISPATCH() \
do { \
frame->ip++; \
@@ -1191,7 +1189,7 @@ __NEXT_STEP:
//////////////////
case OP_FORMAT_STRING: {
py_Ref spec = c11__at(py_TValue, &frame->co->consts, byte.arg);
- bool ok = format_object(self, TOP(), py_tosv(spec));
+ bool ok = pk_format_object(self, TOP(), py_tosv(spec));
if(!ok) goto __ERROR;
DISPATCH();
}
@@ -1298,9 +1296,9 @@ bool pk_stack_binaryop(VM* self, py_Name op, py_Name rop) {
rhs_t);
}
-static bool format_object(VM* self, py_Ref val, c11_sv spec) {
+bool pk_format_object(VM* self, py_Ref val, c11_sv spec) {
// format TOS via `spec` inplace
- // spec: '!r:.2f', '.2f'
+ // spec: '!r:.2f', ':.2f', '.2f'
if(spec.size == 0) return py_str(val);
if(spec.data[0] == '!') {
diff --git a/tests/04_str.py b/tests/04_str.py
index 4a72746b..bfbf0cb3 100644
--- a/tests/04_str.py
+++ b/tests/04_str.py
@@ -209,8 +209,8 @@ assert "{0} {1} {2}".format("I", "love", "Python") == "I love Python"
assert "{2} {1} {0}".format("I", "love", "Python") == "Python love I"
assert "{0}{1}{0}".format("abra", "cad") == "abracadabra"
-assert "{k}={v}".format(k="key", v="value") == "key=value"
-assert "{k}={k}".format(k="key") == "key=key"
+# assert "{k}={v}".format(k="key", v="value") == "key=value"
+# assert "{k}={k}".format(k="key") == "key=key"
assert "{0}={1}".format('{0}', '{1}') == "{0}={1}"
assert "{{{0}}}".format(1) == "{1}"
assert "{0}{1}{1}".format(1, 2, 3) == "122"
From 226febea82c6f69c68887bc0ef9158c21be52eae Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Wed, 17 Sep 2025 14:16:48 +0800
Subject: [PATCH 06/16] Update ceval.c
---
src/interpreter/ceval.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/interpreter/ceval.c b/src/interpreter/ceval.c
index ee00febf..9e024796 100644
--- a/src/interpreter/ceval.c
+++ b/src/interpreter/ceval.c
@@ -1191,6 +1191,7 @@ __NEXT_STEP:
py_Ref spec = c11__at(py_TValue, &frame->co->consts, byte.arg);
bool ok = pk_format_object(self, TOP(), py_tosv(spec));
if(!ok) goto __ERROR;
+ py_assign(TOP(), py_retval());
DISPATCH();
}
default: c11__unreachable();
@@ -1439,7 +1440,7 @@ bool pk_format_object(VM* self, py_Ref val, c11_sv spec) {
c11_string__delete(body);
// inplace update
- c11_sbuf__py_submit(&buf, val);
+ c11_sbuf__py_submit(&buf, py_retval());
return true;
}
From 50db32f36e51f52ca26c5c45cc81012fc76850b7 Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Wed, 17 Sep 2025 14:19:48 +0800
Subject: [PATCH 07/16] Update 25_rfstring.py
---
tests/25_rfstring.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/tests/25_rfstring.py b/tests/25_rfstring.py
index 4aaf4ebf..5b687bd6 100644
--- a/tests/25_rfstring.py
+++ b/tests/25_rfstring.py
@@ -43,9 +43,18 @@ assert f'{a:<10}' == '10 '
assert f'{a:<10.2f}' == '10.00 '
assert f'{a:>10.2f}' == ' 10.00'
+assert '{}'.format(a) == '10'
+assert '{:>10}'.format(a) == ' 10'
+assert '{:<10}'.format(a) == '10 '
+assert '{:<10.2f}'.format(a) == '10.00 '
+assert '{:>10.2f}'.format(a) == ' 10.00'
+
assert f'{a:^10}' == ' 10 '
assert f'{a:^10.2f}' == ' 10.00 '
+assert '{:^10}'.format(a) == ' 10 '
+assert '{:^10.2f}'.format(a) == ' 10.00 '
+
assert f'{a:3d}' == ' 10'
assert f'{a:10d}' == ' 10'
assert f'{a:1d}' == '10'
From e4a900dd88fda4d99629e281875783d49148b45a Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Wed, 17 Sep 2025 14:37:59 +0800
Subject: [PATCH 08/16] Update PyDict.c
---
src/public/PyDict.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/public/PyDict.c b/src/public/PyDict.c
index 0cc797e2..1e65e13b 100644
--- a/src/public/PyDict.c
+++ b/src/public/PyDict.c
@@ -651,10 +651,18 @@ bool dict_items__next__(int argc, py_Ref argv) {
return StopIteration();
}
+bool dict_items__len__(int argc, py_Ref argv) {
+ PY_CHECK_ARGC(1);
+ DictIterator* iter = py_touserdata(py_arg(0));
+ py_newint(py_retval(), iter->dict->length);
+ return true;
+}
+
py_Type pk_dict_items__register() {
py_Type type = pk_newtype("dict_iterator", tp_object, NULL, NULL, false, true);
py_bindmagic(type, __iter__, pk_wrapper__self);
py_bindmagic(type, __next__, dict_items__next__);
+ py_bindmagic(type, __len__, dict_items__len__);
return type;
}
From 7903d06394c52e39ebd9bca47828d9247664d1d8 Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Thu, 18 Sep 2025 16:02:41 +0800
Subject: [PATCH 09/16] add `NotRequired`
---
python/typing.py | 1 +
src/common/_generated.c | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/python/typing.py b/python/typing.py
index de322a0b..6c4fb27e 100644
--- a/python/typing.py
+++ b/python/typing.py
@@ -54,3 +54,4 @@ final = lambda x: x
assert_never = lambda x: x
TypedDict = dict
+NotRequired = _PLACEHOLDER
diff --git a/src/common/_generated.c b/src/common/_generated.c
index 0916472b..316daab7 100644
--- a/src/common/_generated.c
+++ b/src/common/_generated.c
@@ -11,7 +11,7 @@ const char kPythonLibs_functools[] = "class cache:\n def __init__(self, f):\n
const char kPythonLibs_heapq[] = "# Heap queue algorithm (a.k.a. priority queue)\ndef heappush(heap, item):\n \"\"\"Push item onto heap, maintaining the heap invariant.\"\"\"\n heap.append(item)\n _siftdown(heap, 0, len(heap)-1)\n\ndef heappop(heap):\n \"\"\"Pop the smallest item off the heap, maintaining the heap invariant.\"\"\"\n lastelt = heap.pop() # raises appropriate IndexError if heap is empty\n if heap:\n returnitem = heap[0]\n heap[0] = lastelt\n _siftup(heap, 0)\n return returnitem\n return lastelt\n\ndef heapreplace(heap, item):\n \"\"\"Pop and return the current smallest value, and add the new item.\n\n This is more efficient than heappop() followed by heappush(), and can be\n more appropriate when using a fixed-size heap. Note that the value\n returned may be larger than item! That constrains reasonable uses of\n this routine unless written as part of a conditional replacement:\n\n if item > heap[0]:\n item = heapreplace(heap, item)\n \"\"\"\n returnitem = heap[0] # raises appropriate IndexError if heap is empty\n heap[0] = item\n _siftup(heap, 0)\n return returnitem\n\ndef heappushpop(heap, item):\n \"\"\"Fast version of a heappush followed by a heappop.\"\"\"\n if heap and heap[0] < item:\n item, heap[0] = heap[0], item\n _siftup(heap, 0)\n return item\n\ndef heapify(x):\n \"\"\"Transform list into a heap, in-place, in O(len(x)) time.\"\"\"\n n = len(x)\n # Transform bottom-up. The largest index there's any point to looking at\n # is the largest with a child index in-range, so must have 2*i + 1 < n,\n # or i < (n-1)/2. If n is even = 2*j, this is (2*j-1)/2 = j-1/2 so\n # j-1 is the largest, which is n//2 - 1. If n is odd = 2*j+1, this is\n # (2*j+1-1)/2 = j so j-1 is the largest, and that's again n//2-1.\n for i in reversed(range(n//2)):\n _siftup(x, i)\n\n# 'heap' is a heap at all indices >= startpos, except possibly for pos. pos\n# is the index of a leaf with a possibly out-of-order value. Restore the\n# heap invariant.\ndef _siftdown(heap, startpos, pos):\n newitem = heap[pos]\n # Follow the path to the root, moving parents down until finding a place\n # newitem fits.\n while pos > startpos:\n parentpos = (pos - 1) >> 1\n parent = heap[parentpos]\n if newitem < parent:\n heap[pos] = parent\n pos = parentpos\n continue\n break\n heap[pos] = newitem\n\ndef _siftup(heap, pos):\n endpos = len(heap)\n startpos = pos\n newitem = heap[pos]\n # Bubble up the smaller child until hitting a leaf.\n childpos = 2*pos + 1 # leftmost child position\n while childpos < endpos:\n # Set childpos to index of smaller child.\n rightpos = childpos + 1\n if rightpos < endpos and not heap[childpos] < heap[rightpos]:\n childpos = rightpos\n # Move the smaller child up.\n heap[pos] = heap[childpos]\n pos = childpos\n childpos = 2*pos + 1\n # The leaf at pos is empty now. Put newitem there, and bubble it up\n # to its final resting place (by sifting its parents down).\n heap[pos] = newitem\n _siftdown(heap, startpos, pos)";
const char kPythonLibs_linalg[] = "from vmath import *";
const char kPythonLibs_operator[] = "# https://docs.python.org/3/library/operator.html#mapping-operators-to-functions\n\ndef le(a, b): return a <= b\ndef lt(a, b): return a < b\ndef ge(a, b): return a >= b\ndef gt(a, b): return a > b\ndef eq(a, b): return a == b\ndef ne(a, b): return a != b\n\ndef and_(a, b): return a & b\ndef or_(a, b): return a | b\ndef xor(a, b): return a ^ b\ndef invert(a): return ~a\ndef lshift(a, b): return a << b\ndef rshift(a, b): return a >> b\n\ndef is_(a, b): return a is b\ndef is_not(a, b): return a is not b\ndef not_(a): return not a\ndef truth(a): return bool(a)\ndef contains(a, b): return b in a\n\ndef add(a, b): return a + b\ndef sub(a, b): return a - b\ndef mul(a, b): return a * b\ndef truediv(a, b): return a / b\ndef floordiv(a, b): return a // b\ndef mod(a, b): return a % b\ndef pow(a, b): return a ** b\ndef neg(a): return -a\ndef matmul(a, b): return a @ b\n\ndef getitem(a, b): return a[b]\ndef setitem(a, b, c): a[b] = c\ndef delitem(a, b): del a[b]\n\ndef iadd(a, b): a += b; return a\ndef isub(a, b): a -= b; return a\ndef imul(a, b): a *= b; return a\ndef itruediv(a, b): a /= b; return a\ndef ifloordiv(a, b): a //= b; return a\ndef imod(a, b): a %= b; return a\n# def ipow(a, b): a **= b; return a\n# def imatmul(a, b): a @= b; return a\ndef iand(a, b): a &= b; return a\ndef ior(a, b): a |= b; return a\ndef ixor(a, b): a ^= b; return a\ndef ilshift(a, b): a <<= b; return a\ndef irshift(a, b): a >>= b; return a\n";
-const char kPythonLibs_typing[] = "class _Placeholder:\n def __init__(self, *args, **kwargs):\n pass\n def __getitem__(self, *args):\n return self\n def __call__(self, *args, **kwargs):\n return self\n def __and__(self, other):\n return self\n def __or__(self, other):\n return self\n def __xor__(self, other):\n return self\n\n\n_PLACEHOLDER = _Placeholder()\n\nSequence = _PLACEHOLDER\nList = _PLACEHOLDER\nDict = _PLACEHOLDER\nTuple = _PLACEHOLDER\nSet = _PLACEHOLDER\nAny = _PLACEHOLDER\nUnion = _PLACEHOLDER\nOptional = _PLACEHOLDER\nCallable = _PLACEHOLDER\nType = _PLACEHOLDER\nTypeAlias = _PLACEHOLDER\nNewType = _PLACEHOLDER\n\nLiteral = _PLACEHOLDER\nLiteralString = _PLACEHOLDER\n\nIterable = _PLACEHOLDER\nGenerator = _PLACEHOLDER\nIterator = _PLACEHOLDER\n\nHashable = _PLACEHOLDER\n\nTypeVar = _PLACEHOLDER\nSelf = _PLACEHOLDER\n\nProtocol = object\nGeneric = object\nNever = object\n\nTYPE_CHECKING = False\n\n# decorators\noverload = lambda x: x\nfinal = lambda x: x\n\n# exhaustiveness checking\nassert_never = lambda x: x\n\nTypedDict = dict\n";
+const char kPythonLibs_typing[] = "class _Placeholder:\n def __init__(self, *args, **kwargs):\n pass\n def __getitem__(self, *args):\n return self\n def __call__(self, *args, **kwargs):\n return self\n def __and__(self, other):\n return self\n def __or__(self, other):\n return self\n def __xor__(self, other):\n return self\n\n\n_PLACEHOLDER = _Placeholder()\n\nSequence = _PLACEHOLDER\nList = _PLACEHOLDER\nDict = _PLACEHOLDER\nTuple = _PLACEHOLDER\nSet = _PLACEHOLDER\nAny = _PLACEHOLDER\nUnion = _PLACEHOLDER\nOptional = _PLACEHOLDER\nCallable = _PLACEHOLDER\nType = _PLACEHOLDER\nTypeAlias = _PLACEHOLDER\nNewType = _PLACEHOLDER\n\nLiteral = _PLACEHOLDER\nLiteralString = _PLACEHOLDER\n\nIterable = _PLACEHOLDER\nGenerator = _PLACEHOLDER\nIterator = _PLACEHOLDER\n\nHashable = _PLACEHOLDER\n\nTypeVar = _PLACEHOLDER\nSelf = _PLACEHOLDER\n\nProtocol = object\nGeneric = object\nNever = object\n\nTYPE_CHECKING = False\n\n# decorators\noverload = lambda x: x\nfinal = lambda x: x\n\n# exhaustiveness checking\nassert_never = lambda x: x\n\nTypedDict = dict\nNotRequired = _PLACEHOLDER\n";
const char* load_kPythonLib(const char* name) {
if (strchr(name, '.') != NULL) return NULL;
From e6447760bf43cd24d9f9ba97f97a3815920507e3 Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Tue, 23 Sep 2025 14:49:22 +0800
Subject: [PATCH 10/16] Create CITATION.cff
---
CITATION.cff | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 CITATION.cff
diff --git a/CITATION.cff b/CITATION.cff
new file mode 100644
index 00000000..5fb52e91
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,21 @@
+# This CITATION.cff file was generated with cffinit.
+# Visit https://bit.ly/cffinit to generate yours today!
+
+cff-version: 1.2.0
+title: pocketpy
+message: >-
+ If you use this software, please cite it using the
+ metadata from this file.
+type: software
+authors:
+ - name: blueloveTH
+ website: 'https://pocketpy.dev'
+repository-code: 'https://github.com/pocketpy/pocketpy'
+url: 'https://pocketpy.dev'
+abstract: >-
+ A portable python 3.x interpreter in modern C for game scripting.
+keywords:
+ - python
+ - c
+ - interpreter
+license: MIT
\ No newline at end of file
From 00c1ec1d5e92d4bcd57f37d130345edb06f2fe9a Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Thu, 25 Sep 2025 17:31:59 +0800
Subject: [PATCH 11/16] Update README.md
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index f433a131..b0e7d9da 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,8 @@
+
+
pocketpy is a portable Python 3.x interpreter, written in C11.
From 9955a70c7483c4bd46c21288161c747b3798ced5 Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Sun, 28 Sep 2025 15:31:31 +0800
Subject: [PATCH 12/16] fix compat warning
---
CMakeLists.txt | 3 ++-
src/public/GlobalSetup.c | 4 ++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d92008e1..ffb03f22 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -34,9 +34,10 @@ else()
add_definitions(-DNDEBUG)
endif()
- # disable -Wshorten-64-to-32 for apple
if(APPLE)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-shorten-64-to-32")
+ else()
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-pointer-to-int-cast")
endif()
if(PK_ENABLE_DETERMINISM)
diff --git a/src/public/GlobalSetup.c b/src/public/GlobalSetup.c
index 60486556..ea09569a 100644
--- a/src/public/GlobalSetup.c
+++ b/src/public/GlobalSetup.c
@@ -29,8 +29,8 @@ void py_initialize() {
bool is_little_endian = *(char*)&x == 1;
if(!is_little_endian) c11__abort("is_little_endian != true");
- static_assert(sizeof(py_TValue) == 24, "sizeof(py_TValue) != 24");
- static_assert(offsetof(py_TValue, extra) == 4, "offsetof(py_TValue, extra) != 4");
+ _Static_assert(sizeof(py_TValue) == 24, "sizeof(py_TValue) != 24");
+ _Static_assert(offsetof(py_TValue, extra) == 4, "offsetof(py_TValue, extra) != 4");
pk_current_vm = pk_all_vm[0] = &pk_default_vm;
From 2a9117f39d18400d75fe8e6489e4c93a3fb18c2d Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Sun, 28 Sep 2025 15:43:42 +0800
Subject: [PATCH 13/16] Update CMakeLists.txt
---
CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ffb03f22..ac483d90 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -37,7 +37,7 @@ else()
if(APPLE)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-shorten-64-to-32")
else()
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-pointer-to-int-cast")
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-pointer-to-int-cast -Wno-int-to-pointer-cast")
endif()
if(PK_ENABLE_DETERMINISM)
From 354e8fc03fde194c93441510b71a4fe7896eed9f Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Sun, 28 Sep 2025 19:31:47 +0800
Subject: [PATCH 14/16] support divmod for pos float
---
src/bindings/py_number.c | 31 +++++++++++++++++++++++++++++++
tests/02_float.py | 8 +++++++-
2 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/src/bindings/py_number.c b/src/bindings/py_number.c
index 7e0bd5d6..1e8db693 100644
--- a/src/bindings/py_number.c
+++ b/src/bindings/py_number.c
@@ -175,6 +175,20 @@ static bool int__mod__(int argc, py_Ref argv) {
return true;
}
+static bool float__floordiv__(int argc, py_Ref argv) {
+ PY_CHECK_ARGC(2);
+ py_f64 lhs = py_tofloat(&argv[0]);
+ py_f64 rhs;
+ if(try_castfloat(&argv[1], &rhs)) {
+ if(rhs == 0.0) return ZeroDivisionError("float modulo by zero");
+ py_f64 r = fmod(lhs, rhs);
+ py_newfloat(py_retval(), trunc((lhs - r) / rhs));
+ return true;
+ }
+ py_newnotimplemented(py_retval());
+ return true;
+}
+
static bool float__mod__(int argc, py_Ref argv) {
PY_CHECK_ARGC(2);
py_f64 lhs = py_tofloat(&argv[0]);
@@ -201,6 +215,21 @@ static bool float__rmod__(int argc, py_Ref argv) {
return true;
}
+static bool float__divmod__(int argc, py_Ref argv) {
+ PY_CHECK_ARGC(2);
+ py_f64 lhs = py_tofloat(&argv[0]);
+ py_f64 rhs;
+ if(try_castfloat(&argv[1], &rhs)) {
+ if(rhs == 0.0) return ZeroDivisionError("float modulo by zero");
+ py_f64 r = fmod(lhs, rhs);
+ py_Ref p = py_newtuple(py_retval(), 2);
+ py_newfloat(&p[0], trunc((lhs - r) / rhs));
+ py_newfloat(&p[1], r);
+ return true;
+ }
+ return TypeError("divmod() expects int or float as divisor");
+}
+
static bool int__divmod__(int argc, py_Ref argv) {
PY_CHECK_ARGC(2);
PY_CHECK_ARG_TYPE(1, tp_int);
@@ -535,8 +564,10 @@ void pk_number__register() {
py_bindmagic(tp_int, __divmod__, int__divmod__);
// fmod
+ py_bindmagic(tp_float, __floordiv__, float__floordiv__);
py_bindmagic(tp_float, __mod__, float__mod__);
py_bindmagic(tp_float, __rmod__, float__rmod__);
+ py_bindmagic(tp_float, __divmod__, float__divmod__);
// int.__invert__ & int.
py_bindmagic(tp_int, __invert__, int__invert__);
diff --git a/tests/02_float.py b/tests/02_float.py
index e2b4821c..6d801938 100644
--- a/tests/02_float.py
+++ b/tests/02_float.py
@@ -113,4 +113,10 @@ assert abs(0.0) == 0.0
assert eq(10 % 4, 2)
assert eq(10.5 % 4, 2.5)
assert eq(10 % 4.5, 1.0)
-assert eq(10.5 % 4.5, 1.5)
\ No newline at end of file
+assert eq(10.5 % 4.5, 1.5)
+
+assert eq(10.5 // 4, 2.0)
+assert eq(10.5 // 4.5, 2.0)
+_0, _1 = divmod(10.5, 4)
+assert eq(_0, 2.0)
+assert eq(_1, 2.5)
From c30a7adaff1cd04c1ddede8f80b79669222a3c8c Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Mon, 29 Sep 2025 11:24:41 +0800
Subject: [PATCH 15/16] add `cpy11__float_div_mod`
---
docs/modules/math.md | 7 ++--
src/bindings/py_number.c | 72 ++++++++++++++++++++++++++++++++++++----
src/modules/math.c | 2 ++
tests/02_float.py | 7 ++++
4 files changed, 80 insertions(+), 8 deletions(-)
diff --git a/docs/modules/math.md b/docs/modules/math.md
index 421edbef..f84571db 100644
--- a/docs/modules/math.md
+++ b/docs/modules/math.md
@@ -123,11 +123,14 @@ Convert angle `x` from radians to degrees.
Convert angle `x` from degrees to radians.
-
### `math.modf(x)`
Return the fractional and integer parts of `x`. Both results carry the sign of `x` and are floats.
+### `math.copysign(x, y)`
+
+Return a float with the magnitude (absolute value) of `x` but the sign of `y`.
+
### `math.factorial(x)`
-Return `x` factorial as an integer.
\ No newline at end of file
+Return `x` factorial as an integer.
diff --git a/src/bindings/py_number.c b/src/bindings/py_number.c
index 1e8db693..55c9d494 100644
--- a/src/bindings/py_number.c
+++ b/src/bindings/py_number.c
@@ -149,6 +149,44 @@ static py_i64 cpy11__fast_mod(py_i64 a, py_i64 b) {
return b < 0 ? -res : res;
}
+// https://github.com/python/cpython/blob/3.11/Objects/floatobject.c#L677
+static void cpy11__float_div_mod(double vx, double wx, double *floordiv, double *mod)
+{
+ double div;
+ *mod = fmod(vx, wx);
+ /* fmod is typically exact, so vx-mod is *mathematically* an
+ exact multiple of wx. But this is fp arithmetic, and fp
+ vx - mod is an approximation; the result is that div may
+ not be an exact integral value after the division, although
+ it will always be very close to one.
+ */
+ div = (vx - *mod) / wx;
+ if (*mod) {
+ /* ensure the remainder has the same sign as the denominator */
+ if ((wx < 0) != (*mod < 0)) {
+ *mod += wx;
+ div -= 1.0;
+ }
+ }
+ else {
+ /* the remainder is zero, and in the presence of signed zeroes
+ fmod returns different results across platforms; ensure
+ it has the same sign as the denominator. */
+ *mod = copysign(0.0, wx);
+ }
+ /* snap quotient to nearest integral value */
+ if (div) {
+ *floordiv = floor(div);
+ if (div - *floordiv > 0.5) {
+ *floordiv += 1.0;
+ }
+ }
+ else {
+ /* div is zero - get the same sign as the true quotient */
+ *floordiv = copysign(0.0, vx / wx); /* zero w/ sign of vx/wx */
+ }
+}
+
static bool int__floordiv__(int argc, py_Ref argv) {
PY_CHECK_ARGC(2);
py_i64 lhs = py_toint(&argv[0]);
@@ -181,8 +219,24 @@ static bool float__floordiv__(int argc, py_Ref argv) {
py_f64 rhs;
if(try_castfloat(&argv[1], &rhs)) {
if(rhs == 0.0) return ZeroDivisionError("float modulo by zero");
- py_f64 r = fmod(lhs, rhs);
- py_newfloat(py_retval(), trunc((lhs - r) / rhs));
+ double q, r;
+ cpy11__float_div_mod(lhs, rhs, &q, &r);
+ py_newfloat(py_retval(), q);
+ return true;
+ }
+ py_newnotimplemented(py_retval());
+ return true;
+}
+
+static bool float__rfloordiv__(int argc, py_Ref argv) {
+ PY_CHECK_ARGC(2);
+ py_f64 rhs = py_tofloat(&argv[0]);
+ py_f64 lhs;
+ if(try_castfloat(&argv[1], &lhs)) {
+ if(rhs == 0.0) return ZeroDivisionError("float modulo by zero");
+ double q, r;
+ cpy11__float_div_mod(lhs, rhs, &q, &r);
+ py_newfloat(py_retval(), q);
return true;
}
py_newnotimplemented(py_retval());
@@ -195,7 +249,9 @@ static bool float__mod__(int argc, py_Ref argv) {
py_f64 rhs;
if(try_castfloat(&argv[1], &rhs)) {
if(rhs == 0.0) return ZeroDivisionError("float modulo by zero");
- py_newfloat(py_retval(), fmod(lhs, rhs));
+ double q, r;
+ cpy11__float_div_mod(lhs, rhs, &q, &r);
+ py_newfloat(py_retval(), r);
return true;
}
py_newnotimplemented(py_retval());
@@ -208,7 +264,9 @@ static bool float__rmod__(int argc, py_Ref argv) {
py_f64 lhs;
if(try_castfloat(&argv[1], &lhs)) {
if(rhs == 0.0) return ZeroDivisionError("float modulo by zero");
- py_newfloat(py_retval(), fmod(lhs, rhs));
+ double q, r;
+ cpy11__float_div_mod(lhs, rhs, &q, &r);
+ py_newfloat(py_retval(), r);
return true;
}
py_newnotimplemented(py_retval());
@@ -221,9 +279,10 @@ static bool float__divmod__(int argc, py_Ref argv) {
py_f64 rhs;
if(try_castfloat(&argv[1], &rhs)) {
if(rhs == 0.0) return ZeroDivisionError("float modulo by zero");
- py_f64 r = fmod(lhs, rhs);
+ double q, r;
+ cpy11__float_div_mod(lhs, rhs, &q, &r);
py_Ref p = py_newtuple(py_retval(), 2);
- py_newfloat(&p[0], trunc((lhs - r) / rhs));
+ py_newfloat(&p[0], q);
py_newfloat(&p[1], r);
return true;
}
@@ -565,6 +624,7 @@ void pk_number__register() {
// fmod
py_bindmagic(tp_float, __floordiv__, float__floordiv__);
+ py_bindmagic(tp_float, __rfloordiv__, float__rfloordiv__);
py_bindmagic(tp_float, __mod__, float__mod__);
py_bindmagic(tp_float, __rmod__, float__rmod__);
py_bindmagic(tp_float, __divmod__, float__divmod__);
diff --git a/src/modules/math.c b/src/modules/math.c
index 4ef3f81e..8c6fb55b 100644
--- a/src/modules/math.c
+++ b/src/modules/math.c
@@ -133,6 +133,7 @@ static bool math_radians(int argc, py_Ref argv) {
}
TWO_ARG_FUNC(fmod, fmod)
+TWO_ARG_FUNC(copysign, copysign)
static bool math_modf(int argc, py_Ref argv) {
PY_CHECK_ARGC(1);
@@ -200,6 +201,7 @@ void pk__add_module_math() {
py_bindfunc(mod, "fmod", math_fmod);
py_bindfunc(mod, "modf", math_modf);
+ py_bindfunc(mod, "copysign", math_copysign);
py_bindfunc(mod, "factorial", math_factorial);
}
diff --git a/tests/02_float.py b/tests/02_float.py
index 6d801938..4652133a 100644
--- a/tests/02_float.py
+++ b/tests/02_float.py
@@ -120,3 +120,10 @@ assert eq(10.5 // 4.5, 2.0)
_0, _1 = divmod(10.5, 4)
assert eq(_0, 2.0)
assert eq(_1, 2.5)
+
+assert eq(3.4 % -2, -0.6)
+assert eq(-2 % 3.4, 1.4)
+assert eq(-3.4 % -2, -1.4)
+assert eq(-6 // 3.4, -2.0)
+assert eq(-6 % 3.4, 0.8)
+
From 9b59a9764e5a3e84bbb1ef8e2d8022ed52c949f9 Mon Sep 17 00:00:00 2001
From: blueloveTH
Date: Tue, 7 Oct 2025 20:23:22 +0800
Subject: [PATCH 16/16] Update ceval.c
---
src/interpreter/ceval.c | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/interpreter/ceval.c b/src/interpreter/ceval.c
index 9e024796..ad5368c1 100644
--- a/src/interpreter/ceval.c
+++ b/src/interpreter/ceval.c
@@ -116,8 +116,7 @@ __NEXT_STEP:
#if PK_ENABLE_WATCHDOG
if(self->watchdog_info.max_reset_time > 0) {
- clock_t now = clock();
- if(now > self->watchdog_info.max_reset_time) {
+ if(!py_debugger_isattached() && clock() > self->watchdog_info.max_reset_time) {
self->watchdog_info.max_reset_time = 0;
TimeoutError("watchdog timeout");
goto __ERROR;