From 992c71571572ac172def8aa11522f260d0e82a29 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:00:20 -0700 Subject: [PATCH 1/4] interp: evaluate memset during initialization Zeroing local storage should not force otherwise constant initializers to run at startup. Evaluate nonvolatile fills when their destination is known and has not escaped. --- interp/interpreter.go | 38 +++++++++++++++++++++++++++++++++++++ interp/testdata/copy.ll | 28 +++++++++++++++++++++++++++ interp/testdata/copy.out.ll | 9 +++++++++ 3 files changed, 75 insertions(+) diff --git a/interp/interpreter.go b/interp/interpreter.go index 1f6bc060bc..f249867e80 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -409,6 +409,44 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent result = max(lhs, rhs) } locals[inst.localIndex] = makeLiteralInt(uint64(result), inst.llvmInst.Type().IntTypeWidth()) + case strings.HasPrefix(callFn.name, "llvm.memset.p0"): + nBytes, err := operands[3].Uint(r) + if err != nil { + return nil, mem, r.errorAt(inst, err) + } + volatile, err := operands[4].Uint(r) + if err != nil { + return nil, mem, r.errorAt(inst, err) + } + if volatile != 0 { + if err := r.runAtRuntime(fn, inst, locals, &mem, indent); err != nil { + return nil, mem, err + } + continue + } + if nBytes == 0 { + continue + } + dst, err := operands[1].asPointer(r) + if err != nil { + return nil, mem, r.errorAt(inst, err) + } + obj := mem.get(dst.index()) + if mem.hasExternalLoadOrStore(dst) || obj.buffer == nil || uint64(dst.offset())+nBytes > uint64(obj.size) { + if err := r.runAtRuntime(fn, inst, locals, &mem, indent); err != nil { + return nil, mem, err + } + continue + } + fill, err := operands[2].Uint(r) + if err != nil { + return nil, mem, r.errorAt(inst, err) + } + buf := newRawValue(uint32(nBytes)) + for i := range buf.buf { + buf.buf[i] = fill + } + mem.store(buf, dst) case strings.HasPrefix(callFn.name, "llvm.memcpy.p0") || strings.HasPrefix(callFn.name, "llvm.memmove.p0"): // Copy a block of memory from one pointer to another. if operands[4].(literalValue).value.(uint8) != 0 { diff --git a/interp/testdata/copy.ll b/interp/testdata/copy.ll index f389be340a..f3c1887ba8 100644 --- a/interp/testdata/copy.ll +++ b/interp/testdata/copy.ll @@ -13,6 +13,9 @@ target triple = "x86_64--linux" @volatileSrc = global [2 x i8] c"xy" @volatileDst = global [2 x i8] zeroinitializer +@fillDst = global [4 x i8] c"ABCD" +@fillEscapedDst = global [4 x i8] c"ABCD" +@fillVolatileDst = global [4 x i8] c"ABCD" declare void @use(ptr) @@ -27,6 +30,9 @@ define internal void @main.init() { call void @testMoveExternal() call void @testMoveEscaped() call void @testVolatileCopy() + call void @testFill() + call void @testFillEscaped() + call void @testVolatileFill() ret void } @@ -66,3 +72,25 @@ define internal void @testVolatileCopy() { declare void @llvm.memmove.p0.p0.i64(ptr, ptr, i64, i1) declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1) + +define internal void @testFill() { + %dst = getelementptr [4 x i8], ptr @fillDst, i64 0, i64 1 + call void @llvm.memset.p0.i64(ptr %dst, i8 127, i64 2, i1 false) + call void @llvm.memset.p0.i64(ptr null, i8 0, i64 0, i1 false) + ret void +} + +define internal void @testFillEscaped() { + call void @use(ptr @fillEscapedDst) + %dst = getelementptr [4 x i8], ptr @fillEscapedDst, i64 0, i64 1 + call void @llvm.memset.p0.i64(ptr %dst, i8 127, i64 2, i1 false) + ret void +} + +define internal void @testVolatileFill() { + %dst = getelementptr [4 x i8], ptr @fillVolatileDst, i64 0, i64 1 + call void @llvm.memset.p0.i64(ptr %dst, i8 127, i64 2, i1 true) + ret void +} + +declare void @llvm.memset.p0.i64(ptr, i8, i64, i1) diff --git a/interp/testdata/copy.out.ll b/interp/testdata/copy.out.ll index dcabd7b72d..81d1cfda35 100644 --- a/interp/testdata/copy.out.ll +++ b/interp/testdata/copy.out.ll @@ -9,6 +9,9 @@ target triple = "x86_64--linux" @moveEscapedDst = local_unnamed_addr global [4 x i8] zeroinitializer @volatileSrc = global [2 x i8] c"xy" @volatileDst = global [2 x i8] zeroinitializer +@fillDst = local_unnamed_addr global [4 x i8] c"A\7F\7FD" +@fillEscapedDst = global [4 x i8] c"ABCD" +@fillVolatileDst = global [4 x i8] c"ABCD" declare void @use(ptr) local_unnamed_addr @@ -17,6 +20,9 @@ define void @runtime.initAll() local_unnamed_addr { call void @use(ptr @moveEscapedSrc) call void @llvm.memmove.p0.p0.i64(ptr @moveEscapedDst, ptr @moveEscapedSrc, i64 4, i1 false) call void @llvm.memcpy.p0.p0.i64(ptr @volatileDst, ptr @volatileSrc, i64 2, i1 true) + call void @use(ptr @fillEscapedDst) + call void @llvm.memset.p0.i64(ptr getelementptr inbounds (i8, ptr @fillEscapedDst, i32 1), i8 127, i64 2, i1 false) + call void @llvm.memset.p0.i64(ptr getelementptr inbounds (i8, ptr @fillVolatileDst, i32 1), i8 127, i64 2, i1 true) ret void } @@ -24,4 +30,7 @@ declare void @llvm.memmove.p0.p0.i64(ptr nocapture writeonly, ptr nocapture read declare void @llvm.memcpy.p0.p0.i64(ptr noalias nocapture writeonly, ptr noalias nocapture readonly, i64, i1 immarg) #0 +declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg) #1 + attributes #0 = { nocallback nofree nounwind willreturn memory(argmem: readwrite) } +attributes #1 = { nocallback nofree nounwind willreturn memory(argmem: write) } From 94e6296d1f9c80f0010aa8c04141925b697c350c Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:00:20 -0700 Subject: [PATCH 2/4] interp: evaluate private atomic operations during initialization Atomic state can be initialized before it becomes visible to other execution contexts. Evaluate operations on known storage while retaining runtime accesses once that storage has escaped. --- interp/interpreter.go | 51 +++++++++++++++++++++++++++++++++-- interp/testdata/revert.ll | 34 +++++++++++++++++++++++ interp/testdata/revert.out.ll | 27 ++++++++++++++++--- 3 files changed, 106 insertions(+), 6 deletions(-) diff --git a/interp/interpreter.go b/interp/interpreter.go index f249867e80..af3fcfe15a 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -272,6 +272,53 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent } callFn := r.getFunction(fnPtr.llvmValue(&mem)) switch { + case callFn.name == "sync/atomic.SwapInt32" || callFn.name == "sync/atomic.SwapInt64" || + callFn.name == "sync/atomic.SwapUint32" || callFn.name == "sync/atomic.SwapUint64" || + callFn.name == "sync/atomic.SwapUintptr" || callFn.name == "sync/atomic.SwapPointer" || + callFn.name == "sync/atomic.CompareAndSwapInt32" || callFn.name == "sync/atomic.CompareAndSwapInt64" || + callFn.name == "sync/atomic.CompareAndSwapUint32" || callFn.name == "sync/atomic.CompareAndSwapUint64" || + callFn.name == "sync/atomic.CompareAndSwapUintptr" || callFn.name == "sync/atomic.CompareAndSwapPointer": + ptr, err := operands[1].asPointer(r) + if err != nil { + return nil, mem, r.errorAt(inst, err) + } + if mem.hasExternalLoadOrStore(ptr) { + if err := r.runAtRuntime(fn, inst, locals, &mem, indent); err != nil { + return nil, mem, err + } + continue + } + old := mem.load(ptr, operands[2].len(r)) + if old == nil { + if err := r.runAtRuntime(fn, inst, locals, &mem, indent); err != nil { + return nil, mem, err + } + continue + } + newValue := operands[2] + compare := strings.HasPrefix(callFn.name, "sync/atomic.CompareAndSwap") + if compare { + equal, err := r.interpretICmp(old, operands[2], llvm.IntEQ) + if err != nil { + return nil, mem, r.errorAt(inst, err) + } + if !equal { + locals[inst.localIndex] = literalValue{uint8(0)} + continue + } + newValue = operands[3] + } + if !mem.store(newValue, ptr) { + if err := r.runAtRuntime(fn, inst, locals, &mem, indent); err != nil { + return nil, mem, err + } + continue + } + if compare { + locals[inst.localIndex] = literalValue{uint8(1)} + } else { + locals[inst.localIndex] = old + } case callFn.name == "runtime.trackPointer": // Allocas and such are created as globals, so don't need a // runtime.trackPointer. @@ -647,7 +694,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent return nil, mem, r.errorAt(inst, err) } size := operands[1].(literalValue).value.(uint64) - if inst.llvmInst.IsVolatile() || inst.llvmInst.Ordering() != llvm.AtomicOrderingNotAtomic || mem.hasExternalStore(ptr) { + if inst.llvmInst.IsVolatile() || mem.hasExternalStore(ptr) { // If there could be an external store (for example, because a // pointer to the object was passed to a function that could not // be interpreted at compile time) then the load must be done at @@ -677,7 +724,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent if err != nil { return nil, mem, r.errorAt(inst, err) } - if inst.llvmInst.IsVolatile() || inst.llvmInst.Ordering() != llvm.AtomicOrderingNotAtomic || mem.hasExternalLoadOrStore(ptr) { + if inst.llvmInst.IsVolatile() || mem.hasExternalLoadOrStore(ptr) { err := r.runAtRuntime(fn, inst, locals, &mem, indent) if err != nil { return nil, mem, err diff --git a/interp/testdata/revert.ll b/interp/testdata/revert.ll index e82c0fb78b..9a757e4912 100644 --- a/interp/testdata/revert.ll +++ b/interp/testdata/revert.ll @@ -5,6 +5,10 @@ declare void @externalCall(i64) declare i64 @ptrHash(ptr nocapture) +declare void @modifyAtomic(ptr) +declare i32 @"sync/atomic.SwapUint32"(ptr, i32, ptr) +declare i1 @"sync/atomic.CompareAndSwapUint32"(ptr, i32, i32, ptr) + @foo.knownAtRuntime = global i64 0 @bar.knownAtRuntime = global i64 0 @baz.someGlobal = external global [3 x {i64, i32}] @@ -14,6 +18,14 @@ declare i64 @ptrHash(ptr nocapture) @y.ready = global i32 0 @z.bloom = global i64 0 @z.arr = global [32 x i8] zeroinitializer +@atomic.value = global i32 0 +@atomic.old = global i32 0 +@atomic.success = global i1 false +@atomic.failure = global i1 false +@atomic.seen = global i32 0 +@atomic.runtimeOld = global i32 0 +@atomic.runtimeSuccess = global i1 false +@atomic.runtimeSeen = global i32 0 define void @runtime.initAll() unnamed_addr { entry: @@ -22,6 +34,7 @@ entry: call void @bar.init(ptr undef) call void @main.init(ptr undef) call void @x.init(ptr undef) + call void @atomic.init(ptr undef) call void @y.init(ptr undef) call void @z.init(ptr undef) ret void @@ -78,6 +91,27 @@ end: ret void } +define internal void @atomic.init(ptr %context) unnamed_addr { + store atomic i32 7, ptr @atomic.value seq_cst, align 4 + %old = call i32 @"sync/atomic.SwapUint32"(ptr @atomic.value, i32 9, ptr undef) + store i32 %old, ptr @atomic.old + %success = call i1 @"sync/atomic.CompareAndSwapUint32"(ptr @atomic.value, i32 9, i32 11, ptr undef) + store i1 %success, ptr @atomic.success + %failure = call i1 @"sync/atomic.CompareAndSwapUint32"(ptr @atomic.value, i32 9, i32 13, ptr undef) + store i1 %failure, ptr @atomic.failure + %seen = load atomic i32, ptr @atomic.value seq_cst, align 4 + store i32 %seen, ptr @atomic.seen + call void @modifyAtomic(ptr @atomic.value) + %runtimeOld = call i32 @"sync/atomic.SwapUint32"(ptr @atomic.value, i32 15, ptr undef) + store i32 %runtimeOld, ptr @atomic.runtimeOld + %runtimeSuccess = call i1 @"sync/atomic.CompareAndSwapUint32"(ptr @atomic.value, i32 15, i32 17, ptr undef) + store i1 %runtimeSuccess, ptr @atomic.runtimeSuccess + %runtimeSeen = load atomic i32, ptr @atomic.value seq_cst, align 4 + store i32 %runtimeSeen, ptr @atomic.runtimeSeen + store atomic i32 19, ptr @atomic.value seq_cst, align 4 + ret void +} + define internal void @z.init(ptr %context) unnamed_addr { ; This can be safely expanded. call void @z.setArr(ptr @z.bloom, i64 1, ptr @z.bloom) diff --git a/interp/testdata/revert.out.ll b/interp/testdata/revert.out.ll index db226c2635..a97249699c 100644 --- a/interp/testdata/revert.out.ll +++ b/interp/testdata/revert.out.ll @@ -5,16 +5,30 @@ target triple = "x86_64--linux" @bar.knownAtRuntime = local_unnamed_addr global i64 0 @baz.someGlobal = external local_unnamed_addr global [3 x { i64, i32 }] @baz.someInt = local_unnamed_addr global i32 0 -@x.atomicNum = local_unnamed_addr global i32 0 +@x.atomicNum = local_unnamed_addr global i32 1 @x.volatileNum = global i32 0 @y.ready = local_unnamed_addr global i32 0 @z.bloom = global i64 0 @z.arr = global [32 x i8] zeroinitializer +@atomic.value = global i32 11 +@atomic.old = local_unnamed_addr global i32 7 +@atomic.success = local_unnamed_addr global i1 true +@atomic.failure = local_unnamed_addr global i1 false +@atomic.seen = local_unnamed_addr global i32 11 +@atomic.runtimeOld = local_unnamed_addr global i32 0 +@atomic.runtimeSuccess = local_unnamed_addr global i1 false +@atomic.runtimeSeen = local_unnamed_addr global i32 0 declare void @externalCall(i64) local_unnamed_addr declare i64 @ptrHash(ptr nocapture) local_unnamed_addr +declare void @modifyAtomic(ptr) local_unnamed_addr + +declare i32 @"sync/atomic.SwapUint32"(ptr, i32, ptr) local_unnamed_addr + +declare i1 @"sync/atomic.CompareAndSwapUint32"(ptr, i32, i32, ptr) local_unnamed_addr + define void @runtime.initAll() unnamed_addr { entry: call fastcc void @baz.init(ptr undef) @@ -22,11 +36,16 @@ entry: %val = load i64, ptr @foo.knownAtRuntime, align 8 store i64 %val, ptr @bar.knownAtRuntime, align 8 call void @externalCall(i64 3) - store atomic i32 1, ptr @x.atomicNum seq_cst, align 4 - %x = load atomic i32, ptr @x.atomicNum seq_cst, align 4 - store i32 %x, ptr @x.atomicNum, align 4 %y = load volatile i32, ptr @x.volatileNum, align 4 store volatile i32 %y, ptr @x.volatileNum, align 4 + call void @modifyAtomic(ptr @atomic.value) + %runtimeOld = call i32 @"sync/atomic.SwapUint32"(ptr @atomic.value, i32 15, ptr undef) + store i32 %runtimeOld, ptr @atomic.runtimeOld, align 4 + %runtimeSuccess = call i1 @"sync/atomic.CompareAndSwapUint32"(ptr @atomic.value, i32 15, i32 17, ptr undef) + store i1 %runtimeSuccess, ptr @atomic.runtimeSuccess, align 1 + %runtimeSeen = load atomic i32, ptr @atomic.value seq_cst, align 4 + store i32 %runtimeSeen, ptr @atomic.runtimeSeen, align 4 + store atomic i32 19, ptr @atomic.value seq_cst, align 4 call fastcc void @y.init(ptr undef) call fastcc void @z.set(ptr @z.bloom, ptr @z.bloom) call fastcc void @z.setArr(ptr @z.arr, i64 32, ptr @z.bloom) From 63a2136ea4827c1fde3def642c19a002400f089f Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:00:20 -0700 Subject: [PATCH 3/4] interp: allow map lookup and interface hashing during initialization These runtime helpers no longer use a separate interpreter map model. Keeping them opaque prevents constant callers from being evaluated. --- interp/interpreter.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/interp/interpreter.go b/interp/interpreter.go index af3fcfe15a..6ddd0f36d7 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -326,16 +326,13 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // which case this call won't even get to this point but will // already be emitted in initAll. continue - case strings.HasPrefix(callFn.name, "runtime.print") || callFn.name == "runtime._panic" || callFn.name == "runtime.hashmapGet" || callFn.name == "runtime.hashmapInterfaceHash" || + case strings.HasPrefix(callFn.name, "runtime.print") || callFn.name == "runtime._panic" || callFn.name == "os.runtime_args" || callFn.name == "internal/task.start" || callFn.name == "internal/task.Current" || callFn.name == "time.startTimer" || callFn.name == "time.stopTimer" || callFn.name == "time.resetTimer": // These functions should be run at runtime. Specifically: // * Print and panic functions are best emitted directly without // interpreting them, otherwise we get a ton of putchar (etc.) // calls. - // * runtime.hashmapGet tries to access the map value directly. - // This is not possible as the map value is treated as a special - // kind of object in this package. // * os.runtime_args reads globals that are initialized outside // the view of the interp package so it always needs to be run // at runtime. From 9cdd16dffc6e5c28fb5ce83914aa3f160bb3f401 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:00:20 -0700 Subject: [PATCH 4/4] interp: evaluate deferred initialization at compile time Defers currently keep otherwise constant initialization at runtime. Evaluate complete calls without runtime recovery bookkeeping, and keep the original call whenever any runtime execution is still required. --- interp/compiler.go | 28 ++++++++-- interp/errors.go | 3 +- interp/interpreter.go | 25 +++++++++ interp/testdata/defer.ll | 102 +++++++++++++++++++++++++++++++++++ interp/testdata/defer.out.ll | 98 +++++++++++++++++++++++++++++++++ testdata/init.go | 22 ++++++++ testdata/init.txt | 2 + testdata/recover.go | 12 +++++ testdata/recover.txt | 1 + 9 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 interp/testdata/defer.ll create mode 100644 interp/testdata/defer.out.ll diff --git a/interp/compiler.go b/interp/compiler.go index 141071be5b..a38db5971b 100644 --- a/interp/compiler.go +++ b/interp/compiler.go @@ -15,11 +15,13 @@ import ( // Functions are in SSA form, just like the LLVM version if it. The first block // (blocks[0]) is the entry block. type function struct { - llvmFn llvm.Value - name string // precalculated llvmFn.Name() - params []llvm.Value // precalculated llvmFn.Params() - blocks []*basicBlock - locals map[llvm.Value]int + llvmFn llvm.Value + name string // precalculated llvmFn.Name() + params []llvm.Value // precalculated llvmFn.Params() + blocks []*basicBlock + locals map[llvm.Value]int + deferFrame llvm.Value + deferStack llvm.Value } // basicBlock represents a LLVM basic block and contains a slice of @@ -77,6 +79,22 @@ func (r *runner) compileFunction(llvmFn llvm.Value) *function { return fn } + for inst := llvmFn.EntryBasicBlock().FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) { + if inst.IsACallInst().IsNil() || inst.CalledValue().Name() != "runtime.setupDeferFrame" { + continue + } + frame := inst.Operand(0) + stack := inst.Operand(1) + if frame.IsAAllocaInst().IsNil() || stack.IsACallInst().IsNil() || !strings.HasPrefix(stack.CalledValue().Name(), "llvm.stacksave") { + continue + } + use := stack.FirstUse() + if !use.IsNil() && use.User() == inst && use.NextUse().IsNil() { + fn.deferFrame = frame + fn.deferStack = stack + } + } + for i, param := range fn.params { fn.locals[param] = i } diff --git a/interp/errors.go b/interp/errors.go index 36aaa0cfb3..a15e51d12d 100644 --- a/interp/errors.go +++ b/interp/errors.go @@ -17,6 +17,7 @@ var ( errIntegerAsPointer = errors.New("interp: trying to use an integer as a pointer (memory-mapped I/O?)") errUnsupportedInst = errors.New("interp: unsupported instruction") errUnsupportedRuntimeInst = errors.New("interp: unsupported instruction (to be emitted at runtime)") + errDeferUnsupported = errors.New("interp: cannot partially evaluate a function with defer") errMapAlreadyCreated = errors.New("interp: map already created") errLoopUnrolled = errors.New("interp: loop unrolled") errLoopTooLong = errors.New("interp: loop ran too many iterations") @@ -29,7 +30,7 @@ var errInvalidPtrToIntSize = errors.New("interp: ptrtoint integer size does not func isRecoverableError(err error) bool { return err == errIntegerAsPointer || err == errUnsupportedInst || - err == errUnsupportedRuntimeInst || err == errMapAlreadyCreated || + err == errUnsupportedRuntimeInst || err == errDeferUnsupported || err == errMapAlreadyCreated || err == errLoopUnrolled || err == errLoopTooLong || err == errInvalidPtrToIntSize } diff --git a/interp/interpreter.go b/interp/interpreter.go index 6ddd0f36d7..ba025ac5e9 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -122,6 +122,25 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent } inst := bb.instructions[instIndex] + // compiler/defer.go emits these checkpoints for panic recovery. + // A fully evaluated call cannot panic or need a runtime defer frame. + if !fn.deferFrame.IsNil() && inst.opcode == llvm.Call { + if inst.llvmInst == fn.deferStack { + locals[inst.localIndex] = literalValue{uint64(0)} + continue + } + called := inst.llvmInst.CalledValue() + if inst.llvmInst.OperandsCount() >= 2 && inst.llvmInst.Operand(0) == fn.deferFrame { + name := called.Name() + if name == "runtime.setupDeferFrame" || name == "runtime.destroyDeferFrame" { + continue + } + if attr := inst.llvmInst.GetCallSiteEnumAttribute(-1, llvm.AttributeKindID("returns_twice")); !attr.IsNil() { + locals[inst.localIndex] = makeLiteralInt(0, inst.llvmInst.Type().IntTypeWidth()) + continue + } + } + } operands = operands[:0] isRuntimeInst := false if inst.opcode != llvm.PHI { @@ -201,6 +220,9 @@ 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 { + return nil, mem, r.errorAt(inst, errDeferUnsupported) + } if time.Since(r.start) > r.timeout { // Running for more than the allowed timeout; This shouldn't happen, but it does. // See github.com/tinygo-org/tinygo/issues/2124 @@ -1154,6 +1176,9 @@ 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() { + return r.errorAt(inst, errDeferUnsupported) + } numOperands := inst.llvmInst.OperandsCount() operands := make([]llvm.Value, numOperands) for i := range numOperands { diff --git a/interp/testdata/defer.ll b/interp/testdata/defer.ll new file mode 100644 index 0000000000..79d0fb8e92 --- /dev/null +++ b/interp/testdata/defer.ll @@ -0,0 +1,102 @@ +target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" +target triple = "x86_64--linux" + +%frame = type { ptr, ptr, i32, i32, ptr, ptr, ptr } +%record = type { ptr, ptr } + +@constant.counter = global i32 0 +@constant.result = global i32 0 +@external.counter = global i32 0 +@external.result = global i32 0 +@nested.counter = global i32 0 +@nested.result = global i32 0 +@panic.counter = global i32 0 +@panic.result = global i32 0 + +declare ptr @llvm.stacksave.p0() +declare void @runtime.setupDeferFrame(ptr, ptr, ptr) +declare void @runtime.destroyDeferFrame(ptr, ptr) +declare void @runtime._panic(ptr, ptr, ptr) +declare void @external() + +define void @runtime.initAll() { + call void @main.init() + ret void +} + +define internal void @main.init() { + %constant = call i32 @deferred(ptr @constant.counter, ptr @constantBody) + store i32 %constant, ptr @constant.result + %external = call i32 @deferred(ptr @external.counter, ptr @externalBody) + store i32 %external, ptr @external.result + %nested = call i32 @deferred(ptr @nested.counter, ptr @nestedBody) + store i32 %nested, ptr @nested.result + %panic = call i32 @deferred(ptr @panic.counter, ptr @panicBody) + store i32 %panic, ptr @panic.result + ret void +} + +define internal void @constantBody() { + ret void +} + +define internal void @externalBody() { + call void @external() + ret void +} + +define internal void @nestedBody() { + call void @externalBody() + ret void +} + +define internal void @panicBody() { + call void @runtime._panic(ptr undef, ptr undef, ptr undef) + unreachable +} + +define internal void @callback(ptr %counter) { + %old = load i32, ptr %counter + %next = add i32 %old, 1 + store i32 %next, ptr %counter + ret void +} + +define internal i32 @deferred(ptr %counter, ptr %bodyFn) { +entry: + %frame = alloca %frame + %head = getelementptr %frame, ptr %frame, i32 0, i32 6 + %sp = call ptr @llvm.stacksave.p0() + call void @runtime.setupDeferFrame(ptr %frame, ptr %sp, ptr undef) + %record = alloca %record + %prev = load ptr, ptr %head + store ptr %prev, ptr %record + %argument = getelementptr %record, ptr %record, i32 0, i32 1 + store ptr %counter, ptr %argument + store ptr %record, ptr %head + %checkpoint = call i32 asm "", "=r,r"(ptr %frame) returns_twice + %normal = icmp eq i32 %checkpoint, 0 + br i1 %normal, label %body, label %defers + +body: + store i32 40, ptr %counter + call void %bodyFn() + br label %defers + +defers: + %current = load ptr, ptr %head + %done = icmp eq ptr %current, null + br i1 %done, label %return, label %callback + +callback: + %next = load ptr, ptr %current + store ptr %next, ptr %head + %argptr = getelementptr %record, ptr %current, i32 0, i32 1 + %arg = load ptr, ptr %argptr + call void @callback(ptr %arg) + br label %defers + +return: + call void @runtime.destroyDeferFrame(ptr %frame, ptr undef) + ret i32 42 +} diff --git a/interp/testdata/defer.out.ll b/interp/testdata/defer.out.ll new file mode 100644 index 0000000000..27916cd516 --- /dev/null +++ b/interp/testdata/defer.out.ll @@ -0,0 +1,98 @@ +target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" +target triple = "x86_64--linux" + +%frame = type { ptr, ptr, i32, i32, ptr, ptr, ptr } +%record = type { ptr, ptr } + +@constant.counter = local_unnamed_addr global i32 41 +@constant.result = local_unnamed_addr global i32 42 +@external.counter = global i32 0 +@external.result = local_unnamed_addr global i32 0 +@nested.counter = global i32 0 +@nested.result = local_unnamed_addr global i32 0 +@panic.counter = global i32 0 +@panic.result = local_unnamed_addr global i32 0 + +declare ptr @llvm.stacksave.p0() #0 + +declare void @runtime.setupDeferFrame(ptr, ptr, ptr) local_unnamed_addr + +declare void @runtime.destroyDeferFrame(ptr, ptr) local_unnamed_addr + +declare void @runtime._panic(ptr, ptr, ptr) local_unnamed_addr + +declare void @external() local_unnamed_addr + +define void @runtime.initAll() local_unnamed_addr { + %external = call fastcc i32 @deferred(ptr @external.counter, ptr @externalBody) + store i32 %external, ptr @external.result, align 4 + %nested = call fastcc i32 @deferred(ptr @nested.counter, ptr @nestedBody) + store i32 %nested, ptr @nested.result, align 4 + %panic = call fastcc i32 @deferred(ptr @panic.counter, ptr @panicBody) + store i32 %panic, ptr @panic.result, align 4 + ret void +} + +define internal void @externalBody() { + call void @external() + ret void +} + +define internal void @nestedBody() { + call void @externalBody() + ret void +} + +define internal void @panicBody() { + call void @runtime._panic(ptr undef, ptr undef, ptr undef) + unreachable +} + +define internal fastcc void @callback(ptr %counter) unnamed_addr { + %old = load i32, ptr %counter, align 4 + %next = add i32 %old, 1 + store i32 %next, ptr %counter, align 4 + ret void +} + +define internal fastcc i32 @deferred(ptr %counter, ptr %bodyFn) unnamed_addr { +entry: + %frame = alloca %frame, align 8 + %head = getelementptr %frame, ptr %frame, i32 0, i32 6 + %sp = call ptr @llvm.stacksave.p0() + call void @runtime.setupDeferFrame(ptr %frame, ptr %sp, ptr undef) + %record = alloca %record, align 8 + %prev = load ptr, ptr %head, align 8 + store ptr %prev, ptr %record, align 8 + %argument = getelementptr %record, ptr %record, i32 0, i32 1 + store ptr %counter, ptr %argument, align 8 + store ptr %record, ptr %head, align 8 + %checkpoint = call i32 asm "", "=r,r"(ptr %frame) #1 + %normal = icmp eq i32 %checkpoint, 0 + br i1 %normal, label %body, label %defers + +body: ; preds = %entry + store i32 40, ptr %counter, align 4 + call void %bodyFn() + br label %defers + +defers: ; preds = %callback, %body, %entry + %current = load ptr, ptr %head, align 8 + %done = icmp eq ptr %current, null + br i1 %done, label %return, label %callback + +callback: ; preds = %defers + %next = load ptr, ptr %current, align 8 + store ptr %next, ptr %head, align 8 + %argptr = getelementptr %record, ptr %current, i32 0, i32 1 + %arg = load ptr, ptr %argptr, align 8 + call fastcc void @callback(ptr %arg) + br label %defers + +return: ; preds = %defers + call void @runtime.destroyDeferFrame(ptr %frame, ptr undef) + ret i32 42 +} + +attributes #0 = { nocallback nofree nosync nounwind willreturn } +attributes #1 = { returns_twice } diff --git a/testdata/init.go b/testdata/init.go index 4201dff3fe..c80b3b3d78 100644 --- a/testdata/init.go +++ b/testdata/init.go @@ -1,5 +1,7 @@ package main +import "sync" + func init() { println("init") } @@ -15,6 +17,9 @@ func main() { println("v7:", cap(v7), string(v7)) println("v8:", v8) println("v9:", len(v9), v9[0], v9[1], v9[2]) + println("deferred init:", deferredInitValue) + initOnce.Do(func() { initOnceCalls++ }) + println("once init:", onceInitValue, initOnceCalls) println(uint8SliceSrc[0]) println(uint8SliceDst[0]) @@ -24,6 +29,23 @@ func main() { println("shifted:", string(shifted[:]), shiftDone) } +var deferredInitValue = deferredInit() +var initOnce sync.Once +var initOnceCalls int +var onceInitValue = onceInit() + +func deferredInit() (result int) { + result = 20 + defer func() { result++ }() + defer func() { result *= 2 }() + return +} + +func onceInit() int { + initOnce.Do(func() { initOnceCalls++ }) + return initOnceCalls +} + type ( t2 struct { x int diff --git a/testdata/init.txt b/testdata/init.txt index ceeacac24c..c3da32ea94 100644 --- a/testdata/init.txt +++ b/testdata/init.txt @@ -10,6 +10,8 @@ v6: false v7: 3 foo v8: barba v9: 3 2 3 4 +deferred init: 41 +once init: 1 1 3 3 5 diff --git a/testdata/recover.go b/testdata/recover.go index 48f01f3f30..29d2243c1b 100644 --- a/testdata/recover.go +++ b/testdata/recover.go @@ -8,8 +8,10 @@ import ( var wg sync.WaitGroup var panicMap = map[interface{}]int{} +var recoveredInitValue = recoveredInit() func main() { + println("recovered init:", recoveredInitValue) println("# simple recover") recoverSimple() @@ -54,6 +56,16 @@ func main() { recoverSignals() } +func recoveredInit() (result int) { + defer func() { + if recover() == "init panic" { + result += 2 + } + }() + result = 40 + panic("init panic") +} + func recoverSimple() { defer func() { println("recovering...") diff --git a/testdata/recover.txt b/testdata/recover.txt index 620d182951..c335a9b9d9 100644 --- a/testdata/recover.txt +++ b/testdata/recover.txt @@ -1,3 +1,4 @@ +recovered init: 42 # simple recover running panic... recovering...