From e55613ba57e20e3cf863334bdc8d7ebbb66878d9 Mon Sep 17 00:00:00 2001 From: JBenda Date: Sun, 17 Dec 2023 15:08:49 +0100 Subject: [PATCH] Fixing output of strings with non printable characters closes #66 closes #63 + Disable shortcut in `jump` and add check for strings only containing spaces in output + Added tests from issues + version bump pipeline to avoid deprecation warning + add formatting + re introduced ink proof result label --- .clang-format | 99 + .github/workflows/build.yml | 34 +- README.md | 4 +- inkcpp/array.h | 776 +++--- inkcpp/output.cpp | 731 +++-- inkcpp/runner_impl.cpp | 2446 +++++++++-------- inkcpp/story_impl.cpp | 4 - inkcpp/story_ptr.cpp | 2 +- inkcpp_cl/inkcpp_cl.cpp | 144 +- inkcpp_test/CMakeLists.txt | 2 + inkcpp_test/SpaceAfterBracketChoice.cpp | 48 + inkcpp_test/ThirdTierChoiceAfterBrackets.cpp | 43 + inkcpp_test/ink/ChoiceBracketStory.ink | 7 + .../ink/ThirdTierChoiceAfterBracketsStory.ink | 13 + proofing/ink-proof | 2 +- proofing/inkcpp_runtime_driver | 2 +- 16 files changed, 2291 insertions(+), 2066 deletions(-) create mode 100644 .clang-format create mode 100644 inkcpp_test/SpaceAfterBracketChoice.cpp create mode 100644 inkcpp_test/ThirdTierChoiceAfterBrackets.cpp create mode 100644 inkcpp_test/ink/ChoiceBracketStory.ink create mode 100644 inkcpp_test/ink/ThirdTierChoiceAfterBracketsStory.ink diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..b5be3ec7 --- /dev/null +++ b/.clang-format @@ -0,0 +1,99 @@ +--- +AccessModifierOffset: -2 +AlignAfterOpenBracket: BlockIndent +AlignArrayOfStructures: Right +# readd padOperation if bumped from clang 14 +AlignConsecutiveAssignments: AcrossComments +AlignConsecutiveBitFields: AcrossComments +AlignConsecutiveDeclarations: AcrossComments +AlignConsecutiveMacros: AcrossComments +AlignEscapedNewlines: Left +AlignOperands: AlignAfterOperator +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: true +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Inline +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BreakBeforeBraces: Linux +# BreakAfterAttributes: Always +BreakBeforeBinaryOperators: All +UseTab: ForIndentation +Standard: Latest +SpacesInSquareBrackets: false +SpacesInParentheses: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: 1 +SpacesInContainerLiterals: false +SpacesInConditionalStatement: false +SpacesInCStyleCastParentheses: true +SpacesInAngles: Never +SpacesBeforeTrailingComments: 1 +SpaceInEmptyParentheses: false +SpaceInEmptyBlock: false +SpaceBeforeSquareBrackets: false +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeInheritanceColon: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCaseColon: false +SpaceBeforeAssignmentOperators: true +SpaceAroundPointerQualifiers: Before +PointerAlignment: Left +SpaceAfterTemplateKeyword: false +SpaceAfterLogicalNot: true +SpaceAfterCStyleCast: true +SortUsingDeclarations: false +SortIncludes: Never +ShortNamespaceLines: 0 +SeparateDefinitionBlocks: Always +# RequiresExpressionIndentation: OuterScope +# BreakBeforeConceptDeclarations: Always +# BreakBeforeInlineASMColon: Always +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeComma +BreakStringLiterals: true +ColumnLimit: 100 +CompactNamespaces: true +Cpp11BracedListStyle: true +EmptyLineBeforeAccessModifier: Always +FixNamespaceComments: true +IncludeBlocks: Preserve +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: true +IndentExternBlock: Indent +IndentGotoLabels: false +IndentPPDirectives: AfterHash +# IndentRequiresClause: true +IndentWidth: 2 +TabWidth: 2 +IndentWrappedFunctionNames: true +# InsertBraces: true +# InsertNewlineAtEOF: true +KeepEmptyLinesAtTheStartOfBlocks: true +LambdaBodyIndentation: Signature +Language: Cpp +# LineEnding: LF +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: Inner +QualifierAlignment: Left +ReferenceAlignment: Left +ReflowComments: true +RemoveBracesLLVM: false +# RemoveSemicolon: false +# RequiresClausePosition: OwnLine +UseCRLF: false +DeriveLineEnding: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7865218..92af0cac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,7 +39,7 @@ jobs: submodules: true # Download inklecate - - uses: suisei-cn/actions-download-file@v1 + - uses: suisei-cn/actions-download-file@v1.4.0 name: Download Inklecate id: download_inklecate with: @@ -62,7 +62,7 @@ jobs: # Setup CMake - name: Setup cmake - uses: jwlawson/actions-setup-cmake@v1.9 + uses: jwlawson/actions-setup-cmake@v1.14.2 with: cmake-version: '3.21.x' @@ -144,7 +144,7 @@ jobs: shell: bash working-directory: proofing/ink-proof run: | - python3 proof.py --examples 'I...' inkcpp inklecate_v0.9.0 > run.out + python3 proof.py --examples 'I...' inklecate_v1.1.1 inkcpp_runtime > run.out echo -e "| ${{ matrix.name }} | $(cat run.out) |" > ${{ matrix.artifact }}.txt # Creates a "disabled" artifact if ink proofing is disabled @@ -170,11 +170,33 @@ jobs: name: ${{ matrix.artifact }}-www path: proofing/ink-proof/out + clang-format: + name: "Check Formatting" + runs-on: ubuntu-latest + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' }} + steps: + - uses: actions/checkout@v3 + - name: Fetch master branch + run: | + git fetch origin master --depth 1 + - name: Check clang-format + run: | + diff=$(git clang-format-14 --style file -q --diff origin/master) + echo $diff + if [ "$diff" != "" ]; then + echo run git clang-format --style file master + echo or upstream/master depending on your setup + clang + fi + reporting: name: "Pull Request Report" - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} + # if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'brwarner/inkcpp' }} + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'master' }} runs-on: ubuntu-latest - needs: compilation + needs: [compilation, clang-format] + permissions: + pull-requests: write steps: # Download Ink Proof Results - uses: actions/download-artifact@v2 @@ -200,7 +222,7 @@ jobs: done # Post Comment - - uses: marocchino/sticky-pull-request-comment@v2 + - uses: marocchino/sticky-pull-request-comment@v2.8.0 with: recreate: true path: comment.txt diff --git a/README.md b/README.md index c75dd990..2304c1b2 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # inkcpp -![build](https://github.com/brwarner/inkcpp/workflows/build/badge.svg "Build Status") +![build](https://github.com/JBenda/inkcpp/workflows/build/badge.svg "Build Status") Inkle Ink C++ Runtime with JSON -> Binary Compiler. -Ink Proofing Test Results: https://brwarner.github.io/inkcpp +Ink Proofing Test Results: https://jbenda.github.io/inkcpp ## Project Goals * Fast, simple, clean syntax diff --git a/inkcpp/array.h b/inkcpp/array.h index 5c3ac652..0be3484b 100644 --- a/inkcpp/array.h +++ b/inkcpp/array.h @@ -4,464 +4,462 @@ #include "system.h" #include "traits.h" -namespace ink::runtime::internal { - template - class managed_array : public snapshot_interface +namespace ink::runtime::internal +{ +template +class managed_array : public snapshot_interface +{ +public: + managed_array() + : _static_data{} + , _capacity{initialCapacity} + , _size{0} { - public: - managed_array() - : _static_data{}, - _capacity{ initialCapacity }, - _size{ 0 } - { - if constexpr ( dynamic ) - { - _dynamic_data = new T[initialCapacity]; - } - } - - const T& operator[]( size_t i ) const { return data()[i]; } - T& operator[]( size_t i ) { return data()[i]; } - const T* data() const - { - if constexpr ( dynamic ) - { - return _dynamic_data; - } - else - { - return _static_data; - } - } - T* data() - { - if constexpr ( dynamic ) - { - return _dynamic_data; - } - else - { - return _static_data; - } - } - const T* begin() const { return data(); } - T* begin() { return data(); } - const T* end() const { return data() + _size; } - T* end() { return data() + _size; } - const T& back() const { return end()[-1]; } - T& back() { return end()[-1]; } - - const size_t size() const { return _size; } - const size_t capacity() const { return _capacity; } - T& push() - { - if constexpr ( dynamic ) - { - if ( _size == _capacity ) - { - extend(); - } - } - else - { - inkAssert( _size <= _capacity, "Stack Overflow!" ); - /// FIXME silent fail!! - } - return data()[_size++]; + if constexpr (dynamic) { + _dynamic_data = new T[initialCapacity]; } - void clear() { _size = 0; } - void resize( size_t size ) - { - if constexpr ( dynamic ) - { - if ( size > _capacity ) - { - extend( size ); - } - } - else - { - inkAssert( size <= _size, "Only allow to reduce size" ); - } - _size = size; - } - - void extend( size_t capacity = 0 ); - - size_t snap( unsigned char* data, const snapper& snapper) const - { - inkAssert( !is_pointer{}(), "here is a special case oversight" ); - unsigned char* ptr = data; - bool should_write = data != nullptr; - ptr = snap_write( ptr, _size, should_write ); - for (const T& e : *this) - { - if constexpr (is_base_of::value) { - ptr += e.snap(data == nullptr ? nullptr : ptr, snapper); - } else { - ptr = snap_write( ptr, e, should_write ); - } - } - return ptr - data; - } - const unsigned char* snap_load( const unsigned char* ptr, const loader& loader) - { - decltype( _size ) size; - ptr = snap_read( ptr, size ); - if constexpr ( dynamic ) - { - resize( size ); - } - else - { - inkAssert( size <= initialCapacity, "capacity of non dynamic array is to small vor snapshot!" ); - _size = size; - } - for ( T& e : *this ) - { - if constexpr (is_base_of::value) { - ptr = e.snap_load(ptr, loader); - } else { - ptr = snap_read( ptr, e ); - } - } - return ptr; - } - - private: - if_t _static_data[dynamic ? 1 : initialCapacity]; - T* _dynamic_data = nullptr; - size_t _capacity; - size_t _size; - }; + } - template - class managed_restorable_array : public managed_array + ~managed_array() { - using base = managed_array; - - public: - managed_restorable_array() - : base() {} - void restore() - { - base::resize( _last_size ); - } - void save() - { - _last_size = this->size(); - } - void forgett() - { - _last_size = 0; - } - bool has_changed() const - { - return base::size() != _last_size; - } - - size_t snap( unsigned char* data, const snapshot_interface::snapper& snapper ) const - { - unsigned char* ptr = data; - bool should_write = data != nullptr; - ptr += base::snap( ptr, snapper ); - ptr = base::snap_write( ptr, _last_size, should_write ); - return ptr - data; + if constexpr (dynamic) { + delete[] _dynamic_data; } + } - const unsigned char* snap_load(const unsigned char* ptr, const snapshot_interface::loader& loader) { - ptr = base::snap_load(ptr, loader); - ptr = base::snap_read(ptr, _last_size); - return ptr; - } + const T& operator[](size_t i) const { return data()[i]; } - private: - size_t _last_size = 0; - }; + T& operator[](size_t i) { return data()[i]; } - template - void managed_array::extend( size_t capacity ) + const T* data() const { - static_assert( dynamic, "Can only extend if array is dynamic!" ); - size_t new_capacity = capacity > _capacity - ? capacity - : 1.5f * _capacity; - if ( new_capacity < 5 ) - { - new_capacity = 5; + if constexpr (dynamic) { + return _dynamic_data; + } else { + return _static_data; } - T* new_data = new T[new_capacity]; - - for ( size_t i = 0; i < _capacity; ++i ) - { - new_data[i] = _dynamic_data[i]; - } - - delete[] _dynamic_data; - _dynamic_data = new_data; - _capacity = new_capacity; } - template - class basic_restorable_array : public snapshot_interface + T* data() { - public: - basic_restorable_array( T* array, size_t capacity, T nullValue ) - : _saved( false ), - _array( array ), - _temp( array + capacity / 2 ), - _capacity( capacity / 2 ), - _null( nullValue ) - { - inkAssert( capacity % 2 == 0, "basic_restorable_array requires a datablock of even length to split into two arrays" ); - - // zero out main array and put 'nulls' in the clear_temp() - inkZeroMemory( _array, _capacity * sizeof( T ) ); - clear_temp(); + if constexpr (dynamic) { + return _dynamic_data; + } else { + return _static_data; } + } - // == Non-Copyable == - basic_restorable_array( const basic_restorable_array& ) = delete; - basic_restorable_array& operator=( const basic_restorable_array& ) = delete; + const T* begin() const { return data(); } - // set value by index - void set( size_t index, const T& value ); + T* begin() { return data(); } - // get value by index - const T& get( size_t index ) const; + const T* end() const { return data() + _size; } - // size of the array - inline size_t capacity() const { return _capacity; } + T* end() { return data() + _size; } - // only const indexing is supported due to save/restore system - inline const T& operator[]( size_t index ) const { return get( index ); } + const T& back() const { return end()[-1]; } - // == Save/Restore == - void save(); - void restore(); - void forget(); + T& back() { return end()[-1]; } - // Resets all values and clears any save points - void clear( const T& value ); + const size_t size() const { return _size; } - // snapshot interface - virtual size_t snap( unsigned char* data, const snapper& ) const; - virtual const unsigned char* snap_load( const unsigned char* data, const loader& ); + const size_t capacity() const { return _capacity; } - protected: - inline T* buffer() { return _array; } - void set_new_buffer( T* buffer, size_t capacity ) - { - _array = buffer; - _temp = buffer + capacity / 2; - _capacity = capacity / 2; + T& push() + { + if constexpr (dynamic) { + if (_size == _capacity) { + extend(); + } + } else { + inkAssert(_size <= _capacity, "Stack Overflow!"); + /// FIXME silent fail!! } + return data()[_size++]; + } - private: - inline void check_index( size_t index ) const { inkAssert( index < capacity(), "Index out of range!" ); } - void clear_temp(); - - private: - bool _saved; - - // real values live here - T* _array; - - // we store values here when we're in save mode - // they're copied on a call to forget() - T* _temp; + void clear() { _size = 0; } - // size of both _array and _temp - size_t _capacity; + void resize(size_t size) + { + if constexpr (dynamic) { + if (size > _capacity) { + extend(size); + } + } else { + inkAssert(size <= _size, "Only allow to reduce size"); + } + _size = size; + } - // null - const T _null; - }; + void extend(size_t capacity = 0); - template - inline size_t basic_restorable_array::snap( unsigned char* data, const snapper& snapper ) const + size_t snap(unsigned char* data, const snapper& snapper) const { + inkAssert(! is_pointer{}(), "here is a special case oversight"); unsigned char* ptr = data; bool should_write = data != nullptr; - ptr = snap_write( ptr, _saved, should_write ); - ptr = snap_write( ptr, _capacity, should_write ); - ptr = snap_write( ptr, _null, should_write ); - for ( size_t i = 0; i < _capacity; ++i ) - { - ptr = snap_write( ptr, _array[i], should_write ); - ptr = snap_write( ptr, _temp[i], should_write ); + ptr = snap_write(ptr, _size, should_write); + for (const T& e : *this) { + if constexpr (is_base_of::value) { + ptr += e.snap(data == nullptr ? nullptr : ptr, snapper); + } else { + ptr = snap_write(ptr, e, should_write); + } } return ptr - data; } - template - inline const unsigned char* basic_restorable_array::snap_load( const unsigned char* data, const loader& loader ) + const unsigned char* snap_load(const unsigned char* ptr, const loader& loader) { - auto ptr = data; - ptr = snap_read( ptr, _saved ); - ptr = snap_read( ptr, _capacity ); - T null; - ptr = snap_read( ptr, null ); - inkAssert( null == _null, "null value is different to snapshot!" ); - for ( size_t i = 0; i < _capacity; ++i ) - { - ptr = snap_read( ptr, _array[i] ); - ptr = snap_read( ptr, _temp[i] ); + decltype(_size) size; + ptr = snap_read(ptr, size); + if constexpr (dynamic) { + resize(size); + } else { + inkAssert(size <= initialCapacity, "capacity of non dynamic array is to small vor snapshot!"); + _size = size; + } + for (T& e : *this) { + if constexpr (is_base_of::value) { + ptr = e.snap_load(ptr, loader); + } else { + ptr = snap_read(ptr, e); + } } return ptr; } - template - inline void basic_restorable_array::set( size_t index, const T& value ) +private: + if_t _static_data[dynamic ? 1 : initialCapacity]; + T* _dynamic_data = nullptr; + size_t _capacity; + size_t _size; +}; + +template +class managed_restorable_array : public managed_array +{ + using base = managed_array; + +public: + managed_restorable_array() + : base() { - check_index( index ); - inkAssert( value != _null, "Can not add a value considered a 'null' to a restorable_array" ); - - // If we're saved, store in second half of the array - if ( _saved ) - _temp[index] = value; - else - // Otherwise, store in the main array - _array[index] = value; } - template - inline const T& basic_restorable_array::get( size_t index ) const - { - check_index( index ); + void restore() { base::resize(_last_size); } - // If we're in save mode and we have a value at that index, return that instead - if ( _saved && _temp[index] != _null ) - return _temp[index]; + void save() { _last_size = this->size(); } - // Otherwise, fall back on the real array - return _array[index]; - } + void forgett() { _last_size = 0; } - template - inline void basic_restorable_array::save() + bool has_changed() const { return base::size() != _last_size; } + + size_t snap(unsigned char* data, const snapshot_interface::snapper& snapper) const { - // Put us into save/restore mode - _saved = true; + unsigned char* ptr = data; + bool should_write = data != nullptr; + ptr += base::snap(ptr, snapper); + ptr = base::snap_write(ptr, _last_size, should_write); + return ptr - data; } - template - inline void basic_restorable_array::restore() + const unsigned char* snap_load(const unsigned char* ptr, const snapshot_interface::loader& loader) { - clear_temp(); + ptr = base::snap_load(ptr, loader); + ptr = base::snap_read(ptr, _last_size); + return ptr; + } - // Clear saved flag - _saved = false; +private: + size_t _last_size = 0; +}; + +template +void managed_array::extend(size_t capacity) +{ + static_assert(dynamic, "Can only extend if array is dynamic!"); + size_t new_capacity = capacity > _capacity ? capacity : 1.5f * _capacity; + if (new_capacity < 5) { + new_capacity = 5; } + T* new_data = new T[new_capacity]; - template - inline void basic_restorable_array::forget() - { - // Run through the _temp array - for ( size_t i = 0; i < _capacity; i++ ) - { - // Copy if there's values - if ( _temp[i] != _null ) - _array[i] = _temp[i]; - - // Clear - _temp[i] = _null; - } + for (size_t i = 0; i < _capacity; ++i) { + new_data[i] = _dynamic_data[i]; } - template - inline void basic_restorable_array::clear_temp() + delete[] _dynamic_data; + _dynamic_data = new_data; + _capacity = new_capacity; +} + +template +class basic_restorable_array : public snapshot_interface +{ +public: + basic_restorable_array(T* array, size_t capacity, T nullValue) + : _saved(false) + , _array(array) + , _temp(array + capacity / 2) + , _capacity(capacity / 2) + , _null(nullValue) { - // Run through the _temp array - for ( size_t i = 0; i < _capacity; i++ ) - { - // Clear - _temp[i] = _null; - } + inkAssert( + capacity % 2 == 0, + "basic_restorable_array requires a datablock of even length to split into two arrays" + ); + + // zero out main array and put 'nulls' in the clear_temp() + inkZeroMemory(_array, _capacity * sizeof(T)); + clear_temp(); } - template - inline void basic_restorable_array::clear( const T& value ) + // == Non-Copyable == + basic_restorable_array(const basic_restorable_array&) = delete; + basic_restorable_array& operator=(const basic_restorable_array&) = delete; + + // set value by index + void set(size_t index, const T& value); + + // get value by index + const T& get(size_t index) const; + + // size of the array + inline size_t capacity() const { return _capacity; } + + // only const indexing is supported due to save/restore system + inline const T& operator[](size_t index) const { return get(index); } + + // == Save/Restore == + void save(); + void restore(); + void forget(); + + // Resets all values and clears any save points + void clear(const T& value); + + // snapshot interface + virtual size_t snap(unsigned char* data, const snapper&) const; + virtual const unsigned char* snap_load(const unsigned char* data, const loader&); + +protected: + inline T* buffer() { return _array; } + + void set_new_buffer(T* buffer, size_t capacity) { - _saved = false; - for ( size_t i = 0; i < _capacity; i++ ) - { - _temp[i] = _null; - _array[i] = value; - } + _array = buffer; + _temp = buffer + capacity / 2; + _capacity = capacity / 2; } - template - class fixed_restorable_array : public basic_restorable_array +private: + inline void check_index(size_t index) const { - public: - fixed_restorable_array( const T& initial, const T& nullValue ) - : basic_restorable_array( _buffer, SIZE * 2, nullValue ) - { - basic_restorable_array::clear( initial ); + inkAssert(index < capacity(), "Index out of range!"); + } + + void clear_temp(); + +private: + bool _saved; + + // real values live here + T* _array; + + // we store values here when we're in save mode + // they're copied on a call to forget() + T* _temp; + + // size of both _array and _temp + size_t _capacity; + + // null + const T _null; +}; + +template +inline size_t basic_restorable_array::snap(unsigned char* data, const snapper& snapper) const +{ + unsigned char* ptr = data; + bool should_write = data != nullptr; + ptr = snap_write(ptr, _saved, should_write); + ptr = snap_write(ptr, _capacity, should_write); + ptr = snap_write(ptr, _null, should_write); + for (size_t i = 0; i < _capacity; ++i) { + ptr = snap_write(ptr, _array[i], should_write); + ptr = snap_write(ptr, _temp[i], should_write); + } + return ptr - data; +} + +template +inline const unsigned char* + basic_restorable_array::snap_load(const unsigned char* data, const loader& loader) +{ + auto ptr = data; + ptr = snap_read(ptr, _saved); + ptr = snap_read(ptr, _capacity); + T null; + ptr = snap_read(ptr, null); + inkAssert(null == _null, "null value is different to snapshot!"); + for (size_t i = 0; i < _capacity; ++i) { + ptr = snap_read(ptr, _array[i]); + ptr = snap_read(ptr, _temp[i]); + } + return ptr; +} + +template +inline void basic_restorable_array::set(size_t index, const T& value) +{ + check_index(index); + inkAssert(value != _null, "Can not add a value considered a 'null' to a restorable_array"); + + // If we're saved, store in second half of the array + if (_saved) { + _temp[index] = value; + } else { + // Otherwise, store in the main array + _array[index] = value; + } +} + +template +inline const T& basic_restorable_array::get(size_t index) const +{ + check_index(index); + + // If we're in save mode and we have a value at that index, return that instead + if (_saved && _temp[index] != _null) { + return _temp[index]; + } + + // Otherwise, fall back on the real array + return _array[index]; +} + +template +inline void basic_restorable_array::save() +{ + // Put us into save/restore mode + _saved = true; +} + +template +inline void basic_restorable_array::restore() +{ + clear_temp(); + + // Clear saved flag + _saved = false; +} + +template +inline void basic_restorable_array::forget() +{ + // Run through the _temp array + for (size_t i = 0; i < _capacity; i++) { + // Copy if there's values + if (_temp[i] != _null) { + _array[i] = _temp[i]; } - private: - T _buffer[SIZE * 2]; - }; + // Clear + _temp[i] = _null; + } +} + +template +inline void basic_restorable_array::clear_temp() +{ + // Run through the _temp array + for (size_t i = 0; i < _capacity; i++) { + // Clear + _temp[i] = _null; + } +} + +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, const T& nullValue) + : basic_restorable_array(_buffer, SIZE * 2, nullValue) + { + basic_restorable_array::clear(initial); + } - template - class allocated_restorable_array final : public basic_restorable_array +private: + T _buffer[SIZE * 2]; +}; + +template +class allocated_restorable_array final : public basic_restorable_array +{ + using base = basic_restorable_array; + +public: + allocated_restorable_array(const T& initial, const T& nullValue) + : basic_restorable_array(0, 0, nullValue) + , _initialValue{initial} + , _nullValue{nullValue} + , _buffer{nullptr} { - using base = basic_restorable_array; - - public: - allocated_restorable_array( const T& initial, const T& nullValue ) - : basic_restorable_array( 0, 0, nullValue ), - _initialValue{ initial }, - _nullValue{ nullValue }, - _buffer{ nullptr } - {} - allocated_restorable_array( size_t capacity, const T& initial, const T& nullValue ) - : basic_restorable_array( new T[capacity * 2], capacity * 2, nullValue ), - _initialValue{ initial }, - _nullValue{ nullValue } - { - _buffer = this->buffer(); - this->clear( _initialValue ); - } + } - void resize( size_t n ) - { - size_t new_capacity = 2 * n; - T* new_buffer = new T[new_capacity]; - if ( _buffer ) - { - for ( size_t i = 0; i < base::capacity(); ++i ) - { - new_buffer[i] = _buffer[i]; - // copy temp - new_buffer[i + base::capacity()] = _buffer[i + base::capacity()]; - } - delete[] _buffer; - } - for ( size_t i = base::capacity(); i < new_capacity; ++i ) - { - new_buffer[i] = _initialValue; - new_buffer[i + base::capacity()] = _nullValue; - } + allocated_restorable_array(size_t capacity, const T& initial, const T& nullValue) + : basic_restorable_array(new T[capacity * 2], capacity * 2, nullValue) + , _initialValue{initial} + , _nullValue{nullValue} + { + _buffer = this->buffer(); + this->clear(_initialValue); + } - _buffer = new_buffer; - this->set_new_buffer( _buffer, new_capacity ); + void resize(size_t n) + { + size_t new_capacity = 2 * n; + T* new_buffer = new T[new_capacity]; + if (_buffer) { + for (size_t i = 0; i < base::capacity(); ++i) { + new_buffer[i] = _buffer[i]; + // copy temp + new_buffer[i + base::capacity()] = _buffer[i + base::capacity()]; + } + delete[] _buffer; + } + for (size_t i = base::capacity(); i < new_capacity; ++i) { + new_buffer[i] = _initialValue; + new_buffer[i + base::capacity()] = _nullValue; } - virtual ~allocated_restorable_array() - { - if ( _buffer ) - { - delete[] _buffer; - _buffer = nullptr; - } + _buffer = new_buffer; + this->set_new_buffer(_buffer, new_capacity); + } + + virtual ~allocated_restorable_array() + { + if (_buffer) { + delete[] _buffer; + _buffer = nullptr; } + } - private: - T _initialValue; - T _nullValue; - T* _buffer; - }; -} // namespace ink::runtime::internal +private: + T _initialValue; + T _nullValue; + T* _buffer; +}; +} // namespace ink::runtime::internal diff --git a/inkcpp/output.cpp b/inkcpp/output.cpp index 0c13452e..09075bf6 100644 --- a/inkcpp/output.cpp +++ b/inkcpp/output.cpp @@ -5,380 +5,385 @@ #include "string_utils.h" #ifdef INK_ENABLE_STL -#include +# include #endif namespace ink::runtime::internal { - basic_stream::basic_stream(value* buffer, size_t len) - : _data(buffer), _max(len), _size(0), _save(~0) - {} - - void basic_stream::append(const value& in) - { - // SPECIAL: Incoming newline - if (in.type() == value_type::newline && _size > 1) - { - // If the end of the stream is a function start marker, we actually - // want to ignore this. Function start trimming. - if (_data[_size - 1].type() == value_type::func_start) - return; - } +basic_stream::basic_stream(value* buffer, size_t len) + : _data(buffer) + , _max(len) + , _size(0) + , _save(~0) +{ +} - // Ignore leading newlines - if (in.type() == value_type::newline && _size == 0) +void basic_stream::append(const value& in) +{ + // SPECIAL: Incoming newline + if (in.type() == value_type::newline && _size > 1) { + // If the end of the stream is a function start marker, we actually + // want to ignore this. Function start trimming. + if (_data[_size - 1].type() == value_type::func_start) return; + } - // Add to data stream - inkAssert(_size < _max, "Output stream overflow"); - _data[_size++] = in; - - // Special: Incoming glue. Trim whitespace/newlines prior - // This also applies when a function ends to trim trailing whitespace. - if ((in.type() == value_type::glue || in.type() == value_type::func_end) && _size > 1) - { - // Run backwards - size_t i = _size - 2; - while(true) - { - value& d = _data[i]; - - // Nullify newlines - if (d.type() == value_type::newline) { - d = value{}; - } + // Ignore leading newlines + if (in.type() == value_type::newline && _size == 0) + return; + + // Add to data stream + inkAssert(_size < _max, "Output stream overflow"); + _data[_size++] = in; + + // Special: Incoming glue. Trim whitespace/newlines prior + // This also applies when a function ends to trim trailing whitespace. + if ((in.type() == value_type::glue || in.type() == value_type::func_end) && _size > 1) { + // Run backwards + size_t i = _size - 2; + while (true) { + value& d = _data[i]; + + // Nullify newlines + if (d.type() == value_type::newline) { + d = value{}; + } - // Nullify whitespace - else if ( d.type() == value_type::string - && is_whitespace(d.get())) - d = value{}; + // Nullify whitespace + else if (d.type() == value_type::string && is_whitespace(d.get())) + d = value{}; - // If it's not a newline or whitespace, stop - else break; + // If it's not a newline or whitespace, stop + else + break; - // If we've hit the end, break - if (i == 0) - break; + // If we've hit the end, break + if (i == 0) + break; - // Move on to next element - i--; - } + // Move on to next element + i--; } } +} - void basic_stream::append(const value* in, unsigned int length) - { - // TODO: Better way to bulk while still executing glue checks? - for (size_t i = 0; i < length; i++) - append(in[i]); - } +void basic_stream::append(const value* in, unsigned int length) +{ + // TODO: Better way to bulk while still executing glue checks? + for (size_t i = 0; i < length; i++) + append(in[i]); +} - template - inline void write_char(T& output, char c) - { - static_assert(always_false::value, "Invalid output type"); - } +template +inline void write_char(T& output, char c) +{ + static_assert(always_false::value, "Invalid output type"); +} - template<> - inline void write_char(char*& output, char c) - { - (*output++) = c; - } +template<> +inline void write_char(char*& output, char c) +{ + (*output++) = c; +} #ifdef INK_ENABLE_STL - template<> - inline void write_char(std::stringstream& output, char c) - { - output.put(c); - } +template<> +inline void write_char(std::stringstream& output, char c) +{ + output.put(c); +} #endif - inline bool get_next(const value* list, size_t i, size_t size, const value** next) - { - while (i + 1 < size) - { - *next = &list[i + 1]; - value_type type = (*next)->type(); - if ((*next)->printable()) { return true; } - i++; +inline bool get_next(const value* list, size_t i, size_t size, const value** next) +{ + while (i + 1 < size) { + *next = &list[i + 1]; + value_type type = (*next)->type(); + if ((*next)->printable()) { + return true; } - - return false; + i++; } - template - void basic_stream::copy_string(const char* str, size_t& dataIter, T& output) - { - while(*str != 0) { + return false; +} + +template +void basic_stream::copy_string(const char* str, size_t& dataIter, T& output) +{ + while (*str != 0) { write_char(output, *str++); - } } +} #ifdef INK_ENABLE_STL - std::string basic_stream::get() - { - size_t start = find_start(); - - // Move up from marker - bool hasGlue = false, lastNewline = false; - std::stringstream str; - for (size_t i = start; i < _size; i++) - { - if (should_skip(i, hasGlue, lastNewline)) - continue; - if (_data[i].printable()){ - _data[i].write(str, _lists_table); - } - +std::string basic_stream::get() +{ + size_t start = find_start(); + + // Move up from marker + bool hasGlue = false, lastNewline = false; + std::stringstream str; + for (size_t i = start; i < _size; i++) { + if (should_skip(i, hasGlue, lastNewline)) + continue; + if (_data[i].printable()) { + _data[i].write(str, _lists_table); } + } - // Reset stream size to where we last held the marker - _size = start; + // Reset stream size to where we last held the marker + _size = start; - // Return processed string - // remove mulitple accourencies of ' ' - std::string result = str.str(); - if ( !result.empty() ) { - auto end = clean_string( result.begin(), result.end() ); - _last_char = *( end - 1 ); - result.resize( end - result.begin() - ( _last_char == ' ' ? 1 : 0 ) ); + // Return processed string + // remove mulitple accourencies of ' ' + std::string result = str.str(); + if (! result.empty()) { + auto end = clean_string(result.begin(), result.end()); + if (result.begin() == end) { + result.resize(0); + } else { + _last_char = *(end - 1); + result.resize(end - result.begin() - (_last_char == ' ' ? 1 : 0)); } - return result; } + return result; +} #endif #ifdef INK_ENABLE_UNREAL - FString basic_stream::get() - { - UE_LOG( InkCpp, Warning, TEXT("Basic stream::get is not implemented correctly and should not be used implemented correctly!" ) ); - FString str; - return str; - } +FString basic_stream::get() +{ + UE_LOG( + InkCpp, Warning, + TEXT("Basic stream::get is not implemented correctly and should not be used implemented " + "correctly!") + ); + FString str; + return str; +} #endif - int basic_stream::queued() const - { - size_t start = find_start(); - return _size - start; - } +int basic_stream::queued() const +{ + size_t start = find_start(); + return _size - start; +} - const value& basic_stream::peek() const - { - inkAssert(_size > 0, "Attempting to peek empty stream!"); - return _data[_size - 1]; - } +const value& basic_stream::peek() const +{ + inkAssert(_size > 0, "Attempting to peek empty stream!"); + return _data[_size - 1]; +} - void basic_stream::discard(size_t length) - { - // discard elements - _size -= length; - if (_size < 0) - _size = 0; - } +void basic_stream::discard(size_t length) +{ + // discard elements + _size -= length; + if (_size < 0) + _size = 0; +} - void basic_stream::get(value* ptr, size_t length) - { - // Find start - size_t start = find_start(); +void basic_stream::get(value* ptr, size_t length) +{ + // Find start + size_t start = find_start(); - const value* end = ptr + length; - //inkAssert(_size - start < length, "Insufficient space in data array to store stream contents!"); + const value* end = ptr + length; + // inkAssert(_size - start < length, "Insufficient space in data array to store stream + // contents!"); - // Move up from marker - bool hasGlue = false, lastNewline = false; - for (size_t i = start; i < _size; i++) - { - if (should_skip(i, hasGlue, lastNewline)) - continue; + // Move up from marker + bool hasGlue = false, lastNewline = false; + for (size_t i = start; i < _size; i++) { + if (should_skip(i, hasGlue, lastNewline)) + continue; - // Make sure we can fit the next element - inkAssert(ptr < end, "Insufficient space in data array to store stream contents!"); + // Make sure we can fit the next element + inkAssert(ptr < end, "Insufficient space in data array to store stream contents!"); - // Copy any value elements - if (_data[i].printable()) { - *(ptr++) = _data[i]; - } + // Copy any value elements + if (_data[i].printable()) { + *(ptr++) = _data[i]; } - - // Reset stream size to where we last held the marker - _size = start; } - bool basic_stream::has_marker() const { - return entries_since_marker() >= 0; - } + // Reset stream size to where we last held the marker + _size = start; +} - int basic_stream::entries_since_marker() const - { - // TODO: Cache? - for (size_t i = 0; i < _size; i++) - { - if (_data[i].type() == value_type::marker) - return i; - } +bool basic_stream::has_marker() const +{ + return entries_since_marker() >= 0; +} - return -1; +int basic_stream::entries_since_marker() const +{ + // TODO: Cache? + for (size_t i = 0; i < _size; i++) { + if (_data[i].type() == value_type::marker) + return i; } - bool basic_stream::ends_with(value_type type) const - { - if (_size == 0) - return false; + return -1; +} - return _data[_size - 1].type() == type; - } +bool basic_stream::ends_with(value_type type) const +{ + if (_size == 0) + return false; - bool basic_stream::saved_ends_with(value_type type) const - { - inkAssert(_save != ~0, "Stream is not saved!"); + return _data[_size - 1].type() == type; +} - if (_save == 0) - return false; +bool basic_stream::saved_ends_with(value_type type) const +{ + inkAssert(_save != ~0, "Stream is not saved!"); - return _data[_save - 1].type() == type; - } + if (_save == 0) + return false; - void basic_stream::save() - { - inkAssert(_save == ~0, "Can not save over existing save point!"); + return _data[_save - 1].type() == type; +} - // Save the current size - _save = _size; - } +void basic_stream::save() +{ + inkAssert(_save == ~0, "Can not save over existing save point!"); - void basic_stream::restore() - { - inkAssert(_save != ~0, "No save point to restore!"); + // Save the current size + _save = _size; +} - // Restore size to saved position - _size = _save; - _save = ~0; - } +void basic_stream::restore() +{ + inkAssert(_save != ~0, "No save point to restore!"); - void basic_stream::forget() - { - inkAssert(_save != ~0, "No save point to forget!"); + // Restore size to saved position + _size = _save; + _save = ~0; +} - // Just null the save point and continue as normal - _save = ~0; - } - - template char* basic_stream::get_alloc(string_table& strings, list_table& lists); - template char* basic_stream::get_alloc(string_table& strings, list_table& lists); - - template - char* basic_stream::get_alloc(string_table& strings, list_table& lists) - { - size_t start = find_start(); - - // Two passes. First for length - size_t length = 0; - bool hasGlue = false, lastNewline = false; - for (size_t i = start; i < _size; i++) - { - if (should_skip(i, hasGlue, lastNewline)) - continue; - ++length; // potenzial space to sperate - if (_data[i].printable()) { - switch(_data[i].type()) { - case value_type::list: - length += lists.stringLen(_data[i].get()); - break; - case value_type::list_flag: - length += lists.stringLen(_data[i].get()); - break; - default: length += value_length(_data[i]); - } +void basic_stream::forget() +{ + inkAssert(_save != ~0, "No save point to forget!"); + + // Just null the save point and continue as normal + _save = ~0; +} + +template char* basic_stream::get_alloc(string_table& strings, list_table& lists); +template char* basic_stream::get_alloc(string_table& strings, list_table& lists); + +template +char* basic_stream::get_alloc(string_table& strings, list_table& lists) +{ + size_t start = find_start(); + + // Two passes. First for length + size_t length = 0; + bool hasGlue = false, lastNewline = false; + for (size_t i = start; i < _size; i++) { + if (should_skip(i, hasGlue, lastNewline)) + continue; + ++length; // potenzial space to sperate + if (_data[i].printable()) { + switch (_data[i].type()) { + case value_type::list: length += lists.stringLen(_data[i].get()); break; + case value_type::list_flag: + length += lists.stringLen(_data[i].get()); + break; + default: length += value_length(_data[i]); } } + } - // Allocate - char* buffer = strings.create(length + 1); - char* end = buffer + length + 1; - char* ptr = buffer; - hasGlue = false; lastNewline = false; - for (size_t i = start; i < _size; i++) - { - if (should_skip(i, hasGlue, lastNewline)) - continue; - if(!_data[i].printable()) { continue; } - switch (_data[i].type()) - { + // Allocate + char* buffer = strings.create(length + 1); + char* end = buffer + length + 1; + char* ptr = buffer; + hasGlue = false; + lastNewline = false; + for (size_t i = start; i < _size; i++) { + if (should_skip(i, hasGlue, lastNewline)) + continue; + if (! _data[i].printable()) { + continue; + } + switch (_data[i].type()) { case value_type::int32: case value_type::float32: case value_type::uint32: // Convert to string and advance toStr(ptr, end - ptr, _data[i]); - while (*ptr != 0) ptr++; + while (*ptr != 0) + ptr++; break; - case value_type::string: - { + case value_type::string: { // Copy string and advance - const char* value = _data[i].get(); + const char* value = _data[i].get(); copy_string(value, i, ptr); - } break; + } break; case value_type::newline: - *ptr = '\n'; ptr++; - break; - case value_type::list: - ptr = lists.toString(ptr, _data[i].get()); + *ptr = '\n'; + ptr++; break; + case value_type::list: ptr = lists.toString(ptr, _data[i].get()); break; case value_type::list_flag: ptr = lists.toString(ptr, _data[i].get()); break; default: inkFail("cant convert expression to string!"); - } } + } - // Make sure last character is a null - *ptr = 0; + // Make sure last character is a null + *ptr = 0; - // Reset stream size to where we last held the marker - _size = start; + // Reset stream size to where we last held the marker + _size = start; - // Return processed string - end = clean_string(buffer, buffer + c_str_len(buffer)); - *end = 0; - _last_char = end[-1]; - if constexpr (RemoveTail) { - if (_last_char == ' ') { end[-1] = 0; } + // Return processed string + end = clean_string(buffer, buffer + c_str_len(buffer)); + *end = 0; + _last_char = end[-1]; + if constexpr (RemoveTail) { + if (_last_char == ' ') { + end[-1] = 0; } - - return buffer; } - size_t basic_stream::find_start() const - { - // Find marker (or start) - size_t start = _size; - while (start > 0) - { - start--; - if (_data[start].type() == value_type::marker) - break; - } + return buffer; +} - // Make sure we're not violating a save point - if (_save != ~0 && start < _save) { - // TODO: check if we don't reset save correct - // at some point we can modifiy the output even behind save (probally discard?) and push a new element -> invalid save point - // inkAssert(false, "Trying to access output stream prior to save point!"); - const_cast(*this).clear(); - } +size_t basic_stream::find_start() const +{ + // Find marker (or start) + size_t start = _size; + while (start > 0) { + start--; + if (_data[start].type() == value_type::marker) + break; + } - return start; + // Make sure we're not violating a save point + if (_save != ~0 && start < _save) { + // TODO: check if we don't reset save correct + // at some point we can modifiy the output even behind save (probally discard?) and push a new + // element -> invalid save point inkAssert(false, "Trying to access output stream prior to save + // point!"); + const_cast(*this).clear(); } - bool basic_stream::should_skip(size_t iter, bool& hasGlue, bool& lastNewline) const - { - if (_data[iter].printable() - && _data[iter].type() != value_type::newline - && _data[iter].type() != value_type::string) { - lastNewline = false; - hasGlue = false; - } else { - switch (_data[iter].type()) - { + return start; +} + +bool basic_stream::should_skip(size_t iter, bool& hasGlue, bool& lastNewline) const +{ + if (_data[iter].printable() && _data[iter].type() != value_type::newline + && _data[iter].type() != value_type::string) { + lastNewline = false; + hasGlue = false; + } else { + switch (_data[iter].type()) { case value_type::newline: if (lastNewline) return true; @@ -386,116 +391,104 @@ namespace ink::runtime::internal return true; lastNewline = true; break; - case value_type::glue: - hasGlue = true; - break; - case value_type::string: - { + case value_type::glue: hasGlue = true; break; + case value_type::string: { lastNewline = false; // an empty string don't count as glued I095 - for(const char* i=_data[iter].get(); - *i; ++i) - { + for (const char* i = _data[iter].get(); *i; ++i) { // isspace only supports characters in [0, UCHAR_MAX] - if (!isspace(static_cast(*i))) { + if (! isspace(static_cast(*i))) { hasGlue = false; break; } } } break; - default: - break; - } + default: break; } - - return false; } - bool basic_stream::text_past_save() const - { - // Check if there is text past the save - for (size_t i = _save; i < _size; i++) - { - const value& d = _data[i]; - if (d.type() == value_type::string) - { - // TODO: Cache what counts as whitespace? - if (!is_whitespace(d.get(), false)) - return true; - } - } + return false; +} - // No text - return false; +bool basic_stream::text_past_save() const +{ + // Check if there is text past the save + for (size_t i = _save; i < _size; i++) { + const value& d = _data[i]; + if (d.type() == value_type::string) { + // TODO: Cache what counts as whitespace? + if (! is_whitespace(d.get(), false)) + return true; + } } - void basic_stream::clear() - { - _save = ~0; - _size = 0; - } + // No text + return false; +} - void basic_stream::mark_used(string_table& strings, list_table& lists) const - { - // Find all allocated strings and mark them as used - for (size_t i = 0; i < _size; i++) - { - if (_data[i].type() == value_type::string) { - string_type str = _data[i].get(); - if (str.allocated) { - strings.mark_used(str.str); - } - } else if (_data[i].type() == value_type::list) { - lists.mark_used(_data[i].get()); +void basic_stream::clear() +{ + _save = ~0; + _size = 0; +} + +void basic_stream::mark_used(string_table& strings, list_table& lists) const +{ + // Find all allocated strings and mark them as used + for (size_t i = 0; i < _size; i++) { + if (_data[i].type() == value_type::string) { + string_type str = _data[i].get(); + if (str.allocated) { + strings.mark_used(str.str); } + } else if (_data[i].type() == value_type::list) { + lists.mark_used(_data[i].get()); } } +} #ifdef INK_ENABLE_STL - std::ostream& operator<<(std::ostream& out, basic_stream& in) - { - out << in.get(); - return out; - } +std::ostream& operator<<(std::ostream& out, basic_stream& in) +{ + out << in.get(); + return out; +} - basic_stream& operator>>(basic_stream& in, std::string& out) - { - out = in.get(); - return in; - } +basic_stream& operator>>(basic_stream& in, std::string& out) +{ + out = in.get(); + return in; +} #endif #ifdef INK_ENABLE_UNREAL - basic_stream& operator>>(basic_stream& in, FString& out) - { - out = in.get(); - return in; - } +basic_stream& operator>>(basic_stream& in, FString& out) +{ + out = in.get(); + return in; +} #endif - size_t basic_stream::snap(unsigned char* data, const snapper& snapper) const - { - unsigned char* ptr = data; - ptr = snap_write(ptr, _last_char, data != nullptr); - ptr = snap_write(ptr, _size, data != nullptr); - ptr = snap_write(ptr, _save, data != nullptr); - for(auto itr = _data; itr != _data + _size; ++itr) - { - ptr += itr->snap(data ? ptr : nullptr, snapper); - } - return ptr - data; +size_t basic_stream::snap(unsigned char* data, const snapper& snapper) const +{ + unsigned char* ptr = data; + ptr = snap_write(ptr, _last_char, data != nullptr); + ptr = snap_write(ptr, _size, data != nullptr); + ptr = snap_write(ptr, _save, data != nullptr); + for (auto itr = _data; itr != _data + _size; ++itr) { + ptr += itr->snap(data ? ptr : nullptr, snapper); } + return ptr - data; +} - const unsigned char* basic_stream::snap_load(const unsigned char* ptr, const loader& loader) - { - ptr = snap_read(ptr, _last_char); - ptr = snap_read(ptr, _size); - ptr = snap_read(ptr, _save); - inkAssert(_max >= _size, "output is to small to hold stored data"); - for(auto itr = _data; itr != _data + _size; ++itr) - { - ptr = itr->snap_load(ptr, loader); - } - return ptr; +const unsigned char* basic_stream::snap_load(const unsigned char* ptr, const loader& loader) +{ + ptr = snap_read(ptr, _last_char); + ptr = snap_read(ptr, _size); + ptr = snap_read(ptr, _save); + inkAssert(_max >= _size, "output is to small to hold stored data"); + for (auto itr = _data; itr != _data + _size; ++itr) { + ptr = itr->snap_load(ptr, loader); } + return ptr; } - +} // namespace ink::runtime::internal diff --git a/inkcpp/runner_impl.cpp b/inkcpp/runner_impl.cpp index 7ae1a136..22881d50 100644 --- a/inkcpp/runner_impl.cpp +++ b/inkcpp/runner_impl.cpp @@ -11,1393 +11,1413 @@ namespace ink::runtime { - const choice* runner_interface::get_choice(size_t index) const - { - inkAssert(index < num_choices(), "Choice out of bounds!"); - return begin() + index; - } +const choice* runner_interface::get_choice(size_t index) const +{ + inkAssert(index < num_choices(), "Choice out of bounds!"); + return begin() + index; +} - size_t runner_interface::num_choices() const - { - return end() - begin(); - } +size_t runner_interface::num_choices() const +{ + return end() - begin(); } +} // namespace ink::runtime namespace ink::runtime::internal { - template<> - value* runner_impl::get_var(hash_t variableName) { - return _globals->get_variable(variableName); +template<> +value* runner_impl::get_var(hash_t variableName) +{ + return _globals->get_variable(variableName); +} + +template<> +value* runner_impl::get_var(hash_t variableName) +{ + value* ret = _stack.get(variableName); + if (! ret) { + return nullptr; + } + if (ret->type() == value_type::value_pointer) { + auto [name, ci] = ret->get(); + inkAssert(ci == 0, "only Global pointer are allowd on the _stack!"); + return get_var(name); } + return ret; +} - template<> - value* runner_impl::get_var(hash_t variableName) { - value* ret = _stack.get(variableName); - if(!ret) { return nullptr; } - if(ret->type() == value_type::value_pointer) { - auto [name, ci] = ret->get(); - inkAssert(ci == 0, "only Global pointer are allowd on the _stack!"); - return get_var(name); - } +template<> +value* runner_impl::get_var(hash_t variableName) +{ + value* ret = get_var(variableName); + if (ret) { return ret; } + return get_var(variableName); +} - template<> - value* runner_impl::get_var(hash_t variableName) { - value* ret = get_var(variableName); - if(ret) { return ret; } - return get_var(variableName); - } - template - const value* runner_impl::get_var(hash_t variableName) const { - return const_cast(this)->get_var(variableName); +template +const value* runner_impl::get_var(hash_t variableName) const +{ + return const_cast(this)->get_var(variableName); +} + +template<> +void runner_impl::set_var( + hash_t variableName, const value& val, bool is_redef +) +{ + if (is_redef) { + value* src = _globals->get_variable(variableName); + _globals->set_variable(variableName, src->redefine(val, _globals->lists())); + } else { + _globals->set_variable(variableName, val); } +} - template<> - void runner_impl::set_var(hash_t variableName, const value& val, bool is_redef) { - if(is_redef) { - value* src = _globals->get_variable(variableName); - _globals->set_variable(variableName, src->redefine(val, _globals->lists())); - } else { - _globals->set_variable(variableName, val); - } +const value* runner_impl::dereference(const value& val) +{ + if (val.type() != value_type::value_pointer) { + return &val; } - const value* runner_impl::dereference(const value& val) { - if(val.type() != value_type::value_pointer) { return &val; } - - auto [name, ci] = val.get(); - if(ci == 0) { return get_var(name); } - return _stack.get_from_frame(ci, name); + auto [name, ci] = val.get(); + if (ci == 0) { + return get_var(name); } + return _stack.get_from_frame(ci, name); +} - template<> - void runner_impl::set_var(hash_t variableName, const value& val, bool is_redef) { - if(val.type() == value_type::value_pointer) { - inkAssert(is_redef == false, "value pointer can only use to initelize variable!"); - auto [name, ci] = val.get(); - if(ci == 0) { _stack.set(variableName, val); } - else { - const value* dref = dereference(val); - if(dref == nullptr) { - value v = val; - auto ref = v.get(); - v.set(ref.name, 0); - _stack.set(variableName, v); - } else { - _ref_stack.set(variableName, val); - _stack.set(variableName, *dref); - } - } +template<> +void runner_impl::set_var( + hash_t variableName, const value& val, bool is_redef +) +{ + if (val.type() == value_type::value_pointer) { + inkAssert(is_redef == false, "value pointer can only use to initelize variable!"); + auto [name, ci] = val.get(); + if (ci == 0) { + _stack.set(variableName, val); } else { - if(is_redef) { - value* src = _stack.get(variableName); - if(src->type() == value_type::value_pointer) { - auto [name, ci] = src->get(); - inkAssert(ci == 0, "Only global pointer are allowed on _stack!"); - set_var( - name, - get_var(name)->redefine(val, _globals->lists()), - true); - } else { - _stack.set(variableName, src->redefine(val, _globals->lists())); - } + const value* dref = dereference(val); + if (dref == nullptr) { + value v = val; + auto ref = v.get(); + v.set(ref.name, 0); + _stack.set(variableName, v); } else { - _stack.set(variableName, val); + _ref_stack.set(variableName, val); + _stack.set(variableName, *dref); } } - } - - template<> - void runner_impl::set_var(hash_t variableName, const value& val, bool is_redef) - { - inkAssert(is_redef, "define set scopeless variables!"); - if(_stack.get(variableName)) { - return set_var(variableName, val, is_redef); + } else { + if (is_redef) { + value* src = _stack.get(variableName); + if (src->type() == value_type::value_pointer) { + auto [name, ci] = src->get(); + inkAssert(ci == 0, "Only global pointer are allowed on _stack!"); + set_var( + name, get_var(name)->redefine(val, _globals->lists()), true + ); + } else { + _stack.set(variableName, src->redefine(val, _globals->lists())); + } } else { - return set_var(variableName, val, is_redef); + _stack.set(variableName, val); } } +} +template<> +void runner_impl::set_var( + hash_t variableName, const value& val, bool is_redef +) +{ + inkAssert(is_redef, "define set scopeless variables!"); + if (_stack.get(variableName)) { + return set_var(variableName, val, is_redef); + } else { + return set_var(variableName, val, is_redef); + } +} +template +inline T runner_impl::read() +{ + using header = ink::internal::header; + // Sanity + inkAssert(_ptr + sizeof(T) <= _story->end(), "Unexpected EOF in Ink execution"); + + // Read memory + T val = *( const T* ) _ptr; + if (_story->get_header().endien == header::endian_types::differ) { + val = header::swap_bytes(val); + } - template - inline T runner_impl::read() - { - using header = ink::internal::header; - // Sanity - inkAssert(_ptr + sizeof(T) <= _story->end(), "Unexpected EOF in Ink execution"); - - // Read memory - T val = *(const T*)_ptr; - if (_story->get_header().endien == header::endian_types::differ) { - val = header::swap_bytes(val); - } + // Advance ip + _ptr += sizeof(T); - // Advance ip - _ptr += sizeof(T); + // Return + return val; +} - // Return - return val; - } +template<> +inline const char* runner_impl::read() +{ + offset_t str = read(); + return _story->string(str); +} - template<> - inline const char* runner_impl::read() - { - offset_t str = read(); - return _story->string(str); - } +choice& runner_impl::add_choice() +{ + inkAssert( + config::maxChoices < 0 || _choices.size() < config::maxChoices, "Ran out of choice storage!" + ); + return _choices.push(); +} - choice& runner_impl::add_choice() - { - inkAssert(config::maxChoices < 0 || _choices.size() < config::maxChoices, - "Ran out of choice storage!"); - return _choices.push(); - } +void runner_impl::clear_choices() +{ + // TODO: Garbage collection? ? which garbage ? + _fallback_choice = nullopt; + _choices.clear(); +} - void runner_impl::clear_choices() - { - // TODO: Garbage collection? ? which garbage ? - _fallback_choice = nullopt; - _choices.clear(); - } +void runner_impl::clear_tags() +{ + _tags.clear(); + _choice_tags_begin = -1; +} - void runner_impl::clear_tags() - { - _tags.clear(); - _choice_tags_begin = -1; +void runner_impl::jump(ip_t dest, bool record_visits) +{ + // Optimization: if we are _is_falling, then we can + // _should be_ able to safely assume that there is nothing to do here. A falling + // divert should only be taking us from a container to that same container's end point + // without entering any other containers + // OR IF if target is same position do nothing + // could happend if jumping to and of an unnamed container + if (dest == _ptr) { + _ptr = dest; + return; } - void runner_impl::jump(ip_t dest, bool record_visits) - { - // Optimization: if we are _is_falling, then we can - // _should be_ able to safely assume that there is nothing to do here. A falling - // divert should only be taking us from a container to that same container's end point - // without entering any other containers - // OR IF if target is same position do nothing - // could happend if jumping to and of an unnamed container - if (_is_falling || dest ==_ptr) - { - _ptr = dest; - return; - } - - const uint32_t* iter = nullptr; - container_t id; - ip_t offset; - size_t comm_end; - bool reversed = _ptr > dest; - - if (reversed) { - comm_end = 0; - iter = nullptr; - const ContainerData* old_iter = nullptr; - const uint32_t* last_comm_iter = nullptr; - _container.rev_iter(old_iter); - - // find commen part of old and new stack - while(_story->iterate_containers(iter, id, offset)) { - if(old_iter == nullptr || offset >= dest) { break; } - if(old_iter !=nullptr && id == old_iter->id) { - last_comm_iter = iter; - _container.rev_iter(old_iter); - ++comm_end; - } + const uint32_t* iter = nullptr; + container_t id; + ip_t offset = nullptr; + size_t comm_end; + bool reversed = _ptr > dest; + + if (reversed) { + comm_end = 0; + iter = nullptr; + const ContainerData* old_iter = nullptr; + const uint32_t* last_comm_iter = nullptr; + _container.rev_iter(old_iter); + + // find commen part of old and new stack + while (_story->iterate_containers(iter, id, offset)) { + if (old_iter == nullptr || offset >= dest) { + break; } - - // clear old part from stack - while(_container.size() > comm_end) { _container.pop(); } - iter = last_comm_iter; - - } else { - iter = nullptr; - comm_end = _container.size(); - // go to current possition in container list - while(_story->iterate_containers(iter, id, offset)) { - if(offset >= _ptr) {break;} + if (old_iter != nullptr && id == old_iter->id) { + last_comm_iter = iter; + _container.rev_iter(old_iter); + ++comm_end; } - _story->iterate_containers(iter, id, offset, true); } - // move to destination and update container stack on the go - while(_story->iterate_containers(iter, id, offset)) { - if (offset >= dest) { break; } - if(_container.empty() || _container.top().id != id) { - _container.push({.id = id, .offset = offset}); - } else { - _container.pop(); - if (_container.size() < comm_end) { - comm_end = _container.size(); - } + // clear old part from stack + while (_container.size() > comm_end) { + _container.pop(); + } + iter = last_comm_iter; + + } else { + iter = nullptr; + comm_end = _container.size(); + // go to current possition in container list + while (_story->iterate_containers(iter, id, offset)) { + if (offset >= _ptr) { + break; } } - _ptr = dest; + _story->iterate_containers(iter, id, offset, true); + } - // if we jump directly to a named container start, go inside, if its a ONLY_FIRST container - // it will get visited in the next step - if (offset == dest && static_cast(offset[0]) == Command::START_CONTAINER_MARKER) { - _ptr += 6; - _container.push({.id=id, .offset=offset}); - if (reversed && comm_end == _container.size() - 1) { ++comm_end; } + // move to destination and update container stack on the go + while (_story->iterate_containers(iter, id, offset)) { + if (offset >= dest) { + break; } - - // iff all container (until now) are entered at first position - bool allEnteredAtStart = true; - ip_t child_position = dest; - if(record_visits) { - const ContainerData* iData = nullptr; - size_t level = _container.size(); - while(_container.iter(iData) && - (level > comm_end || _story->container_flag(iData->offset) & CommandFlag::CONTAINER_MARKER_ONLY_FIRST )) - { - auto parrent_offset = iData->offset; - inkAssert(child_position >= parrent_offset, "Container stack order is broken"); - // 6 == len of START_CONTAINER_SIGNAL, if its 6 bytes behind the container it is a unnnamed subcontainers first child - // check if child_positino is the first child of current container - allEnteredAtStart = allEnteredAtStart && ((child_position - parrent_offset) <= 6); - child_position = parrent_offset; - _globals->visit(iData->id, allEnteredAtStart); + if (_container.empty() || _container.top().id != id) { + _container.push({.id = id, .offset = offset}); + } else { + _container.pop(); + if (_container.size() < comm_end) { + comm_end = _container.size(); } } - - - } - template - void runner_impl::start_frame(uint32_t target) { - if constexpr (type == frame_type::function) { - // add a function start marker - _output << values::func_start; + _ptr = dest; + + // if we jump directly to a named container start, go inside, if its a ONLY_FIRST container + // it will get visited in the next step + if (offset == dest && static_cast(offset[0]) == Command::START_CONTAINER_MARKER) { + _ptr += 6; + _container.push({.id = id, .offset = offset}); + if (reversed && comm_end == _container.size() - 1) { + ++comm_end; } - // Push next address onto the callstack - { - size_t address = _ptr - _story->instructions(); - _stack.push_frame(address, _evaluation_mode); - _ref_stack.push_frame(address, _evaluation_mode); + } + + // iff all container (until now) are entered at first position + bool allEnteredAtStart = true; + ip_t child_position = dest; + if (record_visits) { + const ContainerData* iData = nullptr; + size_t level = _container.size(); + while (_container.iter(iData) + && (level > comm_end + || _story->container_flag(iData->offset) & CommandFlag::CONTAINER_MARKER_ONLY_FIRST) + ) { + auto parrent_offset = iData->offset; + inkAssert(child_position >= parrent_offset, "Container stack order is broken"); + // 6 == len of START_CONTAINER_SIGNAL, if its 6 bytes behind the container it is a unnnamed + // subcontainers first child check if child_positino is the first child of current container + allEnteredAtStart = allEnteredAtStart && ((child_position - parrent_offset) <= 6); + child_position = parrent_offset; + _globals->visit(iData->id, allEnteredAtStart); } - _evaluation_mode = false; // unset eval mode when enter function or tunnel + } +} - // Do the jump - inkAssert(_story->instructions() + target < _story->end(), "Diverting past end of story data!"); - jump(_story->instructions() + target, true); +template +void runner_impl::start_frame(uint32_t target) +{ + if constexpr (type == frame_type::function) { + // add a function start marker + _output << values::func_start; } + // Push next address onto the callstack + { + size_t address = _ptr - _story->instructions(); + _stack.push_frame(address, _evaluation_mode); + _ref_stack.push_frame(address, _evaluation_mode); + } + _evaluation_mode = false; // unset eval mode when enter function or tunnel + + // Do the jump + inkAssert(_story->instructions() + target < _story->end(), "Diverting past end of story data!"); + jump(_story->instructions() + target, true); +} - frame_type runner_impl::execute_return() +frame_type runner_impl::execute_return() +{ + // Pop the callstack + _ref_stack.fetch_values(_stack); + frame_type type; + offset_t offset = _stack.pop_frame(&type, _evaluation_mode); + _ref_stack.push_values(_stack); { - // Pop the callstack - _ref_stack.fetch_values(_stack); - frame_type type; - offset_t offset = _stack.pop_frame(&type,_evaluation_mode); - _ref_stack.push_values(_stack); - { frame_type t; bool eval; - // TODO: write all refs to new frame - offset_t o = _ref_stack.pop_frame(&t, eval); - inkAssert(o == offset && t == type && eval == _evaluation_mode, - "_ref_stack and _stack should be in frame sync!"); - } + frame_type t; + bool eval; + // TODO: write all refs to new frame + offset_t o = _ref_stack.pop_frame(&t, eval); + inkAssert( + o == offset && t == type && eval == _evaluation_mode, + "_ref_stack and _stack should be in frame sync!" + ); + } - // SPECIAL: On function, do a trim - if (type == frame_type::function) - _output << values::func_end; - else if(type == frame_type::tunnel) { - // if we return but there is a divert target on top of - // the evaluation stack, we should follow this instead - // inkproof: I060 - if(!_eval.is_empty() && _eval.top().type() == value_type::divert) { - start_frame(_eval.pop().get()); - return type; - } + // SPECIAL: On function, do a trim + if (type == frame_type::function) { + _output << values::func_end; + } else if (type == frame_type::tunnel) { + // if we return but there is a divert target on top of + // the evaluation stack, we should follow this instead + // inkproof: I060 + if (! _eval.is_empty() && _eval.top().type() == value_type::divert) { + start_frame(_eval.pop().get()); + return type; } + } + + // Jump to the old offset + inkAssert( + _story->instructions() + offset < _story->end(), + "Callstack return is outside bounds of story!" + ); + jump(_story->instructions() + offset, false); - // Jump to the old offset - inkAssert(_story->instructions() + offset < _story->end(), "Callstack return is outside bounds of story!"); - jump(_story->instructions() + offset, false); + // Return frame type + return type; +} - // Return frame type - return type; +runner_impl::runner_impl(const story_impl* data, globals global) + : _story(data) + , _globals(global.cast()) + , _operations( + global.cast()->strings(), global.cast()->lists(), _rng, + *global.cast(), *data, static_cast(*this) + ) + , _backup(nullptr) + , _done(nullptr) + , _choices() + , _container(ContainerData{}) + , _rng(time(NULL)) +{ + _ptr = _story->instructions(); + _evaluation_mode = false; + _choice_tags_begin = -1; + + // register with globals + _globals->add_runner(this); + if (_globals->lists()) { + _output.set_list_meta(_globals->lists()); } - runner_impl::runner_impl(const story_impl* data, globals global) - : _story(data), _globals(global.cast()), - _operations( - global.cast()->strings(), - global.cast()->lists(), - _rng, - *global.cast(), - *data, - static_cast(*this)), - _backup(nullptr), _done(nullptr), _choices(), _container(ContainerData{}), _rng(time(NULL)) - { + // initialize globals if necessary + if (! _globals->are_globals_initialized()) { + _globals->initialize_globals(this); + + // Set us back to the beginning of the story + reset(); _ptr = _story->instructions(); - _evaluation_mode = false; - _choice_tags_begin = -1; + } +} + +runner_impl::~runner_impl() +{ + // unregister with globals + _globals->remove_runner(this); +} - // register with globals - _globals->add_runner(this); - if(_globals->lists()) { - _output.set_list_meta(_globals->lists()); +#ifdef INK_ENABLE_STL +std::string runner_impl::getline() +{ + std::string result{""}; + bool fill = false; + do { + if (fill) { + result += " "; } + // Advance interpreter one line + advance_line(); + // Read line into std::string + result += _output.get(); + fill = _output.last_char() == ' '; + } while (_ptr != nullptr && _output.last_char() != '\n'); + + // TODO: fallback choice = no choice + if (! has_choices() && _fallback_choice) { + choose(~0); + } - // initialize globals if necessary - if (!_globals->are_globals_initialized()) - { - _globals->initialize_globals(this); + // Return result + inkAssert(_output.is_empty(), "Output should be empty after getline!"); + return result; +} - // Set us back to the beginning of the story - reset(); - _ptr = _story->instructions(); +void runner_impl::getline(std::ostream& out) +{ + bool fill = false; + do { + if (fill) { + out << " "; } - } + // Advance interpreter one line + advance_line(); + // Write into out + out << _output; + fill = _output.last_char() == ' '; + } while (_ptr != nullptr && _output.last_char() != '\n'); - runner_impl::~runner_impl() - { - // unregister with globals - _globals->remove_runner(this); + // TODO: fallback choice = no choice + if (! has_choices() && _fallback_choice) { + choose(~0); } -#ifdef INK_ENABLE_STL - std::string runner_impl::getline() - { - std::string result{""}; - bool fill = false; - do { - if (fill) { - result += " "; - } - // Advance interpreter one line - advance_line(); - // Read line into std::string - result += _output.get(); - fill = _output.last_char() == ' '; - } while(_ptr != nullptr && _output.last_char() != '\n'); - - // TODO: fallback choice = no choice - if(!has_choices() && _fallback_choice) { choose(~0); } - - // Return result - inkAssert(_output.is_empty(), "Output should be empty after getline!"); - return result; - } + // Make sure we read everything + inkAssert(_output.is_empty(), "Output should be empty after getline!"); +} - void runner_impl::getline(std::ostream& out) - { - bool fill = false; - do { - if (fill) { out << " "; } - // Advance interpreter one line - advance_line(); - // Write into out - out << _output; - fill = _output.last_char() == ' '; - } while(_ptr != nullptr && _output.last_char() != '\n'); - - // TODO: fallback choice = no choice - if(!has_choices() && _fallback_choice) { choose(~0); } - - // Make sure we read everything - inkAssert(_output.is_empty(), "Output should be empty after getline!"); +std::string runner_impl::getall() +{ + // Advance interpreter until we're stopped + std::stringstream str; + while (can_continue()) { + getline(str); } - std::string runner_impl::getall() - { - // Advance interpreter until we're stopped - std::stringstream str; - while(can_continue()) { - getline(str); - } + // Read output into std::string - // Read output into std::string + // Return result + inkAssert(_output.is_empty(), "Output should be empty after getall!"); + return str.str(); +} - // Return result - inkAssert(_output.is_empty(), "Output should be empty after getall!"); - return str.str(); +void runner_impl::getall(std::ostream& out) +{ + // Advance interpreter until we're stopped + while (can_continue()) { + advance_line(); } - void runner_impl::getall(std::ostream& out) - { - // Advance interpreter until we're stopped - while (can_continue()) - advance_line(); + // Send output into stream + out << _output; - // Send output into stream - out << _output; - - // Return result - inkAssert(_output.is_empty(), "Output should be empty after getall!"); - } + // Return result + inkAssert(_output.is_empty(), "Output should be empty after getall!"); +} #endif #ifdef INK_ENABLE_UNREAL - FString runner_impl::getline() - { - clear_tags(); - FString result{}; - bool fill = false; - do { - if ( fill ) { - result += " "; - } - // Advance interpreter one line - advance_line(); - // Read lin ve into std::string - const char* str = _output.get_alloc(_globals->strings(), _globals->lists()); - result.Append( str, c_str_len( str ) ); - fill = _output.last_char() == ' '; - } while ( _ptr != nullptr && _output.last_char() != '\n' ); - - // TODO: fallback choice = no choice - if ( !has_choices() && _fallback_choice ) { choose( ~0 ); } - - // Return result - inkAssert( _output.is_empty(), "Output should be empty after getline!" ); - return result; +FString runner_impl::getline() +{ + clear_tags(); + FString result{}; + bool fill = false; + do { + if (fill) { + result += " "; + } + // Advance interpreter one line + advance_line(); + // Read lin ve into std::string + const char* str = _output.get_alloc(_globals->strings(), _globals->lists()); + result.Append(str, c_str_len(str)); + fill = _output.last_char() == ' '; + } while (_ptr != nullptr && _output.last_char() != '\n'); + + // TODO: fallback choice = no choice + if (! has_choices() && _fallback_choice) { + choose(~0); } + + // Return result + inkAssert(_output.is_empty(), "Output should be empty after getline!"); + return result; +} #endif - void runner_impl::advance_line() - { - // Step while we still have instructions to execute - while (_ptr != nullptr) - { - // Stop if we hit a new line - if (line_step()) - break; +void runner_impl::advance_line() +{ + // Step while we still have instructions to execute + while (_ptr != nullptr) { + // Stop if we hit a new line + if (line_step()) { + break; } - - // can be in save state becaues of choice - // Garbage collection TODO: How often do we want to do this? - _globals->gc(); } - bool runner_impl::can_continue() const - { - return _ptr != nullptr; - } + // can be in save state becaues of choice + // Garbage collection TODO: How often do we want to do this? + _globals->gc(); +} - void runner_impl::choose(size_t index) - { - if(has_choices()) { - inkAssert(index < _choices.size(), "Choice index out of range"); - } - restore(); // restore to stack state when choice was maked - _globals->turn(); - // Get the choice - const auto& c = has_choices() ? _choices[index] : _fallback_choice.value(); - - // 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 = _threads.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); - _ref_stack.collapse_to_thread(choiceThread); - _threads.clear(); - - // Jump to destination and clear choice list - jump(_story->instructions() + c.path(), true); - clear_choices(); - clear_tags(); - } +bool runner_impl::can_continue() const +{ + return _ptr != nullptr; +} - void runner_impl::getline_silent() - { - // advance and clear output stream - advance_line(); - _output.clear(); +void runner_impl::choose(size_t index) +{ + if (has_choices()) { + inkAssert(index < _choices.size(), "Choice index out of range"); } - - bool runner_impl::has_tags() const - { - return num_tags() > 0; + restore(); // restore to stack state when choice was maked + _globals->turn(); + // Get the choice + const auto& c = has_choices() ? _choices[index] : _fallback_choice.value(); + + // 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 = _threads.get(choiceThread); } - size_t runner_impl::num_tags() const - { - return _choice_tags_begin < 0 ? _tags.size() : _choice_tags_begin; - } + // Make sure we have a previous pointer + inkAssert(prev != nullptr, "No 'done' point recorded before finishing choice output"); - const char* runner_impl::get_tag(size_t index) const - { - inkAssert(index < _tags.size(), "Tag index exceeds _num_tags"); - return _tags[index]; - } + // Move to the previous pointer so we track our movements correctly + jump(prev, false); + _done = nullptr; - snapshot* runner_impl::create_snapshot() const - { - return _globals->create_snapshot(); - } + // Collapse callstacks to the correct thread + _stack.collapse_to_thread(choiceThread); + _ref_stack.collapse_to_thread(choiceThread); + _threads.clear(); - size_t runner_impl::snap(unsigned char* data, snapper& snapper) const - { - unsigned char* ptr = data; - bool should_write = data != nullptr; - snapper.current_runner_tags = _tags[0].ptr(); - std::uintptr_t offset = _ptr != nullptr ? _ptr - _story->instructions() : 0; - ptr = snap_write(ptr, offset, should_write); - offset = _backup - _story->instructions(); - ptr = snap_write(ptr, offset, should_write); - offset = _done - _story->instructions(); - ptr = snap_write(ptr, offset, should_write); - ptr = snap_write(ptr, _rng.get_state(), should_write); - ptr = snap_write(ptr, _evaluation_mode, should_write); - ptr = snap_write(ptr, _string_mode, should_write); - ptr = snap_write(ptr, _saved_evaluation_mode, should_write); - ptr = snap_write(ptr, _saved, should_write); - ptr = snap_write(ptr, _is_falling, should_write); - ptr += _output.snap(data ? ptr : nullptr, snapper); - ptr += _stack.snap(data ? ptr : nullptr, snapper); - ptr += _ref_stack.snap(data ? ptr : nullptr, snapper); - ptr += _eval.snap(data ? ptr : nullptr, snapper); - ptr = snap_write(ptr, _choice_tags_begin, should_write); - ptr += _tags.snap(data ? ptr : nullptr, snapper); - ptr += _container.snap(data ? ptr : nullptr, snapper); - ptr += _threads.snap(data ? ptr : nullptr, snapper); - ptr = snap_write(ptr, _fallback_choice.has_value(), should_write); - if (_fallback_choice) { - ptr += _fallback_choice.value().snap(data ? ptr : nullptr, snapper); - } - ptr += _choices.snap(data ? ptr : nullptr, snapper); - return ptr - data; + // Jump to destination and clear choice list + jump(_story->instructions() + c.path(), true); + clear_choices(); + clear_tags(); +} + +void runner_impl::getline_silent() +{ + // advance and clear output stream + advance_line(); + _output.clear(); +} + +bool runner_impl::has_tags() const +{ + return num_tags() > 0; +} + +size_t runner_impl::num_tags() const +{ + return _choice_tags_begin < 0 ? _tags.size() : _choice_tags_begin; +} + +const char* runner_impl::get_tag(size_t index) const +{ + inkAssert(index < _tags.size(), "Tag index exceeds _num_tags"); + return _tags[index]; +} + +snapshot* runner_impl::create_snapshot() const +{ + return _globals->create_snapshot(); +} + +size_t runner_impl::snap(unsigned char* data, snapper& snapper) const +{ + unsigned char* ptr = data; + bool should_write = data != nullptr; + snapper.current_runner_tags = _tags[0].ptr(); + std::uintptr_t offset = _ptr != nullptr ? _ptr - _story->instructions() : 0; + ptr = snap_write(ptr, offset, should_write); + offset = _backup - _story->instructions(); + ptr = snap_write(ptr, offset, should_write); + offset = _done - _story->instructions(); + ptr = snap_write(ptr, offset, should_write); + ptr = snap_write(ptr, _rng.get_state(), should_write); + ptr = snap_write(ptr, _evaluation_mode, should_write); + ptr = snap_write(ptr, _string_mode, should_write); + ptr = snap_write(ptr, _saved_evaluation_mode, should_write); + ptr = snap_write(ptr, _saved, should_write); + ptr = snap_write(ptr, _is_falling, should_write); + ptr += _output.snap(data ? ptr : nullptr, snapper); + ptr += _stack.snap(data ? ptr : nullptr, snapper); + ptr += _ref_stack.snap(data ? ptr : nullptr, snapper); + ptr += _eval.snap(data ? ptr : nullptr, snapper); + ptr = snap_write(ptr, _choice_tags_begin, should_write); + ptr += _tags.snap(data ? ptr : nullptr, snapper); + ptr += _container.snap(data ? ptr : nullptr, snapper); + ptr += _threads.snap(data ? ptr : nullptr, snapper); + ptr = snap_write(ptr, _fallback_choice.has_value(), should_write); + if (_fallback_choice) { + ptr += _fallback_choice.value().snap(data ? ptr : nullptr, snapper); } + ptr += _choices.snap(data ? ptr : nullptr, snapper); + return ptr - data; +} - const unsigned char* runner_impl::snap_load(const unsigned char* data, loader& loader) - { - auto ptr = data; - std::uintptr_t offset; - ptr = snap_read(ptr, offset); - _ptr = offset == 0 ? nullptr : _story->instructions() + offset; - ptr = snap_read(ptr, offset); - _backup = _story->instructions() + offset; - ptr = snap_read(ptr, offset); - _done = _story->instructions() + offset; - int32_t seed; - ptr = snap_read(ptr, seed); - _rng.srand(seed); - ptr = snap_read(ptr, _evaluation_mode); - ptr = snap_read(ptr, _string_mode); - ptr = snap_read(ptr, _saved_evaluation_mode); - ptr = snap_read(ptr, _saved); - ptr = snap_read(ptr, _is_falling); - ptr = _output.snap_load(ptr, loader); - ptr = _stack.snap_load(ptr, loader); - ptr = _ref_stack.snap_load(ptr, loader); - ptr = _eval.snap_load(ptr, loader); - int choice_tags_begin; - ptr = snap_read(ptr, choice_tags_begin); - _choice_tags_begin = choice_tags_begin; - ptr = _tags.snap_load(ptr, loader); - loader.current_runner_tags = _tags[0].ptr(); - ptr = _container.snap_load(ptr, loader); - ptr = _threads.snap_load(ptr, loader); - bool has_fallback_choice; - ptr = snap_read(ptr, has_fallback_choice); - _fallback_choice = nullopt; - if (has_fallback_choice) { - _fallback_choice.emplace(); - ptr = _fallback_choice.value().snap_load(ptr, loader); - } - ptr = _choices.snap_load(ptr, loader); - return ptr; +const unsigned char* runner_impl::snap_load(const unsigned char* data, loader& loader) +{ + auto ptr = data; + std::uintptr_t offset; + ptr = snap_read(ptr, offset); + _ptr = offset == 0 ? nullptr : _story->instructions() + offset; + ptr = snap_read(ptr, offset); + _backup = _story->instructions() + offset; + ptr = snap_read(ptr, offset); + _done = _story->instructions() + offset; + int32_t seed; + ptr = snap_read(ptr, seed); + _rng.srand(seed); + ptr = snap_read(ptr, _evaluation_mode); + ptr = snap_read(ptr, _string_mode); + ptr = snap_read(ptr, _saved_evaluation_mode); + ptr = snap_read(ptr, _saved); + ptr = snap_read(ptr, _is_falling); + ptr = _output.snap_load(ptr, loader); + ptr = _stack.snap_load(ptr, loader); + ptr = _ref_stack.snap_load(ptr, loader); + ptr = _eval.snap_load(ptr, loader); + int choice_tags_begin; + ptr = snap_read(ptr, choice_tags_begin); + _choice_tags_begin = choice_tags_begin; + ptr = _tags.snap_load(ptr, loader); + loader.current_runner_tags = _tags[0].ptr(); + ptr = _container.snap_load(ptr, loader); + ptr = _threads.snap_load(ptr, loader); + bool has_fallback_choice; + ptr = snap_read(ptr, has_fallback_choice); + _fallback_choice = nullopt; + if (has_fallback_choice) { + _fallback_choice.emplace(); + ptr = _fallback_choice.value().snap_load(ptr, loader); } + ptr = _choices.snap_load(ptr, loader); + return ptr; +} #ifdef INK_ENABLE_CSTD - char* runner_impl::getline_alloc() - { - /// TODO - inkFail("Not implemented yet!"); - return nullptr; - } +char* runner_impl::getline_alloc() +{ + /// TODO + inkFail("Not implemented yet!"); + return nullptr; +} #endif - bool runner_impl::move_to(hash_t path) - { - // find the path - ip_t destination = _story->find_offset_for(path); - if (destination == nullptr) - { - // TODO: Error state? - return false; - } - - // Clear state and move to destination - reset(); - _ptr = _story->instructions(); - jump(destination, false); - - return true; +bool runner_impl::move_to(hash_t path) +{ + // find the path + ip_t destination = _story->find_offset_for(path); + if (destination == nullptr) { + // TODO: Error state? + return false; } - void runner_impl::internal_bind(hash_t name, internal::function_base* function) - { - _functions.add(name, function); - } + // Clear state and move to destination + reset(); + _ptr = _story->instructions(); + jump(destination, false); - runner_impl::change_type runner_impl::detect_change() const - { - // Check if the old newline is still present (hasn't been glu'd) and - // if there is new text (non-whitespace) in the stream since saving - bool stillHasNewline = _output.saved_ends_with(value_type::newline); - bool hasAddedNewText = _output.text_past_save() || _tags.has_changed(); + return true; +} - // Newline is still there and there's no new text - if (stillHasNewline && !hasAddedNewText) - return change_type::no_change; +void runner_impl::internal_bind(hash_t name, internal::function_base* function) +{ + _functions.add(name, function); +} - // If the newline is gone, we got glue'd. Continue as if we never had that newline - if (!stillHasNewline) - return change_type::newline_removed; +runner_impl::change_type runner_impl::detect_change() const +{ + // Check if the old newline is still present (hasn't been glu'd) and + // if there is new text (non-whitespace) in the stream since saving + bool stillHasNewline = _output.saved_ends_with(value_type::newline); + bool hasAddedNewText = _output.text_past_save() || _tags.has_changed(); - // TODO New Tags -> extended + // Newline is still there and there's no new text + if (stillHasNewline && ! hasAddedNewText) { + return change_type::no_change; + } - // If there's new text content, we went too far - if (hasAddedNewText) - return change_type::extended_past_newline; + // If the newline is gone, we got glue'd. Continue as if we never had that newline + if (! stillHasNewline) { + return change_type::newline_removed; + } - inkFail("Invalid change detction. Never should be here!"); - return change_type::no_change; + // TODO New Tags -> extended + + // If there's new text content, we went too far + if (hasAddedNewText) { + return change_type::extended_past_newline; } - bool runner_impl::line_step() - { - // Step the interpreter - step(); - - // If we're not within string evaluation - if (!_output.has_marker()) - { - // If we have a saved state after a previous newline - // don't do this if we behind choice - if (_saved && !has_choices() && !_fallback_choice) - { - // Check for changes in the output stream - switch (detect_change()) - { + inkFail("Invalid change detction. Never should be here!"); + return change_type::no_change; +} + +bool runner_impl::line_step() +{ + // Step the interpreter + step(); + + // If we're not within string evaluation + if (! _output.has_marker()) { + // If we have a saved state after a previous newline + // don't do this if we behind choice + if (_saved && ! has_choices() && ! _fallback_choice) { + // Check for changes in the output stream + switch (detect_change()) { case change_type::extended_past_newline: - // We've gone too far. Restore to before we moved past the newline and return that we are done + // We've gone too far. Restore to before we moved past the newline and return that we are + // done restore(); return true; case change_type::newline_removed: // Newline was removed. Proceed as if we never hit it forget(); break; - case change_type::no_change: - break; - } + case change_type::no_change: break; } + } - // If we're on a newline - if (_output.ends_with(value_type::newline)) - { - // Unless we are out of content, we are going to try - // to continue a little further. This is to check for - // glue (which means there is potentially more content - // in this line) OR for non-text content such as choices. - if (_ptr != nullptr) - { - // Save a snapshot of the current runtime state so we - // can return here if we end up hitting a new line - forget(); - save(); - } - // Otherwise, make sure we don't have any snapshots hanging around - // expect we are in choice handleing - else if( !has_choices() && !_fallback_choice) { - forget(); - } else { - _output.forget(); - } + // If we're on a newline + if (_output.ends_with(value_type::newline)) { + // Unless we are out of content, we are going to try + // to continue a little further. This is to check for + // glue (which means there is potentially more content + // in this line) OR for non-text content such as choices. + if (_ptr != nullptr) { + // Save a snapshot of the current runtime state so we + // can return here if we end up hitting a new line + forget(); + save(); + } + // Otherwise, make sure we don't have any snapshots hanging around + // expect we are in choice handleing + else if (! has_choices() && ! _fallback_choice) { + forget(); + } else { + _output.forget(); } } - - return false; } - void runner_impl::step() - { + return false; +} + +void runner_impl::step() +{ #ifndef INK_ENABLE_UNREAL - try + try #endif - { - inkAssert(_ptr != nullptr, "Can not step! Do not have a valid pointer"); - - // Load current command - Command cmd = read(); - CommandFlag flag = read(); - - // If we're falling and we hit a non-fallthrough command, stop the fall. - if (_is_falling && !((cmd == Command::DIVERT && flag & CommandFlag::DIVERT_IS_FALLTHROUGH) || cmd == Command::END_CONTAINER_MARKER)) - { - _is_falling = false; - set_done_ptr(nullptr); - } - if (cmd >= Command::OP_BEGIN && cmd < Command::OP_END) - { - _operations(cmd, _eval); - } - else switch (cmd) - { - // == Value Commands == - case Command::STR: - { - const char* str = read(); - if (_evaluation_mode) - _eval.push(value{}.set(str)); - else - _output << value{}.set(str); - } - break; - case Command::INT: - { - int val = read(); - if (_evaluation_mode) - _eval.push(value{}.set(val)); - // TEST-CASE B006 don't print integers - } - break; - case Command::BOOL: - { - bool val = read() ? true : false; - if(_evaluation_mode) - _eval.push(value{}.set(val)); - else - _output << value{}.set(val); - } - break; - case Command::FLOAT: - { - float val = read(); - if (_evaluation_mode) - _eval.push(value{}.set(val)); - // TEST-CASE B006 don't print floats - } break; - case Command::VALUE_POINTER: - { - hash_t val = read(); - if(_evaluation_mode) { - _eval.push(value{}.set(val, static_cast(flag) - 1)); - } else { - inkFail("never conciderd what should happend here! (value pointer print)"); - } - } - break; - case Command::LIST: - { - list_table::list list(read()); - if(_evaluation_mode) - _eval.push(value{}.set(list)); - else { - char* str = _globals->strings().create(_globals->lists().stringLen( - list)+1); - _globals->lists().toString(str, list)[0] = 0; - _output << value{}.set(str); - } - } - break; - case Command::DIVERT_VAL: - { - inkAssert(_evaluation_mode, "Can not push divert value into the output stream!"); - - // Push the divert target onto the stack - uint32_t target = read(); - _eval.push(value{}.set(target)); - } - break; - case Command::NEWLINE: - { - if (_evaluation_mode) - _eval.push(values::newline); - else - _output << values::newline; - } - break; - case Command::GLUE: - { - if (_evaluation_mode) - _eval.push(values::glue); - else - _output << values::glue; - } - break; - case Command::VOID: - { - if (_evaluation_mode) - _eval.push(values::null); // TODO: void type? - } - break; - - // == Divert commands - case Command::DIVERT: - { - // Find divert address - uint32_t target = read(); - - // Check for condition - if (flag & CommandFlag::DIVERT_HAS_CONDITION && !_eval.pop().truthy(_globals->lists())) - break; - - // SPECIAL: Fallthrough divert. We're starting to fall out of containers - if (flag & CommandFlag::DIVERT_IS_FALLTHROUGH && !_is_falling) - { - // 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 - set_done_ptr(_ptr); - _is_falling = true; - } - - // If we're falling out of the story, then we're hitting an implied done - if (_is_falling && _story->instructions() + target == _story->end()) { - // Wait! We may be returning from a function! - frame_type type; - if (_stack.has_frame(&type) && type == frame_type::function) // implicit return is only for functions - { - // push null and return - _eval.push(values::null); - - // HACK - _ptr += sizeof(Command) + sizeof(CommandFlag); - execute_return(); + { + inkAssert(_ptr != nullptr, "Can not step! Do not have a valid pointer"); + + // Load current command + Command cmd = read(); + CommandFlag flag = read(); + + // If we're falling and we hit a non-fallthrough command, stop the fall. + if (_is_falling + && ! ( + (cmd == Command::DIVERT && flag & CommandFlag::DIVERT_IS_FALLTHROUGH) + || cmd == Command::END_CONTAINER_MARKER + )) { + _is_falling = false; + set_done_ptr(nullptr); + } + if (cmd >= Command::OP_BEGIN && cmd < Command::OP_END) { + _operations(cmd, _eval); + } else { + switch (cmd) { + // == Value Commands == + case Command::STR: { + const char* str = read(); + if (_evaluation_mode) { + _eval.push(value{}.set(str)); + } else { + _output << value{}.set(str); } - else - { - on_done(false); + } break; + case Command::INT: { + int val = read(); + if (_evaluation_mode) { + _eval.push(value{}.set(val)); } - break; - } + // TEST-CASE B006 don't print integers + } break; + case Command::BOOL: { + bool val = read() ? true : false; + if (_evaluation_mode) { + _eval.push(value{}.set(val)); + } else { + _output << value{}.set(val); + } + } break; + case Command::FLOAT: { + float val = read(); + if (_evaluation_mode) { + _eval.push(value{}.set(val)); + } + // TEST-CASE B006 don't print floats + } break; + case Command::VALUE_POINTER: { + hash_t val = read(); + if (_evaluation_mode) { + _eval.push(value{}.set(val, static_cast(flag) - 1)); + } else { + inkFail("never conciderd what should happend here! (value pointer print)"); + } + } break; + case Command::LIST: { + list_table::list list(read()); + if (_evaluation_mode) { + _eval.push(value{}.set(list)); + } else { + char* str = _globals->strings().create(_globals->lists().stringLen(list) + 1); + _globals->lists().toString(str, list)[0] = 0; + _output << value{}.set(str); + } + } break; + case Command::DIVERT_VAL: { + inkAssert(_evaluation_mode, "Can not push divert value into the output stream!"); + + // Push the divert target onto the stack + uint32_t target = read(); + _eval.push(value{}.set(target)); + } break; + case Command::NEWLINE: { + if (_evaluation_mode) { + _eval.push(values::newline); + } else { + _output << values::newline; + } + } break; + case Command::GLUE: { + if (_evaluation_mode) { + _eval.push(values::glue); + } else { + _output << values::glue; + } + } break; + case Command::VOID: { + if (_evaluation_mode) { + _eval.push(values::null); // TODO: void type? + } + } break; - // Do the jump - inkAssert(_story->instructions() + target < _story->end(), "Diverting past end of story data!"); - jump(_story->instructions() + target, true); - } - break; - case Command::DIVERT_TO_VARIABLE: - { - // Get variable value - hash_t variable = read(); + // == Divert commands + case Command::DIVERT: { + // Find divert address + uint32_t target = read(); - // Check for condition - if (flag & CommandFlag::DIVERT_HAS_CONDITION && !_eval.pop().truthy(_globals->lists())) - break; - - const value* val = get_var(variable); - inkAssert(val, "Jump destiniation needs to be defined!"); + // Check for condition + if (flag & CommandFlag::DIVERT_HAS_CONDITION && ! _eval.pop().truthy(_globals->lists())) { + break; + } - // Move to location - jump(_story->instructions() + val->get(), true); - inkAssert(_ptr < _story->end(), "Diverted past end of story data!"); - } - break; + // SPECIAL: Fallthrough divert. We're starting to fall out of containers + if (flag & CommandFlag::DIVERT_IS_FALLTHROUGH && ! _is_falling) { + // 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 + set_done_ptr(_ptr); + _is_falling = true; + } - // == Terminal commands - case Command::DONE: - on_done(true); - break; + // If we're falling out of the story, then we're hitting an implied done + if (_is_falling && _story->instructions() + target == _story->end()) { + // Wait! We may be returning from a function! + frame_type type; + if (_stack.has_frame(&type) + && type == frame_type::function) // implicit return is only for functions + { + // push null and return + _eval.push(values::null); + + // HACK + _ptr += sizeof(Command) + sizeof(CommandFlag); + execute_return(); + } else { + on_done(false); + } + break; + } - case Command::END: - _ptr = nullptr; - break; + // Do the jump + inkAssert( + _story->instructions() + target < _story->end(), "Diverting past end of story data!" + ); + jump(_story->instructions() + target, true); + } break; + case Command::DIVERT_TO_VARIABLE: { + // Get variable value + hash_t variable = read(); + + // Check for condition + if (flag & CommandFlag::DIVERT_HAS_CONDITION && ! _eval.pop().truthy(_globals->lists())) { + break; + } - // == Tunneling - case Command::TUNNEL: - { - uint32_t target; - // Find divert address - if(flag & CommandFlag::TUNNEL_TO_VARIABLE) { - hash_t var_name = read(); - const value* val = get_var(var_name); - inkAssert(val != nullptr, "Variable containing tunnel target could not be found!"); - target = val->get(); - } else { - target = read(); - } - start_frame(target); - } - break; - case Command::FUNCTION: - { - uint32_t target; - // Find divert address - if(flag & CommandFlag::FUNCTION_TO_VARIABLE) { - hash_t var_name = read(); - const value* val = get_var(var_name); - inkAssert(val != nullptr, "Varibale containing function could not be found!"); - target = val->get(); - } else { - target = read(); - } - if (!(flag & CommandFlag::FALLBACK_FUNCTION)) { - start_frame(target); - } else { - inkAssert(!_eval.is_empty(), "fallback function but no function call before?"); - if(_eval.top_value().type() == value_type::ex_fn_not_found) { - _eval.pop(); - inkAssert(target != 0, "Exetrnal function was not binded, and no fallback function provided!"); + const value* val = get_var(variable); + inkAssert(val, "Jump destiniation needs to be defined!"); + + // Move to location + jump(_story->instructions() + val->get(), true); + inkAssert(_ptr < _story->end(), "Diverted past end of story data!"); + } break; + + // == Terminal commands + case Command::DONE: on_done(true); break; + + case Command::END: _ptr = nullptr; break; + + // == Tunneling + case Command::TUNNEL: { + uint32_t target; + // Find divert address + if (flag & CommandFlag::TUNNEL_TO_VARIABLE) { + hash_t var_name = read(); + const value* val = get_var(var_name); + inkAssert(val != nullptr, "Variable containing tunnel target could not be found!"); + target = val->get(); + } else { + target = read(); + } + start_frame(target); + } break; + case Command::FUNCTION: { + uint32_t target; + // Find divert address + if (flag & CommandFlag::FUNCTION_TO_VARIABLE) { + hash_t var_name = read(); + const value* val = get_var(var_name); + inkAssert(val != nullptr, "Varibale containing function could not be found!"); + target = val->get(); + } else { + target = read(); + } + if (! (flag & CommandFlag::FALLBACK_FUNCTION)) { start_frame(target); + } else { + inkAssert(! _eval.is_empty(), "fallback function but no function call before?"); + if (_eval.top_value().type() == value_type::ex_fn_not_found) { + _eval.pop(); + inkAssert( + target != 0, + "Exetrnal function was not binded, and no fallback function provided!" + ); + start_frame(target); + } + } + } break; + case Command::TUNNEL_RETURN: + case Command::FUNCTION_RETURN: { + 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(), _evaluation_mode + ); + _ref_stack.push_frame( + returnTo - _story->instructions(), _evaluation_mode + ); + + // Fork a new thread on the callstack + thread_t thread = _stack.fork_thread(); + { + thread_t t = _ref_stack.fork_thread(); + inkAssert(t == thread, "ref_stack and stack should be in sync!"); } - } - } - break; - case Command::TUNNEL_RETURN: - case Command::FUNCTION_RETURN: - { - 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(), _evaluation_mode); - _ref_stack.push_frame(returnTo - _story->instructions(), _evaluation_mode); - - // Fork a new thread on the callstack - thread_t thread = _stack.fork_thread(); - { - thread_t t = _ref_stack.fork_thread(); - inkAssert(t == thread, "ref_stack and stack should be in sync!"); - } - - // Push that thread onto our thread stack - _threads.push(thread); - } - break; - - // == set temporärie variable - case Command::DEFINE_TEMP: - { - hash_t variableName = read(); - bool is_redef = flag & CommandFlag::ASSIGNMENT_IS_REDEFINE; - - // Get the top value and put it into the variable - value v = _eval.pop(); - set_var(variableName, v, is_redef); - } - break; - case Command::SET_VARIABLE: - { - hash_t variableName = read(); + // Push that thread onto our thread stack + _threads.push(thread); + } break; + + // == set temporärie variable + case Command::DEFINE_TEMP: { + hash_t variableName = read(); + bool is_redef = flag & CommandFlag::ASSIGNMENT_IS_REDEFINE; + + // Get the top value and put it into the variable + value v = _eval.pop(); + set_var(variableName, v, is_redef); + } break; + + case Command::SET_VARIABLE: { + hash_t variableName = read(); + + // Check if it's a redefinition (not yet used, seems important for pointers later?) + bool is_redef = flag & CommandFlag::ASSIGNMENT_IS_REDEFINE; + + // If not, we're setting a global (temporary variables are explicitely defined as such, + // where globals are defined using SET_VARIABLE). + value val = _eval.pop(); + if (is_redef) { + set_var(variableName, val, is_redef); + } else { + set_var(variableName, val, is_redef); + } + } break; - // Check if it's a redefinition (not yet used, seems important for pointers later?) - bool is_redef = flag & CommandFlag::ASSIGNMENT_IS_REDEFINE; + // == Function calls + case Command::CALL_EXTERNAL: { + // Read function name + hash_t functionName = read(); - // If not, we're setting a global (temporary variables are explicitely defined as such, - // where globals are defined using SET_VARIABLE). - value val = _eval.pop(); - if(is_redef) { - set_var(variableName, val, is_redef); - } else { - set_var(variableName, val, is_redef); - } - } - break; + // Interpret flag as argument count + int numArguments = ( int ) flag; - // == Function calls - case Command::CALL_EXTERNAL: - { - // Read function name - hash_t functionName = read(); + // find and execute. will automatically push a valid if applicable + bool success = _functions.call( + functionName, &_eval, numArguments, _globals->strings(), _globals->lists() + ); - // Interpret flag as argument count - int numArguments = (int)flag; + // If we failed, notify a potential fallback function + if (! success) { + _eval.push(values::ex_fn_not_found); + } + } break; - // find and execute. will automatically push a valid if applicable - bool success = _functions.call(functionName, &_eval, numArguments, _globals->strings(), _globals->lists()); + // == Evaluation stack + case Command::START_EVAL: _evaluation_mode = true; break; + case Command::END_EVAL: + _evaluation_mode = false; - // If we failed, notify a potential fallback function - if (!success) - { - _eval.push(values::ex_fn_not_found); + // Assert stack is empty? Is that necessary? + break; + case Command::OUTPUT: { + value v = _eval.pop(); + _output << v; + } break; + case Command::POP: _eval.pop(); break; + case Command::DUPLICATE: _eval.push(_eval.top_value()); break; + case Command::PUSH_VARIABLE_VALUE: { + // Try to find in local stack + hash_t variableName = read(); + const value* val = get_var(variableName); + + inkAssert(val != nullptr, "Could not find variable!"); + _eval.push(*val); + break; } - } - break; - - // == Evaluation stack - case Command::START_EVAL: - _evaluation_mode = true; - break; - case Command::END_EVAL: - _evaluation_mode = false; + case Command::START_STR: { + inkAssert(_evaluation_mode, "Can not enter string mode while not in evaluation mode!"); + _string_mode = true; + _evaluation_mode = false; + _output << values::marker; + } break; + case Command::END_STR: { + // TODO: Assert we really had a marker on there? + inkAssert(! _evaluation_mode, "Must be in evaluation mode"); + _string_mode = false; + _evaluation_mode = true; + + // Load value from output stream + // Push onto stack + _eval.push(value{}.set( + _output.get_alloc(_globals->strings(), _globals->lists()) + )); + } break; + + case Command::START_TAG: { + _output << values::marker; + } break; + + case Command::END_TAG: { + auto tag = _output.get_alloc(_globals->strings(), _globals->lists()); + if (_string_mode && _choice_tags_begin < 0) { + _choice_tags_begin = _tags.size(); + } + _tags.push() = tag; + } break; + + // == Choice commands + case Command::CHOICE: { + // Read path + uint32_t path = read(); + + // If we're a once only choice, make sure our destination hasn't + // been visited + if (flag & CommandFlag::CHOICE_IS_ONCE_ONLY) { + // Need to convert offset to container index + container_t destination = -1; + if (_story->get_container_id(_story->instructions() + path, destination)) { + // Ignore the choice if we've visited the destination before + if (_globals->visits(destination) > 0) { + break; + } + } else { + inkAssert(false, "Destination for choice block does not have counting flags."); + } + } - // Assert stack is empty? Is that necessary? - break; - case Command::OUTPUT: - { - value v = _eval.pop(); - _output << v; - } - break; - case Command::POP: - _eval.pop(); - break; - case Command::DUPLICATE: - _eval.push(_eval.top_value()); - break; - case Command::PUSH_VARIABLE_VALUE: - { - // Try to find in local stack - hash_t variableName = read(); - const value* val = get_var(variableName); - - inkAssert(val != nullptr, "Could not find variable!"); - _eval.push(*val); - break; - } - case Command::START_STR: - { - inkAssert(_evaluation_mode, "Can not enter string mode while not in evaluation mode!"); - _string_mode = true; - _evaluation_mode = false; - _output << values::marker; - } break; - case Command::END_STR: - { - // TODO: Assert we really had a marker on there? - inkAssert(!_evaluation_mode, "Must be in evaluation mode"); - _string_mode = false; - _evaluation_mode = true; - - // Load value from output stream - // Push onto stack - _eval.push(value{}.set(_output.get_alloc( - _globals->strings(), - _globals->lists()))); - } break; - - case Command::START_TAG: - { - _output << values::marker; - } break; - - case Command::END_TAG: - { - auto tag = _output.get_alloc(_globals->strings(), _globals->lists()); - if(_string_mode && _choice_tags_begin < 0) { - _choice_tags_begin = _tags.size(); - } - _tags.push() = tag; - } break; - - // == Choice commands - case Command::CHOICE: - { - // Read path - uint32_t path = read(); - - // If we're a once only choice, make sure our destination hasn't - // been visited - if (flag & CommandFlag::CHOICE_IS_ONCE_ONLY) { - // Need to convert offset to container index - container_t destination = -1; - if (_story->get_container_id(_story->instructions() + path, destination)) - { - // Ignore the choice if we've visited the destination before - if (_globals->visits(destination) > 0) + // Choice is conditional + if (flag & CommandFlag::CHOICE_HAS_CONDITION) { + // Only show if the top of the eval stack is 'truthy' + if (! _eval.pop().truthy(_globals->lists())) { break; + } } - else - { - inkAssert(false, "Destination for choice block does not have counting flags."); - } - } - // Choice is conditional - if (flag & CommandFlag::CHOICE_HAS_CONDITION) { - // Only show if the top of the eval stack is 'truthy' - if(!_eval.pop().truthy(_globals->lists())) - break; - } + // Use a marker to start compiling the choice text + _output << values::marker; + value stack[2]; + int sc = 0; - // Use a marker to start compiling the choice text - _output << values::marker; - value stack[2]; - int sc = 0; + if (flag & CommandFlag::CHOICE_HAS_START_CONTENT) { + stack[sc++] = _eval.pop(); + } + if (flag & CommandFlag::CHOICE_HAS_CHOICE_ONLY_CONTENT) { + stack[sc++] = _eval.pop(); + } + for (; sc; --sc) { + _output << stack[sc - 1]; + } - if (flag & CommandFlag::CHOICE_HAS_START_CONTENT) { - stack[sc++] = _eval.pop(); - } - if (flag & CommandFlag::CHOICE_HAS_CHOICE_ONLY_CONTENT) { - stack[sc++] = _eval.pop(); - } - for(;sc;--sc) { _output << stack[sc-1]; } + // fetch relevant tags + const snap_tag* tags = nullptr; + if (_choice_tags_begin >= 0 && _tags[_tags.size() - 1] != nullptr) { + for (tags = _tags.end() - 1; + *(tags - 1) != nullptr && (tags - _tags.begin()) > _choice_tags_begin; --tags) + ; + _tags.push() = nullptr; + } - // fetch relevant tags - const snap_tag* tags = nullptr; - if (_choice_tags_begin >= 0 && _tags[_tags.size()-1] != nullptr) { - for(tags = _tags.end() - 1; *(tags-1) != nullptr && (tags - _tags.begin()) > _choice_tags_begin; --tags); - _tags.push() = nullptr; - } + // Create choice and record it + if (flag & CommandFlag::CHOICE_IS_INVISIBLE_DEFAULT) { + _fallback_choice = choice{}.setup( + _output, _globals->strings(), _globals->lists(), _choices.size(), path, + current_thread(), tags->ptr() + ); + } else { + add_choice().setup( + _output, _globals->strings(), _globals->lists(), _choices.size(), path, + current_thread(), tags->ptr() + ); + } + // save stack at last choice + if (_saved) { + forget(); + } + save(); + } break; + case Command::START_CONTAINER_MARKER: { + // Keep track of current container + auto index = read(); + // offset points to command, command has size 6 + _container.push({.id = index, .offset = _ptr - 6}); + + // Increment visit count + if (flag & CommandFlag::CONTAINER_MARKER_TRACK_VISITS + || flag & CommandFlag::CONTAINER_MARKER_TRACK_TURNS) { + _globals->visit(_container.top().id, true); + } - // Create choice and record it - if (flag & CommandFlag::CHOICE_IS_INVISIBLE_DEFAULT) { - _fallback_choice - = choice{}.setup(_output, _globals->strings(), _globals->lists(), _choices.size(), path, current_thread(), tags->ptr()); - } else { - add_choice().setup(_output, _globals->strings(), _globals->lists(), _choices.size(), path, current_thread(), tags->ptr()); - } - // save stack at last choice - if(_saved) { forget(); } - save(); - } break; - case Command::START_CONTAINER_MARKER: - { - // Keep track of current container - auto index = read(); - // offset points to command, command has size 6 - _container.push({.id=index, .offset=_ptr - 6}); - - // Increment visit count - if (flag & CommandFlag::CONTAINER_MARKER_TRACK_VISITS || flag & CommandFlag::CONTAINER_MARKER_TRACK_TURNS) - { - _globals->visit(_container.top().id, true); - } + } break; + case Command::END_CONTAINER_MARKER: { + container_t index = read(); - } break; - case Command::END_CONTAINER_MARKER: - { - container_t index = read(); - inkAssert(_container.top().id == index, "Leaving container we are not in!"); + inkAssert(_container.top().id == index, "Leaving container we are not in!"); - // Move up out of the current container - _container.pop(); + // Move up out of the current container + _container.pop(); - // SPECIAL: If we've popped all containers, then there's an implied 'done' command or return - if (_container.empty()) - { - _is_falling = false; + // SPECIAL: If we've popped all containers, then there's an implied + // 'done' command or return + if (_container.empty()) { + _is_falling = false; - frame_type type; - if (!_threads.empty()) - { - on_done(false); - break; - } - else if (_stack.has_frame(&type) && type == frame_type::function) // implicit return is only for functions - { - // push null and return - _eval.push(values::null); - - // HACK - _ptr += sizeof(Command) + sizeof(CommandFlag); - execute_return(); - } else if (_ptr == _story->end()){ // check needed, because it colud exist an unnamed toplevel container (empty named container stack != empty container stack) - on_done(true); + frame_type type; + if (! _threads.empty()) { + on_done(false); + break; + } else if (_stack.has_frame(&type) && type == frame_type::function) // implicit return + // is only for + // functions + { + // push null and return + _eval.push(values::null); + + // HACK + _ptr += sizeof(Command) + sizeof(CommandFlag); + execute_return(); + } else if (_ptr == _story->end()) { // check needed, because it colud exist an unnamed + // toplevel container (empty named container stack + // != empty container stack) + on_done(true); + } } - } - } break; - case Command::VISIT: - { - // Push the visit count for the current container to the top - // is 0-indexed for some reason. idk why but this is what ink expects - _eval.push(value{}.set((int)_globals->visits(_container.top().id) - 1)); - } break; - case Command::TURN: - { - _eval.push(value{}.set((int)_globals->turns())); - } break; - case Command::SEQUENCE: - { - // TODO: The C# ink runtime does a bunch of fancy logic - // to make sure each element is picked at least once in every - // iteration loop. I don't feel like replicating that right now. - // So, let's just return a random number and *shrug* - int sequenceLength = _eval.pop().get(); - int index = _eval.pop().get(); - - _eval.push(value{}.set(static_cast(_rng.rand(sequenceLength)))); - } break; - case Command::SEED: - { - int32_t seed = _eval.pop().get(); - _rng.srand(seed); - - _eval.push(values::null); - } break; - - case Command::READ_COUNT: - { - // Get container index - container_t container = read(); - - // Push the read count for the requested container index - _eval.push(value{}.set((int)_globals->visits(container))); - } break; - case Command::TAG: - { - _tags.push() = read(); - } break; - default: - inkAssert(false, "Unrecognized command!"); - break; + } break; + case Command::VISIT: { + // Push the visit count for the current container to the top + // is 0-indexed for some reason. idk why but this is what ink expects + _eval.push( + value{}.set(( int ) _globals->visits(_container.top().id) - 1) + ); + } break; + case Command::TURN: { + _eval.push(value{}.set(( int ) _globals->turns())); + } break; + case Command::SEQUENCE: { + // TODO: The C# ink runtime does a bunch of fancy logic + // to make sure each element is picked at least once in every + // iteration loop. I don't feel like replicating that right now. + // So, let's just return a random number and *shrug* + int sequenceLength = _eval.pop().get(); + int index = _eval.pop().get(); + + _eval.push(value{}.set(static_cast(_rng.rand(sequenceLength))) + ); + } break; + case Command::SEED: { + int32_t seed = _eval.pop().get(); + _rng.srand(seed); + + _eval.push(values::null); + } break; + + case Command::READ_COUNT: { + // Get container index + container_t container = read(); + + // Push the read count for the requested container index + _eval.push(value{}.set(( int ) _globals->visits(container))); + } break; + case Command::TAG: { + _tags.push() = read(); + } break; + default: inkAssert(false, "Unrecognized command!"); break; } - } + + } #ifndef INK_ENABLE_UNREAL - catch (...) - { - // Reset our whole state as it's probably corrupt - reset(); - throw; - } -#endif + catch (...) { + // Reset our whole state as it's probably corrupt + reset(); + throw; } +#endif +} - 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); - _ref_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..."); - // if thread ends, move stave point with, else the thread end marker is missing - // and we can't collect the other threads - if(_saved) { forget(); save(); } +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); + _ref_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..." + ); + // if thread ends, move stave point with, else the thread end marker is missing + // and we can't collect the other threads + if (_saved) { + forget(); + save(); } - else - { - if (setDone) - set_done_ptr(_ptr); - _ptr = nullptr; + } 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 { - _threads.set(curr, ptr); - } +void runner_impl::set_done_ptr(ip_t ptr) +{ + thread_t curr = current_thread(); + if (curr == ~0) { + _done = ptr; + } else { + _threads.set(curr, ptr); } +} - void runner_impl::reset() - { - _eval.clear(); - _output.clear(); - _stack.clear(); - _ref_stack.clear(); - _threads.clear(); - _evaluation_mode = false; - _saved = false; - _choices.clear(); - _ptr = nullptr; - _done = nullptr; - _container.clear(); - } +void runner_impl::reset() +{ + _eval.clear(); + _output.clear(); + _stack.clear(); + _ref_stack.clear(); + _threads.clear(); + _evaluation_mode = false; + _saved = false; + _choices.clear(); + _ptr = nullptr; + _done = nullptr; + _container.clear(); +} - void runner_impl::mark_used(string_table& strings, list_table& lists) const - { - // Find strings in output and stacks - _output.mark_used(strings, lists); - _stack.mark_used(strings, lists); - // ref_stack has no strings and lists! - _eval.mark_used(strings, lists); - - // Take into account tags - for (size_t i = 0; i < _tags.size(); ++i) { - strings.mark_used(_tags[i]); - } - // Take into account choice text - for (size_t i = 0; i < _choices.size(); i++) - strings.mark_used(_choices[i]._text); +void runner_impl::mark_used(string_table& strings, list_table& lists) const +{ + // Find strings in output and stacks + _output.mark_used(strings, lists); + _stack.mark_used(strings, lists); + // ref_stack has no strings and lists! + _eval.mark_used(strings, lists); + + // Take into account tags + for (size_t i = 0; i < _tags.size(); ++i) { + strings.mark_used(_tags[i]); } - - void runner_impl::save() - { - inkAssert(!_saved, "Runner state already saved"); - - _saved = true; - _output.save(); - _stack.save(); - _ref_stack.save(); - _backup = _ptr; - _container.save(); - _globals->save(); - _eval.save(); - _threads.save(); - _choices.save(); - _tags.save(); - _saved_evaluation_mode = _evaluation_mode; - - // Not doing this anymore. There can be lingering stack entries from function returns - // inkAssert(_eval.is_empty(), "Can not save interpreter state while eval stack is not empty"); + // Take into account choice text + for (size_t i = 0; i < _choices.size(); i++) { + strings.mark_used(_choices[i]._text); } +} - void runner_impl::restore() - { - inkAssert(_saved, "Can't restore. No runner state saved."); - // the output can be restored without the rest - if(_output.saved()) {_output.restore(); } - _stack.restore(); - _ref_stack.restore(); - _ptr = _backup; - _container.restore(); - _globals->restore(); - _eval.restore(); - _threads.restore(); - _choices.restore(); - _tags.restore(); - _evaluation_mode = _saved_evaluation_mode; - - // Not doing this anymore. There can be lingering stack entries from function returns - // inkAssert(_eval.is_empty(), "Can not save interpreter state while eval stack is not empty"); - - _saved = false; +void runner_impl::save() +{ + inkAssert(! _saved, "Runner state already saved"); + + _saved = true; + _output.save(); + _stack.save(); + _ref_stack.save(); + _backup = _ptr; + _container.save(); + _globals->save(); + _eval.save(); + _threads.save(); + _choices.save(); + _tags.save(); + _saved_evaluation_mode = _evaluation_mode; + + // Not doing this anymore. There can be lingering stack entries from function returns + // inkAssert(_eval.is_empty(), "Can not save interpreter state while eval stack is not empty"); +} + +void runner_impl::restore() +{ + inkAssert(_saved, "Can't restore. No runner state saved."); + // the output can be restored without the rest + if (_output.saved()) { + _output.restore(); } + _stack.restore(); + _ref_stack.restore(); + _ptr = _backup; + _container.restore(); + _globals->restore(); + _eval.restore(); + _threads.restore(); + _choices.restore(); + _tags.restore(); + _evaluation_mode = _saved_evaluation_mode; + + // Not doing this anymore. There can be lingering stack entries from function returns + // inkAssert(_eval.is_empty(), "Can not save interpreter state while eval stack is not empty"); + + _saved = false; +} - void runner_impl::forget() - { - // Do nothing if we haven't saved - if (!_saved) - return; - - _output.forget(); - _stack.forget(); - _ref_stack.forget(); - _container.forget(); - _globals->forget(); - _eval.forget(); - _threads.forget(); - _choices.forgett(); - _tags.forgett(); - - // Nothing to do for eval stack. It should just stay as it is - - _saved = false; +void runner_impl::forget() +{ + // Do nothing if we haven't saved + if (! _saved) { + return; } + _output.forget(); + _stack.forget(); + _ref_stack.forget(); + _container.forget(); + _globals->forget(); + _eval.forget(); + _threads.forget(); + _choices.forgett(); + _tags.forgett(); + + // Nothing to do for eval stack. It should just stay as it is + + _saved = false; +} + #ifdef INK_ENABLE_STL - std::ostream& operator<<(std::ostream& out, runner_impl& in) - { - in.getline(out); - return out; - } -#endif +std::ostream& operator<<(std::ostream& out, runner_impl& in) +{ + in.getline(out); + return out; } +#endif +} // namespace ink::runtime::internal diff --git a/inkcpp/story_impl.cpp b/inkcpp/story_impl.cpp index 223d1407..64ee33b6 100644 --- a/inkcpp/story_impl.cpp +++ b/inkcpp/story_impl.cpp @@ -7,10 +7,6 @@ #include "snapshot_interface.h" #include "version.h" -#ifdef INK_ENABLE_STL -#include -#endif - namespace ink::runtime { #ifdef INK_ENABLE_STL diff --git a/inkcpp/story_ptr.cpp b/inkcpp/story_ptr.cpp index 99f9b6dd..780aa8e8 100644 --- a/inkcpp/story_ptr.cpp +++ b/inkcpp/story_ptr.cpp @@ -66,4 +66,4 @@ namespace ink::runtime::internal _instance_block = _story_block = nullptr; return is_destroyed; } -} \ No newline at end of file + } // namespace ink::runtime::internal diff --git a/inkcpp_cl/inkcpp_cl.cpp b/inkcpp_cl/inkcpp_cl.cpp index f5d55f4b..ab429508 100644 --- a/inkcpp_cl/inkcpp_cl.cpp +++ b/inkcpp_cl/inkcpp_cl.cpp @@ -17,58 +17,46 @@ void usage() { using namespace std; - cout - << "Usage: inkcpp_cl \n" - << "\t-o :\tOutput file name\n" - << "\t-p []:\tPlay mode\n\toptional snapshot file to load\n\tto create a snapshot file enter '-1' as choice\n" - << "\t--ommit-choice-tags:\tdo not print tags after choices, primarly used to be compatible with inkclecat output" - << endl; + cout << "Usage: inkcpp_cl \n" + << "\t-o :\tOutput file name\n" + << "\t-p []:\tPlay mode\n\toptional snapshot file to load\n\tto create a " + "snapshot file enter '-1' as choice\n" + << "\t--ommit-choice-tags:\tdo not print tags after choices, primarly used to be compatible " + "with inkclecat output" + << endl; } int main(int argc, const char** argv) { // Usage - if (argc == 1) - { + if (argc == 1) { usage(); return 1; } // Parse options std::string outputFilename; - bool playMode = false, - testMode = false, - testDirectory = false, - ommit_choice_tags = false; + bool playMode = false, testMode = false, testDirectory = false, ommit_choice_tags = false; std::string snapshotFile; - for (int i = 1; i < argc - 1; i++) - { + for (int i = 1; i < argc - 1; i++) { std::string option = argv[i]; - if (option == "-o") - { + if (option == "-o") { outputFilename = argv[i + 1]; i += 1; - } - else if (option == "-p") { + } else if (option == "-p") { playMode = true; - if (i + 1 < argc - 1 && argv[i+1][0] != '-') { + if (i + 1 < argc - 1 && argv[i + 1][0] != '-') { ++i; snapshotFile = argv[i]; } - } - else if (option == "--ommit-choice-tags") - { + } else if (option == "--ommit-choice-tags") { ommit_choice_tags = true; - } - else if (option == "-t") - testMode = true; - else if (option == "-td") - { + } else if (option == "-t") { testMode = true; + } else if (option == "-td") { + testMode = true; testDirectory = true; - } - else - { + } else { std::cerr << "Unrecognized option: '" << option << "'\n"; } } @@ -77,120 +65,117 @@ int main(int argc, const char** argv) std::string inputFilename = argv[argc - 1]; // Test mode - if (testMode) - { + if (testMode) { bool result; - if (testDirectory) + if (testDirectory) { result = test_directory(inputFilename); - else + } else { result = test(inputFilename); + } return result ? 0 : -1; } // If output filename not specified, use input filename as guideline - if (outputFilename.empty()) - { + if (outputFilename.empty()) { outputFilename = std::regex_replace(inputFilename, std::regex("\\.[^\\.]+$"), ".bin"); } // If input filename is an .ink file - int val = inputFilename.find(".ink"); + int val = inputFilename.find(".ink"); bool json_file_is_tmp_file = false; - if (val == inputFilename.length() - 4) - { + if (val == inputFilename.length() - 4) { // Create temporary filename std::string jsonFile = std::regex_replace(inputFilename, std::regex("\\.[^\\.]+$"), ".tmp"); // Then we need to do a compilation with inklecate - try - { + try { inklecate(inputFilename, jsonFile); - } - catch (const std::exception& e) - { + } catch (const std::exception& e) { std::cerr << "Inklecate Error: " << e.what() << std::endl; return 1; } // New input is the json file json_file_is_tmp_file = true; - inputFilename = jsonFile; + inputFilename = jsonFile; } // Open file and compile - try - { + try { ink::compiler::compilation_results results; - std::ofstream fout(outputFilename, std::ios::binary | std::ios::out); + std::ofstream fout(outputFilename, std::ios::binary | std::ios::out); ink::compiler::run(inputFilename.c_str(), fout, &results); fout.close(); - if(json_file_is_tmp_file) { remove(inputFilename.c_str()); } + if (json_file_is_tmp_file) { + remove(inputFilename.c_str()); + } // Report errors - for (auto& warn : results.warnings) + for (auto& warn : results.warnings) { std::cerr << "WARNING: " << warn << '\n'; - for (auto& err : results.errors) + } + for (auto& err : results.errors) { std::cerr << "ERROR: " << err << '\n'; + } - if (results.errors.size() > 0 && playMode) - { + if (results.errors.size() > 0 && playMode) { std::cerr << "Cancelling play mode. Errors detected in compilation" << std::endl; return -1; } - } - catch (std::exception& e) - { - if(json_file_is_tmp_file) { remove(inputFilename.c_str()); } + } catch (std::exception& e) { + if (json_file_is_tmp_file) { + remove(inputFilename.c_str()); + } std::cerr << "Unhandled InkBin compiler exception: " << e.what() << std::endl; return 1; } - if (!playMode) + if (! playMode) { return 0; + } // Run the story - try - { + try { using namespace ink::runtime; // Load story - story* myInk = story::from_file(outputFilename.c_str()); + std::unique_ptr myInk{story::from_file(outputFilename.c_str())}; // Start runner runner thread; if (snapshotFile.size()) { - auto snap_ptr = snapshot::from_file( snapshotFile.c_str() ); - thread = myInk->new_runner_from_snapshot(*snap_ptr); + auto snap_ptr = snapshot::from_file(snapshotFile.c_str()); + thread = myInk->new_runner_from_snapshot(*snap_ptr); delete snap_ptr; } else { thread = myInk->new_runner(); } - while (true) - { - while (thread->can_continue()) + while (true) { + while (thread->can_continue()) { std::cout << thread->getline(); - if (thread->has_tags()){ + } + if (thread->has_tags()) { std::cout << "# tags: "; for (int i = 0; i < thread->num_tags(); ++i) { - if(i != 0) std::cout << ", "; + if (i != 0) { + std::cout << ", "; + } std::cout << thread->get_tag(i); } std::cout << std::endl; } - if (thread->has_choices()) - { + if (thread->has_choices()) { // Extra end line std::cout << std::endl; int index = 1; - for (const ink::runtime::choice& c : *thread) - { + for (const ink::runtime::choice& c : *thread) { std::cout << index++ << ": " << c.text(); - if(!ommit_choice_tags && c.has_tags()) { + if (! ommit_choice_tags && c.has_tags()) { std::cout << "\n\t"; - for(size_t i = 0; i < c.num_tags(); ++i) { + for (size_t i = 0; i < c.num_tags(); ++i) { std::cout << "# " << c.get_tag(i) << " "; } } @@ -201,7 +186,9 @@ int main(int argc, const char** argv) std::cin >> c; if (c == -1) { snapshot* snap = thread->create_snapshot(); - snap->write_to_file(std::regex_replace(inputFilename, std::regex("\\.[^\\.]+$"), ".snap").c_str()); + snap->write_to_file( + std::regex_replace(inputFilename, std::regex("\\.[^\\.]+$"), ".snap").c_str() + ); delete snap; break; } @@ -213,12 +200,9 @@ int main(int argc, const char** argv) // out of content break; } - - return 0; - } - catch (const std::exception& e) - { + } catch (const std::exception& e) { std::cerr << "Unhandled ink runtime exception: " << e.what() << std::endl; return 1; } + return 0; } diff --git a/inkcpp_test/CMakeLists.txt b/inkcpp_test/CMakeLists.txt index 7ffc8aff..40efe207 100644 --- a/inkcpp_test/CMakeLists.txt +++ b/inkcpp_test/CMakeLists.txt @@ -13,6 +13,8 @@ add_executable(inkcpp_test catch.hpp Main.cpp LabelCondition.cpp Observer.cpp InkyJson.cpp + SpaceAfterBracketChoice.cpp + ThirdTierChoiceAfterBrackets.cpp ) target_link_libraries(inkcpp_test PUBLIC inkcpp inkcpp_compiler inkcpp_shared) diff --git a/inkcpp_test/SpaceAfterBracketChoice.cpp b/inkcpp_test/SpaceAfterBracketChoice.cpp new file mode 100644 index 00000000..923a81a0 --- /dev/null +++ b/inkcpp_test/SpaceAfterBracketChoice.cpp @@ -0,0 +1,48 @@ +#include "catch.hpp" +#include "../inkcpp_cl/test.h" + +#include +#include +#include +#include + +using namespace ink::runtime; + +SCENARIO("a story with bracketed choices and spaces can choose correctly", "[choices]") +{ + GIVEN("a story with line breaks") + { + inklecate("ink/ChoiceBracketStory.ink", "ChoiceBracketStory.tmp"); + ink::compiler::run("ChoiceBracketStory.tmp", "ChoiceBracketStory.bin"); + auto ink = story::from_file("ChoiceBracketStory.bin"); + runner thread = ink->new_runner(); + thread->getall(); + WHEN("start thread") + { + THEN("thread has choices") + { + thread->getall(); + REQUIRE(thread->has_choices()); + } + WHEN("choose choice 1") + { + thread->choose(0); + thread->getall(); + THEN("still has choices") + { + thread->getall(); + REQUIRE(thread->has_choices()); + } + } + WHEN("choose choice 2") + { + thread->choose(1); + thread->getall(); + THEN("still has choices") + { + REQUIRE(thread->has_choices()); + } + } + } + } +} diff --git a/inkcpp_test/ThirdTierChoiceAfterBrackets.cpp b/inkcpp_test/ThirdTierChoiceAfterBrackets.cpp new file mode 100644 index 00000000..71fbfce6 --- /dev/null +++ b/inkcpp_test/ThirdTierChoiceAfterBrackets.cpp @@ -0,0 +1,43 @@ +#include "catch.hpp" +#include "../inkcpp_cl/test.h" + +#include +#include +#include +#include + +using namespace ink::runtime; + +SCENARIO( + "a story with a bracketed choice as a second choice, and then a third choice, chooses properly", + "[choices]" +) +{ + GIVEN("a story with brackets and nested choices") + { + inklecate("ink/ThirdTierChoiceAfterBracketsStory.ink", "ThirdTierChoiceAfterBracketsStory.tmp"); + ink::compiler::run( + "ThirdTierChoiceAfterBracketsStory.tmp", "ThirdTierChoiceAfterBracketsStory.bin" + ); + auto ink = story::from_file("ThirdTierChoiceAfterBracketsStory.bin"); + runner thread = ink->new_runner(); + + WHEN("start thread") + { + THEN("thread doesn't error") + { + thread->getall(); + thread->has_choices(); + thread->choose(0); + thread->getall(); + thread->has_choices(); + thread->choose(0); + thread->getall(); + thread->has_choices(); + thread->choose(0); + thread->getall(); + thread->has_choices(); + } + } + } +} diff --git a/inkcpp_test/ink/ChoiceBracketStory.ink b/inkcpp_test/ink/ChoiceBracketStory.ink new file mode 100644 index 00000000..6658b24c --- /dev/null +++ b/inkcpp_test/ink/ChoiceBracketStory.ink @@ -0,0 +1,7 @@ +-> choices += choices + ++ [Choice 1] ++ [Choice 2] + +- -> choices \ No newline at end of file diff --git a/inkcpp_test/ink/ThirdTierChoiceAfterBracketsStory.ink b/inkcpp_test/ink/ThirdTierChoiceAfterBracketsStory.ink new file mode 100644 index 00000000..72e67814 --- /dev/null +++ b/inkcpp_test/ink/ThirdTierChoiceAfterBracketsStory.ink @@ -0,0 +1,13 @@ +-> content +== content + +* Some choice + blah blah + blah blah blah +* * [This is the killer!] Text can be here though, just not on the left. + Blah blah + Something something +* * * Encountering this choice causes an error. +- Weave... + +-> DONE diff --git a/proofing/ink-proof b/proofing/ink-proof index efdfb70e..6f22c543 160000 --- a/proofing/ink-proof +++ b/proofing/ink-proof @@ -1 +1 @@ -Subproject commit efdfb70e81350d94375e221b457197be140dc968 +Subproject commit 6f22c5430f98e76cf29f1d92d8e8b718207e407e diff --git a/proofing/inkcpp_runtime_driver b/proofing/inkcpp_runtime_driver index 5db41602..25022f95 100644 --- a/proofing/inkcpp_runtime_driver +++ b/proofing/inkcpp_runtime_driver @@ -6,7 +6,7 @@ import shutil ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PATH = os.path.join(ROOT, 'deps', 'inkcpp', 'inkcpp_cl') -ARGS = ["inkcpp_cl", "-p"] + sys.argv[1:] +ARGS = ["inkcpp_cl", "--ommit-choice-tags", "-p"] + sys.argv[1:] os.execv(PATH, ARGS) sleep(2)