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
28 changes: 23 additions & 5 deletions interp/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion interp/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
}

Expand Down
119 changes: 113 additions & 6 deletions interp/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -272,23 +294,67 @@ 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.
// Unless the object is allocated at runtime for example, in
// 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.
Expand Down Expand Up @@ -409,6 +475,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 {
Expand Down Expand Up @@ -609,7 +713,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
Expand Down Expand Up @@ -639,7 +743,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
Expand Down Expand Up @@ -1072,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 {
Expand Down
28 changes: 28 additions & 0 deletions interp/testdata/copy.ll
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
}

Expand Down Expand Up @@ -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)
9 changes: 9 additions & 0 deletions interp/testdata/copy.out.ll
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -17,11 +20,17 @@ 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
}

declare void @llvm.memmove.p0.p0.i64(ptr nocapture writeonly, ptr nocapture readonly, i64, i1 immarg) #0

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) }
Loading
Loading