Add basic variable support

Assignments to arbitrary variables are now supported in Blueprint
files.  Variables cannot be reassigned, and they stay in scope for
the remainder of the current Blueprint file and any subdirs Blueprint
files.  Variables can be used in place of a value in properites or
as an entry in a list.

Change-Id: I04447e4c541304ded802bb82d1ca4ce07d5d95d2
diff --git a/blueprint/context.go b/blueprint/context.go
index 788d45b..d43c3af 100644
--- a/blueprint/context.go
+++ b/blueprint/context.go
@@ -145,9 +145,9 @@
 
 type mutatorInfo struct {
 	// set during RegisterMutator
-	topDownMutator TopDownMutator
-	bottomUpMutator    BottomUpMutator
-	name              string
+	topDownMutator  TopDownMutator
+	bottomUpMutator BottomUpMutator
+	name            string
 }
 
 func (e *Error) Error() string {
@@ -293,7 +293,7 @@
 
 	c.mutatorInfo = append(c.mutatorInfo, &mutatorInfo{
 		topDownMutator: mutator,
-		name:              name,
+		name:           name,
 	})
 }
 
@@ -311,7 +311,7 @@
 
 	c.mutatorInfo = append(c.mutatorInfo, &mutatorInfo{
 		bottomUpMutator: mutator,
-		name:           name,
+		name:            name,
 	})
 }
 
@@ -339,17 +339,19 @@
 // This method should probably not be used directly.  It is provided to simplify
 // testing.  Instead ParseBlueprintsFiles should be called to parse a set of
 // Blueprints files starting from a top-level Blueprints file.
-func (c *Context) Parse(rootDir, filename string, r io.Reader) (subdirs []string,
-	errs []error) {
+func (c *Context) Parse(rootDir, filename string, r io.Reader,
+	scope *parser.Scope) (subdirs []string, errs []error, outScope *parser.Scope) {
 
 	c.dependenciesReady = false
 
 	relBlueprintsFile, err := filepath.Rel(rootDir, filename)
 	if err != nil {
-		return nil, []error{err}
+		return nil, []error{err}, nil
 	}
 
-	defs, errs := parser.Parse(filename, r)
+	scope = parser.NewScope(scope)
+	scope.Remove("subdirs")
+	defs, errs := parser.Parse(filename, r, scope)
 	if len(errs) > 0 {
 		for i, err := range errs {
 			if parseErr, ok := err.(*parser.ParseError); ok {
@@ -363,7 +365,7 @@
 
 		// If there were any parse errors don't bother trying to interpret the
 		// result.
-		return nil, errs
+		return nil, errs, nil
 	}
 
 	for _, def := range defs {
@@ -373,12 +375,7 @@
 			newErrs = c.processModuleDef(def, relBlueprintsFile)
 
 		case *parser.Assignment:
-			var newSubdirs []string
-			newSubdirs, newErrs = c.processAssignment(def)
-			if newSubdirs != nil {
-				subdirs = newSubdirs
-			}
-
+			// Already handled via Scope object
 		default:
 			panic("unknown definition type")
 		}
@@ -391,7 +388,17 @@
 		}
 	}
 
-	return subdirs, errs
+	subdirs, newErrs := c.processSubdirs(scope)
+	if len(newErrs) > 0 {
+		errs = append(errs, newErrs...)
+	}
+
+	return subdirs, errs, scope
+}
+
+type blueprintAndScope struct {
+	blueprint string
+	scope     *parser.Scope
 }
 
 // ParseBlueprintsFiles parses a set of Blueprints files starting with the file
@@ -409,7 +416,7 @@
 	rootDir := filepath.Dir(rootFile)
 
 	depsSet := map[string]bool{rootFile: true}
-	blueprints := []string{rootFile}
+	blueprints := []blueprintAndScope{blueprintAndScope{rootFile, nil}}
 
 	var file *os.File
 	defer func() {
@@ -425,7 +432,8 @@
 			return
 		}
 
-		filename := blueprints[i]
+		filename := blueprints[i].blueprint
+		scope := blueprints[i].scope
 		dir := filepath.Dir(filename)
 
 		file, err = os.Open(filename)
@@ -434,7 +442,7 @@
 			continue
 		}
 
-		subdirs, newErrs := c.Parse(rootDir, filename, file)
+		subdirs, newErrs, subScope := c.Parse(rootDir, filename, file, scope)
 		if len(newErrs) > 0 {
 			errs = append(errs, newErrs...)
 			continue
@@ -475,7 +483,11 @@
 						// We haven't seen this Blueprints file before, so add
 						// it to our list.
 						depsSet[subBlueprints] = true
-						blueprints = append(blueprints, subBlueprints)
+						blueprints = append(blueprints,
+							blueprintAndScope{
+								subBlueprints,
+								subScope,
+							})
 					}
 				}
 
@@ -487,7 +499,11 @@
 				subBlueprints := filepath.Join(subdir, "Blueprints")
 				if !depsSet[subBlueprints] {
 					depsSet[subBlueprints] = true
-					blueprints = append(blueprints, subBlueprints)
+					blueprints = append(blueprints,
+						blueprintAndScope{
+							subBlueprints,
+							subScope,
+						})
 				}
 			}
 		}
@@ -523,10 +539,10 @@
 	return subdirs, nil
 }
 
-func (c *Context) processAssignment(
-	assignment *parser.Assignment) (subdirs []string, errs []error) {
+func (c *Context) processSubdirs(
+	scope *parser.Scope) (subdirs []string, errs []error) {
 
-	if assignment.Name == "subdirs" {
+	if assignment, err := scope.Get("subdirs"); err == nil {
 		switch assignment.Value.Type {
 		case parser.List:
 			subdirs = make([]string, 0, len(assignment.Value.ListValue))
@@ -574,12 +590,7 @@
 		}
 	}
 
-	return nil, []error{
-		&Error{
-			Err: fmt.Errorf("only 'subdirs' assignment is supported"),
-			Pos: assignment.Pos,
-		},
-	}
+	return nil, nil
 }
 
 func (c *Context) createVariants(origModule *moduleInfo, mutatorName string,
diff --git a/blueprint/parser/parser.go b/blueprint/parser/parser.go
index 8dd2eba..07b26e8 100644
--- a/blueprint/parser/parser.go
+++ b/blueprint/parser/parser.go
@@ -4,6 +4,7 @@
 	"errors"
 	"fmt"
 	"io"
+	"sort"
 	"strconv"
 	"strings"
 	"text/scanner"
@@ -22,8 +23,8 @@
 	return fmt.Sprintf("%s: %s", e.Pos, e.Err)
 }
 
-func Parse(filename string, r io.Reader) (defs []Definition, errs []error) {
-	p := newParser(r)
+func Parse(filename string, r io.Reader, scope *Scope) (defs []Definition, errs []error) {
+	p := newParser(r, scope)
 	p.scanner.Filename = filename
 
 	defer func() {
@@ -48,10 +49,12 @@
 	scanner scanner.Scanner
 	tok     rune
 	errors  []error
+	scope   *Scope
 }
 
-func newParser(r io.Reader) *parser {
+func newParser(r io.Reader, scope *Scope) *parser {
 	p := &parser{}
+	p.scope = scope
 	p.scanner.Init(r)
 	p.scanner.Error = func(sc *scanner.Scanner, msg string) {
 		p.errorf(msg)
@@ -140,6 +143,10 @@
 	assignment.Value = value
 	assignment.Pos = pos
 
+	if p.scope != nil {
+		p.scope.Add(assignment)
+	}
+
 	return
 }
 
@@ -196,7 +203,7 @@
 func (p *parser) parseValue() (value Value) {
 	switch p.tok {
 	case scanner.Ident:
-		return p.parseBoolValue()
+		return p.parseVariable()
 	case scanner.String:
 		return p.parseStringValue()
 	case '[':
@@ -210,18 +217,23 @@
 	}
 }
 
-func (p *parser) parseBoolValue() (value Value) {
-	value.Type = Bool
+func (p *parser) parseVariable() (value Value) {
 	value.Pos = p.scanner.Position
 	switch text := p.scanner.TokenText(); text {
 	case "true":
+		value.Type = Bool
 		value.BoolValue = true
 	case "false":
+		value.Type = Bool
 		value.BoolValue = false
 	default:
-		p.errorf("expected true or false; found %q", text)
-		return
+		assignment, err := p.scope.Get(p.scanner.TokenText())
+		if err != nil {
+			p.errorf(err.Error())
+		}
+		value = assignment.Value
 	}
+
 	p.accept(scanner.Ident)
 	return
 }
@@ -247,8 +259,13 @@
 	}
 
 	var elements []Value
-	for p.tok == scanner.String {
-		elements = append(elements, p.parseStringValue())
+	for p.tok != ']' {
+		element := p.parseValue()
+		if element.Type != String {
+			p.errorf("Expected string in list, found %s", element.String())
+			return
+		}
+		elements = append(elements, element)
 
 		if p.tok != ',' {
 			// There was no comma, so the list is done.
@@ -379,3 +396,60 @@
 		panic(fmt.Errorf("bad property type: %d", p.Type))
 	}
 }
+
+type Scope struct {
+	vars map[string]*Assignment
+}
+
+func NewScope(s *Scope) *Scope {
+	newScope := &Scope{
+		vars: make(map[string]*Assignment),
+	}
+
+	if s != nil {
+		for k, v := range s.vars {
+			newScope.vars[k] = v
+		}
+	}
+
+	return newScope
+}
+
+func (s *Scope) Add(assignment *Assignment) error {
+	if old, ok := s.vars[assignment.Name]; ok {
+		return fmt.Errorf("variable already set, previous assignment: %s", old)
+	}
+
+	s.vars[assignment.Name] = assignment
+
+	return nil
+}
+
+func (s *Scope) Remove(name string) {
+	delete(s.vars, name)
+}
+
+func (s *Scope) Get(name string) (*Assignment, error) {
+	if a, ok := s.vars[name]; ok {
+		return a, nil
+	}
+
+	return nil, fmt.Errorf("variable %s not set", name)
+}
+
+func (s *Scope) String() string {
+	vars := []string{}
+
+	for k := range s.vars {
+		vars = append(vars, k)
+	}
+
+	sort.Strings(vars)
+
+	ret := []string{}
+	for _, v := range vars {
+		ret = append(ret, s.vars[v].String())
+	}
+
+	return strings.Join(ret, "\n")
+}