fix(vm): make <= and >= fall back through __lt per Lua 5.3 §3.4.4#213
Merged
Conversation
The :less_equal opcode now follows the spec's dual-metamethod rule: dispatch __le when present, otherwise fall back to `not (b < a)` via __lt with operands swapped. The same path covers >= (translated to b <= a). Also routes :greater_than through __lt(b, a) so > dispatches metamethods like < does today, removing the asymmetry where >, >=, and < all behaved differently for table operands. Plan: A8e
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
__le falls back to
not (b < a)when __le is unsetPlan:
.agents/plans/A8e-le-fallback-not-lt.mdGoal
Make the
:less_equalopcode fall back tonot (b < a)when neitheroperand has an
__lemetamethod, per Lua 5.3 §3.4.4. Today:less_equallooks up__leand — if missing — runssafe_compare_le, which raises on table operands. The referencemanual is explicit:
So when
__leis unset, dispatch should consult__lt(with operandsswapped) and negate the result. Only fall through to
safe_compare_lewhen neither metamethod is defined and the operandshave a primitive
<=(numbers, strings).Success criteria
Op(1) <= Op(2)returnstruewhen only__ltis set on themetatable (was:
attempt to compare table with table). Pinned bythe new
<= falls back to not (b < a) when only __lt is definedtest.
Op(2) <= Op(1)returnsfalsein the same setup. Pinned bythe same test.
Op(1) <= Op(1)returnstrue(negation ofnot (a < a)).Pinned by the same test.
__le(when set) still wins. Pinned by__le is preferred over __lt when both are defined(raises if__ltfires) and__le on either operand wins over __lt fallback.>=between tables defined only via__ltworks (translateda >= b ⇒ b <= achains into the new fallback). Pinned by>= falls back via __lt when only __lt is defined.safe_compare_lestill raises for primitive operands of mixed,non-comparable type (
1 <= "x"still raises). Pinned by<= on incompatible primitive types still raises.assert((Set{1,2,3,4} <= Set{1,2,3,4})))passes standalone. Verified via the file-probe pattern (with line
188-190 patched out, since that's A8b's territory and A8b is in
review). Next stop after the fix is line 285:
assert(Set{1,3,5,1} == rawSet{3,5,1})— table-with-__eqcompared against a raw table; that's A8d-adjacent (
==/__eq), out of scope here.>and>=paths.
mix testpasses; no regressions. 1564 → 1570 (+6 new tests),0 failures.
Changes
lib/lua/vm/executor.ex:compare_le/3helper implements the §3.4.4 fallback chain:__le→__lt(b, a)negated → primitive<=.:less_equalnow delegates tocompare_le.:greater_equalnow delegates tocompare_le(b, a, state),matching the spec's
a >= b ⇔ b <= atranslation. Previously:greater_equalskipped metamethod dispatch entirely and wentstraight to
safe_compare_ge.:greater_thannow goes throughtry_binary_metamethod("__lt", b, a, ...), matchinga > b ⇔ b < a. Previously:greater_thanalso skipped metamethod dispatch. The plan's Risks section flagged
this asymmetry; fixing it was needed to advance events.lua past
the
test()block at line 222 (which exercises>).safe_compare_gtandsafe_compare_geremoved (no longerreferenced;
safe_compare_ltandsafe_compare_lecover all fourdirections through operand swapping).
test/lua/vm/metatable_test.exs: 6 new tests pinning the fallbacksemantics for
<=,>=,>, the__le-wins precedence, the__le-on-either-operand path, and the primitive raise.Discoveries
:greater_thanskipped metamethod dispatch entirely on main. Theplan's Risks section anticipated this (
safe_compare_*may needparallel treatment). Fixing it was necessary in scope: events.lua's
test()function at line 207-220 callsOp(1) > Op(1)etc., whichtriggered
attempt to compare table with tableon main even though__ltwas set. Per speca > b ⇔ b < a, so the fix routes:greater_thanthrough__lt(b, a). This also brings parity tothe four comparison opcodes.
:greater_equalwas already a separate opcode, not a desugaring.Codegen at
lib/lua/compiler/codegen.ex:929emits:greater_equalfor
>=. So the plan's "verify in the implementation that fixing:less_equalalso fixes>=" required mirroring the change to:greater_equal. Done.events.lua next stop: line 285. With this fix and A8b's stub
patched out, the file probes cleanly through line 284. Line 285 is
assert(Set{1,3,5,1} == rawSet{3,5,1})— comparing a table with__eqto a raw table. Per Lua 5.3 §3.4.4 this should consult__eq. That's adjacent to A8d (~=/__eqdispatch); a separatefollow-up plan can pick it up. events.lua remains in
@skipped_tests.Verification
Repros from the plan (both pass):
Out of scope (intentional)
@ready_tests. Line 285 still blocks thefull file; that's a separate plan.
__eqshort-circuit at events.lua line 285. That's A8d-adjacent.__ltitself (already correct). VerifiedOp(1) < Op(2)works onmain with only
__ltset.