diff --git a/internal/vm/find.go b/internal/vm/find.go index 8a2a9b77..8870b939 100644 --- a/internal/vm/find.go +++ b/internal/vm/find.go @@ -145,14 +145,22 @@ func findYield(vm *VM, emit func(object.Value), path string) (yieldErr error) { fileStackDepth := len(vm.fileStack) frameNamesDepth := len(vm.frameNames) frameFilesDepth := len(vm.frameFiles) + // frameCrefs and frameMethods mirror frameNames one for one, so they are + // restored WITH it: index i means the same frame in all three only while the + // three are the same length, and frameLabel now reads all three (#649). See + // Kernel#catch, which keeps the same six depths. + frameCrefsDepth := len(vm.frameCrefs) + frameMethodsDepth := len(vm.frameMethods) requireDirsDepth := len(vm.requireDirs) defer func() { if r := recover(); r != nil { if sig, ok := r.(throwSignal); ok && sig.tag == pruneTag { - vm.fileStack = vm.fileStack[:fileStackDepth] - vm.frameNames = vm.frameNames[:frameNamesDepth] - vm.frameFiles = vm.frameFiles[:frameFilesDepth] - vm.requireDirs = vm.requireDirs[:requireDirsDepth] + vm.fileStack = truncFrames(vm.fileStack, fileStackDepth) + vm.frameNames = truncFrames(vm.frameNames, frameNamesDepth) + vm.frameFiles = truncFrames(vm.frameFiles, frameFilesDepth) + vm.frameCrefs = truncFrames(vm.frameCrefs, frameCrefsDepth) + vm.frameMethods = truncFrames(vm.frameMethods, frameMethodsDepth) + vm.requireDirs = truncFrames(vm.requireDirs, requireDirsDepth) yieldErr = find.ErrPrune return } diff --git a/internal/vm/irb_bind.go b/internal/vm/irb_bind.go index 51e4bef3..fd1cbd2f 100644 --- a/internal/vm/irb_bind.go +++ b/internal/vm/irb_bind.go @@ -54,6 +54,11 @@ func (vm *VM) irbEval(b *Binding, code string) (result object.Value, errClass, e nNames, nFiles := len(vm.frameNames), len(vm.frameFiles) nStack, nDirs := len(vm.fileStack), len(vm.requireDirs) + // frameCrefs and frameMethods mirror frameNames one for one and are restored + // WITH it: frameLabel reads all three (#649), so a single leaked entry would + // make every later label name a different frame — for the rest of the IRB + // session, since a binding outlives the statement that raised. + nCrefs, nMethods := len(vm.frameCrefs), len(vm.frameMethods) defer func() { if r := recover(); r != nil { rerr, ok := r.(RubyError) @@ -62,10 +67,12 @@ func (vm *VM) irbEval(b *Binding, code string) (result object.Value, errClass, e } // An exception unwinding past exec frames leaves their per-frame tracking // entries unpopped; truncate back so the next statement sees clean state. - vm.frameNames = vm.frameNames[:nNames] - vm.frameFiles = vm.frameFiles[:nFiles] - vm.fileStack = vm.fileStack[:nStack] - vm.requireDirs = vm.requireDirs[:nDirs] + vm.frameNames = truncFrames(vm.frameNames, nNames) + vm.frameFiles = truncFrames(vm.frameFiles, nFiles) + vm.frameCrefs = truncFrames(vm.frameCrefs, nCrefs) + vm.frameMethods = truncFrames(vm.frameMethods, nMethods) + vm.fileStack = truncFrames(vm.fileStack, nStack) + vm.requireDirs = truncFrames(vm.requireDirs, nDirs) result, errClass, errMsg, raised = nil, rerr.Class, rerr.Message, true } }() diff --git a/internal/vm/object_model.go b/internal/vm/object_model.go index 06843ec1..61f7fb13 100644 --- a/internal/vm/object_model.go +++ b/internal/vm/object_model.go @@ -2,8 +2,10 @@ package vm import ( "fmt" + "reflect" "runtime" "strings" + "sync" rake "github.com/go-ruby-rake/rake" @@ -2441,72 +2443,113 @@ func getIvar(self object.Value, name string) object.Value { } func setIvar(self object.Value, name string, v object.Value) { - if t := ivarTable(self); t != nil { - if _, exists := t[name]; !exists { - switch o := self.(type) { - case *RObject: - o.ivarOrder = append(o.ivarOrder, name) - case boxed: - st := o.state() - st.ivarOrder = append(st.ivarOrder, name) - } - } - t[name] = v + st := ivarStoreOf(self, true) + if st.tbl == nil { + return + } + if _, exists := st.tbl[name]; !exists && st.order != nil { + // A remove_instance_variable took the name out of the map and left it in the + // order list. MRI takes it out of the SHAPE (rb_shape_transition_shape_remove_ivar, + // variable.c rb_ivar_delete), so a re-assignment appends at the END — and + // appears once. Without the drop, `set @a; set @b; remove @a; set @a` reported + // [:@a, :@b, :@a] where MRI reports [:@b, :@a]. + *st.order = append(dropName(*st.order, name), name) } + st.tbl[name] = v } -// ivarNamesInOrder returns self's instance-variable names as Symbols. For a -// plain object the names are reported in the order they were first assigned -// (matching MRI); other value kinds report in map order, which suffices for the -// cases that reach them. -func ivarNamesInOrder(self object.Value) []object.Value { - if o, ok := self.(*RObject); ok { - out := make([]object.Value, 0, len(o.ivarOrder)) - for _, n := range o.ivarOrder { - if _, live := o.ivars[n]; live { - out = append(out, object.Symbol(n)) - } +// dropName removes the first occurrence of name from names, in place, and is a +// no-op when it is absent (the common case: a name assigned for the first time). +func dropName(names []string, name string) []string { + for i, n := range names { + if n == name { + return append(names[:i], names[i+1:]...) } - return out } - if b, ok := self.(boxed); ok { - st := b.state() - out := make([]object.Value, 0, len(st.ivarOrder)) - for _, n := range st.ivarOrder { - if _, live := st.ivars[n]; live { + return names +} + +// ivarNamesInOrder returns self's instance-variable names as Symbols. Every kind +// that records an assignment order — a plain object, a Bound/UnboundMethod, and +// every natively-backed value through the generic table — reports in +// first-assignment order, which is what MRI reports: rb_obj_instance_variables +// collects through rb_ivar_foreach, and that walks the shape chain in creation +// order (variable.c). The remaining kinds (a class, main, the Sinatra and +// ActionView contexts) report in map order, which suffices for the cases that +// reach them. +func ivarNamesInOrder(self object.Value) []object.Value { + st := ivarStoreOf(self, false) + if st.order != nil { + out := make([]object.Value, 0, len(*st.order)) + for _, n := range *st.order { + // A remove_instance_variable leaves the name in the order list; the map + // says which names are still live. + if _, live := st.tbl[n]; live { out = append(out, object.Symbol(n)) } } return out } - t := ivarTable(self) - out := make([]object.Value, 0, len(t)) - for n := range t { + out := make([]object.Value, 0, len(st.tbl)) + for n := range st.tbl { out = append(out, object.Symbol(n)) } return out } -// ivarTable returns the instance-variable map backing self, or nil for values -// (Integer, String, …) that cannot hold ivars in this phase. +// ivarTable returns the instance-variable map backing self, or nil when there is +// none to show: an immediate (Integer, Float, Symbol, nil, true, false — MRI's +// SPECIAL_CONST_P, for which rb_ivar_defined answers false and rb_ivar_count 0 +// outright) or a value no ivar has ever been written to. It never creates a +// generic table, so a pure read — `defined?(@x)`, instance_variable_defined?, +// remove_instance_variable — costs a lookup and leaves nothing behind. A writer +// goes through setIvar, hence ivarStoreOf(self, true). func ivarTable(self object.Value) map[string]object.Value { + return ivarStoreOf(self, false).tbl +} + +// ivarStore is where one value's instance variables live: the name-to-value map +// and, for a kind that records it, the first-assignment order to report them in. +type ivarStore struct { + tbl map[string]object.Value + order *[]string // nil for a kind that records no order +} + +// ivarStoreOf resolves self's ivar storage. With create set the caller is about +// to write, so what is missing is allocated; without it, the function reports +// only what already exists. +// +// Every kind with room of its own answers from that room. Everything else — +// File, StringIO, Dir, IO::Buffer, Array, Hash, String, Proc, a Regexp, … — +// answers from the VM-wide generic table, which is how MRI stores the ivars of +// an object with no slots of its own: variable.c's ivar_set sends T_OBJECT to +// rb_obj_ivar_set, T_CLASS/T_MODULE to rb_class_ivar_set and EVERYTHING ELSE to +// generic_ivar_set. Before this, the kinds with no room returned no table at all +// and every write was DISCARDED in silence (#672). +func ivarStoreOf(self object.Value, create bool) ivarStore { switch o := self.(type) { case *RObject: - return o.ivars + if o.ivars == nil && create { + o.ivars = map[string]object.Value{} + } + return ivarStore{tbl: o.ivars, order: &o.ivarOrder} case *RClass: - return o.ivars + if o.ivars == nil && create { + o.ivars = map[string]object.Value{} + } + return ivarStore{tbl: o.ivars} case *object.Main: - return o.IvarTable() + return ivarStore{tbl: o.IvarTable()} case *SinatraCtx: if o.ivars == nil { o.ivars = map[string]object.Value{} } - return o.ivars + return ivarStore{tbl: o.ivars} case *ActionViewBase: if o.ivars == nil { o.ivars = map[string]object.Value{} } - return o.ivars + return ivarStore{tbl: o.ivars} } // Bound/UnboundMethod carry their ivars in an embedded methodValueState. if b, ok := self.(boxed); ok { @@ -2514,9 +2557,73 @@ func ivarTable(self object.Value) map[string]object.Value { if st.ivars == nil { st.ivars = map[string]object.Value{} } - return st.ivars + return ivarStore{tbl: st.ivars, order: &st.ivarOrder} } - return nil + if g := genericIvarsOf(self, create); g != nil { + return ivarStore{tbl: g.ivars, order: &g.order} + } + return ivarStore{} +} + +// genericIvars is the ivar table of a value that has no room for one of its own: +// the map, created with the entry, plus the assignment order. MRI's struct +// gen_ivtbl. +type genericIvars struct { + ivars map[string]object.Value + order []string +} + +// genericIvarTbl is MRI's generic_iv_tbl_ (variable.c): one process-wide table +// KEYED BY THE OBJECT holding the instance variables of values that carry none +// themselves. An entry is created by the first write and never by a read, so a +// value no Ruby code ever sets an ivar on costs nothing at all — no field on the +// struct, no allocation, no map entry. Keying a map by an object.Value is how +// internal/object's recursion guard (ReprEnter) already tracks identity. +// +// The mutex mirrors MRI's own: rb_gen_ivtbl_get and generic_ivar_set take the +// VM lock (RB_VM_LOCK_ENTER) around this table, because a Ruby thread may set an +// ivar on an object another thread shares. It guards the outer lookup only — the +// returned map is then used exactly as an RObject's is, unsynchronized. +// +// One deliberate difference from MRI: MRI drops an object's entry when the GC +// frees the object (rb_free_generic_ivar) and moves or copies it on dup/clone +// (rb_mv_generic_ivar, rb_copy_generic_ivar). Go's GC offers no free hook and the +// key keeps the object reachable, so an entry lives as long as the process. +var ( + genericIvarMu sync.Mutex + genericIvarTbl = map[object.Value]*genericIvars{} +) + +// genericIvarsOf returns self's entry in the generic table, creating it when +// create is set. It answers nil for a value that cannot key the table. +func genericIvarsOf(self object.Value, create bool) *genericIvars { + if !hasIdentity(self) { + return nil + } + genericIvarMu.Lock() + defer genericIvarMu.Unlock() + if g := genericIvarTbl[self]; g != nil { + return g + } + if !create { + return nil + } + g := &genericIvars{ivars: map[string]object.Value{}} + genericIvarTbl[self] = g + return g +} + +// hasIdentity reports whether self is a reference. That is what the generic +// table needs of a key — an entry belongs to ONE object, and a copy of a Go +// struct value would answer to the entry of every equal copy — and it is the +// complement of MRI's SPECIAL_CONST_P screen, which makes rb_ivar_defined answer +// false and rb_ivar_count 0 for an immediate. rbgo's immediates (Integer, Float, +// Symbol, nil, true, false) are Go values, and MRI holds every one of them +// frozen, so a write there reaches rb_check_frozen and raises instead of reaching +// any table (Object#instance_variable_set raises FrozenError for them here too). +func hasIdentity(self object.Value) bool { + t := reflect.TypeOf(self) + return t != nil && t.Kind() == reflect.Pointer } // refDefineeOf picks the second end of the cref chain to record on a block diff --git a/internal/vm/pstore.go b/internal/vm/pstore.go index 9ba875b1..0a78cbbe 100644 --- a/internal/vm/pstore.go +++ b/internal/vm/pstore.go @@ -354,6 +354,11 @@ func (p *PStore) transaction(vm *VM, readOnly bool, blk *Proc) object.Value { fileStackDepth := len(vm.fileStack) frameNamesDepth := len(vm.frameNames) frameFilesDepth := len(vm.frameFiles) + // frameCrefs and frameMethods mirror frameNames one for one and are restored + // WITH it — frameLabel reads all three (#649), so one leaked entry renames + // every later frame in a backtrace. + frameCrefsDepth := len(vm.frameCrefs) + frameMethodsDepth := len(vm.frameMethods) requireDirsDepth := len(vm.requireDirs) body := func(t *libpstore.Tx) (bodyErr error) { @@ -362,10 +367,12 @@ func (p *PStore) transaction(vm *VM, readOnly bool, blk *Proc) object.Value { p.tx = nil if r := recover(); r != nil { if sig, ok := r.(pstoreSignal); ok { - vm.fileStack = vm.fileStack[:fileStackDepth] - vm.frameNames = vm.frameNames[:frameNamesDepth] - vm.frameFiles = vm.frameFiles[:frameFilesDepth] - vm.requireDirs = vm.requireDirs[:requireDirsDepth] + vm.fileStack = truncFrames(vm.fileStack, fileStackDepth) + vm.frameNames = truncFrames(vm.frameNames, frameNamesDepth) + vm.frameFiles = truncFrames(vm.frameFiles, frameFilesDepth) + vm.frameCrefs = truncFrames(vm.frameCrefs, frameCrefsDepth) + vm.frameMethods = truncFrames(vm.frameMethods, frameMethodsDepth) + vm.requireDirs = truncFrames(vm.requireDirs, requireDirsDepth) if sig.abort { bodyErr = t.Abort() } else { diff --git a/internal/vm/wave33_ivar_bench_test.go b/internal/vm/wave33_ivar_bench_test.go new file mode 100644 index 00000000..e74253d8 --- /dev/null +++ b/internal/vm/wave33_ivar_bench_test.go @@ -0,0 +1,100 @@ +package vm_test + +import ( + "io" + "testing" + + "github.com/go-embedded-ruby/ruby/internal/compiler" + "github.com/go-embedded-ruby/ruby/internal/vm" + "github.com/go-ruby-parser/parser" +) + +// benchIvarProgram is benchProgram, kept here so these benchmarks can be run +// against a tree that does not have them yet (the before/after comparison for +// #672 copies this file onto the earlier revision). +func benchIvarProgram(b *testing.B, src string) { + prog, err := parser.Parse(src) + if err != nil { + b.Fatal(err) + } + iseq, err := compiler.Compile(prog) + if err != nil { + b.Fatal(err) + } + m := vm.New(io.Discard) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := m.Run(iseq); err != nil { + b.Fatal(err) + } + } +} + +// The ivar read/write path of a plain object: the hot case, and the one every +// Ruby program pays. It goes through setIvar/getIvar, which #672 rerouted through +// ivarStoreOf — the object's own map is still found by the same type switch, one +// arm in. +func BenchmarkIvarObjectAccessors(b *testing.B) { + benchIvarProgram(b, ` +class C + def initialize; @a = 0; @b = 0; end + def bump; @a += 1; @b = @a; end + def read; @a + @b; end +end +c = C.new +i = 0 +while i < 20000 + c.bump + c.read + i += 1 +end +`) +} + +// Allocating objects that take ivars: what a per-object ivar field or an eager +// table would have made more expensive. +func BenchmarkIvarObjectAlloc(b *testing.B) { + benchIvarProgram(b, ` +class P + def initialize(x, y); @x = x; @y = y; end + def sum; @x + @y; end +end +i = 0 +t = 0 +while i < 20000 + t += P.new(i, i).sum + i += 1 +end +`) +} + +// A String taking an instance variable is the case #672 made work at all: it +// costs one lookup in the VM-wide generic table, and the table entry is created +// by the first write. +func BenchmarkIvarOnString(b *testing.B) { + benchIvarProgram(b, ` +i = 0 +while i < 5000 + s = "carrier" + s.instance_variable_set(:@tag, i) + s.instance_variable_get(:@tag) + i += 1 +end +`) +} + +// Strings that never take an ivar, which is nearly all of them: the point of a +// lazily-created generic table is that this path is untouched — no field, no +// allocation, no map entry. +func BenchmarkStringsWithoutIvars(b *testing.B) { + benchIvarProgram(b, ` +i = 0 +n = 0 +while i < 20000 + s = "a string " + i.to_s + n += s.length + i += 1 +end +`) +} diff --git a/internal/vm/wave33_ivars_internal_test.go b/internal/vm/wave33_ivars_internal_test.go new file mode 100644 index 00000000..02f192bb --- /dev/null +++ b/internal/vm/wave33_ivars_internal_test.go @@ -0,0 +1,315 @@ +package vm + +import ( + "bytes" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/go-embedded-ruby/ruby/internal/compiler" + "github.com/go-embedded-ruby/ruby/internal/object" + "github.com/go-ruby-parser/parser" +) + +// runOnVM runs src on the given VM and returns its stdout trimmed, so a test can +// keep the VM and read what the run left behind. +func runOnVM(t *testing.T, vm *VM, out *bytes.Buffer, src string) string { + t.Helper() + prog, err := parser.Parse(src) + if err != nil { + t.Fatalf("parse: %v", err) + } + iseq, err := compiler.Compile(prog) + if err != nil { + t.Fatalf("compile: %v", err) + } + if _, err := vm.Run(iseq); err != nil { + t.Fatalf("run: %v", err) + } + return strings.TrimRight(out.String(), "\n") +} + +// frameDepths is the length of each of the three parallel frame stacks. They +// mirror each other one for one, so index i means the same frame in all three +// only while all three numbers agree. +type frameDepths struct{ names, crefs, methods int } + +func depthsOf(vm *VM) frameDepths { + return frameDepths{len(vm.frameNames), len(vm.frameCrefs), len(vm.frameMethods)} +} + +func (d frameDepths) aligned() bool { return d.names == d.crefs && d.names == d.methods } + +// installDepthProbe defines a Ruby method that records the three frame-stack +// depths at the moment Ruby calls it. The depths are per-VM state that Run +// resets on the way out, so they have to be read from INSIDE the program. +func installDepthProbe(vm *VM, name string, got *frameDepths) { + vm.cObject.define(name, func(vm *VM, _ object.Value, _ []object.Value, _ *Proc) object.Value { + *got = depthsOf(vm) + return object.NilV + }) +} + +// A natively-backed value now carries instance variables (#672). Before this, +// ivarTable answered nil for every one of these kinds, so the write was +// DISCARDED and the read answered nil — with nothing raised either way, which is +// what let a singleton attr_accessor look like it worked and store nothing. +// +// MRI stores these in the generic table: variable.c's ivar_set sends T_OBJECT to +// rb_obj_ivar_set, T_CLASS/T_MODULE to rb_class_ivar_set and everything else to +// generic_ivar_set. Every expectation below is MRI 4.0.5's own answer. +func TestGenericIvarsOnNativelyBackedValues(t *testing.T) { + var buf bytes.Buffer + src := ` +require 'stringio' +out = [[], {}, "s", proc {}, StringIO.new("x")].map do |o| + o.instance_variable_set(:@zz, 7) + [o.instance_variable_get(:@zz), o.instance_variables, o.instance_variable_defined?(:@zz)] +end +p out +` + // MRI 4.0.5's own output for this program, byte for byte. + want := "[[7, [:@zz], true], [7, [:@zz], true], [7, [:@zz], true], [7, [:@zz], true], [7, [:@zz], true]]" + if got := runOnVM(t, New(&buf), &buf, src); got != want { + t.Errorf("natively-backed ivars:\n got %s\nwant %s", got, want) + } +} + +// instance_variables reports the names of a natively-backed value in +// FIRST-ASSIGNMENT order (MRI's rb_ivar_foreach walks the shape chain in creation +// order), and remove_instance_variable takes one back out. +func TestGenericIvarNamesInAssignmentOrder(t *testing.T) { + var buf bytes.Buffer + src := ` +s = "order" +s.instance_variable_set(:@b, 2) +s.instance_variable_set(:@a, 1) +p s.instance_variables +s.remove_instance_variable(:@b) +p s.instance_variables +p s.instance_variable_defined?(:@b) +` + want := "[:@b, :@a]\n[:@a]\nfalse" + if got := runOnVM(t, New(&buf), &buf, src); got != want { + t.Errorf("order:\n got %q\nwant %q", got, want) + } +} + +// The idiom that made #672 visible: an attr_accessor on the singleton class of a +// natively-backed object (ruby/spec's FileSpecs.make_closer stores an exception +// that way) has to STORE, and `defined?` has to see it. +func TestSingletonAccessorOnNativeObjectStores(t *testing.T) { + var buf bytes.Buffer + src := ` +s = "carrier" +class << s + attr_accessor :payload +end +s.payload = :kept +p s.payload +p s.instance_eval { defined?(@payload) } +` + want := ":kept\n\"instance-variable\"" + if got := runOnVM(t, New(&buf), &buf, src); got != want { + t.Errorf("singleton accessor:\n got %q\nwant %q", got, want) + } +} + +// A read must NOT create a generic table. MRI's rb_gen_ivtbl_get only looks up, +// and rb_ivar_defined answers false for an object with no entry, so +// `defined?(@x)` and instance_variable_defined? leave nothing behind — which is +// what keeps a value that never takes an ivar free of cost. +func TestIvarTableReadCreatesNothing(t *testing.T) { + s := object.NewString("untouched") + if tbl := ivarTable(s); tbl != nil { + t.Fatalf("ivarTable on a fresh String = %v, want nil", tbl) + } + if got := ivarNamesInOrder(s); len(got) != 0 { + t.Fatalf("ivarNamesInOrder on a fresh String = %v, want empty", got) + } + if g := genericIvarsOf(s, false); g != nil { + t.Fatal("a read left a generic-table entry behind") + } + setIvar(s, "@a", object.Integer(1)) + if got := getIvar(s, "@a"); got != object.Value(object.Integer(1)) { + t.Fatalf("after setIvar, getIvar = %v, want 1", got) + } + if g := genericIvarsOf(s, false); g == nil { + t.Fatal("a write did not create a generic-table entry") + } + // A second write reuses the entry rather than replacing it. + first := genericIvarsOf(s, true) + setIvar(s, "@b", object.Integer(2)) + if second := genericIvarsOf(s, true); second != first { + t.Fatal("a second write created a second generic-table entry") + } + if got, want := len(ivarNamesInOrder(s)), 2; got != want { + t.Fatalf("names after two writes = %d, want %d", got, want) + } +} + +// An immediate keys nothing: it is a Go value, so a copy would answer to the +// entry of every equal copy, and MRI screens immediates out of the ivar API +// altogether (SPECIAL_CONST_P in rb_ivar_defined / rb_ivar_count). The visible +// Ruby path raises FrozenError before reaching here (every immediate is frozen), +// so what is asserted is that nothing is stored and nothing panics. +func TestImmediatesKeyNoGenericTable(t *testing.T) { + for _, v := range []object.Value{object.Integer(7), object.Float(1.5), object.Symbol("s"), object.Bool(true), object.NilV, nil} { + if hasIdentity(v) { + t.Errorf("hasIdentity(%#v) = true, want false", v) + } + setIvar(v, "@a", object.Integer(1)) + if got := getIvar(v, "@a"); got != object.Value(object.NilV) { + t.Errorf("getIvar(%#v) = %v, want nil", v, got) + } + if got := ivarNamesInOrder(v); len(got) != 0 { + t.Errorf("ivarNamesInOrder(%#v) = %v, want empty", v, got) + } + } + if !hasIdentity(object.NewString("x")) { + t.Error("hasIdentity(*object.String) = false, want true") + } +} + +// ivarStoreOf allocates the map of a kind that owns one lazily, so a value built +// without it (every RObject the VM makes gets one, but the zero value does not) +// takes an ivar rather than panicking on a nil map. A class records no +// assignment order, so its names come out in map order. +func TestIvarStoreOfAllocatesOwnTables(t *testing.T) { + o := &RObject{} + setIvar(o, "@a", object.Integer(1)) + if got := getIvar(o, "@a"); got != object.Value(object.Integer(1)) { + t.Fatalf("RObject with no map: getIvar = %v, want 1", got) + } + c := &RClass{name: "K"} + if tbl := ivarTable(c); tbl != nil { + t.Fatalf("ivarTable on a bare RClass = %v, want nil", tbl) + } + setIvar(c, "@only", object.Integer(2)) + if got := getIvar(c, "@only"); got != object.Value(object.Integer(2)) { + t.Fatalf("RClass with no map: getIvar = %v, want 2", got) + } + names := ivarNamesInOrder(c) + if len(names) != 1 || names[0] != object.Value(object.Symbol("@only")) { + t.Fatalf("class ivar names = %v, want [:@only]", names) + } + // The two template-context kinds allocate their map on FIRST TOUCH, read + // included, since a template reads an @ivar the controller may never have set. + for _, v := range []object.Value{&SinatraCtx{}, &ActionViewBase{}} { + if tbl := ivarTable(v); tbl == nil { + t.Errorf("%T: ivarTable did not lazily allocate the ivars map", v) + } + setIvar(v, "@ctx", object.Integer(3)) + if got := getIvar(v, "@ctx"); got != object.Value(object.Integer(3)) { + t.Errorf("%T: getIvar = %v, want 3", v, got) + } + } +} + +// Find.prune unwinds the block's frames straight to findYield's recover, which +// restores the tracking stacks by hand. It restored frameNames, frameFiles, +// fileStack and requireDirs and LEFT frameCrefs and frameMethods long (#651): +// index i then meant a different frame in each, and frameLabel — which reads all +// three since #649 — stopped naming the right method for the rest of the program. +func TestFindPruneKeepsFrameStacksAligned(t *testing.T) { + var buf bytes.Buffer + vm := New(&buf) + var before, after frameDepths + installDepthProbe(vm, "__depths_before", &before) + installDepthProbe(vm, "__depths_after", &after) + dir := t.TempDir() + src := ` +require 'find' +def labels; caller_locations(0, 2).map(&:label); end +class Box; def peek; labels; end; end +__depths_before +p Box.new.peek +Find.find(` + strconv.Quote(dir) + `) { |_p| Find.prune } +__depths_after +p Box.new.peek +` + got := runOnVM(t, vm, &buf, src) + if !before.aligned() || !after.aligned() || before != after { + t.Errorf("frame depths before=%+v after=%+v, want equal and aligned", before, after) + } + want := `["Object#labels", "Box#peek"]` + "\n" + `["Object#labels", "Box#peek"]` + if got != want { + t.Errorf("labels across a prune:\n got %s\nwant %s", got, want) + } +} + +// PStore#transaction with #commit unwinds the block the same way, through +// pstore.go's own recover, and left the same two stacks long. +func TestPStoreCommitKeepsFrameStacksAligned(t *testing.T) { + var buf bytes.Buffer + vm := New(&buf) + var before, after frameDepths + installDepthProbe(vm, "__depths_before", &before) + installDepthProbe(vm, "__depths_after", &after) + path := filepath.Join(t.TempDir(), "store.dat") + src := ` +require 'pstore' +def labels; caller_locations(0, 2).map(&:label); end +class Box; def peek; labels; end; end +__depths_before +store = PStore.new(` + strconv.Quote(path) + `) +store.transaction { |s| s[:k] = 1; s.commit } +store.transaction { |s| s[:k] = 2; s.abort } +__depths_after +p Box.new.peek +` + got := runOnVM(t, vm, &buf, src) + if !before.aligned() || !after.aligned() || before != after { + t.Errorf("frame depths before=%+v after=%+v, want equal and aligned", before, after) + } + if want := `["Object#labels", "Box#peek"]`; got != want { + t.Errorf("labels after a commit/abort:\n got %s\nwant %s", got, want) + } +} + +// An exception raised in an IRB statement unwinds past exec's frames to +// irbEval's recover, which restored frameNames and left frameCrefs and +// frameMethods long — for the rest of the session, since the binding outlives +// the statement that raised. +func TestIRBEvalExceptionKeepsFrameStacksAligned(t *testing.T) { + vm := New(&bytes.Buffer{}) + b := &Binding{env: &Env{}, self: vm.main, definee: vm.cObject} + before := depthsOf(vm) + if _, cls, _, raised := vm.irbEval(b, `def boom; raise "x"; end; boom`); !raised || cls != "RuntimeError" { + t.Fatalf("irbEval: raised=%v cls=%q, want a RuntimeError", raised, cls) + } + after := depthsOf(vm) + if !after.aligned() || after != before { + t.Errorf("frame depths before=%+v after=%+v, want equal and aligned", before, after) + } +} + +// A name taken out by remove_instance_variable and assigned again appears ONCE, +// at the END. MRI removes it from the shape (variable.c rb_ivar_delete through +// rb_shape_transition_shape_remove_ivar), so the re-assignment is a fresh one; +// rbgo kept the name in the order list and appended a second copy, which +// reported [:@a, :@b, :@a] where MRI reports [:@b, :@a]. The defect predates +// #672 for a plain object and would have reached every natively-backed kind +// with it. +func TestIvarReassignedAfterRemovalAppearsOnceAtTheEnd(t *testing.T) { + var buf bytes.Buffer + src := ` +o = Object.new +o.instance_variable_set(:@a, 1) +o.instance_variable_set(:@b, 2) +o.remove_instance_variable(:@a) +o.instance_variable_set(:@a, 3) +p o.instance_variables +s = "str" +s.instance_variable_set(:@a, 1) +s.remove_instance_variable(:@a) +s.instance_variable_set(:@a, 3) +p s.instance_variables +` + // MRI 4.0.5's own output for this program. + want := "[:@b, :@a]\n[:@a]" + if got := runOnVM(t, New(&buf), &buf, src); got != want { + t.Errorf("re-assignment after removal:\n got %s\nwant %s", got, want) + } +}