db80229899
* Persue the default/optional checking if a property value == nil. * Use the Interface() function to convert a reflect.Type to its underlying interface{} value. This is required for typechecking to check out. * Also, unrelated to the above, change type assertions to use nil rather than allocating real objects. Although minimal, this incurs less GC pressure.
213 lines
6.9 KiB
Go
213 lines
6.9 KiB
Go
// Copyright 2016 Marapongo, Inc. All rights reserved.
|
|
|
|
package compiler
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/golang/glog"
|
|
|
|
"github.com/marapongo/mu/pkg/ast"
|
|
"github.com/marapongo/mu/pkg/compiler/core"
|
|
"github.com/marapongo/mu/pkg/diag"
|
|
"github.com/marapongo/mu/pkg/errors"
|
|
)
|
|
|
|
// PTAnalyzer knows how to walk and validate parse trees.
|
|
type PTAnalyzer interface {
|
|
core.Visitor
|
|
|
|
// AnalyzeStack checks the validity of an entire Stack parse tree.
|
|
AnalyzeStack(stack *ast.Stack)
|
|
// AnalyzeWorkspace checks the validity of an entire Workspace parse tree.
|
|
AnalyzeWorkspace(w *ast.Workspace)
|
|
}
|
|
|
|
// NewPTAnalayzer allocates a new PTAnalyzer associated with the given Compiler.
|
|
func NewPTAnalyzer(c Compiler) PTAnalyzer {
|
|
return &ptAnalyzer{c: c}
|
|
}
|
|
|
|
type ptAnalyzer struct {
|
|
c Compiler
|
|
}
|
|
|
|
var _ PTAnalyzer = (*ptAnalyzer)(nil) // compile-time assert that binder implements PTAnalyzer.
|
|
|
|
func (a *ptAnalyzer) Diag() diag.Sink {
|
|
return a.c.Diag()
|
|
}
|
|
|
|
func (a *ptAnalyzer) AnalyzeStack(stack *ast.Stack) {
|
|
glog.Infof("Parsetree analyzing Mu Stack: %v", stack.Name)
|
|
if glog.V(2) {
|
|
defer func() {
|
|
glog.V(2).Infof("Parsetree analysis for Mu Stack %v completed w/ %v warnings and %v errors",
|
|
stack.Name, a.Diag().Warnings(), a.Diag().Errors())
|
|
}()
|
|
}
|
|
|
|
// Use an InOrderVisitor to walk the tree in-order; this handles determinism for us.
|
|
v := core.NewInOrderVisitor(a, nil)
|
|
v.VisitStack(stack)
|
|
}
|
|
|
|
func (a *ptAnalyzer) AnalyzeWorkspace(w *ast.Workspace) {
|
|
glog.Infof("Parsetree analyzing workspace file: %v", w.Doc.File)
|
|
if glog.V(2) {
|
|
defer func() {
|
|
glog.V(2).Infof("Parsetree analysis for workspace %v completed w/ %v warnings and %v errors",
|
|
w.Doc.File, a.Diag().Warnings(), a.Diag().Errors())
|
|
}()
|
|
}
|
|
|
|
// Use an InOrderVisitor to walk the tree in-order; this handles determinism for us.
|
|
v := core.NewInOrderVisitor(a, nil)
|
|
v.VisitWorkspace(w)
|
|
}
|
|
|
|
func (a *ptAnalyzer) VisitWorkspace(w *ast.Workspace) {
|
|
}
|
|
|
|
func (a *ptAnalyzer) VisitCluster(name string, cluster *ast.Cluster) {
|
|
// Decorate the AST with contextual information so subsequent passes can operate context-free.
|
|
cluster.Name = name
|
|
}
|
|
|
|
func (a *ptAnalyzer) VisitDependency(parent *ast.Workspace, ref ast.Ref, dep *ast.Dependency) {
|
|
}
|
|
|
|
func (a *ptAnalyzer) VisitStack(stack *ast.Stack) {
|
|
}
|
|
|
|
func (a *ptAnalyzer) VisitSchemas(parent *ast.Stack, schemas *ast.Schemas) {
|
|
}
|
|
|
|
func (a *ptAnalyzer) VisitSchema(pstack *ast.Stack, parent *ast.Schemas, name ast.Name, public bool,
|
|
schema *ast.Schema) {
|
|
// Decorate the AST with contextual information.
|
|
schema.Name = name
|
|
schema.Public = public
|
|
|
|
// If the schema has a base type listed, parse it to the best of our ability.
|
|
if schema.Base != "" {
|
|
schema.BoundBase = a.parseType(schema.Base)
|
|
}
|
|
}
|
|
|
|
func (a *ptAnalyzer) VisitProperty(parent *ast.Stack, schema *ast.Schema, name string, prop *ast.Property) {
|
|
// Decorate the AST with contextual information so subsequent passes can operate context-free.
|
|
prop.Name = name
|
|
|
|
// Parse the property type to the best of our ability at this phase in the compiler.
|
|
prop.BoundType = a.parseType(prop.Type)
|
|
}
|
|
|
|
func (a *ptAnalyzer) VisitServices(parent *ast.Stack, svcs *ast.Services) {
|
|
// We need to expand the UntypedServiceMaps into strongly typed ServiceMaps. As part of this, we also decorate the
|
|
// AST with extra contextual information so subsequent passes can operate context-free.
|
|
// TODO[marapongo/mu#4]: once we harden the marshalers, we should be able to largely eliminate this.
|
|
svcs.Public = make(ast.ServiceMap)
|
|
for _, name := range ast.StableUntypedServices(svcs.PublicUntyped) {
|
|
svcs.Public[name] = a.untypedServiceToTyped(parent, name, true, svcs.PublicUntyped[name])
|
|
}
|
|
svcs.Private = make(ast.ServiceMap)
|
|
for _, name := range ast.StableUntypedServices(svcs.PrivateUntyped) {
|
|
svcs.Private[name] = a.untypedServiceToTyped(parent, name, false, svcs.PrivateUntyped[name])
|
|
}
|
|
}
|
|
|
|
func (a *ptAnalyzer) untypedServiceToTyped(parent *ast.Stack, name ast.Name, public bool,
|
|
bag map[string]interface{}) *ast.Service {
|
|
var typ ast.Name
|
|
t, has := bag["type"]
|
|
if has {
|
|
// If the bag contains a type, ensure that it is a string.
|
|
ts, ok := t.(string)
|
|
if ok {
|
|
typ = ast.Name(ts)
|
|
} else {
|
|
a.Diag().Errorf(errors.ErrorIllegalMufileSyntax.At(parent), "service type must be a string")
|
|
}
|
|
|
|
// Delete the type property so it's not considered semantically meaningful for the target.
|
|
delete(bag, "type")
|
|
}
|
|
|
|
return &ast.Service{
|
|
Name: name,
|
|
Type: ast.Ref(typ),
|
|
Public: public,
|
|
Properties: bag,
|
|
}
|
|
}
|
|
|
|
func (a *ptAnalyzer) VisitService(pstack *ast.Stack, parent *ast.Services, name ast.Name, public bool,
|
|
svc *ast.Service) {
|
|
}
|
|
|
|
// parseType produces an ast.Type. This will not have been bound yet, so for example, we won't know whether
|
|
// an arbitrary non-primitive reference name references a stack or a schema, however at least this is a start.
|
|
func (a *ptAnalyzer) parseType(ref ast.Ref) *ast.Type {
|
|
refs := string(ref)
|
|
|
|
mix := strings.Index(refs, ast.TypeDecorsMapPrefix)
|
|
if mix == 0 {
|
|
// If we have a map, find the separator, and then parse the key and value parts.
|
|
rest := refs[mix+len(ast.TypeDecorsMapPrefix):]
|
|
if sep := strings.Index(rest, ast.TypeDecorsMapSeparator); sep != -1 {
|
|
keyn := ast.Ref(rest[:sep])
|
|
valn := ast.Ref(rest[sep+len(ast.TypeDecorsMapSeparator):])
|
|
if keyn != "" && valn != "" {
|
|
keyt := a.parseType(keyn)
|
|
valt := a.parseType(valn)
|
|
if keyt != nil && valt != nil {
|
|
return ast.NewMapType(keyt, valt)
|
|
}
|
|
}
|
|
}
|
|
|
|
a.Diag().Errorf(errors.ErrorIllegalMapLikeSyntax, refs)
|
|
} else if aix := strings.LastIndex(refs, ast.TypeDecorsArraySuffix); aix != -1 {
|
|
if aix == len(refs)-len(ast.TypeDecorsArraySuffix) {
|
|
// If we have an array, peel off the front and keep going.
|
|
rest := refs[:aix]
|
|
if rest != "" {
|
|
if elem := a.parseType(ast.Ref(rest)); elem != nil {
|
|
return ast.NewArrayType(elem)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The array part was in the wrong position. Issue an error. Maybe they did T[] instead of []T?
|
|
a.Diag().Errorf(errors.ErrorIllegalArrayLikeSyntax, refs)
|
|
} else if mix != -1 {
|
|
// The map part was in the wrong position. Issue an error.
|
|
a.Diag().Errorf(errors.ErrorIllegalMapLikeSyntax, refs)
|
|
} else {
|
|
// Otherwise, there are no decorators. Parse the result as either a primitive type or unresolved name.
|
|
switch ast.PrimitiveType(refs) {
|
|
case ast.PrimitiveTypeAny:
|
|
return ast.NewAnyType()
|
|
case ast.PrimitiveTypeString:
|
|
return ast.NewStringType()
|
|
case ast.PrimitiveTypeNumber:
|
|
return ast.NewNumberType()
|
|
case ast.PrimitiveTypeBool:
|
|
return ast.NewBoolType()
|
|
case ast.PrimitiveTypeService:
|
|
return ast.NewServiceType()
|
|
}
|
|
|
|
// If we didn't recognize anything thus far, it's a simple name. We don't yet know what it references --
|
|
// it could be a stack, schema, or even a completely bogus, missing name -- so just store it as it is.
|
|
if _, err := ref.Parse(); err != nil {
|
|
a.Diag().Errorf(errors.ErrorIllegalNameLikeSyntax, refs, err)
|
|
} else {
|
|
return ast.NewUnresolvedRefType(&ref)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|