From ada0f34c472b8c11a645e16c4268197b9c5ea07b Mon Sep 17 00:00:00 2001 From: Francesco Kirchhoff Date: Tue, 6 Oct 2026 12:52:18 +0100 Subject: [PATCH] Boxed receivers dispatch user <, >, <= and >= without coerce A boxed comparison reaches a user class's operator through sp_user_binop_hook. Its dispatch table already has comparison arms, but hook detection required the class to have the coerce shape. A class with its own ordering could therefore fall through to sp_poly_cmp and raise ArgumentError when its receiver arrived in a box. The existing operator scan now uses is_cmp_op and comp_method_in_chain to enable the table for a class defining or inheriting <, >, <= or >=, independently of coerce. The other hook conditions and numeric comparison fast paths stay unchanged. The regression covers all four operators, non-Boolean results, inherited methods and mixed numeric receivers. Co-Authored-By: Claude Opus 5.5 Gate: green tree 6bddc26cf97d master 9922a2c74ee1 (linux-x86_64 gcc-13.3.0) tests 6440/0 --- Makefile | 2 ++ src/codegen.c | 4 ++++ test/poly_user_relop_no_coerce.rb | 24 ++++++++++++++++++++++ test/poly_user_relop_no_coerce.rb.expected | 10 +++++++++ 4 files changed, 40 insertions(+) create mode 100644 test/poly_user_relop_no_coerce.rb create mode 100644 test/poly_user_relop_no_coerce.rb.expected diff --git a/Makefile b/Makefile index 5500ddbd6c..82fffa8d00 100644 --- a/Makefile +++ b/Makefile @@ -3513,6 +3513,8 @@ infer-test: $(SPINEL) $(SP_RT_LIB) grep -E '^#define _SP_HCR' "$$tmp/lahc.c" | grep -q 'lv_cur\b' && { echo "infer-test: FAIL (an array local the loop reassigns was read through a cached header)"; ok=0; }; \ grep -E '^#define _SP_HCR' "$$tmp/lahc.c" | grep -q 'self->iv_v\b' && { echo "infer-test: FAIL (an ivar the loop writes was read through a cached header)"; ok=0; }; \ grep -qE '^#define _SP_HCR[0-9]+\(\) .*lv_qv.*lv_qk.*lv_qs' "$$tmp/lahc.c" || { echo "infer-test: FAIL (a class test on a scalar kept a loop from caching its arrays' headers)"; ok=0; }; \ + $(SPINEL) test/poly_user_relop_no_coerce.rb -c --no-line-map -o "$$tmp/urn.c" >/dev/null 2>&1 || { echo "infer-test: FAIL (poly_user_relop_no_coerce: -c)"; ok=0; }; \ + grep -q 'SP_INSTALL_HOOK(sp_user_binop_hook, sp_user_binop_dispatch);' "$$tmp/urn.c" || { echo "infer-test: FAIL (a class ordering itself with < and no coerce has no binop table for a boxed comparison)"; ok=0; }; \ $(SPINEL) test/loop_bounded_index_read.rb -c --no-line-map -o "$$tmp/lbi.c" >/dev/null 2>&1 || { echo "infer-test: FAIL (loop_bounded_index_read: -c)"; ok=0; }; \ grep -qE '_hcd[0-9]+_[0-9]+\[lv_i\]' "$$tmp/lbi.c" && grep -qE '_hcd[0-9]+_[0-9]+\[lv_j\]' "$$tmp/lbi.c" || { echo "infer-test: FAIL (a read bounded by its loop's own i < a.length test still tests its index)"; ok=0; }; \ grep -qE '_hcd[0-9]+_[0-9]+\[lv_(m|q|r|w|x|y|z)\]' "$$tmp/lbi.c" && { echo "infer-test: FAIL (a read whose index the loop does not keep in range lost its bounds test)"; ok=0; }; \ diff --git a/src/codegen.c b/src/codegen.c index 8a1e657df7..7a2a690975 100644 --- a/src/codegen.c +++ b/src/codegen.c @@ -16615,6 +16615,10 @@ char *codegen_program(const NodeTable *nt) { if (!c->classes[k].instantiated && !breopen) continue; for (int u = 0; uops[u]; u++) if (comp_method_in_chain(c, k, uops[u], NULL) >= 0) { g_has_user_binop = 1; break; } + /* A class's own <, >, <= or >= reaches a boxed receiver through the + table even without coerce; sp_poly_cmp cannot supply its ordering. */ + for (int u = 0; cops[u] && !g_has_user_binop; u++) + if (is_cmp_op(cops[u]) && comp_method_in_chain(c, k, cops[u], NULL) >= 0) g_has_user_binop = 1; /* a `<=>` with no `==` is Comparable's equality, which the table derives: a boxed `m == n` (a block parameter, a hash value, a `when FIVE`) reaches it only through the table */ diff --git a/test/poly_user_relop_no_coerce.rb b/test/poly_user_relop_no_coerce.rb new file mode 100644 index 0000000000..a5cce170a1 --- /dev/null +++ b/test/poly_user_relop_no_coerce.rb @@ -0,0 +1,24 @@ +# A class that defines its own <, >, <= or >= -- and no coerce -- orders its +# instances when they arrive boxed (out of a mixed Array, a poly local or +# parameter): the comparison reaches the user method through the binop +# table, as `+` and `==` always did, where it raised "comparison of Money +# with Money failed". Numbers beside them keep comparing as numbers, and a +# subclass answers through the method it inherits. +class Money + attr_reader :c + def initialize(c) + @c = c + end + def <(o) = c < o.c + def >(o) = c > o.c + def <=(o) = c <= o.c ? :le : nil + def >=(o) = c >= o.c +end +class Euro < Money; end + +vals = [Money.new(1), 3, Euro.new(5), 2.5] +p vals[0] < vals[2], vals[2] > vals[0], vals[0] <= vals[2], vals[2] <= vals[0], vals[2] >= Money.new(5) +p vals[1] < vals[3], vals[3] > vals[1] +def lt(a, b) = a < b +p lt(vals[0], vals[2]), lt(vals[1], 4) +p vals.select { |v| v.is_a?(Money) }.sort { |a, b| a > b ? -1 : 1 }.map(&:c) diff --git a/test/poly_user_relop_no_coerce.rb.expected b/test/poly_user_relop_no_coerce.rb.expected new file mode 100644 index 0000000000..29cb7da045 --- /dev/null +++ b/test/poly_user_relop_no_coerce.rb.expected @@ -0,0 +1,10 @@ +true +true +:le +nil +true +false +false +true +true +[5, 1]