Commit 0d947420 authored by Robert Griesemer's avatar Robert Griesemer

exp/types/staging: more flexible API, cleanups

- Changed Check signature to take function parameters for
  more flexibility: Now a client can interrupt type checking
  early (via panic in one the upcalls) once the desired
  type information or number of errors is reached. Default
  use is still simple.

- Cleaned up main typechecking loops. Now does not neglect
  _ declarations anymore.

- Various other cleanups.

R=golang-dev, r, rsc
CC=golang-dev
https://golang.org/cl/6612049
parent 52248750
...@@ -9,16 +9,20 @@ package types ...@@ -9,16 +9,20 @@ package types
import ( import (
"fmt" "fmt"
"go/ast" "go/ast"
"go/scanner"
"go/token" "go/token"
"sort" "sort"
) )
type checker struct { type checker struct {
fset *token.FileSet fset *token.FileSet
pkg *ast.Package pkg *ast.Package
errors scanner.ErrorList errh func(token.Pos, string)
types map[ast.Expr]Type mapf func(ast.Expr, Type)
// lazily initialized
firsterr error
filenames []string // sorted list of package file names for reproducible iteration order
initexprs map[*ast.ValueSpec][]ast.Expr // "inherited" initialization expressions for constant declarations
} }
// declare declares an object of the given kind and name (ident) in scope; // declare declares an object of the given kind and name (ident) in scope;
...@@ -47,7 +51,7 @@ func (check *checker) declare(scope *ast.Scope, kind ast.ObjKind, ident *ast.Ide ...@@ -47,7 +51,7 @@ func (check *checker) declare(scope *ast.Scope, kind ast.ObjKind, ident *ast.Ide
} }
} }
func (check *checker) decl(pos token.Pos, obj *ast.Object, lhs []*ast.Ident, typ ast.Expr, rhs []ast.Expr, iota int) { func (check *checker) valueSpec(pos token.Pos, obj *ast.Object, lhs []*ast.Ident, typ ast.Expr, rhs []ast.Expr, iota int) {
if len(lhs) == 0 { if len(lhs) == 0 {
check.invalidAST(pos, "missing lhs in declaration") check.invalidAST(pos, "missing lhs in declaration")
return return
...@@ -96,42 +100,12 @@ func (check *checker) decl(pos token.Pos, obj *ast.Object, lhs []*ast.Ident, typ ...@@ -96,42 +100,12 @@ func (check *checker) decl(pos token.Pos, obj *ast.Object, lhs []*ast.Ident, typ
} }
} }
// specValues returns the list of initialization expressions // ident type checks an identifier.
// for the given part (spec) of a constant declaration. func (check *checker) ident(name *ast.Ident, cycleOk bool) {
// TODO(gri) Make this more efficient by caching results obj := name.Obj
// (using a map in checker). if obj == nil {
func (check *checker) specValues(spec *ast.ValueSpec) []ast.Expr { check.invalidAST(name.Pos(), "missing object for %s", name.Name)
if len(spec.Values) > 0 { return
return spec.Values
}
// find the corresponding values
for _, file := range check.pkg.Files {
for _, d := range file.Decls {
if d, ok := d.(*ast.GenDecl); ok && d.Tok == token.CONST {
var values []ast.Expr
for _, s := range d.Specs {
if s, ok := s.(*ast.ValueSpec); ok {
if len(s.Values) > 0 {
values = s.Values
}
if s == spec {
return values
}
}
}
}
}
}
check.invalidAST(spec.Pos(), "no initialization values provided")
return nil
}
// obj type checks an object.
func (check *checker) obj(obj *ast.Object, cycleOk bool) {
if trace {
fmt.Printf("obj(%s)\n", obj.Name)
} }
if obj.Type != nil { if obj.Type != nil {
...@@ -143,29 +117,28 @@ func (check *checker) obj(obj *ast.Object, cycleOk bool) { ...@@ -143,29 +117,28 @@ func (check *checker) obj(obj *ast.Object, cycleOk bool) {
case ast.Bad, ast.Pkg: case ast.Bad, ast.Pkg:
// nothing to do // nothing to do
case ast.Con: case ast.Con, ast.Var:
// The obj.Data field for constants and variables is initialized
// to the respective (hypothetical, for variables) iota value by
// the parser. The object's fields can be in one of the following
// states:
// Type != nil => the constant value is Data
// Type == nil => the object is not typechecked yet, and Data can be:
// Data is int => Data is the value of iota for this declaration
// Data == nil => the object's expression is being evaluated
if obj.Data == nil { if obj.Data == nil {
check.errorf(obj.Pos(), "illegal cycle in initialization of %s", obj.Name) check.errorf(obj.Pos(), "illegal cycle in initialization of %s", obj.Name)
return return
} }
spec, ok := obj.Decl.(*ast.ValueSpec) spec := obj.Decl.(*ast.ValueSpec)
assert(ok)
// The Data stored with the constant is the value of iota for that
// ast.ValueSpec. Use it for the evaluation of the initialization
// expressions.
iota := obj.Data.(int) iota := obj.Data.(int)
obj.Data = nil obj.Data = nil
check.decl(spec.Pos(), obj, spec.Names, spec.Type, check.specValues(spec), iota) // determine initialization expressions
values := spec.Values
case ast.Var: if len(values) == 0 && obj.Kind == ast.Con {
// TODO(gri) missing cycle detection values = check.initexprs[spec]
spec, ok := obj.Decl.(*ast.ValueSpec)
if !ok {
// TODO(gri) the assertion fails for "x, y := 1, 2, 3" it seems
fmt.Printf("var = %s\n", obj.Name)
} }
assert(ok) check.valueSpec(spec.Pos(), obj, spec.Names, spec.Type, values, iota)
check.decl(spec.Pos(), obj, spec.Names, spec.Type, spec.Values, 0)
case ast.Typ: case ast.Typ:
typ := &NamedType{Obj: obj} typ := &NamedType{Obj: obj}
...@@ -215,108 +188,165 @@ func (check *checker) obj(obj *ast.Object, cycleOk bool) { ...@@ -215,108 +188,165 @@ func (check *checker) obj(obj *ast.Object, cycleOk bool) {
} }
} }
func check(fset *token.FileSet, pkg *ast.Package, types map[ast.Expr]Type) error { // assocInitvals associates "inherited" initialization expressions
var check checker // with the corresponding *ast.ValueSpec in the check.initexprs map
check.fset = fset // for constant declarations without explicit initialization expressions.
check.pkg = pkg //
check.types = types func (check *checker) assocInitvals(decl *ast.GenDecl) {
var values []ast.Expr
for _, s := range decl.Specs {
if s, ok := s.(*ast.ValueSpec); ok {
if len(s.Values) > 0 {
values = s.Values
} else {
check.initexprs[s] = values
}
}
}
if len(values) == 0 {
check.invalidAST(decl.Pos(), "no initialization values provided")
}
}
// assocMethod associates a method declaration with the respective
// receiver base type. meth.Recv must exist.
//
func (check *checker) assocMethod(meth *ast.FuncDecl) {
// The receiver type is one of the following (enforced by parser):
// - *ast.Ident
// - *ast.StarExpr{*ast.Ident}
// - *ast.BadExpr (parser error)
typ := meth.Recv.List[0].Type
if ptr, ok := typ.(*ast.StarExpr); ok {
typ = ptr.X
}
// determine receiver base type object (or nil if error)
var obj *ast.Object
if ident, ok := typ.(*ast.Ident); ok && ident.Obj != nil {
obj = ident.Obj
if obj.Kind != ast.Typ {
check.errorf(ident.Pos(), "%s is not a type", ident.Name)
obj = nil
}
// TODO(gri) determine if obj was defined in this package
/*
if check.notLocal(obj) {
check.errorf(ident.Pos(), "cannot define methods on non-local type %s", ident.Name)
obj = nil
}
*/
} else {
// If it's not an identifier or the identifier wasn't declared/resolved,
// the parser/resolver already reported an error. Nothing to do here.
}
// determine base type scope (or nil if error)
var scope *ast.Scope
if obj != nil {
if obj.Data != nil {
scope = obj.Data.(*ast.Scope)
} else {
scope = ast.NewScope(nil)
obj.Data = scope
}
} else {
// use a dummy scope so that meth can be declared in
// presence of an error and get an associated object
// (always use a new scope so that we don't get double
// declaration errors)
scope = ast.NewScope(nil)
}
check.declare(scope, ast.Fun, meth.Name, meth)
}
// Compute sorted list of file names so that func (check *checker) assocInitvalsOrMethod(decl ast.Decl) {
// package file iterations are reproducible (needed for testing). switch d := decl.(type) {
filenames := make([]string, len(pkg.Files)) case *ast.GenDecl:
{ if d.Tok == token.CONST {
i := 0 check.assocInitvals(d)
for filename := range pkg.Files { }
filenames[i] = filename case *ast.FuncDecl:
i++ if d.Recv != nil {
check.assocMethod(d)
} }
sort.Strings(filenames)
} }
}
// Associate methods with types func (check *checker) decl(decl ast.Decl) {
// TODO(gri) All other objects are resolved by the parser. switch d := decl.(type) {
// Consider doing this in the parser (and provide the info case *ast.BadDecl:
// in the AST. In the long-term (might require Go 1 API // ignore
// changes) it's probably easier to do all the resolution case *ast.GenDecl:
// in one place in the type checker. See also comment for _, spec := range d.Specs {
// with checker.declare. switch s := spec.(type) {
for _, filename := range filenames { case *ast.ImportSpec:
file := pkg.Files[filename] // nothing to do (handled by ast.NewPackage)
for _, decl := range file.Decls { case *ast.ValueSpec:
if meth, ok := decl.(*ast.FuncDecl); ok && meth.Recv != nil { for _, name := range s.Names {
// The receiver type is one of the following (enforced by parser): if name.Name == "_" {
// - *ast.Ident // TODO(gri) why is _ special here?
// - *ast.StarExpr{*ast.Ident}
// - *ast.BadExpr (parser error)
typ := meth.Recv.List[0].Type
if ptr, ok := typ.(*ast.StarExpr); ok {
typ = ptr.X
}
// determine receiver base type object (or nil if error)
var obj *ast.Object
if ident, ok := typ.(*ast.Ident); ok && ident.Obj != nil {
obj = ident.Obj
if obj.Kind != ast.Typ {
check.errorf(ident.Pos(), "%s is not a type", ident.Name)
obj = nil
}
// TODO(gri) determine if obj was defined in this package
/*
if check.notLocal(obj) {
check.errorf(ident.Pos(), "cannot define methods on non-local type %s", ident.Name)
obj = nil
}
*/
} else {
// If it's not an identifier or the identifier wasn't declared/resolved,
// the parser/resolver already reported an error. Nothing to do here.
}
// determine base type scope (or nil if error)
var scope *ast.Scope
if obj != nil {
if obj.Data != nil {
scope = obj.Data.(*ast.Scope)
} else { } else {
scope = ast.NewScope(nil) check.ident(name, false)
obj.Data = scope
} }
} else {
// use a dummy scope so that meth can be declared in
// presence of an error and get an associated object
// (always use a new scope so that we don't get double
// declaration errors)
scope = ast.NewScope(nil)
} }
check.declare(scope, ast.Fun, meth.Name, meth) case *ast.TypeSpec:
check.ident(s.Name, false)
default:
check.invalidAST(s.Pos(), "unknown ast.Spec node %T", s)
} }
} }
case *ast.FuncDecl:
check.ident(d.Name, false)
default:
check.invalidAST(d.Pos(), "unknown ast.Decl node %T", d)
} }
}
// iterate calls f for each package-level declaration.
func (check *checker) iterate(f func(*checker, ast.Decl)) {
list := check.filenames
// Sort objects so that we get reproducible error if list == nil {
// positions (this is only needed for testing). // initialize lazily
// TODO(gri): Consider ast.Scope implementation that for filename := range check.pkg.Files {
// provides both a list and a map for fast lookup. list = append(list, filename)
// Would permit the use of scopes instead of ObjMaps
// elsewhere.
list := make(ObjList, len(pkg.Scope.Objects))
{
i := 0
for _, obj := range pkg.Scope.Objects {
list[i] = obj
i++
} }
list.Sort() sort.Strings(list)
check.filenames = list
} }
// Check global objects. for _, filename := range list {
for _, obj := range list { for _, decl := range check.pkg.Files[filename].Decls {
check.obj(obj, false) f(check, decl)
}
} }
}
// A bailout panic is raised to indicate early termination.
type bailout struct{}
func check(fset *token.FileSet, pkg *ast.Package, errh func(token.Pos, string), f func(ast.Expr, Type)) (err error) {
// initialize checker
var check checker
check.fset = fset
check.pkg = pkg
check.errh = errh
check.mapf = f
check.initexprs = make(map[*ast.ValueSpec][]ast.Expr)
// handle bailouts
defer func() {
if p := recover(); p != nil {
_ = p.(bailout) // re-panic if not a bailout
}
err = check.firsterr
}()
// determine missing constant initialization expressions
// and associate methods with types
check.iterate((*checker).assocInitvalsOrMethod)
// TODO(gri) Missing pieces: // typecheck all declarations
// - blank (_) objects and init functions are not in scopes but should be type-checked check.iterate((*checker).decl)
// do not remove multiple errors per line - depending on return
// order or error reporting this may hide the real error
return check.errors.Err()
} }
...@@ -17,6 +17,7 @@ import ( ...@@ -17,6 +17,7 @@ import (
const debug = false const debug = false
const trace = false const trace = false
// TODO(gri) eventually assert and unimplemented should disappear.
func assert(p bool) { func assert(p bool) {
if !p { if !p {
panic("assertion failed") panic("assertion failed")
...@@ -33,19 +34,36 @@ func unreachable() { ...@@ -33,19 +34,36 @@ func unreachable() {
panic("unreachable") panic("unreachable")
} }
func (check *checker) formatMsg(format string, args []interface{}) string {
for i, arg := range args {
switch a := arg.(type) {
case token.Pos:
args[i] = check.fset.Position(a)
case ast.Expr:
args[i] = exprString(a)
case Type:
args[i] = typeString(a)
case operand:
panic("internal error: should always pass *operand")
}
}
return fmt.Sprintf(format, args...)
}
// dump is only needed for debugging // dump is only needed for debugging
func (check *checker) dump(format string, args ...interface{}) { func (check *checker) dump(format string, args ...interface{}) {
if n := len(format); n > 0 && format[n-1] != '\n' { fmt.Println(check.formatMsg(format, args))
format += "\n"
}
check.convertArgs(args)
fmt.Printf(format, args...)
} }
func (check *checker) errorf(pos token.Pos, format string, args ...interface{}) { func (check *checker) errorf(pos token.Pos, format string, args ...interface{}) {
check.convertArgs(args) msg := check.formatMsg(format, args)
msg := fmt.Sprintf(format, args...) if check.firsterr == nil {
check.errors.Add(check.fset.Position(pos), msg) check.firsterr = fmt.Errorf("%s: %s", check.fset.Position(pos), msg)
}
if check.errh == nil {
panic(bailout{}) // report only first error
}
check.errh(pos, msg)
} }
func (check *checker) invalidAST(pos token.Pos, format string, args ...interface{}) { func (check *checker) invalidAST(pos token.Pos, format string, args ...interface{}) {
...@@ -60,21 +78,6 @@ func (check *checker) invalidOp(pos token.Pos, format string, args ...interface{ ...@@ -60,21 +78,6 @@ func (check *checker) invalidOp(pos token.Pos, format string, args ...interface{
check.errorf(pos, "invalid operation: "+format, args...) check.errorf(pos, "invalid operation: "+format, args...)
} }
func (check *checker) convertArgs(args []interface{}) {
for i, arg := range args {
switch a := arg.(type) {
case token.Pos:
args[i] = check.fset.Position(a)
case ast.Expr:
args[i] = exprString(a)
case Type:
args[i] = typeString(a)
case operand:
panic("internal error: should always pass *operand")
}
}
}
// exprString returns a (simplified) string representation for an expression. // exprString returns a (simplified) string representation for an expression.
func exprString(expr ast.Expr) string { func exprString(expr ast.Expr) string {
var buf bytes.Buffer var buf bytes.Buffer
......
...@@ -40,7 +40,8 @@ func FindPkg(path, srcDir string) (filename, id string) { ...@@ -40,7 +40,8 @@ func FindPkg(path, srcDir string) (filename, id string) {
switch { switch {
default: default:
// "x" -> "$GOPATH/pkg/$GOOS_$GOARCH/x.ext", "x" // "x" -> "$GOPATH/pkg/$GOOS_$GOARCH/x.ext", "x"
bp, _ := build.Import(path, srcDir, build.FindOnly) // Don't require the source files to be present.
bp, _ := build.Import(path, srcDir, build.FindOnly|build.AllowBinary)
if bp.PkgObj == "" { if bp.PkgObj == "" {
return return
} }
......
...@@ -41,10 +41,9 @@ func compile(t *testing.T, dirname, filename string) string { ...@@ -41,10 +41,9 @@ func compile(t *testing.T, dirname, filename string) string {
cmd.Dir = dirname cmd.Dir = dirname
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
t.Logf("%s", out)
t.Fatalf("%s %s failed: %s", gcPath, filename, err) t.Fatalf("%s %s failed: %s", gcPath, filename, err)
return ""
} }
t.Logf("%s", string(out))
archCh, _ := build.ArchChar(runtime.GOARCH) archCh, _ := build.ArchChar(runtime.GOARCH)
// filename should end with ".go" // filename should end with ".go"
return filepath.Join(dirname, filename[:len(filename)-2]+archCh) return filepath.Join(dirname, filename[:len(filename)-2]+archCh)
......
...@@ -15,21 +15,16 @@ import ( ...@@ -15,21 +15,16 @@ import (
"sort" "sort"
) )
// Check typechecks the given package pkg and augments the AST by // Check typechecks a package pkg. It returns the first error, or nil.
// assigning types to all ast.Objects. Check can be used in two
// different modes:
// //
// 1) If a nil types map is provided, Check typechecks the entire // Check augments the AST by assigning types to ast.Objects. It
// package. If no error is returned, the package source code has // calls err with the error position and message for each error.
// no type errors. // It calls f with each valid AST expression and corresponding
// type. If err == nil, Check terminates as soon as the first error
// is found. If f is nil, it is not invoked.
// //
// 2) If a non-nil types map is provided, Check operates like in func Check(fset *token.FileSet, pkg *ast.Package, err func(token.Pos, string), f func(ast.Expr, Type)) error {
// mode 1) but also records the types for all expressions in the return check(fset, pkg, err, f)
// map. Pre-existing expression types in the map are replaced if
// the expression appears in the AST.
//
func Check(fset *token.FileSet, pkg *ast.Package, types map[ast.Expr]Type) error {
return check(fset, pkg, types)
} }
// All types implement the Type interface. // All types implement the Type interface.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment