blob: df873c958165fa5221478f294d3c9cc376fc78dd [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"
"v.io/v23/vdl"
"v.io/x/ref/lib/vdl/parse"
)
// TypeDef represents a user-defined named type definition in the compiled
// results.
type TypeDef struct {
NamePos // name, parse position and docs
Exported bool // is this type definition exported?
Type *vdl.Type // type of this type definition
// BaseType is the type that Type is based on. The BaseType may be named or
// unnamed. E.g.
// BaseType
// type Bool bool; bool
// type Bool2 Bool; Bool
// type List []int32; []int32
// type List2 List; List
// type Struct struct{A bool}; struct{A bool}
// type Struct2 Struct; Struct
BaseType *vdl.Type
LabelDoc []string // [valid for enum] docs for each label
LabelDocSuffix []string // [valid for enum] suffix docs for each label
FieldDoc []string // [valid for struct, union] docs for each field
FieldDocSuffix []string // [valid for struct, union] suffix docs for each field
File *File // parent file that this type is defined in
}
func (x *TypeDef) String() string {
c := *x
c.File = nil // avoid infinite loop
return fmt.Sprintf("%+v", c)
}
// compileTypeDefs is the "entry point" to the rest of this file. It takes the
// types defined in pfiles and compiles them into TypeDefs in pkg.
func compileTypeDefs(pkg *Package, pfiles []*parse.File, env *Env) {
td := typeDefiner{
pkg: pkg,
pfiles: pfiles,
env: env,
tbuilder: &vdl.TypeBuilder{},
builders: make(map[string]*typeDefBuilder),
}
if td.Declare(); !env.Errors.IsEmpty() {
return
}
if td.Define(); !env.Errors.IsEmpty() {
return
}
if td.Build(); !env.Errors.IsEmpty() {
return
}
td.AttachDoc()
// TODO(toddw): should we disallow inter-file cyclic type dependencies? That
// might be an issue for generated C++.
}
// typeDefiner defines types in a package. This is split into three phases:
// 1) Declare ensures local type references can be resolved.
// 2) Define describes each type, resolving named references.
// 3) Build builds all types.
//
// It holds a builders map from type name to typeDefBuilder, where the
// typeDefBuilder is responsible for compiling and defining a single type.
type typeDefiner struct {
pkg *Package
pfiles []*parse.File
env *Env
tbuilder *vdl.TypeBuilder
builders map[string]*typeDefBuilder
}
type typeDefBuilder struct {
def *TypeDef
ptype parse.Type
pending vdl.PendingNamed // named type that's being built
base vdl.PendingType // base type that pending is based on
}
// Declare creates builders for each type defined in the package.
func (td typeDefiner) Declare() {
for ix := range td.pkg.Files {
file, pfile := td.pkg.Files[ix], td.pfiles[ix]
for _, pdef := range pfile.TypeDefs {
detail := identDetail("type", file, pdef.Pos)
if err := file.DeclareIdent(pdef.Name, detail); err != nil {
td.env.prefixErrorf(file, pdef.Pos, err, "type %s name conflict", pdef.Name)
continue
}
td.builders[pdef.Name] = td.makeTypeDefBuilder(file, pdef)
}
}
}
func (td typeDefiner) makeTypeDefBuilder(file *File, pdef *parse.TypeDef) *typeDefBuilder {
export, err := validIdent(pdef.Name, reservedNormal)
if err != nil {
td.env.prefixErrorf(file, pdef.Pos, err, "type %s invalid name", pdef.Name)
return nil
}
ret := new(typeDefBuilder)
ret.def = &TypeDef{NamePos: NamePos(pdef.NamePos), Exported: export, File: file}
ret.ptype = pdef.Type
// We use the qualified name to actually name the type, to ensure types
// defined in separate packages are hash-consed separately.
qname := file.Package.QualifiedName(pdef.Name)
ret.pending = td.tbuilder.Named(qname)
switch pt := pdef.Type.(type) {
case *parse.TypeEnum:
ret.def.LabelDoc = make([]string, len(pt.Labels))
ret.def.LabelDocSuffix = make([]string, len(pt.Labels))
for index, plabel := range pt.Labels {
if err := validExportedIdent(plabel.Name, reservedFirstRuneLower); err != nil {
td.env.prefixErrorf(file, plabel.Pos, err, "invalid enum label name %s", plabel.Name)
return nil
}
ret.def.LabelDoc[index] = plabel.Doc
ret.def.LabelDocSuffix[index] = plabel.DocSuffix
}
case *parse.TypeStruct:
ret = attachFieldDoc(ret, pt.Fields, file, td.env)
case *parse.TypeUnion:
ret = attachFieldDoc(ret, pt.Fields, file, td.env)
}
return ret
}
func attachFieldDoc(ret *typeDefBuilder, fields []*parse.Field, file *File, env *Env) *typeDefBuilder {
ret.def.FieldDoc = make([]string, len(fields))
ret.def.FieldDocSuffix = make([]string, len(fields))
for index, pfield := range fields {
if err := validExportedIdent(pfield.Name, reservedFirstRuneLower); err != nil {
env.prefixErrorf(file, pfield.Pos, err, "invalid field name %s", pfield.Name)
return nil
}
ret.def.FieldDoc[index] = pfield.Doc
ret.def.FieldDocSuffix[index] = pfield.DocSuffix
}
return ret
}
// Define uses the builders to describe each type. Named types defined in
// other packages must have already been compiled, and in env. Named types
// defined in this package are represented by the builders.
func (td typeDefiner) Define() {
for _, b := range td.builders {
def, file := b.def, b.def.File
base := compileDefinedType(b.ptype, file, td.env, td.tbuilder, td.builders)
switch tbase := base.(type) {
case nil:
continue // keep going to catch more errors
case *vdl.Type:
if tbase == vdl.ErrorType {
td.env.Errorf(file, def.Pos, "error cannot be renamed")
continue // keep going to catch more errors
}
def.BaseType = tbase
case vdl.PendingType:
b.base = tbase
default:
panic(fmt.Errorf("vdl: typeDefiner.Define unhandled TypeOrPending %T %v", tbase, tbase))
}
b.pending.AssignBase(base)
}
}
// compileType returns the *vdl.Type corresponding to ptype. All named types
// referenced by ptype must already be defined.
func compileType(ptype parse.Type, file *File, env *Env) *vdl.Type {
var tbuilder vdl.TypeBuilder
typeOrPending := compileLiteralType(ptype, file, env, &tbuilder, nil)
tbuilder.Build()
switch top := typeOrPending.(type) {
case nil:
return nil
case *vdl.Type:
return top
case vdl.PendingType:
t, err := top.Built()
if err != nil {
env.prefixErrorf(file, ptype.Pos(), err, "invalid type")
return nil
}
return t
default:
panic(fmt.Errorf("vdl: compileType unhandled TypeOrPending %T %v", top, top))
}
}
// compileDefinedType compiles ptype. It can handle definitions based on array,
// enum, struct and union, as well as definitions based on any literal type.
func compileDefinedType(ptype parse.Type, file *File, env *Env, tbuilder *vdl.TypeBuilder, builders map[string]*typeDefBuilder) vdl.TypeOrPending {
switch pt := ptype.(type) {
case *parse.TypeArray:
elem := compileLiteralType(pt.Elem, file, env, tbuilder, builders)
if elem == nil {
return nil
}
return tbuilder.Array().AssignLen(pt.Len).AssignElem(elem)
case *parse.TypeEnum:
enum := tbuilder.Enum()
for _, plabel := range pt.Labels {
enum.AppendLabel(plabel.Name)
}
return enum
case *parse.TypeStruct:
st := tbuilder.Struct()
for _, pfield := range pt.Fields {
ftype := compileLiteralType(pfield.Type, file, env, tbuilder, builders)
if ftype == nil {
return nil
}
st.AppendField(pfield.Name, ftype)
}
return st
case *parse.TypeUnion:
union := tbuilder.Union()
for _, pfield := range pt.Fields {
ftype := compileLiteralType(pfield.Type, file, env, tbuilder, builders)
if ftype == nil {
return nil
}
union.AppendField(pfield.Name, ftype)
}
return union
}
lit := compileLiteralType(ptype, file, env, tbuilder, builders)
if _, ok := lit.(vdl.PendingOptional); ok {
// Don't allow Optional at the top-level of a type definition. The purpose
// of this rule is twofold:
// 1) Reduce confusion; the Optional modifier cannot be hidden in a type
// definition, it must be explicitly mentioned on each use.
// 2) The Optional concept is typically translated to pointers in generated
// languages, and many languages don't support named pointer types.
//
// type A string // ok
// type B []?string // ok
// type C struct{X ?string} // ok
// type D ?string // bad
// type E ?struct{X string} // bad
env.Errorf(file, ptype.Pos(), "can't define type based on top-level optional")
return nil
}
return lit
}
// compileLiteralType compiles ptype. It can handle any literal type. Note
// that array, enum, struct and union are required to be defined and named,
// and aren't allowed as regular literal types.
func compileLiteralType(ptype parse.Type, file *File, env *Env, tbuilder *vdl.TypeBuilder, builders map[string]*typeDefBuilder) vdl.TypeOrPending {
switch pt := ptype.(type) {
case *parse.TypeNamed:
if def, matched := env.ResolveType(pt.Name, file); def != nil {
if len(matched) < len(pt.Name) {
env.Errorf(file, pt.Pos(), "type %s invalid (%s unmatched)", pt.Name, pt.Name[len(matched):])
return nil
}
return def.Type
}
if b, ok := builders[pt.Name]; ok {
return b.pending
}
env.Errorf(file, pt.Pos(), "type %s undefined", pt.Name)
return nil
case *parse.TypeList:
elem := compileLiteralType(pt.Elem, file, env, tbuilder, builders)
if elem == nil {
return nil
}
return tbuilder.List().AssignElem(elem)
case *parse.TypeSet:
key := compileLiteralType(pt.Key, file, env, tbuilder, builders)
if key == nil {
return nil
}
return tbuilder.Set().AssignKey(key)
case *parse.TypeMap:
key := compileLiteralType(pt.Key, file, env, tbuilder, builders)
elem := compileLiteralType(pt.Elem, file, env, tbuilder, builders)
if key == nil || elem == nil {
return nil
}
return tbuilder.Map().AssignKey(key).AssignElem(elem)
case *parse.TypeOptional:
elem := compileLiteralType(pt.Base, file, env, tbuilder, builders)
if elem == nil {
return nil
}
return tbuilder.Optional().AssignElem(elem)
default:
env.Errorf(file, pt.Pos(), "unnamed %s type invalid (type must be defined)", ptype.Kind())
return nil
}
}
// Build actually builds each type and updates the package with the typedefs.
// The order we call each pending type doesn't matter; the v.io/v23/vdl package
// deals with arbitrary orders, and supports recursive types. However we want
// the order to be deterministic, otherwise the output will constantly change.
// So we use the same order as the parsed file.
func (td typeDefiner) Build() {
td.tbuilder.Build()
for _, pfile := range td.pfiles {
for _, pdef := range pfile.TypeDefs {
b := td.builders[pdef.Name]
def, file := b.def, b.def.File
if b.base != nil {
base, err := b.base.Built()
if err != nil {
td.env.prefixErrorf(file, b.ptype.Pos(), err, "%s base type invalid", def.Name)
return
}
def.BaseType = base
}
t, err := b.pending.Built()
if err != nil {
td.env.prefixErrorf(file, def.Pos, err, "%s invalid", def.Name)
return
}
def.Type = t
addTypeDef(def, td.env)
}
}
}
// AttachDoc makes another pass to fill in doc and doc suffix slices for enums,
// structs and unions. Typically these are initialized in makeTypeDefBuilder,
// based on the underlying parse data. But type definitions based on other
// named types can't be updated until the base type is actually compiled.
//
// TODO(toddw): This doesn't actually attach comments from the base type, it
// just leaves everything empty. This is fine for now, but we should revamp the
// vdl parsing / comment attaching strategy in the future.
func (td typeDefiner) AttachDoc() {
for _, file := range td.pkg.Files {
for _, def := range file.TypeDefs {
switch t := def.Type; t.Kind() {
case vdl.Enum:
if len(def.LabelDoc) == 0 {
def.LabelDoc = make([]string, t.NumEnumLabel())
}
if len(def.LabelDocSuffix) == 0 {
def.LabelDocSuffix = make([]string, t.NumEnumLabel())
}
case vdl.Struct, vdl.Union:
if len(def.FieldDoc) == 0 {
def.FieldDoc = make([]string, t.NumField())
}
if len(def.FieldDocSuffix) == 0 {
def.FieldDocSuffix = make([]string, t.NumField())
}
}
}
}
}
// addTypeDef updates our various structures to add a new type def.
func addTypeDef(def *TypeDef, env *Env) {
def.File.TypeDefs = append(def.File.TypeDefs, def)
def.File.Package.typeDefs[def.Name] = def
if env != nil {
// env should only be nil during initialization of the built-in package;
// NewEnv ensures new environments have the built-in types.
env.typeDefs[def.Type] = def
}
}