The recent changes that added some redeclaration checks implicitly added more strictness related to namespaces and scopes which, among other things, broke some uses that "accidentally" used to work. For example, given const script1 = ` import "fmt" // more code ` const script2 = ` import "fmt" // some other code ` If one Evals script1, then script2, with the same interpreter, without specifying any scope, as the two fragments would be considered part of the same (.go file) scope by default, a redeclaration error would be triggered because import "fmt" is seen twice. A work-around would have been to specify (a different) i.Name before each Eval call, so that each script is considered as coming from a different .go file, and hence are respectively in different scopes with respect to imports. That lead us to realize we had to make specifying things such as file-scope, and "incremental mode" (aka REPL), more obvious in the context of an Eval call. In addition, we want to lay down the foundations for Yaegi being able to behave more like the go tool wrt to various inputs, i.e. it should be able to take a package directory, or an import path, as input, instead of just a .go file. Hence the introduction of a new kind of Eval method (whose signature is not fixed yet): func (interp *Interpreter) EvalPath(path string) (res reflect.Value, err error) It partially solves the problem described above because: 1. the path given to EvalPath can be used as the file-scope hint mentioned above, for now (even though the related implementation details might change). 2. Eval always runs in incremental mode, whereas EvalPath always runs in non-incremental mode, hence clarifying the situation in that respect. And to avoid confusion, the Name field of Interpreter is now non-exported, since it is somewhat redundant with the path argument of EvalPath. Note that #731 is not fully fixed (and might never be), as a requirement of the proposed solution is to move the input bits of code into respective files (instead of leaving them as strings). Finally, some related bugfixes, documention changes, and some refactoring have been included. Notably, there is no "empty scope" anymore, i.e. name defaults to "_.go" when it is not specified. Updates #731 Fixes #778 Fixes #798 Fixes #789 Co-authored-by: Marc Vertes <mvertes@free.fr>
358 lines
10 KiB
Go
358 lines
10 KiB
Go
package interp
|
|
|
|
import (
|
|
"path/filepath"
|
|
"reflect"
|
|
)
|
|
|
|
// gta performs a global types analysis on the AST, registering types,
|
|
// variables and functions symbols at package level, prior to CFG.
|
|
// All function bodies are skipped. GTA is necessary to handle out of
|
|
// order declarations and multiple source files packages.
|
|
// rpath is the relative path to the directory containing the source for the package.
|
|
func (interp *Interpreter) gta(root *node, rpath, importPath string) ([]*node, error) {
|
|
sc := interp.initScopePkg(importPath)
|
|
var err error
|
|
var revisit []*node
|
|
|
|
baseName := filepath.Base(interp.fset.Position(root.pos).Filename)
|
|
|
|
root.Walk(func(n *node) bool {
|
|
if err != nil {
|
|
return false
|
|
}
|
|
switch n.kind {
|
|
case constDecl:
|
|
// Early parse of constDecl subtree, to compute all constant
|
|
// values which may be used in further declarations.
|
|
if _, err = interp.cfg(n, importPath); err != nil {
|
|
// No error processing here, to allow recovery in subtree nodes.
|
|
err = nil
|
|
}
|
|
|
|
case blockStmt:
|
|
if n != root {
|
|
return false // skip statement block if not the entry point
|
|
}
|
|
|
|
case defineStmt:
|
|
var atyp *itype
|
|
if n.nleft+n.nright < len(n.child) {
|
|
// Type is declared explicitly in the assign expression.
|
|
if atyp, err = nodeType(interp, sc, n.child[n.nleft]); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
|
|
var sbase int
|
|
if n.nright > 0 {
|
|
sbase = len(n.child) - n.nright
|
|
}
|
|
|
|
for i := 0; i < n.nleft; i++ {
|
|
dest, src := n.child[i], n.child[sbase+i]
|
|
val := reflect.ValueOf(sc.iota)
|
|
if n.anc.kind == constDecl {
|
|
if _, err2 := interp.cfg(n, importPath); err2 != nil {
|
|
// Constant value can not be computed yet.
|
|
// Come back when child dependencies are known.
|
|
revisit = append(revisit, n)
|
|
return false
|
|
}
|
|
}
|
|
typ := atyp
|
|
if typ == nil {
|
|
if typ, err = nodeType(interp, sc, src); err != nil {
|
|
return false
|
|
}
|
|
val = src.rval
|
|
}
|
|
if !typ.isComplete() {
|
|
// Come back when type is known.
|
|
revisit = append(revisit, n)
|
|
return false
|
|
}
|
|
if typ.cat == nilT {
|
|
err = n.cfgErrorf("use of untyped nil")
|
|
return false
|
|
}
|
|
if typ.isBinMethod {
|
|
typ = &itype{cat: valueT, rtype: typ.methodCallType(), isBinMethod: true, scope: sc}
|
|
}
|
|
if sc.sym[dest.ident] == nil || sc.sym[dest.ident].typ.incomplete {
|
|
sc.sym[dest.ident] = &symbol{kind: varSym, global: true, index: sc.add(typ), typ: typ, rval: val, node: n}
|
|
}
|
|
if n.anc.kind == constDecl {
|
|
sc.sym[dest.ident].kind = constSym
|
|
if childPos(n) == len(n.anc.child)-1 {
|
|
sc.iota = 0
|
|
} else {
|
|
sc.iota++
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
|
|
case defineXStmt:
|
|
err = compDefineX(sc, n)
|
|
|
|
case valueSpec:
|
|
l := len(n.child) - 1
|
|
if n.typ = n.child[l].typ; n.typ == nil {
|
|
if n.typ, err = nodeType(interp, sc, n.child[l]); err != nil {
|
|
return false
|
|
}
|
|
if !n.typ.isComplete() {
|
|
// Come back when type is known.
|
|
revisit = append(revisit, n)
|
|
return false
|
|
}
|
|
}
|
|
for _, c := range n.child[:l] {
|
|
asImportName := filepath.Join(c.ident, baseName)
|
|
sym1, exists1 := sc.sym[asImportName]
|
|
sym2, exists2 := sc.sym[c.ident]
|
|
if !exists1 && !exists2 {
|
|
sc.sym[c.ident] = &symbol{index: sc.add(n.typ), kind: varSym, global: true, typ: n.typ, node: n}
|
|
continue
|
|
}
|
|
|
|
var sym *symbol
|
|
if exists1 {
|
|
// prev declaration is an import statement
|
|
sym = sym1
|
|
} else {
|
|
// prev declaration is whatever else (var, type, etc)
|
|
sym = sym2
|
|
}
|
|
// redeclaration error
|
|
if sym.typ.node != nil && sym.typ.node.anc != nil {
|
|
prevDecl := n.interp.fset.Position(sym.typ.node.anc.pos)
|
|
err = n.cfgErrorf("%s redeclared in this block\n\tprevious declaration at %v", c.ident, prevDecl)
|
|
return false
|
|
}
|
|
err = n.cfgErrorf("%s redeclared in this block", c.ident)
|
|
return false
|
|
}
|
|
|
|
case funcDecl:
|
|
if n.typ, err = nodeType(interp, sc, n.child[2]); err != nil {
|
|
return false
|
|
}
|
|
ident := n.child[1].ident
|
|
switch {
|
|
case isMethod(n):
|
|
// TODO(mpl): redeclaration detection
|
|
// Add a method symbol in the receiver type name space
|
|
var rcvrtype *itype
|
|
n.ident = ident
|
|
rcvr := n.child[0].child[0]
|
|
rtn := rcvr.lastChild()
|
|
typeName := rtn.ident
|
|
if typeName == "" {
|
|
// The receiver is a pointer, retrieve typeName from indirection
|
|
typeName = rtn.child[0].ident
|
|
elementType := sc.getType(typeName)
|
|
if elementType == nil {
|
|
// Add type if necessary, so method can be registered
|
|
sc.sym[typeName] = &symbol{kind: typeSym, typ: &itype{name: typeName, path: rpath, incomplete: true, node: rtn.child[0], scope: sc}}
|
|
elementType = sc.sym[typeName].typ
|
|
}
|
|
rcvrtype = &itype{cat: ptrT, val: elementType, incomplete: elementType.incomplete, node: rtn, scope: sc}
|
|
elementType.method = append(elementType.method, n)
|
|
} else {
|
|
rcvrtype = sc.getType(typeName)
|
|
if rcvrtype == nil {
|
|
// Add type if necessary, so method can be registered
|
|
sc.sym[typeName] = &symbol{kind: typeSym, typ: &itype{name: typeName, path: rpath, incomplete: true, node: rtn, scope: sc}}
|
|
rcvrtype = sc.sym[typeName].typ
|
|
}
|
|
}
|
|
rcvrtype.method = append(rcvrtype.method, n)
|
|
n.child[0].child[0].lastChild().typ = rcvrtype
|
|
case ident == "init":
|
|
// init functions do not get declared as per the Go spec.
|
|
default:
|
|
asImportName := filepath.Join(ident, baseName)
|
|
if _, exists := sc.sym[asImportName]; exists {
|
|
// redeclaration error
|
|
err = n.cfgErrorf("%s redeclared in this block", ident)
|
|
return false
|
|
}
|
|
sym, exists := sc.sym[ident]
|
|
if exists {
|
|
// Make sure the symbol we found seems to be about another node, before calling
|
|
// it a redeclaration.
|
|
if sym.typ.isComplete() {
|
|
// TODO(mpl): this check might be too permissive?
|
|
if sym.kind != funcSym || sym.typ.cat != n.typ.cat || sym.node != n || sym.index != -1 {
|
|
// redeclaration error
|
|
err = n.cfgErrorf("%s redeclared in this block", ident)
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
// Add a function symbol in the package name space except for init
|
|
sc.sym[n.child[1].ident] = &symbol{kind: funcSym, typ: n.typ, node: n, index: -1}
|
|
}
|
|
if !n.typ.isComplete() {
|
|
revisit = append(revisit, n)
|
|
}
|
|
return false
|
|
|
|
case importSpec:
|
|
var name, ipath string
|
|
if len(n.child) == 2 {
|
|
ipath = constToString(n.child[1].rval)
|
|
name = n.child[0].ident
|
|
} else {
|
|
ipath = constToString(n.child[0].rval)
|
|
}
|
|
// Try to import a binary package first, or a source package
|
|
var pkgName string
|
|
if interp.binPkg[ipath] != nil {
|
|
switch name {
|
|
case "_": // no import of symbols
|
|
case ".": // import symbols in current scope
|
|
for n, v := range interp.binPkg[ipath] {
|
|
typ := v.Type()
|
|
if isBinType(v) {
|
|
typ = typ.Elem()
|
|
}
|
|
sc.sym[n] = &symbol{kind: binSym, typ: &itype{cat: valueT, rtype: typ, scope: sc}, rval: v}
|
|
}
|
|
default: // import symbols in package namespace
|
|
if name == "" {
|
|
name = identifier.FindString(ipath)
|
|
}
|
|
// imports of a same package are all mapped in the same scope, so we cannot just
|
|
// map them by their names, otherwise we could have collisions from same-name
|
|
// imports in different source files of the same package. Therefore, we suffix
|
|
// the key with the basename of the source file.
|
|
name = filepath.Join(name, baseName)
|
|
if _, exists := sc.sym[name]; !exists {
|
|
sc.sym[name] = &symbol{kind: pkgSym, typ: &itype{cat: binPkgT, path: ipath, scope: sc}}
|
|
break
|
|
}
|
|
|
|
// redeclaration error
|
|
err = n.cfgErrorf("%s redeclared in this block", name)
|
|
return false
|
|
}
|
|
} else if pkgName, err = interp.importSrc(rpath, ipath); err == nil {
|
|
sc.types = interp.universe.types
|
|
switch name {
|
|
case "_": // no import of symbols
|
|
case ".": // import symbols in current namespace
|
|
for k, v := range interp.srcPkg[ipath] {
|
|
if canExport(k) {
|
|
sc.sym[k] = v
|
|
}
|
|
}
|
|
default: // import symbols in package namespace
|
|
if name == "" {
|
|
name = pkgName
|
|
}
|
|
name = filepath.Join(name, baseName)
|
|
if _, exists := sc.sym[name]; !exists {
|
|
sc.sym[name] = &symbol{kind: pkgSym, typ: &itype{cat: srcPkgT, path: ipath, scope: sc}}
|
|
break
|
|
}
|
|
|
|
// redeclaration error
|
|
err = n.cfgErrorf("%s redeclared as imported package name", name)
|
|
return false
|
|
}
|
|
} else {
|
|
err = n.cfgErrorf("import %q error: %v", ipath, err)
|
|
}
|
|
|
|
case typeSpec:
|
|
typeName := n.child[0].ident
|
|
var typ *itype
|
|
if typ, err = nodeType(interp, sc, n.child[1]); err != nil {
|
|
return false
|
|
}
|
|
|
|
if n.child[1].kind == identExpr {
|
|
n.typ = &itype{cat: aliasT, val: typ, name: typeName, path: rpath, field: typ.field, incomplete: typ.incomplete, scope: sc, node: n.child[0]}
|
|
copy(n.typ.method, typ.method)
|
|
} else {
|
|
n.typ = typ
|
|
n.typ.name = typeName
|
|
n.typ.path = rpath
|
|
}
|
|
|
|
asImportName := filepath.Join(typeName, baseName)
|
|
if _, exists := sc.sym[asImportName]; exists {
|
|
// redeclaration error
|
|
err = n.cfgErrorf("%s redeclared in this block", typeName)
|
|
return false
|
|
}
|
|
sym, exists := sc.sym[typeName]
|
|
if !exists {
|
|
sc.sym[typeName] = &symbol{kind: typeSym}
|
|
} else {
|
|
if sym.typ != nil && (len(sym.typ.method) > 0) {
|
|
// Type has already been seen as a receiver in a method function
|
|
n.typ.method = append(n.typ.method, sym.typ.method...)
|
|
} else {
|
|
// TODO(mpl): figure out how to detect redeclarations without breaking type aliases.
|
|
// Allow redeclarations for now.
|
|
sc.sym[typeName] = &symbol{kind: typeSym}
|
|
}
|
|
}
|
|
sc.sym[typeName].typ = n.typ
|
|
if !n.typ.isComplete() {
|
|
revisit = append(revisit, n)
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}, nil)
|
|
|
|
if sc != interp.universe {
|
|
sc.pop()
|
|
}
|
|
return revisit, err
|
|
}
|
|
|
|
// gtaRetry (re)applies gta until all global constants and types are defined.
|
|
func (interp *Interpreter) gtaRetry(nodes []*node, importPath string) error {
|
|
revisit := []*node{}
|
|
for {
|
|
for _, n := range nodes {
|
|
list, err := interp.gta(n, importPath, importPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
revisit = append(revisit, list...)
|
|
}
|
|
|
|
if len(revisit) == 0 || equalNodes(nodes, revisit) {
|
|
break
|
|
}
|
|
|
|
nodes = revisit
|
|
revisit = []*node{}
|
|
}
|
|
|
|
if len(revisit) > 0 {
|
|
return revisit[0].cfgErrorf("constant definition loop")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// equalNodes returns true if two slices of nodes are identical.
|
|
func equalNodes(a, b []*node) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i, n := range a {
|
|
if n != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|