From 5f3a179b7382bffb58eee851cda390095e36fcd5 Mon Sep 17 00:00:00 2001 From: willvale <66674079+willvale@users.noreply.github.com> Date: Fri, 21 Nov 2025 22:19:14 +1300 Subject: [PATCH 1/5] Fix restrictive numeric casts/assignments. Allow type conversion on redefine if the casting matrix says the types have a common base. Allow bool->float conversion. --- inkcpp/numeric_operations.h | 1 + inkcpp/operations.h | 16 +++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/inkcpp/numeric_operations.h b/inkcpp/numeric_operations.h index 489919b4..d2593139 100644 --- a/inkcpp/numeric_operations.h +++ b/inkcpp/numeric_operations.h @@ -116,6 +116,7 @@ namespace casting // int value can cast to float case value_type::int32: return static_cast(v.get()); case value_type::uint32: return static_cast(v.get()); + case value_type::boolean: return v.get() ? 1.0f : 0.0f; default: inkFail("invalid numeric_cast!"); return 0; } } diff --git a/inkcpp/operations.h b/inkcpp/operations.h index 48080beb..d1422ef2 100644 --- a/inkcpp/operations.h +++ b/inkcpp/operations.h @@ -86,15 +86,13 @@ template ink::runtime::internal::value ink::runtime::internal::value::redefine(const value& oth, T&... env) const { - if (type() != oth.type() && (type() == value_type::list_flag || type() == value_type::list) - && (oth.type() == value_type::list_flag || oth.type() == value_type::list)) { - /// @todo could break origin - if (oth.type() == value_type::list) { - return value{}.set(oth.get()); - } else { - return value{}.set(oth.get()); - } + if (type() != oth.type()) { + + const value vs[] = { *this, oth }; + inkAssert(casting::common_base<2>(vs) != value_type::none, "try to redefine value of other type with no cast available"); + + // There's a valid conversion, so redefine as input value. + return oth; } - inkAssert(type() == oth.type(), "try to redefine value of other type"); return redefine(oth, {&env...}); } From adefe3ee0b7b9f3be23790d76b7f80775133bddf Mon Sep 17 00:00:00 2001 From: willvale <66674079+willvale@users.noreply.github.com> Date: Fri, 21 Nov 2025 22:30:55 +1300 Subject: [PATCH 2/5] Clangformat fixes. --- inkcpp/operations.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/inkcpp/operations.h b/inkcpp/operations.h index d1422ef2..7546a015 100644 --- a/inkcpp/operations.h +++ b/inkcpp/operations.h @@ -88,8 +88,11 @@ ink::runtime::internal::value { if (type() != oth.type()) { - const value vs[] = { *this, oth }; - inkAssert(casting::common_base<2>(vs) != value_type::none, "try to redefine value of other type with no cast available"); + const value vs[] = {*this, oth}; + inkAssert( + casting::common_base<2>(vs) != value_type::none, + "try to redefine value of other type with no cast available" + ); // There's a valid conversion, so redefine as input value. return oth; From 710617926c2552983ef4ac4634949d497946a37c Mon Sep 17 00:00:00 2001 From: willvale <66674079+willvale@users.noreply.github.com> Date: Sun, 14 Dec 2025 14:22:37 +1300 Subject: [PATCH 3/5] Fix a variety of cast problems Allow conversions from bool to string. Added test case for allowed and forbidden casts. Added missing include to UTF8 test. --- inkcpp/string_operations.h | 3 ++ inkcpp/string_utils.h | 6 +++ inkcpp_test/Fixes.cpp | 31 ++++++++++++++ inkcpp_test/UTF8.cpp | 1 + inkcpp_test/ink/134_restrictive_casts.ink | 51 +++++++++++++++++++++++ 5 files changed, 92 insertions(+) create mode 100644 inkcpp_test/ink/134_restrictive_casts.ink diff --git a/inkcpp/string_operations.h b/inkcpp/string_operations.h index a0f8ff49..007cb846 100644 --- a/inkcpp/string_operations.h +++ b/inkcpp/string_operations.h @@ -23,6 +23,9 @@ namespace ink::runtime::internal { struct cast { static constexpr value_type value = value_type::string; }; template<> + struct cast + { static constexpr value_type value = value_type::string; }; + template<> struct cast { static constexpr value_type value = value_type::string; }; } diff --git a/inkcpp/string_utils.h b/inkcpp/string_utils.h index 00df3111..5924af95 100644 --- a/inkcpp/string_utils.h +++ b/inkcpp/string_utils.h @@ -100,12 +100,18 @@ inline int toStr(char* buffer, size_t size, const char* c) return 0; } +inline int toStr(char* buffer, size_t size, bool b) +{ + return toStr(buffer, size, b ? "true" : "false"); +} + inline int toStr(char* buffer, size_t size, const value& v) { switch (v.type()) { case value_type::int32: return toStr(buffer, size, v.get()); case value_type::uint32: return toStr(buffer, size, v.get()); case value_type::float32: return toStr(buffer, size, v.get()); + case value_type::boolean: return toStr(buffer, size, v.get()); case value_type::newline: return toStr(buffer, size, "\n"); default: inkFail("only support toStr for numeric types"); return -1; } diff --git a/inkcpp_test/Fixes.cpp b/inkcpp_test/Fixes.cpp index 33134408..edf60976 100644 --- a/inkcpp_test/Fixes.cpp +++ b/inkcpp_test/Fixes.cpp @@ -126,3 +126,34 @@ SCENARIO("missing leading whitespace inside choice-only text and glued text _ #1 } } } + +SCENARIO("Casting during redefinition is too strict _ #134", "[fixes]") +{ + GIVEN("story with problematic text") + { + auto ink = story::from_file(INK_TEST_RESOURCE_DIR "134_restrictive_casts.bin"); + runner thread = ink->new_runner(); + + WHEN("run story") + { + // Initial casts/assignments are allowed. + auto line = thread->getline(); + THEN("expect initial values") { REQUIRE(line == "true 1 1 text A\n"); } + line = thread->getline(); + THEN("expect evaluated") { REQUIRE(line == "1.5 1.5 1.5 text0.5 B\n"); } + line = thread->getline(); + THEN("expect assigned") { REQUIRE(line == "1.5 1.5 1.5 text0.5 B\n"); } + } + + // Six cases that should fail. We can't pollute lookahead with these so they need to be separated out. + for (int i = 0; i < 6; ++i) { + WHEN("Jump to failing case") + { + const std::string name = "Fail" + std::to_string(i); + REQUIRE_NOTHROW(thread->move_to(ink::hash_string(name.c_str()))); + std::string line; + REQUIRE_THROWS_AS((line = thread->getline(), std::cerr< +#include #include #include diff --git a/inkcpp_test/ink/134_restrictive_casts.ink b/inkcpp_test/ink/134_restrictive_casts.ink new file mode 100644 index 00000000..563b5986 --- /dev/null +++ b/inkcpp_test/ink/134_restrictive_casts.ink @@ -0,0 +1,51 @@ +VAR b = true +VAR i = 1 +VAR f = 1.0 +VAR t = "text" +LIST l = (A), B +VAR d = ->Knot + +// Input with initial values +{b} {i} {f} {t} {l} {d} + +// Cast during evaluation +{b+0.5} {i+0.5} {f+0.5} {t+0.5} {l+1} + +// Cast by variable redefinition +~b = b + 0.5 +~i = i + 0.5 +~f = f + 0.5 +~t = t + 0.5 +~l = l + 1 +{b} {i} {f} {t} {l} +->DONE + +===Knot +->DONE + +===Fail0 +{l + true} +->DONE + +===Fail1 +{l + 0.5} +->DONE + +===Fail2 +{d + 0.5} +->DONE + +===Fail3 +~l = l + true +{l} +->DONE + +===Fail4 +~l = l + 0.5 +{l} +->DONE + +===Fail5 +~d = d + 0.5 +{d} +->DONE From be2b6f7f4d8e1e77e065c75ba76cf9b4a1b6d64e Mon Sep 17 00:00:00 2001 From: willvale <66674079+willvale@users.noreply.github.com> Date: Sun, 14 Dec 2025 14:34:32 +1300 Subject: [PATCH 4/5] Clangformat fixes Also removed debug trace left in by mistake. --- inkcpp_test/Fixes.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/inkcpp_test/Fixes.cpp b/inkcpp_test/Fixes.cpp index edf60976..57101d87 100644 --- a/inkcpp_test/Fixes.cpp +++ b/inkcpp_test/Fixes.cpp @@ -145,14 +145,15 @@ SCENARIO("Casting during redefinition is too strict _ #134", "[fixes]") THEN("expect assigned") { REQUIRE(line == "1.5 1.5 1.5 text0.5 B\n"); } } - // Six cases that should fail. We can't pollute lookahead with these so they need to be separated out. + // Six cases that should fail. We can't pollute lookahead with these so they need to be + // separated out. for (int i = 0; i < 6; ++i) { WHEN("Jump to failing case") { const std::string name = "Fail" + std::to_string(i); REQUIRE_NOTHROW(thread->move_to(ink::hash_string(name.c_str()))); std::string line; - REQUIRE_THROWS_AS((line = thread->getline(), std::cerr<getline(), ink::ink_exception); } } } From 2c02eec6de5d37d7b9b89257e489730a306ce7a9 Mon Sep 17 00:00:00 2001 From: Julian Benda Date: Mon, 15 Dec 2025 11:28:34 +0100 Subject: [PATCH 5/5] style: run clang format --- inkcpp/operations.h | 4 +- inkcpp/string_operations.h | 104 +++++++++++++++++++++---------------- inkcpp_test/Fixes.cpp | 2 +- 3 files changed, 63 insertions(+), 47 deletions(-) diff --git a/inkcpp/operations.h b/inkcpp/operations.h index 7546a015..5bab700c 100644 --- a/inkcpp/operations.h +++ b/inkcpp/operations.h @@ -90,8 +90,8 @@ ink::runtime::internal::value const value vs[] = {*this, oth}; inkAssert( - casting::common_base<2>(vs) != value_type::none, - "try to redefine value of other type with no cast available" + casting::common_base<2>(vs) != value_type::none, + "try to redefine value of other type with no cast available" ); // There's a valid conversion, so redefine as input value. diff --git a/inkcpp/string_operations.h b/inkcpp/string_operations.h index 007cb846..2617871a 100644 --- a/inkcpp/string_operations.h +++ b/inkcpp/string_operations.h @@ -8,63 +8,79 @@ /// defines operations allowed on strings. -namespace ink::runtime::internal { +namespace ink::runtime::internal +{ - namespace casting { - // define valid castings - // when operate on float and string, the result is a string - template<> - struct cast - { static constexpr value_type value = value_type::string; }; - template<> - struct cast - { static constexpr value_type value = value_type::string; }; - template<> - struct cast - { static constexpr value_type value = value_type::string; }; - template<> - struct cast - { static constexpr value_type value = value_type::string; }; - template<> - struct cast - { static constexpr value_type value = value_type::string; }; - } - - // operation declaration add +namespace casting +{ + // define valid castings + // when operate on float and string, the result is a string template<> - class operation : public operation_base { - public: - using operation_base::operation_base; - void operator()(basic_eval_stack& stack, value* vals); + struct cast { + static constexpr value_type value = value_type::string; }; - // operation declaration equality template<> - class operation : public operation_base { - public: - using operation_base::operation_base; - void operator()(basic_eval_stack& stack, value* vals); + struct cast { + static constexpr value_type value = value_type::string; }; template<> - class operation : public operation_base { - public: - using operation_base::operation_base; - void operator()(basic_eval_stack& stack, value* vals); + struct cast { + static constexpr value_type value = value_type::string; }; template<> - class operation : public operation_base { - public: - using operation_base::operation_base; - void operator()(basic_eval_stack& stack, value* vals); + struct cast { + static constexpr value_type value = value_type::string; }; template<> - class operation : public operation_base { - public: - using operation_base::operation_base; - void operator()(basic_eval_stack& stack, value* vals); + struct cast { + static constexpr value_type value = value_type::string; }; +} // namespace casting + +// operation declaration add +template<> +class operation : public operation_base +{ +public: + using operation_base::operation_base; + void operator()(basic_eval_stack& stack, value* vals); +}; + +// operation declaration equality +template<> +class operation : public operation_base +{ +public: + using operation_base::operation_base; + void operator()(basic_eval_stack& stack, value* vals); +}; + +template<> +class operation : public operation_base +{ +public: + using operation_base::operation_base; + void operator()(basic_eval_stack& stack, value* vals); +}; + +template<> +class operation : public operation_base +{ +public: + using operation_base::operation_base; + void operator()(basic_eval_stack& stack, value* vals); +}; + +template<> +class operation : public operation_base +{ +public: + using operation_base::operation_base; + void operator()(basic_eval_stack& stack, value* vals); +}; -} +} // namespace ink::runtime::internal diff --git a/inkcpp_test/Fixes.cpp b/inkcpp_test/Fixes.cpp index 57101d87..94d5170a 100644 --- a/inkcpp_test/Fixes.cpp +++ b/inkcpp_test/Fixes.cpp @@ -145,7 +145,7 @@ SCENARIO("Casting during redefinition is too strict _ #134", "[fixes]") THEN("expect assigned") { REQUIRE(line == "1.5 1.5 1.5 text0.5 B\n"); } } - // Six cases that should fail. We can't pollute lookahead with these so they need to be + // Six cases that should fail. We can't pollute lookahead with these so they need to be // separated out. for (int i = 0; i < 6; ++i) { WHEN("Jump to failing case")