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
5 changes: 4 additions & 1 deletion .github/workflows/arch-qemu-nightly.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
name: arch-qemu-nightly

# Scheduled QEMU validation of the four EXOTIC 64-bit targets that GitHub-hosted
# runners cannot run natively: riscv64, loong64, ppc64le, s390x.
# runners cannot run natively: riscv64, loong64, ppc64le, s390x — and of 32-bit
# arm (vetted per PR in ci.yml's arch-386 job, whose 386 suite runs natively).
#
# These lanes used to run on every PR/push, where qemu-user emulation (~50-100x
# slower than native) was the CI bottleneck (~10 min/lane). They have been moved
Expand Down Expand Up @@ -39,6 +40,8 @@ jobs:
- { goarch: loong64, qemu: qemu-loongarch64-static }
- { goarch: ppc64le, qemu: qemu-ppc64le-static }
- { goarch: s390x, qemu: qemu-s390x-static }
# 32-bit: int is 32 bits. (386 runs natively per PR in ci.yml.)
- { goarch: arm, qemu: qemu-arm-static }
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,29 @@ jobs:
go-version: 1.27.2
- run: go env GOARCH && go test ./...

# 32-bit gate: int is 32 bits. 386 runs natively on the amd64 runner (the
# kernel executes 32-bit x86 binaries), not under qemu-i386, whose emulation
# loses x87/SSE state across preemption. 32-bit arm is vetted here (a cheap
# compile of every package and test) and run under qemu in the nightly
# (arch-qemu-nightly.yml). The mysql2 tests are 64-bit only: their in-process
# server, dolthub/go-mysql-server, does not compile for 32-bit targets.
arch-386:
name: arch 386 (+ vet arm)
runs-on: ubuntu-latest
env:
CGO_ENABLED: "0"
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: 1.27.2
- name: Vet (GOARCH=386, GOARCH=arm)
run: |
GOARCH=386 go vet ./...
GOARCH=arm go vet ./...
- name: Test (GOARCH=386)
run: GOARCH=386 go test -timeout=60m ./...

# WebAssembly (WASI preview 1) gate: the same interpreter compiled for
# GOOS=wasip1 GOARCH=wasm and RUN under a real wasm runtime. The database /
# search / KV / heavy-network gem backends are compiled out on wasm (their
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -771,12 +771,13 @@ 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 |
| **386, arm (32-bit)** | `go vet ./...` for `GOARCH=386` and `GOARCH=arm`, then `go test ./...` for `386`, run natively on the amd64 runner (`arch 386 (+ vet arm)` in `ci.yml`). Ruby semantics do not change with the word size: Integer stays a 64-bit Fixnum promoted to Bignum on every target. The `mysql2` tests are skipped there, because their in-process server, `dolthub/go-mysql-server`, does not compile for 32-bit targets; the binding itself does. One limit is the word size's: a buffered IO's position is a Go `int`, so on 32-bit targets it stops at 2**31-1 |
| **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`, then the playground module is **run** in node by `web/smoke.mjs` (`wasm (js, browser)` in `ci.yml`). Added with #684, which fixed the three `syscall` constants the `js` port does not define |
| **rbgo_core** | `go vet` and the `internal/vm` suite with `-tags rbgo_core`, then the core playground module is built for js/wasm, its size bounded, and **run** in node by `web/smoke.mjs --core` (`rbgo_core (core build)` in `ci.yml`). See [the `rbgo_core` build](#the-rbgo_core-build-the-language-and-the-numeric-stack-no-gems) |

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

Expand Down Expand Up @@ -1183,7 +1184,8 @@ SPECDIR=/path/to/ruby-spec CACHE=/path/to/ruby-spec scripts/conformance/rubyspec
### CI layering

Per-PR CI is a **fast native gate**: the `-race` + 100 %-coverage suite on
ubuntu/macos/windows plus native `amd64` and `arm64` lanes. Because rbgo is
ubuntu/macos/windows plus native `amd64`, `arm64` and 32-bit `386` lanes (32-bit
`arm` is vetted per PR and run nightly under QEMU). Because rbgo is
pure-Go (CGO=0, no assembly), the four exotic 64-bit targets
(`riscv64`/`loong64`/`ppc64le`/`s390x`) are validated **off the per-PR critical
path** — nightly under QEMU, and on **real hardware via the GCC Compile Farm**
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ require (
github.com/go-ruby-capistrano/capistrano v0.0.0-20260927173236-3b90af3c6888
github.com/go-ruby-capybara/capybara v0.0.0-20260910084630-0731c9ea8d6d
github.com/go-ruby-cgi/cgi v0.0.0-20260916091525-3d328d3ff447
github.com/go-ruby-chronic/chronic v0.0.0-20260916091614-bd47d560b6f4
github.com/go-ruby-chronic/chronic v0.1.0
github.com/go-ruby-cmath/cmath v0.0.0-20260916091648-4963ab4145f7
github.com/go-ruby-concurrent-ruby/concurrent-ruby v0.0.0-20260916091840-b820f7a57284
github.com/go-ruby-confd/confd v0.0.0-20260825131031-1ac780d51c57
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,8 @@ github.com/go-ruby-capybara/capybara v0.0.0-20260910084630-0731c9ea8d6d h1:eDH85
github.com/go-ruby-capybara/capybara v0.0.0-20260910084630-0731c9ea8d6d/go.mod h1:igWLNrYKtKkssHOzlXm1YFoPH0Yi6LGazOSA4Obo6T0=
github.com/go-ruby-cgi/cgi v0.0.0-20260916091525-3d328d3ff447 h1:o6V4n2Td46cjbCwPN0pZnUVFfHbC648AnYtMQVU9O60=
github.com/go-ruby-cgi/cgi v0.0.0-20260916091525-3d328d3ff447/go.mod h1:TAIxiFMAj68dWClzEmDY1vCSNFiPfupD3HRADRXrkZw=
github.com/go-ruby-chronic/chronic v0.0.0-20260916091614-bd47d560b6f4 h1:Z2aDUPl3f6fQIJ4Y+9KCE16p77dAxJBBJjR3j974A5o=
github.com/go-ruby-chronic/chronic v0.0.0-20260916091614-bd47d560b6f4/go.mod h1:6H3V0VdtD8NB/WEt+8+bIM0n78g45H8HDAnWNwJByCM=
github.com/go-ruby-chronic/chronic v0.1.0 h1:3rar6r+rLVFNmyGpTPZvMbjEnnRh9JI2vNI11048A70=
github.com/go-ruby-chronic/chronic v0.1.0/go.mod h1:hhudekjG06iXIHV99juSwG+aOJ+Jv7dNS7tLzEX8ywg=
github.com/go-ruby-cmath/cmath v0.0.0-20260916091648-4963ab4145f7 h1:2tUvNHeR8mL8NcJyxTZxaaujvusucfBsnZAQg18D9Zg=
github.com/go-ruby-cmath/cmath v0.0.0-20260916091648-4963ab4145f7/go.mod h1:djGUM2WR7AdnZqeRg/OQ4xVzmsl8PAWQKILX2n/foas=
github.com/go-ruby-concurrent-ruby/concurrent-ruby v0.0.0-20260916091840-b820f7a57284 h1:hIdCXEwLLYl+NPKBCbI3aFul6ARXfgEczxlA0hIDOkY=
Expand Down
2 changes: 1 addition & 1 deletion internal/vm/actionpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func apStr(v object.Value) string {
func apInt(v object.Value) int {
switch n := v.(type) {
case object.Integer:
return int(n)
return clampInt(n)
case object.Float:
return int(n)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/vm/actionview.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ func avNumberOpts(h *object.Hash) []actionview.Option {
var o []actionview.Option
if v, ok := avHashVal(h, "precision"); ok {
if i, ok := v.(object.Integer); ok {
o = append(o, actionview.Precision(int(i)))
o = append(o, actionview.Precision(clampInt(i)))
}
}
if v, ok := avHashVal(h, "significant"); ok {
Expand Down
4 changes: 2 additions & 2 deletions internal/vm/actionview_bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ func avValue(v object.Value) any {
case object.Bool:
return bool(x)
case object.Integer:
return int(x)
return clampInt(x)
case object.Float:
return float64(x)
case *object.String:
Expand Down Expand Up @@ -325,7 +325,7 @@ func avHashStr(h *object.Hash, key, def string) string {
func avHashInt(h *object.Hash, key string, def int) int {
if v, ok := avHashVal(h, key); ok {
if i, ok := v.(object.Integer); ok {
return int(i)
return clampInt(i)
}
}
return def
Expand Down
2 changes: 1 addition & 1 deletion internal/vm/active_ldap_bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func alFindOptions(args []object.Value) activeldap.FindOptions {
}
}
if v, ok := alKw(h, "limit"); ok {
opts.Limit = int(intArg(v))
opts.Limit = clampInt(intArg(v))
}
if v, ok := alKw(h, "attributes"); ok {
opts.Attributes = rubyStrList(v)
Expand Down
2 changes: 1 addition & 1 deletion internal/vm/activejob_bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ func ajRawFromRuby(v object.Value) any {
// ajInt coerces an Integer argument to an int (a non-Integer reads as 0).
func ajInt(v object.Value) int {
if n, ok := v.(object.Integer); ok {
return int(n)
return clampInt(n)
}
return 0
}
Expand Down
6 changes: 3 additions & 3 deletions internal/vm/activemodel_bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ func (vm *VM) amBuildOptions(h *object.Hash) activemodel.Options {
// amIntPtr reads an Integer option into a *int, or nil.
func amIntPtr(v object.Value) *int {
if n, ok := v.(object.Integer); ok {
i := int(n)
i := clampInt(n)
return &i
}
return nil
Expand All @@ -368,11 +368,11 @@ func amIntRange(v object.Value) *activemodel.Range {
}
lo, _ := r.Lo.(object.Integer)
hi, _ := r.Hi.(object.Integer)
max := int(hi)
max := clampInt(hi)
if r.Exclusive {
max--
}
return &activemodel.Range{Min: int(lo), Max: max}
return &activemodel.Range{Min: clampInt(lo), Max: max}
}

// amLengthOptions reads a length: options Hash into LengthOptions.
Expand Down
4 changes: 2 additions & 2 deletions internal/vm/activerecord_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func arInt(args []object.Value) int {
return 0
}
if n, ok := args[0].(object.Integer); ok {
return int(n)
return clampInt(n)
}
return 0
}
Expand Down Expand Up @@ -207,7 +207,7 @@ func arLengthOpts(args []object.Value) activerecord.LengthOpts {
if !isInt {
continue
}
i := int(n)
i := clampInt(n)
switch arStr(k) {
case "minimum":
o.Minimum = &i
Expand Down
2 changes: 1 addition & 1 deletion internal/vm/activerecord_schema_bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ func arColOpts(h *object.Hash) []activerecord.ColOpt {
}
if val, ok := arHashGet(h, "limit"); ok {
if n, isInt := val.(object.Integer); isInt {
opts = append(opts, activerecord.Limit(int(n)))
opts = append(opts, activerecord.Limit(clampInt(n)))
}
}
return opts
Expand Down
14 changes: 7 additions & 7 deletions internal/vm/activesupport.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ func (vm *VM) registerActiveSupportInflector(as *RClass) {
return object.NewString(vm.asInflections.ForeignKey(strArg(args[0]), true))
})
sm("ordinal", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(inflector.Ordinal(int(intArg(args[0]))))
return object.NewString(inflector.Ordinal(clampInt(intArg(args[0]))))
})
sm("ordinalize", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(inflector.Ordinalize(int(intArg(args[0]))))
return object.NewString(inflector.Ordinalize(clampInt(intArg(args[0]))))
})
sm("parameterize", func(_ *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(inflector.Parameterize(strArg(args[0]), "-", false))
Expand Down Expand Up @@ -311,7 +311,7 @@ func (vm *VM) registerASString() {
return object.NewString(coreext.Squish(str(self)))
})
vm.cString.define("truncate", func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
return object.NewString(coreext.Truncate(str(self), int(intArg(args[0])), "", ""))
return object.NewString(coreext.Truncate(str(self), clampInt(intArg(args[0])), "", ""))
})
d("camelize", -1, func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
upper := true
Expand Down Expand Up @@ -354,14 +354,14 @@ func (vm *VM) registerASArray() {
return object.Bool(coreext.ArrayBlank(boxSlice(elems(self))))
})
vm.cArray.define("in_groups", func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
n := int(intArg(args[0]))
n := clampInt(intArg(args[0]))
if len(args) > 1 && isFalse(args[1]) {
return rubyFromGroups(coreext.InGroupsNoFill(boxSlice(elems(self)), n))
}
return rubyFromGroups(coreext.InGroups(boxSlice(elems(self)), n, fillArg(args)))
})
vm.cArray.define("in_groups_of", func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
n := int(intArg(args[0]))
n := clampInt(intArg(args[0]))
if len(args) > 1 && isFalse(args[1]) {
return rubyFromGroups(coreext.InGroupsOfNoFill(boxSlice(elems(self)), n))
}
Expand Down Expand Up @@ -420,10 +420,10 @@ func (vm *VM) registerASInteger() {
d := func(name string, argc int, fn NativeFn) { vm.cInteger.defineArgc(name, argc, fn) }

d("ordinalize", 0, func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
return object.NewString(coreext.Ordinalize(int(self.(object.Integer))))
return object.NewString(coreext.Ordinalize(clampInt(self.(object.Integer))))
})
d("multiple_of?", 1, func(_ *VM, self object.Value, args []object.Value, _ *Proc) object.Value {
return object.Bool(coreext.MultipleOf(int(self.(object.Integer)), int(intArg(args[0]))))
return object.Bool(coreext.MultipleOf(clampInt(self.(object.Integer)), clampInt(intArg(args[0]))))
})
}

Expand Down
4 changes: 2 additions & 2 deletions internal/vm/age.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (vm *VM) registerAgeScrypt(mod *RClass) {
}
if h := ageOptsHash(args[1:]); h != nil {
if v, ok := h.Get(object.Symbol("work_factor")); ok {
r.SetWorkFactor(int(intArg(v)))
r.SetWorkFactor(clampInt(intArg(v)))
}
}
return &AgeScryptRecipient{cls: rc, rcp: r}
Expand All @@ -155,7 +155,7 @@ func (vm *VM) registerAgeScrypt(mod *RClass) {
}
if h := ageOptsHash(args[1:]); h != nil {
if v, ok := h.Get(object.Symbol("max_work_factor")); ok {
id.SetMaxWorkFactor(int(intArg(v)))
id.SetMaxWorkFactor(clampInt(intArg(v)))
}
}
return &AgeScryptIdentity{cls: ic, id: id}
Expand Down
26 changes: 25 additions & 1 deletion internal/vm/alloc_bounds.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package vm
import (
"math"
"runtime"
"strconv"
"strings"
"unsafe"

Expand Down Expand Up @@ -78,6 +79,28 @@ func repeatFitsString(srcLen int, n int64) bool {
return n == 0 || strMaxSize/n >= int64(srcLen)
}

// hostAllocMax is the largest single allocation, in bytes, rbgo asks the Go
// runtime for on behalf of a Ruby size. Above Go's own maxAlloc, make panics
// with a recoverable "makeslice:" fault, which convertAllocPanic turns into
// NoMemoryError. BELOW it, a request the host cannot satisfy is a fatal
// "out of memory" that nothing can recover.
//
// On a 64-bit target maxAlloc is 2**48 and this bound is set there, so it
// refuses exactly what make would have refused, with the same NoMemoryError.
// On a 32-bit target maxAlloc is about 2**32, the whole address space, so
// everything between ~1 GiB and 4 GiB was the fatal window: "x".center(2**62)
// asked for 4 GiB and killed the process (386 CI). There the bound is 2**30.
const hostAllocMax = int64(1) << (30 + 18*(strconv.IntSize/32-1))

// checkAlloc raises NoMemoryError, MRI's answer for a refused allocation, when n
// elements of elemSize bytes exceed hostAllocMax. It divides rather than
// multiplies, so n*elemSize is never formed.
func checkAlloc(n, elemSize int64) {
if n > hostAllocMax/elemSize {
raise("NoMemoryError", "failed to allocate memory")
}
}

// fitsArrayAlloc reports whether n elements stay within ARY_MAX_SIZE. It is the
// single-factor form of the test, for a size that is taken from a Ruby value
// without being multiplied (Array.new's capacity, Array#fill's new length).
Expand Down Expand Up @@ -164,5 +187,6 @@ func randomByteCount(v object.Value) int {
if n < 0 {
raise("ArgumentError", "negative string size (or size too big)")
}
return int(n)
checkAlloc(n, 1)
return clampInt(n)
}
10 changes: 9 additions & 1 deletion internal/vm/alloc_bounds_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"bytes"
"fmt"
"math"
"strconv"
"strings"
"testing"

Expand Down Expand Up @@ -55,6 +56,13 @@ func runKeepingError(t *testing.T, src string) (*VM, string, error) {
// product is never formed. Expectations measured byte-for-byte against the
// installed ruby 4.0.5.
func TestRepeatBoundRegimes(t *testing.T) {
// "x" * (2**62) passes MRI's LONG_MAX/n test only because LONG_MAX/2**62 is
// 1 with a 64-bit long. With a 32-bit int the quotient is 0, so the same
// division test (strMaxSize is math.MaxInt) refuses with ArgumentError.
strOneByte := "NoMemoryError: failed to allocate memory\n"
if strconv.IntSize == 32 {
strOneByte = "ArgumentError: argument too big\n"
}
cases := []struct{ name, src, want string }{
// --- Array#*: MRI divides ARY_MAX_SIZE, so all of these are ArgumentError.
{"ary_over_maxalloc", `begin; [1] * (2**62); rescue Exception=>e; puts "#{e.class}: #{e.message}"; end`,
Expand All @@ -74,7 +82,7 @@ func TestRepeatBoundRegimes(t *testing.T) {
// receiver's LENGTH. This asymmetry is MRI's and is reproduced, not
// normalised -- see alloc_bounds.go.
{"str_one_byte_is_nomemory", `begin; "x" * (2**62); rescue Exception=>e; puts "#{e.class}: #{e.message}"; end`,
"NoMemoryError: failed to allocate memory\n"},
strOneByte},
{"str_two_byte_is_argerror", `begin; "ab" * (2**62); rescue Exception=>e; puts "#{e.class}: #{e.message}"; end`,
"ArgumentError: argument too big\n"},
{"str_three_byte_is_argerror", `begin; "xyz" * (2**62); rescue Exception=>e; puts "#{e.class}: #{e.message}"; end`,
Expand Down
2 changes: 1 addition & 1 deletion internal/vm/argf.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ func (vm *VM) registerARGF() {
return object.IntValue(int64(self(v).lineno))
})
d("lineno=", func(vm *VM, v object.Value, args []object.Value, _ *Proc) object.Value {
self(v).lineno = int(intArg(args[0]))
self(v).lineno = clampInt(intArg(args[0]))
return args[0]
})

Expand Down
15 changes: 12 additions & 3 deletions internal/vm/array_fill_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

package vm_test

import "testing"
import (
"strconv"
"testing"
)

// TestArrayFill covers Array#fill in every MRI form: the no-block filler forms
// (obj / obj,start / obj,start,length / obj,range) and the block forms, including
Expand Down Expand Up @@ -62,6 +65,10 @@ func TestArrayFill(t *testing.T) {

// TestArrayFillErrors covers Array#fill's error and boundary branches.
func TestArrayFillErrors(t *testing.T) {
fill50 := "failed to allocate memory\n"
if strconv.IntSize == 32 {
fill50 = "argument too big\n"
}
cases := []struct{ src, want string }{
// Argument-count errors.
{`begin; [].fill; rescue ArgumentError; puts "AE"; end`, "AE\n"},
Expand All @@ -84,8 +91,10 @@ func TestArrayFillErrors(t *testing.T) {
// `NoMemoryError: failed to allocate memory`, and 1<<40 and 1<<35 are
// genuinely materialised (both passed 2 GB resident before the probe's cap
// stopped them), so the old bound was wrong in both directions.
{`begin; [1, 2, 3].fill(10, 1, 1 << 50); rescue NoMemoryError => e; puts e.message; end`,
"failed to allocate memory\n"},
// On a 32-bit target ARY_MAX_SIZE is (2**31-1)/8, below 1<<50, so the
// size test refuses first, as MRI's own does with a 32-bit long.
{`begin; [1, 2, 3].fill(10, 1, 1 << 50); rescue NoMemoryError, ArgumentError => e; puts e.message; end`,
fill50},
// A range whose begin is before the start of the array -> RangeError.
{`begin; [1, 2, 3].fill("x", -5..-3); rescue RangeError; puts "RE"; end`, "RE\n"},
// A block raising mid-fill leaves the already-filled elements in place.
Expand Down
Loading
Loading