diff --git a/cheatsheet.md b/cheatsheet.md index d2fb216b..4f5f0ff5 100644 --- a/cheatsheet.md +++ b/cheatsheet.md @@ -829,6 +829,94 @@ class My_Class { }; ``` +## Strong Typedefs + +Strong typedefs create distinct types that are not interchangeable with their underlying type +or with other typedefs of the same underlying type. They use `Dynamic_Object` internally and +automatically expose operators that the underlying type supports. + +### Basic Usage + +``` +using Meters = int +using Seconds = int + +var d = Meters(100) +var t = Seconds(10) + +// d and t are distinct types — you cannot accidentally mix them +// Meters + Seconds would require an explicit conversion +``` + +### Arithmetic and Comparison + +Operators from the underlying type are forwarded and remain strongly typed: + +``` +using Meters = int + +var a = Meters(10) +var b = Meters(20) + +var c = a + b // Meters(30) — result is still Meters +var bigger = b > a // true — comparisons return bool + +// Compound assignment operators work too +a += b // a is now Meters(30) +``` + +### String-Based Strong Typedefs + +Strong typedefs work with any type, not just numeric types: + +``` +using Name = string + +var n = Name("Alice") +var greeting = Name("Hello, ") + Name("world") // Name — string concatenation is forwarded +``` + +### Accessing the Underlying Value + +Use `to_underlying` to extract the wrapped value: + +``` +using Meters = int + +var d = Meters(42) +var raw = to_underlying(d) // 42, plain int +``` + +### Extending Strong Typedefs + +You can add custom operations to strong typedefs just like any other ChaiScript type: + +``` +using Meters = int +using Seconds = int +using MetersPerSecond = int + +def speed(Meters d, Seconds t) { + MetersPerSecond(to_underlying(d) / to_underlying(t)) +} + +var s = speed(Meters(100), Seconds(10)) // MetersPerSecond(10) +``` + +You can also overload operators between different strong typedefs: + +``` +using Meters = int +using Feet = int + +def to_feet(Meters m) { + Feet((to_underlying(m) * 328) / 100) +} + +var m = Meters(10) +var f = to_feet(m) // Feet(32) +``` + ## method_missing A function of the signature `method_missing(object, name, param1, param2, param3)` will be called if an appropriate diff --git a/grammar/chaiscript.ebnf b/grammar/chaiscript.ebnf index d95fe92b..6e13d547 100644 --- a/grammar/chaiscript.ebnf +++ b/grammar/chaiscript.ebnf @@ -68,6 +68,7 @@ enum_entry ::= id ( "=" integer )? underlying_type ::= id + /* ---- Blocks & flow keywords ---- */ block ::= "{" statements* "}" diff --git a/include/chaiscript/language/chaiscript_common.hpp b/include/chaiscript/language/chaiscript_common.hpp index 36383731..0e770e9e 100644 --- a/include/chaiscript/language/chaiscript_common.hpp +++ b/include/chaiscript/language/chaiscript_common.hpp @@ -33,7 +33,7 @@ namespace chaiscript { template static bool is_reserved_word(const T &s) noexcept { const static std::unordered_set - words{utility::hash("def"), utility::hash("fun"), utility::hash("while"), utility::hash("for"), utility::hash("if"), utility::hash("else"), utility::hash("&&"), utility::hash("||"), utility::hash(","), utility::hash("auto"), utility::hash("return"), utility::hash("break"), utility::hash("true"), utility::hash("false"), utility::hash("class"), utility::hash("attr"), utility::hash("var"), utility::hash("global"), utility::hash("GLOBAL"), utility::hash("_"), utility::hash("__LINE__"), utility::hash("__FILE__"), utility::hash("__FUNC__"), utility::hash("__CLASS__"), utility::hash("const"), utility::hash("enum")}; + words{utility::hash("def"), utility::hash("fun"), utility::hash("while"), utility::hash("for"), utility::hash("if"), utility::hash("else"), utility::hash("&&"), utility::hash("||"), utility::hash(","), utility::hash("auto"), utility::hash("return"), utility::hash("break"), utility::hash("true"), utility::hash("false"), utility::hash("class"), utility::hash("attr"), utility::hash("var"), utility::hash("global"), utility::hash("GLOBAL"), utility::hash("_"), utility::hash("__LINE__"), utility::hash("__FILE__"), utility::hash("__FUNC__"), utility::hash("__CLASS__"), utility::hash("const"), utility::hash("using"), utility::hash("enum")}; return words.count(utility::hash(s)) == 1; } @@ -107,6 +107,7 @@ namespace chaiscript { Compiled, Const_Var_Decl, Const_Assign_Decl, + Using, Enum, Namespace_Block }; @@ -129,7 +130,7 @@ namespace chaiscript { namespace { /// Helper lookup to get the name of each node type constexpr const char *ast_node_type_to_string(AST_Node_Type ast_node_type) noexcept { - constexpr const char *const ast_node_types[] = {"Id", "Fun_Call", "Unused_Return_Fun_Call", "Arg_List", "Equation", "Var_Decl", "Assign_Decl", "Array_Call", "Dot_Access", "Lambda", "Block", "Scopeless_Block", "Def", "While", "If", "For", "Ranged_For", "Inline_Array", "Inline_Map", "Return", "File", "Prefix", "Break", "Continue", "Map_Pair", "Value_Range", "Inline_Range", "Try", "Catch", "Finally", "Method", "Attr_Decl", "Logical_And", "Logical_Or", "Reference", "Switch", "Case", "Default", "Noop", "Class", "Binary", "Arg", "Global_Decl", "Constant", "Compiled", "Const_Var_Decl", "Const_Assign_Decl", "Enum", "Namespace_Block"}; + constexpr const char *const ast_node_types[] = {"Id", "Fun_Call", "Unused_Return_Fun_Call", "Arg_List", "Equation", "Var_Decl", "Assign_Decl", "Array_Call", "Dot_Access", "Lambda", "Block", "Scopeless_Block", "Def", "While", "If", "For", "Ranged_For", "Inline_Array", "Inline_Map", "Return", "File", "Prefix", "Break", "Continue", "Map_Pair", "Value_Range", "Inline_Range", "Try", "Catch", "Finally", "Method", "Attr_Decl", "Logical_And", "Logical_Or", "Reference", "Switch", "Case", "Default", "Noop", "Class", "Binary", "Arg", "Global_Decl", "Constant", "Compiled", "Const_Var_Decl", "Const_Assign_Decl", "Using", "Enum", "Namespace_Block"}; return ast_node_types[static_cast(ast_node_type)]; } diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index 970cdd3a..759accd9 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -109,6 +109,169 @@ 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}; + }; + + class Strong_Typedef_Compound_Assign_Op final : public dispatch::Proxy_Function_Base { + public: + Strong_Typedef_Compound_Assign_Op( + std::string t_type_name, + std::string t_op_name, + Operators::Opers t_base_oper, + std::string t_base_op_name, + chaiscript::detail::Dispatch_Engine &t_engine) + : Proxy_Function_Base( + {user_type(), + user_type(), + user_type()}, + 2) + , m_type_name(std::move(t_type_name)) + , m_op_name(std::move(t_op_name)) + , m_base_oper(t_base_oper) + , m_base_op_name(std::move(t_base_op_name)) + , 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(); + } + + 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_base_oper != Operators::Opers::invalid + && lhs_val.get_type_info().is_arithmetic() + && rhs_val.get_type_info().is_arithmetic()) { + result = Boxed_Number::do_oper(m_base_oper, lhs_val, rhs_val); + } else { + std::array underlying_params{lhs_val, rhs_val}; + result = m_engine.call_function(m_base_op_name, m_loc, Function_Params(underlying_params), t_conversions); + } + + lhs.get_attr("__value") = result; + return params[0]; + } + + 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_base_oper; + std::string m_base_op_name; + chaiscript::detail::Dispatch_Engine &m_engine; + mutable std::atomic_uint_fast32_t m_loc{0}; + }; + } // namespace detail template @@ -891,6 +1054,119 @@ namespace chaiscript { } }; + template + struct Using_AST_Node final : AST_Node_Impl { + Using_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector> t_children) + : AST_Node_Impl(std::move(t_ast_node_text), AST_Node_Type::Using, std::move(t_loc), std::move(t_children)) { + assert(this->children.size() == 2); + } + + Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override { + const auto &new_type_name = this->children[0]->text; + const auto &base_type_name = this->children[1]->text; + + const auto base_type = t_ss->get_type(base_type_name, true); + + t_ss->add(user_type(), new_type_name); + + dispatch::Param_Types param_types(std::vector>{ + {new_type_name, Type_Info()}, + {base_type_name, base_type}}); + + auto ctor_body = dispatch::make_dynamic_proxy_function( + [](const Function_Params &t_params) -> Boxed_Value { + auto *obj = static_cast(t_params[0].get_ptr()); + obj->get_attr("__value") = t_params[1]; + return void_var(); + }, + 2, + std::shared_ptr(), + param_types); + + try { + t_ss->add(std::make_shared(new_type_name, ctor_body), new_type_name); + } catch (const exception::name_conflict_error &e) { + throw exception::eval_error("Type alias redefined '" + e.name() + "'"); + } + + dispatch::Param_Types to_underlying_param_types(std::vector>{ + {new_type_name, user_type()}}); + + auto to_underlying_body = dispatch::make_dynamic_proxy_function( + [](const Function_Params &t_params) -> Boxed_Value { + const auto *obj = static_cast(t_params[0].get_const_ptr()); + return obj->get_attr("__value"); + }, + 1, + std::shared_ptr(), + to_underlying_param_types); + + 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::shift_left, true}, + {">>", Operators::Opers::shift_right, true}, + {"&", Operators::Opers::bitwise_and, true}, + {"|", Operators::Opers::bitwise_or, true}, + {"^", Operators::Opers::bitwise_xor, 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); + } + + struct Compound_Op_Entry { + const char *name; + Operators::Opers base_oper; + const char *base_op_name; + }; + + static constexpr Compound_Op_Entry compound_ops[] = { + {"+=", Operators::Opers::sum, "+"}, + {"-=", Operators::Opers::difference, "-"}, + {"*=", Operators::Opers::product, "*"}, + {"/=", Operators::Opers::quotient, "/"}, + {"%=", Operators::Opers::remainder, "%"}, + {"<<=", Operators::Opers::shift_left, "<<"}, + {">>=", Operators::Opers::shift_right, ">>"}, + {"&=", Operators::Opers::bitwise_and, "&"}, + {"|=", Operators::Opers::bitwise_or, "|"}, + {"^=", Operators::Opers::bitwise_xor, "^"}, + }; + + for (const auto &op : compound_ops) { + t_ss->add( + chaiscript::make_shared( + new_type_name, std::string(op.name), op.base_oper, std::string(op.base_op_name), engine), + op.name); + } + + return void_var(); + } + }; + template struct Enum_AST_Node final : AST_Node_Impl { Enum_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector> t_children) diff --git a/include/chaiscript/language/chaiscript_parser.hpp b/include/chaiscript/language/chaiscript_parser.hpp index 0af66d4e..3ad3a7a2 100644 --- a/include/chaiscript/language/chaiscript_parser.hpp +++ b/include/chaiscript/language/chaiscript_parser.hpp @@ -2069,6 +2069,43 @@ namespace chaiscript { return retval; } + bool Using(const bool t_class_allowed) { + Depth_Counter dc{this}; + + const auto prev_stack_top = m_match_stack.size(); + + if (Keyword("using")) { + if (!t_class_allowed) { + throw exception::eval_error("Type alias definitions only allowed at top scope", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + if (!Id(true)) { + throw exception::eval_error("Missing type name in 'using' declaration", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + if (!Symbol("=", true)) { + throw exception::eval_error("Missing '=' in 'using' declaration", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + if (!Id(true)) { + throw exception::eval_error("Missing base type name in 'using' declaration", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + build_match>(prev_stack_top); + return true; + } + + return false; + } + bool Enum(const bool t_allowed) { Depth_Counter dc{this}; bool retval = false; @@ -2905,7 +2942,7 @@ namespace chaiscript { while (has_more) { const auto start = m_position; - if (Def() || Try() || If() || While() || Namespace_Block() || Class(t_class_allowed) || Enum(t_class_allowed) || For() || Switch()) { + if (Def() || Try() || If() || While() || Namespace_Block() || Class(t_class_allowed) || Using(t_class_allowed) || Enum(t_class_allowed) || For() || Switch()) { if (!saw_eol) { throw exception::eval_error("Two function definitions missing line separator", File_Position(start.line, start.col), diff --git a/unittests/strong_typedef.chai b/unittests/strong_typedef.chai new file mode 100644 index 00000000..73aaa4a9 --- /dev/null +++ b/unittests/strong_typedef.chai @@ -0,0 +1,200 @@ +// Strong typedef: using Type = int creates a distinct type +using Meters = int + +def measure(Meters m) { + return m +} + +// Constructing a strong typedef value should work +var m = Meters(42) + +// Calling with the typedef'd value should succeed +measure(m) + +// Calling with a plain int should fail (strong typedef) +try { + measure(42) + assert_equal(true, false) +} catch(e) { + // Expected: type mismatch because int is not Meters +} + +// Multiple strong typedefs from the same base type should be distinct +using Seconds = int + +def wait(Seconds s) { + return s +} + +var s = Seconds(10) +wait(s) + +// Meters and Seconds should not be interchangeable +try { + wait(m) + assert_equal(true, false) +} catch(e) { + // Expected: Meters is not Seconds +} + +try { + measure(s) + assert_equal(true, false) +} catch(e) { + // Expected: Seconds is not Meters +} + +// to_underlying should return the base value +assert_equal(to_underlying(m), 42) +assert_equal(to_underlying(s), 10) + +// to_underlying result should be a plain value, not a strong typedef +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) + +// --- Bitwise and shift operators --- +assert_equal(to_underlying(Meters(6) & Meters(3)), 2) +assert_equal(to_underlying(Meters(6) | Meters(3)), 7) +assert_equal(to_underlying(Meters(6) ^ Meters(3)), 5) +assert_equal(to_underlying(Meters(5) << Meters(2)), 20) +assert_equal(to_underlying(Meters(12) >> Meters(1)), 6) + +// Bitwise results are strongly typed +try { + takes_int(Meters(6) & Meters(3)) + assert_equal(true, false) +} catch(e) { + // Expected: result is Meters, not int +} + +// --- 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) + +// Operators not supported by the underlying type error at call time +try { + var bad = ss1 * ss2 + assert_equal(true, false) +} catch(e) { + // Expected: underlying string has no * operator +} +try { + var bad = ss1 - ss2 + assert_equal(true, false) +} catch(e) { + // Expected: underlying string has no - operator +} +try { + var bad = ss1 / ss2 + assert_equal(true, false) +} catch(e) { + // Expected: underlying string has no / operator +} +try { + var bad = ss1 % ss2 + assert_equal(true, false) +} catch(e) { + // Expected: underlying string has no % operator +} + +// 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") + +// --- Compound assignment operators --- +var m3 = Meters(10) +m3 += Meters(5) +assert_equal(to_underlying(m3), 15) +measure(m3) + +m3 -= Meters(3) +assert_equal(to_underlying(m3), 12) + +m3 *= Meters(2) +assert_equal(to_underlying(m3), 24) + +m3 /= Meters(4) +assert_equal(to_underlying(m3), 6) + +m3 %= Meters(4) +assert_equal(to_underlying(m3), 2) + +// Compound assignment result is still the strong typedef +var m4 = Meters(10) +m4 += Meters(5) +assert_equal(to_underlying(m4), 15) +measure(m4) + +// Compound assignment on StrongString +var ss3 = StrongString("hello") +ss3 += StrongString(" world") +assert_equal(to_underlying(ss3), "hello world") +takes_strong_string(ss3)