diff --git a/include/pocketpy/collections.h b/include/pocketpy/collections.h index 7920a899..83543a1a 100644 --- a/include/pocketpy/collections.h +++ b/include/pocketpy/collections.h @@ -18,33 +18,37 @@ namespace pkpy // PyDeque members std::deque dequeItems; - int maxlen=-1; - bool bounded=false; + int maxlen=-1; // -1 means unbounded + bool bounded=false; // if true, maxlen is not -1 // PyDeque methods: add, remove, insert, etc. - void appendLeft(PyObject *item); - void append(PyObject *item); - PyObject *popLeft(); - PyObject *pop(); - bool insert(int index, PyObject *item); - bool remove(VM *vm, PyObject *item); + void appendLeft(PyObject *item); // add to the left + void append(PyObject *item); // add to the right + PyObject *popLeft(); // remove from the left + PyObject *pop(); // remove from the right + bool insert(int index, PyObject *item); // insert at index + bool remove(VM *vm, PyObject *item); // remove first occurence of item - void rotate(int n); - void reverse(); - void clear(); + void rotate(int n); // rotate n steps to the right + void reverse();// reverse the deque + void clear(); // clear the deque - int count(VM *vm, PyObject *obj); // vm is needed for the py_equals - int findIndex(VM *vm, PyObject *obj, int startPos, int endPos); // vm is needed for the py_equals + int count(VM *vm, PyObject *obj); // count the number of occurences of obj + int findIndex(VM *vm, PyObject *obj, int startPos, int endPos); // find the index of obj in range starting from startPos and ending at endPos, default range is entire deque + PyObject* getItem(int index); // get item at index + bool setItem(int index, PyObject* item); // set item at index + bool eraseItem(int index);// erase item at index - std::stringstream getRepr(VM *vm); + std::stringstream getRepr(VM *vm); // get the string representation of the deque + int fixIndex(int index); // for internal use only, returns -1 if index is out of range, handles negative indices // Special methods - static void _register(VM *vm, PyObject *mod, PyObject *type); - void _gc_mark() const; // needed for container types + static void _register(VM *vm, PyObject *mod, PyObject *type); // register the type + void _gc_mark() const; // needed for container types, mark all objects in the deque for gc }; void add_module_mycollections(VM *vm); diff --git a/src/collections.cpp b/src/collections.cpp index 8e887a6c..7023b253 100644 --- a/src/collections.cpp +++ b/src/collections.cpp @@ -10,174 +10,128 @@ namespace pkpy Type cls_t = PK_OBJ_GET(Type, args[0]); PyObject *iterable = args[1]; PyObject *maxlen = args[2]; + return vm->heap.gcnew(cls_t, vm, iterable, maxlen); }); - vm->bind(type, "__add__(self, other) -> deque", + // gets the item at the given index, if index is negative, it will be treated as index + len(deque) + // if the index is out of range, IndexError will be thrown --> required for [] operator + vm->bind(type, "__getitem__(self, index) -> PyObject", [](VM *vm, ArgsView args) { - auto _lock = vm->heap.gc_scope_lock(); - - PyObject *newDequeObj = vm->heap.gcnew(PyDeque::_type(vm), vm, vm->None, vm->None); // create the new deque - - PyDeque &newDeque = _CAST(PyDeque &, newDequeObj); PyDeque &self = _CAST(PyDeque &, args[0]); - PyDeque &other = _CAST(PyDeque &, args[1]); + int index = CAST(int, args[1]); - for (auto it = self.dequeItems.begin(); it != self.dequeItems.end(); ++it) + PyObject *item = self.getItem(index); + if (item == nullptr) { - newDeque.append(*it); + vm->IndexError("deque index out of range"); + return vm->None; } - for (auto it = other.dequeItems.begin(); it != other.dequeItems.end(); ++it) + return item; + }); + + // sets the item at the given index, if index is negative, it will be treated as index + len(deque) + // if the index is out of range, IndexError will be thrown --> required for [] operator + vm->bind(type, "__setitem__(self, index, newValue) -> None", + [](VM *vm, ArgsView args) + { + PyDeque &self = _CAST(PyDeque &, args[0]); + int index = CAST(int, args[1]); + PyObject *newValue = args[2]; + + bool success = self.setItem(index, newValue); + if (!success) { - newDeque.append(*it); + vm->IndexError("deque index out of range"); + return vm->None; } - return newDequeObj; + return vm->None; }); - vm->bind(type, "__bool__(self) -> bool", - [](VM *vm, ArgsView args) - { - PyDeque &self = _CAST(PyDeque &, args[0]); - return VAR(!self.dequeItems.empty()); - }); - - vm->bind(type, "__class__(self) -> deque()", // creates a new empty deque - [](VM *vm, ArgsView args) - { - return vm->heap.gcnew(PyDeque::_type(vm), vm, vm->None, vm->None); - }); - - vm->bind(type, "__contains__(self, obj) -> bool", - [](VM *vm, ArgsView args) - { - PyDeque &self = _CAST(PyDeque &, args[0]); - PyObject *obj = args[1]; - bool found = self.findIndex(vm, obj, -1, -1) != -1; - return VAR(found); - }); - - vm->bind(type, "__copy__(self)", // shallow copy - [](VM *vm, ArgsView args) - { - auto _lock = vm->heap.gc_scope_lock(); // locking the heap - PyDeque &self = _CAST(PyDeque &, args[0]); - PyObject *newDequeObj = vm->heap.gcnew(PyDeque::_type(vm), vm, vm->None, vm->None); // create the empty deque - PyDeque &newDeque = _CAST(PyDeque &, newDequeObj); - - for (auto it = self.dequeItems.begin(); it != self.dequeItems.end(); ++it) - { - newDeque.append(*it); - } - return newDequeObj; - }); + // erases the item at the given index, if index is negative, it will be treated as index + len(deque) + // if the index is out of range, IndexError will be thrown --> required for [] operator vm->bind(type, "__delitem__(self, index) -> None", - [](VM *vm, ArgsView args) - { - // TODO - return vm->None; - }); - - vm->bind(type, "__eq__(self, other) -> bool", - [](VM *vm, ArgsView args) - { - //TODO - return vm->None; - }); - - vm->bind(type, "__getitem__(self, idx) -> PyObject*", - [](VM *vm, ArgsView args) - { - //TODO - return VM->None; - } - ); - - // TODO: __iadd__, __imul__, __reversed__, __str__, __mul__, __rmul__, __setitem__, - - + [](VM *vm, ArgsView args) + { + PyDeque &self = _CAST(PyDeque &, args[0]); + int index = CAST(int, args[1]); + bool success = self.eraseItem(index); + if (!success) + { + vm->IndexError("deque index out of range"); + return vm->None; + } + return vm->None; + }); + // returns the length of the deque vm->bind(type, "__len__(self) -> int", [](VM *vm, ArgsView args) { PyDeque &self = _CAST(PyDeque &, args[0]); + return VAR(self.dequeItems.size()); }); - vm->bind(type, "__repr__(self) -> str", - [](VM *vm, ArgsView args) - { - PyDeque &self = _CAST(PyDeque &, args[0]); - std::stringstream ss = self.getRepr(vm); - return VAR(ss.str()); - }); - + // returns an iterator for the deque vm->bind(type, "__iter__(self) -> deque_iterator", [](VM *vm, ArgsView args) { PyDeque &self = _CAST(PyDeque &, args[0]); - return vm->heap.gcnew(PyDequeIter::_type(vm), args[0], self.dequeItems.begin(), self.dequeItems.end()); - }); - - vm->bind(type, "append(self, item) -> None", + return vm->heap.gcnew( + PyDequeIter::_type(vm), args[0], + self.dequeItems.begin(), self.dequeItems.end()); + }); + + // returns a string representation of the deque + vm->bind(type, "__repr__(self) -> str", [](VM *vm, ArgsView args) { PyDeque &self = _CAST(PyDeque &, args[0]); - PyObject *item = args[1]; - self.append(item); - return vm->None; + + std::stringstream ss = self.getRepr(vm); + return VAR(ss.str()); }); - vm->bind(type, "appendleft(self, item) -> None", + // enables comparison between two deques, == and != are supported + vm->bind(type, "__eq__(self, other) -> bool", [](VM *vm, ArgsView args) { PyDeque &self = _CAST(PyDeque &, args[0]); - PyObject *item = args[1]; - self.appendLeft(item); - return vm->None; + PyDeque &other = _CAST(PyDeque &, args[1]); + + if (self.dequeItems.size() != other.dequeItems.size()) + return VAR(false); + for (int i = 0; i < self.dequeItems.size(); i++) + { + if (!vm->py_equals(self.dequeItems[i], other.dequeItems[i])) + return VAR(false); + } + return VAR(true); }); + // clear the deque vm->bind(type, "clear(self) -> None", [](VM *vm, ArgsView args) { PyDeque &self = _CAST(PyDeque &, args[0]); + self.clear(); return vm->None; }); - vm->bind(type, "copy(self) -> deque", - [](VM *vm, ArgsView args) - { - auto _lock = vm->heap.gc_scope_lock(); // locking the heap - PyDeque &self = _CAST(PyDeque &, args[0]); - // shallow copy - PyObject *newDequeObj = vm->heap.gcnew(PyDeque::_type(vm), vm, vm->None, vm->None); // create the empty deque - PyDeque &newDeque = _CAST(PyDeque &, newDequeObj); - - for (auto it = self.dequeItems.begin(); it != self.dequeItems.end(); ++it) - { - newDeque.append(*it); - } - return newDequeObj; - }); - - vm->bind(type, "count(self, obj) -> int", - [](VM *vm, ArgsView args) - { - PyDeque &self = _CAST(PyDeque &, args[0]); - PyObject *obj = args[1]; - int cnt = self.count(vm, obj); - return VAR(cnt); - }); - + // extend the deque with the given iterable vm->bind(type, "extend(self, iterable) -> None", [](VM *vm, ArgsView args) { - auto _lock = vm->heap.gc_scope_lock(); + auto _lock = vm->heap.gc_scope_lock(); // locking the heap + PyDeque &self = _CAST(PyDeque &, args[0]); PyObject *it = vm->py_iter(args[1]); // strong ref + PyObject *obj = vm->py_next(it); while (obj != vm->StopIteration) { @@ -187,12 +141,92 @@ namespace pkpy return vm->None; }); + // append at the end of the deque + vm->bind(type, "append(self, item) -> None", + [](VM *vm, ArgsView args) + { + PyDeque &self = _CAST(PyDeque &, args[0]); + PyObject *item = args[1]; + + self.append(item); + return vm->None; + }); + + // append at the beginning of the deque + vm->bind(type, "appendleft(self, item) -> None", + [](VM *vm, ArgsView args) + { + PyDeque &self = _CAST(PyDeque &, args[0]); + PyObject *item = args[1]; + + self.appendLeft(item); + return vm->None; + }); + + // pop from the end of the deque + vm->bind(type, "pop(self) -> PyObject", + [](VM *vm, ArgsView args) + { + PyDeque &self = _CAST(PyDeque &, args[0]); + + if (self.dequeItems.empty()) + { + vm->IndexError("pop from an empty deque"); + return vm->None; + } + return self.pop(); + }); + + // pop from the beginning of the deque + vm->bind(type, "popleft(self) -> PyObject", + [](VM *vm, ArgsView args) + { + PyDeque &self = _CAST(PyDeque &, args[0]); + + if (self.dequeItems.empty()) + { + vm->IndexError("pop from an empty deque"); + return vm->None; + } + return self.popLeft(); + }); + + // shallow copy of the deque + vm->bind(type, "copy(self) -> deque", + [](VM *vm, ArgsView args) + { + auto _lock = vm->heap.gc_scope_lock(); // locking the heap + PyDeque &self = _CAST(PyDeque &, args[0]); + + // shallow copy + PyObject *newDequeObj = vm->heap.gcnew(PyDeque::_type(vm), vm, vm->None, vm->None); // create the empty deque + PyDeque &newDeque = _CAST(PyDeque &, newDequeObj); // cast it to PyDeque so we can use its methods + for (auto it = self.dequeItems.begin(); it != self.dequeItems.end(); ++it) + { + newDeque.append(*it); // append each item to the new deque + } + return newDequeObj; + }); + + //NEW: counts the number of occurences of the given object in the deque + vm->bind(type, "count(self, obj) -> int", + [](VM *vm, ArgsView args) + { + PyDeque &self = _CAST(PyDeque &, args[0]); + PyObject *obj = args[1]; + + return VAR(self.count(vm, obj)); + }); + + // NEW: extends the deque from the left vm->bind(type, "extendleft(self, iterable) -> None", [](VM *vm, ArgsView args) { auto _lock = vm->heap.gc_scope_lock(); + PyDeque &self = _CAST(PyDeque &, args[0]); PyObject *it = vm->py_iter(args[1]); // strong ref + PyObject *obj = vm->py_next(it); while (obj != vm->StopIteration) { @@ -202,22 +236,27 @@ namespace pkpy return vm->None; }); + // NEW: returns the index of the given object in the deque vm->bind(type, "index(self, obj, start=-1, stop=-1) -> int", [](VM *vm, ArgsView args) { // Return the position of x in the deque (at or after index start and before index stop). Returns the first match or raises ValueError if not found. PyDeque &self = _CAST(PyDeque &, args[0]); PyObject *obj = args[1]; + int start = CAST(int, args[2]); int stop = CAST(int, args[3]); int idx = self.findIndex(vm, obj, start, stop); if (idx == -1) - vm->ValueError(_CAST(Str &, vm->py_repr(obj)) + " is not in list"); - + { + vm->ValueError(_CAST(Str &, vm->py_repr(obj)) + " is not in deque"); + return vm->None; + } return VAR(idx); }); + // NEW: inserts the given object at the given index vm->bind(type, "insert(self, index, obj) -> None", [](VM *vm, ArgsView args) { @@ -225,57 +264,50 @@ namespace pkpy int index = CAST(int, args[1]); PyObject *obj = args[2]; - // TODO: HANDLE MAX SIZE CASE LATER -> Throw IndexError - + if (self.bounded && self.dequeItems.size() == self.maxlen) + { + vm->ValueError("deque already at its maximum size"); + return vm->None; + } self.insert(index, obj); return vm->None; }); - vm->bind(type, "pop(self) -> PyObject", - [](VM *vm, ArgsView args) - { - PyDeque &self = _CAST(PyDeque &, args[0]); - return self.pop(); - }); - - vm->bind(type, "popleft(self) -> PyObject", - [](VM *vm, ArgsView args) - { - PyDeque &self = _CAST(PyDeque &, args[0]); - return self.popLeft(); - }); - + // NEW: removes the first occurence of the given object from the deque vm->bind(type, "remove(self, obj) -> None", [](VM *vm, ArgsView args) { PyDeque &self = _CAST(PyDeque &, args[0]); PyObject *obj = args[1]; - bool removed = self.remove(vm, obj); + bool removed = self.remove(vm, obj); if (!removed) vm->ValueError(_CAST(Str &, vm->py_repr(obj)) + " is not in list"); - return vm->None; }); + // NEW: reverses the deque vm->bind(type, "reverse(self) -> None", [](VM *vm, ArgsView args) { PyDeque &self = _CAST(PyDeque &, args[0]); + self.reverse(); return vm->None; }); + // NEW: rotates the deque by n steps vm->bind(type, "rotate(self, n=1) -> None", [](VM *vm, ArgsView args) { PyDeque &self = _CAST(PyDeque &, args[0]); int n = CAST(int, args[1]); + self.rotate(n); return vm->None; }); - // getter and setter of property `maxlen` + // NEW: getter and setter of property `maxlen` vm->bind_property( type, "maxlen: int", [](VM *vm, ArgsView args) @@ -291,14 +323,19 @@ namespace pkpy vm->AttributeError("attribute 'maxlen' of 'collections.deque' objects is not writable"); return vm->None; }); - } + /// @brief initializes a new PyDeque object + /// @param vm required for the py_iter and max_len casting + /// @param iterable a list-like object to initialize the deque with + /// @param maxlen the maximum length of the deque, makes the deque bounded PyDeque::PyDeque(VM *vm, PyObject *iterable, PyObject *maxlen) { if (maxlen != vm->None) { this->maxlen = CAST(int, maxlen); + if (this->maxlen < 0) + vm->ValueError("maxlen must be non-negative"); this->bounded = true; } else @@ -309,7 +346,7 @@ namespace pkpy if (iterable != vm->None) { - auto _lock = vm->heap.gc_scope_lock(); + auto _lock = vm->heap.gc_scope_lock(); // locking the heap PyObject *it = vm->py_iter(iterable); // strong ref PyObject *obj = vm->py_next(it); while (obj != vm->StopIteration) @@ -319,15 +356,44 @@ namespace pkpy } } } - + /// @brief returns the item at the given index, if index is negative, it will be treated as index + len(deque) + /// @param index the index of the item to get + /// @return PyObject* the item at the given index, nullptr if the index is out of range + PyObject *PyDeque::getItem(int index) + { + index = this->fixIndex(index); + if (index == -1) return nullptr; + return this->dequeItems.at(index); + } + /// @brief sets the item at the given index, if index is negative, it will be treated as index + len(deque) + /// @param index the index of the item to set + /// @param item the newValue for the item at the given index + /// @return true if the item was set successfully, false if the index is out of range + bool PyDeque::setItem(int index, PyObject *item) + { + index = this->fixIndex(index); + if (index == -1) return false; + this->dequeItems.at(index) = item; + return true; + } + /// @brief erases the item at the given index, if index is negative, it will be treated as index + len(deque) + /// @param index the index of the item to erase + /// @return true if the item was erased successfully, false if the index is out of range + bool PyDeque::eraseItem(int index) + { + index = this->fixIndex(index); + if (index == -1) return false; + this->dequeItems.erase(this->dequeItems.begin() + index); + return true; + } + /// @brief rotates the deque by n steps + /// @param n the number of steps to rotate the deque by, can be -ve for left rotation, +ve for right rotation, can be out of range void PyDeque::rotate(int n) { int direction = n > 0 ? 1 : -1; int sz = this->dequeItems.size(); - n = abs(n); n = n % sz; // make sure n is in range - for (int i = 0; i < n; i++) { if (direction == 1) @@ -344,52 +410,48 @@ namespace pkpy } } } - - bool PyDeque::remove(VM *vm, PyObject *item) + /// @brief removes the first occurence of the given item + /// @param vm is needed for the py_equals + /// @param item the item to remove + /// @return true if the item was removed successfully, false if the item was not found + bool PyDeque::remove(VM *vm, PyObject *item) // removes the first occurence of the given item { for (auto it = this->dequeItems.begin(); it != this->dequeItems.end(); ++it) - { if (vm->py_equals((*it), item)) { this->dequeItems.erase(it); return true; } - } return false; } - + /// @brief inserts the given item at the given index, if index is negative, it will be treated as index + len(deque) + /// @param index index at which the item will be inserted + /// @param item the item to insert + /// @return true if the item was inserted successfully, false if the index is out of range bool PyDeque::insert(int index, PyObject *item) { + if (index < 0) + index = this->dequeItems.size() + index; // adjust for the -ve indexing if (index < 0) this->dequeItems.push_front(item); else if (index >= this->dequeItems.size()) this->dequeItems.push_back(item); else this->dequeItems.insert((this->dequeItems.begin() + index), item); - return true; } - - void PyDeque::_gc_mark() const - { - for (PyObject *obj : this->dequeItems) - { - PK_OBJ_MARK(obj); - } - } - + /// @brief returns a string representation of the deque + /// @param vm is needed for the py_repr and String casting + /// @return std::stringstream the string representation of the deque std::stringstream PyDeque::getRepr(VM *vm) { std::stringstream ss; - int sz = this->dequeItems.size(); ss << "deque(["; for (auto it = this->dequeItems.begin(); it != this->dequeItems.end(); ++it) { ss << CAST(Str &, vm->py_repr(*it)); if (it != this->dequeItems.end() - 1) - { ss << ", "; - } } if (this->bounded) ss << "], maxlen=" << this->maxlen << ")"; @@ -397,95 +459,124 @@ namespace pkpy ss << "])"; return ss; } - + /// @brief returns the index of the given object in the deque, can search in a range + /// @param vm is needed for the py_equals + /// @param obj the object to search for + /// @param startPos start position of the search + /// @param endPos end position of the search + /// @return int the index of the given object in the deque, -1 if not found int PyDeque::findIndex(VM *vm, PyObject *obj, int startPos = -1, int endPos = -1) { - if (startPos == -1) startPos = 0; if (endPos == -1) endPos = this->dequeItems.size(); - if (!(startPos <= endPos)) - { - throw std::runtime_error("startPos<=endPos"); - } - int dequeSize = this->dequeItems.size(); - - for (int i = startPos; i < dequeSize && i < endPos; i++) - { + return -1; // invalid range + int loopSize = min(this->dequeItems.size(), endPos); + for (int i = startPos; i < loopSize; i++) if (vm->py_equals(this->dequeItems[i], obj)) - { return i; - } - } return -1; } - + /// @brief reverses the deque void PyDeque::reverse() { + if (this->dequeItems.empty() || this->dequeItems.size() == 1) + return; // handle trivial cases int sz = this->dequeItems.size(); for (int i = 0; i < sz / 2; i++) { PyObject *tmp = this->dequeItems[i]; - this->dequeItems[i] = this->dequeItems[sz - i - 1]; + this->dequeItems[i] = this->dequeItems[sz - i - 1]; // swapping this->dequeItems[sz - i - 1] = tmp; } } - + /// @brief appends the given item to the beginning of the deque + /// @param item the item to append void PyDeque::appendLeft(PyObject *item) { + if (this->bounded){ // handle bounded case + if(this->maxlen == 0) return; // bounded and maxlen is 0, so we can't append + else if (this->dequeItems.size() == this->maxlen) + this->dequeItems.pop_back(); // remove the last item + } this->dequeItems.emplace_front(item); } + /// @brief appends the given item to the end of the deque + /// @param item the item to append void PyDeque::append(PyObject *item) { + if(this->bounded){ // handle bounded case + if(this->maxlen == 0) return; // bounded and maxlen is 0, so we can't append + else if (this->dequeItems.size() == this->maxlen) + this->dequeItems.pop_front(); // remove the first item + } this->dequeItems.emplace_back(item); } - + /// @brief pops the first item from the deque, i.e. beginning of the deque + /// @return PyObject* the popped item PyObject *PyDeque::popLeft() { if (this->dequeItems.empty()) - { - throw std::runtime_error("pop from an empty deque"); - } + throw std::runtime_error("pop from an empty deque");//shouldn't happen + PyObject *obj = this->dequeItems.front(); this->dequeItems.pop_front(); return obj; } - + /// @brief pops the last item from the deque, i.e. end of the deque + /// @return PyObject* the popped item PyObject *PyDeque::pop() { if (this->dequeItems.empty()) - { - throw std::runtime_error("pop from an empty deque"); - } + throw std::runtime_error("pop from an empty deque"); //shouldn't happen + PyObject *obj = this->dequeItems.back(); this->dequeItems.pop_back(); return obj; } - + /// @brief counts the number of occurences of the given object in the deque + /// @param vm is needed for the py_equals + /// @param obj the object to search for + /// @return int the number of occurences of the given object in the deque int PyDeque::count(VM *vm, PyObject *obj) { int cnt = 0; - for (auto it = this->dequeItems.begin(); it != this->dequeItems.end(); ++it) - { if (vm->py_equals((*it), obj)) - { cnt++; - } - } return cnt; } - + /// @brief clears the deque void PyDeque::clear() { this->dequeItems.clear(); } - + /// @brief fixes the given index, if index is negative, it will be treated as index + len(deque) + /// @param index the index to fix + /// @return int the fixed index, -1 if the index is out of range + int PyDeque::fixIndex(int index) + { + if (index < 0) + index = this->dequeItems.size() + index; + if (index < 0 || index >= this->dequeItems.size()) + return -1; + return index; + } + /// @brief marks the deque items for garbage collection + void PyDeque::_gc_mark() const + { + for (PyObject *obj : this->dequeItems) + { + PK_OBJ_MARK(obj); + } + } + /// @brief registers the PyDeque class + /// @param vm is needed for the new_module and register_class void add_module_mycollections(VM *vm) { - PyObject *mycollections = vm->new_module("mycollections"); + PyObject *mycollections = vm->new_module("mycollections"); // TODO: change this to collections PyDeque::register_class(vm, mycollections); } } // namespace pkpypkpy