From 84f64590edbb5666cbf669b3afc8e5c2231afa94 Mon Sep 17 00:00:00 2001 From: Dave Cheney Date: Fri, 23 Oct 2026 08:45:13 +1100 Subject: [PATCH] interp: name the function of a rollback from its instruction Each rollback site builds a recoverableError that records its message, the function being interpreted and, at two sites, the opcode of the failing instruction. errorAt already stores that instruction in the Error it returns, so every site repeats the function, which the Error can derive, and keeps the opcode apart from a message that could simply include it. This change adds rollbackAt, which builds a recoverableError from a message and wraps it with errorAt, and has Error name the function of any recoverable error when it is formatted. Rollback messages stay ad hoc, written where the rollback happens, and the two that name an opcode include it in the message. The messages are unchanged except that an instruction the interpreter does not model now names its opcode, and the remaining recoverable errors, such as an integer used as a pointer, now also name the function. --- interp/compiler.go | 12 ++++++------ interp/errors.go | 34 ++++++++++++++++++---------------- interp/interp_test.go | 4 ++-- interp/interpreter.go | 23 ++++++++--------------- 4 files changed, 34 insertions(+), 39 deletions(-) diff --git a/interp/compiler.go b/interp/compiler.go index 8e0cf3549b..babeb50821 100644 --- a/interp/compiler.go +++ b/interp/compiler.go @@ -51,16 +51,16 @@ func (inst *instruction) String() string { for i, op := range inst.operands { operands[i] = op.String() } - return inst.opcodeName() + " " + strings.Join(operands, " ") + return opcodeName(inst.llvmInst) + " " + strings.Join(operands, " ") } -// opcodeName returns the name of the instruction's opcode. -func (inst *instruction) opcodeName() string { +// opcodeName returns the name of the opcode of inst. +func opcodeName(inst llvm.Value) string { name := "" - if inst.branch { + if isBranch(inst) { name = "br" - } else if int(inst.opcode) < len(instructionNameMap) { - name = instructionNameMap[inst.opcode] + } else if opcode := inst.InstructionOpcode(); int(opcode) < len(instructionNameMap) { + name = instructionNameMap[opcode] } if name == "" { name = "" diff --git a/interp/errors.go b/interp/errors.go index 8616299e70..737b8e2f53 100644 --- a/interp/errors.go +++ b/interp/errors.go @@ -12,32 +12,23 @@ import ( ) type recoverableError struct { - msg string - op string // instruction involved, if any - fn string // function being interpreted, if known + reason string } func (e *recoverableError) Error() string { - s := e.msg - if e.op != "" { - s += ": " + e.op - } - if e.fn != "" { - s += " (in " + e.fn + ")" - } - return s + return e.reason } // These errors are expected during normal execution and can be recovered from // by running the affected function at runtime instead of compile time. var ( - errIntegerAsPointer = &recoverableError{msg: "interp: trying to use an integer as a pointer (memory-mapped I/O?)"} - errUnsupportedInst = &recoverableError{msg: "interp: unsupported instruction"} + errIntegerAsPointer = &recoverableError{reason: "interp: trying to use an integer as a pointer (memory-mapped I/O?)"} + errUnsupportedInst = &recoverableError{reason: "interp: unsupported instruction"} // This is one of the errors that can be returned from toLLVMValue when the // passed type does not fit the data to serialize. It is recoverable by // serializing without a type (using rawValue.rawLLVMValue). - errInvalidPtrToIntSize = &recoverableError{msg: "interp: ptrtoint integer size does not equal pointer size"} + errInvalidPtrToIntSize = &recoverableError{reason: "interp: ptrtoint integer size does not equal pointer size"} ) func isRecoverableError(err error) bool { @@ -68,9 +59,14 @@ type Error struct { inst llvm.Value } -// Error returns the string of the first error in the list of errors. +// Error returns the string of the first error in the list of errors. A +// recoverable error also names the function it happened in. func (e *Error) Error() string { - return e.Pos.String() + ": " + e.Err.Error() + s := e.Pos.String() + ": " + e.Err.Error() + if isRecoverableError(e.Err) && !e.inst.IsNil() { + s += " (in " + e.inst.InstructionParent().Parent().Name() + ")" + } + return s } // errorAt returns an error value for the currently interpreted package at the @@ -87,6 +83,12 @@ func (r *runner) errorAt(inst instruction, err error) *Error { } } +// rollbackAt returns a recoverable error at the location of the instruction, +// which makes interp run the function at runtime instead. +func (r *runner) rollbackAt(inst instruction, reason string) *Error { + return r.errorAt(inst, &recoverableError{reason: "interp: " + reason}) +} + // materialize fills in the printed instructions of e and its traceback. It // must be called before the error is returned out of the interp package. func (e *Error) materialize() *Error { diff --git a/interp/interp_test.go b/interp/interp_test.go index b3c77b5e4a..ea1fb8578c 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -279,7 +279,7 @@ exit: if !isRecoverableError(runErr.Err) { t.Errorf("expected a recoverable error, got %v", runErr.Err) } - if want := "interp: loop ran too many iterations (in fill)"; runErr.Err.Error() != want { - t.Errorf("expected %q, got %q", want, runErr.Err.Error()) + if want := ": interp: loop ran too many iterations (in fill)"; !strings.HasSuffix(runErr.Error(), want) { + t.Errorf("expected an error ending in %q, got %q", want, runErr.Error()) } } diff --git a/interp/interpreter.go b/interp/interpreter.go index 2b6b5af98b..b3b946c332 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -57,8 +57,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent } blockCounts[currentBB]++ if r.maxLoopIterations > 0 && blockCounts[currentBB] > r.maxLoopIterations { - err := &recoverableError{msg: "interp: loop ran too many iterations", fn: fn.name} - return nil, mem, r.errorAt(bb.instructions[0], err) + return nil, mem, r.rollbackAt(bb.instructions[0], "loop ran too many iterations") } if len(mem.instructions) != startRTInsts { @@ -67,8 +66,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // Avoid doing this, as it can result in a large amount of extra machine code. // This currently uses the branch from the last block, as there is no available information to give a better location. lastBBInsts := fn.blocks[lastBB].instructions - err := &recoverableError{msg: "interp: loop unrolled", fn: fn.name} - return nil, mem, r.errorAt(lastBBInsts[len(lastBBInsts)-1], err) + return nil, mem, r.rollbackAt(lastBBInsts[len(lastBBInsts)-1], "loop unrolled") } // Flag the last block as having run stuff at runtime. @@ -223,8 +221,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent switch inst.opcode { case llvm.Ret: if !fn.deferFrame.IsNil() && len(mem.instructions) != 0 { - err := &recoverableError{msg: "interp: cannot partially evaluate a function with defer", fn: fn.name} - return nil, mem, r.errorAt(inst, err) + return nil, mem, r.rollbackAt(inst, "cannot partially evaluate a function with defer") } if time.Since(r.start) > r.timeout { // Running for more than the allowed timeout; This shouldn't happen, but it does. @@ -371,8 +368,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent } case callFn.name == "internal/task.Pause": // Task scheduling isn't possible at compile time. - err := &recoverableError{msg: "interp: goroutine would block", fn: fn.name} - return nil, mem, r.errorAt(inst, err) + return nil, mem, r.rollbackAt(inst, "goroutine would block") case callFn.name == "runtime.fastrand" || callFn.name == "runtime.fastrand64": // Use a local sequence instead of the global RNG state. The // state is replaced by initRand before initAll runs anyway. @@ -1096,7 +1092,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent if r.debug { fmt.Fprintln(os.Stderr, indent+inst.String()) } - return nil, mem, r.errorAt(inst, errUnsupportedInst) + return nil, mem, r.rollbackAt(inst, "unsupported instruction: "+opcodeName(inst.llvmInst)) } } return nil, mem, r.errorAt(bb.instructions[len(bb.instructions)-1], errors.New("interp: reached end of basic block without terminator")) @@ -1181,8 +1177,7 @@ func intOperands(r *runner, lhs, rhs value) (int64, int64, error) { func (r *runner) runAtRuntime(fn *function, inst instruction, locals []value, mem *memoryView, indent string) *Error { if !fn.deferFrame.IsNil() { - err := &recoverableError{msg: "interp: cannot partially evaluate a function with defer", op: inst.opcodeName(), fn: fn.name} - return r.errorAt(inst, err) + return r.rollbackAt(inst, "cannot partially evaluate a function with defer: "+opcodeName(inst.llvmInst)) } numOperands := inst.llvmInst.OperandsCount() operands := make([]llvm.Value, numOperands) @@ -1292,11 +1287,9 @@ func (r *runner) runAtRuntime(fn *function, inst instruction, locals []value, me result = r.builder.CreateZExt(operands[0], inst.llvmInst.Type(), inst.name) default: if inst.branch || inst.opcode == llvm.Switch || inst.opcode == llvm.IndirectBr { - err := &recoverableError{msg: "interp: branch on a value not known at compile time", fn: fn.name} - return r.errorAt(inst, err) + return r.rollbackAt(inst, "branch on a value not known at compile time") } - err := &recoverableError{msg: "interp: cannot emit instruction at runtime", op: inst.opcodeName(), fn: fn.name} - return r.errorAt(inst, err) + return r.rollbackAt(inst, "cannot emit instruction at runtime: "+opcodeName(inst.llvmInst)) } locals[inst.localIndex] = localValue{result} mem.instructions = append(mem.instructions, result)