package ir import ( "go/ast" "go/token" "go/types" "sort" "strings" "github.com/aykevl/go-llvm" "github.com/aykevl/tinygo/loader" "golang.org/x/tools/go/ssa" ) // This file provides a wrapper around go/ssa values and adds extra // functionality to them. // View on all functions, types, and globals in a program, with analysis // results. type Program struct { Program *ssa.Program LoaderProgram *loader.Program mainPkg *ssa.Package Functions []*Function functionMap map[*ssa.Function]*Function Globals []*Global globalMap map[*ssa.Global]*Global comments map[string]*ast.CommentGroup NamedTypes []*NamedType needsScheduler bool goCalls []*ssa.Go } // Function or method. type Function struct { *ssa.Function LLVMFn llvm.Value linkName string // go:linkname, go:export, go:interrupt exported bool // go:export nobounds bool // go:nobounds blocking bool // calculated by AnalyseBlockingRecursive flag bool // used by dead code elimination interrupt bool // go:interrupt parents []*Function // calculated by AnalyseCallgraph children []*Function // calculated by AnalyseCallgraph } // Global variable, possibly constant. type Global struct { *ssa.Global program *Program LLVMGlobal llvm.Value linkName string // go:extern extern bool // go:extern initializer Value } // Type with a name and possibly methods. type NamedType struct { *ssa.Type LLVMType llvm.Type } // Type that is at some point put in an interface. type TypeWithMethods struct { t types.Type Num int Methods map[string]*types.Selection } // Interface type that is at some point used in a type assert (to check whether // it implements another interface). type Interface struct { Num int Type *types.Interface } // Create and intialize a new *Program from a *ssa.Program. func NewProgram(lprogram *loader.Program, mainPath string) *Program { comments := map[string]*ast.CommentGroup{} for _, pkgInfo := range lprogram.Sorted() { for _, file := range pkgInfo.Files { for _, decl := range file.Decls { switch decl := decl.(type) { case *ast.GenDecl: switch decl.Tok { case token.TYPE, token.VAR: if len(decl.Specs) != 1 { continue } for _, spec := range decl.Specs { switch spec := spec.(type) { case *ast.TypeSpec: // decl.Tok == token.TYPE id := pkgInfo.Pkg.Path() + "." + spec.Name.Name comments[id] = decl.Doc case *ast.ValueSpec: // decl.Tok == token.VAR for _, name := range spec.Names { id := pkgInfo.Pkg.Path() + "." + name.Name comments[id] = decl.Doc } } } } } } } } program := lprogram.LoadSSA() program.Build() // Find the main package, which is a bit difficult when running a .go file // directly. mainPkg := program.ImportedPackage(mainPath) if mainPkg == nil { for _, pkgInfo := range program.AllPackages() { if pkgInfo.Pkg.Name() == "main" { if mainPkg != nil { panic("more than one main package found") } mainPkg = pkgInfo } } } if mainPkg == nil { panic("could not find main package") } // Make a list of packages in import order. packageList := []*ssa.Package{} packageSet := map[string]struct{}{} worklist := []string{"runtime", mainPath} for len(worklist) != 0 { pkgPath := worklist[0] var pkg *ssa.Package if pkgPath == mainPath { pkg = mainPkg // necessary for compiling individual .go files } else { pkg = program.ImportedPackage(pkgPath) } if pkg == nil { // Non-SSA package (e.g. cgo). packageSet[pkgPath] = struct{}{} worklist = worklist[1:] continue } if _, ok := packageSet[pkgPath]; ok { // Package already in the final package list. worklist = worklist[1:] continue } unsatisfiedImports := make([]string, 0) imports := pkg.Pkg.Imports() for _, pkg := range imports { if _, ok := packageSet[pkg.Path()]; ok { continue } unsatisfiedImports = append(unsatisfiedImports, pkg.Path()) } if len(unsatisfiedImports) == 0 { // All dependencies of this package are satisfied, so add this // package to the list. packageList = append(packageList, pkg) packageSet[pkgPath] = struct{}{} worklist = worklist[1:] } else { // Prepend all dependencies to the worklist and reconsider this // package (by not removing it from the worklist). At that point, it // must be possible to add it to packageList. worklist = append(unsatisfiedImports, worklist...) } } p := &Program{ Program: program, LoaderProgram: lprogram, mainPkg: mainPkg, functionMap: make(map[*ssa.Function]*Function), globalMap: make(map[*ssa.Global]*Global), comments: comments, } for _, pkg := range packageList { p.AddPackage(pkg) } return p } // Add a package to this Program. All packages need to be added first before any // analysis is done for correct results. func (p *Program) AddPackage(pkg *ssa.Package) { memberNames := make([]string, 0) for name := range pkg.Members { if isCGoInternal(name) { continue } memberNames = append(memberNames, name) } sort.Strings(memberNames) for _, name := range memberNames { member := pkg.Members[name] switch member := member.(type) { case *ssa.Function: if isCGoInternal(member.Name()) { continue } p.addFunction(member) case *ssa.Type: t := &NamedType{Type: member} p.NamedTypes = append(p.NamedTypes, t) methods := getAllMethods(pkg.Prog, member.Type()) if !types.IsInterface(member.Type()) { // named type for _, method := range methods { p.addFunction(pkg.Prog.MethodValue(method)) } } case *ssa.Global: g := &Global{program: p, Global: member} doc := p.comments[g.RelString(nil)] if doc != nil { g.parsePragmas(doc) } p.Globals = append(p.Globals, g) p.globalMap[member] = g case *ssa.NamedConst: // Ignore: these are already resolved. default: panic("unknown member type: " + member.String()) } } } func (p *Program) addFunction(ssaFn *ssa.Function) { f := &Function{Function: ssaFn} f.parsePragmas() p.Functions = append(p.Functions, f) p.functionMap[ssaFn] = f for _, anon := range ssaFn.AnonFuncs { p.addFunction(anon) } } // Return true if this package imports "unsafe", false otherwise. func hasUnsafeImport(pkg *types.Package) bool { for _, imp := range pkg.Imports() { if imp == types.Unsafe { return true } } return false } func (p *Program) GetFunction(ssaFn *ssa.Function) *Function { return p.functionMap[ssaFn] } func (p *Program) GetGlobal(ssaGlobal *ssa.Global) *Global { return p.globalMap[ssaGlobal] } func (p *Program) MainPkg() *ssa.Package { return p.mainPkg } // Parse compiler directives in the preceding comments. func (f *Function) parsePragmas() { if f.Syntax() == nil { return } if decl, ok := f.Syntax().(*ast.FuncDecl); ok && decl.Doc != nil { for _, comment := range decl.Doc.List { if !strings.HasPrefix(comment.Text, "//go:") { continue } parts := strings.Fields(comment.Text) switch parts[0] { case "//go:export": if len(parts) != 2 { continue } f.linkName = parts[1] f.exported = true case "//go:interrupt": if len(parts) != 2 { continue } name := parts[1] if strings.HasSuffix(name, "_vect") { // AVR vector naming name = "__vector_" + name[:len(name)-5] } f.linkName = name f.exported = true f.interrupt = true case "//go:linkname": if len(parts) != 3 || parts[1] != f.Name() { continue } // Only enable go:linkname when the package imports "unsafe". // This is a slightly looser requirement than what gc uses: gc // requires the file to import "unsafe", not the package as a // whole. if hasUnsafeImport(f.Pkg.Pkg) { f.linkName = parts[2] } case "//go:nobounds": // Skip bounds checking in this function. Useful for some // runtime functions. // This is somewhat dangerous and thus only imported in packages // that import unsafe. if hasUnsafeImport(f.Pkg.Pkg) { f.nobounds = true } } } } } func (f *Function) IsNoBounds() bool { return f.nobounds } // Return true iff this function is externally visible. func (f *Function) IsExported() bool { return f.exported } // Return true for functions annotated with //go:interrupt. The function name is // already customized in LinkName() to hook up in the interrupt vector. // // On some platforms (like AVR), interrupts need a special compiler flag. func (f *Function) IsInterrupt() bool { return f.exported } // Return the link name for this function. func (f *Function) LinkName() string { if f.linkName != "" { return f.linkName } if f.Signature.Recv() != nil { // Method on a defined type (which may be a pointer). return f.RelString(nil) } else { // Bare function. if name := f.CName(); name != "" { // Name CGo functions directly. return name } else { return f.RelString(nil) } } } // Return the name of the C function if this is a CGo wrapper. Otherwise, return // a zero-length string. func (f *Function) CName() string { name := f.Name() if strings.HasPrefix(name, "_Cfunc_") { return name[len("_Cfunc_"):] } return "" } // Parse //go: pragma comments from the source. func (g *Global) parsePragmas(doc *ast.CommentGroup) { for _, comment := range doc.List { if !strings.HasPrefix(comment.Text, "//go:") { continue } parts := strings.Fields(comment.Text) switch parts[0] { case "//go:extern": g.extern = true if len(parts) == 2 { g.linkName = parts[1] } } } } // Return the link name for this global. func (g *Global) LinkName() string { if g.linkName != "" { return g.linkName } return g.RelString(nil) } func (g *Global) IsExtern() bool { return g.extern } 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 { if t, ok := t.(*types.Named); !ok { return false } else { if t.Obj().Pkg() == nil { return false } id := t.Obj().Pkg().Path() + "." + t.Obj().Name() doc := p.comments[id] if doc == nil { return false } for _, line := range doc.List { if strings.TrimSpace(line.Text) == "//go:volatile" { return true } } return false } } // Return true if this is a CGo-internal function that can be ignored. func isCGoInternal(name string) bool { if strings.HasPrefix(name, "_Cgo_") || strings.HasPrefix(name, "_cgo") { // _Cgo_ptr, _Cgo_use, _cgoCheckResult, _cgo_runtime_cgocall return true // CGo-internal functions } if strings.HasPrefix(name, "__cgofn__cgo_") { return true // CGo function pointer in global scope } return false } // Get all methods of a type. func getAllMethods(prog *ssa.Program, typ types.Type) []*types.Selection { ms := prog.MethodSets.MethodSet(typ) methods := make([]*types.Selection, ms.Len()) for i := 0; i < ms.Len(); i++ { methods[i] = ms.At(i) } return methods }