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
27 changes: 20 additions & 7 deletions internal/vm/gc.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,21 @@ func (vm *VM) registerGC() {
// tunables. rbgo runs on Go's GC and exposes no tunable settings, so the
// configuration is just the read-only :implementation name: with no argument
// (or nil) it returns {implementation: "rbgo"}; with a Hash it applies the
// (empty) set of writable settings — ignoring unknown keys — and returns the
// settings without the global :implementation key, but raises ArgumentError if
// asked to write the read-only :implementation. Any other argument type raises
// ArgumentError, matching MRI.
// (empty) set of writable settings — ignoring unknown keys — and raises
// ArgumentError if asked to write the read-only :implementation. Any other
// argument type raises ArgumentError, matching MRI.
//
// ⛔ What the Hash form RETURNS changed in Ruby 4.0, and ruby/spec asserts it
// both ways:
//
// ruby_version_is ""..."4.0" "returns the same as GC.config but WITHOUT the :implementation key"
// ruby_version_is "4.0" "returns the same as GC.config, INCLUDING the :implementation key"
// (core/gc/config_spec.rb:43 and :50)
//
// So from 4.0 the setter answers with the full configuration, exactly what
// the no-argument form returns — `GC.config({}).should == previous`. Here
// that is the one global key, which is why the pre-4.0 answer was an empty
// Hash and the 4.0 answer is `{implementation: "rbgo"}`.
def("config", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
fullConfig := func() *object.Hash {
h := object.NewHash()
Expand All @@ -282,9 +293,11 @@ func (vm *VM) registerGC() {
if _, ok := a.Get(object.NewString("implementation")); ok {
return raise("ArgumentError", `Attempting to set read-only key "Implementation"`)
}
// No writable settings exist on Go's GC; unknown keys are ignored. The
// return value mirrors the settings without the global :implementation key.
return object.NewHash()
// No writable settings exist on Go's GC; unknown keys are ignored. From
// 4.0 the return value is the FULL configuration — the same thing the
// no-argument form answers — rather than the settings with the global
// key stripped out.
return fullConfig()
default:
return raise("ArgumentError", "expected keyword arguments, got %s", vm.classOf(args[0]).name)
}
Expand Down
27 changes: 23 additions & 4 deletions internal/vm/gc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,31 @@ func TestGCModule(t *testing.T) {
{`p GC::OPTS.is_a?(Array)`, "true\n"},

// config: no-arg and nil return {implementation: "rbgo"}; an options Hash
// returns the (empty) writable settings and ignores unknown keys; writing the
// read-only :implementation (Symbol or String) raises; a non-hash raises.
// ignores unknown keys; writing the read-only :implementation (Symbol or
// String) raises; a non-hash raises.
//
// ⛔ What the Hash form RETURNS changed in Ruby 4.0, and the two rows below
// moved with the version rather than because rbgo was wrong — ruby/spec
// asserts it both ways, which is how a reader can tell the two apart:
//
// ruby_version_is ""..."4.0" "returns the same as GC.config but WITHOUT the :implementation key"
// ruby_version_is "4.0" "returns the same as GC.config, INCLUDING the :implementation key"
// (core/gc/config_spec.rb:43 and :50)
//
// So from 4.0 the setter answers with the full configuration — here the one
// global key — where before it answered an empty Hash.
{`c = GC.config; p [c.is_a?(Hash), c[:implementation]]`, `[true, "rbgo"]` + "\n"},
{`p GC.config(nil) == GC.config`, "true\n"},
{`p GC.config({})`, "{}\n"},
{`p (GC.config(foo: "bar") == {})`, "true\n"},
{`p GC.config({})`, `{implementation: "rbgo"}` + "\n"},
{`p (GC.config(foo: "bar") == GC.config)`, "true\n"},
// ⛔ Returning the FULL config where the answer used to be {} makes the
// identity of that Hash matter: a caller who mutates it must not reach the
// VM's own configuration. Each call answers a fresh, unfrozen copy, and an
// unknown key is ignored rather than stored. Measured equal on MRI 4.0.7.
{`p GC.config.equal?(GC.config)`, "false\n"},
{`p GC.config.frozen?`, "false\n"},
{`h = GC.config; h[:implementation] = "zzz"; p GC.config[:implementation]`, `"rbgo"` + "\n"},
{`GC.config(foo: "bar"); p GC.config.key?(:foo)`, "false\n"},
{`begin; GC.config(implementation: "x"); rescue => e; p [e.class, e.message]; end`, `[ArgumentError, "Attempting to set read-only key \"Implementation\""]` + "\n"},
{`begin; GC.config("implementation" => "x"); rescue => e; p e.class; end`, "ArgumentError\n"},
{`begin; GC.config([]); rescue => e; p e.class; end`, "ArgumentError\n"},
Expand Down
Loading