transform: split interface and reflect lowering

These two passes are related, but can definitely work independently.
Which is what this change does: it splits the two passes. This should
make it easier to change these two new passes in the future.

This change now also enables slightly better testing by testing these
two passes independently. In particular, the reflect lowering pass got
some actual tests: it was barely unit-tested before.

I have verified that this doesn't really change code size, at least not
on the microbit target. Two tests do change, but in a very minor way
(and in opposite direction).
This commit is contained in:
Ayke van Laethem
2021-04-26 13:53:05 +02:00
committed by Ron Evans
parent 52d8655eec
commit cd517a30af
10 changed files with 395 additions and 280 deletions
+74 -190
View File
@@ -74,12 +74,10 @@ type methodInfo struct {
// typeInfo describes a single concrete Go type, which can be a basic or a named
// type. If it is a named type, it may have methods.
type typeInfo struct {
name string
typecode llvm.Value
methodSet llvm.Value
num uint64 // the type number after lowering
countTypeAsserts int // how often a type assert happens on this method
methods []*methodInfo
name string
typecode llvm.Value
methodSet llvm.Value
methods []*methodInfo
}
// getMethod looks up the method on this type with the given signature and
@@ -94,27 +92,13 @@ func (t *typeInfo) getMethod(signature *signatureInfo) *methodInfo {
panic("could not find method")
}
// typeInfoSlice implements sort.Slice, sorting the most commonly used types
// first.
type typeInfoSlice []*typeInfo
func (t typeInfoSlice) Len() int { return len(t) }
func (t typeInfoSlice) Less(i, j int) bool {
// Try to sort the most commonly used types first.
if t[i].countTypeAsserts != t[j].countTypeAsserts {
return t[i].countTypeAsserts < t[j].countTypeAsserts
}
return t[i].name < t[j].name
}
func (t typeInfoSlice) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
// interfaceInfo keeps information about a Go interface type, including all
// methods it has.
type interfaceInfo struct {
name string // name with $interface suffix
methodSet llvm.Value // global which this interfaceInfo describes
signatures []*signatureInfo // method set
types typeInfoSlice // types this interface implements
types []*typeInfo // types this interface implements
assertFunc llvm.Value // runtime.interfaceImplements replacement
methodFuncs map[*signatureInfo]llvm.Value // runtime.interfaceMethod replacements for each signature
}
@@ -163,7 +147,6 @@ func LowerInterfaces(mod llvm.Module, sizeLevel int) error {
// run runs the pass itself.
func (p *lowerInterfacesPass) run() error {
// Collect all type codes.
var typecodeIDs []llvm.Value
for global := p.mod.FirstGlobal(); !global.IsNil(); global = llvm.NextGlobal(global) {
if strings.HasPrefix(global.Name(), "reflect/types.type:") {
// Retrieve Go type information based on an opaque global variable.
@@ -171,7 +154,6 @@ func (p *lowerInterfacesPass) run() error {
// discarded afterwards.
name := strings.TrimPrefix(global.Name(), "reflect/types.type:")
if _, ok := p.types[name]; !ok {
typecodeIDs = append(typecodeIDs, global)
t := &typeInfo{
name: name,
typecode: global,
@@ -187,18 +169,6 @@ func (p *lowerInterfacesPass) run() error {
}
}
// Count per type how often it is type asserted on (e.g. in a switch
// statement).
typeAssert := p.mod.NamedFunction("runtime.typeAssert")
typeAssertUses := getUses(typeAssert)
for _, use := range typeAssertUses {
typecode := use.Operand(1)
name := strings.TrimPrefix(typecode.Name(), "reflect/types.typeid:")
if t, ok := p.types[name]; ok {
t.countTypeAsserts++
}
}
// Find all interface method calls.
interfaceMethod := p.mod.NamedFunction("runtime.interfaceMethod")
interfaceMethodUses := getUses(interfaceMethod)
@@ -274,10 +244,11 @@ func (p *lowerInterfacesPass) run() error {
}
}
// Sort all types added to the interfaces, to check for more common types
// first.
// Sort all types added to the interfaces.
for _, itf := range p.interfaces {
sort.Sort(itf.types)
sort.Slice(itf.types, func(i, j int) bool {
return itf.types[i].name > itf.types[j].name
})
}
// Replace all interface methods with their uses, if possible.
@@ -339,43 +310,10 @@ func (p *lowerInterfacesPass) run() error {
use.EraseFromParentAsInstruction()
}
// Make a slice of types sorted by frequency of use.
typeSlice := make(typeInfoSlice, 0, len(p.types))
for _, t := range p.types {
typeSlice = append(typeSlice, t)
}
sort.Sort(sort.Reverse(typeSlice))
// Assign a type code for each type.
assignTypeCodes(p.mod, typeSlice)
// Replace each use of a ptrtoint runtime.typecodeID with the constant type
// code.
for _, global := range typecodeIDs {
for _, use := range getUses(global) {
if use.IsAConstantExpr().IsNil() {
continue
}
t := p.types[strings.TrimPrefix(global.Name(), "reflect/types.type:")]
typecode := llvm.ConstInt(p.uintptrType, t.num, false)
switch use.Opcode() {
case llvm.PtrToInt:
// Already of the correct type.
case llvm.BitCast:
// Could happen when stored in an interface (which is of type
// i8*).
typecode = llvm.ConstIntToPtr(typecode, use.Type())
default:
panic("unexpected constant expression")
}
use.ReplaceAllUsesWith(typecode)
}
}
// Replace each type assert with an actual type comparison or (if the type
// assert is impossible) the constant false.
llvmFalse := llvm.ConstInt(p.ctx.Int1Type(), 0, false)
for _, use := range typeAssertUses {
for _, use := range getUses(p.mod.NamedFunction("runtime.typeAssert")) {
actualType := use.Operand(0)
name := strings.TrimPrefix(use.Operand(1).Name(), "reflect/types.typeid:")
if t, ok := p.types[name]; ok {
@@ -395,54 +333,13 @@ func (p *lowerInterfacesPass) run() error {
use.EraseFromParentAsInstruction()
}
// Fill in each helper function for type asserts on interfaces
// (interface-to-interface matches).
for _, itf := range p.interfaces {
if !itf.assertFunc.IsNil() {
p.createInterfaceImplementsFunc(itf)
}
for signature := range itf.methodFuncs {
p.createInterfaceMethodFunc(itf, signature)
}
}
// Replace all ptrtoint typecode placeholders with their final type code
// numbers.
for _, typ := range p.types {
for _, use := range getUses(typ.typecode) {
if !use.IsAConstantExpr().IsNil() && use.Opcode() == llvm.PtrToInt {
use.ReplaceAllUsesWith(llvm.ConstInt(p.uintptrType, typ.num, false))
}
}
}
// Remove most objects created for interface and reflect lowering.
// Unnecessary, but cleans up the IR for inspection and testing.
for _, typ := range p.types {
// Only some typecodes have an initializer.
initializer := typ.typecode.Initializer()
if !initializer.IsNil() {
references := llvm.ConstExtractValue(initializer, []uint32{0})
typ.typecode.SetInitializer(llvm.ConstNull(initializer.Type()))
if strings.HasPrefix(typ.name, "reflect/types.type:struct:") {
// Structs have a 'references' field that is not a typecode but
// a pointer to a runtime.structField array and therefore a
// bitcast. This global should be erased separately, otherwise
// typecode objects cannot be erased.
structFields := references.Operand(0)
structFields.EraseFromParentAsGlobal()
}
}
if !typ.methodSet.IsNil() {
typ.methodSet.EraseFromParentAsGlobal()
typ.methodSet = llvm.Value{}
}
}
for _, itf := range p.interfaces {
// Remove method sets of interfaces.
itf.methodSet.EraseFromParentAsGlobal()
itf.methodSet = llvm.Value{}
// Remove all method sets, which are now unnecessary and inhibit later
// optimizations if they are left in place.
for _, t := range p.types {
initializer := t.typecode.Initializer()
methodSet := llvm.ConstExtractValue(initializer, []uint32{2})
initializer = llvm.ConstInsertValue(initializer, llvm.ConstNull(methodSet.Type()), []uint32{2})
t.typecode.SetInitializer(initializer)
}
return nil
@@ -559,6 +456,10 @@ func (p *lowerInterfacesPass) replaceInvokeWithCall(use llvm.Value, typ *typeInf
// getInterfaceImplementsFunc returns a function that checks whether a given
// interface type implements a given interface, by checking all possible types
// that implement this interface.
//
// The type match is implemented using an if/else chain over all possible types.
// This if/else chain is easily converted to a big switch over all possible
// types by the LLVM simplifycfg pass.
func (p *lowerInterfacesPass) getInterfaceImplementsFunc(itf *interfaceInfo) llvm.Value {
if !itf.assertFunc.IsNil() {
return itf.assertFunc
@@ -568,60 +469,49 @@ func (p *lowerInterfacesPass) getInterfaceImplementsFunc(itf *interfaceInfo) llv
// TODO: debug info
fnName := itf.id() + "$typeassert"
fnType := llvm.FunctionType(p.ctx.Int1Type(), []llvm.Type{p.uintptrType}, false)
itf.assertFunc = llvm.AddFunction(p.mod, fnName, fnType)
itf.assertFunc.Param(0).SetName("actualType")
// Type asserts will be made for each type, so increment the counter for
// those.
for _, typ := range itf.types {
typ.countTypeAsserts++
}
return itf.assertFunc
}
// createInterfaceImplementsFunc finishes the work of
// getInterfaceImplementsFunc, because it needs to run after types have a type
// code assigned.
//
// The type match is implemented using a big type switch over all possible
// types.
func (p *lowerInterfacesPass) createInterfaceImplementsFunc(itf *interfaceInfo) {
fn := itf.assertFunc
fn := llvm.AddFunction(p.mod, fnName, fnType)
itf.assertFunc = fn
fn.Param(0).SetName("actualType")
fn.SetLinkage(llvm.InternalLinkage)
fn.SetUnnamedAddr(true)
if p.sizeLevel >= 2 {
fn.AddFunctionAttr(p.ctx.CreateEnumAttribute(llvm.AttributeKindID("optsize"), 0))
}
// TODO: debug info
// Create all used basic blocks.
// Start the if/else chain at the entry block.
entry := p.ctx.AddBasicBlock(fn, "entry")
thenBlock := p.ctx.AddBasicBlock(fn, "then")
elseBlock := p.ctx.AddBasicBlock(fn, "else")
// Add all possible types as cases.
p.builder.SetInsertPointAtEnd(entry)
// Iterate over all possible types. Each iteration creates a new branch
// either to the 'then' block (success) or the .next block, for the next
// check.
actualType := fn.Param(0)
sw := p.builder.CreateSwitch(actualType, elseBlock, len(itf.types))
for _, typ := range itf.types {
sw.AddCase(llvm.ConstInt(p.uintptrType, typ.num, false), thenBlock)
nextBlock := p.ctx.AddBasicBlock(fn, typ.name+".next")
cmp := p.builder.CreateICmp(llvm.IntEQ, actualType, llvm.ConstPtrToInt(typ.typecode, p.uintptrType), typ.name+".icmp")
p.builder.CreateCondBr(cmp, thenBlock, nextBlock)
p.builder.SetInsertPointAtEnd(nextBlock)
}
// The builder is now inserting at the last *.next block. Once we reach
// this point, all types have been checked so the type assert will have
// failed.
p.builder.CreateRet(llvm.ConstInt(p.ctx.Int1Type(), 0, false))
// Fill 'then' block (type assert was successful).
p.builder.SetInsertPointAtEnd(thenBlock)
p.builder.CreateRet(llvm.ConstInt(p.ctx.Int1Type(), 1, false))
// Fill 'else' block (type asserted failed).
p.builder.SetInsertPointAtEnd(elseBlock)
p.builder.CreateRet(llvm.ConstInt(p.ctx.Int1Type(), 0, false))
return itf.assertFunc
}
// getInterfaceMethodFunc returns a thunk for calling a method on an interface.
// It only declares the function, createInterfaceMethodFunc actually defines the
// function.
func (p *lowerInterfacesPass) getInterfaceMethodFunc(itf *interfaceInfo, signature *signatureInfo, returnType llvm.Type, params []llvm.Type) llvm.Value {
//
// Matching the actual type is implemented using an if/else chain over all
// possible types. This is later converted to a switch statement by the LLVM
// simplifycfg pass.
func (p *lowerInterfacesPass) getInterfaceMethodFunc(itf *interfaceInfo, signature *signatureInfo, returnType llvm.Type, paramTypes []llvm.Type) llvm.Value {
if fn, ok := itf.methodFuncs[signature]; ok {
// This function has already been created.
return fn
@@ -634,22 +524,11 @@ func (p *lowerInterfacesPass) getInterfaceMethodFunc(itf *interfaceInfo, signatu
// Construct the function name, which is of the form:
// (main.Stringer).String
fnName := "(" + itf.id() + ")." + signature.methodName()
fnType := llvm.FunctionType(returnType, append(params, llvm.PointerType(p.ctx.Int8Type(), 0)), false)
fnType := llvm.FunctionType(returnType, append(paramTypes, llvm.PointerType(p.ctx.Int8Type(), 0)), false)
fn := llvm.AddFunction(p.mod, fnName, fnType)
llvm.PrevParam(fn.LastParam()).SetName("actualType")
fn.LastParam().SetName("parentHandle")
itf.methodFuncs[signature] = fn
return fn
}
// createInterfaceMethodFunc finishes the work of getInterfaceMethodFunc,
// because it needs to run after type codes have been assigned to concrete
// types.
//
// Matching the actual type is implemented using a big type switch over all
// possible types.
func (p *lowerInterfacesPass) createInterfaceMethodFunc(itf *interfaceInfo, signature *signatureInfo) {
fn := itf.methodFuncs[signature]
fn.SetLinkage(llvm.InternalLinkage)
fn.SetUnnamedAddr(true)
if p.sizeLevel >= 2 {
@@ -658,29 +537,6 @@ func (p *lowerInterfacesPass) createInterfaceMethodFunc(itf *interfaceInfo, sign
// TODO: debug info
// Create entry block.
entry := p.ctx.AddBasicBlock(fn, "entry")
// Create default block and call runtime.nilPanic.
// The only other possible value remaining is nil for nil interfaces. We
// could panic with a different message here such as "nil interface" but
// that would increase code size and "nil panic" is close enough. Most
// importantly, it avoids undefined behavior when accidentally calling a
// method on a nil interface.
defaultBlock := p.ctx.AddBasicBlock(fn, "default")
p.builder.SetInsertPointAtEnd(defaultBlock)
nilPanic := p.mod.NamedFunction("runtime.nilPanic")
p.builder.CreateCall(nilPanic, []llvm.Value{
llvm.Undef(llvm.PointerType(p.ctx.Int8Type(), 0)),
llvm.Undef(llvm.PointerType(p.ctx.Int8Type(), 0)),
}, "")
p.builder.CreateUnreachable()
// Create type switch in entry block.
p.builder.SetInsertPointAtEnd(entry)
actualType := llvm.PrevParam(fn.LastParam())
sw := p.builder.CreateSwitch(actualType, defaultBlock, len(itf.types))
// Collect the params that will be passed to the functions to call.
// These params exclude the receiver (which may actually consist of multiple
// parts).
@@ -689,10 +545,18 @@ func (p *lowerInterfacesPass) createInterfaceMethodFunc(itf *interfaceInfo, sign
params[i] = fn.Param(i + 1)
}
// Start chain in the entry block.
entry := p.ctx.AddBasicBlock(fn, "entry")
p.builder.SetInsertPointAtEnd(entry)
// Define all possible functions that can be called.
actualType := llvm.PrevParam(fn.LastParam())
for _, typ := range itf.types {
// Create type check (if/else).
bb := p.ctx.AddBasicBlock(fn, typ.name)
sw.AddCase(llvm.ConstInt(p.uintptrType, typ.num, false), bb)
next := p.ctx.AddBasicBlock(fn, typ.name+".next")
cmp := p.builder.CreateICmp(llvm.IntEQ, actualType, llvm.ConstPtrToInt(typ.typecode, p.uintptrType), typ.name+".icmp")
p.builder.CreateCondBr(cmp, bb, next)
// The function we will redirect to when the interface has this type.
function := typ.getMethod(signature).function
@@ -725,5 +589,25 @@ func (p *lowerInterfacesPass) createInterfaceMethodFunc(itf *interfaceInfo, sign
} else {
p.builder.CreateRet(retval)
}
// Start next comparison in the 'next' block (which is jumped to when
// the type doesn't match).
p.builder.SetInsertPointAtEnd(next)
}
// The builder now points to the last *.then block, after all types have
// been checked. Call runtime.nilPanic here.
// The only other possible value remaining is nil for nil interfaces. We
// could panic with a different message here such as "nil interface" but
// that would increase code size and "nil panic" is close enough. Most
// importantly, it avoids undefined behavior when accidentally calling a
// method on a nil interface.
nilPanic := p.mod.NamedFunction("runtime.nilPanic")
p.builder.CreateCall(nilPanic, []llvm.Value{
llvm.Undef(llvm.PointerType(p.ctx.Int8Type(), 0)),
llvm.Undef(llvm.PointerType(p.ctx.Int8Type(), 0)),
}, "")
p.builder.CreateUnreachable()
return fn
}