Skip to content

instance_variable_set is a silent no-op on File/StringIO/Dir/IO::Buffer/Array/Hash/String/Proc (ivarTable returns nil) #672

Closed

Description

@tannevaled

What

instance_variable_set on most of rbgo's natively-backed objects is a silent no-op. It does not raise; the write is discarded and the next read answers nil.

f = File.open("x", "r")
f.instance_variable_set(:@zz, 7)
p f.instance_variable_get(:@zz)   # MRI: 7      rbgo: nil

Measured across the natively-backed kinds (instance_variable_set(:@zz, 7) then get):

receiver MRI 4.0.5 rbgo
File 7 nil
StringIO 7 nil
Dir 7 nil
IO::Buffer 7 nil
Array 7 nil
Hash 7 nil
String 7 nil
Proc 7 nil
Range FrozenError (a literal Range is frozen) nil
Time 7 7
Object 7 7

ivarTable (internal/vm/object_model.go) enumerates the kinds that have ivar storage — RObject, RClass, object.Main, SinatraCtx, ActionViewBase, and the boxed method values — and returns nil for everything else. Every caller then has nowhere to write and, apparently, no complaint.

The silence is the dangerous part: attr_accessor on a singleton class of such an object appears to work — the reader and writer are defined and callable — and simply never stores anything.

Measured cost

Two examples in core/file/open_spec.rb, via core/file/fixtures/common.rb's FileSpecs.make_closer:

core/file/open_spec.rb    rbgo = 78    MRI 4.0.5 = 82
def self.make_closer(obj, exc=nil)
  class << obj
    attr_accessor :close_exception
    alias_method :original_close, :close
    def close
      original_close
      ScratchPad << :file_closed
      raise @close_exception if @close_exception   # @close_exception is always nil in rbgo
    end
  end
  obj.close_exception = exc                       # discarded
end

So "propagates StandardErrors produced by close" and "propagates non-StandardErrors produced by close" both see no exception. The File.open ensure machinery is correct — ioYieldAndClose already re-raises a #close exception and filters the IOError "closed stream" case, and it is witnessed working when the override raises unconditionally:

File.open(p, "r") do |f|
  class << f
    alias_method :oc, :close
    def close; oc; raise StandardError, "boom"; end   # this DOES propagate in rbgo
  end
end

The defect is only that the fixture's exception never gets stored.

Given attr_accessor on a singleton is a common spec-fixture idiom, and String/Array/Hash/Proc are on the list too, this very likely costs more than these two examples across the suite.

Suggested shape

Give the natively-backed values an ivar table the way boxed already does — a small embedded struct with a lazily-created map — rather than extending the type switch one kind at a time. Time and the boxed method values already show the pattern.

Meanwhile, a caller that cannot find a table should probably not stay silent.

Where

internal/vm/object_model.go (ivarTable) and internal/object. Both outside the file set of #668, which is why this is filed rather than fixed there.

Activity

  1. tannevaled commented on Sep 26, 2026

    @tannevaled
    ContributorAuthor

    Fixed and verifiable on main @8f2a019. Closing.

    instance_variable_set on the native kinds now works, matching MRI 4.0.5:

    s = "abc"; s.instance_variable_set(:@x, 1)
    s.instance_variable_get(:@x)   # 1    (both)
    s.instance_variables           # [:@x] (both)
    [].instance_variable_set(:@y, 2)   # reads back 2 (both)
    {}.instance_variable_set(:@z, 3)   # reads back 3 (both)

    core/string/scrub_spec.rb is at its measured ceiling (24/24). Landed in #674
    ("every object carries an ivar table").

    Recorded because it cost time twice today: this issue was routed as a live defect
    by two separate agents
    whose baselines predated #674, and each attributed further
    examples to it that belong to other causes. An open issue here is not reliably an
    unfinished one — reproduce before building on one.

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