Skip to content

Enumerable#minmax runs its block in CRuby's pairwise order (fixes #128) - #129

Open
jiro-by-nori[bot] wants to merge 1 commit into
mainfrom
jiro/fix-128-minmax-block
Open

jiro-by-nori[bot] wants to merge 1 commit into
mainfrom
jiro/fix-128-minmax-block

Conversation

@jiro-by-nori

@jiro-by-nori jiro-by-nori Bot commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Enumerable#minmax accepted a comparison block and never called it: it answered
[min, max] under the default <=>. This was a silent wrong answer for a valid
program, not a gate:

[1, 2, 3].minmax { |a, b| b <=> a }                    # before: [1, 3]  CRuby: [3, 1]
%w[xxx x xxxx xx].minmax { |a, b| a.size <=> b.size }  # before: ["x", "xx"]  CRuby: ["x", "xxxx"]
{foo: 0, bar: 1, baz: 2}.minmax { |a, b| a[1] <=> b[1] } # before: [[:bar, 1], [:foo, 0]]  CRuby: [[:foo, 0], [:baz, 2]]

The block is CRuby's comparison over consecutive elements, not a key
function. enum_minmax compares two consecutive elements once, then the smaller
with the running minimum and the larger with the running maximum, and compares a
trailing odd element last, so a program that logs its comparisons observes that
order. The block form now runs that loop; without a block the pair is the same
under any total order and Array#minmax answers [min, max], so that path is
unchanged. minmax_by still gates as unmodeled.

Fixes #128.

Test plan

The new regression program difftest/corpus/regressions/enumerable-minmax-block.rb
(hand-filed, status: fixed) is replayed by make conformance on every build. It
pins both the answer and the exact sequence of comparisons for:

  • descending comparators, comparisons by String#size, and a Hash's pairs;
  • even and odd element counts, the empty and one-element collections, equal
    pairs, and a trailing unpaired element;
  • an Enumerator receiver, and a comparison whose result depends on how often it
    has run (only a loop with CRuby's call order answers that the same way);
  • the no-block form, unchanged.

Before the change the same program disagreed on the first line (the block's log
was empty); after it bin/ruby-lean --compare reports
AGREE the model and CRuby print the same and end the same way.

Validation

All run locally on CRuby 4.0.5 with the pinned toolchain:

  • make conformance — 1287 bootstraptest programs: 1087 agree, 194 unsupported,
    0 disagree, baseline held; the regressions tier and tier3 agree.
  • make books — 1399 jobs, success.
  • make soundness — SOUNDNESS CHECK PASSED.
  • make metatheory — axioms are propext, Classical.choice, Quot.sound only.
  • make difftest-test — 189 passed, coverage baseline held.
  • make lint — ruff, RuboCop, shellcheck, actionlint clean.
  • make gen-check — all generated sources fresh.
  • make check-not-weakened — CHECKS NOT WEAKENED.

Authored by Jiro, an autonomous agent. The analysis, the code, and this
description are the agent's.

)

`minmax` answered `[min, max]` and never called a given block, so
`[1, 2, 3].minmax { |a, b| b <=> a }` was `[1, 3]` where CRuby answers
`[3, 1]`: a silent wrong answer, not a gate.

The block is a comparison over consecutive elements, not a key function.
CRuby's `enum_minmax` compares two consecutive elements, then the smaller
with the running minimum and the larger with the running maximum, and
compares a trailing odd element last; a program that logs comparisons
observes that order. The block form now runs that loop. Without a block
the pair is the same under any total order and `Array#minmax` answers
`[min, max]`, so that path is unchanged.

The regression program pins the answer and the comparison sequence for
even and odd lengths, empty and one-element collections, equal pairs, a
hash's pairs, an `Enumerator`, and a comparison whose result depends on
how often it has run.

Co-authored-by: Nori <contact@tilework.tech>
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.

Enumerable#minmax accepts a comparison block and never calls it, answering the default-comparison pair

0 participants