Fix #677: Add strong typedefs (#680)

* Fix #677: Add strong typedefs via 'using Type = BaseType' syntax

Strong typedefs create distinct types backed by Dynamic_Object, so
'using Meters = int' makes Meters a type that is not interchangeable
with int or other typedefs of int. The constructor Meters(val) wraps
the base value, and function dispatch enforces the type distinction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review: add to_underlying function for strong typedefs

Registers a to_underlying() function for each strong typedef that
returns the wrapped base value from the Dynamic_Object's __value attr.

Requested by @lefticus in PR #680 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review: expose strongly-typed operators for strong typedefs

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) <noreply@anthropic.com>

* Address review: conditionally register operators based on underlying type support

Only register strong typedef operators that actually exist for the
underlying type. Previously all operators were added unconditionally,
causing confusing reflection entries (e.g. * for StrongString) that
would fail at runtime. Now each operator is probed via call_match
against default-constructed base type values before registration.

Also adds bitwise/shift operators (&, |, ^, <<, >>) for types that
support them, and expands test coverage for unsupported operator
rejection.

Requested by @lefticus in PR #680 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review: register all operators unconditionally and add compound assignment operators

Remove conditional operator registration (op_exists_for_base_type check)
since users could add underlying operators later, and the runtime check was
expensive. Operators that fail on the underlying type now error at call time
instead of being absent. Add compound assignment operators (*=, +=, -=, /=,
%=, <<=, >>=, &=, |=, ^=) via Strong_Typedef_Compound_Assign_Op which
computes the base operation and stores the result back in __value.

Requested by @lefticus in PR #680 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Merge upstream/develop into fix/issue-677-add-strong-typedefs

Resolve merge conflicts with ChaiScript:develop. Upstream added
nested namespace support (#675), grammar railroad diagrams (#673),
and WASM exception support (#689). Conflicts in chaiscript_common.hpp,
chaiscript_eval.hpp, and chaiscript_parser.hpp resolved by keeping
both Using and Namespace_Block AST node types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review: add strong typedef documentation to cheatsheet

Add a new "Strong Typedefs" section to the cheatsheet covering:
- Basic usage with `using Type = BaseType` syntax
- Arithmetic and comparison operator forwarding
- String-based strong typedefs
- Accessing the underlying value via to_underlying
- Extending strong typedefs with custom operations

Requested by @lefticus in PR #680 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: leftibot <leftibot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-15 14:48:49 -06:00 committed by GitHub
parent 1df1b4ad92
commit bb06919061
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 606 additions and 3 deletions

View File

@ -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

View File

@ -68,6 +68,7 @@ enum_entry ::= id ( "=" integer )?
underlying_type ::= id
/* ---- Blocks & flow keywords ---- */
block ::= "{" statements* "}"

View File

@ -33,7 +33,7 @@ namespace chaiscript {
template<typename T>
static bool is_reserved_word(const T &s) noexcept {
const static std::unordered_set<std::uint32_t>
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<int>(ast_node_type)];
}

View File

@ -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<Boxed_Value>::get(),
user_type<dispatch::Dynamic_Object>(),
user_type<dispatch::Dynamic_Object>()},
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<const Strong_Typedef_Binary_Op *>(&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 &params, const Type_Conversions_State &t_conversions) const override {
if (!call_match(params, t_conversions)) {
throw chaiscript::exception::guard_error();
}
const auto &lhs = boxed_cast<const dispatch::Dynamic_Object &>(params[0], &t_conversions);
const auto &rhs = boxed_cast<const dispatch::Dynamic_Object &>(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<Boxed_Value, 2> 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<dispatch::Dynamic_Object *>(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<dispatch::Dynamic_Object>())) {
return false;
}
try {
const auto &d = boxed_cast<const dispatch::Dynamic_Object &>(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<dispatch::Dynamic_Object>(),
user_type<dispatch::Dynamic_Object>(),
user_type<dispatch::Dynamic_Object>()},
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<const Strong_Typedef_Compound_Assign_Op *>(&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 &params, const Type_Conversions_State &t_conversions) const override {
if (!call_match(params, t_conversions)) {
throw chaiscript::exception::guard_error();
}
auto &lhs = boxed_cast<dispatch::Dynamic_Object &>(params[0], &t_conversions);
const auto &rhs = boxed_cast<const dispatch::Dynamic_Object &>(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<Boxed_Value, 2> 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<dispatch::Dynamic_Object>())) {
return false;
}
try {
const auto &d = boxed_cast<const dispatch::Dynamic_Object &>(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<typename T>
@ -891,6 +1054,119 @@ namespace chaiscript {
}
};
template<typename T>
struct Using_AST_Node final : AST_Node_Impl<T> {
Using_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector<AST_Node_Impl_Ptr<T>> t_children)
: AST_Node_Impl<T>(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<dispatch::Dynamic_Object>(), new_type_name);
dispatch::Param_Types param_types(std::vector<std::pair<std::string, Type_Info>>{
{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<dispatch::Dynamic_Object *>(t_params[0].get_ptr());
obj->get_attr("__value") = t_params[1];
return void_var();
},
2,
std::shared_ptr<AST_Node>(),
param_types);
try {
t_ss->add(std::make_shared<dispatch::detail::Dynamic_Object_Constructor>(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<std::pair<std::string, Type_Info>>{
{new_type_name, user_type<dispatch::Dynamic_Object>()}});
auto to_underlying_body = dispatch::make_dynamic_proxy_function(
[](const Function_Params &t_params) -> Boxed_Value {
const auto *obj = static_cast<const dispatch::Dynamic_Object *>(t_params[0].get_const_ptr());
return obj->get_attr("__value");
},
1,
std::shared_ptr<AST_Node>(),
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<dispatch::Proxy_Function_Base, detail::Strong_Typedef_Binary_Op>(
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<dispatch::Proxy_Function_Base, detail::Strong_Typedef_Compound_Assign_Op>(
new_type_name, std::string(op.name), op.base_oper, std::string(op.base_op_name), engine),
op.name);
}
return void_var();
}
};
template<typename T>
struct Enum_AST_Node final : AST_Node_Impl<T> {
Enum_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector<AST_Node_Impl_Ptr<T>> t_children)

View File

@ -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<eval::Using_AST_Node<Tracer>>(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),

View File

@ -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)