Merge branch 'pocketpy:main' into gsoc-2025-debugger

This commit is contained in:
lightovernight 2025-10-07 21:47:08 +08:00 committed by GitHub
commit bd3ace340a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 294 additions and 154 deletions

21
CITATION.cff Normal file
View File

@ -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

View File

@ -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 -Wno-int-to-pointer-cast")
endif()
if(PK_ENABLE_DETERMINISM)

View File

@ -21,6 +21,8 @@
<img src="https://img.shields.io/discord/1048978026131640390.svg" /></a>
<!-- HelloGithub -->
<a href="https://hellogithub.com/repository/dd9c509d72a64caca03d99d5b1991a33" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=dd9c509d72a64caca03d99d5b1991a33&claim_uid=jhOYmWGE75AL0Bp&theme=small" alt="FeaturedHelloGitHub" /></a>
<!-- DeepWiki -->
<a href="https://deepwiki.com/pocketpy/pocketpy"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
</p>
pocketpy is a portable Python 3.x interpreter, written in C11.

View File

@ -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.
Return `x` factorial as an integer.

View File

@ -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();

View File

@ -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<len(s)):
curChar = s[R]
nextChar = s[R+1] if R+1<len(s) else ''
# Invalid case 1: stray '}' encountered, example: "ABCD EFGH {name} IJKL}", "Hello {vv}}", "HELLO {0} WORLD}"
if curChar == '}' and nextChar != '}':
raise ValueError("Single '}' encountered in format string")
# Valid Case 1: Escaping case, we escape "{{ or "}}" to be "{" or "}", example: "{{}}", "{{My Name is {0}}}"
if (curChar == '{' and nextChar == '{') or (curChar == '}' and nextChar == '}'):
if (L<R): # Valid Case 1.1: make sure we are not adding empty string
tokens.append(s[L:R]) # add the string before the escape
tokens.append(curChar) # Valid Case 1.2: add the escape char
L = R+2 # move the left pointer to the next char
R = R+2 # move the right pointer to the next char
continue
# Valid Case 2: Regular command line arg case: example: "ABCD EFGH {} IJKL", "{}", "HELLO {} WORLD"
elif curChar == '{' and nextChar == '}':
if mode is not None and mode != 'auto':
# Invalid case 2: mixing automatic and manual field specifications -- example: "ABCD EFGH {name} IJKL {}", "Hello {vv} {}", "HELLO {0} WORLD {}"
raise ValueError("Cannot switch from manual field numbering to automatic field specification")
mode = 'auto'
if(L<R): # Valid Case 2.1: make sure we are not adding empty string
tokens.append(s[L:R]) # add the string before the special marker for the arg
tokens.append("{"+str(curArg)+"}") # Valid Case 2.2: add the special marker for the arg
curArg+=1 # increment the arg position, this will be used for referencing the arg later
L = R+2 # move the left pointer to the next char
R = R+2 # move the right pointer to the next char
continue
# Valid Case 3: Key-word arg case: example: "ABCD EFGH {name} IJKL", "Hello {vv}", "HELLO {name} WORLD"
elif (curChar == '{'):
if mode is not None and mode != 'manual':
# # Invalid case 2: mixing automatic and manual field specifications -- example: "ABCD EFGH {} IJKL {name}", "Hello {} {1}", "HELLO {} WORLD {name}"
raise ValueError("Cannot switch from automatic field specification to manual field numbering")
mode = 'manual'
if(L<R): # Valid case 3.1: make sure we are not adding empty string
tokens.append(s[L:R]) # add the string before the special marker for the arg
# We look for the end of the keyword
kwL = R # Keyword left pointer
kwR = R+1 # Keyword right pointer
while(kwR<len(s) and s[kwR]!='}'):
if s[kwR] == '{': # Invalid case 3: stray '{' encountered, example: "ABCD EFGH {n{ame} IJKL {", "Hello {vv{}}", "HELLO {0} WOR{LD}"
raise ValueError("Unexpected '{' in field name")
kwR += 1
# Valid case 3.2: We have successfully found the end of the keyword
if kwR<len(s) and s[kwR] == '}':
tokens.append(s[kwL:kwR+1]) # add the special marker for the arg
L = kwR+1
R = kwR+1
# Invalid case 4: We didn't find the end of the keyword, throw error
else:
raise ValueError("Expected '}' before end of string")
continue
R = R+1
# Valid case 4: We have reached the end of the string, add the remaining string to the tokens
if L<R:
tokens.append(s[L:R])
# print(tokens)
return tokens
tokens = tokenizeString(self)
argMap = {}
for i, a in enumerate(args):
argMap[str(i)] = a
final_tokens = []
for t in tokens:
if t[0] == '{' and t[-1] == '}':
key = t[1:-1]
argMapVal = argMap.get(key, None)
kwargsVal = kwargs.get(key, None)
if argMapVal is None and kwargsVal is None:
raise ValueError("No arg found for token: "+t)
elif argMapVal is not None:
final_tokens.append(str(argMapVal))
else:
final_tokens.append(str(kwargsVal))
else:
final_tokens.append(t)
return ''.join(final_tokens)
str.format = __format_string
del __format_string
def help(obj):
if hasattr(obj, '__func__'):

View File

@ -52,3 +52,6 @@ final = lambda x: x
# exhaustiveness checking
assert_never = lambda x: x
TypedDict = dict
NotRequired = _PLACEHOLDER

View File

@ -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]);
@ -175,13 +213,45 @@ 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");
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());
return true;
}
static bool float__mod__(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_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());
@ -194,13 +264,31 @@ 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());
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");
double q, r;
cpy11__float_div_mod(lhs, rhs, &q, &r);
py_Ref p = py_newtuple(py_retval(), 2);
py_newfloat(&p[0], q);
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 +623,11 @@ void pk_number__register() {
py_bindmagic(tp_int, __divmod__, int__divmod__);
// 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__);
// int.__invert__ & int.<BITWISE OP>
py_bindmagic(tp_int, __invert__, int__invert__);

View File

@ -1,9 +1,11 @@
#include "pocketpy/common/str.h"
#include "pocketpy/objects/base.h"
#include "pocketpy/pocketpy.h"
#include "pocketpy/objects/object.h"
#include "pocketpy/interpreter/vm.h"
#include "pocketpy/common/sstream.h"
#include <stdbool.h>
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;
}

File diff suppressed because one or more lines are too long

View File

@ -11,8 +11,6 @@
#include <assert.h>
#include <time.h>
static bool stack_format_object(VM* self, c11_sv spec);
#define DISPATCH() \
do { \
frame->ip++; \
@ -118,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;
@ -1191,8 +1188,9 @@ __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 = pk_format_object(self, TOP(), py_tosv(spec));
if(!ok) goto __ERROR;
py_assign(TOP(), py_retval());
DISPATCH();
}
default: c11__unreachable();
@ -1298,10 +1296,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) {
bool pk_format_object(VM* self, py_Ref val, c11_sv spec) {
// format TOS via `spec` inplace
// spec: '!r:.2f', '.2f'
py_StackRef val = TOP();
// spec: '!r:.2f', ':.2f', '.2f'
if(spec.size == 0) return py_str(val);
if(spec.data[0] == '!') {
@ -1442,7 +1439,7 @@ static bool stack_format_object(VM* self, c11_sv spec) {
c11_string__delete(body);
// inplace update
c11_sbuf__py_submit(&buf, val);
c11_sbuf__py_submit(&buf, py_retval());
return true;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
}

View File

@ -113,4 +113,17 @@ 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)
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)
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)

View File

@ -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"

View File

@ -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'