all: remove init interpretation during IR construction

The interp package does a much better job at interpretation, and is
implemented as a pass on the IR which makes it much easier to compose.
Also, the implementation works much better as it is based on LLVM IR
instead of Go SSA.
This commit is contained in:
Ayke van Laethem
2019-02-09 13:31:51 +01:00
committed by Ron Evans
parent da345e8723
commit 92d9b780b5
6 changed files with 46 additions and 917 deletions
-522
View File
@@ -1,522 +0,0 @@
package ir
// This file provides functionality to interpret very basic Go SSA, for
// compile-time initialization of globals.
import (
"errors"
"fmt"
"go/constant"
"go/token"
"go/types"
"strings"
"golang.org/x/tools/go/ssa"
)
var ErrCGoWrapper = errors.New("tinygo internal: cgo wrapper") // a signal, not an error
// Ignore these calls (replace with a zero return value) when encountered during
// interpretation.
var ignoreInitCalls = map[string]struct{}{
"syscall.runtime_envs": struct{}{},
"syscall/js.predefValue": struct{}{},
"(syscall/js.Value).Get": struct{}{},
"(syscall/js.Value).New": struct{}{},
"(syscall/js.Value).Int": struct{}{},
"os.init$1": struct{}{},
}
// Interpret instructions as far as possible, and drop those instructions from
// the basic block.
func (p *Program) Interpret(block *ssa.BasicBlock, dumpSSA bool) error {
if dumpSSA {
fmt.Printf("\ninterpret: %s\n", block.Parent().Pkg.Pkg.Path())
}
for {
i, err := p.interpret(block.Instrs, nil, nil, nil, dumpSSA)
if err == ErrCGoWrapper {
// skip this instruction
block.Instrs = block.Instrs[i+1:]
continue
}
block.Instrs = block.Instrs[i:]
return err
}
}
// Interpret instructions as far as possible, and return the index of the first
// unknown instruction.
func (p *Program) interpret(instrs []ssa.Instruction, paramKeys []*ssa.Parameter, paramValues []Value, results []Value, dumpSSA bool) (int, error) {
locals := map[ssa.Value]Value{}
for i, key := range paramKeys {
locals[key] = paramValues[i]
}
for i, instr := range instrs {
if _, ok := instr.(*ssa.DebugRef); ok {
continue
}
if dumpSSA {
if val, ok := instr.(ssa.Value); ok && val.Name() != "" {
fmt.Printf("\t%s: %s = %s\n", instr.Parent().RelString(nil), val.Name(), val.String())
} else {
fmt.Printf("\t%s: %s\n", instr.Parent().RelString(nil), instr.String())
}
}
switch instr := instr.(type) {
case *ssa.Alloc:
alloc, err := p.getZeroValue(instr.Type().Underlying().(*types.Pointer).Elem())
if err != nil {
return i, err
}
locals[instr] = &PointerValue{nil, &alloc}
case *ssa.BinOp:
if typ, ok := instr.Type().(*types.Basic); ok && typ.Kind() == types.String {
// Concatenate two strings.
// This happens in the time package, for example.
x, err := p.getValue(instr.X, locals)
if err != nil {
return i, err
}
y, err := p.getValue(instr.Y, locals)
if err != nil {
return i, err
}
xstr := constant.StringVal(x.(*ConstValue).Expr.Value)
ystr := constant.StringVal(y.(*ConstValue).Expr.Value)
locals[instr] = &ConstValue{ssa.NewConst(constant.MakeString(xstr+ystr), types.Typ[types.String])}
} else {
return i, errors.New("init: unknown binop: " + instr.String())
}
case *ssa.Call:
common := instr.Common()
callee := common.StaticCallee()
if callee == nil {
return i, nil // don't understand dynamic dispatch
}
if _, ok := ignoreInitCalls[callee.String()]; ok {
// These calls are not needed and can be ignored, for the time
// being.
results := make([]Value, callee.Signature.Results().Len())
for i := range results {
var err error
results[i], err = p.getZeroValue(callee.Signature.Results().At(i).Type())
if err != nil {
return i, err
}
}
if len(results) == 1 {
locals[instr] = results[0]
} else if len(results) > 1 {
locals[instr] = &StructValue{Fields: results}
}
continue
}
if callee.String() == "os.NewFile" {
// Emulate the creation of os.Stdin, os.Stdout and os.Stderr.
resultPtrType := callee.Signature.Results().At(0).Type().(*types.Pointer)
resultStructOuterType := resultPtrType.Elem().Underlying().(*types.Struct)
if resultStructOuterType.NumFields() != 1 {
panic("expected 1 field in os.File struct")
}
fileInnerPtrType := resultStructOuterType.Field(0).Type().(*types.Pointer)
fileInnerType := fileInnerPtrType.Elem().(*types.Named)
fileInnerStructType := fileInnerType.Underlying().(*types.Struct)
fileInner, err := p.getZeroValue(fileInnerType) // os.file
if err != nil {
return i, err
}
for fieldIndex := 0; fieldIndex < fileInnerStructType.NumFields(); fieldIndex++ {
field := fileInnerStructType.Field(fieldIndex)
if field.Name() == "name" {
// Set the 'name' field.
name, err := p.getValue(common.Args[1], locals)
if err != nil {
return i, err
}
fileInner.(*StructValue).Fields[fieldIndex] = name
} else if field.Type().String() == "internal/poll.FD" {
// Set the file descriptor field.
field := field.Type().Underlying().(*types.Struct)
for subfieldIndex := 0; subfieldIndex < field.NumFields(); subfieldIndex++ {
subfield := field.Field(subfieldIndex)
if subfield.Name() == "Sysfd" {
sysfd, err := p.getValue(common.Args[0], locals)
if err != nil {
return i, err
}
sysfd = &ConstValue{Expr: ssa.NewConst(sysfd.(*ConstValue).Expr.Value, subfield.Type())}
fileInner.(*StructValue).Fields[fieldIndex].(*StructValue).Fields[subfieldIndex] = sysfd
}
}
}
}
fileInnerPtr := &PointerValue{fileInnerPtrType, &fileInner} // *os.file
var fileOuter Value = &StructValue{Type: resultPtrType.Elem(), Fields: []Value{fileInnerPtr}} // os.File
result := &PointerValue{resultPtrType.Elem(), &fileOuter} // *os.File
locals[instr] = result
continue
}
if canInterpret(callee) {
params := make([]Value, len(common.Args))
for i, arg := range common.Args {
val, err := p.getValue(arg, locals)
if err != nil {
return i, err
}
params[i] = val
}
results := make([]Value, callee.Signature.Results().Len())
subi, err := p.interpret(callee.Blocks[0].Instrs, callee.Params, params, results, dumpSSA)
if err != nil {
return i, err
}
if subi != len(callee.Blocks[0].Instrs) {
return i, errors.New("init: could not interpret all instructions of subroutine")
}
if len(results) == 1 {
locals[instr] = results[0]
} else {
panic("unimplemented: not exactly 1 result")
}
continue
}
if callee.Object() == nil || callee.Object().Name() == "init" {
return i, nil // arrived at the init#num functions
}
return i, errors.New("todo: init call: " + callee.String())
case *ssa.ChangeType:
x, err := p.getValue(instr.X, locals)
if err != nil {
return i, err
}
// The only case when we need to bitcast is when casting between named
// struct types, as those are actually different in LLVM. Let's just
// bitcast all struct types for ease of use.
if _, ok := instr.Type().Underlying().(*types.Struct); ok {
return i, errors.New("todo: init: " + instr.String())
}
locals[instr] = x
case *ssa.Convert:
x, err := p.getValue(instr.X, locals)
if err != nil {
return i, err
}
typeFrom := instr.X.Type().Underlying()
switch typeTo := instr.Type().Underlying().(type) {
case *types.Basic:
if typeTo.Kind() == types.String {
return i, nil
}
if _, ok := typeFrom.(*types.Pointer); ok && typeTo.Kind() == types.UnsafePointer {
locals[instr] = &PointerBitCastValue{typeTo, x}
} else if typeFrom, ok := typeFrom.(*types.Basic); ok {
if typeFrom.Kind() == types.UnsafePointer && typeTo.Kind() == types.Uintptr {
locals[instr] = &PointerToUintptrValue{x}
} else if typeFrom.Info()&types.IsInteger != 0 && typeTo.Info()&types.IsInteger != 0 {
locals[instr] = &ConstValue{Expr: ssa.NewConst(x.(*ConstValue).Expr.Value, typeTo)}
} else {
return i, nil
}
} else {
return i, nil
}
case *types.Pointer:
if typeFrom, ok := typeFrom.(*types.Basic); ok && typeFrom.Kind() == types.UnsafePointer {
locals[instr] = &PointerBitCastValue{typeTo, x}
} else {
panic("expected unsafe pointer conversion")
}
default:
return i, nil
}
case *ssa.DebugRef:
// ignore
case *ssa.Extract:
tuple, err := p.getValue(instr.Tuple, locals)
if err != nil {
return i, err
}
locals[instr] = tuple.(*StructValue).Fields[instr.Index]
case *ssa.FieldAddr:
x, err := p.getValue(instr.X, locals)
if err != nil {
return i, err
}
var structVal *StructValue
switch x := x.(type) {
case *GlobalValue:
structVal = x.Global.initializer.(*StructValue)
case *PointerValue:
structVal = (*x.Elem).(*StructValue)
default:
panic("expected a pointer")
}
locals[instr] = &PointerValue{nil, &structVal.Fields[instr.Field]}
case *ssa.IndexAddr:
x, err := p.getValue(instr.X, locals)
if err != nil {
return i, err
}
if cnst, ok := instr.Index.(*ssa.Const); ok {
index, _ := constant.Int64Val(cnst.Value)
switch xPtr := x.(type) {
case *GlobalValue:
x = xPtr.Global.initializer
case *PointerValue:
x = *xPtr.Elem
default:
panic("expected a pointer")
}
switch x := x.(type) {
case *ArrayValue:
locals[instr] = &PointerValue{nil, &x.Elems[index]}
default:
return i, errors.New("todo: init IndexAddr not on an array or struct")
}
} else {
return i, errors.New("todo: init IndexAddr index: " + instr.Index.String())
}
case *ssa.MakeMap:
locals[instr] = &MapValue{instr.Type().Underlying().(*types.Map), nil, nil}
case *ssa.MapUpdate:
// Assume no duplicate keys exist. This is most likely true for
// autogenerated code, but may not be true when trying to interpret
// user code.
key, err := p.getValue(instr.Key, locals)
if err != nil {
return i, err
}
value, err := p.getValue(instr.Value, locals)
if err != nil {
return i, err
}
x := locals[instr.Map].(*MapValue)
x.Keys = append(x.Keys, key)
x.Values = append(x.Values, value)
case *ssa.Return:
for i, r := range instr.Results {
val, err := p.getValue(r, locals)
if err != nil {
return i, err
}
results[i] = val
}
case *ssa.Slice:
// Turn a just-allocated array into a slice.
if instr.Low != nil || instr.High != nil || instr.Max != nil {
return i, errors.New("init: slice expression with bounds")
}
source, err := p.getValue(instr.X, locals)
if err != nil {
return i, err
}
switch source := source.(type) {
case *PointerValue: // pointer to array
array := (*source.Elem).(*ArrayValue)
locals[instr] = &SliceValue{instr.Type().Underlying().(*types.Slice), array}
default:
return i, errors.New("init: unknown slice type")
}
case *ssa.Store:
if addr, ok := instr.Addr.(*ssa.Global); ok {
if strings.HasPrefix(instr.Addr.Name(), "__cgofn__cgo_") || strings.HasPrefix(instr.Addr.Name(), "_cgo_") {
// Ignore CGo global variables which we don't use.
continue
}
value, err := p.getValue(instr.Val, locals)
if err != nil {
return i, err
}
p.GetGlobal(addr).initializer = value
} else if addr, ok := locals[instr.Addr]; ok {
value, err := p.getValue(instr.Val, locals)
if err != nil {
return i, err
}
if addr, ok := addr.(*PointerValue); ok {
*(addr.Elem) = value
} else {
panic("store to non-pointer")
}
} else {
return i, errors.New("todo: init Store: " + instr.String())
}
case *ssa.UnOp:
if instr.Op != token.MUL || instr.CommaOk {
return i, errors.New("init: unknown unop: " + instr.String())
}
valPtr, err := p.getValue(instr.X, locals)
if err != nil {
return i, err
}
switch valPtr := valPtr.(type) {
case *GlobalValue:
locals[instr] = valPtr.Global.initializer
case *PointerValue:
locals[instr] = *valPtr.Elem
default:
panic("expected a pointer")
}
default:
return i, nil
}
}
return len(instrs), nil
}
// Check whether this function can be interpreted at compile time. For that, it
// needs to only contain relatively simple instructions (for example, no control
// flow).
func canInterpret(callee *ssa.Function) bool {
if len(callee.Blocks) != 1 || callee.Signature.Results().Len() != 1 {
// No control flow supported so only one basic block.
// Only exactly one return value supported right now so check that as
// well.
return false
}
for _, instr := range callee.Blocks[0].Instrs {
switch instr.(type) {
// Ignore all functions fully supported by Program.interpret()
// above.
case *ssa.Alloc:
case *ssa.ChangeType:
case *ssa.DebugRef:
case *ssa.Extract:
case *ssa.FieldAddr:
case *ssa.IndexAddr:
case *ssa.MakeMap:
case *ssa.MapUpdate:
case *ssa.Return:
case *ssa.Slice:
case *ssa.Store:
case *ssa.UnOp:
default:
return false
}
}
return true
}
func (p *Program) getValue(value ssa.Value, locals map[ssa.Value]Value) (Value, error) {
switch value := value.(type) {
case *ssa.Const:
return &ConstValue{value}, nil
case *ssa.Function:
return &FunctionValue{value.Type(), value}, nil
case *ssa.Global:
if strings.HasPrefix(value.Name(), "__cgofn__cgo_") || strings.HasPrefix(value.Name(), "_cgo_") {
// Ignore CGo global variables which we don't use.
return nil, ErrCGoWrapper
}
g := p.GetGlobal(value)
if g.initializer == nil {
value, err := p.getZeroValue(value.Type().Underlying().(*types.Pointer).Elem())
if err != nil {
return nil, err
}
g.initializer = value
}
return &GlobalValue{g}, nil
default:
if local, ok := locals[value]; ok {
return local, nil
} else {
return nil, errors.New("todo: init: unknown value: " + value.String())
}
}
}
func (p *Program) getZeroValue(t types.Type) (Value, error) {
switch typ := t.Underlying().(type) {
case *types.Array:
elems := make([]Value, typ.Len())
for i := range elems {
elem, err := p.getZeroValue(typ.Elem())
if err != nil {
return nil, err
}
elems[i] = elem
}
return &ArrayValue{typ.Elem(), elems}, nil
case *types.Basic:
return &ZeroBasicValue{typ}, nil
case *types.Signature:
return &FunctionValue{typ, nil}, nil
case *types.Map:
return &MapValue{typ, nil, nil}, nil
case *types.Pointer:
return &PointerValue{typ, nil}, nil
case *types.Struct:
elems := make([]Value, typ.NumFields())
for i := range elems {
elem, err := p.getZeroValue(typ.Field(i).Type())
if err != nil {
return nil, err
}
elems[i] = elem
}
return &StructValue{t, elems}, nil
case *types.Slice:
return &SliceValue{typ, nil}, nil
default:
return nil, errors.New("todo: init: unknown global type: " + typ.String())
}
}
// Boxed value for interpreter.
type Value interface {
}
type ConstValue struct {
Expr *ssa.Const
}
type ZeroBasicValue struct {
Type *types.Basic
}
type PointerValue struct {
Type types.Type
Elem *Value
}
type FunctionValue struct {
Type types.Type
Elem *ssa.Function
}
type PointerBitCastValue struct {
Type types.Type
Elem Value
}
type PointerToUintptrValue struct {
Elem Value
}
type GlobalValue struct {
Global *Global
}
type ArrayValue struct {
ElemType types.Type
Elems []Value
}
type StructValue struct {
Type types.Type // types.Struct or types.Named
Fields []Value
}
type SliceValue struct {
Type *types.Slice
Array *ArrayValue
}
type MapValue struct {
Type *types.Map
Keys []Value
Values []Value
}
+4 -9
View File
@@ -43,11 +43,10 @@ type Function struct {
// Global variable, possibly constant.
type Global struct {
*ssa.Global
program *Program
LLVMGlobal llvm.Value
linkName string // go:extern
extern bool // go:extern
initializer Value
program *Program
LLVMGlobal llvm.Value
linkName string // go:extern
extern bool // go:extern
}
// Type with a name and possibly methods.
@@ -416,10 +415,6 @@ func (g *Global) CName() string {
return ""
}
func (g *Global) Initializer() Value {
return g.initializer
}
// Return true if this named type is annotated with the //go:volatile pragma,
// for volatile loads and stores.
func (p *Program) IsVolatile(t types.Type) bool {