From 64a823c630bfba091db96656dc7505c9f6f11c25 Mon Sep 17 00:00:00 2001 From: leftibot Date: Tue, 14 Apr 2026 11:04:46 -0600 Subject: [PATCH] Address review: expose strongly-typed operators for strong typedefs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register forwarding binary operators at typedef creation time via a custom Proxy_Function_Base subclass (Strong_Typedef_Binary_Op). Each operator unwraps __value from both operands, dispatches on the underlying types, and re-wraps arithmetic results in the typedef. Comparison operators return the raw bool. - Arithmetic: +, -, *, /, % → Meters + Meters -> Meters - Comparison: <, >, <=, >=, ==, != → Meters < Meters -> bool - Operators that don't exist on the base type error at call time (e.g. StrongString * StrongString -> error) - Users can extend typedefs with their own operations using to_underlying() for unwrapping Tests cover int-based arithmetic, string-based concatenation, string multiplication error, comparison ops, type safety of results, and user-defined operator extensions. Requested by @lefticus in PR #680 review. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chaiscript/language/chaiscript_eval.hpp | 113 ++++++++++++++++++ unittests/strong_typedef.chai | 82 +++++++++++++ 2 files changed, 195 insertions(+) diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index b7fe4dfd..20e30c07 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -108,6 +108,90 @@ namespace chaiscript { return incoming; } } + class Strong_Typedef_Binary_Op final : public dispatch::Proxy_Function_Base { + public: + Strong_Typedef_Binary_Op( + std::string t_type_name, + std::string t_op_name, + Operators::Opers t_oper, + bool t_rewrap, + chaiscript::detail::Dispatch_Engine &t_engine) + : Proxy_Function_Base( + {chaiscript::detail::Get_Type_Info::get(), + user_type(), + user_type()}, + 2) + , m_type_name(std::move(t_type_name)) + , m_op_name(std::move(t_op_name)) + , m_oper(t_oper) + , m_rewrap(t_rewrap) + , m_engine(t_engine) { + } + + bool operator==(const Proxy_Function_Base &f) const noexcept override { + if (const auto *other = dynamic_cast(&f)) { + return m_type_name == other->m_type_name && m_op_name == other->m_op_name; + } + return false; + } + + bool call_match(const Function_Params &vals, const Type_Conversions_State &t_conversions) const noexcept override { + return vals.size() == 2 + && type_matches(vals[0], t_conversions) + && type_matches(vals[1], t_conversions); + } + + protected: + Boxed_Value do_call(const Function_Params ¶ms, const Type_Conversions_State &t_conversions) const override { + if (!call_match(params, t_conversions)) { + throw chaiscript::exception::guard_error(); + } + + const auto &lhs = boxed_cast(params[0], &t_conversions); + const auto &rhs = boxed_cast(params[1], &t_conversions); + const auto lhs_val = lhs.get_attr("__value"); + const auto rhs_val = rhs.get_attr("__value"); + + Boxed_Value result; + if (m_oper != Operators::Opers::invalid + && lhs_val.get_type_info().is_arithmetic() + && rhs_val.get_type_info().is_arithmetic()) { + result = Boxed_Number::do_oper(m_oper, lhs_val, rhs_val); + } else { + std::array underlying_params{lhs_val, rhs_val}; + result = m_engine.call_function(m_op_name, m_loc, Function_Params(underlying_params), t_conversions); + } + + if (m_rewrap) { + auto bv = Boxed_Value(dispatch::Dynamic_Object(m_type_name), true); + auto *obj = static_cast(bv.get_ptr()); + obj->get_attr("__value") = result; + return bv; + } + return result; + } + + private: + bool type_matches(const Boxed_Value &bv, const Type_Conversions_State &t_conversions) const noexcept { + if (!bv.get_type_info().bare_equal(user_type())) { + return false; + } + try { + const auto &d = boxed_cast(bv, &t_conversions); + return d.get_type_name() == m_type_name; + } catch (...) { + return false; + } + } + + std::string m_type_name; + std::string m_op_name; + Operators::Opers m_oper; + bool m_rewrap; + chaiscript::detail::Dispatch_Engine &m_engine; + mutable std::atomic_uint_fast32_t m_loc{0}; + }; + } // namespace detail template @@ -936,6 +1020,35 @@ namespace chaiscript { t_ss->add(to_underlying_body, "to_underlying"); + auto &engine = *t_ss; + + struct Op_Entry { + const char *name; + Operators::Opers oper; + bool rewrap; + }; + + static constexpr Op_Entry ops[] = { + {"+", Operators::Opers::sum, true}, + {"-", Operators::Opers::difference, true}, + {"*", Operators::Opers::product, true}, + {"/", Operators::Opers::quotient, true}, + {"%", Operators::Opers::remainder, true}, + {"<", Operators::Opers::less_than, false}, + {">", Operators::Opers::greater_than, false}, + {"<=", Operators::Opers::less_than_equal, false}, + {">=", Operators::Opers::greater_than_equal, false}, + {"==", Operators::Opers::equals, false}, + {"!=", Operators::Opers::not_equal, false}, + }; + + for (const auto &op : ops) { + t_ss->add( + chaiscript::make_shared( + new_type_name, std::string(op.name), op.oper, op.rewrap, engine), + op.name); + } + return void_var(); } }; diff --git a/unittests/strong_typedef.chai b/unittests/strong_typedef.chai index aa75b711..c29a0f5d 100644 --- a/unittests/strong_typedef.chai +++ b/unittests/strong_typedef.chai @@ -53,3 +53,85 @@ def takes_int(int i) { return i } assert_equal(takes_int(to_underlying(m)), 42) + +// --- Arithmetic operators: strongly typed --- +var m2 = Meters(8) +var m_sum = m + m2 +assert_equal(to_underlying(m_sum), 50) +measure(m_sum) + +var m_diff = m - m2 +assert_equal(to_underlying(m_diff), 34) + +var m_prod = Meters(3) * Meters(4) +assert_equal(to_underlying(m_prod), 12) + +var m_quot = Meters(20) / Meters(5) +assert_equal(to_underlying(m_quot), 4) + +var m_rem = Meters(17) % Meters(5) +assert_equal(to_underlying(m_rem), 2) + +// Arithmetic result is strongly typed, not plain int +try { + takes_int(m_sum) + assert_equal(true, false) +} catch(e) { + // Expected: m_sum is Meters, not int +} + +// --- Comparison operators --- +assert_equal(Meters(5) == Meters(5), true) +assert_equal(Meters(5) != Meters(3), true) +assert_equal(Meters(3) < Meters(5), true) +assert_equal(Meters(5) > Meters(3), true) +assert_equal(Meters(5) <= Meters(5), true) +assert_equal(Meters(3) >= Meters(3), true) +assert_equal(Meters(3) >= Meters(5), false) + +// --- Strong typedef over string --- +using StrongString = string + +var ss1 = StrongString("hello") +var ss2 = StrongString(" world") +var ss_cat = ss1 + ss2 +assert_equal(to_underlying(ss_cat), "hello world") + +// StrongString + StrongString -> StrongString (strongly typed) +def takes_strong_string(StrongString ss) { + return ss +} +takes_strong_string(ss_cat) + +// StrongString * StrongString -> error (no * for strings) +try { + var bad = ss1 * ss2 + assert_equal(true, false) +} catch(e) { + // Expected: no * operator for strings +} + +// Comparison on StrongString +assert_equal(StrongString("abc") < StrongString("def"), true) +assert_equal(StrongString("abc") == StrongString("abc"), true) +assert_equal(StrongString("abc") != StrongString("def"), true) +assert_equal(StrongString("def") > StrongString("abc"), true) +assert_equal(StrongString("abc") <= StrongString("abc"), true) +assert_equal(StrongString("def") >= StrongString("abc"), true) + +// --- User-defined extensions on strong typedefs --- +def first_char(StrongString ss) { + return to_string(to_underlying(ss)[0]) +} +assert_equal(first_char(StrongString("hello")), "h") + +def double_meters(Meters m) { + return Meters(to_underlying(m) * 2) +} +assert_equal(to_underlying(double_meters(Meters(21))), 42) + +// User-defined operator extension +def `[]`(StrongString ss, int offset) { + return to_string(to_underlying(ss)[offset]) +} +assert_equal(StrongString("hello")[1], "e")