github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/transform/rtcalls.go (about) 1 package transform 2 3 // This file implements several small optimizations of runtime and reflect 4 // calls. 5 6 import ( 7 "strings" 8 9 "tinygo.org/x/go-llvm" 10 ) 11 12 // OptimizeStringToBytes transforms runtime.stringToBytes(...) calls into const 13 // []byte slices whenever possible. This optimizes the following pattern: 14 // 15 // w.Write([]byte("foo")) 16 // 17 // where Write does not store to the slice. 18 func OptimizeStringToBytes(mod llvm.Module) { 19 stringToBytes := mod.NamedFunction("runtime.stringToBytes") 20 if stringToBytes.IsNil() { 21 // nothing to optimize 22 return 23 } 24 25 for _, call := range getUses(stringToBytes) { 26 strptr := call.Operand(0) 27 strlen := call.Operand(1) 28 29 // strptr is always constant because strings are always constant. 30 31 var pointerUses []llvm.Value 32 canConvertPointer := true 33 for _, use := range getUses(call) { 34 if use.IsAExtractValueInst().IsNil() { 35 // Expected an extractvalue, but this is something else. 36 canConvertPointer = false 37 continue 38 } 39 switch use.Type().TypeKind() { 40 case llvm.IntegerTypeKind: 41 // A length (len or cap). Propagate the length value. 42 // This can always be done because the byte slice is always the 43 // same length as the original string. 44 use.ReplaceAllUsesWith(strlen) 45 use.EraseFromParentAsInstruction() 46 case llvm.PointerTypeKind: 47 // The string pointer itself. 48 if !isReadOnly(use) { 49 // There is a store to the byte slice. This means that none 50 // of the pointer uses can't be propagated. 51 canConvertPointer = false 52 continue 53 } 54 // It may be that the pointer value can be propagated, if all of 55 // the pointer uses are readonly. 56 pointerUses = append(pointerUses, use) 57 default: 58 // should not happen 59 panic("unknown return type of runtime.stringToBytes: " + use.Type().String()) 60 } 61 } 62 if canConvertPointer { 63 // All pointer uses are readonly, so they can be converted. 64 for _, use := range pointerUses { 65 use.ReplaceAllUsesWith(strptr) 66 use.EraseFromParentAsInstruction() 67 } 68 69 // Call to runtime.stringToBytes can be eliminated: both the input 70 // and the output is constant. 71 call.EraseFromParentAsInstruction() 72 } 73 } 74 } 75 76 // OptimizeStringEqual transforms runtime.stringEqual(...) calls into simple 77 // integer comparisons if at least one of the sides of the comparison is zero. 78 // Ths converts str == "" into len(str) == 0 and "" == "" into false. 79 func OptimizeStringEqual(mod llvm.Module) { 80 stringEqual := mod.NamedFunction("runtime.stringEqual") 81 if stringEqual.IsNil() { 82 // nothing to optimize 83 return 84 } 85 86 builder := mod.Context().NewBuilder() 87 defer builder.Dispose() 88 89 for _, call := range getUses(stringEqual) { 90 str1len := call.Operand(1) 91 str2len := call.Operand(3) 92 93 zero := llvm.ConstInt(str1len.Type(), 0, false) 94 if str1len == zero || str2len == zero { 95 builder.SetInsertPointBefore(call) 96 icmp := builder.CreateICmp(llvm.IntEQ, str1len, str2len, "") 97 call.ReplaceAllUsesWith(icmp) 98 call.EraseFromParentAsInstruction() 99 continue 100 } 101 } 102 } 103 104 // OptimizeReflectImplements optimizes the following code: 105 // 106 // implements := someType.Implements(someInterfaceType) 107 // 108 // where someType is an arbitrary reflect.Type and someInterfaceType is a 109 // reflect.Type of interface kind, to the following code: 110 // 111 // _, implements := someType.(interfaceType) 112 // 113 // if the interface type is known at compile time (that is, someInterfaceType is 114 // a LLVM constant aggregate). This optimization is especially important for the 115 // encoding/json package, which uses this method. 116 // 117 // As of this writing, the (reflect.Type).Interface method has not yet been 118 // implemented so this optimization is critical for the encoding/json package. 119 func OptimizeReflectImplements(mod llvm.Module) { 120 implementsSignature := mod.NamedGlobal("reflect/methods.Implements(reflect.Type) bool") 121 if implementsSignature.IsNil() { 122 return 123 } 124 125 builder := mod.Context().NewBuilder() 126 defer builder.Dispose() 127 128 // Look up the (reflect.Value).Implements() method. 129 var implementsFunc llvm.Value 130 for fn := mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) { 131 attr := fn.GetStringAttributeAtIndex(-1, "tinygo-invoke") 132 if attr.IsNil() { 133 continue 134 } 135 if attr.GetStringValue() == "reflect/methods.Implements(reflect.Type) bool" { 136 implementsFunc = fn 137 break 138 } 139 } 140 if implementsFunc.IsNil() { 141 // Doesn't exist in the program, so nothing to do. 142 return 143 } 144 145 for _, call := range getUses(implementsFunc) { 146 if call.IsACallInst().IsNil() { 147 continue 148 } 149 interfaceType := stripPointerCasts(call.Operand(2)) 150 if interfaceType.IsAGlobalVariable().IsNil() { 151 // Interface is unknown at compile time. This can't be optimized. 152 continue 153 } 154 155 if strings.HasPrefix(interfaceType.Name(), "reflect/types.type:named:") { 156 // Get the underlying type. 157 interfaceType = stripPointerCasts(builder.CreateExtractValue(interfaceType.Initializer(), 3, "")) 158 } 159 if !strings.HasPrefix(interfaceType.Name(), "reflect/types.type:interface:") { 160 // This is an error. The Type passed to Implements should be of 161 // interface type. Ignore it here (don't report it), it will be 162 // reported at runtime. 163 continue 164 } 165 typeAssertFunction := mod.NamedFunction(strings.TrimPrefix(interfaceType.Name(), "reflect/types.type:") + ".$typeassert") 166 if typeAssertFunction.IsNil() { 167 continue 168 } 169 170 // Replace Implements call with the type assert call. 171 builder.SetInsertPointBefore(call) 172 implements := builder.CreateCall(typeAssertFunction.GlobalValueType(), typeAssertFunction, []llvm.Value{ 173 call.Operand(0), // typecode to check 174 }, "") 175 call.ReplaceAllUsesWith(implements) 176 call.EraseFromParentAsInstruction() 177 } 178 }