From 6e75c37fe9b54a1be5761395656f7936af0acf0b Mon Sep 17 00:00:00 2001 From: Brook Warner Jensen Date: Thu, 10 Dec 2020 12:45:10 -0800 Subject: [PATCH 1/9] new restorable collection base class --- inkcpp/CMakeLists.txt | 5 + inkcpp/collections/restorable.cpp | 0 inkcpp/collections/restorable.h | 218 ++++++++++++++++++++++ inkcpp_test/CMakeLists.txt | 1 + inkcpp_test/Restorable.cpp | 294 ++++++++++++++++++++++++++++++ 5 files changed, 518 insertions(+) create mode 100644 inkcpp/collections/restorable.cpp create mode 100644 inkcpp/collections/restorable.h create mode 100644 inkcpp_test/Restorable.cpp diff --git a/inkcpp/CMakeLists.txt b/inkcpp/CMakeLists.txt index c159f10b..7ea88661 100644 --- a/inkcpp/CMakeLists.txt +++ b/inkcpp/CMakeLists.txt @@ -1,4 +1,8 @@ +set_property(GLOBAL PROPERTY USE_FOLDERS ON) + list(APPEND SOURCES + collections/restorable.h + collections/restorable.cpp array.h choice.cpp functional.cpp @@ -14,6 +18,7 @@ list(APPEND SOURCES value.h value.cpp string_table.h string_table.cpp avl_array.h ) +source_group(Collections REGULAR_EXPRESSION collections/.*) add_library(inkcpp ${SOURCES}) # Make sure the include directory is included diff --git a/inkcpp/collections/restorable.cpp b/inkcpp/collections/restorable.cpp new file mode 100644 index 00000000..e69de29b diff --git a/inkcpp/collections/restorable.h b/inkcpp/collections/restorable.h new file mode 100644 index 00000000..887cabd7 --- /dev/null +++ b/inkcpp/collections/restorable.h @@ -0,0 +1,218 @@ +#include + +namespace ink::runtime::internal +{ + /** + * A special base class for collections which have save/restore/forget functionality + * + * In order to properly handle Ink's glue system, we need to be able to execute beyond + * the end of the line and "peek" to see if a glue command is executed. If one is, we keep + * executing (as the next line will be glued to the current). If we don't, or find more content + * before finding glue, then in actuality, we never should have executed beyond the newline. We + * need to *restore* back to our state before we moved past the end of line. Collections inheriting + * from this class gain this functionality. + */ + template + class restorable + { + public: + restorable(ElementType* buffer, size_t size) + : _buffer(buffer), _size(size), _pos(0), _save(~0), _jump(~0) + { } + + // Creates a save point which can later be restored to or forgotten + void save() + { + inkAssert(_save == ~0, "Collection is already saved. You should never call save twice. Ignoring."); + if (_save != ~0) { + return; + } + + // Set the save and jump points to the current position. + _save = _jump = _pos; + } + + // Restore to the last save point + void restore() + { + inkAssert(_save != ~0, "Collection can't be restored because it's not saved. Ignoring."); + if (_save == ~0) { + return; + } + + // Move our position back to the saved position + _pos = _save; + + // Clear save point + _save = _jump = ~0; + } + + // Forget the save point and continue with the current data + template + void forget(NullifyMethod nullify) + { + inkAssert(_save != ~0, "Can't forget save point because there is none. Ignoring."); + if (_save == ~0) { + return; + } + + // If we're behind the save point but past the jump point + if (_save != _jump && _pos > _jump) + { + // Nullify everything between the jump point and the save point + for (size_t i = _jump; i < _save; ++i) + nullify(_buffer[i]); + } + + // Reset save position + _save = _jump = ~0; + } + + // Push element onto the top of collection + void push(const ElementType& elem) + { + // Don't destroy saved data. Jump over it + if (_pos < _save && _save != ~0) + { + _jump = _pos; + _pos = _save; + } + + // Overflow check + if (_pos >= _size) + overflow(_buffer, _size); + + // Push onto the top + _buffer[_pos++] = elem; + } + + // Pop an element off the top of the collection + template + const ElementType& pop(IsNullPredicate isNull) + { + // Make sure we have something to pop + inkAssert(_pos > 0, "Can not pop. No elements to pop!"); + if (_pos <= 0) { + throw 0; // TODO + } + + // Jump over save data + if (_pos == _save) + _pos = _jump; + + // Move over empty data + while (isNull(_buffer[_pos])) + _pos--; + + // Decrement and return + _pos--; + return _buffer[_pos]; + } + + const ElementType& top() + { + if (_pos == _save) + return _buffer[_jump - 1]; + + return _buffer[_pos - 1]; + } + + // Forward iterate + template + void for_each(CallbackMethod callback, IsNullPredicate isNull) const + { + if (_pos == 0) { + return; + } + + // Start at the beginning + size_t i = 0; + do + { + // Jump over saved data + if (i == _jump) + i = _save; + + // Run callback + if(!isNull(_buffer[i])) + callback(_buffer[i]); + + // Move forward one element + i++; + } while (i < _pos); + } + + // Reverse iterate + template + void reverse_for_each(CallbackMethod callback, IsNullPredicate isNull) const + { + if (_pos == 0) { + return; + } + + // Start at the end + size_t i = _pos; + do + { + // Move back one element + i--; + + // Run callback + if (!isNull(_buffer[i])) + callback(_buffer[i]); + + // Jump over saved data + if (i == _save) + i = _jump; + + } while (i > 0); + } + + template + size_t size(IsNullPredicate isNull) const + { + if (_pos == 0) { + return 0; + } + + size_t count = 0; + + // Start at the end + size_t i = _pos; + do + { + // Move back one element + i--; + + // Run callback + if(!isNull(_buffer[i])) + count++; + + // Jump over saved data + if (i == _save) + i = _jump; + + } while (i > 0); + + return count; + } + + protected: + // Called when we run out of space in buffer. + virtual void overflow(ElementType*& buffer, size_t& size) { throw 0; /* TODO: What to do here? Throw something more useful? */ } + + private: + // Data buffer. Collection is stored here + ElementType* _buffer; + + // Size of the _buffer array + size_t _size; + + // Set to the next empty position in the buffer. + size_t _pos; + + // Jump and save points. Used when we've been saved. + size_t _jump; + size_t _save; + }; +} \ No newline at end of file diff --git a/inkcpp_test/CMakeLists.txt b/inkcpp_test/CMakeLists.txt index 48525966..27649042 100644 --- a/inkcpp_test/CMakeLists.txt +++ b/inkcpp_test/CMakeLists.txt @@ -2,6 +2,7 @@ add_executable(inkcpp_test catch.hpp Main.cpp Array.cpp Pointer.cpp Stack.cpp + Restorable.cpp ) target_link_libraries(inkcpp_test PUBLIC inkcpp) \ No newline at end of file diff --git a/inkcpp_test/Restorable.cpp b/inkcpp_test/Restorable.cpp new file mode 100644 index 00000000..a498046c --- /dev/null +++ b/inkcpp_test/Restorable.cpp @@ -0,0 +1,294 @@ +#include "catch.hpp" + +#include "../inkcpp/collections/restorable.h" + +using ink::runtime::internal::restorable; + +SCENARIO("a restorable collection can operate like a stack", "[restorable]") +{ + GIVEN("an empty restorable collection") + { + // Create the collection + constexpr size_t size = 128; + int buffer[size]; + auto collection = restorable(buffer, size); + + // Lambdas + auto isNull = [](const int&) { return false; }; + + THEN("it should have zero size") REQUIRE(collection.size(isNull) == 0); + + WHEN("we push ten elements") + { + for (int i = 0; i < 10; i++) + collection.push(i); + + THEN("the count should be ten") REQUIRE(collection.size(isNull) == 10); + + THEN("The top should be nine") REQUIRE(collection.top() == 9); + + THEN("We can iterate forward") + { + int check = 0; + collection.for_each([&check](int& elem) { REQUIRE(elem == check++); }, isNull); + } + + THEN("We can iterate backward") + { + int check = 10; + collection.reverse_for_each([&check](int& elem) { REQUIRE(elem == --check); }, isNull); + } + + THEN("Pop five elements") + { + REQUIRE(collection.pop(isNull) == 9); + REQUIRE(collection.pop(isNull) == 8); + REQUIRE(collection.pop(isNull) == 7); + REQUIRE(collection.pop(isNull) == 6); + + REQUIRE(collection.top() == 5); + } + } + } +} + +void VerifyStack(restorable& stack, const std::vector& expected) +{ + THEN("it should match the expected array in both directions") + { + auto isNull = [](const int& e) { return e == -1; }; + + // Check + REQUIRE(stack.size(isNull) == expected.size()); + + // Iterate and make sure each element matches + int counter = 0; + stack.for_each([&counter, expected](const int& elem) { + REQUIRE(counter < expected.size()); + REQUIRE(counter >= 0); + REQUIRE(expected[counter] == elem); + counter++; + }, isNull); + + // Make sure we hit every element in the expected vector + REQUIRE(counter == expected.size()); + + // Try the other direction + stack.reverse_for_each([&counter, expected](const int& elem) { + counter--; + REQUIRE(counter >= 0); + REQUIRE(expected[counter] == elem); + }, isNull); + REQUIRE(counter == 0); + } +} + +template +void RestoreAndVerifyStack(restorable& stack, const std::vector& expected) +{ + WHEN("the stack is restored") + { + // Restore stack + stack.restore(); + + VerifyStack(stack, expected); + } +} + +template +void ForgetAndVerifyStack(restorable& stack, const std::vector& expected, NullifyCallback nullify) +{ + WHEN("the save state is forgotten") + { + // Forget save point + stack.forget(nullify); + + VerifyStack(stack, expected); + } +} + +SCENARIO("a collection can be restored no matter how many times you push or pop", "[restorable]") +{ + // Create the collection + constexpr size_t size = 128; + int buffer[size]; + auto collection = restorable(buffer, size); + + // Lambdas (we'll use negative one for null) + auto isNull = [](const int& elem) { return elem == -1; }; + auto nullify = [](int& elem) { elem = -1; }; + + GIVEN("a stack with five items that has been saved") + { + // Create five elements + std::vector expected; + for (int i = 0; i < 5; i++) + { + collection.push(i); + expected.push_back(i); + } + + // Create save point + collection.save(); + + WHEN("more elements are pushed") + { + collection.push(10); + collection.push(11); + RestoreAndVerifyStack(collection, expected); + } + + WHEN("elements are popped") + { + collection.pop(isNull); + collection.pop(isNull); + RestoreAndVerifyStack(collection, expected); + } + + WHEN("elements are popped and pushed") + { + collection.pop(isNull); + collection.pop(isNull); + collection.push(100); + collection.push(200); + collection.push(300); + RestoreAndVerifyStack(collection, expected); + } + + WHEN("all elements are popped") + { + for (int i = 0; i < 5; i++) { + collection.pop(isNull); + } + REQUIRE(collection.size(isNull) == 0); + RestoreAndVerifyStack(collection, expected); + + THEN("More are pushed") + { + collection.push(100); collection.push(200); + VerifyStack(collection, { 100, 200 }); + } + } + } +} + +SCENARIO("saving does not disrupt iteration", "[restorable]") +{ + // Create the collection + constexpr size_t size = 128; + int buffer[size]; + auto collection = restorable(buffer, size); + + // Lambdas (we'll use negative one for null) + auto isNull = [](const int& elem) { return elem == -1; }; + auto nullify = [](int& elem) { elem = -1; }; + + GIVEN("a stack with five items that has been saved") + { + // Create five elements + std::vector expected; + for (int i = 0; i < 5; i++) + { + collection.push(i); + expected.push_back(i); + } + + // Create save point + collection.save(); + + WHEN("elements are pushed") + { + collection.push(10); expected.push_back(10); + collection.push(20); expected.push_back(20); + VerifyStack(collection, expected); + } + + WHEN("elements are popped") + { + collection.pop(isNull); expected.pop_back(); + collection.pop(isNull); expected.pop_back(); + VerifyStack(collection, expected); + } + + WHEN("elements are popped and pushed") + { + collection.pop(isNull); expected.pop_back(); + collection.pop(isNull); expected.pop_back(); + collection.push(10); expected.push_back(10); + collection.push(20); expected.push_back(20); + VerifyStack(collection, expected); + } + } + + GIVEN("an empty saved stack") + { + std::vector expected; + collection.save(); + + WHEN("elements are pushed") + { + collection.push(10); expected.push_back(10); + collection.push(20); expected.push_back(20); + VerifyStack(collection, expected); + } + + WHEN("elements are pushed then popped") + { + collection.push(10); expected.push_back(10); + collection.push(20); expected.push_back(20); + collection.pop(isNull); expected.pop_back(); + collection.pop(isNull); expected.pop_back(); + VerifyStack(collection, expected); + } + } +} + +SCENARIO("save points can be forgotten", "[restorable]") +{ + // Create the collection + constexpr size_t size = 128; + int buffer[size]; + auto collection = restorable(buffer, size); + + // Lambdas (we'll use negative one for null) + auto isNull = [](const int& elem) { return elem == -1; }; + auto nullify = [](int& elem) { elem = -1; }; + + GIVEN("a stack with five items that has been saved") + { + // Create five elements + std::vector expected; + for (int i = 0; i < 5; i++) + { + collection.push(i); + expected.push_back(i); + } + + // Create save point + collection.save(); + + WHEN("elements are pushed") + { + collection.push(10); expected.push_back(10); + collection.push(20); expected.push_back(20); + ForgetAndVerifyStack(collection, expected, nullify); + } + + WHEN("elements are popped") + { + collection.pop(isNull); expected.pop_back(); + collection.pop(isNull); expected.pop_back(); + ForgetAndVerifyStack(collection, expected, nullify); + } + + WHEN("elements are popped and pushed") + { + collection.pop(isNull); expected.pop_back(); + collection.pop(isNull); expected.pop_back(); + collection.push(10); expected.push_back(10); + collection.push(20); expected.push_back(20); + collection.push(30); expected.push_back(30); + ForgetAndVerifyStack(collection, expected, nullify); + } + } +} \ No newline at end of file From ceba40cd64c1c6fe5fd06f44e8c735bb2d3ed6c3 Mon Sep 17 00:00:00 2001 From: Brook Warner Jensen Date: Thu, 10 Dec 2020 12:59:18 -0800 Subject: [PATCH 2/9] Moved eval stack over to restorable base class --- inkcpp/collections/restorable.h | 21 +++++++++- inkcpp/stack.cpp | 71 +++++++-------------------------- inkcpp/stack.h | 17 ++------ inkcpp/value.cpp | 2 +- inkcpp/value.h | 2 +- 5 files changed, 40 insertions(+), 73 deletions(-) diff --git a/inkcpp/collections/restorable.h b/inkcpp/collections/restorable.h index 887cabd7..a77305e3 100644 --- a/inkcpp/collections/restorable.h +++ b/inkcpp/collections/restorable.h @@ -109,7 +109,7 @@ namespace ink::runtime::internal return _buffer[_pos]; } - const ElementType& top() + const ElementType& top() const { if (_pos == _save) return _buffer[_jump - 1]; @@ -117,6 +117,14 @@ namespace ink::runtime::internal return _buffer[_pos - 1]; } + bool is_empty() const { return _pos == 0; } + + void clear() + { + _pos = 0; + _save = _jump = ~0; + } + // Forward iterate template void for_each(CallbackMethod callback, IsNullPredicate isNull) const @@ -142,6 +150,17 @@ namespace ink::runtime::internal } while (i < _pos); } + template + void for_each_all(CallbackMethod callback) const + { + // no matter if we're saved or not, we iterate everything + int len = (_save == ~0 || _pos > _save) ? _pos : _save; + + // Iterate + for (int i = 0; i < len; i++) + callback(_buffer[i]); + } + // Reverse iterate template void reverse_for_each(CallbackMethod callback, IsNullPredicate isNull) const diff --git a/inkcpp/stack.cpp b/inkcpp/stack.cpp index 59ee8cd6..2bd04fd5 100644 --- a/inkcpp/stack.cpp +++ b/inkcpp/stack.cpp @@ -172,101 +172,58 @@ namespace ink } basic_eval_stack::basic_eval_stack(value* data, size_t size) - : _stack(data), _size(size), _pos(0), _save(~0), _jump(~0) + : base(data, size) { } void basic_eval_stack::push(const value& val) { - // Don't destroy saved data. Jump over it - if (_pos < _save && _save != ~0) - { - _jump = _pos; - _pos = _save; - } - - inkAssert(_pos < _size, "Stack overflow!"); - _stack[_pos++] = val; + base::push(val); } value basic_eval_stack::pop() { - inkAssert(_pos > 0, "Nothing left to pop!"); - - // Jump over save data - if (_pos == _save) - _pos = _jump; - - // Move over none data - while (_stack[_pos].is_none()) - _pos--; - - // Decrement and return - _pos--; - return _stack[_pos]; + return base::pop([](const value& v) { return v.is_none(); }); } const value& basic_eval_stack::top() const { - inkAssert(_pos > 0, "Stack is empty! No top()"); - - return _stack[_pos - 1]; + return base::top(); } bool basic_eval_stack::is_empty() const { - return _pos == 0; + return base::is_empty(); } void basic_eval_stack::clear() { - _pos = 0; - _jump = _save = ~0; + base::clear(); } void basic_eval_stack::mark_strings(string_table& strings) const { - // no matter if we're saved or not, we consider all strings - int len = (_save == ~0 || _pos > _save) ? _pos : _save; - - for (int i = 0; i < len; i++) - _stack[i].mark_strings(strings); + // Iterate everything (including what we have saved) and mark strings + base::for_each_all([&strings](const value& elem) { elem.mark_strings(strings); }); } void basic_eval_stack::save() { - inkAssert(_save == ~0, "Can not save stack twice! restore() or forget() first"); - - // Save current stack position - _save = _jump = _pos; + base::save(); } void basic_eval_stack::restore() { - inkAssert(_save != ~0, "Can not restore() when there is no save!"); - - // Move position back to saved position - _pos = _save; - _save = _jump = ~0; + base::restore(); } void basic_eval_stack::forget() { - inkAssert(_save != ~0, "Can not forget when the stack has never been saved!"); - - // If we have moven to a point earlier than the save point but we have a jump point - if (_pos < _save && _pos > _jump) - { - // Everything between the jump point and the save point needs to be nullified - data x; x.set_none(); - value none = value(x); - for (size_t i = _jump; i < _save; i++) - _stack[i] = none; - } - - // Just reset save position - _save = ~0; + // Clear out + data x; x.set_none(); + value none = value(x); + base::forget([&none](value& elem) { elem = none; }); } } diff --git a/inkcpp/stack.h b/inkcpp/stack.h index 731bd64d..f16efa99 100644 --- a/inkcpp/stack.h +++ b/inkcpp/stack.h @@ -1,6 +1,7 @@ #pragma once #include "value.h" +#include "collections/restorable.h" namespace ink { @@ -79,11 +80,13 @@ namespace ink entry _stack[N]; }; - class basic_eval_stack + class basic_eval_stack : protected restorable { protected: basic_eval_stack(value* data, size_t size); + using base = restorable; + public: // Push value onto the stack void push(const value&); @@ -107,18 +110,6 @@ namespace ink void save(); void restore(); void forget(); - - private: - // stack - value * const _stack; - const size_t _size; - - // Current stack position - size_t _pos; - - // Fuck me - size_t _save; - size_t _jump; }; template diff --git a/inkcpp/value.cpp b/inkcpp/value.cpp index 34790783..3cf9004e 100644 --- a/inkcpp/value.cpp +++ b/inkcpp/value.cpp @@ -121,7 +121,7 @@ namespace ink return type; } - void value::mark_strings(string_table& strings) + void value::mark_strings(string_table& strings) const { // mark any allocated strings we're using for (int i = 0; i < VALUE_DATA_LENGTH; i++) diff --git a/inkcpp/value.h b/inkcpp/value.h index fd0388f6..aa98132c 100644 --- a/inkcpp/value.h +++ b/inkcpp/value.h @@ -109,7 +109,7 @@ namespace ink uint32_t get() const { return as_divert(); } // Garbage collection - void mark_strings(string_table&); + void mark_strings(string_table&) const; #ifdef INK_ENABLE_STL template<> From d50be658d977948a4466fcb37f64d4040fcf98d9 Mon Sep 17 00:00:00 2001 From: Brook Warner Jensen Date: Thu, 10 Dec 2020 14:12:03 -0800 Subject: [PATCH 3/9] Moving callstack over to restorable collection --- inkcpp/collections/restorable.h | 57 +++++++++++++++ inkcpp/stack.cpp | 118 +++++++++----------------------- inkcpp/stack.h | 17 ++--- 3 files changed, 93 insertions(+), 99 deletions(-) diff --git a/inkcpp/collections/restorable.h b/inkcpp/collections/restorable.h index a77305e3..375d352b 100644 --- a/inkcpp/collections/restorable.h +++ b/inkcpp/collections/restorable.h @@ -20,6 +20,9 @@ namespace ink::runtime::internal : _buffer(buffer), _size(size), _pos(0), _save(~0), _jump(~0) { } + // Checks if we have a save state + bool is_saved() const { return _save != ~0; } + // Creates a save point which can later be restored to or forgotten void save() { @@ -150,6 +153,32 @@ namespace ink::runtime::internal } while (i < _pos); } + template + const ElementType* find(Predicate predicate) const + { + if (_pos == 0) { + return nullptr; + } + + // Start at the beginning + size_t i = 0; + do + { + // Jump over saved data + if (i == _jump) + i = _save; + + // Run callback + if (!isNull(_buffer[i]) && predicate(_buffer[i])) + return &_buffer[i]; + + // Move forward one element + i++; + } while (i < _pos); + + return nullptr; + } + template void for_each_all(CallbackMethod callback) const { @@ -187,6 +216,34 @@ namespace ink::runtime::internal } while (i > 0); } + // Reverse find + template + const ElementType* reverse_find(Predicate predicate) const + { + if (_pos == 0) { + return nullptr; + } + + // Start at the end + size_t i = _pos; + do + { + // Move back one element + i--; + + // Run callback + if (predicate(_buffer[i])) + return &_buffer[i]; + + // Jump over saved data + if (i == _save) + i = _jump; + + } while (i > 0); + + return nullptr; + } + template size_t size(IsNullPredicate isNull) const { diff --git a/inkcpp/stack.cpp b/inkcpp/stack.cpp index 2bd04fd5..56c08414 100644 --- a/inkcpp/stack.cpp +++ b/inkcpp/stack.cpp @@ -7,15 +7,14 @@ namespace ink namespace internal { basic_stack::basic_stack(entry* data, size_t size) - : _stack(data), _size(size), _pos(0), _save(~0), _jump(~0) + : base(data, size) { - } void basic_stack::set(hash_t name, const value& val) { // If we have a save point, always add no matter what - if (_save != ~0) + if (base::is_saved()) { add(name, val); return; @@ -31,27 +30,18 @@ namespace ink const value* basic_stack::get(hash_t name) const { - if (_pos == 0) - return nullptr; - - // Move backwards and find the variable - size_t i = _pos; - do - { - i--; - - if (_stack[i].name == name) - return &_stack[i].data; + // Find whatever comes first: a matching entry or a stack frame entry + const entry* found = base::reverse_find([name](entry& e) { return e.name == name || e.name == InvalidHash; }); - // We hit the top of this stack frame. Not found! - if (_stack[i].name == InvalidHash) - break; + // If nothing found, no value + if (found == nullptr) + return nullptr; - // Jump over saved data - if (i == _save) - i = _jump; - } while (i > 0); + // If we found something of that name, return the value + if (found->name == name) + return &found->data; + // Otherwise, nothing in this stack frame return nullptr; } @@ -63,112 +53,68 @@ namespace ink offset_t basic_stack::pop_frame(frame_type* type) { - inkAssert(_pos > 0, "Can not pop frame from empty callstack"); + inkAssert(!base::is_empty(), "Can not pop frame from empty callstack."); - // Advance up the callstack until we find the frame record - _pos--; - while (_stack[_pos].name != InvalidHash && _pos > 0) + const entry* popped = &base::pop([](const entry&) { return false; }); + while (popped->name != InvalidHash && !base::is_empty()) { - // Jump over saved data - if (_pos == _save) - _pos = _jump; - - _pos--; + popped = &base::pop([](const entry&) { return false; }); } - inkAssert(_stack[_pos].name == InvalidHash, "Attempting to pop_frame when no frames exist! Stack reset"); + inkAssert(popped->name == InvalidHash, "Attempting to pop_frame when no frames exist! Stack reset."); // Store frame type if (type != nullptr) - *type = (_stack[_pos].data.data_type() == data_type::tunnel_frame) ? frame_type::tunnel : frame_type::function; + *type = (popped->data.data_type() == data_type::tunnel_frame) ? frame_type::tunnel : frame_type::function; // Return the offset stored in the frame record - return _stack[_pos].data.as_divert(); + return popped->data.as_divert(); } bool basic_stack::has_frame() const { // Empty case - if (_pos == 0) + if (base::is_empty()) return false; - size_t iter = _pos - 1; - while (_stack[iter].name != InvalidHash && iter > 0) - { - // Jump over saved data - if (iter == _save) - iter = _jump; + // Search in reverse for a stack frame + const entry* frame = base::reverse_find([](const entry& elem) { + return elem.name == InvalidHash; + }); - iter--; - } - return _stack[iter].name == InvalidHash; + // Return true if a frame was found + return frame != nullptr; } void basic_stack::clear() { - _save = _jump = ~0; - _pos = 0; + base::clear(); } void basic_stack::mark_strings(string_table& strings) const { - // no matter if we're saved or not, we consider all strings - int len = (_save == ~0 || _pos > _save) ? _pos : _save; - - // iterate and mark - for (int i = 0; i < len; i++) - _stack[i].data.mark_strings(strings); + // Mark all strings + base::for_each_all([&strings](const entry& elem) { elem.data.mark_strings(strings); }); } void basic_stack::save() { - inkAssert(_save == ~0, "Can not save stack twice! restore() or forget() first"); - - // Save current stack position - _save = _jump = _pos; + base::save(); } void basic_stack::restore() { - inkAssert(_save != ~0, "Can not restore() when there is no save!"); - - // Move position back to saved position - _pos = _save; - _save = _jump = ~0; + base::restore(); } void basic_stack::forget() { - inkAssert(_save != ~0, "Can not forget when the stack has never been saved!"); - - // If we have moven to a point earlier than the save point but we have a jump point - if (_pos < _save && _pos > _jump) - { - // Everything between the jump point and the save point needs to be nullified - for (size_t i = _jump; i < _save; i++) - _stack[i].name = ~0; - } - - // Just reset save position - _save = ~0; + base::forget([](entry& elem) { elem.name = ~0; }); } void basic_stack::add(hash_t name, const value& val) { - // Don't destroy saved data - if (_pos < _save && _save != ~0) - { - // Move to next safe spot after saved data and save where we came from - _jump = _pos; - _pos = _save; - } - - inkAssert(_pos < _size, "Stack overflow!"); - - // Push onto the top of the stack - _stack[_pos].name = name; - _stack[_pos].data = val; - _pos++; + base::push({ name, val }); } basic_eval_stack::basic_eval_stack(value* data, size_t size) diff --git a/inkcpp/stack.h b/inkcpp/stack.h index f16efa99..505ec91e 100644 --- a/inkcpp/stack.h +++ b/inkcpp/stack.h @@ -21,13 +21,15 @@ namespace ink tunnel }; - class basic_stack + class basic_stack : protected restorable { protected: basic_stack(entry* data, size_t size); - public: + // base class + using base = restorable; + public: // Sets existing value, or creates a new one at this callstack entry void set(hash_t name, const value& val); @@ -56,17 +58,6 @@ namespace ink private: void add(hash_t name, const value& val); - private: - // stack - entry* _stack; - size_t _size; - - // Current stack position - size_t _pos; - - // Fuck me - size_t _save; - size_t _jump; }; // stack for call history and temporary variables From bc7ff3386d45a62b7d017b65f38cabb1a8ed1186 Mon Sep 17 00:00:00 2001 From: Brook Warner Jensen Date: Thu, 10 Dec 2020 16:33:04 -0800 Subject: [PATCH 4/9] Basic thread support in callstack. Still missing a few things --- inkcpp/stack.cpp | 357 +++++++++++++++++++++++-------------- inkcpp/stack.h | 18 ++ inkcpp/value.h | 4 + inkcpp_test/CMakeLists.txt | 1 + inkcpp_test/Callstack.cpp | 183 +++++++++++++++++++ shared/public/system.h | 3 + 6 files changed, 437 insertions(+), 129 deletions(-) create mode 100644 inkcpp_test/Callstack.cpp diff --git a/inkcpp/stack.cpp b/inkcpp/stack.cpp index 56c08414..94b897ee 100644 --- a/inkcpp/stack.cpp +++ b/inkcpp/stack.cpp @@ -1,177 +1,276 @@ #include "stack.h" -namespace ink +namespace ink::runtime::internal { - namespace runtime + basic_stack::basic_stack(entry* data, size_t size) + : base(data, size) { - namespace internal + } + + void basic_stack::set(hash_t name, const value& val) + { + // If we have a save point, always add no matter what + if (base::is_saved()) { - basic_stack::basic_stack(entry* data, size_t size) - : base(data, size) - { + add(name, val); + return; + } + + // Either set an existing variable or add it to the stack + value* existing = const_cast(get(name)); + if (existing == nullptr) + add(name, val); + else + *existing = val; + } + + const value* basic_stack::get(hash_t name) const + { + // Find whatever comes first: a matching entry or a stack frame entry + thread_t skip = ~0; + const entry* found = base::reverse_find([name, &skip](entry& e) { + // If this is an end thread marker, skip over it + if (e.data.data_type() == data_type::thread_end) { + skip = e.data.as_divert(); } - void basic_stack::set(hash_t name, const value& val) - { - // If we have a save point, always add no matter what - if (base::is_saved()) - { - add(name, val); - return; + // If we're skipping + if (skip != ~0) { + // Stop if we get to the start of the thread block + if (e.data.data_type() == data_type::thread_start) { + skip = ~0; } - // Either set an existing variable or add it to the stack - value* existing = const_cast(get(name)); - if (existing == nullptr) - add(name, val); - else - *existing = val; + // Don't return anything in the hidden thread block + return false; } - const value* basic_stack::get(hash_t name) const - { - // Find whatever comes first: a matching entry or a stack frame entry - const entry* found = base::reverse_find([name](entry& e) { return e.name == name || e.name == InvalidHash; }); + return e.name == name || e.name == InvalidHash; + }); - // If nothing found, no value - if (found == nullptr) - return nullptr; + // If nothing found, no value + if (found == nullptr) + return nullptr; - // If we found something of that name, return the value - if (found->name == name) - return &found->data; + // If we found something of that name, return the value + if (found->name == name) + return &found->data; - // Otherwise, nothing in this stack frame - return nullptr; - } + // Otherwise, nothing in this stack frame + return nullptr; + } - void basic_stack::push_frame(offset_t return_to, frame_type type) - { - // Add to top of stack - add(InvalidHash, value(return_to, type == frame_type::tunnel ? data_type::tunnel_frame : data_type::function_frame)); - } + void basic_stack::push_frame(offset_t return_to, frame_type type) + { + // Add to top of stack + add(InvalidHash, value(return_to, type == frame_type::tunnel ? data_type::tunnel_frame : data_type::function_frame)); + } - offset_t basic_stack::pop_frame(frame_type* type) - { - inkAssert(!base::is_empty(), "Can not pop frame from empty callstack."); + const entry* basic_stack::pop() + { + return &base::pop([](const entry& elem) { return elem.name == ~0; }); + } - const entry* popped = &base::pop([](const entry&) { return false; }); - while (popped->name != InvalidHash && !base::is_empty()) - { - popped = &base::pop([](const entry&) { return false; }); - } + offset_t basic_stack::pop_frame(frame_type* type) + { + inkAssert(!base::is_empty(), "Can not pop frame from empty callstack."); - inkAssert(popped->name == InvalidHash, "Attempting to pop_frame when no frames exist! Stack reset."); + // Keep popping until we find a frame entry + const entry* popped = pop(); + while (popped->name != InvalidHash && !base::is_empty()) + { + popped = pop(); + } - // Store frame type - if (type != nullptr) - *type = (popped->data.data_type() == data_type::tunnel_frame) ? frame_type::tunnel : frame_type::function; + // If we didn't find a frame entry, we never had a frame to return from + inkAssert(popped->name == InvalidHash, "Attempting to pop_frame when no frames exist! Stack reset."); - // Return the offset stored in the frame record - return popped->data.as_divert(); - } + // Make sure we're not somehow trying to "return" from a thread + inkAssert(popped->data.data_type() != data_type::thread_start && popped->data.data_type() != data_type::thread_end, + "Can not return from a thread! How did this happen?"); - bool basic_stack::has_frame() const - { - // Empty case - if (base::is_empty()) - return false; + // Store frame type + if (type != nullptr) + *type = (popped->data.data_type() == data_type::tunnel_frame) ? frame_type::tunnel : frame_type::function; - // Search in reverse for a stack frame - const entry* frame = base::reverse_find([](const entry& elem) { - return elem.name == InvalidHash; - }); + // Return the offset stored in the frame record + return popped->data.as_divert(); + } - // Return true if a frame was found - return frame != nullptr; - } + bool basic_stack::has_frame() const + { + // Empty case + if (base::is_empty()) + return false; - void basic_stack::clear() - { - base::clear(); - } + // Search in reverse for a stack frame + const entry* frame = base::reverse_find([](const entry& elem) { + return elem.name == InvalidHash; + }); - void basic_stack::mark_strings(string_table& strings) const - { - // Mark all strings - base::for_each_all([&strings](const entry& elem) { elem.data.mark_strings(strings); }); - } + // Return true if a frame was found + return frame != nullptr; + } - void basic_stack::save() - { - base::save(); - } + void basic_stack::clear() + { + base::clear(); + } - void basic_stack::restore() - { - base::restore(); - } + void basic_stack::mark_strings(string_table& strings) const + { + // Mark all strings + base::for_each_all([&strings](const entry& elem) { elem.data.mark_strings(strings); }); + } - void basic_stack::forget() - { - base::forget([](entry& elem) { elem.name = ~0; }); - } + thread_t basic_stack::fork_thread() + { + // TODO create unique thread ID + thread_t new_thread = _next_thread++; - void basic_stack::add(hash_t name, const value& val) - { - base::push({ name, val }); - } + // Push a thread start marker here + // TODO: Back counter + add(InvalidHash, value(new_thread, data_type::thread_start)); - basic_eval_stack::basic_eval_stack(value* data, size_t size) - : base(data, size) - { + return new_thread; + } - } + void basic_stack::complete_thread(thread_t thread) + { + // Add a thread complete marker + add(InvalidHash, value(thread, data_type::thread_end)); + } - void basic_eval_stack::push(const value& val) + void basic_stack::collapse_to_thread(thread_t thread) + { + // If we're restoring a specific thread (and not the main thread) + if (thread != ~0) + { + // Keep popping until we find the requested thread's end marker + const entry* top = pop(); + while (!( + top->data.data_type() == data_type::thread_end && + top->data.as_divert() == thread)) { - base::push(val); + top = pop(); } + } - value basic_eval_stack::pop() - { - return base::pop([](const value& v) { return v.is_none(); }); - } + // Now, start iterating backwards + thread_t nulling = ~0; + base::reverse_for_each([&nulling](entry& elem) { - const value& basic_eval_stack::top() const - { - return base::top(); + // Thread end. We just need to delete this whole block + if (nulling == ~0 && elem.data.data_type() == data_type::thread_end) { + nulling = elem.data.as_divert(); } - bool basic_eval_stack::is_empty() const - { - return base::is_empty(); - } + // If we're deleting a useless thread block + if (nulling != ~0) { + // If this is the start of the block, stop deleting + if (elem.data.data_type() == data_type::thread_start && elem.data.as_divert() == nulling) { + nulling = ~0; + } - void basic_eval_stack::clear() - { - base::clear(); + // delete data + elem.name = NulledHashId; } - - void basic_eval_stack::mark_strings(string_table& strings) const + else { - // Iterate everything (including what we have saved) and mark strings - base::for_each_all([&strings](const value& elem) { elem.mark_strings(strings); }); - } + // Clear thread start markers. We don't need or want them anymore + if (elem.data.data_type() == data_type::thread_start) { + elem.name = NulledHashId; + } - void basic_eval_stack::save() - { - base::save(); - } + // TODO: Threads with negative offsets - void basic_eval_stack::restore() - { - base::restore(); + // TODO: Jump markers! } - void basic_eval_stack::forget() - { - // Clear out - data x; x.set_none(); - value none = value(x); - base::forget([&none](value& elem) { elem = none; }); - } + }, [](entry& elem) { return elem.name == NulledHashId; }); - } + // No more threads. Clear next thread counter + _next_thread = 0; + } + + void basic_stack::save() + { + base::save(); + + // Save thread counter + _backup_next_thread = _next_thread; + } + + void basic_stack::restore() + { + base::restore(); + + // Restore thread counter + _next_thread = _backup_next_thread; + } + + void basic_stack::forget() + { + base::forget([](entry& elem) { elem.name = ~0; }); + } + + void basic_stack::add(hash_t name, const value& val) + { + base::push({ name, val }); + } + + basic_eval_stack::basic_eval_stack(value* data, size_t size) + : base(data, size) + { + + } + + void basic_eval_stack::push(const value& val) + { + base::push(val); + } + + value basic_eval_stack::pop() + { + return base::pop([](const value& v) { return v.is_none(); }); + } + + const value& basic_eval_stack::top() const + { + return base::top(); + } + + bool basic_eval_stack::is_empty() const + { + return base::is_empty(); + } + + void basic_eval_stack::clear() + { + base::clear(); + } + + void basic_eval_stack::mark_strings(string_table& strings) const + { + // Iterate everything (including what we have saved) and mark strings + base::for_each_all([&strings](const value& elem) { elem.mark_strings(strings); }); + } + + void basic_eval_stack::save() + { + base::save(); + } + + void basic_eval_stack::restore() + { + base::restore(); + } + + void basic_eval_stack::forget() + { + // Clear out + data x; x.set_none(); + value none = value(x); + base::forget([&none](value& elem) { elem = none; }); } } \ No newline at end of file diff --git a/inkcpp/stack.h b/inkcpp/stack.h index 505ec91e..a35bb4b6 100644 --- a/inkcpp/stack.h +++ b/inkcpp/stack.h @@ -51,6 +51,17 @@ namespace ink // Garbage collection void mark_strings(string_table&) const; + // == Threading == + + // Forks a new thread from the current callstack and returns that thread's unique id + thread_t fork_thread(); + + // Mark a thread as "done". It's callstack is still preserved until collapse_to_thread is called. + void complete_thread(thread_t thread); + + // Collapses the callstack to the state of a single thread + void collapse_to_thread(thread_t thread); + // == Save/Restore == void save(); void restore(); @@ -58,6 +69,13 @@ namespace ink private: void add(hash_t name, const value& val); + const entry* pop(); + + // thread ids + thread_t _next_thread = 0; + thread_t _backup_next_thread = 0; + + static const hash_t NulledHashId = ~0; }; // stack for call history and temporary variables diff --git a/inkcpp/value.h b/inkcpp/value.h index aa98132c..096f1841 100644 --- a/inkcpp/value.h +++ b/inkcpp/value.h @@ -31,6 +31,8 @@ namespace ink null, // void/null (used for void function returns) tunnel_frame, // Return from tunnel function_frame, // Return from function + thread_start, // Start of a new thread frame + thread_end, // End of a thread frame }; // Container for any data used as part of the runtime (variable values, output streams, evaluation stack, etc.) @@ -124,6 +126,8 @@ namespace ink inline operator float() const { return as_float(); } inline operator uint32_t() const { return as_divert(); } + inline bool is_thread_marker() const { return _first.type == data_type::thread_start || _first.type == data_type::thread_end; } + // Is this value "true" bool is_truthy() const; inline operator bool() const { return is_truthy(); } diff --git a/inkcpp_test/CMakeLists.txt b/inkcpp_test/CMakeLists.txt index 27649042..031667f4 100644 --- a/inkcpp_test/CMakeLists.txt +++ b/inkcpp_test/CMakeLists.txt @@ -2,6 +2,7 @@ add_executable(inkcpp_test catch.hpp Main.cpp Array.cpp Pointer.cpp Stack.cpp + Callstack.cpp Restorable.cpp ) diff --git a/inkcpp_test/Callstack.cpp b/inkcpp_test/Callstack.cpp new file mode 100644 index 00000000..d71ea060 --- /dev/null +++ b/inkcpp_test/Callstack.cpp @@ -0,0 +1,183 @@ +#include "catch.hpp" + +#include "../inkcpp/stack.h" + +using ink::hash_t; +using ink::thread_t; +using ink::runtime::internal::value; +using ink::runtime::internal::frame_type; + +const hash_t X = ink::hash_string("X"); +const hash_t Y = ink::hash_string("Y"); +const hash_t Z = ink::hash_string("Z"); + +SCENARIO("threading with the callstack", "[callstack]") +{ + GIVEN("a callstack with a few temporary variables") + { + // Create the stack + auto stack = ink::runtime::internal::stack<50>(); + + // Set X and Y temporary variables + stack.set(X, 100); + stack.set(Y, 200); + + WHEN("there is a fork and more is pushed") + { + thread_t thread = stack.fork_thread(); + + THEN("old variables aren't accessible") + { + REQUIRE(stack.get(X) == nullptr); + REQUIRE(stack.get(Y) == nullptr); + } + + // Push something onto the thread + stack.set(X, 200); + REQUIRE((int)*stack.get(X) == 200); + + WHEN("when that thread ends") + { + stack.complete_thread(thread); + + WHEN("we collapse to the thread") + { + stack.collapse_to_thread(thread); + + THEN("we should have the value from that thread") + { + REQUIRE((int)*stack.get(X) == 200); + } + } + + WHEN("we collapse to the main thraed") + { + stack.collapse_to_thread(~0); + + THEN("we should have the value from the original thread") + { + REQUIRE((int)*stack.get(X) == 100); + REQUIRE((int)*stack.get(Y) == 200); + } + } + + THEN("we should be able to access original thread values") + { + REQUIRE((int)*stack.get(X) == 100); + REQUIRE((int)*stack.get(Y) == 200); + } + + WHEN("we push more on the main thread") + { + stack.set(Z, 500); + + THEN("collapsing to the thread shouldn't have the values anymore") + { + stack.collapse_to_thread(thread); + REQUIRE(stack.get(Z) == nullptr); + } + + WHEN("we start a second thread that closes") + { + thread_t thread2 = stack.fork_thread(); + stack.set(X, 999); + stack.complete_thread(thread2); + + THEN("we can still collapse to the main thread") + { + stack.collapse_to_thread(~0); + REQUIRE((int)*stack.get(X) == 100); + REQUIRE((int)*stack.get(Y) == 200); + } + + THEN("we can still collapse to the first thread") + { + stack.collapse_to_thread(thread); + REQUIRE((int)*stack.get(X) == 200); + REQUIRE(stack.get(Z) == nullptr); + } + } + } + } + + WHEN("that thread also forks a thread") + { + thread_t thread2 = stack.fork_thread(); + + // Put something on this thread + stack.set(X, 999); + + WHEN("that inner thread and outer thread complete") + { + stack.complete_thread(thread2); + stack.complete_thread(thread); + + WHEN("we collapse to the inner thread") + { + stack.collapse_to_thread(thread2); + + THEN("we should have the value from that thread") + { + REQUIRE((int)*stack.get(X) == 999); + } + } + + WHEN("we collapse to the outer thread") + { + stack.collapse_to_thread(thread); + + THEN("we should have the value from that thread") + { + REQUIRE((int)*stack.get(X) == 200); + } + } + + WHEN("we collapse to the main thraed") + { + stack.collapse_to_thread(~0); + + THEN("we should have the value from the original thread") + { + REQUIRE((int)*stack.get(X) == 100); + REQUIRE((int)*stack.get(Y) == 200); + } + } + } + } + } + } + + GIVEN("a callstack with a single tunnel pushed") + { + // Create the stack + auto stack = ink::runtime::internal::stack<50>(); + + // Set X and Y temporary variables + stack.set(X, 100); + stack.set(Y, 200); + + // Push a tunnel + stack.push_frame(505, frame_type::tunnel); + + // Push some more temps + stack.set(X, 101); + stack.set(Y, 201); + + WHEN("a thread is forked") + { + thread_t thread = stack.fork_thread(); + + WHEN("that thread does a tunnel return") + { + frame_type type; + auto offset = stack.pop_frame(&type); + + THEN("that thread should be outside the tunnel") + { + REQUIRE(type == frame_type::tunnel); + REQUIRE(offset == 505); + } + } + } + } +} diff --git a/shared/public/system.h b/shared/public/system.h index 13f9ed22..cca1f289 100644 --- a/shared/public/system.h +++ b/shared/public/system.h @@ -49,6 +49,9 @@ namespace ink // Used as the unique identifier for an ink container typedef uint32_t container_t; + // Used to uniquely identify threads + typedef uint32_t thread_t; + // Checks if a string is only whitespace static bool is_whitespace(const char* string, bool includeNewline = true) { From f055f4d559eaba10aa52df61547f47cee66c86e4 Mon Sep 17 00:00:00 2001 From: Brook Warner Jensen Date: Tue, 22 Dec 2020 17:04:39 -0800 Subject: [PATCH 5/9] Finishing implementing jump markers for thread stack --- inkcpp/collections/restorable.h | 71 +++++++++++++- inkcpp/stack.cpp | 168 ++++++++++++++++++++++++++----- inkcpp/stack.h | 4 +- inkcpp/value.h | 7 ++ inkcpp_test/Callstack.cpp | 169 ++++++++++++++++++++++++++++++++ 5 files changed, 394 insertions(+), 25 deletions(-) diff --git a/inkcpp/collections/restorable.h b/inkcpp/collections/restorable.h index 375d352b..edd1ebd2 100644 --- a/inkcpp/collections/restorable.h +++ b/inkcpp/collections/restorable.h @@ -2,6 +2,61 @@ namespace ink::runtime::internal { + template + constexpr auto EmptyNullPredicate = [](const ElementType&) { return false; }; + + // Iterator type used with restorable + template + class restorable_iter + { + public: + // Create an iterator moving from start (inclusive) to end (exclusive) + restorable_iter(ElementType* start, ElementType* end) + : _current(start), _end(end) { } + + // Move to the next non-null element + template)> + bool next(IsNullPredicate isNull = EmptyNullPredicate) + { + if (_current != _end) + { + // Determine direction of iteration + int dir = _end - _current > 0 ? 1 : -1; + + // Move pointer + _current += dir; + + // Make sure to skip over null items + while (isNull(*_current) && _current != _end) { + _current += dir; + } + } + + // If we've hit the end, return false + if (_current == _end) + return false; + + // Otherwise, iteration is valid + return true; + } + + // Get current element + inline ElementType* get() { return _current; } + + // Get current element (const) + inline const ElementType* get() const { return _current; } + + // Is iteration complete (opposite of is valid) + inline bool done() const { return _current == _end; } + + private: + // Current point of iteration + ElementType* _current; + + // End point (non-valid) + ElementType* _end; + }; + /** * A special base class for collections which have save/restore/forget functionality * @@ -71,8 +126,19 @@ namespace ink::runtime::internal _save = _jump = ~0; } + using iterator = restorable_iter; + using const_iterator = restorable_iter; + + // Iterator that begins at the end of the stack + iterator begin() { return iterator(&_buffer[_pos - 1], _buffer - 1); } + const_iterator begin() const { return iterator(&_buffer[_pos - 1], _buffer - 1); } + + // Iterator that points to the element past the beginning of the stack + iterator end() { return iterator(_buffer - 1, _buffer - 1); } + iterator end() const { return const_iterator(_buffer - 1, _buffer - 1); } + // Push element onto the top of collection - void push(const ElementType& elem) + ElementType& push(const ElementType& elem) { // Don't destroy saved data. Jump over it if (_pos < _save && _save != ~0) @@ -87,6 +153,9 @@ namespace ink::runtime::internal // Push onto the top _buffer[_pos++] = elem; + + // Return reference + return _buffer[_pos - 1]; } // Pop an element off the top of the collection diff --git a/inkcpp/stack.cpp b/inkcpp/stack.cpp index 94b897ee..ad9de0a9 100644 --- a/inkcpp/stack.cpp +++ b/inkcpp/stack.cpp @@ -28,16 +28,23 @@ namespace ink::runtime::internal { // Find whatever comes first: a matching entry or a stack frame entry thread_t skip = ~0; - const entry* found = base::reverse_find([name, &skip](entry& e) { + uint32_t jumping = 0; + const entry* found = base::reverse_find([name, &skip, &jumping](entry& e) { + // Jumping + if (jumping > 0) { + jumping--; + return false; + } + // If this is an end thread marker, skip over it - if (e.data.data_type() == data_type::thread_end) { + if (skip == ~0 && e.data.data_type() == data_type::thread_end) { skip = e.data.as_divert(); } // If we're skipping if (skip != ~0) { // Stop if we get to the start of the thread block - if (e.data.data_type() == data_type::thread_start) { + if (e.data.data_type() == data_type::thread_start && skip == e.data.as_divert()) { skip = ~0; } @@ -45,6 +52,19 @@ namespace ink::runtime::internal return false; } + // Is it a thread start or a jump marker + if (e.name == InvalidHash && (e.data.data_type() == data_type::thread_start || e.data.data_type() == data_type::jump_marker)) + { + // If this thread start has a jump value + uint32_t jump = e.data.thread_jump(); + + // Then we need to do some jumping. Skip + if (jump > 0) { + jumping = jump; + return false; + } + } + return e.name == name || e.name == InvalidHash; }); @@ -71,30 +91,119 @@ namespace ink::runtime::internal return &base::pop([](const entry& elem) { return elem.name == ~0; }); } + entry* basic_stack::do_thread_jump_pop(const basic_stack::iterator& jumpStart) + { + // Start an iterator right after the jumping marker (might be a thread_start or a jump_marker) + iterator threadIter = jumpStart; + + // Get a reference to its jump count + uint32_t& jump = threadIter.get()->data.thread_jump(); + + // Move over it + threadIter.next(); + + // Move back over the current jump value + for (uint32_t i = 0; i < jump; i++) + threadIter.next(); + + // Now keep iterating back until we get to a frame marker + while (!threadIter.done() && (threadIter.get()->name != InvalidHash || threadIter.get()->data.is_thread_marker())) + { + // If we've hit an end of thread marker + auto e = threadIter.get(); + if (e->data.is_thread_end()) + { + // We basically want to skip until we get to the start of this thread (leave the block alone) + thread_t tid = e->data.as_thread_id(); + while (!threadIter.get()->data.is_thread_start() || threadIter.get()->data.as_thread_id() != tid) + { + jump++; + threadIter.next(); + } + + // Now let us skip over the thread start + } + + threadIter.next(); + jump++; + } + + // Move us over the frame marker + jump++; + + // Now that thread marker is set to the correct jump value. + return threadIter.get(); + } + offset_t basic_stack::pop_frame(frame_type* type) { inkAssert(!base::is_empty(), "Can not pop frame from empty callstack."); - // Keep popping until we find a frame entry - const entry* popped = pop(); - while (popped->name != InvalidHash && !base::is_empty()) + const entry* returnedFrame = nullptr; + + // Start iterating backwards + iterator iter = base::begin(); + while (!iter.done()) { - popped = pop(); + // Keep popping if it's not a frame marker or thread marker of some kind + entry* frame = iter.get(); + if (frame->name != InvalidHash) + { + pop(); + iter = base::begin(); + continue; + } + + // We now have a frame marker. Check if it's a thread + // Thread handling + if (frame->data.is_thread_marker() || frame->data.is_jump_marker()) + { + // End of thread marker, we need to create a jump marker + if (frame->data.data_type() == data_type::thread_end) + { + // Push a new jump marker after the thread end + entry& jump = push({ InvalidHash, value(0, data_type::jump_marker) }); + jump.data.thread_jump() = 0; + + // Do a pop back + returnedFrame = do_thread_jump_pop(base::begin()); + break; + } + + // If this is a jump marker, we actually want to extend it to the next frame + if (frame->data.is_jump_marker()) + { + // Use the thread jump pop method using this jump marker + returnedFrame = do_thread_jump_pop(iter); + break; + } + + // Popping past thread start + if (frame->data.data_type() == data_type::thread_start) + { + returnedFrame = do_thread_jump_pop(iter); + break; + } + } + + // Otherwise, pop the frame marker off and return it + returnedFrame = pop(); + break; } // If we didn't find a frame entry, we never had a frame to return from - inkAssert(popped->name == InvalidHash, "Attempting to pop_frame when no frames exist! Stack reset."); + inkAssert(returnedFrame, "Attempting to pop_frame when no frames exist! Stack reset."); // Make sure we're not somehow trying to "return" from a thread - inkAssert(popped->data.data_type() != data_type::thread_start && popped->data.data_type() != data_type::thread_end, + inkAssert(returnedFrame->data.data_type() != data_type::thread_start && returnedFrame->data.data_type() != data_type::thread_end, "Can not return from a thread! How did this happen?"); // Store frame type if (type != nullptr) - *type = (popped->data.data_type() == data_type::tunnel_frame) ? frame_type::tunnel : frame_type::function; + *type = (returnedFrame->data.data_type() == data_type::tunnel_frame) ? frame_type::tunnel : frame_type::function; // Return the offset stored in the frame record - return popped->data.as_divert(); + return returnedFrame->data.as_divert(); } bool basic_stack::has_frame() const @@ -129,8 +238,11 @@ namespace ink::runtime::internal thread_t new_thread = _next_thread++; // Push a thread start marker here - // TODO: Back counter - add(InvalidHash, value(new_thread, data_type::thread_start)); + entry& thread_entry = add(InvalidHash, value(new_thread, data_type::thread_start)); + + // Set stack jump counter for thread to 0. This number is used if the thread ever + // tries to pop past its origin. It keeps track of how much of the preceeding stack it's popped back + thread_entry.data.thread_jump() = 0; return new_thread; } @@ -152,23 +264,33 @@ namespace ink::runtime::internal top->data.data_type() == data_type::thread_end && top->data.as_divert() == thread)) { + inkAssert(!is_empty(), "Ran out of stack while searching for end of thread marker. Did you call complete_thread?"); top = pop(); } } // Now, start iterating backwards thread_t nulling = ~0; - base::reverse_for_each([&nulling](entry& elem) { + uint32_t jumping = 0; + base::reverse_for_each([&nulling, &jumping](entry& elem) { + if (jumping > 0) { + // delete data + elem.name = NulledHashId; + + // Move on + jumping--; + return; + } // Thread end. We just need to delete this whole block - if (nulling == ~0 && elem.data.data_type() == data_type::thread_end) { + if (nulling == ~0 && elem.data.is_thread_end() && elem.name == InvalidHash) { nulling = elem.data.as_divert(); } // If we're deleting a useless thread block if (nulling != ~0) { // If this is the start of the block, stop deleting - if (elem.data.data_type() == data_type::thread_start && elem.data.as_divert() == nulling) { + if (elem.name == InvalidHash && elem.data.data_type() == data_type::thread_start && elem.data.as_divert() == nulling) { nulling = ~0; } @@ -178,13 +300,13 @@ namespace ink::runtime::internal else { // Clear thread start markers. We don't need or want them anymore - if (elem.data.data_type() == data_type::thread_start) { + if (elem.name == InvalidHash && (elem.data.is_thread_start() || elem.data.is_jump_marker())) { + // Clear it out elem.name = NulledHashId; - } - // TODO: Threads with negative offsets - - // TODO: Jump markers! + // Check if this is a jump, if so we need to ignore even more data + jumping = elem.data.thread_jump(); + } } }, [](entry& elem) { return elem.name == NulledHashId; }); @@ -214,9 +336,9 @@ namespace ink::runtime::internal base::forget([](entry& elem) { elem.name = ~0; }); } - void basic_stack::add(hash_t name, const value& val) + entry& basic_stack::add(hash_t name, const value& val) { - base::push({ name, val }); + return base::push({ name, val }); } basic_eval_stack::basic_eval_stack(value* data, size_t size) diff --git a/inkcpp/stack.h b/inkcpp/stack.h index a35bb4b6..6bfdec39 100644 --- a/inkcpp/stack.h +++ b/inkcpp/stack.h @@ -68,9 +68,11 @@ namespace ink void forget(); private: - void add(hash_t name, const value& val); + entry& add(hash_t name, const value& val); const entry* pop(); + entry* do_thread_jump_pop(const iterator& jump); + // thread ids thread_t _next_thread = 0; thread_t _backup_next_thread = 0; diff --git a/inkcpp/value.h b/inkcpp/value.h index 096f1841..a8bc93b3 100644 --- a/inkcpp/value.h +++ b/inkcpp/value.h @@ -33,6 +33,7 @@ namespace ink function_frame, // Return from function thread_start, // Start of a new thread frame thread_end, // End of a thread frame + jump_marker, // Used to mark a callstack jump }; // Container for any data used as part of the runtime (variable values, output streams, evaluation stack, etc.) @@ -96,6 +97,7 @@ namespace ink int as_int() const { return _first.integer_value; } float as_float() const { return _first.float_value; } uint32_t as_divert() const { return _first.uint_value; } + uint32_t as_thread_id() const { return _first.uint_value; } // TODO: String access? template @@ -126,7 +128,12 @@ namespace ink inline operator float() const { return as_float(); } inline operator uint32_t() const { return as_divert(); } + // == Threading == inline bool is_thread_marker() const { return _first.type == data_type::thread_start || _first.type == data_type::thread_end; } + inline bool is_thread_end() const { return _first.type == data_type::thread_end; } + inline bool is_thread_start() const { return _first.type == data_type::thread_start; } + inline bool is_jump_marker() const { return _first.type == data_type::jump_marker; } + inline uint32_t& thread_jump() { return _second.uint_value; } // Is this value "true" bool is_truthy() const; diff --git a/inkcpp_test/Callstack.cpp b/inkcpp_test/Callstack.cpp index d71ea060..885090e0 100644 --- a/inkcpp_test/Callstack.cpp +++ b/inkcpp_test/Callstack.cpp @@ -176,6 +176,175 @@ SCENARIO("threading with the callstack", "[callstack]") { REQUIRE(type == frame_type::tunnel); REQUIRE(offset == 505); + + REQUIRE(*stack.get(X) == value(100)); + REQUIRE(*stack.get(Y) == value(200)); + } + + stack.complete_thread(thread); + + WHEN("we collapse to that thread") + { + stack.collapse_to_thread(thread); + THEN("the stack should be outside the tunnel") + { + REQUIRE(*stack.get(X) == value(100)); + REQUIRE(*stack.get(Y) == value(200)); + } + } + + WHEN("we collapse back to the main thread") + { + stack.collapse_to_thread(~0); + THEN("the stack should be inside the tunnel") + { + REQUIRE(*stack.get(X) == value(101)); + REQUIRE(*stack.get(Y) == value(201)); + } + + WHEN("we do a tunnel return") + { + frame_type type; + auto offset = stack.pop_frame(&type); + + THEN("we should be back outside") + { + REQUIRE(type == frame_type::tunnel); + REQUIRE(offset == 505); + REQUIRE(*stack.get(X) == value(100)); + REQUIRE(*stack.get(Y) == value(200)); + } + } + } + } + + WHEN("that thread completes and we pop off the main thread") + { + stack.complete_thread(thread); + frame_type type; + auto offset = stack.pop_frame(&type); + + THEN("we should be outside the tunnel") + { + REQUIRE(*stack.get(X) == value(100)); + REQUIRE(*stack.get(Y) == value(200)); + } + + THEN("collapsing to the thread will have the correct callstack") + { + stack.collapse_to_thread(thread); + + REQUIRE(*stack.get(X) == value(101)); + REQUIRE(*stack.get(Y) == value(201)); + } + } + } + } + + GIVEN("A thread with a frame pushed") + { + // Create the stack + auto stack = ink::runtime::internal::stack<50>(); + + // Create the thread + thread_t thread = stack.fork_thread(); + + // Set X and Y temporary variables + stack.set(X, 100); + stack.set(Y, 200); + + // Push a tunnel + stack.push_frame(505, frame_type::tunnel); + + // Push some more temps + stack.set(X, 101); + stack.set(Y, 201); + + WHEN("a second thread is forked off the first") + { + thread_t thread2 = stack.fork_thread(); + + WHEN("the second thread ends") + { + stack.complete_thread(thread2); + + WHEN("the first thread does a pop") + { + frame_type _ignore; + stack.pop_frame(&_ignore); + + THEN("accessing the variable should return the original") + { + REQUIRE(*stack.get(X) == value(100)); + REQUIRE(*stack.get(Y) == value(200)); + } + + WHEN("the first thread ends") + { + stack.complete_thread(thread); + + THEN("collapsing to the first thread should return the correct variables") + { + stack.collapse_to_thread(thread); + + REQUIRE(*stack.get(X) == value(100)); + REQUIRE(*stack.get(Y) == value(200)); + } + } + } + } + } + } + + GIVEN("A thread with two frames pushed") + { + // Create the stack + auto stack = ink::runtime::internal::stack<50>(); + + // Create the thread + thread_t thread = stack.fork_thread(); + + // Set X and Y temporary variables + stack.set(X, 100); + stack.set(Y, 200); + + // Push a tunnel + stack.push_frame(505, frame_type::tunnel); + + // Push some more temps + stack.set(X, 101); + stack.set(Y, 201); + + // Push another tunnel + stack.push_frame(505, frame_type::tunnel); + + // Push some more temps + stack.set(X, 102); + stack.set(Y, 202); + + WHEN("another thread is started and completed on top of it") + { + thread_t thread2 = stack.fork_thread(); + stack.complete_thread(thread2); + + WHEN("we then try to pop both frames") + { + frame_type _ignore; + stack.pop_frame(&_ignore); + stack.pop_frame(&_ignore); + + THEN("we should have access to the original variables") + { + REQUIRE(*stack.get(X) == value(100)); + REQUIRE(*stack.get(Y) == value(200)); + } + + THEN("collapsing to the thread should also get us the original variables") + { + stack.complete_thread(thread); + stack.collapse_to_thread(thread); + REQUIRE(*stack.get(X) == value(100)); + REQUIRE(*stack.get(Y) == value(200)); } } } From 84a448601eaa2cb5e9b6b7b4c9c68c9687a6e632 Mon Sep 17 00:00:00 2001 From: Brook Warner Jensen Date: Wed, 23 Dec 2020 11:35:04 -0800 Subject: [PATCH 6/9] Thread support --- inkcpp/array.h | 27 ++++++++ inkcpp/choice.cpp | 3 +- inkcpp/collections/restorable.h | 2 +- inkcpp/include/choice.h | 3 +- inkcpp/runner_impl.cpp | 119 ++++++++++++++++++++++++++++---- inkcpp/runner_impl.h | 18 +++-- inkcpp/stack.cpp | 83 ++++++++++++++++++++-- inkcpp/stack.h | 5 +- inkcpp/value.h | 2 + inkcpp_compiler/command.cpp | 1 + inkcpp_test/Callstack.cpp | 12 ++++ shared/private/command.h | 6 ++ shared/public/system.h | 6 ++ 13 files changed, 259 insertions(+), 28 deletions(-) diff --git a/inkcpp/array.h b/inkcpp/array.h index e507ccff..ae2010a9 100644 --- a/inkcpp/array.h +++ b/inkcpp/array.h @@ -42,6 +42,9 @@ namespace ink::runtime::internal void restore(); void forget(); + // Resets all values and clears any save points + void clear(const T& value); + protected: inline T* buffer() { return _array; } @@ -131,6 +134,30 @@ namespace ink::runtime::internal } } + template + inline void basic_restorable_array::clear(const T& value) + { + _saved = false; + for (size_t i = 0; i < _capacity; i++) + { + _temp[i] = null; + _array[i] = value; + } + } + + template + class fixed_restorable_array : public basic_restorable_array + { + public: + fixed_restorable_array(const T& initial) : basic_restorable_array(_buffer, SIZE * 2) + { + clear(initial); + } + + private: + T _buffer[SIZE * 2]; + }; + template class allocated_restorable_array : public basic_restorable_array { diff --git a/inkcpp/choice.cpp b/inkcpp/choice.cpp index 3bf2a623..028e8656 100644 --- a/inkcpp/choice.cpp +++ b/inkcpp/choice.cpp @@ -5,7 +5,7 @@ namespace ink { namespace runtime { - void choice::setup(internal::basic_stream& in, internal::string_table& strings, int index, uint32_t path) + void choice::setup(internal::basic_stream& in, internal::string_table& strings, int index, uint32_t path, thread_t thread) { // if we only have one item in our output stream if (in.queued() == 2) @@ -32,6 +32,7 @@ namespace ink // Index/path _index = index; _path = path; + _thread = thread; } } } \ No newline at end of file diff --git a/inkcpp/collections/restorable.h b/inkcpp/collections/restorable.h index edd1ebd2..e3a73581 100644 --- a/inkcpp/collections/restorable.h +++ b/inkcpp/collections/restorable.h @@ -173,7 +173,7 @@ namespace ink::runtime::internal _pos = _jump; // Move over empty data - while (isNull(_buffer[_pos])) + while (isNull(_buffer[_pos - 1])) _pos--; // Decrement and return diff --git a/inkcpp/include/choice.h b/inkcpp/include/choice.h index 47a79a08..5938043c 100644 --- a/inkcpp/include/choice.h +++ b/inkcpp/include/choice.h @@ -46,11 +46,12 @@ namespace ink friend class internal::runner_impl; uint32_t path() const { return _path; } - void setup(internal::basic_stream&, internal::string_table& strings, int index, uint32_t path); + void setup(internal::basic_stream&, internal::string_table& strings, int index, uint32_t path, thread_t thread); private: const char* _text; int _index; uint32_t _path; + thread_t _thread; }; } } diff --git a/inkcpp/runner_impl.cpp b/inkcpp/runner_impl.cpp index 5b71eb5d..a4810494 100644 --- a/inkcpp/runner_impl.cpp +++ b/inkcpp/runner_impl.cpp @@ -213,7 +213,7 @@ namespace ink::runtime::internal _eval.push(result); } - void runner_impl::execute_return() + frame_type runner_impl::execute_return() { // Pop the callstack frame_type type; @@ -226,11 +226,14 @@ namespace ink::runtime::internal // Jump to the old offset inkAssert(_story->instructions() + offset < _story->end(), "Callstack return is outside bounds of story!"); jump(_story->instructions() + offset); + + // Return frame type + return type; } runner_impl::runner_impl(const story_impl* data, globals global) - : _story(data), _globals(global.cast()), _container(~0), - _backup(nullptr), _done(false), _choices() + : _story(data), _globals(global.cast()), _container(~0), _threads(~0), + _threadDone(nullptr), _backup(nullptr), _done(nullptr), _choices() { _ptr = _story->instructions(); bEvaluationMode = false; @@ -350,12 +353,33 @@ namespace ink::runtime::internal void runner_impl::choose(size_t index) { - // Restore pointer to the last "done" point - inkAssert(_done != nullptr, "No 'done' point recorded before finishing choice output"); inkAssert(index < _num_choices, "Choice index out of range"); - jump(_done, false); + + // Get the choice + const auto& c = _choices[index]; + + // Get its thread + thread_t choiceThread = c._thread; + + // Figure out where our previous pointer was for that thread + ip_t prev = nullptr; + if (choiceThread == ~0) + prev = _done; + else + prev = _threadDone.get(choiceThread); + + // Make sure we have a previous pointer + inkAssert(prev != nullptr, "No 'done' point recorded before finishing choice output"); + + // Move to the previous pointer so we track our movements correctly + jump(prev, false); _done = nullptr; + // Collapse callstacks to the correct thread + _stack.collapse_to_thread(choiceThread); + _threads.clear(); + _threadDone.clear(nullptr); + // Jump to destination and clear choice list jump(_story->instructions() + _choices[index].path()); clear_choices(); @@ -453,7 +477,7 @@ namespace ink::runtime::internal if (_output.ends_with(data_type::newline)) { // TODO: REMOVE - return true; + // return true; // Unless we are out of content, we are going to try // to continue a little further. This is to check for @@ -488,7 +512,7 @@ namespace ink::runtime::internal if (_is_falling && !((cmd == Command::DIVERT && flag & CommandFlag::DIVERT_IS_FALLTHROUGH) || cmd == Command::END_CONTAINER_MARKER)) { _is_falling = false; - _done = nullptr; + set_done_ptr(nullptr); } if (cmd >= Command::BINARY_OPERATORS_START && cmd <= Command::BINARY_OPERATORS_END) @@ -578,13 +602,13 @@ namespace ink::runtime::internal // Record the position of the instruction pointer at the first fallthrough. // We'll use this if we run out of content and hit an implied "done" to restore // our position when a choice is chosen. See ::choose - _done = _ptr; + set_done_ptr(_ptr); _is_falling = true; } - // If we're falling out of the story, then we're hitting an implied done + // If we're falling out of the story, then we're hitting an implied done if (_is_falling && _story->instructions() + target == _story->end()) { - _ptr = nullptr; + on_done(false); break; } @@ -612,7 +636,9 @@ namespace ink::runtime::internal // == Terminal commands case Command::DONE: - _done = _ptr; + on_done(true); + break; + case Command::END: _ptr = nullptr; break; @@ -642,6 +668,21 @@ namespace ink::runtime::internal { execute_return(); } + break; + + case Command::THREAD: + { + // Push a thread frame so we can return easily + // TODO We push ahead of a single divert. Is that correct in all cases....????? + auto returnTo = _ptr + CommandSize; + _stack.push_frame(returnTo - _story->instructions(), frame_type::thread); + + // Fork a new thread on the callstack + thread_t thread = _stack.fork_thread(); + + // Push that thread onto our thread stack + _threads.push(thread); + } break; // == Variable definitions @@ -803,7 +844,7 @@ namespace ink::runtime::internal if (flag & CommandFlag::CHOICE_IS_INVISIBLE_DEFAULT) {} // TODO // Create choice and record it - add_choice().setup(_output, _globals->strings(), _num_choices, path); + add_choice().setup(_output, _globals->strings(), _num_choices, path, current_thread()); } break; case Command::START_CONTAINER_MARKER: { @@ -831,7 +872,13 @@ namespace ink::runtime::internal { _is_falling = false; - if (_stack.has_frame()) + frame_type type; + if (!_threads.empty()) + { + on_done(false); + return; + } + else if (_stack.has_frame(&type) && type == frame_type::function) // implicit return is only for functions { // push null and return _eval.push(value()); @@ -842,7 +889,7 @@ namespace ink::runtime::internal } else { - _ptr = nullptr; + on_done(false); // do we need to not set _done here? It wasn't set in the original code #implieddone return; } } @@ -894,11 +941,47 @@ namespace ink::runtime::internal } } + void runner_impl::on_done(bool setDone) + { + // If we're in a thread + if (!_threads.empty()) + { + // Get the thread ID of the current thread + thread_t completedThreadId = _threads.pop(); + + // Push in a complete marker + _stack.complete_thread(completedThreadId); + + // Go to where the thread started + frame_type type = execute_return(); + inkAssert(type == frame_type::thread, "Expected thread frame marker to hold return to value but none found..."); + } + else + { + if (setDone) + set_done_ptr(_ptr); + _ptr = nullptr; + } + } + + void runner_impl::set_done_ptr(ip_t ptr) + { + thread_t curr = current_thread(); + if (curr == ~0) { + _done = ptr; + } + else { + _threadDone.set(curr, ptr); + } + } + void runner_impl::reset() { _eval.clear(); _output.clear(); _stack.clear(); + _threads.clear(); + _threadDone.clear(nullptr); bEvaluationMode = false; _saved = false; _num_choices = 0; @@ -930,6 +1013,8 @@ namespace ink::runtime::internal _container.save(); _globals->save(); _eval.save(); + _threads.save(); + _threadDone.save(); bSavedEvaluationMode = bEvaluationMode; // Not doing this anymore. There can be lingering stack entries from function returns @@ -946,6 +1031,8 @@ namespace ink::runtime::internal _container.restore(); _globals->restore(); _eval.restore(); + _threads.restore(); + _threadDone.restore(); bEvaluationMode = bSavedEvaluationMode; // Not doing this anymore. There can be lingering stack entries from function returns @@ -965,6 +1052,8 @@ namespace ink::runtime::internal _container.forget(); _globals->forget(); _eval.forget(); + _threads.forget(); + _threadDone.forget(); // Nothing to do for eval stack. It should just stay as it is diff --git a/inkcpp/runner_impl.h b/inkcpp/runner_impl.h index ce94506b..e1ab909c 100644 --- a/inkcpp/runner_impl.h +++ b/inkcpp/runner_impl.h @@ -10,6 +10,7 @@ #include "types.h" #include "functions.h" #include "string_table.h" +#include "array.h" #include "runner.h" #include "choice.h" @@ -119,7 +120,12 @@ namespace ink::runtime::internal void run_binary_operator(unsigned char cmd); void run_unary_operator(unsigned char cmd); - void execute_return(); + frame_type execute_return(); + + void on_done(bool setDone); + void set_done_ptr(ip_t ptr); + + inline thread_t current_thread() const { return _threads.empty() ? ~0 : _threads.top(); } private: const story_impl* const _story; @@ -138,10 +144,14 @@ namespace ink::runtime::internal // Runtime stack. Used to store temporary variables and callstack internal::stack<50> _stack; - // Evaluation (NOTE: Will later need to be per-callstack entry) - bool bEvaluationMode; + // Evaluation stack + bool bEvaluationMode = false; internal::eval_stack<20> _eval; - bool bSavedEvaluationMode; + bool bSavedEvaluationMode = false; + + // Keeps track of what threads we're inside + internal::restorable_stack _threads; + internal::fixed_restorable_array _threadDone; // Choice list static const size_t MAX_CHOICES = 10; diff --git a/inkcpp/stack.cpp b/inkcpp/stack.cpp index ad9de0a9..71feb8d8 100644 --- a/inkcpp/stack.cpp +++ b/inkcpp/stack.cpp @@ -82,8 +82,22 @@ namespace ink::runtime::internal void basic_stack::push_frame(offset_t return_to, frame_type type) { + data_type frameDataType; + switch (type) + { + case frame_type::tunnel: + frameDataType = data_type::tunnel_frame; + break; + case frame_type::function: + frameDataType = data_type::function_frame; + break; + case frame_type::thread: + frameDataType = data_type::thread_frame; + break; + } + // Add to top of stack - add(InvalidHash, value(return_to, type == frame_type::tunnel ? data_type::tunnel_frame : data_type::function_frame)); + add(InvalidHash, value(return_to, frameDataType)); } const entry* basic_stack::pop() @@ -135,6 +149,22 @@ namespace ink::runtime::internal return threadIter.get(); } + frame_type get_frame_type(data_type type) + { + switch (type) + { + case data_type::tunnel_frame: + return frame_type::tunnel; + case data_type::function_frame: + return frame_type::function; + case data_type::thread_frame: + return frame_type::thread; + default: + inkAssert(false, "Unknown frame type detected"); + return (frame_type)-1; + } + } + offset_t basic_stack::pop_frame(frame_type* type) { inkAssert(!base::is_empty(), "Can not pop frame from empty callstack."); @@ -200,23 +230,60 @@ namespace ink::runtime::internal // Store frame type if (type != nullptr) - *type = (returnedFrame->data.data_type() == data_type::tunnel_frame) ? frame_type::tunnel : frame_type::function; + { + *type = get_frame_type(returnedFrame->data.data_type()); + } // Return the offset stored in the frame record return returnedFrame->data.as_divert(); } - bool basic_stack::has_frame() const + bool basic_stack::has_frame(frame_type* returnType) const { // Empty case if (base::is_empty()) return false; + uint32_t jumping = 0; + uint32_t thread = ~0; // Search in reverse for a stack frame - const entry* frame = base::reverse_find([](const entry& elem) { + const entry* frame = base::reverse_find([&jumping, &thread](const entry& elem) { + // If we're jumping over data, just keep returning false until we're done + if (jumping > 0) { + jumping--; + return false; + } + + // We only care about elements with InvalidHash + if (elem.name != InvalidHash) + return false; + + // If we're skipping over a thread, wait until we hit its start before checking + if (thread != ~0) { + if (elem.data.is_thread_start() && elem.data.as_thread_id() == thread) + thread = ~0; + + return false; + } + + // If it's a jump marker or a thread start + if (elem.data.is_jump_marker() || elem.data.is_thread_start()) { + jumping = elem.data.thread_jump(); + return false; + } + + // If it's a thread end, we need to skip to the matching thread start + if (elem.data.is_thread_end()) { + thread = elem.data.as_thread_id(); + return false; + } + return elem.name == InvalidHash; }); + if (frame != nullptr && returnType != nullptr) + *returnType = get_frame_type(frame->data.data_type()); + // Return true if a frame was found return frame != nullptr; } @@ -255,6 +322,9 @@ namespace ink::runtime::internal void basic_stack::collapse_to_thread(thread_t thread) { + // Reset thread counter + _next_thread = 0; + // If we're restoring a specific thread (and not the main thread) if (thread != ~0) { @@ -307,6 +377,11 @@ namespace ink::runtime::internal // Check if this is a jump, if so we need to ignore even more data jumping = elem.data.thread_jump(); } + + // Clear thread frame markers. We can't use them anymore + if (elem.name == InvalidHash && elem.data.data_type() == data_type::thread_frame) { + elem.name = NulledHashId; + } } }, [](entry& elem) { return elem.name == NulledHashId; }); diff --git a/inkcpp/stack.h b/inkcpp/stack.h index 6bfdec39..6108061c 100644 --- a/inkcpp/stack.h +++ b/inkcpp/stack.h @@ -18,7 +18,8 @@ namespace ink enum class frame_type : uint32_t { function, - tunnel + tunnel, + thread }; class basic_stack : protected restorable @@ -43,7 +44,7 @@ namespace ink offset_t pop_frame(frame_type* type); // Returns true if there are any frames on the stack - bool has_frame() const; + bool has_frame(frame_type* type = nullptr) const; // Clears the entire stack void clear(); diff --git a/inkcpp/value.h b/inkcpp/value.h index a8bc93b3..99872c8c 100644 --- a/inkcpp/value.h +++ b/inkcpp/value.h @@ -31,6 +31,7 @@ namespace ink null, // void/null (used for void function returns) tunnel_frame, // Return from tunnel function_frame, // Return from function + thread_frame, // Special tunnel marker for returning from threads thread_start, // Start of a new thread frame thread_end, // End of a thread frame jump_marker, // Used to mark a callstack jump @@ -134,6 +135,7 @@ namespace ink inline bool is_thread_start() const { return _first.type == data_type::thread_start; } inline bool is_jump_marker() const { return _first.type == data_type::jump_marker; } inline uint32_t& thread_jump() { return _second.uint_value; } + inline uint32_t thread_jump() const { return _second.uint_value; } // Is this value "true" bool is_truthy() const; diff --git a/inkcpp_compiler/command.cpp b/inkcpp_compiler/command.cpp index 86090f04..0648276b 100644 --- a/inkcpp_compiler/command.cpp +++ b/inkcpp_compiler/command.cpp @@ -38,6 +38,7 @@ namespace ink "/str", "CHOICE", + "thread", "+", "-", diff --git a/inkcpp_test/Callstack.cpp b/inkcpp_test/Callstack.cpp index 885090e0..39ab64ad 100644 --- a/inkcpp_test/Callstack.cpp +++ b/inkcpp_test/Callstack.cpp @@ -145,6 +145,18 @@ SCENARIO("threading with the callstack", "[callstack]") } } } + + WHEN("there is a fork with a tunnel that finishes") + { + thread_t thread = stack.fork_thread(); + stack.push_frame(555, frame_type::tunnel); + stack.complete_thread(thread); + + THEN("there should be no frames on the stack") + { + REQUIRE(stack.has_frame() == false); + } + } } GIVEN("a callstack with a single tunnel pushed") diff --git a/shared/private/command.h b/shared/private/command.h index 5988f724..0c904f88 100644 --- a/shared/private/command.h +++ b/shared/private/command.h @@ -49,6 +49,9 @@ namespace ink // == Choice commands CHOICE, + // == Threading + THREAD, + // == Binary operators BINARY_OPERATORS_START, ADD = BINARY_OPERATORS_START, @@ -119,6 +122,9 @@ namespace ink return lhs; } + template + constexpr unsigned int CommandSize = sizeof(Command) + sizeof(CommandFlag) + sizeof(PayloadType); + #ifdef INK_COMPILER const char* CommandStrings[]; #endif diff --git a/shared/public/system.h b/shared/public/system.h index cca1f289..ae8f9a10 100644 --- a/shared/public/system.h +++ b/shared/public/system.h @@ -139,6 +139,12 @@ namespace ink { static constexpr T* value = nullptr; }; + + template<> + struct restorable_type_null + { + static constexpr ip_t value = (ip_t)~0; + }; } } From d8053f3e4afbb5f71f9edaa441d9c570087714d5 Mon Sep 17 00:00:00 2001 From: Brook Warner Jensen Date: Wed, 23 Dec 2020 11:44:34 -0800 Subject: [PATCH 7/9] fixed inline seqeuence test --- inkcpp/runner_impl.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/inkcpp/runner_impl.cpp b/inkcpp/runner_impl.cpp index a4810494..6135822f 100644 --- a/inkcpp/runner_impl.cpp +++ b/inkcpp/runner_impl.cpp @@ -477,7 +477,7 @@ namespace ink::runtime::internal if (_output.ends_with(data_type::newline)) { // TODO: REMOVE - // return true; + return true; // Unless we are out of content, we are going to try // to continue a little further. This is to check for @@ -887,11 +887,11 @@ namespace ink::runtime::internal _ptr += sizeof(Command) + sizeof(CommandFlag); execute_return(); } - else + /*else TODO I had to remove this to make a test work.... is this important? Have I broken something? { on_done(false); // do we need to not set _done here? It wasn't set in the original code #implieddone return; - } + }*/ } } break; case Command::VISIT: From 8ee2fbef0233b66434576f53bb0baa08b023d0c1 Mon Sep 17 00:00:00 2001 From: Brook Warner Jensen Date: Wed, 20 Jan 2021 18:43:39 -0800 Subject: [PATCH 8/9] Making runtime output use the same convetion as inklecate for inkcpp testing --- inkcpp_cl/inkcpp_cl.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/inkcpp_cl/inkcpp_cl.cpp b/inkcpp_cl/inkcpp_cl.cpp index ccc1e5f0..46b02e79 100644 --- a/inkcpp_cl/inkcpp_cl.cpp +++ b/inkcpp_cl/inkcpp_cl.cpp @@ -133,14 +133,16 @@ int main(int argc, const char** argv) if (thread->has_choices()) { + int index = 1; for (const ink::runtime::choice& c : *thread) { - std::cout << "* " << c.text() << std::endl; + std::cout << index++ << ": " << c.text() << std::endl; } int c = 0; std::cin >> c; - thread->choose(c); + thread->choose(c - 1); + std::cout << "?> "; continue; } From b407e9fc61680ecbdc85cc3a898b8b1a3d53629c Mon Sep 17 00:00:00 2001 From: Brook Warner Jensen Date: Wed, 20 Jan 2021 18:47:28 -0800 Subject: [PATCH 9/9] Updating Readme --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 00397bcc..951342b7 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,11 @@ thread->choose(0); ``` ## Current Status -`inkcpp.cpp` has a `main()` function which compiles `test.json` -> `test.bin` and executes it using standard output and input. +Run `inkcpp_cl.exe -p myfile.json` to execute a compiled Ink JSON file in play mode. It can also operate on `.ink` files but `inklecate.exe` must me in the same folder or in the PATH. -Many, but not all features of the Ink language are supported (see Glaring Omissions below). +Without the `-p` flag, it'll just compile the JSON/Ink file into InkCPP's binary format (see the Wiki on GitHub). + +Many, but not all features of the Ink language are supported (see Glaring Omissions below), but be warned, this runtime is still highly unstable. I am currently working on getting it to pass all the unit tests on [ink-proof](https://github.com/chromy/ink-proof). * Temporary and global variables * Int, String, or Divert values @@ -57,6 +59,7 @@ Many, but not all features of the Ink language are supported (see Glaring Omissi * Global store that can be shared between runners * External function binding (no fallback support yet) * Tunnels and internal functions +* Ink threads (probably incredibly unstable though) ## CMake Project is organized using `cmake`. Just run `cmake` and it should configure all the projects properly into a runtime, compiler, and command line project. @@ -75,7 +78,6 @@ Part of that involves slowly migrating all the unit tests from the main inkle in The big things we're missing right now are: * Fallback functions for externals. -* Threads * Variable observers * Lists and whatever cool, crazy stuff Ink has been adding recently.