blueloveTH 2 лет назад
Родитель
Сommit
dcb784a7a8
9 измененных файлов с 420 добавлено и 13 удалено
  1. 1 1
      amalgamate.py
  2. 9 0
      include/pocketpy/array2d.h
  3. 1 0
      include/pocketpy/pocketpy.h
  4. 1 0
      include/pocketpy/vm.h
  5. 91 0
      include/typings/array2d.pyi
  6. 201 0
      src/array2d.cpp
  7. 4 11
      src/pocketpy.cpp
  8. 11 1
      src/vm.cpp
  9. 101 0
      tests/80_array2d.py

+ 1 - 1
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"]
 ]
 

+ 9 - 0
include/pocketpy/array2d.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include "bindings.h"
+
+namespace pkpy {
+
+void add_module_array2d(VM* vm);
+
+} // namespace pkpy

+ 1 - 0
include/pocketpy/pocketpy.h

@@ -15,4 +15,5 @@
 #include "collections.h"
 #include "csv.h"
 #include "dataclasses.h"
+#include "array2d.h"
 #include "modules.h"

+ 1 - 0
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);

+ 91 - 0
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()

+ 201 - 0
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<Array2d>(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>(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>(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

+ 4 - 11
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);

+ 11 - 1
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<std::string_view>& cpnts){

+ 101 - 0
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