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 @@ Featured|HelloGitHub + +Ask DeepWiki

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;