Fix #625: function_less_than comparator violates strict-weak ordering (#654)

The function_less_than comparator used by std::stable_sort violated the
strict-weak ordering requirement in two ways: (1) functions with different
arities but matching overlapping parameters were treated as equivalent,
breaking transitivity, and (2) the dynamic_object_type_name comparison
silently fell through when one side had an empty type name. Fixed by
ordering by arity when overlapping parameters match, and imposing a total
order on dynamic object type names.

Co-authored-by: leftibot <leftibot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jason Turner <jason@emptycrate.com>
This commit is contained in:
leftibot 2026-04-11 16:42:40 -06:00 committed by GitHub
parent 7b95ff5126
commit 11fec25112
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 35 additions and 6 deletions

View File

@ -1087,7 +1087,8 @@ namespace chaiscript {
// overridden methods in derived classes are tried first during dispatch
const auto &lhs_dotn = lhs->dynamic_object_type_name();
const auto &rhs_dotn = rhs->dynamic_object_type_name();
if (!lhs_dotn.empty() && !rhs_dotn.empty() && lhs_dotn != rhs_dotn) {
if (lhs_dotn != rhs_dotn) {
if (!lhs_dotn.empty() && !rhs_dotn.empty()) {
if (dispatch::Dynamic_Object::type_matches(lhs_dotn, rhs_dotn)) {
return true; // lhs is derived from rhs, so lhs is more specific
}
@ -1095,6 +1096,13 @@ namespace chaiscript {
return false; // rhs is derived from lhs, so rhs is more specific
}
}
// Impose a total order on type names to maintain strict-weak ordering:
// non-empty names sort before empty names, then lexicographically
if (lhs_dotn.empty() != rhs_dotn.empty()) {
return !lhs_dotn.empty();
}
return lhs_dotn < rhs_dotn;
}
const auto &lhsparamtypes = lhs->get_param_types();
const auto &rhsparamtypes = rhs->get_param_types();
@ -1143,7 +1151,9 @@ namespace chaiscript {
return lt < rt;
}
return false;
// When all overlapping parameters match, order by arity to maintain
// strict-weak ordering (transitivity of equivalence)
return lhssize < rhssize;
}
/// Implementation detail for adding a function.

View File

@ -1437,6 +1437,25 @@ TEST_CASE("Issue #421 - Switch with type_conversion does not compare destroyed o
})") == 0);
}
// Issue #625: function_less_than comparator must satisfy strict-weak ordering.
// Registering overloaded functions with different arities triggered a
// std::stable_sort assertion on macOS 15.2 (hardened libc++) because the
// comparator violated transitivity of equivalence.
TEST_CASE("Issue 625: function_less_than strict-weak ordering with different arities") {
chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser());
// Register overloaded functions with varying arities under the same name.
// If the comparator doesn't order by arity when overlapping params match,
// std::stable_sort may exhibit undefined behavior.
CHECK_NOTHROW(chai.add(chaiscript::fun([](int x, const chaiscript::Boxed_Value &) { return x; }), "overloaded"));
CHECK_NOTHROW(chai.add(chaiscript::fun([](int x, double y) { return x + static_cast<int>(y); }), "overloaded"));
CHECK_NOTHROW(chai.add(chaiscript::fun([](int x) { return x; }), "overloaded"));
// Verify dispatch still works correctly
CHECK(chai.eval<int>("overloaded(5)") == 5);
CHECK(chai.eval<int>("overloaded(3, 2.0)") == 5);
}
// Regression test: push_back() on script-created vector has no effect when
// vector_conversion is in effect. The bug occurs because dispatch selects
// the C++ push_back for the converted type over the built-in one, operating