Repository navigation
internal/vm, ci: fix the js/wasm build, and make the guard that caught it reachable - #684
Merged
Merged
Conversation
…get bucket GOOS=js GOARCH=wasm has not compiled since #629: errno.go referred to syscall.ENOTRECOVERABLE, syscall.EOWNERDEAD and syscall.ETXTBSY, which Go's js/wasm syscall package does not define. wasip1 defines all three, so the existing wasm lane could not see it. errno.go already had the rule this needs: a known_errors name with no number on this platform is still a constant, bound to Errno::NOERROR, the way MRI's undefined_error does it. The three names were simply in the wrong bucket on one target. They now live in errnoPlatformNames, with the numbers (or their absence) supplied per target by errno_native.go and errno_wasm.go -- the same !(js && wasm) / (js && wasm) file pair this package already uses for jsbridge and servergems. No number is hard-coded; the browser has no MRI to witness one against. The bucket rule is now stated once, at the top of errno.go, with the invariant it exists to protect: the buckets partition known_errors on every target, so a name that leaves one bucket must arrive in another or Errno.constants silently shrinks. TestErrnoBucketsPartitionKnownNames asserts that partition against a literal 158, MRI's known_errors count. That matters: TestErrnoClassRegistration compares the registered constants to len(errnoNumbers)+len(errnoUndefinedNames)+len(errnoAliases), an expectation that shrinks with its subject -- orphaning ETXTBSY in a scratch mutation left it green while the new test failed with "the buckets hold 157 distinct names, want 158". Measured while enumerating rather than fixing the three names given: js/wasm is the ONLY one of darwin/{amd64,arm64}, linux/{amd64,386,arm64,riscv64,loong64, ppc64le,s390x}, windows/{amd64,arm64}, wasip1/wasm and js/wasm that lacks any name errnoNumbers claimed was universal. The comment claiming Go exposes the other known_errors names "only under a build tag" is wrong and is corrected: they are plainly available per-GOOS (57 of 81 on linux/amd64, 56 on windows, 31 on darwin/amd64, 2 on wasip1), so the 0 rbgo reports for them is a larger declared conformance gap than the one macOS EAUTH the comment named. Closing it still needs a per-platform MRI witness and is left to the open per-platform errno issue. Refs #682 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… a lane Two screens failed open around #682 and both are closed here. ci.yml gated wasip1 only. GOOS=js and GOOS=wasip1 are different targets with different syscall packages, so the wasip1 lane stayed green for three days while the browser target did not compile at all. The new `wasm (js, browser)` lane builds ./... for js/wasm -- not just ./cmd/wasm, because the break was three names in internal/vm, a package the playground reaches only transitively -- then vets, builds the playground module and RUNS it. Running it is the part that matters. web/smoke.mjs loads the module into node with Go's own lib/wasm/wasm_exec.js, the same glue web/index.html loads in a browser, waits for main() to publish its entry points and then calls rbgoEval: arithmetic and $stdout capture, a rescued Errno::ENOENT, a syntax error coming back as an error string rather than a crash, and both halves of errno.go's bucket rule live in the browser runtime (Errno::ENOENT::Errno is 2, Errno::ETXTBSY is Errno::NOERROR here, Errno.constants.size is 158). A compile is not a witness for a runtime target. The second screen: three integration tests in cmd/rbgo sat behind `os.Getenv("RBGO_BUILD_IT")` and `grep -rn RBGO_BUILD_IT .github/workflows/` returned nothing. TestClosedWasmBuildIntegration fails on the #629 regression with exactly its three errors -- correct, complete and unreachable. The choice is to wire it in, in its own lane, because those three cover what nothing else does: the nested `go build` the rbgo command runs, that --closed really drops the front-end, and that a baked program executes. MEASURED COST, cold GOCACHE, M-series host (a GitHub runner is slower): build ./... (js/wasm) 14s vet ./cmd/wasm ./cmd/rbgo ./internal/vm (js/wasm) 18s ./cmd/wasm module 96.9 MB ./cmd/rbgo module 98.4 MB warm rebuild of the module 1s all three RBGO_BUILD_IT tests 35s Both lanes together are minutes, none of it -race-priced, and they run in parallel with the existing ones -- next to the 20-90 min -race coverage lane the addition is small. Being its own lane rather than a step in `test` keeps it off that lane's critical path. Proof the new lane is a gate: `CGO_ENABLED=0 GOOS=js GOARCH=wasm go build ./...` run in a detached worktree at origin/main (2aa1c94, before this branch) fails with the three undefined-symbol errors. A gate nobody has seen fail is not known to be a gate. TestClosedWasmBuildIntegration's docstring claimed the baked program's JS.log line "ran in a headless browser when one is available". No version of that function ever ran anything -- it read the module's first four bytes and scanned it for symbols. It now really runs the module under node with the toolchain's wasm_exec_node.js and asserts both JS.log lines reach the console, including that the absent `document` global comes back as a value rather than trapping. node is a hard requirement of RBGO_BUILD_IT=1 and a missing node fails the test: a check that steps aside when its host is absent is the exact failure this lane exists to correct. Refs #682 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…and the leak
Two things came out of actually RUNNING what RBGO_BUILD_IT was hiding.
The closed wasm module aborts after its program finishes, and that is by design:
closed_main_wasm.go ends in select{} to keep the runtime alive for JS event and
animation callbacks, and the baked test program registers none, so Go's deadlock
detector fires once every goroutine is parked. The test now asserts that REASON
rather than tolerating any failure -- "all goroutines are asleep - deadlock!"
after both JS.log lines reached the console is the documented behaviour; a trap, a
link error or a Ruby exception still fails.
The second is the finding: with RBGO_BUILD_IT on, both closed-world tests fail
their "the front-end must not be linked" assertion, on this branch and identically
on the commit it branched from (verified in a detached worktree at origin/main,
unmodified). `rbgo build --closed` announces "front-end dropped (no
lexer/parser/compiler linked)" and produces a 172.9 MiB binary holding 255
github.com/go-ruby-parser/parser.(*Parser).* text symbols.
The cause is one edge: internal/vm/rubocop.go and rubocop_bind.go carry no build
tags, and github.com/go-ruby-rubocop/rubocop imports the parser, so the front-end
re-enters the closed dependency graph through the RuboCop binding --
`go list -deps -tags rbgo_closed ./cmd/rbgo` names it and nothing else.
That fix is not here. It is a scope decision about what --closed supports (does a
closed build still answer `require "rubocop"`, or degrade to a LoadError as the
network backends do on wasm?) and it touches internal/vm. The lane says so in
place, so the next reader meets the diagnosis rather than a red tick. No assertion
was weakened to make it green: a check nobody runs is documentation, and the point
of turning the var on was to stop that.
Refs #682
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repo pins checkout and setup-go at v7 under a "track the latest stable" convention. actions/setup-node latest major is v7 too; v4 was three majors stale on arrival. Caught by reading the action back with `gh api repos/actions/setup-node/git/matching-refs/tags/v` rather than assuming the major a different action happens to be on. Refs #682 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lane comments carried a laptop number and the word "budget". The build-integration lane has now run on ubuntu-latest: 141s of test time, 5.3 min for the job, against 35s cold on an M-series host -- the runner is 4.0x slower, which is the figure to reuse rather than the guess. The useful frame is not "cheap" but where it lands among lanes this file already pays for on the same commit: wasm (wasip1) 2.0 min, rbgo build integration 5.3 min, arch arm64 5.9 min, arch amd64 9.4 min. The new lane is cheaper than a lane already on every PR. wasm (js, browser) has not had a runner of its own yet, so the comment says that instead of inventing a number for it. Also records that the lane fails on Linux for EXACTLY the one predicted reason: TestBuildIntegration, TestBuildEnv and TestRunSendsWarningsToTheRealStderr pass, the node run of the closed wasm module passes, and the only failing lines are the two "still links the front-end" assertions. The leak is platform-independent. Refs #682 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the file The lane comment said it had not yet had a runner of its own to time. It has now: 1.9 min end to end on ubuntu-latest with all nine smoke checks green, which makes it the cheapest lane here -- below the wasm (wasip1) lane beside it at 2.0 min. No estimate is left in either new lane comment. The runner log also carries the part that matters more than the timing: the bucket rule witnessed inside a JS host ON LINUX, not just on a laptop -- Errno::ETXTBSY::Errno is 0 there, Errno::ETXTBSY.equal?(Errno::NOERROR) is true, Errno.constants.size is 158. That is the half of #682 no compile-only check could have reached. Refs #682 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`rbgo build --closed` announced "front-end dropped (no lexer/parser/compiler linked)" and then linked the parser anyway. internal/vm/rubocop.go and rubocop_bind.go carried no build tags, and github.com/go-ruby-rubocop/rubocop imports github.com/go-ruby-parser/parser -- a linter needs an AST to inspect -- so the front-end re-entered the closed dependency graph through the RuboCop binding. Both closed-build integration tests asserted against it and both failed, which is what wiring RBGO_BUILD_IT to a CI lane finally made visible (#682). The reasoning for dropping rather than keeping: --closed already promises no front-end, and a lint needs a parser, so a linter has no business in a binary that claims to have dropped one. There is also no useful subset of RuboCop to keep, so this takes the LoadError degradation of backends_wasm.go rather than the graceful-module one of bindings_wasm.go: `require "rubocop"` fails exactly as a missing gem would. rubocop.go, rubocop_bind.go -> //go:build !rbgo_closed rubocop_test.go, rubocop_internal_test.go -> the same, since they assert the open build's behaviour rubocop_closed.go (rbgo_closed) -> the registerRuboCop stub builtins.go calls, plus hookClosedUnavailable rubocop_unavailable.go (no tag) -> the roster and the raise, tag-free so a NATIVE test can assert the LoadError without a closed-world build builtins.go needs no tag: the stub supplies the symbol it calls, exactly as backends_wasm.go does for registerGRPC and the rest. The four wrapper types are redeclared as empty shells in the closed file. They are not vestigial: object_model.go's classOf has a case arm per wrapper type in a file with no build tag, so the names must resolve for the closed build to compile. No value of them can be constructed there, so the arms are unreachable -- the same shape as arSQLiteAdapter in bindings_wasm.go. MEASURED, on a --closed binary built from the same one-line program: before after parser.(*Parser).* T syms 98 0 any go-ruby-parser sym 255 0 any go-ruby-rubocop sym n/a 0 internal/compiler sym n/a 0 binary 172.9 MiB 172.2 MiB The symbol counts are over 173,293 nm lines, and the same grep finds 15,412 internal/vm symbols in that input -- so the zeros are zeros, not a grep that could not run. `go list -deps -tags rbgo_closed ./cmd/rbgo` now names no parser, no rubocop and no compiler, so rubocop was the only edge. AND A CORRECTION THAT MATTERS: the size barely moved, and not because another edge remains. The front-end is simply not what makes this binary large. An ordinary (non-closed) rbgo is 173.7 MiB, so dropping the whole front-end saves 1.5 MiB -- 0.85%. Earlier notes on this, including mine, cited "172.9 MiB carrying 255 parser symbols" as if the size were the symptom. It was not: the symbols were the symptom and the size is a separate, much larger question about the gem bindings. The integration test's "closed should be smaller than open" assertion holds, but it was never measuring much. Both directions verified, because the risk of a build tag is breaking the ordinary path silently: normal build: require "rubocop" succeeds, RuboCop::Runner#inspect returns an Array, RuboCop::Cop::Offense is a Class -- unchanged closed build: LoadError "cannot load such file -- rubocop (not available in the closed-world build)", a retried require re-raises, and the program keeps running All five RBGO_BUILD_IT tests now pass. No assertion was rewritten to accept the 255 symbols; the product changed, which is the point. Refs #682 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TestNetHTTPThroughRbgoBinary and TestSocketThroughRbgoBinary each built cmd/rbgo
and, if the build failed, called t.Skipf("cannot build cmd/rbgo"). A compile break
in cmd/rbgo therefore reported SKIP, which reads as green. That is the defect class
of issue #682: the guard that caught a three-day js/wasm compile break was itself
unreachable, and these two would have swallowed the next one.
Both now t.Fatalf.
The socket test's skip was justified in its own comment by "the unrelated
pre-existing cmd/rbgo codegen issue". That issue no longer exists -- cmd/rbgo
builds -- so the premise had expired and left behind a skip that protected nothing
and hid everything. The nethttp test cited only "mirroring the socket suite", which
means the reasoning was never independently held there.
Refs #682
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 26, 2026
tannevaled
added a commit
that referenced
this pull request
Oct 9, 2026
…o it does not (#801) * README: drop two limitations the repository already disproved, add two it does not "What does not work yet" promised a dated re-check and got one. Two entries no longer reproduce, and one of them had not for weeks: GOOS=js GOARCH=wasm go build ./cmd/rbgo exit 0 GOOS=js GOARCH=wasm go build ./cmd/wasm exit 0 rbgo -e '$stdout.print "O"; $stderr.print "E"' -> O on stdout, E on stderr The browser one is the worse of the two. #684 fixed the three missing syscall constants AND added the lane, so `wasm (js, browser)` has been building that target green on every pull request while the README said it did not build and had no lane -- in two separate places, one of them the Platforms table, which omitted the lane entirely. The answer was in the tree the whole time. readme_wasm_lane_test.go closes that class: if ci.yml builds the target and the README says it cannot, the test fails and names both. It is narrow on purpose -- it does not try to validate the whole section against reality, only the claims that files already in the repository can settle. Proved in both directions, and it caught the first draft of this very commit, which quoted the old sentence back verbatim. Two entries added, both measured against MRI 4.0.7 on this host, and both fail-open rather than merely missing: Timeout.timeout(0.3) { sleep 2 } returns after 2s, no error (MRI raises at 0.3s) `sleep 1` freezes every other Ruby Thread (MRI: they run) Timeout is stated as a paragraph rather than a table row because of the shape: it is the ordinary Ruby way to bound work that might hang, and it returns successfully, so code relying on it is unprotected and nothing says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * README: two more rows re-measured, and one of them was understating a crash Both rows in the "Otherwise" table were re-checked against MRI 4.0.7 along with the rest of that section. Every other row still reproduces; these two do not. # encoding: ascii-8bit rbgo prints ASCII-8BIT, same as MRI -- row removed BEGIN { puts "B" } rbgo prints B then main -- works The BEGIN/END row was the one worth catching. It said both "do not parse", which reads as an ordinary gap: the feature is unavailable, use something else. What actually happens now is that BEGIN works and END parses, registers, lets the program finish, and then kills the process with an uncontained Go panic: $ echo 'END { puts "E" }' > p.rb && rbgo p.rb panic: runtime error: slice bounds out of range [:1] with capacity 0 $ echo $? 2 For an embedder that is worse than a gap: ruby.Run does not return an error, it takes the host process down. Filed as #805 with the isolation -- at_exit and BEGIN are fine, the ISeq and its environment disagree about the block's locals, and the repanic in Run is deliberate policy rather than the defect. A stale note that promises LESS than the code delivers is a nuisance. One that promises a missing feature where the code delivers a process death points the reader the wrong way about the risk, which is why this row is now explicit about what happens rather than about what is absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
tannevaled
added a commit
that referenced
this pull request
Oct 11, 2026
…813) The go-ndarray playground shows each pipeline as Go | Ruby | NumPy and runs the Ruby column in the browser through cmd/wasm. This implements the API its code generator emits (ruby-ndarray-contract.md): - creation: arange(stop | start, stop | start, stop, step), linspace, eye(n, m, k), zeros()/ones() (0-d with no shape, or a shape Array), full(value, *shape) - views: reshape (with -1), T, squeeze(*axes), expand_dims, dup/copy (copy of the data), and NumPy basic indexing through a[...]: Integer, true for ':', a Range (exclusive, inclusive, endless, beginless) or an Enumerator::ArithmeticSequence ((0..) % 2, (-1..) % -1, (5...1) % -2) - elementwise: square, ** (scalar, array with broadcasting, and 2 ** a through NDArray#coerce), clip, NDArray.maximum/minimum - reductions: sum/mean/max/min/prod/argmax/argmin with (axis, keepdims:), cumsum - products and joins: NDArray.outer, NDArray.concatenate/stack(axis:) - p/inspect/to_s print go-ndarray's NumPy-style Repr Every method now checks its arguments: a wrong count raises ArgumentError, a wrong type TypeError, an index or axis out of range IndexError, other go-ndarray errors ArgumentError with go-ndarray's message. NDArray.arange(5) and NDArray.full() used to raise "ArgumentError: arange: runtime error: index out of range [1] with length 1", a recovered Go panic. TestNativeFaultBecomesRubyError relied on that bug; it now plants its own faulting native. go-ndarray v0.3.0 -> v0.11.0 (and go-fft v0.1.14 -> v0.23.0 with it), outside the grouped Renovate PR #792. The README no longer says the browser target does not build (it has since #684), in three places; readme_wasm_lane_test.go now catches that wording across line breaks. web/smoke.mjs runs an NDArray program in the JS host. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Fixes #682.
Part 1 — the build
errno.goreferred tosyscall.ENOTRECOVERABLE,syscall.EOWNERDEADandsyscall.ETXTBSY, which Go's js/wasmsyscallpackage does not define. wasip1defines all three, which is why the existing wasm lane never saw it.
The file already had the rule this needed. MRI's
Init_syserrtreats aknown_errorsname the platform does not define as a constant namingErrno::NOERROR(undefined_error), anderrno.goreproduced that in threebuckets. The three names were simply in the wrong bucket on one target. They
now live in
errnoPlatformNames, with numbers (or their absence) supplied pertarget by
errno_native.go/errno_wasm.go— the same!(js && wasm)/(js && wasm)file pair this package already uses forjsbridgeandservergems. No number is hard-coded: the browser has no MRI to witness oneagainst, and the rule is that a name without a number is a
NOERRORconstant,not a name with a number we invented.
The bucket rule is now stated once, at the top of
errno.go, together with theinvariant it protects: the buckets partition
known_errorson every target, so aname that leaves one bucket must arrive in another or
Errno.constantssilentlyshrinks.
The enumeration — per-target constant availability
I enumerated rather than fixing the three names I was handed. A probe generates a
package referencing every name in the table, compiles it with
-gcflags=-e(reportall errors, not the first ten) for each target, and reads the
undefined: syscall.Xlines back. The name list is parsed out oferrno.goitself, so the probe cannot drift from the table it judges.
Pass 1 — the 76 syscall-sourced names
errnoNumbersclaimed every targetdefines (its 77th entry,
NOERROR, is the literal 0):So the answer to "are there others?" is no — js/wasm is the only target that
breaks the premise, and those are exactly the three. Twelve targets where nothing
was wrong is part of the result.
Pass 2 — the 81 names the table assumes are absent everywhere. This one
contradicts the file's own comment, which said Go "exposes the rest only under a
build tag, so from rbgo's point of view they are undefined names". They are not
behind a build tag; they are plainly available per-GOOS:
EBADFD,ENOTCAPABLE)rbgo reports
0for all of them. That is a declared conformance gap, not abug introduced here — but it is much larger than the one macOS
EAUTHat 80 thecomment named, and closing it needs a per-platform MRI witness per number. The
comment is corrected to say what is true; the gap is left to the open per-platform
errno issue.
EWOULDBLOCKis the one genuinely per-platform alias, and theprobe shows why: wasip1 is the only target that lacks it, js/wasm included.
Two incidental findings from the same sweep:
linux/386does not build, for a reason unrelated to errno:go-ruby-chronic/chronic@v0.0.0-20260916091614has1_000_000_000_000in a struct literal typedint, which overflows on a 32-bittarget. Not in
release.yml's target list, so not a supported target — flagged,not touched (it is a dependency, outside this cluster).
syscall.EWOULDBLOCKandsyscall.EAGAINare separately defined on everytarget but wasip1, so
errnoAliasesis load-bearing for exactly one target.Part 2 — the guard that could not run
The js/wasm lane
GOOS=jsandGOOS=wasip1are different targets with differentsyscallpackages. The
wasm (wasip1)lane stayed green for three days while the browsertarget did not compile at all: one wasm lane is not wasm coverage. The new
wasm (js, browser)lane builds./...— not just./cmd/wasm, because the breakwas in
internal/vm, which the playground reaches only transitively — vets, buildsthe playground module, and then runs it.
web/smoke.mjsloads the module into node with Go's ownlib/wasm/wasm_exec.js,the same glue
web/index.htmlloads in a browser, waits formain()to publishits entry points, and calls
rbgoEval: arithmetic and$stdoutcapture, a rescuedErrno::ENOENT, a syntax error coming back as an error string rather than a crash,and both halves of the bucket rule live in the browser runtime —
Errno::ENOENT::Errnois 2,Errno::ETXTBSYisErrno::NOERRORhere, andErrno.constants.sizeis 158. A compile is not a witness for a runtime target.The lane passed on a GitHub
ubuntu-latestrunner, all nine smoke checks, withthe bucket rule witnessed inside a JS host on Linux:
That is the half of #682 no compile-only check could have reached, and it is now
reached on the platform CI runs on rather than only on my laptop.
Proof the lane is a gate
CGO_ENABLED=0 GOOS=js GOARCH=wasm go build ./...run in a detached worktree atorigin/main(2aa1c94, the commit this branched from, unmodified) fails with:A gate nobody has seen fail is not known to be a gate.
The
RBGO_BUILD_ITdecision: wire it inChoice: wire it in, in its own lane. The three tests behind it cover what
nothing else does — the nested
go buildtherbgocommand performs, that--closedreally drops the front-end, and that a baked program executes. Deletingthe claim would have thrown that away; leaving it as it was is documentation
pretending to be a check.
Measured cost (cold
GOCACHE, M-series host; a GitHub runner is slower):go build ./...(js/wasm)go vet ./cmd/wasm ./cmd/rbgo ./internal/vm(js/wasm)./cmd/wasmmodule./cmd/rbgomoduleRBGO_BUILD_ITtestsAnd now measured on a runner, not extrapolated. The
build-integrationlaneran on
ubuntu-latest: 141s of test time, 5.3 min for the whole job. Thehonest frame is not "cheap in the abstract" but where it lands among lanes this
file already pays for, on the same commit:
wasm (js, browser)wasm (wasip1)rbgo build integrationarch arm64arch amd64test (…)with-race+ coverageBoth figures are from
ubuntu-latest, not extrapolated. The js/wasm lane is thecheapest lane in the file, and the integration lane costs less than
arch arm64, which is already on every PR. The runner is 4.0× slower than my host(141s vs 35s) — the number to reuse next time someone extrapolates CI cost from a
laptop. Keeping the integration tests in their own lane is about staying off the
coverage lane's critical path, not about price.
What turning it on revealed — and the fix for it
Turning the env var on exposed a pre-existing defect it had been hiding, on this
branch and identically on
origin/main:rbgo build --closedprinted "closed-world — front-end dropped (nolexer/parser/compiler linked)" and then linked the parser anyway. Cause, one edge:
internal/vm/rubocop.goandrubocop_bind.gocarried no build tags, andgithub.com/go-ruby-rubocop/rubocopimports the parser — a linter needs an AST toinspect — so the front-end re-entered the closed dependency graph through the
RuboCop binding.
Fixed by dropping the binding from closed builds, on the reasoning that
--closedalready promises no front-end and a lint needs a parser, so a linter hasno business in a binary that claims to have dropped one. There is no useful subset
of RuboCop to keep, so this takes the
LoadErrordegradation ofbackends_wasm.go, not the graceful-module one ofbindings_wasm.go.rubocop.go,rubocop_bind.go//go:build !rbgo_closedrubocop_test.go,rubocop_internal_test.gorubocop_closed.go(rbgo_closed)registerRuboCopstubbuiltins.gocalls, plushookClosedUnavailablerubocop_unavailable.go(no tag)LoadErrorwithout a closed-world buildbuiltins.goneeds no tag: the stub supplies the symbol it calls, exactly asbackends_wasm.godoes forregisterGRPCand the rest. The four wrapper types areredeclared as empty shells in the closed file — not vestigial:
object_model.go'sclassOfhas a case arm per wrapper type in an untagged file, so the names mustresolve for the closed build to compile. No value of them can be constructed there,
so the arms are unreachable; same shape as
arSQLiteAdapterinbindings_wasm.go.Measured on a
--closedbinary built from the same one-line program:T …parser.(*Parser).symbolsgo-ruby-parser/parsersymbolgo-ruby-rubocopsymbolinternal/compilersymbolThe counts are over 173,293
nmlines, and the same grep finds 15,412internal/vmsymbols in that input — so the zeros are zeros, not a grep that couldnot run.
go list -deps -tags rbgo_closed ./cmd/rbgonow names no parser, norubocop and no compiler: rubocop was the only edge.
A correction, because the size barely moved
It moved 0.7 MiB, and not because another edge remains — the dep graph and the
symbol table both say zero. The front-end is simply not what makes this binary
large. An ordinary non-closed
rbgois 173.7 MiB, so dropping the entirefront-end saves 1.5 MiB, or 0.85%.
Earlier notes on this defect, mine included, cited "172.9 MiB carrying 255
parser symbols" as though the size were the symptom. It was not: the symbols were
the symptom, and the size is a separate and much larger question about the gem
bindings. The integration test's "closed should be smaller than open" assertion
holds, but it was never measuring much. Reported rather than acted on.
Both directions verified
The risk of a build tag is silently breaking the ordinary path, so both were run:
require "rubocop"succeeds,RuboCop::Runner#inspectreturnsan
Array,RuboCop::Cop::Offenseis aClass. Unchanged.LoadError: cannot load such file -- rubocop (not available in the closed-world build), a retriedrequirere-raises rather than returningfalse, and the program keeps running. Closed binaries still execute blocks,
ranges and
Errno::ENOENTcorrectly with no front-end linked.All five
RBGO_BUILD_ITtests now pass. No assertion was rewritten to acceptthe 255 symbols — the product changed, which was the point.
The duplication this creates, and its guard
raiseClosedUnavailableis a second copy ofraiseWasmUnavailable's rule, differingonly in the reason named, because merging them means editing
backends_unavailable.go, which this change was not scoped to touch.TestClosedAndWasmUnavailableAgreeOnShapepins the two to each other — it takes thewasm message as the reference, swaps only the parenthesised reason, and requires the
closed message to match, so changing either format string alone fails the test.
Verified by mutation: it does. A third condition should collapse all three.
Two skips that turned a build failure into a pass
From the same sweep, and the same defect class as #682:
TestNetHTTPThroughRbgoBinaryandTestSocketThroughRbgoBinaryeach builtcmd/rbgoand reported skip — which reads as green — if it would not compile.Both are now
t.Fatalf. The socket one justified itself in its own comment by "theunrelated pre-existing cmd/rbgo codegen issue", which no longer exists: the premise
had expired and left a skip that protected nothing and hid everything. The nethttp
one cited only "mirroring the socket suite", so its reasoning was never
independently held.
TestClosedWasmBuildIntegrationnow runs what it buildsIts docstring said the baked program's
JS.logline "ran in a headless browserwhen one is available". No version of that function ever ran anything — it read the
module's first four bytes and scanned it for symbols. It now runs the module under
node with the toolchain's
wasm_exec_node.jsand asserts bothJS.loglines reachthe console, including that the absent
documentglobal comes back as a valuerather than trapping. node is a hard requirement of
RBGO_BUILD_IT=1, not areason to skip: a check that steps aside when its host is absent is the failure
this lane exists to correct.
The module exits non-zero and that is by design —
closed_main_wasm.goends inselect{}to keep the runtime alive for JS callbacks, and this program registersnone, so Go's deadlock detector fires once every goroutine parks. The test asserts
that reason, so a trap, a link error or a Ruby exception still fails. Worth
knowing separately: a
--closedwasm app that merely prints will showfatal error: all goroutines are asleep - deadlock!in a browser console too,after its output. Reported, not changed.
The rest of the suite: every env-var gate and skip, against what workflows set
The reusable half of the question.
RBGO_BUILD_ITis the only environmentvariable gating any test in this repo — there is no second one to find. But the
sweep turned up the same failure shape elsewhere, which is the part worth
keeping:
1. Env-var gates (1 variable, 3 tests) —
RBGO_BUILD_IT:TestBuildIntegration,TestClosedBuildIntegration,TestClosedWasmBuildIntegration. Set by no workflow before this PR; set by the newbuild-integrationlane now.2. A build failure that becomes a PASS (2 tests) — the most dangerous items
found, and the same shape as #682:
in
internal/vm/nethttp_bind_test.go:387(TestNetHTTPThroughRbgoBinary) andinternal/vm/socket_test.go:424(TestSocketThroughRbgoBinary). Ifcmd/rbgostopped compiling, both would report skip, i.e. green. Not changed here —
internal/vm/*_test.gooutside this cluster — but they should becomet.Fatalf.3.
-tags rbgo_closedis compiled by no workflow at all.grep -rn -- -tags .github/workflows/is empty, so nothing in CI ever builds the closed side of thetree;
internal/vm/prelude_test.gois//go:build !rbgo_closed, and itsrbgo_closedcounterpart is exercised only through theRBGO_BUILD_ITtests. Thenew lane is therefore the only CI path that compiles that configuration at all.
4.
testing.Short()gates (3 tests) —TestNetHTTPThroughRbgoBinary,TestNetHTTPOpenTimeout,TestSocketThroughRbgoBinary. No workflow passes-short, so these do run in CI. The inverse case, listed so it is notrediscovered.
5. Capability-probe skips (11), which pass silently when the capability is
absent — symlink creation (
dir_glob_wave16_internal_test.go:65,97,filestat_internal_test.go:139,wave19_filedir_internal_test.go:115,292),/dev/zeroandmkfifo(io_nonregular_internal_test.go:31,51),user.Current()(
file_path_internal_test.go:195), andos.Geteuid() == 0(
wave28_open_paths_internal_test.go:137,wave19_filedir_internal_test.go:407).All of them are satisfied on the GitHub POSIX runners today, so they run — but none
of them would say so if that changed.
6. Platform skips (~24) —
runtime.GOOS-conditioned, all three OS lanes exist,so each branch is reached by some lane. These are genuinely fine.
7. Two conformance "gap recorded" skips —
scripts/conformance/webapp/webapp_test.go:131,177turn a missing Sinatra /ActiveRecord binding into a skip. That package is in
./..., so thetestlane runs it; the skips are deliberate gap markers, but a regression that removed a
binding would present as a skip rather than a failure.
8. One skip that can never fire —
grape_internal_test.go:171testsr.handler != niltwo lines after settinghandler: nil. Harmless, dead.Verification
CGO_ENABLED=0 GOOS=js GOARCH=wasm go build ./...— green, and the moduleruns under node via
web/smoke.mjs(9 checks).CGO_ENABLED=0 GOOS=wasip1 GOARCH=wasm go build ./...— green, and the modulestill runs under
wazero(the runtime the existing lane uses), which givesthe bucket rule its two-sided witness — the same name, the same build, two
targets:
Errno::ETXTBSY::ErrnoErrno.constants.sizeErrno::NOERROR, no number here)A single table would have had to be wrong on one of those two.
Cross-builds green for darwin/{amd64,arm64},
linux/{amd64,arm64,riscv64,loong64,ppc64le,s390x}, windows/{amd64,arm64},
wasip1/wasm, js/wasm.
gofmt -l internal/ cmd/empty.100% coverage: the below-100 function LISTS are identical to
origin/main's.Same
-coverpkg=$(go list ./internal/... | paste -sd, -)profile, on a detachedworktree at
origin/main(2aa1c94) and on this branch, each profile resolved withgo tool cover -funcfrom its own worktree. Both totals read 99.9% on thishost (the rounding the gate hides), so totals were not the comparison: 16
functions below 100% on each side, the same 16, at the same percentages —
arith.go:{binary,rubyEql,valueEqlRec},bigdecimal_bind.go:Inspect,builtins.go:{bootstrap,cloneSingleton},complex.go:registerNumericGeneric,converter_codec.go:{encodeCharTo,xtextRuneStep},date_bind.go:Inspect,io.go:inspectStr,nethttp.go:registerNetHTTP,object.go:repr,object_model.go:{classOf,defaultNegation,objSingleton}.No orphaning, checked against the FULL measured set and not just the below-100
list. This matters because the below-100 diff cannot see orphaning at all: a
function that leaves the profile entirely appears in neither list, so that diff
stays clean while code silently stops being measured. The full set went 7293 →
7295, and the diff is exactly two additions with zero removals:
errno.go:mergeErrnoNumbersandrubocop_unavailable.go:raiseClosedUnavailable,both at 100%.
Two build tags were added in this PR and neither moved code out of the measured
set, by design.
!rbgo_closedis compiled on every ordinary lane, so all 19pre-existing rubocop functions stay measured (19 → 20 with the new raise).
errno_wasm.goandrubocop_closed.gocarry only the tagged-out halves; theformer holds data and no functions at all, and the latter's stubs are unmeasured on
POSIX lanes exactly as
backends_wasm.go's have always been — stated plainlyrather than glossed. It is also why
mergeErrnoNumberstakes its tables asarguments: the js-shaped case (an empty platform table) is then covered by a POSIX
lane instead of by a lane that cannot run this package's tests.
CGO_ENABLED=0 GOWORK=off go test ./...green (full suite, 8 packages, both trees).A first attempt at this comparison was wrong and is worth recording: running
go tool cover -funcon the baseline profile from the branch worktreeresolved its line numbers against the branch's sources and invented three
errno.gofunctions at 0.0%. The judge and the subject have to read the samesource.
The new guard was seen to fail: orphaning
ETXTBSYfrom both per-target varsin a scratch mutation gave
the buckets hold 157 distinct names, want MRI's known_errors count 158— whileTestErrnoClassRegistrationstayed green, because it compares the registeredconstants against the tables' own lengths, an expectation that shrinks with its
subject. That is why the new test pins a literal 158.
🤖 Generated with Claude Code