Skip to content

conformance: derive CUR_VERSION from RUBY_VERSION — one fact, one place - #845

Merged
tannevaled merged 1 commit into
mainfrom
derive-cur-version
Oct 31, 2026
Merged

tannevaled merged 1 commit into
mainfrom
derive-cur-version

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

conformance: derive CUR_VERSION from RUBY_VERSION — one fact, one place

Closes the last item of #799: the version the gate resolves ruby_version_is
guards at stops being a second hardcoded copy of what the runtime already
reports.

-CUR_VERSION = "3.4.1"
+CUR_VERSION = RUBY_VERSION

Why this is worth a PR of its own

CUR_VERSION selects the population the whole conformance figure is counted
over. At 3.4.1 every example behind a ruby_version_is "4.0" guard is removed
from the suite before anything runs — 61 block guards and 5 inline sites at that
boundary, and 41 guards asserting what 4.0 changed away from (#836's census).

The two copies agreed, so nothing was wrong on any given day. The defect #799
names is that half a version change could land: rubyVersion bumped in
internal/vm/kernel_introspect.go, this file forgotten, and the gate goes on
measuring the 3.4 population while every page claims the new version. Both
halves look correct read on their own, which is exactly why nothing would have
said so.

This lands before the move to 4.0.x rather than with it, so the move is a
one-line change to one place whose effect the ratchet can then attribute
cleanly.

Measured: a true no-op today

RUBY_VERSION is "3.4.1", the same string the literal held, so the population
does not move. Measured rather than asserted — the derived shim run against
main's own binary (3a3fb81, so nothing else of mine is in the reading), on a
private two-file corpus:

$ rbgo core/builtin_constants/builtin_constants_spec.rb
RBGO_RESULT pass=17 fail=0 error=0 skip=0

BASELINE records 17 for that file. A green ratchet on this PR is therefore
itself the proof of equivalence
, across all 2,206 files rather than the one I
could run here.

Upstream behaviour, not a local convenience

mspec reads RUBY_VERSION for this too. The chain, at mspec b79c65e:

guards/version.rb:67  VersionGuard.new(VersionGuard::FULL_RUBY_VERSION, …)
guards/version.rb:6   FULL_RUBY_VERSION = SpecVersion.new SpecGuard.ruby_version(:full)
guards/guard.rb:41    def self.ruby_version(which = :minor)
guards/guard.rb:51      RUBY_VERSION.split('.')[0,n].join('.')   # n = 3 for :full

i.e. RUBY_VERSION itself for a three-part version. A consequence worth having:
run under a different interpreter, the shim now measures at THAT interpreter's
version.

Two tests, and what each one cannot see

TestShimVersionIsTheRuntimesNotASecondCopy asserts the equality through the
real shim rather than by matching the file's text, because what matters is
not how the line is spelled but what the constant holds at run time.

TestShimVersionGuardsResolveAtThatVersion proves CUR_VERSION actually feeds
_version_in_range: a guard at the running version keeps its example, one above
it removes its example (removes, not skips — a block guard below the threshold
deletes rather than counting, so the deliberately-failing example inside it
contributes to no counter).

⛔ The control, planted: with the shim set back to a literal
CUR_VERSION = "9.9.9", the first test fails and names the disagreement
(expected "9.9.9" to == "3.4.1") — while the second still passes, because a
guard at >= RUBY_VERSION is satisfied by any higher value. So the second test
proves the wiring and the first proves the value; reading the second alone as the
guard against a hardcode would be wrong, and the comment says so at the site.

Not in this PR

The documentation side is go-embedded-ruby/docs#41, which makes the stated
version a checked claim and already recognises this derived spelling — so that
guard does not break on the day this merges.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

🤖 Generated with Claude Code

Closes the last item of #799: the version the gate resolves `ruby_version_is`
guards at stops being a second hardcoded copy of what the runtime already
reports.

    -CUR_VERSION = "3.4.1"
    +CUR_VERSION = RUBY_VERSION

## Why this is worth a PR of its own

`CUR_VERSION` selects the **population** the whole conformance figure is counted
over. At 3.4.1 every example behind a `ruby_version_is "4.0"` guard is removed
from the suite before anything runs — 61 block guards and 5 inline sites at that
boundary, and 41 guards asserting what 4.0 changed away from (#836's census).

The two copies agreed, so nothing was wrong on any given day. The defect #799
names is that **half a version change could land**: `rubyVersion` bumped in
internal/vm/kernel_introspect.go, this file forgotten, and the gate goes on
measuring the 3.4 population while every page claims the new version. Both
halves look correct read on their own, which is exactly why nothing would have
said so.

This lands **before** the move to 4.0.x rather than with it, so the move is a
one-line change to one place whose effect the ratchet can then attribute
cleanly.

## Measured: a true no-op today

`RUBY_VERSION` is `"3.4.1"`, the same string the literal held, so the population
does not move. Measured rather than asserted — the derived shim run against
`main`'s own binary (3a3fb81, so nothing else of mine is in the reading), on a
private two-file corpus:

    $ rbgo core/builtin_constants/builtin_constants_spec.rb
    RBGO_RESULT pass=17 fail=0 error=0 skip=0

`BASELINE` records `17` for that file. **A green ratchet on this PR is therefore
itself the proof of equivalence**, across all 2,206 files rather than the one I
could run here.

## Upstream behaviour, not a local convenience

mspec reads RUBY_VERSION for this too. The chain, at mspec b79c65e:

    guards/version.rb:67  VersionGuard.new(VersionGuard::FULL_RUBY_VERSION, …)
    guards/version.rb:6   FULL_RUBY_VERSION = SpecVersion.new SpecGuard.ruby_version(:full)
    guards/guard.rb:41    def self.ruby_version(which = :minor)
    guards/guard.rb:51      RUBY_VERSION.split('.')[0,n].join('.')   # n = 3 for :full

i.e. RUBY_VERSION itself for a three-part version. A consequence worth having:
run under a different interpreter, the shim now measures at THAT interpreter's
version.

## Two tests, and what each one cannot see

`TestShimVersionIsTheRuntimesNotASecondCopy` asserts the equality through the
**real shim** rather than by matching the file's text, because what matters is
not how the line is spelled but what the constant holds at run time.

`TestShimVersionGuardsResolveAtThatVersion` proves CUR_VERSION actually feeds
`_version_in_range`: a guard at the running version keeps its example, one above
it removes its example (removes, not skips — a block guard below the threshold
deletes rather than counting, so the deliberately-failing example inside it
contributes to no counter).

⛔ **The control, planted:** with the shim set back to a literal
`CUR_VERSION = "9.9.9"`, the first test fails and names the disagreement
(`expected "9.9.9" to == "3.4.1"`) — while **the second still passes**, because a
guard at `>= RUBY_VERSION` is satisfied by any higher value. So the second test
proves the wiring and the first proves the value; reading the second alone as the
guard against a hardcode would be wrong, and the comment says so at the site.

## Not in this PR

- The move to 4.0.x itself. #799 asks for it with the baseline **not**
  regenerated, so the ratchet names every file that shifts in both directions;
  that reading is only clean once missing capabilities have landed separately
  (#844 is the first).
- `PLATFORMS = [:darwin, :bsd, :unix]`, three lines below and the same shape of
  defect — a hardcoded copy of something `RUBY_PLATFORM` already knows (#837).
  It is left alone deliberately: unlike this change it is **not** a no-op, it
  moves the population on the Linux lane, and it needs its own measurement.

The documentation side is go-embedded-ruby/docs#41, which makes the stated
version a checked claim and already recognises this derived spelling — so that
guard does not break on the day this merges.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled added a commit to go-embedded-ruby/go-embedded-ruby.github.io that referenced this pull request Oct 31, 2026
* say which Ruby version the 23,511 was counted at

This page states the ruby/spec total five times and never said which population
it counts over. The shim resolves ruby/spec's own `ruby_version_is` guards at
Ruby 3.4.1, so every example behind a `ruby_version_is "4.0"` guard is removed
from the suite before anything runs — 61 block guards and 5 inline sites at that
boundary (censused by the interpreter repo's `cmd/guards`), and 41 guards
asserting what 4.0 changed away from.

Meanwhile the same page says the project targets Ruby 4.0 semantics and names an
MRI 4.0.5 oracle. Every one of those statements is true, and together they read
as a 4.0 result. It is a 3.4-shaped one.

Two sentences, in the two places that carry the figure most plainly: the
"Conformance, measured" card, beside the existing caveat that only `language/`
and `core/` are swept — the same kind of caveat, and it was missing its pair —
and the Phase 8 blurb in hugo.toml, which already explains the gate at length.

Measured with the tool that will enforce it:

  $ go run ./tools/conformancefigure --root <this tree>
  5 total claim(s) and 2 version claim(s) checked against the gate's 23511 at Ruby 3.4.1 (two copies, agreeing)

Both new sentences are recognised and agree. "two copies, agreeing" is the state
of the interpreter repository today: the version is spelled in both
internal/vm/kernel_introspect.go and the conformance shim, and
go-embedded-ruby/ruby#845 collapses that to one — at which point the same line
will read "derived from RUBY_VERSION" without anything here changing.

The lane bump to the version of the tool that performs this check comes in the
next commit, once that release exists; stating the version without the check
would be the unguarded half of the change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* conformancefigure@v0.6.0: turn the stated version into a checked claim

The previous commit added the sentences; this is the half that makes them
guarded, and neither is worth having alone — a version stated and unchecked is
the next figure to rot, and a check with nothing to check exits 1.

Verified with the exact published command this lane will run, fetched through
the proxy:

    $ go run github.com/go-embedded-ruby/docs/tools/conformancefigure@v0.6.0 --root .
    go: downloading github.com/go-embedded-ruby/docs v0.6.0
    5 total claim(s) and 2 version claim(s) checked against the gate's 23511 at Ruby 3.4.1 (two copies, agreeing)

Five totals and both new version sentences, all agreeing. actionlint clean.

The version is read from **both** files in the interpreter repository that spell
it — `internal/vm/kernel_introspect.go`'s `rubyVersion` and the conformance
shim's `CUR_VERSION` — and a disagreement between them is a refusal (exit 2),
not a verdict on this page: with the copies apart there is no single version to
judge it against. go-embedded-ruby/ruby#799 records that those are two hardcoded
copies of one fact and that "today they happen to agree" is the defect waiting
to happen.

`(two copies, agreeing)` is that repository's state today. ruby#845 collapses it
to one, and the same line will then read `(derived from RUBY_VERSION)` with
nothing here to change — v0.6.0 already recognises that spelling, so this lane
does not break on the day the thing it reports gets fixed.

The **differential oracle** this page names (MRI 4.0.5) is a different claim,
free to be newer than the version guards resolve at, and is deliberately not
matched; three real oracle sentences are negative cases in the tool's own
vocabulary control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* hugo.toml: withdraw the sentence — that blurb's body is never published

Two things, one of them a correction to what I wrote in the previous commit.

## The build lane was right, and it caught what my own check cannot

The push before this one broke the site:

    ERROR failed to load config: "hugo.toml:74:713":
    unmarshal failed: toml: expected newline but got U+0034 '4'

My sentence contained `` `ruby_version_is "4.0"` `` — double quotes inside a
double-quoted TOML string, unescaped. Mine, straightforwardly.

Worth recording is **why `conformancefigure` was green on it**: that tool reads
`.toml` as TEXT, looking for figures in prose, and never parses it. A file that
cannot load is invisible to it, and always will be. `hugo --minify` is the check
for that and it did its job on the first push. What I should have done, and had
no excuse not to — hugo is installed here — is build the site locally before
pushing. Doing that now: `hugo` exits 0.

## ⛔ And then the build showed the sentence was never going to be read

With the quotes escaped and the site building, the rendered page did not contain
it. Nor does it contain anything else from deep in that blurb:

| in the built site | |
|---|---|
| the Phase 8 blurb's **first sentence** | present |
| `LinuxONE` (pre-existing, deep in the same blurb) | **absent** |
| my new sentence (also deep in it) | **absent** |
| the sentence I added to `layouts/index.html` | present |

The site builds two pages (`index.html`, `performance/index.html`) and the string
appears in neither. **Everything in a `params.phases[].blurb` after its first
sentence is unpublished** — the home page takes the first sentence for its card
and nothing else renders the rest.

So the sentence is withdrawn rather than escaped. A guard reporting *"2 version
claim(s) checked"* while the page publishes **one** is a measurement flattering
itself, which is the exact failure this whole change exists to prevent. After
this commit the tool reports `1 version claim(s)` and the page publishes 1 — the
numbers agree because they are counting the same thing.

The claim is still guarded where it is read: `layouts/index.html`, in the
"Conformance, measured" card, beside the existing caveat that only `language/`
and `core/` are swept.

## ⛔ Correcting the previous commit and go-embedded-ruby/docs#41

I wrote that `hugo.toml`'s `params.phases[].blurb` is *"prose Hugo renders into
the page like any other"* and that the stale `23,481` in it *"read 23,481 for 26
days under a green check"*. **The first is wrong for this site's layout, and the
second is therefore overstated.** That figure was stale in a file that feeds only
a first sentence; it was not a wrong number on the published page, and the
published figures were correct throughout.

What stands: the figure WAS stale in the repository, two readings of one number
had drifted, and a tool that reads `.toml` is cheap insurance for the day a
layout change starts publishing that text. What does not stand is the severity I
gave it.

Separately worth deciding, and not decided here: a long prose body kept in
`hugo.toml` that nothing renders is a trap — it cost me this round trip, and it
will cost the next person the same. Either it should be rendered or it should be
deleted. Filed rather than guessed at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 4d1a7fd into main Oct 31, 2026
25 checks passed
@tannevaled
tannevaled deleted the derive-cur-version branch October 31, 2026 23:55
tannevaled added a commit that referenced this pull request Nov 1, 2026
…th (#846)

* vm: Math.log1p and Math.expm1, the two functions Ruby 4.0 added to Math

Found by asking the interpreter, rather than by reading a changelog: of 31 Ruby
4.0 surfaces that ruby/spec gates behind `ruby_version_is "4.0"`, rbgo already
answers **28**. The three it does not are `Array#rfind`, `Math.log1p` and
`Math.expm1`. These are the two pure ones.

## Measured, with the control

`core/math/log1p_spec.rb` and `core/math/expm1_spec.rb`, run on a private corpus
at the pinned `SPEC_SHA` with the shim's `CUR_VERSION` set to 4.0.9 (the whole
of both files sits behind a 4.0 guard, so at 3.4.1 they yield nothing at all):

| spec | `main` (3a3fb81) | this branch |
|---|---|---|
| `core/math/log1p_spec.rb` | `pass=0 fail=5 error=5` | **`pass=10 fail=0 error=0`** |
| `core/math/expm1_spec.rb` | `pass=0 fail=2 error=5` | **`pass=7 fail=0 error=0`** |

All 17 examples, and the control row is what makes the right column mean
something: every one of them failed or errored before.

Those 17 remain **invisible to CI** until `CUR_VERSION` moves (#845, #799) — the
same ordering as #844: capability first, so that when the version moves, a file
that shifts because a method was missing is not noise in the ratchet's reading.

## Why these two functions exist at all

`log1p(x)` is `log(1+x)` and `expm1(x)` is `exp(x)-1`, computed so the result
keeps its precision for |x| far below 1. Written the naive way they both return
**0.0** for 1e-16, because the leading 1 swallows every significant digit:

    Math.log(1 + 1e-16)   # => 0.0
    Math.exp(1e-16) - 1    # => 0.0

Both specs assert exactly that this must not happen (log1p_spec.rb:10,
expm1_spec.rb:10: close to 1e-16 **and** `!= 0.0`). So an implementation that
spells out the identity passes the first example of each file and fails the
second. Go's `math.Log1p`/`math.Expm1` are the same primitives C hands MRI; the
tests carry the naive forms beside the real ones as the control.

Domain: log1p is real for x >= -1 — `log1p(-1)` is `-Infinity`, which is a value
and not an error — so `Math::DomainError` belongs to x < -1, which is what the
spec exercises with `-1 - 1e-15`.

## ⛔ A pre-existing divergence these two do NOT inherit

`core/math/log1p_spec.rb:27` asserts `Math.log1p(9, 3)` raises ArgumentError
"wrong number of arguments (given 2, expected 1)". rbgo does not check arity on
Math functions at all — measured on `main`:

    $ rbgo -e 'p Math.cbrt(9, 3)'
    2.080083823051904            # MRI: ArgumentError

The extra argument is silently dropped, for every function the `unary` helper
installs. **That is not fixed here**, and the asymmetry is deliberate rather than
an oversight: the arities are not uniform (`Math.log` legitimately takes one OR
two arguments, `atan2`/`hypot`/`ldexp` take two), so a blanket check is wrong and
a per-function sweep of ~20 functions moves what the gate measures and needs its
own measurement. It is filed separately.

What must not happen is a new method inheriting the old leniency because its
neighbours have it. `unaryStrict` carries the check, the older helpers are left
as they are, and the comment at the site says why so the next reader does not
read the asymmetry as sloppiness.

## No regression

The whole `internal/vm` suite passes (230s, `-count=1`). The only spec in
`language/`+`core/` that touches Math's method list is
`core/math/shared/atanh.rb:32`, and it asserts **inclusion**, not a count — two
more private instance methods cannot move it.

`gofmt` clean, `go vet` clean. No new input is parsed; both functions are pure
and allocate one Float.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* tests: pin the zero-argument message, where the old helpers leak a Go panic

Probing the arity divergence further turned up something worse than leniency.
With **no** argument the older helpers index `args[0]` out of range, and the
VM's panic-to-exception net surfaces the Go runtime error as the Ruby message.
One run, three calls, on this branch's binary:

    Math.log1p  -> ArgumentError: wrong number of arguments (given 0, expected 1)
    Math.expm1  -> ArgumentError: wrong number of arguments (given 0, expected 1)
    Math.cbrt   -> ArgumentError: cbrt: runtime error: index out of range [0] with length 0

The class is right by accident. The message is a Go internal handed to Ruby
code, and it would read differently on another Go version or architecture — so
anything matching on it is matching on our build.

The two new functions already answered correctly, because `unaryStrict` checks
`len(args) != 1` rather than only the two-argument case. **A measurement in a PR
body is not a regression guard**, so the wording is now pinned by two test
cases; without them, nothing would stop these two drifting into the same leak.

`Math.cbrt` and its ~20 siblings are not changed here — that is the separate
arity issue, where the fix has to be per-function because `Math.log` takes one
or two arguments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tannevaled added a commit to go-embedded-ruby/go-embedded-ruby.github.io that referenced this pull request Nov 4, 2026
v0.6.0's vocabulary spelled out "the sum is", "its sum is" and "whose sum is",
so this page's "The sum of those rows is <b>23,673</b>" was never a candidate.
The lane printed "5 total claim(s) checked" and passed, on a page holding six,
one of them stale. go-embedded-ruby/docs#44 widened the pattern; v0.7.0 carries
it.

Run with the exact published command this lane runs, fetched through the proxy
against this tree:

    $ go run github.com/go-embedded-ruby/docs/tools/conformancefigure@v0.7.0 --root .
    go: downloading github.com/go-embedded-ruby/docs v0.7.0
    6 total claim(s) and 1 version claim(s) checked against the gate's 23672 at Ruby 4.0.9 (derived from RUBY_VERSION)

Six, not five, and no disagreement.

The other number that moved was already predicted in the comment this replaces:
"1 version claim … derived from RUBY_VERSION" is go-embedded-ruby/ruby#845
collapsing the two copies of the version into one, and nothing on the page had
to change for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tannevaled added a commit to go-embedded-ruby/go-embedded-ruby.github.io that referenced this pull request Nov 4, 2026
* Ruby 4.0.9: the figure, and the version it is counted at

rbgo reports RUBY_VERSION 4.0.9 (ruby#882) and BASELINE was re-recorded at that
version on the pinned runner that enforces it: 23,511 -> 23,673.

Six claims on this page move, and the lane's own checker named every one of
them rather than my reading the page:

    hugo.toml:74                 claims 23673, the gate records 23511
    layouts/_default/list.html:76   "
    layouts/index.html:27           "
    layouts/index.html:62           "
    layouts/index.html:71           "
    layouts/index.html:62: says the gate measures at 4.0.9, it measures at 3.4.1

That last line is the v0.6.0 version claim, and it is the one that mattered
most here. The page said:

    Those guards resolve at Ruby 3.4.1 -- the version this runtime reports --
    so every example behind a `ruby_version_is "4.0"` guard is removed from the
    suite before anything is counted: 61 block guards and 5 inline sites sit at
    that boundary, while the latest MRI release is 4.0.9.

Every word of that is now false, and none of it is a number, so only the
version check could catch it. It now says the opposite, and states what the
move cost: +144 passing and 27 examples that WENT RED -- a figure no pass total
shows, found by comparing the two runs' failure descriptions.

`layouts/_default/list.html:73` is a DATED claim and therefore exempt from the
check; it is updated anyway, because exempt from the gate is not the same as
allowed to be wrong.

⛔ This cannot land before ruby#882. The checker reads the interpreter
repository's main, so until the bump merges it refuses this page -- verified,
not assumed: run against main today it prints the six disagreements above and
exits 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* The figure is 23,672, and the page now says why it went down by one

core/random/bytes_spec.rb draws 8 or 9 passing examples from run to run. The
2026-11-03 re-record banked the 9, the next ordinary CI run went red, and
ruby#885 pinned that row to its floor. The gate's total is 23,672.

All six claims on this site move: hugo.toml's blurb, the hero trust line, the
conformance card, the stat tile, and the two baseline bullets in list.html.
The blurb and the first bullet also say what happened, because "23,673 →
23,672" with no explanation reads as a regression and it is the opposite: the
number went down so that the gate stops passing on a coin toss.

conformancefigure could not see one of those six. "The sum of those rows is
<b>23,673</b>" puts two words between "sum" and "is", and its vocabulary
spelled out only "the sum is", "its sum is", "whose sum is" — so the line was
never a candidate and the run reported "5 checked" on a page holding six. That
is fixed in go-embedded-ruby/docs; this PR will bump the pinned version in
deploy-pages.yml once the fix is released, so the gate here can refuse the line
it just missed.

Verified locally with the fixed tool: 6 claims checked, 0 disagreements,
against the gate's 23672 at Ruby 4.0.9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Pin the figure gate to v0.7.0, which can see one more line of this page

v0.6.0's vocabulary spelled out "the sum is", "its sum is" and "whose sum is",
so this page's "The sum of those rows is <b>23,673</b>" was never a candidate.
The lane printed "5 total claim(s) checked" and passed, on a page holding six,
one of them stale. go-embedded-ruby/docs#44 widened the pattern; v0.7.0 carries
it.

Run with the exact published command this lane runs, fetched through the proxy
against this tree:

    $ go run github.com/go-embedded-ruby/docs/tools/conformancefigure@v0.7.0 --root .
    go: downloading github.com/go-embedded-ruby/docs v0.7.0
    6 total claim(s) and 1 version claim(s) checked against the gate's 23672 at Ruby 4.0.9 (derived from RUBY_VERSION)

Six, not five, and no disagreement.

The other number that moved was already predicted in the comment this replaces:
"1 version claim … derived from RUBY_VERSION" is go-embedded-ruby/ruby#845
collapsing the two copies of the version into one, and nothing on the page had
to change for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant