interp: add a function to directly compile Go AST
Adds CompileAST, which can be used to compile Go AST directly. This allows users to delegate parsing of source to their own code instead of relying on the interpreter. CLoses #1251
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package interp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
@@ -362,21 +361,14 @@ func wrapInMain(src string) string {
|
||||
return fmt.Sprintf("package main; func main() {%s\n}", src)
|
||||
}
|
||||
|
||||
// Note: no type analysis is performed at this stage, it is done in pre-order
|
||||
// processing of CFG, in order to accommodate forward type declarations.
|
||||
|
||||
// ast parses src string containing Go code and generates the corresponding AST.
|
||||
// The package name and the AST root node are returned.
|
||||
// The given name is used to set the filename of the relevant source file in the
|
||||
// interpreter's FileSet.
|
||||
func (interp *Interpreter) ast(src, name string, inc bool) (string, *node, error) {
|
||||
var inFunc bool
|
||||
func (interp *Interpreter) parse(src, name string, inc bool) (node ast.Node, err error) {
|
||||
mode := parser.DeclarationErrors
|
||||
|
||||
// Allow incremental parsing of declarations or statements, by inserting
|
||||
// them in a pseudo file package or function. Those statements or
|
||||
// declarations will be always evaluated in the global scope.
|
||||
var tok token.Token
|
||||
var inFunc bool
|
||||
if inc {
|
||||
tok = interp.firstToken(src)
|
||||
switch tok {
|
||||
@@ -393,18 +385,18 @@ func (interp *Interpreter) ast(src, name string, inc bool) (string, *node, error
|
||||
}
|
||||
|
||||
if ok, err := interp.buildOk(&interp.context, name, src); !ok || err != nil {
|
||||
return "", nil, err // skip source not matching build constraints
|
||||
return nil, err // skip source not matching build constraints
|
||||
}
|
||||
|
||||
f, err := parser.ParseFile(interp.fset, name, src, mode)
|
||||
if err != nil {
|
||||
// only retry if we're on an expression/statement about a func
|
||||
if !inc || tok != token.FUNC {
|
||||
return "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
// do not bother retrying if we know it's an error we're going to ignore later on.
|
||||
if ignoreError(err, src) {
|
||||
return "", nil, err
|
||||
return nil, err
|
||||
}
|
||||
// do not lose initial error, in case retrying fails.
|
||||
initialError := err
|
||||
@@ -412,16 +404,32 @@ func (interp *Interpreter) ast(src, name string, inc bool) (string, *node, error
|
||||
src := wrapInMain(strings.TrimPrefix(src, "package main;"))
|
||||
f, err = parser.ParseFile(interp.fset, name, src, mode)
|
||||
if err != nil {
|
||||
return "", nil, initialError
|
||||
return nil, initialError
|
||||
}
|
||||
}
|
||||
|
||||
setYaegiTags(&interp.context, f.Comments)
|
||||
if inFunc {
|
||||
// return the body of the wrapper main function
|
||||
return f.Decls[0].(*ast.FuncDecl).Body, nil
|
||||
}
|
||||
|
||||
setYaegiTags(&interp.context, f.Comments)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Note: no type analysis is performed at this stage, it is done in pre-order
|
||||
// processing of CFG, in order to accommodate forward type declarations.
|
||||
|
||||
// ast parses src string containing Go code and generates the corresponding AST.
|
||||
// The package name and the AST root node are returned.
|
||||
// The given name is used to set the filename of the relevant source file in the
|
||||
// interpreter's FileSet.
|
||||
func (interp *Interpreter) ast(f ast.Node) (string, *node, error) {
|
||||
var err error
|
||||
var root *node
|
||||
var anc astNode
|
||||
var st nodestack
|
||||
var pkgName string
|
||||
pkgName := "main"
|
||||
|
||||
addChild := func(root **node, anc astNode, pos token.Pos, kind nkind, act action) *node {
|
||||
var i interface{}
|
||||
@@ -898,15 +906,7 @@ func (interp *Interpreter) ast(src, name string, inc bool) (string, *node, error
|
||||
}
|
||||
return true
|
||||
})
|
||||
if inFunc {
|
||||
// Incremental parsing: statements were inserted in a pseudo function.
|
||||
// Set root to function body so its statements are evaluated in global scope.
|
||||
root = root.child[1].child[3]
|
||||
root.anc = nil
|
||||
}
|
||||
if pkgName == "" {
|
||||
return "", root, errors.New("no package name found")
|
||||
}
|
||||
|
||||
interp.roots = append(interp.roots, root)
|
||||
return pkgName, root, err
|
||||
}
|
||||
|
||||
84
interp/compile_test.go
Normal file
84
interp/compile_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package interp
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"testing"
|
||||
|
||||
"github.com/traefik/yaegi/stdlib"
|
||||
)
|
||||
|
||||
func TestCompileAST(t *testing.T) {
|
||||
file, err := parser.ParseFile(token.NewFileSet(), "_.go", `
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Foo struct{}
|
||||
|
||||
var foo Foo
|
||||
const bar = "asdf"
|
||||
|
||||
func main() {
|
||||
fmt.Println(1)
|
||||
}
|
||||
`, 0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(file.Imports) != 1 || len(file.Decls) != 5 {
|
||||
panic("wrong number of imports or decls")
|
||||
}
|
||||
|
||||
dType := file.Decls[1].(*ast.GenDecl)
|
||||
dVar := file.Decls[2].(*ast.GenDecl)
|
||||
dConst := file.Decls[3].(*ast.GenDecl)
|
||||
dFunc := file.Decls[4].(*ast.FuncDecl)
|
||||
|
||||
if dType.Tok != token.TYPE {
|
||||
panic("decl[1] is not a type")
|
||||
}
|
||||
if dVar.Tok != token.VAR {
|
||||
panic("decl[2] is not a var")
|
||||
}
|
||||
if dConst.Tok != token.CONST {
|
||||
panic("decl[3] is not a const")
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
node ast.Node
|
||||
skip string
|
||||
}{
|
||||
{desc: "file", node: file},
|
||||
{desc: "import", node: file.Imports[0]},
|
||||
{desc: "type", node: dType},
|
||||
{desc: "var", node: dVar, skip: "not supported"},
|
||||
{desc: "const", node: dConst},
|
||||
{desc: "func", node: dFunc},
|
||||
{desc: "block", node: dFunc.Body},
|
||||
{desc: "expr", node: dFunc.Body.List[0]},
|
||||
}
|
||||
|
||||
i := New(Options{})
|
||||
_ = i.Use(stdlib.Symbols)
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
if c.skip != "" {
|
||||
t.Skip(c.skip)
|
||||
}
|
||||
|
||||
i := i
|
||||
if _, ok := c.node.(*ast.File); ok {
|
||||
i = New(Options{})
|
||||
_ = i.Use(stdlib.Symbols)
|
||||
}
|
||||
_, err := i.CompileAST(c.node)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to compile %s: %v", c.desc, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -573,7 +573,7 @@ func isFile(filesystem fs.FS, path string) bool {
|
||||
}
|
||||
|
||||
func (interp *Interpreter) eval(src, name string, inc bool) (res reflect.Value, err error) {
|
||||
prog, err := interp.compile(src, name, inc)
|
||||
prog, err := interp.compileSrc(src, name, inc)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package interp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go/ast"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"runtime"
|
||||
@@ -17,7 +18,7 @@ type Program struct {
|
||||
|
||||
// Compile parses and compiles a Go code represented as a string.
|
||||
func (interp *Interpreter) Compile(src string) (*Program, error) {
|
||||
return interp.compile(src, "", true)
|
||||
return interp.compileSrc(src, "", true)
|
||||
}
|
||||
|
||||
// CompilePath parses and compiles a Go code located at the given path.
|
||||
@@ -31,10 +32,10 @@ func (interp *Interpreter) CompilePath(path string) (*Program, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return interp.compile(string(b), path, false)
|
||||
return interp.compileSrc(string(b), path, false)
|
||||
}
|
||||
|
||||
func (interp *Interpreter) compile(src, name string, inc bool) (*Program, error) {
|
||||
func (interp *Interpreter) compileSrc(src, name string, inc bool) (*Program, error) {
|
||||
if name != "" {
|
||||
interp.name = name
|
||||
}
|
||||
@@ -43,7 +44,20 @@ func (interp *Interpreter) compile(src, name string, inc bool) (*Program, error)
|
||||
}
|
||||
|
||||
// Parse source to AST.
|
||||
pkgName, root, err := interp.ast(src, interp.name, inc)
|
||||
n, err := interp.parse(src, interp.name, inc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return interp.CompileAST(n)
|
||||
}
|
||||
|
||||
// CompileAST builds a Program for the given Go code AST. Files and block
|
||||
// statements can be compiled, as can most expressions. Var declaration nodes
|
||||
// cannot be compiled.
|
||||
func (interp *Interpreter) CompileAST(n ast.Node) (*Program, error) {
|
||||
// Convert AST.
|
||||
pkgName, root, err := interp.ast(n)
|
||||
if err != nil || root == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -73,8 +73,16 @@ func (interp *Interpreter) importSrc(rPath, importPath string, skipTest bool) (s
|
||||
return "", err
|
||||
}
|
||||
|
||||
n, err := interp.parse(string(buf), name, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var pname string
|
||||
if pname, root, err = interp.ast(string(buf), name, false); err != nil {
|
||||
if pname, root, err = interp.ast(n); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if root == nil {
|
||||
|
||||
Reference in New Issue
Block a user