Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 38 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -623,43 +623,47 @@ throughput tracks the underlying driver rather than the interpreter:

### What does not work yet

Every entry below was re-checked on `3e8e3cc` (darwin/arm64, 2026-09-26) by running
it against **MRI 4.0.5 on the same host**. Entries that no longer reproduced have
been removed.

**The browser WebAssembly target does not currently build.** `GOOS=js GOARCH=wasm`
fails to compile, because `internal/vm/errno.go` references three `syscall`
constants that the `js` port does not define:
Every entry below was re-checked on `fc8ca1a` (darwin/arm64, 2026-10-08) by running
it against **MRI 4.0.7 on the same host**. Entries that no longer reproduced have
been removed — two were, on that pass:

- The two paragraphs saying the **browser WebAssembly target** was broken and
ungated. Both were fixed by #684, which closed the three missing `syscall`
constants **and** added the gate, so this file was describing a gap that a
green lane in the same repository had been contradicting on every pull
request. `readme_wasm_lane_test.go` now fails if the two disagree again —
including, as it happens, if this very note quotes the old wording back.
- *"`$stderr` is not a separate stream"*. The VM takes two writers
(`vm.NewWithStderr`), and `rbgo -e '$stdout.print "O"; $stderr.print "E"'` now
sends one byte to each; `warn` goes to stderr. Covered by
`internal/vm/diagnostic_stream_test.go`.

**`Timeout.timeout` never times out.** The module and `Timeout::Error` exist and
`rescue Timeout::Error` resolves, but the block runs to completion and its value
is returned: enforcing a deadline needs pre-emptive interruption of a running
block, which the scheduler does not do yet.

```console
$ CGO_ENABLED=0 GOOS=js GOARCH=wasm go build ./cmd/rbgo
# github.com/go-embedded-ruby/ruby/internal/vm
internal/vm/errno.go:101:35: undefined: syscall.ENOTRECOVERABLE
internal/vm/errno.go:108:35: undefined: syscall.EOWNERDEAD
internal/vm/errno.go:120:35: undefined: syscall.ETXTBSY
$ rbgo -e 'require "timeout"; Timeout.timeout(0.3) { sleep 2 }; puts "returned"'
returned # MRI 4.0.7: Timeout::Error after 0.3s
```

This affects the playground (`./cmd/wasm`) and the closed-world browser build
(`rbgo build --closed --target wasm`) alike, since both link `internal/vm`. It is a
recent regression — the same build succeeded at `68cb53a`, the commit before the
Errno subsystem landed — and no CI lane catches it, because only the **`wasip1`**
wasm target is gated. **`GOOS=wasip1 GOARCH=wasm` does build and run** (see
*Platforms*), so server-side/WASI WebAssembly is unaffected.
This one **fails open**, which is why it is stated here rather than left in the
table below. `Timeout.timeout` is the ordinary Ruby way to bound work that might
hang, so code relying on it for that is unprotected under rbgo — and unprotected
*silently*, because the call returns successfully. The block is also yielded
`nil` where MRI yields the limit.

**`$stderr` is not a separate stream.** The VM is constructed with a single output
writer, and `$stderr`/`STDERR` are wired to it, so anything a Ruby program sends to
`$stderr` — including `warn` — arrives on **stdout**:
**A backtick command holds the interpreter lock.** While `` `cmd` `` runs, every
other Ruby `Thread` is stopped; under MRI they keep running. `Kernel#sleep` does
yield, so this is specific to the subprocess path:

```console
$ rbgo -e '$stdout.print "O"; $stderr.print "E"' >out.txt 2>err.txt
$ cat out.txt; echo "[err: $(cat err.txt)]"
OE[err: ]
$ rbgo -e 't = Thread.new { loop { $n = ($n||0)+1; sleep 0.01 } }; sleep 0.05
b = $n; `sleep 1`; puts($n > b ? "ran" : "frozen")'
frozen # MRI 4.0.7: ran
```

Uncaught exceptions are the exception to this: the CLI prints those to real stderr
itself. Redirecting `2>` to separate diagnostics from program output therefore does
not work as it does under MRI.

Otherwise:

| | how it differs from MRI 4.0.5 |
Expand All @@ -669,11 +673,10 @@ Otherwise:
| `File::Stat#==` | `a == b` is `false` for two stats of the same file (MRI: `true`); `a.==(b)` answers `true` |
| `File#stat` on a file unlinked while open | raises `Errno::ENOENT`; MRI answers from the open descriptor. rbgo's streams are buffered by path and hold no descriptor |
| `Errno` | all **158** of MRI's constant names are present, but **32** report errno `0` where MRI has a real number (`EAUTH`, `EBADRPC`, `EDEVERR`, …). Measured by set difference on darwin: 81 names are zero-valued under rbgo, 49 under MRI, and every MRI zero is also zero under rbgo. Go's portable `syscall` package does not expose the platform-only names |
| magic encoding comments | `# encoding: ascii-8bit` is not honoured — a literal still reports `UTF-8`, where MRI reports `ASCII-8BIT` |
| `pp` | neither `Kernel#pp` nor `require "pp"` exists (`require "prettyprint"` does work) |
| `Process.fork` | does not exist — Go's runtime cannot be forked safely |
| `Kernel#fork` | **exists, and does not fork.** `fork { ... }` runs the block **in the same process**, so the “child” mutates the parent's globals, `ENV` and working directory, and returns a synthetic pid (`100001`, counting up) while `Process.pid` is unchanged. Under MRI the block runs in a real child and the parent is untouched. A `SystemExit` raised inside the block unwinds the whole program |
| `BEGIN { }` / `END { }` | do not parse (`parse error: unexpected "{" after statement`). Everything else the front-end was known to refuse now parses — see [go-ruby-parser](https://github.com/go-ruby-parser/parser) |
| `END { }` | **crashes the process** with a Go panic when the handler runs at exit — `panic: slice bounds out of range [:1] with capacity 0`, exit 2, where MRI runs the block. For an embedder it takes the host down rather than returning an error from `Run`. Tracked as #805. `BEGIN { }` works. Both used to be refused by the front end, which is how this row came to understate it: a gap turned into a crash and the note still described the gap |
| `Thread#backtrace` for another thread | raises `NotImplementedError`; rbgo keeps one frame stack per VM, not per thread |

## Platforms
Expand All @@ -687,14 +690,17 @@ Otherwise:
| **linux, macOS, windows** | the full suite under `-race`. The **100 % coverage gate** is enforced on linux and macOS only — a few POSIX-only paths (opening `/dev/null` as a character device) are unreachable on windows, so its coverage is structurally below 100 % |
| **amd64, arm64** | `go test ./...` on native runners |
| **wasip1/wasm** | built with `CGO_ENABLED=0 GOOS=wasip1 GOARCH=wasm` and **run** under [wazero](https://wazero.io), a pure-Go runtime — verified here: `wazero run rbgo.wasm -e 'puts (1..10).sum'` prints `55` |
| **js/wasm (browser)** | `CGO_ENABLED=0 GOOS=js GOARCH=wasm go build ./...` plus `go vet` over `./cmd/wasm ./cmd/rbgo ./internal/vm` (`wasm (js, browser)` in `ci.yml`). Added with #684, which fixed the three `syscall` constants the `js` port does not define |

**Not on the pull-request path** — validated on a nightly schedule instead:
`riscv64`, `loong64`, `ppc64le` and `s390x`, under QEMU
(`arch-qemu-nightly.yml`) and natively on real hardware (`native-arch.yml`: the
GCC Compile Farm, plus an IBM LinuxONE s390x host).

**`GOOS=js GOARCH=wasm` (the browser) has no CI lane and does not currently
build** — see *What does not work yet*.
Every target named above is built by a lane on the pull-request path, including
the browser one: an earlier version of this file said `GOOS=js GOARCH=wasm` had
no lane and did not build, which stopped being true at #684 and stayed in the
README afterwards.

## Try it in one command

Expand Down
71 changes: 71 additions & 0 deletions readme_wasm_lane_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package ruby_test

import (
"os"
"regexp"
"strings"
"testing"
)

// TestReadmeDoesNotContradictTheWasmLane.
//
// For weeks the README said, in two places, that `GOOS=js GOARCH=wasm` does not
// build and has no CI lane. #684 fixed the build AND added the lane, which has
// been running green on every pull request since — so the repository was
// shipping a gate and a document that disagreed, and the document was the one a
// reader believes.
//
// A dated limitation going stale is not an error; the section says as much, and
// says when it was last checked. What IS an error is a limitation that a gate in
// THIS repository already disproves on every push: the answer was in the tree
// the whole time and nothing looked at it.
//
// This test is deliberately narrow. It does not try to validate the whole
// "What does not work yet" section against reality, which would need to run the
// world. It checks the one class of claim that can be settled from files
// already here: the README says a target does not build, while ci.yml builds it.
func TestReadmeDoesNotContradictTheWasmLane(t *testing.T) {
readme := readFile(t, "README.md")
ci := readFile(t, ".github/workflows/ci.yml")

// Is the browser target gated? Match the build command rather than the lane
// name, because a name can be renamed and the question is what is RUN.
gated := regexp.MustCompile(`GOOS=js\s+GOARCH=wasm\s+go\s+build`).MatchString(ci)
if !gated {
// If the lane is ever removed, the README's old wording becomes true
// again and this test must stop demanding its absence -- but the removal
// should be deliberate, so say so rather than passing in silence.
t.Skip("no GOOS=js GOARCH=wasm build step in ci.yml; the README is free to say the browser target is ungated")
}

// The claims that are now false. Each is matched on a distinctive phrase
// rather than a loose keyword: "browser" and "wasm" appear all over this
// README for things that are perfectly true.
for _, claim := range []struct{ phrase, why string }{
{"has no CI lane and does not currently",
"ci.yml builds GOOS=js GOARCH=wasm on every pull request"},
{"The browser WebAssembly target does not currently build",
"ci.yml builds GOOS=js GOARCH=wasm on every pull request"},
{"no CI lane catches it, because only the",
"the js/wasm lane catches exactly this"},
} {
if strings.Contains(readme, claim.phrase) {
t.Errorf("README.md claims %q, but %s.\n"+
"Either the claim is stale and should go, or the lane should, "+
"but the repository must not ship both.", claim.phrase, claim.why)
}
}
}

func readFile(t *testing.T, path string) string {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
return string(b)
}
Loading