Skip to content

The conformance shim has two platform sources that its comment says are one: on the ubuntu lane, 48 linux examples never run and 23 darwin ones do #837

Open

Description

@tannevaled

spec_helper.rb has two platform sources, and its own comment says it has one:

PLATFORMS = [:darwin, :bsd, :unix]          # line 23 — a literal

# mspec/lib/mspec/guards/platform.rb. Only the entry points the corpus reaches
# are provided; PLATFORMS/WORDSIZE above stay the single source of truth so the
# guard helpers and this class cannot disagree.
class PlatformGuard
  PLATFORM = RUBY_PLATFORM                  # line 46 — derived

They can disagree, and on the lane that enforces the gate they do.

Proven, not inferred

internal/vm/kernel_introspect.go derives the triple from the build target:

func rubyPlatformFor(arch, os string) string {
    switch arch {
    case "amd64": arch = "x86_64"
    …
    return arch + "-" + os
}

So on the ubuntu-latest runner rbgo answers RUBY_PLATFORM == "x86_64-linux", and the two sources split:

call reads on ubuntu
PlatformGuard.os?(:linux) PLATFORM.include?("linux") true ✓
platform_is :linux PLATFORMS.include?(:linux) false ✗

The comment is not wrong about the intent — it is written on darwin, where PLATFORM contains darwin and PLATFORMS contains :darwin, so the two agree and the split is invisible.

What it costs, measured on the pinned corpus

2206 spec files in language/ + core/, counted by shape:

guard guards examples files the literal decides
platform_is :linux 31 ≥48 23 skipped — should run
platform_is :darwin 18 ≥23 12 run — should be skipped
platform_is_not :darwin 2 ≥6 2 skipped — should run
platform_is_not :linux 4 ≥5 4 run — should be skipped
platform_is :freebsd / :netbsd / :openbsd 29 ≥43 18 skipped, though :bsd is in the literal

Examples are a lower bound: a static reader cannot expand it_behaves_like.

So on the gate's own runner, roughly 48 examples that describe Linux behaviour never execute, and 23 that describe darwin behaviour run on a Linux kernel. The _not forms invert the same error.

Why it is not visible as a failure

BASELINE was recorded through this same shim, so the gate is self-consistent: it enforces, file by file, a darwin-shaped population measured on Linux. Nothing is red. The number is simply about a different machine than the one producing it.

That also makes it a plausible contributor to the environment sensitivity BASELINE's header documents at length — the five files "measured on darwin and recorded at CI's own LOWER ubuntu value" are exactly where a platform mismatch would show up.

Suggested shape of a fix

PLATFORMS should be derived from RUBY_PLATFORM, the way PlatformGuard::PLATFORM already is, so the comment's claim becomes true. mspec itself derives it; the literal is the shim's own shortcut.

⛔ It moves the population, so it must be measured before BASELINE is regenerated — the same discipline #799 asks for, and for the same reason: UPDATE_BASELINE=1 records whatever it sees and would absorb the loss silently. The two changes interact (both select examples), so measuring them together, or one strictly after the other, is worth deciding deliberately.

Measured 2026-10-27 on corpus 87b1631 (the pinned SPEC_SHA), with a Go census of the guard shapes.

🤖 Generated with Claude Code

Activity

  1. tannevaled commented on Oct 31, 2026

    @tannevaled
    ContributorAuthor

    Why this is not in flight yet, and exactly what it should be when it is

    PLATFORMS = [:darwin, :bsd, :unix] sits three lines below CUR_VERSION in the same file, and #845 is currently rewriting that line. Stacking a second branch on it would be stacking on a squash merge, which orphans the follower — so this waits for #845 to land rather than racing it. Recording the plan here so it is one sitting's work when the file is free.

    The defect, stated precisely

    spec_helper.rb carries two sources for the same fact and uses the wrong one for guards:

    PLATFORM = RUBY_PLATFORM            # line 46 — derived, correct
    PLATFORMS = [:darwin, :bsd, :unix]  # line 23 — hardcoded, and what platform_is reads
    match = args.empty? ? true : args.any? { |s| PLATFORMS.include?(s) }   # ~line 819
    match = args.any? { |s| PLATFORMS.include?(s) }                        # ~line 831

    So on the Linux lane — which is the lane the ratchet runs on — platform_is :linux do … end is false and its examples are removed, while platform_is :darwin is true and asserts darwin behaviour on a Linux host. Both directions are wrong on the machine that produces BASELINE.

    ⛔ Unlike #845, this is NOT a no-op, and that governs the PR's shape

    Deriving moves the population on the Linux lane in both directions at once: :linux blocks start running (examples added, some of which may fail on first contact), :darwin blocks stop running (examples removed). UPDATE_BASELINE=1 would record the net and call it progress — the precise mistake #799 argues against.

    So: move PLATFORMS, do not regenerate BASELINE, and let the ratchet name every file that shifts. A file that loses examples because a platform_is :darwin block correctly stopped running is a legitimate loss and needs recording deliberately, file by file, with a reason — not absorbed into a total.

    The part that is measurable here, on darwin, today

    The mapping does not need a Linux machine to be tested — only the population delta does. Extract it as a function of the platform string rather than computing it inline:

    def _platforms_for(platform)
      fams = [:unix]
      case platform
      when /darwin/        then fams.unshift(:darwin, :bsd)
      when /linux/         then fams.unshift(:linux)
      when /freebsd|openbsd|netbsd|dragonfly/ then fams.unshift(:bsd)
      when /mswin|mingw|cygwin/ then return [:windows]
      when /solaris|illumos/    then fams.unshift(:solaris)
      when /aix/           then fams.unshift(:aix)
      when /wasm/          then return [:wasi]
      end
      fams
    end
    
    PLATFORMS = _platforms_for(RUBY_PLATFORM)

    and then internal/vm/rubyspec_shim_test.go can drive it with "x86_64-linux", "arm64-darwin", "x86_64-freebsd14", "x64-mingw-ucrt" … through the existing runShim helper, on any host. That is the same seam internal/vm/kernel_introspect.go already uses for the same reason:

    // It is a package var so a test can drive the Windows branch on any host: that
    // branch is otherwise unreachable off Windows …
    var rbconfigGOOS = func() string { return runtime.GOOS }

    ⛔ Note what such a test does and does not prove. It proves the mapping; it says nothing about whether the specs behind those guards pass. Treating a green mapping test as evidence about the gate would be the mistake, and the two must be reported separately — the mapping in go test, the population in the ratchet's file-by-file output.

    The list of families must come from mspec, not from guessing. mspec/lib/mspec/guards/platform.rb is the authority for which symbols platform_is accepts and how they relate (:unix as a family containing darwin and the BSDs, :windows excluding it). I have not opened it yet; whoever picks this up should, before writing the case above — the shape I sketched is a sketch.

    Sizing it first

    Before changing anything, count what moves: how many platform_is / platform_is_not sites name :linux, :darwin, :windows and the families, and how many examples sit behind each. That belongs in scripts/conformance/rubyspec/cmd/guards (#836), which already censuses ruby_version_is by shape and prints the sites rather than the totals — platform guards are the same shape of question and the same tool should answer it. #836 should merge first for that reason too.

    🤖 Generated with Claude Code

  2. tannevaled commented on Oct 31, 2026

    @tannevaled
    ContributorAuthor

    ⛔ Correcting the comment I posted above, on two counts

    I said the family list "must come from mspec, not from guessing" and that my _platforms_for sketch was a sketch. I then opened mspec/lib/mspec/guards/platform.rb (at b79c65e). The sketch was wrong in kind, not in detail: mspec has no family taxonomy at all.

    def self.os?(*oses)
      oses.any? do |os|
        raise ":java is not a valid OS" if os == :java
        case os
        when :windows then PLATFORM =~ /(mswin|mingw)/
        when :wsl     then wsl?
        else               PLATFORM.include?(os.to_s)
        end
      end
    end

    A substring test against RUBY_PLATFORM, with :windows the only alias and :wsl the only probe. There is no list, and :unix is not a family containing linux — under mspec, platform_is :unix on "x86_64-linux" is simply false, because "x86_64-linux".include?("unix") is false. Implementing the families I sketched would have made rbgo diverge from the reference while looking more thorough than it.

    So the fix is smaller and faithful: delete PLATFORMS and give platform_is/platform_is_not mspec's predicate, reading RUBY_PLATFORM directly. The shim already has PLATFORM = RUBY_PLATFORM on line 46; nothing else is needed. Confirmed safe to delete the constant: grep -rl PLATFORMS language/ core/ finds no spec that references it — it is purely internal to our shim.

    And my first census was wrong, which is worth recording. I wrote the pattern platform_is_not\? *:[a-z]*, intending "platform_is with an optional _not". In a basic regex \? makes the preceding t optional, so it matches platform_is_no[t] and never plain platform_is. Every number it produced described the negative form alone, and nothing about the output said so — 247 and 219 both looked like totals. Re-run with an explicit alternation, the two forms partition the whole and the sum closes.

    The census, closed

    language/ + core/, corpus at the pinned 87b1631, grep -rnoE "platform_is(_not)? *:[a-z0-9_]+":

    402 sites = 155 platform_is + 247 platform_is_not (and 155 + 247 = 402, which is the check that the partition is a partition).

    platform_is :x sites platform_is_not :x sites
    :windows 95 :windows 219
    :linux 32 :openbsd 9
    :darwin 7 :aix 5
    :aix 3 :linux 4
    :netbsd 6 :freebsd 3
    :freebsd 5 :netbsd 2
    :openbsd 3 :mingw 2
    :bsd 2 :darwin 1
    :android 1 :android 1
    :cygwin 1 :cygwin 1
    155 247

    :unix, :wasi and :wsl appear zero times in either form — so the :unix in today's hardcoded list guards nothing, and the families I was about to add would have been dead weight on top of being wrong.

    What the hardcode actually gets wrong, and where

    With PLATFORMS = [:darwin, :bsd, :unix], on the Linux lane that produces BASELINE:

    sites our shim mspec
    platform_is :linux 32 removed runs ⛔ examples never reached
    platform_is :darwin 7 runs removed ⛔ asserts darwin behaviour on Linux
    platform_is :bsd 2 runs removed ⛔
    platform_is_not :linux 4 runs removed ⛔
    platform_is_not :darwin 1 removed runs ⛔ examples never reached
    everything else 356 — — agrees

    46 of 402 guard sites are answered wrongly on that lane, in both directions. The 32 :linux sites are consistent with the "≥48 examples never run" figure quoted earlier in this session — note those are different units, sites here and examples there, and only a sweep gives the second.

    And on darwin, where a developer runs the suite by hand, the 2 platform_is :bsd sites run where mspec would remove them: "arm64-darwin".include?("bsd") is false. So the hardcode is wrong on both hosts, just much less so on this one.

    Consequences for the PR's shape

    Unchanged from the comment above: move it without regenerating BASELINE, and let the ratchet name every file that shifts. Examples will be both added (the 36 :linux sites) and removed (the 10 :darwin/:bsd ones), and a net figure would hide that.

    Changed: there is no taxonomy to test, so the local test is simpler — drive mspec's predicate with "x86_64-linux", "arm64-darwin", "x64-mingw-ucrt", "x86_64-freebsd14" and assert the same answers os? gives. That still proves only the mapping, never the population; the two belong in separate reports.

    This census was taken with grep and is therefore a site count that cannot see examples, cannot expand it_behaves_like, and counted one of my own patterns wrongly before I noticed. It should be re-derived by scripts/conformance/rubyspec/cmd/guards (#836) as a tested tool, with these numbers as the expected values to reproduce — if it disagrees with the table above, the table is the suspect.

    🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions