diff --git a/README.md b/README.md index 7fb9cffe..cc05a47b 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Many, but not all features of the Ink language are supported (see Glaring Omissi * Visit and read counts (`visits` and `CNT?` commands). * `seq` command and all sequence types (stopping, cycle, shuffle) * Global store that can be shared between runners -* External function binding (no fallback support yet) +* External function binding (define a function with same signature and name, which will be used if no function is bindeded) * Tunnels and internal functions * Ink threads (probably incredibly unstable though) diff --git a/inkcpp/functional.cpp b/inkcpp/functional.cpp index 034024ab..36cbff1b 100644 --- a/inkcpp/functional.cpp +++ b/inkcpp/functional.cpp @@ -32,6 +32,12 @@ namespace ink::runtime::internal stack->push(value{}.set(v)); } + void function_base::push_void(basic_eval_stack* stack) + { + stack->push(values::null); + } + + void function_base::push_string(basic_eval_stack* stack, const char* dynamic_string) { stack->push(value{}.set(dynamic_string, true)); diff --git a/inkcpp/include/functional.h b/inkcpp/include/functional.h index 96b029ad..574b2235 100644 --- a/inkcpp/include/functional.h +++ b/inkcpp/include/functional.h @@ -33,6 +33,8 @@ namespace ink::runtime::internal template static void push(basic_eval_stack* stack, const T& value); + static void push_void(basic_eval_stack* stack); + // string special push static void push_string(basic_eval_stack* stack, const char* dynamic_string); @@ -88,7 +90,7 @@ namespace ink::runtime::internal // Ink expects us to push something // TODO -- Should be a special "void" value - push(stack, 0); + push_void(stack); } else if constexpr (is_string::value) { diff --git a/inkcpp/runner_impl.cpp b/inkcpp/runner_impl.cpp index 0fdb3a63..123c4d67 100644 --- a/inkcpp/runner_impl.cpp +++ b/inkcpp/runner_impl.cpp @@ -722,9 +722,6 @@ namespace ink::runtime::internal // If we're on a newline if (_output.ends_with(value_type::newline)) { - // TODO: REMOVE - // return true; - // 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 @@ -962,7 +959,16 @@ namespace ink::runtime::internal } else { target = read(); } - start_frame(target); + 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: @@ -1034,18 +1040,11 @@ namespace ink::runtime::internal // find and execute. will automatically push a valid if applicable bool success = _functions.call(functionName, &_eval, numArguments, _globals->strings()); - // If we failed, we need to at least pretend so our state doesn't get fucked + // If we failed, notify a potential fallback function if (!success) { - // pop arguments - for (int i = 0; i < numArguments; i++) - _eval.pop(); - - // push void - _eval.push(value()); + _eval.push(values::ex_fn_not_found); } - - // TODO: Verify something was found? } break; @@ -1242,6 +1241,7 @@ namespace ink::runtime::internal inkAssert(false, "Unrecognized command!"); break; } + } #ifndef INK_ENABLE_UNREAL catch (...) diff --git a/inkcpp/value.h b/inkcpp/value.h index 277c5916..2e215296 100644 --- a/inkcpp/value.h +++ b/inkcpp/value.h @@ -43,6 +43,7 @@ namespace ink::runtime::internal { func_start, // start of function marker func_end, // end of function marker null, // void value, for function returns + ex_fn_not_found, // value for failed external function calls tunnel_frame, // return from tunnel function_frame, // return from function thread_frame, // return from thread @@ -376,6 +377,11 @@ namespace ink::runtime::internal { return *this; } template<> + inline constexpr value& value::set() { + _type = value_type::ex_fn_not_found; + return *this; + } + template<> inline constexpr value& value::set() { _type = value_type::newline; return *this; @@ -447,5 +453,6 @@ namespace ink::runtime::internal { static constexpr value func_start = value( value_type::func_start ); static constexpr value func_end = value( value_type::func_end ); static constexpr value null = value( value_type::null ); + static constexpr value ex_fn_not_found = value(value_type::ex_fn_not_found); } } diff --git a/inkcpp_compiler/binary_emitter.cpp b/inkcpp_compiler/binary_emitter.cpp index f5f1fd32..716a646a 100644 --- a/inkcpp_compiler/binary_emitter.cpp +++ b/inkcpp_compiler/binary_emitter.cpp @@ -17,7 +17,6 @@ namespace ink::compiler::internal using std::vector; using std::map; using std::string; - using std::tuple; char* strtok_s(char * s, const char * sep, char** context) { #if defined(_WIN32) || defined(_WIN64) @@ -107,8 +106,9 @@ namespace ink::compiler::internal _current->children.push_back(container); _current->indexed_children.insert({ index_in_parent, container }); - if (!name.empty()) + if (!name.empty()) { _current->named_children.insert({ name, container }); + } } // Set this as the current pointer @@ -127,6 +127,23 @@ namespace ink::compiler::internal return _containers.pos(); } + int binary_emitter::function_container_arguments(const std::string& name) + { + if(_root == nullptr) { return -1; } + auto fn = _root->named_children.find(name); + if (fn == _root->named_children.end()) { return -1; } + + size_t offset = fn->second->offset; + byte_t cmd = _containers.get(offset); + int arity = 0; + while(static_cast(cmd) == Command::DEFINE_TEMP) { + offset += 6; // command(1) + flag(1) + variable_name_hash(4) + cmd = _containers.get(offset); + ++arity; + } + return arity; + } + void binary_emitter::write_raw(Command command, CommandFlag flag, const char* payload, ink::size_t payload_size) { _containers.write(command); @@ -142,7 +159,8 @@ namespace ink::compiler::internal // Note the position of this later so we can resolve the paths at the end size_t param_position = _containers.pos() - sizeof(uint32_t); - _paths.push_back(std::make_tuple(param_position, path, _current, useCountIndex)); + bool op = flag & CommandFlag::FALLBACK_FUNCTION; + _paths.push_back(std::make_tuple(param_position, path, op, _current, useCountIndex)); } void binary_emitter::write_variable(Command command, CommandFlag flag, const std::string& name) @@ -274,8 +292,9 @@ namespace ink::compiler::internal using std::get; size_t position = get<0>(pair); const std::string& path = get<1>(pair); - container_data* context = get<2>(pair); - bool useCountIndex = get<3>(pair); + bool optional = get<2>(pair); + container_data* context = get<3>(pair); + bool useCountIndex = get<4>(pair); // Start at the root container_data* container = _root; @@ -325,7 +344,10 @@ namespace ink::compiler::internal // Named child else { - container = container->named_children[token]; + auto itr = container->named_children.find(token); + container = itr == container->named_children.end() + ? nullptr + : itr->second; } firstParent = false; @@ -350,7 +372,12 @@ namespace ink::compiler::internal else { // Otherwise, write container address - _containers.set(position, container->offset); + if (container == nullptr) { + _containers.set(position, 0); + inkAssert(optional, ("Was not able to resolve a not optional path! '" + path + "'").c_str()); + } else { + _containers.set(position, container->offset); + } } } } diff --git a/inkcpp_compiler/binary_emitter.h b/inkcpp_compiler/binary_emitter.h index 1cf73c69..7033c909 100644 --- a/inkcpp_compiler/binary_emitter.h +++ b/inkcpp_compiler/binary_emitter.h @@ -18,6 +18,7 @@ namespace ink::compiler::internal // Begin emitter virtual uint32_t start_container(int index_in_parent, const std::string& name) override; virtual uint32_t end_container() override; + virtual int function_container_arguments(const std::string& name) override; virtual void write_raw(Command command, CommandFlag flag = CommandFlag::NO_FLAGS, const char* payload = nullptr, ink::size_t payload_size = 0) override; virtual void write_path(Command command, CommandFlag flag, const std::string& path, bool useCountIndex = false) override; virtual void write_variable(Command command, CommandFlag flag, const std::string& name) override; @@ -53,6 +54,11 @@ namespace ink::compiler::internal binary_stream _lists; binary_stream _containers; - std::vector> _paths; + // positon to write address + // path as string + // if path may not exists (used for function fallbackes) + // container data + // use count index? + std::vector> _paths; }; } diff --git a/inkcpp_compiler/binary_stream.cpp b/inkcpp_compiler/binary_stream.cpp index 6f44f59e..6c57064d 100644 --- a/inkcpp_compiler/binary_stream.cpp +++ b/inkcpp_compiler/binary_stream.cpp @@ -124,6 +124,23 @@ namespace ink memcpy(ptr, data, len); } + byte_t binary_stream::get(size_t offset) const + { + // Find slab for offset + unsigned int slab_index = offset / DATA_SIZE; + size_t pos = offset % DATA_SIZE; + + // Get slab and ptr + byte_t* slab = nullptr; + if (slab_index < _slabs.size()) + slab = _slabs[slab_index]; + else if (slab_index == _slabs.size()) + slab = _currentSlab; + + inkAssert(slab != nullptr, "try to access invalid slab in binary stream"); + return slab[pos]; + } + void binary_stream::reset() { // Delete all slabs diff --git a/inkcpp_compiler/binary_stream.h b/inkcpp_compiler/binary_stream.h index 6d4efe25..d5228727 100644 --- a/inkcpp_compiler/binary_stream.h +++ b/inkcpp_compiler/binary_stream.h @@ -48,6 +48,9 @@ namespace ink // reset to 0 void reset(); + // read a byte from stream + byte_t get(size_t offset) const; + private: // Size of a data slab. Whenever // a slab runs out of data, diff --git a/inkcpp_compiler/emitter.h b/inkcpp_compiler/emitter.h index 75a99bc9..cee87890 100644 --- a/inkcpp_compiler/emitter.h +++ b/inkcpp_compiler/emitter.h @@ -28,6 +28,12 @@ namespace ink::compiler::internal // ends a container virtual uint32_t end_container() = 0; + // checks if _root contains a container named name to check + // if name is in valid internal function name + // @return number of arguments functions takes (arity) + // @retval -1 if the function was not found + virtual int function_container_arguments(const std::string& name) = 0; + // Writes a command with an optional payload virtual void write_raw(Command command, CommandFlag flag = CommandFlag::NO_FLAGS, const char* payload = nullptr, ink::size_t payload_size = 0) = 0; diff --git a/inkcpp_compiler/json_compiler.cpp b/inkcpp_compiler/json_compiler.cpp index 87388121..77c4c080 100644 --- a/inkcpp_compiler/json_compiler.cpp +++ b/inkcpp_compiler/json_compiler.cpp @@ -364,7 +364,8 @@ namespace ink::compiler::internal // Encode argument count into command flag and write out the hash of the function name _emitter->write(Command::CALL_EXTERNAL, hash_string(val.c_str()), static_cast(numArgs)); - } + _emitter->write_path(Command::FUNCTION, CommandFlag::FALLBACK_FUNCTION, val); + } // list initialisation else if (has(command, "list")) diff --git a/inkcpp_test/CMakeLists.txt b/inkcpp_test/CMakeLists.txt index 4ee6fbb6..e94b72ff 100644 --- a/inkcpp_test/CMakeLists.txt +++ b/inkcpp_test/CMakeLists.txt @@ -1,15 +1,16 @@ add_executable(inkcpp_test catch.hpp Main.cpp - Array.cpp - Pointer.cpp - Stack.cpp - Callstack.cpp - Restorable.cpp - Value.cpp - Globals.cpp - Lists.cpp - Tags.cpp - NewLines.cpp - ) + Array.cpp + Pointer.cpp + Stack.cpp + Callstack.cpp + Restorable.cpp + Value.cpp + Globals.cpp + Lists.cpp + Tags.cpp + NewLines.cpp + FallbackFunction.cpp +) target_link_libraries(inkcpp_test PUBLIC inkcpp inkcpp_compiler inkcpp_shared) target_include_directories(inkcpp_test PRIVATE ../shared/private/) diff --git a/inkcpp_test/FallbackFunction.cpp b/inkcpp_test/FallbackFunction.cpp new file mode 100644 index 00000000..56a07af5 --- /dev/null +++ b/inkcpp_test/FallbackFunction.cpp @@ -0,0 +1,63 @@ +#include "catch.hpp" +#include "../inkcpp_cl/test.h" + +#include +#include +#include +#include +#include + +#include + +using namespace ink::runtime; + +SCENARIO("run a story with external function and fallback function", "[external function]") +{ + GIVEN("story with two external functions, one with fallback") + { + inklecate("ink/FallBack.ink", "FallBack.tmp"); + ink::compiler::run("FallBack.tmp", "FallBack.bin"); + auto ink = story::from_file("FallBack.bin"); + runner thread = ink->new_runner(); + + WHEN("bind both external functions") + { + int cnt_sqrt = 0; + auto fn_sqrt = [&cnt_sqrt](int x)->int{ ++cnt_sqrt; return sqrt(x); }; + int cnt_greeting = 0; + auto fn_greeting = [&cnt_greeting]()->const char*{++cnt_greeting; return "Hohooh"; }; + + thread->bind("sqrt", fn_sqrt); + thread->bind("greeting", fn_greeting); + + std::string out; + REQUIRE_NOTHROW(out = thread->getall()); + THEN("Both function should be called the correct amount of times") + { + REQUIRE(out == "Hohooh ! A small demonstraion of my power:\n4 * 4 = 16, stunning i would say\n"); + REQUIRE(cnt_sqrt == 2); + REQUIRE(cnt_greeting == 1); + } + } + WHEN("only bind function without fallback") + { + int cnt_sqrt = 0; + auto fn_sqrt = [&cnt_sqrt](int x)->int{++cnt_sqrt; return sqrt(x); }; + + thread ->bind("sqrt", fn_sqrt); + + std::string out; + REQUIRE_NOTHROW(out = thread->getall());; + THEN("Sqrt should be falled twice, and uses default greeting") + { + REQUIRE(out == "Hello ! A small demonstraion of my power:\n4 * 4 = 16, stunning i would say\n"); + REQUIRE(cnt_sqrt == 2); + } + } + WHEN("bind no function") + { + std::string out; + REQUIRE_THROWS_AS(out = thread->getall(), ink::ink_exception); + } + } +} diff --git a/inkcpp_test/ink/FallBack.ink b/inkcpp_test/ink/FallBack.ink new file mode 100644 index 00000000..5371ad47 --- /dev/null +++ b/inkcpp_test/ink/FallBack.ink @@ -0,0 +1,13 @@ +-> Start + +EXTERNAL sqrt(x) + +EXTERNAL greeting() + +=== function greeting() === +~ return "Hello" + +== Start == +{greeting()} ! A small demonstraion of my power: +{sqrt(16)} * {sqrt(16)} = 16, stunning i would say +-> DONE \ No newline at end of file diff --git a/shared/private/command.h b/shared/private/command.h index d84b6a72..15465377 100644 --- a/shared/private/command.h +++ b/shared/private/command.h @@ -140,6 +140,8 @@ namespace ink // == Function/Tunnel flags FUNCTION_TO_VARIABLE = 1 << 0, TUNNEL_TO_VARIABLE = 1 << 0, + FALLBACK_FUNCTION = 1 << 1, + // note a internal function which should only be called if external reference is not working }; inline bool operator& (CommandFlag lhs, CommandFlag rhs)