blob: 6cb7a7460cb1b62dd94ce4e1abfed3430ef6d0a4 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package compile
import (
"fmt"
"path"
"regexp"
"strings"
"unicode"
"v.io/v23/vdl"
"v.io/v23/vdlroot/vdltool"
"v.io/x/ref/lib/vdl/opconst"
"v.io/x/ref/lib/vdl/parse"
"v.io/x/ref/lib/vdl/vdlutil"
)
// Env is the environment for compilation. It contains all errors that were
// collected during the execution - you can pass Errors to the parse phase to
// collect all errors together. As packages are compiled it also collects the
// output; after a sequence of dependent packages is compiled, all compiled
// output will be collected.
//
// Always create a new Env via NewEnv; the zero Env is invalid.
type Env struct {
Errors *vdlutil.Errors
pkgs map[string]*Package
typeMap map[*vdl.Type]*TypeDef
constMap map[*vdl.Value]*ConstDef
disallowPathQualifiers bool // Disallow syntax like "a/b/c".Type
}
// NewEnv creates a new Env, allowing up to maxErrors errors before we stop.
func NewEnv(maxErrors int) *Env {
return NewEnvWithErrors(vdlutil.NewErrors(maxErrors))
}
// NewEnvWithErrors creates a new Env, using the given errs to collect errors.
func NewEnvWithErrors(errs *vdlutil.Errors) *Env {
env := &Env{
Errors: errs,
pkgs: make(map[string]*Package),
typeMap: make(map[*vdl.Type]*TypeDef),
constMap: make(map[*vdl.Value]*ConstDef),
}
// The env always starts out with the built-in package.
env.pkgs[BuiltInPackage.Name] = BuiltInPackage
for _, def := range BuiltInFile.TypeDefs {
env.typeMap[def.Type] = def
}
for _, def := range BuiltInFile.ConstDefs {
env.constMap[def.Value] = def
}
return env
}
// FindTypeDef returns the type definition corresponding to t, or nil if t isn't
// a defined type. All built-in and user-defined named types are considered
// defined; e.g. unnamed lists don't have a corresponding type def.
func (e *Env) FindTypeDef(t *vdl.Type) *TypeDef { return e.typeMap[t] }
// FindConstDef returns the const definition corresponding to v, or nil if v
// isn't a defined const. All user-defined named consts are considered defined;
// e.g. method tags don't have a corresponding const def.
func (e *Env) FindConstDef(v *vdl.Value) *ConstDef { return e.constMap[v] }
// ResolvePackage resolves a package path to its previous compiled results.
func (e *Env) ResolvePackage(path string) *Package {
return e.pkgs[path]
}
// ResolvePackageGenPath resolves a package gen path to its previous compiled
// results.
func (e *Env) ResolvePackageGenPath(genPath string) *Package {
for _, pkg := range e.pkgs {
if pkg.GenPath == genPath {
return pkg
}
}
return nil
}
// Resolves a name against the current package and imported package namespace.
func (e *Env) resolve(name string, file *File) (val interface{}, matched string) {
// First handle package-path qualified identifiers, which look like this:
// "a/b/c".Ident (qualified with package path "a/b/c")
// These must be handled first, since the package-path may include dots.
if strings.HasPrefix(name, `"`) {
if parts := strings.SplitN(name[1:], `".`, 2); len(parts) == 2 {
path, remain := parts[0], parts[1]
if e.disallowPathQualifiers {
// TODO(toddw): Add real position.
e.Errorf(file, parse.Pos{}, "package path qualified identifier %s not allowed", name)
}
if file.ValidateImportPackagePath(path) {
if pkg := e.ResolvePackage(path); pkg != nil {
if dotParts := strings.Split(remain, "."); len(dotParts) > 0 {
if val := pkg.resolve(dotParts[0], false); val != nil {
return val, `"` + path + `".` + dotParts[0]
}
}
}
}
}
}
// Now handle built-in and package-local identifiers. Examples:
// string
// TypeName
// EnumType.Label
// ConstName
// StructConst.Field
// InterfaceName
nameParts := strings.Split(name, ".")
if len(nameParts) == 0 {
return nil, ""
}
if builtin := BuiltInPackage.resolve(nameParts[0], false); builtin != nil {
return builtin, nameParts[0]
}
if local := file.Package.resolve(nameParts[0], true); local != nil {
return local, nameParts[0]
}
// Now handle package qualified identifiers, which look like this:
// pkg.Ident (qualified with local package identifier pkg)
if len(nameParts) > 1 {
if path := file.LookupImportPath(nameParts[0]); path != "" {
if pkg := e.ResolvePackage(path); pkg != nil {
if val := pkg.resolve(nameParts[1], false); val != nil {
return val, nameParts[0] + "." + nameParts[1]
}
}
}
}
// No match found.
return nil, ""
}
// ResolveType resolves a name to a type definition.
// Returns the type def and the portion of name that was matched.
func (e *Env) ResolveType(name string, file *File) (td *TypeDef, matched string) {
v, matched := e.resolve(name, file)
td, _ = v.(*TypeDef)
if td == nil {
return nil, ""
}
return td, matched
}
// ResolveConst resolves a name to a const definition.
// Returns the const def and the portion of name that was matched.
func (e *Env) ResolveConst(name string, file *File) (cd *ConstDef, matched string) {
v, matched := e.resolve(name, file)
cd, _ = v.(*ConstDef)
if cd == nil {
return nil, ""
}
return cd, matched
}
// ResolveInterface resolves a name to an interface definition.
// Returns the interface and the portion of name that was matched.
func (e *Env) ResolveInterface(name string, file *File) (i *Interface, matched string) {
v, matched := e.resolve(name, file)
i, _ = v.(*Interface)
if i == nil {
return nil, ""
}
return i, matched
}
// evalSelectorOnValue evaluates the selector on v.
func (e *Env) evalSelectorOnValue(v *vdl.Value, selector string) (opconst.Const, error) {
for _, fieldName := range strings.Split(selector, ".") {
if v.Kind() != vdl.Struct {
return opconst.Const{}, fmt.Errorf("invalid selector on const of kind: %v", v.Type().Kind())
}
next := v.StructFieldByName(fieldName)
if next == nil {
return opconst.Const{}, fmt.Errorf("invalid field name on struct %s: %s", v, fieldName)
}
v = next
}
return opconst.FromValue(v), nil
}
// evalSelectorOnType evaluates the selector on t.
func (e *Env) evalSelectorOnType(t *vdl.Type, selector string) (opconst.Const, error) {
if t.Kind() != vdl.Enum {
return opconst.Const{}, fmt.Errorf("invalid selector on type of kind: %v", t.Kind())
}
index := t.EnumIndex(selector)
if index < 0 {
return opconst.Const{}, fmt.Errorf("invalid label on enum %s: %s", t.Name(), selector)
}
return opconst.FromValue(vdl.EnumValue(t, index)), nil
}
// EvalConst resolves and evaluates a name to a const.
func (e *Env) EvalConst(name string, file *File) (opconst.Const, error) {
if cd, matched := e.ResolveConst(name, file); cd != nil {
if matched == name {
return opconst.FromValue(cd.Value), nil
}
remainder := name[len(matched)+1:]
c, err := e.evalSelectorOnValue(cd.Value, remainder)
if err != nil {
return opconst.Const{}, err
}
return c, nil
}
if td, matched := e.ResolveType(name, file); td != nil {
if matched == name {
return opconst.Const{}, fmt.Errorf("%s is a type", name)
}
remainder := name[len(matched)+1:]
c, err := e.evalSelectorOnType(td.Type, remainder)
if err != nil {
return opconst.Const{}, err
}
return c, nil
}
return opconst.Const{}, fmt.Errorf("%s undefined", name)
}
// Errorf is a helper for error reporting, to consistently contain the file and
// position of the error when possible.
func (e *Env) Errorf(file *File, pos parse.Pos, format string, v ...interface{}) {
e.Errors.Error(fpStringf(file, pos, format, v...))
}
func (e *Env) prefixErrorf(file *File, pos parse.Pos, err error, format string, v ...interface{}) {
e.Errors.Error(fpStringf(file, pos, format, v...) + " (" + err.Error() + ")")
}
func fpString(file *File, pos parse.Pos) string {
return path.Join(file.Package.Path, file.BaseName) + ":" + pos.String()
}
func fpStringf(file *File, pos parse.Pos, format string, v ...interface{}) string {
return fmt.Sprintf(fpString(file, pos)+" "+format, v...)
}
// DisallowPathQualifiers disables syntax like "a/b/c".Type.
func (e *Env) DisallowPathQualifiers() *Env {
e.disallowPathQualifiers = true
return e
}
// Representation of the components of an vdl file. These data types represent
// the results of the compilation, used by generators for different languages.
// Package represents a vdl package, containing a list of files.
type Package struct {
// Name is the name of the package, specified in the vdl files.
// E.g. "bar"
Name string
// Path is the package path; the path used in VDL import clauses.
// E.g. "foo/bar".
Path string
// GenPath is the package path to use for code generation. It is typically
// the same as Path, except for vdlroot standard packages.
// E.g. "v.io/v23/vdlroot/time"
GenPath string
// Files holds the files contained in the package.
Files []*File
// FileDoc holds the top-level file documentation, which must be the same for
// every file in the package. This is typically used to hold boilerplate that
// must appear in every generated file, e.g. a copyright notice.
FileDoc string
// Config holds the configuration for this package, specifying options used
// during compilation and code generation.
Config vdltool.Config
// We hold some internal maps to make local name resolution cheap and easy.
typeMap map[string]*TypeDef
constMap map[string]*ConstDef
ifaceMap map[string]*Interface
// We also hold some internal slices, to remember the dependency ordering.
typeDefs []*TypeDef
constDefs []*ConstDef
errorDefs []*ErrorDef
ifaceDefs []*Interface
// lowercaseIdents maps from lowercased identifier to a detail string; it's
// used to detect and report identifier conflicts.
lowercaseIdents map[string]string
}
func newPackage(name, pkgPath, genPath string, config vdltool.Config) *Package {
return &Package{
Name: name,
Path: pkgPath,
GenPath: genPath,
Config: config,
typeMap: make(map[string]*TypeDef),
constMap: make(map[string]*ConstDef),
ifaceMap: make(map[string]*Interface),
lowercaseIdents: make(map[string]string),
}
}
// QualifiedName returns the fully-qualified name of an identifier, by
// prepending the identifier with the package path.
func (p *Package) QualifiedName(id string) string {
if p.Path == "" {
return id
}
return p.Path + "." + id
}
// ResolveType resolves the type name to its definition.
func (p *Package) ResolveType(name string) *TypeDef { return p.typeMap[name] }
// ResolveConst resolves the const name to its definition.
func (p *Package) ResolveConst(name string) *ConstDef { return p.constMap[name] }
// ResolveInterface resolves the interface name to its definition.
func (p *Package) ResolveInterface(name string) *Interface { return p.ifaceMap[name] }
// resolve resolves a name against the package.
// Checks for duplicate definitions should be performed before this is called.
func (p *Package) resolve(name string, isLocal bool) interface{} {
if t := p.ResolveType(name); t != nil && (t.Exported || isLocal) {
return t
}
if c := p.ResolveConst(name); c != nil && (c.Exported || isLocal) {
return c
}
if i := p.ResolveInterface(name); i != nil && (i.Exported || isLocal) {
return i
}
return nil
}
// Doc returns the package documentation, which should only exist in a single
// file in the package.
func (p *Package) Doc() string {
for _, file := range p.Files {
if doc := file.PackageDef.Doc; doc != "" {
return doc
}
}
return ""
}
// TypeDefs returns all types defined in this package, in dependency order. If
// type B refers to type A, B depends on A, and A will appear before B. Types
// may have cyclic dependencies; the ordering of cyclicly dependent types is
// arbitrary.
func (p *Package) TypeDefs() []*TypeDef { return p.typeDefs }
// ConstDefs returns all consts defined in this package, in dependency order.
// If const B refers to const A, B depends on A, and A will appear before B.
// Consts cannot have cyclic dependencies.
func (p *Package) ConstDefs() (x []*ConstDef) { return p.constDefs }
// ErrorDefs returns all errors defined in this package. Errors don't have
// dependencies.
func (p *Package) ErrorDefs() (x []*ErrorDef) { return p.errorDefs }
// Interfaces returns all interfaces defined in this package, in dependency
// order. If interface B embeds interface A, B depends on A, and A will appear
// before B. Interfaces cannot have cyclic dependencies.
func (p *Package) Interfaces() (x []*Interface) { return p.ifaceDefs }
// File represents a compiled vdl file.
type File struct {
BaseName string // Base name of the vdl file, e.g. "foo.vdl"
PackageDef NamePos // Name, position and docs of the "package" clause
TypeDefs []*TypeDef // Types defined in this file
ConstDefs []*ConstDef // Consts defined in this file
ErrorDefs []*ErrorDef // Errors defined in this file
Interfaces []*Interface // Interfaces defined in this file
Package *Package // Parent package
TypeDeps map[*vdl.Type]bool // Types the file depends on
PackageDeps []*Package // Packages the file depends on, sorted by path
// Imports maps the user-supplied imports from local package name to package
// path. They may be different from PackageDeps since we evaluate all consts
// to their final typed value. E.g. let's say we have three vdl files:
//
// a/a.vdl type Foo int32; const A1 = Foo(1)
// b/b.vdl import "a"; const B1 = a.Foo(1); const B2 = a.A1 + 1
// c/c.vdl import "b"; const C1 = b.B1; const C2 = b.B1 + 1
//
// The final type and value of the constants:
// A1 = a.Foo(1); B1 = a.Foo(1); C1 = a.Foo(1)
// B2 = a.Foo(2); C2 = a.Foo(2)
//
// Note that C1 and C2 both have final type a.Foo, even though c.vdl doesn't
// explicitly import "a", and the generated c.go shouldn't import "b" since
// it's not actually used anymore.
imports map[string]*importPath
}
type importPath struct {
path string
pos parse.Pos
used bool // was this import path ever used?
}
// LookupImportPath translates local into a package path name, based on the
// imports associated with the file. Returns the empty string "" if local
// couldn't be found; every valid package path is non-empty.
func (f *File) LookupImportPath(local string) string {
if imp, ok := f.imports[local]; ok {
imp.used = true
return imp.path
}
return ""
}
// ValidateImportPackagePath returns true iff path is listed in the file's
// imports, and marks the import as used.
func (f *File) ValidateImportPackagePath(path string) bool {
for _, imp := range f.imports {
if imp.path == path {
imp.used = true
return true
}
}
return false
}
// identDetail formats a detail string for calls to DeclareIdent.
func identDetail(kind string, file *File, pos parse.Pos) string {
return fmt.Sprintf("%s at %s:%s", kind, file.BaseName, pos)
}
// DeclareIdent declares ident with the given detail string. Returns an error
// if ident conflicts with an existing identifier in this file or package, where
// the error includes the the previous declaration detail.
func (f *File) DeclareIdent(ident, detail string) error {
// Identifiers must be distinct from the the import names used in this file,
// but can differ by only their capitalization. E.g.
// import "foo"
// type foo string // BAD, type "foo" collides with import "foo"
// type Foo string // OK, type "Foo" distinct from import "foo"
// type FoO string // OK, type "FoO" distinct from import "foo"
if i, ok := f.imports[ident]; ok {
return fmt.Errorf("previous import at %s", i.pos)
}
// Identifiers must be distinct from all other identifiers within this
// package, and cannot differ by only their capitalization. E.g.
// type foo string
// const foo = "a" // BAD, const "foo" collides with type "foo"
// const Foo = "A" // BAD, const "Foo" collides with type "foo"
// const FoO = "A" // BAD, const "FoO" collides with type "foo"
lower := strings.ToLower(ident)
if prevDetail := f.Package.lowercaseIdents[lower]; prevDetail != "" {
return fmt.Errorf("previous %s", prevDetail)
}
f.Package.lowercaseIdents[lower] = detail
return nil
}
// Interface represents a set of embedded interfaces and methods.
type Interface struct {
NamePos // interface name, pos and doc
Exported bool // is this interface exported?
Embeds []*Interface // list of embedded interfaces
Methods []*Method // list of methods
File *File // parent file
}
// Method represents a method in an interface.
type Method struct {
NamePos // method name, pos and doc
InArgs []*Field // list of positional in-args
OutArgs []*Field // list of positional out-args
InStream *vdl.Type // in-stream type, may be nil
OutStream *vdl.Type // out-stream type, may be nil
Tags []*vdl.Value // list of method tags
Interface *Interface // parent interface
}
// Field represents method arguments and error params.
type Field struct {
NamePos // arg name, pos and doc
Type *vdl.Type // arg type, never nil
}
// NamePos represents a name, its associated position and documentation.
type NamePos parse.NamePos
func (x *Field) String() string { return fmt.Sprintf("%+v", *x) }
func (x *NamePos) String() string { return fmt.Sprintf("%+v", *x) }
func (x *Package) String() string {
c := *x
c.typeMap = nil
c.constMap = nil
c.ifaceMap = nil
c.typeDefs = nil
c.constDefs = nil
c.errorDefs = nil
c.ifaceDefs = nil
return fmt.Sprintf("%+v", c)
}
func (x *File) String() string {
c := *x
c.Package = nil // avoid infinite loop
return fmt.Sprintf("%+v", c)
}
func (x *Interface) String() string {
c := *x
c.File = nil // avoid infinite loop
return fmt.Sprintf("%+v", c)
}
func (x *Method) String() string {
c := *x
c.Interface = nil // avoid infinite loop
return fmt.Sprintf("%+v", c)
}
func (x *Interface) AllMethods() []*Method {
result := make([]*Method, len(x.Methods))
copy(result, x.Methods)
for _, embed := range x.Embeds {
result = append(result, embed.AllMethods()...)
}
return result
}
func (x *Interface) FindMethod(name string) *Method {
for _, m := range x.AllMethods() {
if name == m.Name {
return m
}
}
return nil
}
func (x *Interface) TransitiveEmbeds() []*Interface {
return x.transitiveEmbeds(make(map[*Interface]bool))
}
func (x *Interface) transitiveEmbeds(seen map[*Interface]bool) []*Interface {
var ret []*Interface
for _, e := range x.Embeds {
if !seen[e] {
seen[e] = true
ret = append(ret, e)
ret = append(ret, e.transitiveEmbeds(seen)...)
}
}
return ret
}
// We might consider allowing more characters, but we'll need to ensure they're
// allowed in all our codegen languages.
var (
regexpIdent = regexp.MustCompile("^[A-Za-z][A-Za-z0-9_]*$")
)
// hasUpperAcronym returns true if s contains an uppercase acronym; if s
// contains a run of two uppercase letters not followed by a lowercase letter.
// The lowercase letter special-case is to allow identifiers like "AMethod".
func hasUpperAcronym(s string) bool {
upperRun := 0
for _, r := range s {
switch {
case upperRun == 2 && !unicode.IsLower(r):
return true
case unicode.IsUpper(r):
upperRun++
default:
upperRun = 0
}
}
return upperRun >= 2
}
// validConstIdent returns (exported, err) where err is non-nil iff the
// identifer is valid as the name of a const. Exported is true if the
// identifier is exported. Valid: "^[A-Za-z][A-Za-z0-9_]*$"
func validConstIdent(ident string, mode reservedMode) (bool, error) {
if re := regexpIdent; !re.MatchString(ident) {
return false, fmt.Errorf("allowed regexp: %q", re)
}
if reservedWord(ident, mode) {
return false, fmt.Errorf("reserved word in a generated language")
}
return ident[0] >= 'A' && ident[0] <= 'Z', nil
}
// validIdent is like validConstIdent, but applies to all non-const identifiers.
// It adds an additional check for uppercase acronyms.
func validIdent(ident string, mode reservedMode) (bool, error) {
exported, err := validConstIdent(ident, mode)
if err != nil {
return false, err
}
if hasUpperAcronym(ident) {
// TODO(toddw): Link to documentation explaining why.
return false, fmt.Errorf("acronyms must use CamelCase")
}
return exported, nil
}
// validExportedIdent returns a non-nil error iff the identifier is valid and
// exported.
func validExportedIdent(ident string, mode reservedMode) error {
exported, err := validIdent(ident, mode)
if err != nil {
return err
}
if !exported {
return fmt.Errorf("must be exported")
}
return nil
}