Skip to content

Commit eae3f87

Browse files
authored
interp: say why a rollback happened (#5989)
When interp cannot evaluate an initializer at compile time it rolls back and runs it at runtime. The only record of why is the recoverable error printed by -internal-dumpssa, and most rollbacks share one sentinel, "unsupported instruction (to be emitted at runtime)". It covers a branch on a value only known at runtime, an instruction that cannot be emitted at runtime, a goroutine that would block, and a function with a defer, so the cause has to be reconstructed by reading the surrounding IR. This change builds the error at the point of rollback, naming the cause, the instruction where relevant, and the function being interpreted, for example "interp: cannot emit instruction at runtime: icmp (in main.init$1)". Sentinels that are compared by identity are unchanged. The unused errMapAlreadyCreated is removed. Each rollback now allocates its own error. BenchmarkRecoverableErrors shows one extra allocation per rollback (+0.73% allocs/op, +0.88% B/op) and no change in time.
1 parent 3e562c6 commit eae3f87

4 files changed

Lines changed: 82 additions & 15 deletions

File tree

‎interp/compiler.go‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ func (inst *instruction) String() string {
5151
for i, op := range inst.operands {
5252
operands[i] = op.String()
5353
}
54+
return inst.opcodeName() + " " + strings.Join(operands, " ")
55+
}
5456

57+
// opcodeName returns the name of the instruction's opcode.
58+
func (inst *instruction) opcodeName() string {
5559
name := ""
5660
if inst.branch {
5761
name = "br"
@@ -61,7 +65,7 @@ func (inst *instruction) String() string {
6165
if name == "" {
6266
name = "<unknown op>"
6367
}
64-
return name + " " + strings.Join(operands, " ")
68+
return name
6569
}
6670

6771
// compileFunction compiles a given LLVM function to an easier to interpret

‎interp/errors.go‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,26 @@ import (
1313

1414
type recoverableError struct {
1515
msg string
16+
op string // instruction involved, if any
17+
fn string // function being interpreted, if known
1618
}
1719

1820
func (e *recoverableError) Error() string {
19-
return e.msg
21+
s := e.msg
22+
if e.op != "" {
23+
s += ": " + e.op
24+
}
25+
if e.fn != "" {
26+
s += " (in " + e.fn + ")"
27+
}
28+
return s
2029
}
2130

2231
// These errors are expected during normal execution and can be recovered from
2332
// by running the affected function at runtime instead of compile time.
2433
var (
25-
errIntegerAsPointer = &recoverableError{msg: "interp: trying to use an integer as a pointer (memory-mapped I/O?)"}
26-
errUnsupportedInst = &recoverableError{msg: "interp: unsupported instruction"}
27-
errUnsupportedRuntimeInst = &recoverableError{msg: "interp: unsupported instruction (to be emitted at runtime)"}
28-
errDeferUnsupported = &recoverableError{msg: "interp: cannot partially evaluate a function with defer"}
29-
errMapAlreadyCreated = &recoverableError{msg: "interp: map already created"}
30-
errLoopUnrolled = &recoverableError{msg: "interp: loop unrolled"}
31-
errLoopTooLong = &recoverableError{msg: "interp: loop ran too many iterations"}
34+
errIntegerAsPointer = &recoverableError{msg: "interp: trying to use an integer as a pointer (memory-mapped I/O?)"}
35+
errUnsupportedInst = &recoverableError{msg: "interp: unsupported instruction"}
3236

3337
// This is one of the errors that can be returned from toLLVMValue when the
3438
// passed type does not fit the data to serialize. It is recoverable by

‎interp/interp_test.go‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,52 @@ func TestInterpPartialPointer(t *testing.T) {
234234
t.Errorf("expected %v, got %v", errUnsupportedInst, err)
235235
}
236236
}
237+
238+
// TestInterpRollbackReason checks that the error that makes interp roll back
239+
// names its cause and the function being interpreted.
240+
func TestInterpRollbackReason(t *testing.T) {
241+
t.Parallel()
242+
path := filepath.Join(t.TempDir(), "rollback.ll")
243+
ir := `target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
244+
target triple = "x86_64--linux"
245+
246+
define i64 @fill(i64 %n) {
247+
entry:
248+
br label %loop
249+
loop:
250+
%i = phi i64 [ 0, %entry ], [ %next, %loop ]
251+
%next = add i64 %i, 1
252+
%done = icmp eq i64 %next, %n
253+
br i1 %done, label %exit, label %loop
254+
exit:
255+
ret i64 %next
256+
}
257+
`
258+
if err := os.WriteFile(path, []byte(ir), 0o666); err != nil {
259+
t.Fatal(err)
260+
}
261+
ctx := llvm.NewContext()
262+
defer ctx.Dispose()
263+
buf, err := llvm.NewMemoryBufferFromFile(path)
264+
if err != nil {
265+
t.Fatal(err)
266+
}
267+
mod, err := ctx.ParseIR(buf)
268+
if err != nil {
269+
t.Fatal(err)
270+
}
271+
defer mod.Dispose()
272+
273+
r := newRunner(mod, time.Minute, 10, false)
274+
defer r.dispose()
275+
_, _, runErr := r.run(r.getFunction(mod.NamedFunction("fill")), []value{literalValue{uint64(100)}}, nil, "")
276+
if runErr == nil {
277+
t.Fatal("expected the loop limit to stop interpretation")
278+
}
279+
if !isRecoverableError(runErr.Err) {
280+
t.Errorf("expected a recoverable error, got %v", runErr.Err)
281+
}
282+
if want := "interp: loop ran too many iterations (in fill)"; runErr.Err.Error() != want {
283+
t.Errorf("expected %q, got %q", want, runErr.Err.Error())
284+
}
285+
}

‎interp/interpreter.go‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent
5757
}
5858
blockCounts[currentBB]++
5959
if r.maxLoopIterations > 0 && blockCounts[currentBB] > r.maxLoopIterations {
60-
return nil, mem, r.errorAt(bb.instructions[0], errLoopTooLong)
60+
err := &recoverableError{msg: "interp: loop ran too many iterations", fn: fn.name}
61+
return nil, mem, r.errorAt(bb.instructions[0], err)
6162
}
6263

6364
if len(mem.instructions) != startRTInsts {
@@ -66,7 +67,8 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent
6667
// Avoid doing this, as it can result in a large amount of extra machine code.
6768
// This currently uses the branch from the last block, as there is no available information to give a better location.
6869
lastBBInsts := fn.blocks[lastBB].instructions
69-
return nil, mem, r.errorAt(lastBBInsts[len(lastBBInsts)-1], errLoopUnrolled)
70+
err := &recoverableError{msg: "interp: loop unrolled", fn: fn.name}
71+
return nil, mem, r.errorAt(lastBBInsts[len(lastBBInsts)-1], err)
7072
}
7173

7274
// Flag the last block as having run stuff at runtime.
@@ -221,7 +223,8 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent
221223
switch inst.opcode {
222224
case llvm.Ret:
223225
if !fn.deferFrame.IsNil() && len(mem.instructions) != 0 {
224-
return nil, mem, r.errorAt(inst, errDeferUnsupported)
226+
err := &recoverableError{msg: "interp: cannot partially evaluate a function with defer", fn: fn.name}
227+
return nil, mem, r.errorAt(inst, err)
225228
}
226229
if time.Since(r.start) > r.timeout {
227230
// Running for more than the allowed timeout; This shouldn't happen, but it does.
@@ -368,7 +371,8 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent
368371
}
369372
case callFn.name == "internal/task.Pause":
370373
// Task scheduling isn't possible at compile time.
371-
return nil, mem, r.errorAt(inst, errUnsupportedRuntimeInst)
374+
err := &recoverableError{msg: "interp: goroutine would block", fn: fn.name}
375+
return nil, mem, r.errorAt(inst, err)
372376
case callFn.name == "runtime.fastrand" || callFn.name == "runtime.fastrand64":
373377
// Use a local sequence instead of the global RNG state. The
374378
// state is replaced by initRand before initAll runs anyway.
@@ -1177,7 +1181,8 @@ func intOperands(r *runner, lhs, rhs value) (int64, int64, error) {
11771181

11781182
func (r *runner) runAtRuntime(fn *function, inst instruction, locals []value, mem *memoryView, indent string) *Error {
11791183
if !fn.deferFrame.IsNil() {
1180-
return r.errorAt(inst, errDeferUnsupported)
1184+
err := &recoverableError{msg: "interp: cannot partially evaluate a function with defer", op: inst.opcodeName(), fn: fn.name}
1185+
return r.errorAt(inst, err)
11811186
}
11821187
numOperands := inst.llvmInst.OperandsCount()
11831188
operands := make([]llvm.Value, numOperands)
@@ -1286,7 +1291,12 @@ func (r *runner) runAtRuntime(fn *function, inst instruction, locals []value, me
12861291
case llvm.ZExt:
12871292
result = r.builder.CreateZExt(operands[0], inst.llvmInst.Type(), inst.name)
12881293
default:
1289-
return r.errorAt(inst, errUnsupportedRuntimeInst)
1294+
if inst.branch || inst.opcode == llvm.Switch || inst.opcode == llvm.IndirectBr {
1295+
err := &recoverableError{msg: "interp: branch on a value not known at compile time", fn: fn.name}
1296+
return r.errorAt(inst, err)
1297+
}
1298+
err := &recoverableError{msg: "interp: cannot emit instruction at runtime", op: inst.opcodeName(), fn: fn.name}
1299+
return r.errorAt(inst, err)
12901300
}
12911301
locals[inst.localIndex] = localValue{result}
12921302
mem.instructions = append(mem.instructions, result)

0 commit comments

Comments
 (0)