From 490a853fd352c8de44a3e2d1453ba62d4b226e84 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Thu, 22 Aug 2024 16:42:03 +0200 Subject: [PATCH] interp: support big-endian targets The interp package was assuming that all targets were little-endian. But that's not true: we now have a big-endian target (GOARCH=mips). This fixes the interp package to use the appropriate byte order for a given target. --- compiler/llvmutil/llvm.go | 11 +++++ interp/interp.go | 4 ++ interp/interpreter.go | 98 +++++++++++++++++++-------------------- interp/memory.go | 76 ++++++++++++++++-------------- main_test.go | 6 ++- 5 files changed, 110 insertions(+), 85 deletions(-) diff --git a/compiler/llvmutil/llvm.go b/compiler/llvmutil/llvm.go index 607e91e8d8..061bee6c9a 100644 --- a/compiler/llvmutil/llvm.go +++ b/compiler/llvmutil/llvm.go @@ -8,6 +8,7 @@ package llvmutil import ( + "encoding/binary" "strconv" "strings" @@ -216,3 +217,13 @@ func Version() int { } return major } + +// Return the byte order for the given target triple. Most targets are little +// endian, but for example MIPS can be big-endian. +func ByteOrder(target string) binary.ByteOrder { + if strings.HasPrefix(target, "mips-") { + return binary.BigEndian + } else { + return binary.LittleEndian + } +} diff --git a/interp/interp.go b/interp/interp.go index 80afc39c74..30b0872485 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -3,11 +3,13 @@ package interp import ( + "encoding/binary" "fmt" "os" "strings" "time" + "github.com/tinygo-org/tinygo/compiler/llvmutil" "tinygo.org/x/go-llvm" ) @@ -24,6 +26,7 @@ type runner struct { dataPtrType llvm.Type // often used type so created in advance uintptrType llvm.Type // equivalent to uintptr in Go maxAlign int // maximum alignment of an object, alignment of runtime.alloc() result + byteOrder binary.ByteOrder // big-endian or little-endian debug bool // log debug messages pkgName string // package name of the currently executing package functionCache map[llvm.Value]*function // cache of compiled functions @@ -38,6 +41,7 @@ func newRunner(mod llvm.Module, timeout time.Duration, debug bool) *runner { r := runner{ mod: mod, targetData: llvm.NewTargetData(mod.DataLayout()), + byteOrder: llvmutil.ByteOrder(mod.Target()), debug: debug, functionCache: make(map[llvm.Value]*function), objects: []object{{}}, diff --git a/interp/interpreter.go b/interp/interpreter.go index b35129b814..512d93eb74 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -173,7 +173,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent case 3: // Conditional branch: [cond, thenBB, elseBB] lastBB = currentBB - switch operands[0].Uint() { + switch operands[0].Uint(r) { case 1: // true -> thenBB currentBB = int(operands[1].(literalValue).value.(uint32)) case 0: // false -> elseBB @@ -191,12 +191,12 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent } case llvm.Switch: // Switch statement: [value, defaultLabel, case0, label0, case1, label1, ...] - value := operands[0].Uint() - targetLabel := operands[1].Uint() // default label + value := operands[0].Uint(r) + targetLabel := operands[1].Uint(r) // default label // Do a lazy switch by iterating over all cases. for i := 2; i < len(operands); i += 2 { - if value == operands[i].Uint() { - targetLabel = operands[i+1].Uint() + if value == operands[i].Uint(r) { + targetLabel = operands[i+1].Uint(r) break } } @@ -211,7 +211,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // Select is much like a ternary operator: it picks a result from // the second and third operand based on the boolean first operand. var result value - switch operands[0].Uint() { + switch operands[0].Uint(r) { case 1: result = operands[1] case 0: @@ -282,7 +282,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // by creating a global variable. // Get the requested memory size to be allocated. - size := operands[1].Uint() + size := operands[1].Uint(r) // Get the object layout, if it is available. llvmLayoutType := r.getLLVMTypeFromLayout(operands[2]) @@ -318,9 +318,9 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // memmove(dst, src, n*elemSize) // return int(n) // } - dstLen := operands[3].Uint() - srcLen := operands[4].Uint() - elemSize := operands[5].Uint() + dstLen := operands[3].Uint(r) + srcLen := operands[4].Uint(r) + elemSize := operands[5].Uint(r) n := srcLen if n > dstLen { n = dstLen @@ -374,7 +374,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent if err != nil { return nil, mem, r.errorAt(inst, err) } - nBytes := uint32(operands[3].Uint()) + nBytes := uint32(operands[3].Uint(r)) dstObj := mem.getWritable(dst.index()) dstBuf := dstObj.buffer.asRawValue(r) if mem.get(src.index()).buffer == nil { @@ -661,8 +661,8 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // pointer into the underlying object. var offset int64 for i := 1; i < len(operands); i += 2 { - index := operands[i].Int() - elementSize := operands[i+1].Int() + index := operands[i].Int(r) + elementSize := operands[i+1].Int(r) if elementSize < 0 { // This is a struct field. offset += index @@ -677,7 +677,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent return nil, mem, r.errorAt(inst, err) } // GEP on fixed pointer value (for example, memory-mapped I/O). - ptrValue := operands[0].Uint() + uint64(offset) + ptrValue := operands[0].Uint(r) + uint64(offset) locals[inst.localIndex] = makeLiteralInt(ptrValue, int(operands[0].len(r)*8)) continue } @@ -739,11 +739,11 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent var lhs, rhs float64 switch operands[0].len(r) { case 8: - lhs = math.Float64frombits(operands[0].Uint()) - rhs = math.Float64frombits(operands[1].Uint()) + lhs = math.Float64frombits(operands[0].Uint(r)) + rhs = math.Float64frombits(operands[1].Uint(r)) case 4: - lhs = float64(math.Float32frombits(uint32(operands[0].Uint()))) - rhs = float64(math.Float32frombits(uint32(operands[1].Uint()))) + lhs = float64(math.Float32frombits(uint32(operands[0].Uint(r)))) + rhs = float64(math.Float32frombits(uint32(operands[1].Uint(r)))) default: panic("unknown float type") } @@ -782,23 +782,23 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent if inst.opcode == llvm.Add { // This likely means this is part of a // unsafe.Pointer(uintptr(ptr) + offset) pattern. - lhsPtr, err = lhsPtr.addOffset(int64(rhs.Uint())) + lhsPtr, err = lhsPtr.addOffset(int64(rhs.Uint(r))) if err != nil { return nil, mem, r.errorAt(inst, err) } locals[inst.localIndex] = lhsPtr - } else if inst.opcode == llvm.Xor && rhs.Uint() == 0 { + } else if inst.opcode == llvm.Xor && rhs.Uint(r) == 0 { // Special workaround for strings.noescape, see // src/strings/builder.go in the Go source tree. This is // the identity operator, so we can return the input. locals[inst.localIndex] = lhs - } else if inst.opcode == llvm.And && rhs.Uint() < 8 { + } else if inst.opcode == llvm.And && rhs.Uint(r) < 8 { // This is probably part of a pattern to get the lower bits // of a pointer for pointer tagging, like this: // uintptr(unsafe.Pointer(t)) & 0b11 // We can actually support this easily by ANDing with the // pointer offset. - result := uint64(lhsPtr.offset()) & rhs.Uint() + result := uint64(lhsPtr.offset()) & rhs.Uint(r) locals[inst.localIndex] = makeLiteralInt(result, int(lhs.len(r)*8)) } else { // Catch-all for weird operations that should just be done @@ -813,31 +813,31 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent var result uint64 switch inst.opcode { case llvm.Add: - result = lhs.Uint() + rhs.Uint() + result = lhs.Uint(r) + rhs.Uint(r) case llvm.Sub: - result = lhs.Uint() - rhs.Uint() + result = lhs.Uint(r) - rhs.Uint(r) case llvm.Mul: - result = lhs.Uint() * rhs.Uint() + result = lhs.Uint(r) * rhs.Uint(r) case llvm.UDiv: - result = lhs.Uint() / rhs.Uint() + result = lhs.Uint(r) / rhs.Uint(r) case llvm.SDiv: - result = uint64(lhs.Int() / rhs.Int()) + result = uint64(lhs.Int(r) / rhs.Int(r)) case llvm.URem: - result = lhs.Uint() % rhs.Uint() + result = lhs.Uint(r) % rhs.Uint(r) case llvm.SRem: - result = uint64(lhs.Int() % rhs.Int()) + result = uint64(lhs.Int(r) % rhs.Int(r)) case llvm.Shl: - result = lhs.Uint() << rhs.Uint() + result = lhs.Uint(r) << rhs.Uint(r) case llvm.LShr: - result = lhs.Uint() >> rhs.Uint() + result = lhs.Uint(r) >> rhs.Uint(r) case llvm.AShr: - result = uint64(lhs.Int() >> rhs.Uint()) + result = uint64(lhs.Int(r) >> rhs.Uint(r)) case llvm.And: - result = lhs.Uint() & rhs.Uint() + result = lhs.Uint(r) & rhs.Uint(r) case llvm.Or: - result = lhs.Uint() | rhs.Uint() + result = lhs.Uint(r) | rhs.Uint(r) case llvm.Xor: - result = lhs.Uint() ^ rhs.Uint() + result = lhs.Uint(r) ^ rhs.Uint(r) default: panic("unreachable") } @@ -855,11 +855,11 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // and then truncating it as necessary. var value uint64 if inst.opcode == llvm.SExt { - value = uint64(operands[0].Int()) + value = uint64(operands[0].Int(r)) } else { - value = operands[0].Uint() + value = operands[0].Uint(r) } - bitwidth := operands[1].Uint() + bitwidth := operands[1].Uint(r) if r.debug { fmt.Fprintln(os.Stderr, indent+instructionNameMap[inst.opcode]+":", value, bitwidth) } @@ -868,11 +868,11 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent var value float64 switch inst.opcode { case llvm.SIToFP: - value = float64(operands[0].Int()) + value = float64(operands[0].Int(r)) case llvm.UIToFP: - value = float64(operands[0].Uint()) + value = float64(operands[0].Uint(r)) } - bitwidth := operands[1].Uint() + bitwidth := operands[1].Uint(r) if r.debug { fmt.Fprintln(os.Stderr, indent+instructionNameMap[inst.opcode]+":", value, bitwidth) } @@ -918,21 +918,21 @@ func (r *runner) interpretICmp(lhs, rhs value, predicate llvm.IntPredicate) bool } return result case llvm.IntUGT: - return lhs.Uint() > rhs.Uint() + return lhs.Uint(r) > rhs.Uint(r) case llvm.IntUGE: - return lhs.Uint() >= rhs.Uint() + return lhs.Uint(r) >= rhs.Uint(r) case llvm.IntULT: - return lhs.Uint() < rhs.Uint() + return lhs.Uint(r) < rhs.Uint(r) case llvm.IntULE: - return lhs.Uint() <= rhs.Uint() + return lhs.Uint(r) <= rhs.Uint(r) case llvm.IntSGT: - return lhs.Int() > rhs.Int() + return lhs.Int(r) > rhs.Int(r) case llvm.IntSGE: - return lhs.Int() >= rhs.Int() + return lhs.Int(r) >= rhs.Int(r) case llvm.IntSLT: - return lhs.Int() < rhs.Int() + return lhs.Int(r) < rhs.Int(r) case llvm.IntSLE: - return lhs.Int() <= rhs.Int() + return lhs.Int(r) <= rhs.Int(r) default: // _should_ be unreachable, until LLVM adds new icmp operands (unlikely) panic("interp: unsupported icmp") diff --git a/interp/memory.go b/interp/memory.go index 759a1ffe48..176228fdc1 100644 --- a/interp/memory.go +++ b/interp/memory.go @@ -361,8 +361,8 @@ type value interface { clone() value asPointer(*runner) (pointerValue, error) asRawValue(*runner) rawValue - Uint() uint64 - Int() int64 + Uint(*runner) uint64 + Int(*runner) int64 toLLVMValue(llvm.Type, *memoryView) (llvm.Value, error) String() string } @@ -405,7 +405,8 @@ func (v literalValue) len(r *runner) uint32 { } func (v literalValue) String() string { - return strconv.FormatInt(v.Int(), 10) + // Note: passing a nil *runner to v.Int because we know it won't use it. + return strconv.FormatInt(v.Int(nil), 10) } func (v literalValue) clone() value { @@ -421,13 +422,13 @@ func (v literalValue) asRawValue(r *runner) rawValue { switch value := v.value.(type) { case uint64: buf = make([]byte, 8) - binary.LittleEndian.PutUint64(buf, value) + r.byteOrder.PutUint64(buf, value) case uint32: buf = make([]byte, 4) - binary.LittleEndian.PutUint32(buf, uint32(value)) + r.byteOrder.PutUint32(buf, uint32(value)) case uint16: buf = make([]byte, 2) - binary.LittleEndian.PutUint16(buf, uint16(value)) + r.byteOrder.PutUint16(buf, uint16(value)) case uint8: buf = []byte{uint8(value)} default: @@ -440,7 +441,7 @@ func (v literalValue) asRawValue(r *runner) rawValue { return raw } -func (v literalValue) Uint() uint64 { +func (v literalValue) Uint(r *runner) uint64 { switch value := v.value.(type) { case uint64: return value @@ -455,7 +456,7 @@ func (v literalValue) Uint() uint64 { } } -func (v literalValue) Int() int64 { +func (v literalValue) Int(r *runner) int64 { switch value := v.value.(type) { case uint64: return int64(value) @@ -553,11 +554,11 @@ func (v pointerValue) asRawValue(r *runner) rawValue { return rv } -func (v pointerValue) Uint() uint64 { +func (v pointerValue) Uint(r *runner) uint64 { panic("cannot convert pointer to integer") } -func (v pointerValue) Int() int64 { +func (v pointerValue) Int(r *runner) int64 { panic("cannot convert pointer to integer") } @@ -702,7 +703,12 @@ func (v rawValue) String() string { } // Format as number if none of the buf is a pointer. if !v.hasPointer() { - return strconv.FormatInt(v.Int(), 10) + // Construct a fake runner, which is little endian. + // We only use String() for debugging, so this is is good enough + // (the printed value will just be slightly wrong when debugging the + // interp package with GOOS=mips for example). + r := &runner{byteOrder: binary.LittleEndian} + return strconv.FormatInt(v.Int(r), 10) } } return "<[…" + strconv.Itoa(len(v.buf)) + "]>" @@ -738,33 +744,33 @@ func (v rawValue) bytes() []byte { return buf } -func (v rawValue) Uint() uint64 { +func (v rawValue) Uint(r *runner) uint64 { buf := v.bytes() switch len(v.buf) { case 1: return uint64(buf[0]) case 2: - return uint64(binary.LittleEndian.Uint16(buf)) + return uint64(r.byteOrder.Uint16(buf)) case 4: - return uint64(binary.LittleEndian.Uint32(buf)) + return uint64(r.byteOrder.Uint32(buf)) case 8: - return binary.LittleEndian.Uint64(buf) + return r.byteOrder.Uint64(buf) default: panic("unknown integer size") } } -func (v rawValue) Int() int64 { +func (v rawValue) Int(r *runner) int64 { switch len(v.buf) { case 1: - return int64(int8(v.Uint())) + return int64(int8(v.Uint(r))) case 2: - return int64(int16(v.Uint())) + return int64(int16(v.Uint(r))) case 4: - return int64(int32(v.Uint())) + return int64(int32(v.Uint(r))) case 8: - return int64(int64(v.Uint())) + return int64(int64(v.Uint(r))) default: panic("unknown integer size") } @@ -878,11 +884,11 @@ func (v rawValue) toLLVMValue(llvmType llvm.Type, mem *memoryView) (llvm.Value, var n uint64 switch llvmType.IntTypeWidth() { case 64: - n = rawValue{v.buf[:8]}.Uint() + n = rawValue{v.buf[:8]}.Uint(mem.r) case 32: - n = rawValue{v.buf[:4]}.Uint() + n = rawValue{v.buf[:4]}.Uint(mem.r) case 16: - n = rawValue{v.buf[:2]}.Uint() + n = rawValue{v.buf[:2]}.Uint(mem.r) case 8: n = uint64(v.buf[0]) case 1: @@ -951,7 +957,7 @@ func (v rawValue) toLLVMValue(llvmType llvm.Type, mem *memoryView) (llvm.Value, } // This is either a null pointer or a raw pointer for memory-mapped I/O // (such as 0xe000ed00). - ptr := rawValue{v.buf[:mem.r.pointerSize]}.Uint() + ptr := rawValue{v.buf[:mem.r.pointerSize]}.Uint(mem.r) if ptr == 0 { // Null pointer. return llvm.ConstNull(llvmType), nil @@ -969,11 +975,11 @@ func (v rawValue) toLLVMValue(llvmType llvm.Type, mem *memoryView) (llvm.Value, } return llvm.ConstIntToPtr(ptrValue, llvmType), nil case llvm.DoubleTypeKind: - b := rawValue{v.buf[:8]}.Uint() + b := rawValue{v.buf[:8]}.Uint(mem.r) f := math.Float64frombits(b) return llvm.ConstFloat(llvmType, f), nil case llvm.FloatTypeKind: - b := uint32(rawValue{v.buf[:4]}.Uint()) + b := uint32(rawValue{v.buf[:4]}.Uint(mem.r)) f := math.Float32frombits(b) return llvm.ConstFloat(llvmType, float64(f)), nil default: @@ -1065,19 +1071,19 @@ func (v *rawValue) set(llvmValue llvm.Value, r *runner) { switch llvmValue.Type().IntTypeWidth() { case 64: var buf [8]byte - binary.LittleEndian.PutUint64(buf[:], n) + r.byteOrder.PutUint64(buf[:], n) for i, b := range buf { v.buf[i] = uint64(b) } case 32: var buf [4]byte - binary.LittleEndian.PutUint32(buf[:], uint32(n)) + r.byteOrder.PutUint32(buf[:], uint32(n)) for i, b := range buf { v.buf[i] = uint64(b) } case 16: var buf [2]byte - binary.LittleEndian.PutUint16(buf[:], uint16(n)) + r.byteOrder.PutUint16(buf[:], uint16(n)) for i, b := range buf { v.buf[i] = uint64(b) } @@ -1109,14 +1115,14 @@ func (v *rawValue) set(llvmValue llvm.Value, r *runner) { case llvm.DoubleTypeKind: f, _ := llvmValue.DoubleValue() var buf [8]byte - binary.LittleEndian.PutUint64(buf[:], math.Float64bits(f)) + r.byteOrder.PutUint64(buf[:], math.Float64bits(f)) for i, b := range buf { v.buf[i] = uint64(b) } case llvm.FloatTypeKind: f, _ := llvmValue.DoubleValue() var buf [4]byte - binary.LittleEndian.PutUint32(buf[:], math.Float32bits(float32(f))) + r.byteOrder.PutUint32(buf[:], math.Float32bits(float32(f))) for i, b := range buf { v.buf[i] = uint64(b) } @@ -1166,11 +1172,11 @@ func (v localValue) asRawValue(r *runner) rawValue { panic("interp: localValue.asRawValue") } -func (v localValue) Uint() uint64 { +func (v localValue) Uint(r *runner) uint64 { panic("interp: localValue.Uint") } -func (v localValue) Int() int64 { +func (v localValue) Int(r *runner) int64 { panic("interp: localValue.Int") } @@ -1254,7 +1260,7 @@ func (r *runner) readObjectLayout(layoutValue value) (uint64, *big.Int) { ptr, err := layoutValue.asPointer(r) if err == errIntegerAsPointer { // It's an integer, which means it's a small object or unknown. - layout := layoutValue.Uint() + layout := layoutValue.Uint(r) if layout == 0 { // Nil pointer, which means the layout is unknown. return 0, nil @@ -1287,7 +1293,7 @@ func (r *runner) readObjectLayout(layoutValue value) (uint64, *big.Int) { // Read the object size in words and the bitmap from the global. buf := r.objects[ptr.index()].buffer.(rawValue) - objectSizeWords := rawValue{buf: buf.buf[:r.pointerSize]}.Uint() + objectSizeWords := rawValue{buf: buf.buf[:r.pointerSize]}.Uint(r) rawByteValues := buf.buf[r.pointerSize:] rawBytes := make([]byte, len(rawByteValues)) for i, v := range rawByteValues { diff --git a/main_test.go b/main_test.go index 40002ee871..80c8a46cd1 100644 --- a/main_test.go +++ b/main_test.go @@ -181,9 +181,13 @@ func TestBuild(t *testing.T) { // Run a single test for GOARCH=mips to see whether it works at all. // Big-endian MIPS isn't fully supported yet, but simple examples // should work. + // Once big-endian is fully supported, we can probably flip this + // around and do full testing of MIPS big-endian support and only do + // limited testing of MIPS little-endian (because the two are some + // similar). t.Parallel() options := optionsFromOSARCH("linux/mips/softfloat", sema) - runTest("alias.go", options, t, nil, nil) + runTest("map.go", options, t, nil, nil) }) t.Run("WebAssembly", func(t *testing.T) { t.Parallel()