package compiler import ( "encoding/binary" "fmt" "go/token" "go/types" "strings" "github.com/tinygo-org/tinygo/compileopts" "github.com/tinygo-org/tinygo/compiler/llvmutil" "tinygo.org/x/go-llvm" ) // This file contains helper functions for LLVM that are not exposed in the Go // bindings. // createTemporaryAlloca creates a new alloca in the entry block and adds // lifetime start information in the IR signalling that the alloca won't be used // before this point. // // This is useful for creating temporary allocas for intrinsics. Don't forget to // end the lifetime using emitLifetimeEnd after you're done with it. func (b *builder) createTemporaryAlloca(t llvm.Type, name string) (alloca, size llvm.Value) { return llvmutil.CreateTemporaryAlloca(b.Builder, b.mod, t, name) } // insertBasicBlock inserts a new basic block after the current basic block. // This is useful when inserting new basic blocks while converting a // *ssa.BasicBlock to a llvm.BasicBlock and the LLVM basic block needs some // extra blocks. // It does not update b.blockExits, this must be done by the caller. func (b *builder) insertBasicBlock(name string) llvm.BasicBlock { currentBB := b.Builder.GetInsertBlock() nextBB := llvm.NextBasicBlock(currentBB) if nextBB.IsNil() { // Last basic block in the function, so add one to the end. return b.ctx.AddBasicBlock(b.llvmFn, name) } // Insert a basic block before the next basic block - that is, at the // current insert location. return b.ctx.InsertBasicBlock(nextBB, name) } // emitLifetimeEnd signals the end of an (alloca) lifetime by calling the // llvm.lifetime.end intrinsic. It is commonly used together with // createTemporaryAlloca. func (b *builder) emitLifetimeEnd(ptr, size llvm.Value) { llvmutil.EmitLifetimeEnd(b.Builder, b.mod, ptr, size) } // emitPointerPack packs the list of values into a single pointer value using // bitcasts, or else allocates a value on the heap if it cannot be packed in the // pointer value directly. It returns the pointer with the packed data. // If the values are all constants, they are be stored in a constant global and // deduplicated. func (b *builder) emitPointerPack(values []llvm.Value) llvm.Value { valueTypes := make([]llvm.Type, len(values)) for i, value := range values { valueTypes[i] = value.Type() } packedType := b.ctx.StructType(valueTypes, false) // Allocate memory for the packed data. size := b.targetData.TypeAllocSize(packedType) if size == 0 { return llvm.ConstPointerNull(b.dataPtrType) } else if len(values) == 1 && values[0].Type().TypeKind() == llvm.PointerTypeKind { return values[0] } else if size <= b.targetData.TypeAllocSize(b.dataPtrType) { // Packed data fits in a pointer, so store it directly inside the // pointer. if len(values) == 1 && values[0].Type().TypeKind() == llvm.IntegerTypeKind { // Try to keep this cast in SSA form. return b.CreateIntToPtr(values[0], b.dataPtrType, "pack.int") } // Because packedType is a struct and we have to cast it to a *i8, store // it in a *i8 alloca first and load the *i8 value from there. This is // effectively a bitcast. packedAlloc, _ := b.createTemporaryAlloca(b.dataPtrType, "") if size < b.targetData.TypeAllocSize(b.dataPtrType) { // The alloca is bigger than the value that will be stored in it. // To avoid having some bits undefined, zero the alloca first. // Hopefully this will get optimized away. b.CreateStore(llvm.ConstNull(b.dataPtrType), packedAlloc) } // Store all values in the alloca. for i, value := range values { indices := []llvm.Value{ llvm.ConstInt(b.ctx.Int32Type(), 0, false), llvm.ConstInt(b.ctx.Int32Type(), uint64(i), false), } gep := b.CreateInBoundsGEP(packedType, packedAlloc, indices, "") b.CreateStore(value, gep) } // Load value (the *i8) from the alloca. result := b.CreateLoad(b.dataPtrType, packedAlloc, "") // End the lifetime of the alloca, to help the optimizer. packedSize := llvm.ConstInt(b.ctx.Int64Type(), b.targetData.TypeAllocSize(packedAlloc.Type()), false) b.emitLifetimeEnd(packedAlloc, packedSize) return result } else { // Check if the values are all constants. constant := true for _, v := range values { if !v.IsConstant() { constant = false break } } if constant { // The data is known at compile time, so store it in a constant global. // The global address is marked as unnamed, which allows LLVM to merge duplicates. global := llvm.AddGlobal(b.mod, packedType, b.pkg.Path()+"$pack") global.SetInitializer(b.ctx.ConstStruct(values, false)) global.SetGlobalConstant(true) global.SetUnnamedAddr(true) global.SetLinkage(llvm.InternalLinkage) return global } // Packed data is bigger than a pointer, so allocate it on the heap. sizeValue := llvm.ConstInt(b.uintptrType, size, false) align := b.targetData.ABITypeAlignment(packedType) packedAlloc := b.createAlloc(sizeValue, llvm.ConstNull(b.dataPtrType), align, "") if b.NeedsStackObjects { b.trackPointer(packedAlloc) } // Store all values in the heap pointer. for i, value := range values { indices := []llvm.Value{ llvm.ConstInt(b.ctx.Int32Type(), 0, false), llvm.ConstInt(b.ctx.Int32Type(), uint64(i), false), } gep := b.CreateInBoundsGEP(packedType, packedAlloc, indices, "") b.CreateStore(value, gep) } // Return the original heap allocation pointer, which already is an *i8. return packedAlloc } } // emitPointerUnpack extracts a list of values packed using emitPointerPack. func (b *builder) emitPointerUnpack(ptr llvm.Value, valueTypes []llvm.Type) []llvm.Value { packedType := b.ctx.StructType(valueTypes, false) // Get a correctly-typed pointer to the packed data. var packedAlloc llvm.Value needsLifetimeEnd := false size := b.targetData.TypeAllocSize(packedType) if size == 0 { // No data to unpack. } else if len(valueTypes) == 1 && valueTypes[0].TypeKind() == llvm.PointerTypeKind { // A single pointer is always stored directly. return []llvm.Value{ptr} } else if size <= b.targetData.TypeAllocSize(b.dataPtrType) { // Packed data stored directly in pointer. if len(valueTypes) == 1 && valueTypes[0].TypeKind() == llvm.IntegerTypeKind { // Keep this cast in SSA form. return []llvm.Value{b.CreatePtrToInt(ptr, valueTypes[0], "unpack.int")} } // Fallback: load it using an alloca. packedAlloc, _ = b.createTemporaryAlloca(b.dataPtrType, "unpack.raw.alloc") b.CreateStore(ptr, packedAlloc) needsLifetimeEnd = true } else { // Packed data stored on the heap. packedAlloc = ptr } // Load each value from the packed data. values := make([]llvm.Value, len(valueTypes)) for i, valueType := range valueTypes { if b.targetData.TypeAllocSize(valueType) == 0 { // This value has length zero, so there's nothing to load. values[i] = llvm.ConstNull(valueType) continue } indices := []llvm.Value{ llvm.ConstInt(b.ctx.Int32Type(), 0, false), llvm.ConstInt(b.ctx.Int32Type(), uint64(i), false), } gep := b.CreateInBoundsGEP(packedType, packedAlloc, indices, "") values[i] = b.CreateLoad(valueType, gep, "") } if needsLifetimeEnd { allocSize := llvm.ConstInt(b.ctx.Int64Type(), b.targetData.TypeAllocSize(b.uintptrType), false) b.emitLifetimeEnd(packedAlloc, allocSize) } return values } // makeGlobalArray creates a new LLVM global with the given name and integers as // contents, and returns the global and initializer type. // Note that it is left with the default linkage etc., you should set // linkage/constant/etc properties yourself. func (c *compilerContext) makeGlobalArray(buf []byte, name string, elementType llvm.Type) (llvm.Type, llvm.Value) { globalType := llvm.ArrayType(elementType, len(buf)) global := llvm.AddGlobal(c.mod, globalType, name) value := llvm.Undef(globalType) for i := range buf { ch := uint64(buf[i]) value = c.builder.CreateInsertValue(value, llvm.ConstInt(elementType, ch, false), i, "") } global.SetInitializer(value) return globalType, global } // createObjectLayout returns a LLVM value (of type i8*) that describes where // there are pointers in the type t. If all the data fits in a word, it is // returned as a word. Otherwise it will store the data in a global. // // The value contains two pieces of information: the length of the object and // which words contain a pointer (indicated by setting the given bit to 1). For // arrays, only the element is stored. This works because the GC knows the // object size and can therefore know how this value is repeated in the object. // // For details on what's in this value, see src/runtime/gc_precise.go. func (c *compilerContext) createObjectLayout(t llvm.Type, pos token.Pos) llvm.Value { if !typeHasPointers(t) { // There are no pointers in this type, so we can simplify the layout. layout := (uint64(1) << 1) | 1 return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) } // Use the element type for arrays. This works even for nested arrays. for { kind := t.TypeKind() if kind == llvm.ArrayTypeKind { t = t.ElementType() continue } if kind == llvm.StructTypeKind { fields := t.StructElementTypes() if len(fields) == 1 { t = fields[0] continue } } break } // Create the pointer bitmap. objectSizeBytes := c.targetData.TypeAllocSize(t) pointerAlignment := uint64(c.targetData.PrefTypeAlignment(c.dataPtrType)) bitmapLen := objectSizeBytes / pointerAlignment bitmapBytes := (bitmapLen + 7) / 8 bitmap := make([]byte, bitmapBytes, max(bitmapBytes, 8)) c.buildPointerBitmap(bitmap, pointerAlignment, pos, t, 0) // Try to encode the layout inline. pointerSize := c.targetData.TypeAllocSize(c.dataPtrType) pointerBits := pointerSize * 8 if bitmapLen < pointerBits { rawMask := binary.LittleEndian.Uint64(bitmap[0:8]) layout := rawMask*pointerBits + bitmapLen layout <<= 1 layout |= 1 // Check if the layout fits. layout &= 1<>1)/pointerBits == rawMask { // No set bits were shifted off. return llvm.ConstIntToPtr(llvm.ConstInt(c.uintptrType, layout, false), c.dataPtrType) } } // Unfortunately, the object layout is too big to fit in a pointer-sized // integer. Store it in a global instead. // Try first whether the global already exists. All objects with a // particular name have the same type, so this is possible. globalName := "runtime/gc.layout:" + fmt.Sprintf("%d-%0*x", bitmapLen, (bitmapLen+15)/16, bitmap) global := c.mod.NamedGlobal(globalName) if !global.IsNil() { return global } // Create the global initializer. bitmapByteValues := make([]llvm.Value, bitmapBytes) i8 := c.ctx.Int8Type() for i, b := range bitmap { bitmapByteValues[i] = llvm.ConstInt(i8, uint64(b), false) } initializer := c.ctx.ConstStruct([]llvm.Value{ llvm.ConstInt(c.uintptrType, bitmapLen, false), llvm.ConstArray(i8, bitmapByteValues), }, false) // Create the actual global. global = llvm.AddGlobal(c.mod, initializer.Type(), globalName) global.SetInitializer(initializer) global.SetUnnamedAddr(true) global.SetGlobalConstant(true) global.SetLinkage(llvm.LinkOnceODRLinkage) if c.targetData.PrefTypeAlignment(c.uintptrType) < 2 { // AVR doesn't have alignment by default. // The lowest bit must be unset to distinguish this from an inline layout. global.SetAlignment(2) } if c.Debug && pos != token.NoPos { // Creating a fake global so that the value can be inspected in GDB. // For example, the layout for strings.stringFinder (as of Go version // 1.15) has the following type according to GDB: // type = struct { // uintptr numBits; // uint8 data[33]; // } // ...that's sort of a mixed C/Go type, but it is readable. More // importantly, these object layout globals can be read and printed by // GDB which may be useful for debugging. position := c.program.Fset.Position(pos) diglobal := c.dibuilder.CreateGlobalVariableExpression(c.difiles[position.Filename], llvm.DIGlobalVariableExpression{ Name: globalName, File: c.getDIFile(position.Filename), Line: position.Line, Type: c.getDIType(types.NewStruct([]*types.Var{ types.NewVar(pos, nil, "numBits", types.Typ[types.Uintptr]), types.NewVar(pos, nil, "data", types.NewArray(types.Typ[types.Byte], int64(len(bitmapByteValues)))), }, nil)), LocalToUnit: false, Expr: c.dibuilder.CreateExpression(nil), }) global.AddMetadata(0, diglobal) } return global } // buildPointerBitmap scans the given LLVM type for pointers and sets bits in a // bitmap at the word offset that contains a pointer. This scan is recursive. func (c *compilerContext) buildPointerBitmap( dst []byte, ptrAlign uint64, pos token.Pos, t llvm.Type, offset uint64, ) { switch t.TypeKind() { case llvm.IntegerTypeKind, llvm.FloatTypeKind, llvm.DoubleTypeKind: // These types do not contain pointers. case llvm.PointerTypeKind: // Set the corresponding position in the bitmap. dst[offset/8] |= 1 << (offset % 8) case llvm.StructTypeKind: // Recurse over struct elements. for i, et := range t.StructElementTypes() { eo := c.targetData.ElementOffset(t, i) if eo%uint64(ptrAlign) != 0 { if typeHasPointers(et) { // This error will let the compilation fail, but by continuing // the error can still easily be shown. c.addError(pos, "internal error: allocated struct contains unaligned pointer") } continue } c.buildPointerBitmap( dst, ptrAlign, pos, et, offset+(eo/ptrAlign), ) } case llvm.ArrayTypeKind: // Recurse over array elements. len := t.ArrayLength() if len <= 0 { return } et := t.ElementType() elementSize := c.targetData.TypeAllocSize(et) if elementSize%ptrAlign != 0 { if typeHasPointers(et) { // This error will let the compilation fail (but continues so that // other errors can be shown). c.addError(pos, "internal error: allocated array contains unaligned pointer") } return } elementSize /= ptrAlign for i := range len { c.buildPointerBitmap( dst, ptrAlign, pos, et, offset+uint64(i)*elementSize, ) } default: // Should not happen. panic("unknown LLVM type") } } // archFamily returns the architecture from the LLVM triple but with some // architecture names ("armv6", "thumbv7m", etc) merged into a single // architecture name ("arm"). func (c *compilerContext) archFamily() string { return compileopts.CanonicalArchName(c.Triple) } // isThumb returns whether we're in ARM or in Thumb mode. It panics if the // features string is not one for an ARM architecture. func (c *compilerContext) isThumb() bool { var isThumb, isNotThumb bool for feature := range strings.SplitSeq(c.Features, ",") { if feature == "+thumb-mode" { isThumb = true } if feature == "-thumb-mode" { isNotThumb = true } } if isThumb == isNotThumb { panic("unexpected feature flags") } return isThumb } // readStackPointer emits a LLVM intrinsic call that returns the current stack // pointer as an *i8. func (b *builder) readStackPointer() llvm.Value { name := "llvm.stacksave.p0" if llvmutil.Version() < 18 { name = "llvm.stacksave" // backwards compatibility with LLVM 17 and below } stacksave := b.mod.NamedFunction(name) if stacksave.IsNil() { fnType := llvm.FunctionType(b.dataPtrType, nil, false) stacksave = llvm.AddFunction(b.mod, name, fnType) } return b.CreateCall(stacksave.GlobalValueType(), stacksave, nil, "") } // writeStackPointer emits a LLVM intrinsic call that updates the current stack // pointer. func (b *builder) writeStackPointer(sp llvm.Value) { name := "llvm.stackrestore.p0" if llvmutil.Version() < 18 { name = "llvm.stackrestore" // backwards compatibility with LLVM 17 and below } stackrestore := b.mod.NamedFunction(name) if stackrestore.IsNil() { fnType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.dataPtrType}, false) stackrestore = llvm.AddFunction(b.mod, name, fnType) } b.CreateCall(stackrestore.GlobalValueType(), stackrestore, []llvm.Value{sp}, "") } // createZExtOrTrunc lets the input value fit in the output type bits, by zero // extending or truncating the integer. func (b *builder) createZExtOrTrunc(value llvm.Value, t llvm.Type) llvm.Value { valueBits := value.Type().IntTypeWidth() resultBits := t.IntTypeWidth() if valueBits > resultBits { value = b.CreateTrunc(value, t, "") } else if valueBits < resultBits { value = b.CreateZExt(value, t, "") } return value } // Reverse a slice of bytes. From the wiki: // https://github.com/golang/go/wiki/SliceTricks#reversing func reverseBytes(buf []byte) { for i := len(buf)/2 - 1; i >= 0; i-- { opp := len(buf) - 1 - i buf[i], buf[opp] = buf[opp], buf[i] } }