Skip to content

Fix #677: Add strong typedefs#680

Merged
lefticus merged 9 commits intoChaiScript:developfrom
leftibot:fix/issue-677-add-strong-typedefs
Apr 15, 2026
Merged

Fix #677: Add strong typedefs#680
lefticus merged 9 commits intoChaiScript:developfrom
leftibot:fix/issue-677-add-strong-typedefs

Conversation

@leftibot
Copy link
Copy Markdown
Contributor

Automated fix by @leftibot.

What changed

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) [email protected]

Files

 include/chaiscript/language/chaiscript_common.hpp |  7 ++--
 include/chaiscript/language/chaiscript_eval.hpp   | 39 ++++++++++++++++++++
 include/chaiscript/language/chaiscript_parser.hpp | 39 +++++++++++++++++++-
 unittests/strong_typedef.chai                     | 45 +++++++++++++++++++++++
 4 files changed, 126 insertions(+), 4 deletions(-)

Closes #677

Triggered by @lefticus.

…yntax

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) <[email protected]>
@lefticus
Copy link
Copy Markdown
Member

@leftibot add a to_underlying function to be able to get the underlying type/value

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 ChaiScript#680 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@leftibot
Copy link
Copy Markdown
Contributor Author

Addressed review feedback from @lefticus. New commits:

61cbf1cd Address review: add to_underlying function for strong typedefs

@lefticus
Copy link
Copy Markdown
Member

@leftibot strong typedef should demonstrate the same properties of the underlying type. == comparison, members, etc. Is this possible?

@leftibot
Copy link
Copy Markdown
Contributor Author

Yes, this is possible and should be done. Let me explain the current state and what's needed.

What already works (partially):

The == and != operators for Dynamic_Object are registered in bootstrap.hpp:347-357 as ChaiScript functions that compare attrs. Since strong typedefs store their value as __value, two Meters(42) instances will compare equal via the existing attr-by-attr comparison. So == and != should already work between two values of the same strong typedef type.

What doesn't work:

Arithmetic and relational operators (+, -, *, /, <, >, <=, >=) won't work. Binary operator dispatch in chaiscript_eval.hpp:234 has a fast path:

if (t_oper != Operators::Opers::invalid && t_lhs.get_type_info().is_arithmetic() && t_rhs.get_type_info().is_arithmetic()) {
    return Boxed_Number::do_oper(t_oper, t_lhs, t_rhs);

Strong typedefs are Dynamic_Object instances, so is_arithmetic() returns false and this fast path is skipped. The fallback dispatch search then finds no matching operator.

What needs to happen:

When creating a strong typedef, we should register forwarding operators that unwrap __value, delegate to the underlying type's operator, and (for arithmetic results) re-wrap in the typedef. In Using_AST_Node::eval_internal, after registering the constructor and to_underlying, we'd register operators like:

  • Comparison ops (==, !=, <, >, <=, >=): unwrap both operands, compare underlying values, return bool
  • Arithmetic ops (+, -, *, /, %): unwrap, compute, wrap result back into the typedef type
  • Unary ops (-, ++, --): same pattern
  • String conversion (to_string): forward to underlying

The cleanest approach: register these as Dynamic_Object-typed proxy functions with Param_Types constrained to the new type name (same pattern used for the constructor), where the lambda extracts __value, calls the underlying operation via t_ss->call_function, and re-wraps if needed.

Members:

For strong typedefs over class types (e.g., using SafeString = string), member access is harder. The dot-access dispatch goes through Dynamic_Object::get_attr, which only sees __value — it won't know to forward .size() etc. That would require either:

  1. A __getattr__-style fallback that delegates to the underlying value, or
  2. Registering proxy methods for each member of the underlying type at typedef creation time

Option 1 is more general but requires changes to Dot_Access_AST_Node dispatch. Option 2 is simpler but only works for types with known/enumerable methods.

Recommendation:

I'd implement the operator forwarding first (comparison + arithmetic) since that covers the int/double use case well and is straightforward. Member forwarding is a bigger change and might warrant a separate issue. Want me to go ahead with the operator forwarding?

@lefticus
Copy link
Copy Markdown
Member

@leftibot this needs to work generically well with any type. Can we use method_missing or something like that?

@leftibot
Copy link
Copy Markdown
Contributor Author

Yes, method_missing is exactly the right approach here and it's already well-suited for this. Here's why and how:

How method_missing works in ChaiScript

When call_member in dispatchkit.hpp:853 fails to find a matching function, it falls back to looking for method_missing functions. It calls compare_first_type on each one against the first parameter — and for Dynamic_Object-typed functions, that check matches on the type name (via dynamic_object_typename_match in dynamic_object_detail.hpp:112). The 3-parameter form receives (object, method_name, params_vector), which gives us everything we need.

The implementation

When creating a strong typedef in Using_AST_Node::eval_internal, register a single method_missing function constrained to the new type name via Param_Types. The lambda would:

  1. Extract __value from the Dynamic_Object
  2. Forward the call to the underlying value: t_ss->call_function(method_name, {underlying_value, params...})
  3. Return the result directly (for accessors, comparisons, etc.) or re-wrap it in the typedef type (for arithmetic that should return the same type)

Something like this (3-parameter form):

// Conceptually what gets registered for each strong typedef:
def method_missing(Meters obj, string name, Vector params) {
    // delegates to: to_underlying(obj).name(params...)
}

The 2-parameter form handles attribute access (no-arg calls like .size()).

Why this is the right approach

  • Generic: works for any underlying type without enumerating its methods at typedef-creation time
  • Lazy: only dispatches when a call is actually made, no upfront registration of proxy methods
  • Already built-in: the call_member fallback path at dispatchkit.hpp:850-888 does the compare_first_type filtering automatically — we just need to register the function
  • Handles everything: operators, member functions, attribute access — all go through call_member dispatch

Decision point: should arithmetic results re-wrap?

The tricky design question: when you do Meters(5) + Meters(3), should the result be Meters(8) or int(8)? Re-wrapping requires the method_missing lambda to know the typedef name and have access to the constructor, and it requires knowing which operations should re-wrap (arithmetic yes, comparison no, to_string no). One approach: try to construct a new typedef from the result, and if it fails (e.g., result is bool from <), return the raw value. But I'd suggest starting simple — delegate and return the raw result. Users can explicitly re-wrap with Meters(result) when needed. We can always add auto-rewrapping later.

Want me to implement this?

@lefticus
Copy link
Copy Markdown
Member

@leftibot I think the strong typedef is useless if the Meters + Meters returns an int. But we also have a problem with integer promotion rules such that uint8 + uint8 => int.

this is what I want you to do:

Expose all of the common arithmetic operations, if they exist, and make them strongly typed Meters + Meters -> Meters; Meters * Meters -> Meters

But also make sure and add tests so that:

using StrongString = string;
StrongString + StrongString -> StrongString
StrongString * StrongString -> errors

Then also expose all of the comparison operations that exist

StrongString < StrongString -> bool

Then also make sure that the user can extend the strong typedefs, adding their own operations.

def [](StrongString ss, size_t offset) {
return StrongChar(ss.to_underlying()[offset]
}

Take the above examples as pseudocode. There might be a better way to accomplish this:

TL;DR:

All common operators that exist should be exposed and strongly typed, and the user should be able to extend the typdef.

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 ChaiScript#680 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@leftibot
Copy link
Copy Markdown
Contributor Author

Addressed review feedback from @lefticus. New commits:

64a823c6 Address review: expose strongly-typed operators for strong typedefs

@lefticus
Copy link
Copy Markdown
Member

@leftibot resolve merge conflicts and add support for the rest of the binary operators - if they exist for the given type. It looks like right now the operators are being added regardless, then may error. This is going to be confusing to anyone using reflection in the ChaiScript system, to see a * for StrongString that then actually fails to execute.

leftibot and others added 2 commits April 14, 2026 12:30
…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 ChaiScript#680 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@leftibot
Copy link
Copy Markdown
Contributor Author

Addressed review feedback from @lefticus. New commits:

66f06df2 Address review: conditionally register operators based on underlying type support
b02380a1 Merge remote-tracking branch 'origin/develop' into fix/issue-677-add-strong-typedefs
e61be765 Fix #601: Allow operator functions to accept any type with an add() method (#686)
d4c5bdb3 Fix #61: Comprehensive exception test suite and fix for silently swallowed exceptions (#681)

@lefticus
Copy link
Copy Markdown
Member

@leftibot on second thought - register the functions no matter what - it's possible the user could add them later, plus the tests to see if they could work is complicated and expensive. Also - make sure things like *= can work as well

…nd 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 ChaiScript#680 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@leftibot
Copy link
Copy Markdown
Contributor Author

Addressed review feedback from @lefticus. New commits:

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

@lefticus
Copy link
Copy Markdown
Member

@leftibot resolve merge conflicts with ChaiScript:develop and update docs to demonstrate this new feature.

leftibot and others added 2 commits April 14, 2026 15:35
Resolve merge conflicts with ChaiScript:develop. Upstream added
nested namespace support (ChaiScript#675), grammar railroad diagrams (ChaiScript#673),
and WASM exception support (ChaiScript#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) <[email protected]>
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 ChaiScript#680 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@leftibot
Copy link
Copy Markdown
Contributor Author

Addressed review feedback from @lefticus. New commits:

b76afd9d Address review: add strong typedef documentation to cheatsheet
1ce4a81b Merge upstream/develop into fix/issue-677-add-strong-typedefs

@lefticus
Copy link
Copy Markdown
Member

@leftibot carefully resolve merge conflicts with ChaiScript:develop

Carefully resolved merge conflicts to preserve both the strong typedef
(using) feature from this branch and the enum support (ChaiScript#679), nested
namespaces (ChaiScript#552), grammar diagram (ChaiScript#628), and WASM exception (ChaiScript#678)
features from upstream develop. All 376 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@leftibot
Copy link
Copy Markdown
Contributor Author

Addressed review feedback from @lefticus. New commits:

ebcbd23b Merge upstream/develop into fix/issue-677-add-strong-typedefs
1df1b4ad Fix #19: Add enum support (#679)
9ff56426 Fix #552: Feature-request: nested namespaces (#675)
092ec417 Fix #628: Grammar railroad diagram (#673)
0fd9cab6 Fix #678: Add WASM exception support to Emscripten build (#689)

@lefticus lefticus merged commit bb06919 into ChaiScript:develop Apr 15, 2026
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add strong typedefs

2 participants