From dcb784a7a86f25be88cf32b0cde0d2ee53d0b0a9 Mon Sep 17 00:00:00 2001 From: blueloveTH Date: Tue, 6 Feb 2024 23:25:40 +0800 Subject: [PATCH] add `array2d` module --- amalgamate.py | 2 +- include/pocketpy/array2d.h | 9 ++ include/pocketpy/pocketpy.h | 1 + include/pocketpy/vm.h | 1 + include/typings/array2d.pyi | 91 ++++++++++++++++ src/array2d.cpp | 201 ++++++++++++++++++++++++++++++++++++ src/pocketpy.cpp | 15 +-- src/vm.cpp | 12 ++- tests/80_array2d.py | 101 ++++++++++++++++++ 9 files changed, 420 insertions(+), 13 deletions(-) create mode 100644 include/pocketpy/array2d.h create mode 100644 include/typings/array2d.pyi create mode 100644 src/array2d.cpp create mode 100644 tests/80_array2d.py diff --git a/amalgamate.py b/amalgamate.py index 274753c1..6df868b4 100644 --- a/amalgamate.py +++ b/amalgamate.py @@ -9,7 +9,7 @@ pipeline = [ ["config.h", "export.h", "common.h", "memory.h", "vector.h", "str.h", "tuplelist.h", "namedict.h", "error.h"], ["obj.h", "dict.h", "codeobject.h", "frame.h"], ["gc.h", "vm.h", "ceval.h", "lexer.h", "expr.h", "compiler.h", "repl.h"], - ["_generated.h", "cffi.h", "bindings.h", "iter.h", "base64.h", "csv.h", "collections.h", "dataclasses.h", "random.h", "linalg.h", "easing.h", "io.h", "modules.h"], + ["_generated.h", "cffi.h", "bindings.h", "iter.h", "base64.h", "csv.h", "collections.h", "array2d.h", "dataclasses.h", "random.h", "linalg.h", "easing.h", "io.h", "modules.h"], ["pocketpy.h", "pocketpy_c.h"] ] diff --git a/include/pocketpy/array2d.h b/include/pocketpy/array2d.h new file mode 100644 index 00000000..f79cbc45 --- /dev/null +++ b/include/pocketpy/array2d.h @@ -0,0 +1,9 @@ +#pragma once + +#include "bindings.h" + +namespace pkpy { + +void add_module_array2d(VM* vm); + +} // namespace pkpy \ No newline at end of file diff --git a/include/pocketpy/pocketpy.h b/include/pocketpy/pocketpy.h index 6ea88c21..89e11d07 100644 --- a/include/pocketpy/pocketpy.h +++ b/include/pocketpy/pocketpy.h @@ -15,4 +15,5 @@ #include "collections.h" #include "csv.h" #include "dataclasses.h" +#include "array2d.h" #include "modules.h" diff --git a/include/pocketpy/vm.h b/include/pocketpy/vm.h index 60a377fd..f1a87ea0 100644 --- a/include/pocketpy/vm.h +++ b/include/pocketpy/vm.h @@ -325,6 +325,7 @@ public: int normalized_index(int index, int size); PyObject* py_next(PyObject* obj); + bool py_callable(PyObject* obj); /***** Error Reporter *****/ void _raise(bool re_raise=false); diff --git a/include/typings/array2d.pyi b/include/typings/array2d.pyi new file mode 100644 index 00000000..95ef98bc --- /dev/null +++ b/include/typings/array2d.pyi @@ -0,0 +1,91 @@ +from typing import Callable, Any, Generic, TypeVar + +T = TypeVar('T') + +class array2d(Generic[T]): + data: list[T] # not available in native module + + def __init__(self, n_cols: int, n_rows: int, default=None): + self.n_cols = n_cols + self.n_rows = n_rows + if callable(default): + self.data = [default() for _ in range(n_cols * n_rows)] + else: + self.data = [default] * n_cols * n_rows + + @property + def width(self) -> int: + return self.n_cols + + @property + def height(self) -> int: + return self.n_rows + + @property + def numel(self) -> int: + return self.n_cols * self.n_rows + + def is_valid(self, col: int, row: int) -> bool: + return 0 <= col < self.n_cols and 0 <= row < self.n_rows + + def get(self, col: int, row: int, default=None): + if not self.is_valid(col, row): + return default + return self.data[row * self.n_cols + col] + + def __getitem__(self, index: tuple[int, int]): + col, row = index + if not self.is_valid(col, row): + raise IndexError(f'({col}, {row}) is not a valid index for {self!r}') + return self.data[row * self.n_cols + col] + + def __setitem__(self, index: tuple[int, int], value: T): + col, row = index + if not self.is_valid(col, row): + raise IndexError(f'({col}, {row}) is not a valid index for {self!r}') + self.data[row * self.n_cols + col] = value + + def __iter__(self) -> list[list['T']]: + for row in range(self.n_rows): + yield [self[col, row] for col in range(self.n_cols)] + + def __len__(self): + return self.n_rows + + def __eq__(self, other: 'array2d') -> bool: + if not isinstance(other, array2d): + return NotImplemented + for i in range(self.numel): + if self.data[i] != other.data[i]: + return False + return True + + def __ne__(self, other: 'array2d') -> bool: + return not self.__eq__(other) + + def __repr__(self): + return f'array2d({self.n_cols}, {self.n_rows})' + + def map(self, f: Callable[[T], Any]) -> 'array2d': + new_a: array2d = array2d(self.n_cols, self.n_rows) + for i in range(self.n_cols * self.n_rows): + new_a.data[i] = f(self.data[i]) + return new_a + + def copy(self) -> 'array2d[T]': + new_a: array2d[T] = array2d(self.n_cols, self.n_rows) + new_a.data = self.data.copy() + return new_a + + def fill_(self, value: T) -> None: + for i in range(self.n_cols * self.n_rows): + self.data[i] = value + + def apply_(self, f: Callable[[T], T]) -> None: + for i in range(self.n_cols * self.n_rows): + self.data[i] = f(self.data[i]) + + def copy_(self, other: 'array2d[T]') -> None: + self.n_cols = other.n_cols + self.n_rows = other.n_rows + self.data = other.data.copy() diff --git a/src/array2d.cpp b/src/array2d.cpp new file mode 100644 index 00000000..a07206b6 --- /dev/null +++ b/src/array2d.cpp @@ -0,0 +1,201 @@ +#include "pocketpy/array2d.h" + +namespace pkpy{ + +struct Array2d{ + PK_ALWAYS_PASS_BY_POINTER(Array2d) + PY_CLASS(Array2d, array2d, array2d) + + PyObject** data; + int n_cols; + int n_rows; + int numel; + + Array2d(){ + data = nullptr; + n_cols = 0; + n_rows = 0; + numel = 0; + } + + Array2d* _() { return this; } + + void init(int n_cols, int n_rows){ + this->n_cols = n_cols; + this->n_rows = n_rows; + this->numel = n_cols * n_rows; + this->data = new PyObject*[numel]; + } + + bool is_valid(int col, int row) const{ + return 0 <= col && col < n_cols && 0 <= row && row < n_rows; + } + + static void _register(VM* vm, PyObject* mod, PyObject* type){ + vm->bind(type, "__new__(cls, *args, **kwargs)", [](VM* vm, ArgsView args){ + Type cls = PK_OBJ_GET(Type, args[0]); + return vm->heap.gcnew(cls); + }); + + vm->bind(type, "__init__(self, n_cols: int, n_rows: int, default=None)", [](VM* vm, ArgsView args){ + Array2d& self = PK_OBJ_GET(Array2d, args[0]); + int n_cols = CAST(int, args[1]); + int n_rows = CAST(int, args[2]); + if(n_cols <= 0 || n_rows <= 0){ + vm->ValueError("n_cols and n_rows must be positive integers"); + } + self.init(n_cols, n_rows); + if(vm->py_callable(args[3])){ + for(int i = 0; i < self.numel; i++) self.data[i] = vm->call(args[3]); + }else{ + for(int i = 0; i < self.numel; i++) self.data[i] = args[3]; + } + return vm->None; + }); + + PY_READONLY_FIELD(Array2d, "n_cols", _, n_cols); + PY_READONLY_FIELD(Array2d, "n_rows", _, n_rows); + PY_READONLY_FIELD(Array2d, "width", _, n_cols); + PY_READONLY_FIELD(Array2d, "height", _, n_rows); + PY_READONLY_FIELD(Array2d, "numel", _, numel); + + vm->bind(type, "is_valid(self, col: int, row: int)", [](VM* vm, ArgsView args){ + Array2d& self = PK_OBJ_GET(Array2d, args[0]); + int col = CAST(int, args[1]); + int row = CAST(int, args[2]); + return VAR(self.is_valid(col, row)); + }); + + vm->bind(type, "get(self, col: int, row: int, default=None)", [](VM* vm, ArgsView args){ + Array2d& self = PK_OBJ_GET(Array2d, args[0]); + int col = CAST(int, args[1]); + int row = CAST(int, args[2]); + if(!self.is_valid(col, row)) return args[3]; + return self.data[row * self.n_cols + col]; + }); + + vm->bind__getitem__(PK_OBJ_GET(Type, type), [](VM* vm, PyObject* _0, PyObject* _1){ + Array2d& self = PK_OBJ_GET(Array2d, _0); + const Tuple& xy = CAST(Tuple&, _1); + int col = CAST(int, xy[0]); + int row = CAST(int, xy[1]); + if(!self.is_valid(col, row)){ + vm->IndexError(_S('(', col, ", ", row, ')', " is not a valid index for array2d(", self.n_cols, ", ", self.n_rows, ')')); + } + return self.data[row * self.n_cols + col]; + }); + + vm->bind__setitem__(PK_OBJ_GET(Type, type), [](VM* vm, PyObject* _0, PyObject* _1, PyObject* _2){ + Array2d& self = PK_OBJ_GET(Array2d, _0); + const Tuple& xy = CAST(Tuple&, _1); + int col = CAST(int, xy[0]); + int row = CAST(int, xy[1]); + if(!self.is_valid(col, row)){ + vm->IndexError(_S('(', col, ", ", row, ')', " is not a valid index for array2d(", self.n_cols, ", ", self.n_rows, ')')); + } + self.data[row * self.n_cols + col] = _2; + }); + + vm->bind__iter__(PK_OBJ_GET(Type, type), [](VM* vm, PyObject* _0){ + Array2d& self = PK_OBJ_GET(Array2d, _0); + List t(self.n_rows); + List row(self.n_cols); + for(int i = 0; i < self.n_rows; i++){ + for(int j = 0; j < self.n_cols; j++){ + row[j] = self.data[i * self.n_cols + j]; + } + t[i] = VAR(row); // copy + } + return vm->py_iter(VAR(std::move(t))); + }); + + vm->bind__len__(PK_OBJ_GET(Type, type), [](VM* vm, PyObject* _0){ + Array2d& self = PK_OBJ_GET(Array2d, _0); + return (i64)self.n_rows; + }); + + vm->bind__repr__(PK_OBJ_GET(Type, type), [](VM* vm, PyObject* _0){ + Array2d& self = PK_OBJ_GET(Array2d, _0); + return VAR(_S("array2d(", self.n_cols, ", ", self.n_rows, ')')); + }); + + vm->bind(type, "map(self, f)", [](VM* vm, ArgsView args){ + Array2d& self = PK_OBJ_GET(Array2d, args[0]); + PyObject* f = args[1]; + PyObject* new_array_obj = vm->heap.gcnew(Array2d::_type(vm)); + Array2d& new_array = PK_OBJ_GET(Array2d, new_array_obj); + new_array.init(self.n_cols, self.n_rows); + for(int i = 0; i < new_array.numel; i++){ + new_array.data[i] = vm->call(f, self.data[i]); + } + return new_array_obj; + }); + + vm->bind(type, "copy(self)", [](VM* vm, ArgsView args){ + Array2d& self = PK_OBJ_GET(Array2d, args[0]); + PyObject* new_array_obj = vm->heap.gcnew(Array2d::_type(vm)); + Array2d& new_array = PK_OBJ_GET(Array2d, new_array_obj); + new_array.init(self.n_cols, self.n_rows); + for(int i = 0; i < new_array.numel; i++){ + new_array.data[i] = self.data[i]; + } + return new_array_obj; + }); + + vm->bind(type, "fill_(self, value)", [](VM* vm, ArgsView args){ + Array2d& self = PK_OBJ_GET(Array2d, args[0]); + for(int i = 0; i < self.numel; i++){ + self.data[i] = args[1]; + } + return vm->None; + }); + + vm->bind(type, "apply_(self, f)", [](VM* vm, ArgsView args){ + Array2d& self = PK_OBJ_GET(Array2d, args[0]); + PyObject* f = args[1]; + for(int i = 0; i < self.numel; i++){ + self.data[i] = vm->call(f, self.data[i]); + } + return vm->None; + }); + + vm->bind(type, "copy_(self, other)", [](VM* vm, ArgsView args){ + Array2d& self = PK_OBJ_GET(Array2d, args[0]); + Array2d& other = CAST(Array2d&, args[1]); + delete self.data; + self.init(other.n_cols, other.n_rows); + for(int i = 0; i < self.numel; i++){ + self.data[i] = other.data[i]; + } + return vm->None; + }); + + vm->bind__eq__(PK_OBJ_GET(Type, type), [](VM* vm, PyObject* _0, PyObject* _1){ + Array2d& self = PK_OBJ_GET(Array2d, _0); + if(!is_non_tagged_type(_1, Array2d::_type(vm))) return vm->NotImplemented; + Array2d& other = PK_OBJ_GET(Array2d, _1); + if(self.n_cols != other.n_cols || self.n_rows != other.n_rows) return vm->False; + for(int i = 0; i < self.numel; i++){ + if(vm->py_ne(self.data[i], other.data[i])) return vm->False; + } + return vm->True; + }); + } + + void _gc_mark() const{ + for(int i = 0; i < numel; i++) PK_OBJ_MARK(data[i]); + } + + ~Array2d(){ + delete[] data; + } +}; + +void add_module_array2d(VM* vm){ + PyObject* mod = vm->new_module("array2d"); + + Array2d::register_class(vm, mod); +} + + +} // namespace pkpy \ No newline at end of file diff --git a/src/pocketpy.cpp b/src/pocketpy.cpp index 046580ba..363f3e75 100644 --- a/src/pocketpy.cpp +++ b/src/pocketpy.cpp @@ -142,15 +142,7 @@ void init_builtins(VM* _vm) { }); _vm->bind_func<1>(_vm->builtins, "callable", [](VM* vm, ArgsView args) { - Type cls = vm->_tp(args[0]); - switch(cls.index){ - case VM::tp_function.index: return vm->True; - case VM::tp_native_func.index: return vm->True; - case VM::tp_bound_method.index: return vm->True; - case VM::tp_type.index: return vm->True; - } - bool ok = vm->find_name_in_mro(cls, __call__) != nullptr; - return VAR(ok); + return VAR(vm->py_callable(args[0])); }); _vm->bind_func<1>(_vm->builtins, "__import__", [](VM* vm, ArgsView args) { @@ -1509,8 +1501,6 @@ void VM::post_init(){ add_module_random(this); add_module_base64(this); add_module_operator(this); - add_module_csv(this); - add_module_dataclasses(this); for(const char* name: {"this", "functools", "heapq", "bisect", "pickle", "_long", "colorsys", "typing", "datetime", "cmath"}){ _lazy_modules[name] = kPythonLibs[name]; @@ -1533,9 +1523,12 @@ void VM::post_init(){ _import_handler = _default_import_handler; } + add_module_csv(this); + add_module_dataclasses(this); add_module_linalg(this); add_module_easing(this); add_module_collections(this); + add_module_array2d(this); #ifdef PK_USE_CJSON add_module_cjson(this); diff --git a/src/vm.cpp b/src/vm.cpp index 7e2d2b8d..7980b643 100644 --- a/src/vm.cpp +++ b/src/vm.cpp @@ -243,7 +243,6 @@ namespace pkpy{ return false; } - int VM::normalized_index(int index, int size){ if(index < 0) index += size; if(index < 0 || index >= size){ @@ -258,6 +257,17 @@ namespace pkpy{ return call_method(obj, __next__); } + bool VM::py_callable(PyObject* obj){ + Type cls = vm->_tp(obj); + switch(cls.index){ + case VM::tp_function.index: return vm->True; + case VM::tp_native_func.index: return vm->True; + case VM::tp_bound_method.index: return vm->True; + case VM::tp_type.index: return vm->True; + } + return vm->find_name_in_mro(cls, __call__) != nullptr; + } + PyObject* VM::py_import(Str path, bool throw_err){ if(path.empty()) vm->ValueError("empty module name"); static auto f_join = [](const std::vector& cpnts){ diff --git a/tests/80_array2d.py b/tests/80_array2d.py new file mode 100644 index 00000000..946058fd --- /dev/null +++ b/tests/80_array2d.py @@ -0,0 +1,101 @@ +from array2d import array2d + +# test error args for __init__ +try: + a = array2d(0, 0) + exit(0) +except ValueError: + pass + +# test callable constructor +a = array2d(2, 4, default=lambda: 0) + +assert a.width == a.n_cols == 2 +assert a.height == a.n_rows == 4 +assert a.numel == 8 + +# test is_valid +assert a.is_valid(0, 0) +assert a.is_valid(1, 3) +assert not a.is_valid(2, 0) +assert not a.is_valid(0, 4) +assert not a.is_valid(-1, 0) +assert not a.is_valid(0, -1) + +# test get +assert a.get(0, 0) == 0 +assert a.get(1, 3) == 0 +assert a.get(2, 0) is None +assert a.get(0, 4, default='S') == 'S' + +# test __getitem__ +assert a[0, 0] == 0 +assert a[1, 3] == 0 +try: + a[2, 0] + exit(1) +except IndexError: + pass + +# test __setitem__ +a[0, 0] = 5 +assert a[0, 0] == 5 +a[1, 3] = 6 +assert a[1, 3] == 6 +try: + a[0, -1] = 7 + exit(1) +except IndexError: + pass + +# test __iter__ +a_list = [[5, 0], [0, 0], [0, 0], [0, 6]] +assert a_list == list(a) + +# test __len__ +assert len(a) == 4 + +# test __eq__ +x = array2d(2, 4, default=0) +b = array2d(2, 4, default=0) +assert x == b + +b[0, 0] = 1 +assert x != b + +# test __repr__ +assert repr(a) == f'array2d(2, 4)' + +# test map +c = a.map(lambda x: x + 1) +assert list(c) == [[6, 1], [1, 1], [1, 1], [1, 7]] +assert list(a) == [[5, 0], [0, 0], [0, 0], [0, 6]] +assert c.width == c.n_cols == 2 +assert c.height == c.n_rows == 4 +assert c.numel == 8 + +# test copy +d = c.copy() +assert d == c and d is not c + +# test fill_ +d.fill_(-3) +assert d == array2d(2, 4, default=-3) + +# test apply_ +d.apply_(lambda x: x + 3) +assert d == array2d(2, 4, default=0) + +# test copy_ +a.copy_(d) +assert a == d and a is not d + +# test subclass array2d +class A(array2d): + def __init__(self): + super().__init__(2, 4, default=0) + +assert A().width == 2 +assert A().height == 4 +assert A().numel == 8 +assert A().get(0, 0, default=2) == 0