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
17 changes: 9 additions & 8 deletions internal/vm/backtrace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ end
// exact source, written to a file and run under ruby 4.0.5: the raise at 3,
// the call to foo at 6, the call to bar at 9.
want := strings.Join([]string{
script + ":3:in 'foo'",
script + ":6:in 'bar'",
script + ":3:in 'Object#foo'",
script + ":6:in 'Object#bar'",
script + ":9:in '<main>'",
}, "\n") + "\n"
if out != want {
Expand Down Expand Up @@ -123,8 +123,9 @@ end
if err != nil {
t.Fatal(err)
}
// ruby 4.0.5 on this source reports the raise inside deep at line 3.
if out != "/x/p.rb:3:in 'deep'\n" {
// ruby 4.0.5 on this source reports the raise inside deep at line 3, under
// the owner-qualified label rb_gen_method_name builds: 'Object#deep'.
if out != "/x/p.rb:3:in 'Object#deep'\n" {
t.Fatalf("got %q", out)
}
}
Expand Down Expand Up @@ -201,7 +202,7 @@ end
t.Fatal(err)
}
// ruby 4.0.5 on this source: the raise inside foo at 3, the call at 6.
want := "/x/p.rb:3:in 'foo': boom (ArgumentError)\n" +
want := "/x/p.rb:3:in 'Object#foo': boom (ArgumentError)\n" +
"\tfrom /x/p.rb:6:in '<main>'\n"
if out != want {
t.Fatalf("\n got %q\nwant %q", out, want)
Expand Down Expand Up @@ -259,7 +260,7 @@ boom
}
bt := re.Backtrace()
// ruby 4.0.5 on this source: the raise inside boom at 3, the call at 5.
want := []string{"/x/p.rb:3:in 'boom'", "/x/p.rb:5:in '<main>'"}
want := []string{"/x/p.rb:3:in 'Object#boom'", "/x/p.rb:5:in '<main>'"}
if strings.Join(bt, "|") != strings.Join(want, "|") {
t.Fatalf("Backtrace()\n got %v\nwant %v", bt, want)
}
Expand Down Expand Up @@ -297,8 +298,8 @@ a
if err != nil {
t.Fatal(err)
}
// ruby 4.0.5 on this source reports a's call to b at line 3.
if out != "/x/p.rb:3:in 'a'\n" {
// ruby 4.0.5 on this source reports a's call to b at line 3, owner-qualified.
if out != "/x/p.rb:3:in 'Object#a'\n" {
t.Fatalf("got %q", out)
}
}
Expand Down
11 changes: 11 additions & 0 deletions internal/vm/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,15 @@ func (vm *VM) bootstrap() {
fileStackDepth := len(vm.fileStack)
frameNamesDepth := len(vm.frameNames)
frameFilesDepth := len(vm.frameFiles)
// frameCrefs and frameMethods mirror frameNames one-for-one and have to be
// restored WITH it. Restoring only frameNames left the three at different
// depths for the rest of the program, so frameCrefs[i] and frameMethods[i]
// stopped describing frame i. Nothing noticed while a backtrace read
// frameNames alone; frameLabel now reads all three — the owner prefix comes
// from the cref and an aliased method's original name from frameMethods —
// and one leaked catch made every later label some other frame's.
frameCrefsDepth := len(vm.frameCrefs)
frameMethodsDepth := len(vm.frameMethods)
requireDirsDepth := len(vm.requireDirs)
// Register this catch's tag so a Kernel#throw can tell a matched throw (an
// unwind to here) from an unmatched one (an UncaughtThrowError). The depth
Expand All @@ -380,6 +389,8 @@ func (vm *VM) bootstrap() {
vm.fileStack = vm.fileStack[:fileStackDepth]
vm.frameNames = vm.frameNames[:frameNamesDepth]
vm.frameFiles = vm.frameFiles[:frameFilesDepth]
vm.frameCrefs = vm.frameCrefs[:frameCrefsDepth]
vm.frameMethods = vm.frameMethods[:frameMethodsDepth]
vm.requireDirs = vm.requireDirs[:requireDirsDepth]
result = sig.value
return
Expand Down
51 changes: 38 additions & 13 deletions internal/vm/exception.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,30 +391,55 @@ func (vm *VM) registerBacktraceLocation() {
loc.define("absolute_path", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
return getIvar(self, "@path")
})
// #base_label is the label WITHOUT the block qualifier. MRI reads it from a
// #base_label is the label with NO decoration at all. MRI reads it from a
// different field than #label does — location.base_label beside
// location.label (vm_backtrace.c v3_4_0:331) — and the difference is exactly
// the decoration calculate_iseq_label adds for a block frame: #label says
// "block (2 levels) in foo" where #base_label says "foo". Stripping that
// prefix reconstructs the same answer from the one label we carry.
// location.label (vm_backtrace.c v3_4_0:331) — and that field is the plain
// name the ISeq was compiled under, so it carries neither the "block in "
// that calculate_iseq_label adds for a block frame nor the "Owner#" that
// rb_gen_method_name adds for a method one. #label says
// "block (2 levels) in C#foo" where #base_label says "foo". Removing both
// decorations reconstructs the same answer from the one label we carry.
loc.define("base_label", func(_ *VM, self object.Value, _ []object.Value, _ *Proc) object.Value {
return object.NewString(stripBlockQualifier(getIvar(self, "@label").ToS()))
_, base := splitBlockQualifier(getIvar(self, "@label").ToS())
return object.NewString(stripOwnerPrefix(base))
})
}

// stripBlockQualifier removes the "block in " / "block (N levels) in " prefix a
// block frame's label carries, yielding the label of the scope the block was
// written in. A label without the prefix is returned unchanged.
func stripBlockQualifier(label string) string {
// splitBlockQualifier splits the "block in " / "block (N levels) in " decoration
// a block frame's label carries from the label of the scope the block was
// written in. A label without the decoration comes back with an empty qualifier.
func splitBlockQualifier(label string) (qual, base string) {
if rest, ok := strings.CutPrefix(label, "block in "); ok {
return rest
return "block in ", rest
}
if strings.HasPrefix(label, "block (") {
if i := strings.Index(label, " levels) in "); i >= 0 {
return label[i+len(" levels) in "):]
cut := i + len(" levels) in ")
return label[:cut], label[cut:]
}
}
return label
return "", label
}

// stripOwnerPrefix removes the "Owner#" / "Owner." that genMethodName puts in
// front of a method label, yielding the bare name MRI keeps in
// location.base_label.
//
// It removes a prefix only when what precedes the separator is a CONSTANT PATH,
// which is the only shape rb_mod_name0 can produce: a run of "::"-joined
// segments each starting with an upper-case letter. That test is what keeps it
// from eating part of a label that merely contains the separator — a method name
// cannot hold a "#" or a ".", but "<main>" and the other bracketed labels reach
// here too, and an undecorated label must come back untouched.
func stripOwnerPrefix(label string) string {
cut := strings.LastIndexAny(label, "#.")
if cut <= 0 || cut == len(label)-1 {
return label
}
if !isConstantPath(label[:cut]) {
return label
}
return label[cut+1:]
}

// backtraceLocation builds a Thread::Backtrace::Location from one captured
Expand Down
81 changes: 62 additions & 19 deletions internal/vm/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,8 @@ func (vm *VM) registerIO() {
// reads the category) sees it, as in MRI. A String category is converted to a
// Symbol (anything else is a TypeError); a category whose warnings are disabled
// (Warning[category] is false) is dropped, and an unknown category raises. The
// uplevel: keyword is validated (a negative or non-Integer value raises) but not
// acted on, as rbgo has no caller line to prepend. With no message it does
// nothing.
// uplevel: keyword names a caller frame whose "path:lineno: " is prepended to
// the message (see warnUplevelPrefix). With no message it does nothing.
vm.cObject.define("warn", func(vm *VM, _ object.Value, args []object.Value, _ *Proc) object.Value {
var category object.Value = object.NilV
pos := args
Expand All @@ -484,11 +483,14 @@ func (vm *VM) registerIO() {
if object.IsNil(vm.verboseSlot("$VERBOSE")) || len(pos) == 0 {
return object.NilV
}
lev := int64(0)
if !object.IsNil(uplevel) {
// rb_warn_m runs the level through NUM2LONG, so a Float or a Rational
// truncates and anything with no integer conversion is a TypeError.
if int64(vm.toIntCoerce(uplevel)) < 0 {
raise("ArgumentError", "negative level (%d)", vm.toIntCoerce(uplevel))
// truncates and anything with no integer conversion is a TypeError. The
// conversion runs ONCE: a to_int that counts its calls must not see two.
lev = int64(vm.toIntCoerce(uplevel))
if lev < 0 {
raise("ArgumentError", "negative level (%d)", lev)
}
}
// rb_warn_m keeps a lone message that already ends in a newline VERBATIM
Expand All @@ -502,13 +504,7 @@ func (vm *VM) registerIO() {
b.WriteString(first.Str())
} else {
if !object.IsNil(uplevel) {
// With a level, rb_warn_m prefixes "path:lineno: warning: " taken from
// rb_ec_backtrace_location_ary — and, when that yields no location (a
// level past the bottom of the stack), the bare "warning: ". rbgo
// records no line numbers at all, so no frame can ever supply a path:
// the no-location prefix is the truthful answer for every level, not a
// choice. Fixing it means line tracking in internal/compiler.
b.WriteString("warning: ")
b.WriteString(vm.warnUplevelPrefix(lev))
}
write := func(s string) { b.WriteString(s) }
for _, a := range pos {
Expand All @@ -519,12 +515,24 @@ func (vm *VM) registerIO() {
return object.NilV
}
if !object.IsNil(category) {
switch c := category.(type) {
case object.Symbol:
case *object.String:
category = object.Symbol(c.Str())
default:
raise("TypeError", "no implicit conversion of %s into Symbol", vm.classOf(category).name)
// rb_warn_m runs the category through rb_to_symbol_type, which is
// rb_convert_type_with_id(val, T_SYMBOL, "Symbol", idTo_sym): anything
// that answers #to_sym converts, and only a value that does not (or
// whose #to_sym returns a non-Symbol) is the TypeError. A String
// converts through its own String#to_sym, so it needs no case of its
// own here.
if _, isSym := category.(object.Symbol); !isSym {
cls := vm.classOf(category).name
if !vm.respondsToConversion(category, "to_sym") {
raise("TypeError", "no implicit conversion of %s into Symbol", cls)
}
conv := vm.send(category, "to_sym", nil, nil)
sym, isSym := conv.(object.Symbol)
if !isSym {
raise("TypeError", "can't convert %s to Symbol (%s#to_sym gives %s)",
cls, cls, vm.classOf(conv).name)
}
category = sym
}
// Warning[category] filters the message (and raises for an unknown one).
if !vm.send(vm.consts["Warning"], "[]", []object.Value{category}, nil).Truthy() {
Expand Down Expand Up @@ -662,6 +670,41 @@ func (vm *VM) registerIO() {
})
}

// warnUplevelPrefix is the "path:lineno: warning: " that Kernel#warn prepends
// when a uplevel: level is given — error.c v3_4_0 rb_warn_m, which takes the
// location from rb_ec_backtrace_location_ary(ec, lev + 1, 1, TRUE) and formats
// it as rb_sprintf("%s:%ld: warning: ", path, lineno); when that call yields no
// location (a level past the bottom of the stack) the prefix is the bare
// "warning: ".
//
// The `+ 1` in MRI skips the frame of the `warn` cfunc itself, so level 0 names
// the frame that CALLED warn. rbgo pushes no frame for a native method, so that
// frame is already the top of frameNames and the level indexes straight down
// from it: level 0 is len(frameNames)-1, level n is n frames further out. A
// level that walks off the bottom reports no location, which is what makes
// `warn "x", uplevel: 100` print "warning: x" rather than a fabricated frame.
//
// This used to be unconditionally "warning: ", because the parser's AST carried
// no source positions and no frame could supply a line. PR #645 built MRI's
// compressed insns_info/positions table on go-ruby-parser v0.3.0's
// ast.Program.Lines, so frameLine is now calc_lineno over the frame's
// (iseq, pc) pair and there IS a caller line to prepend.
func (vm *VM) warnUplevelPrefix(lev int64) string {
// len(frameNames) is an int, so a level that cannot even be held in one is
// past the bottom of any stack — checked before the conversion narrows it.
if lev > int64(len(vm.frameNames)) {
return "warning: "
}
i := len(vm.frameNames) - 1 - int(lev)
if i < 0 {
return "warning: "
}
// MRI formats the location unconditionally once it HAS one, so a frame whose
// line is unknown reports ":0:" rather than dropping to the bare prefix — the
// path is still the truth about where the caller is.
return vm.frameFileLabel(i) + ":" + strconv.Itoa(vm.frameLine(i)) + ": warning: "
}

// File open-mode flag constants (File::RDWR etc.). The values are the canonical
// POSIX ones, fixed here so behaviour is identical on every OS the test gate runs
// on rather than reflecting the host's <fcntl.h>.
Expand Down
Loading
Loading