Support += assignments

Support += assignments to variables.  Variables are now mutable
up until they are referenced, then they become immutable.  This
will allow variables to be modified in a conditional, or allow
better commenting on why parts of a variable are set.

Change-Id: Iad964da7206b493365fe3686eedd7954e6eaf9a2
diff --git a/parser/parser.go b/parser/parser.go
index 988080b..7f2f304 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -142,12 +142,15 @@
 			p.accept(scanner.Ident)
 
 			switch p.tok {
+			case '+':
+				p.accept('+')
+				defs = append(defs, p.parseAssignment(ident, pos, "+="))
 			case '=':
-				defs = append(defs, p.parseAssignment(ident, pos))
+				defs = append(defs, p.parseAssignment(ident, pos, "="))
 			case '{', '(':
 				defs = append(defs, p.parseModule(ident, pos))
 			default:
-				p.errorf("expected \"=\" or \"{\" or \"(\", found %s",
+				p.errorf("expected \"=\" or \"+=\" or \"{\" or \"(\", found %s",
 					scanner.TokenString(p.tok))
 			}
 		case scanner.EOF:
@@ -161,7 +164,7 @@
 }
 
 func (p *parser) parseAssignment(name string,
-	namePos scanner.Position) (assignment *Assignment) {
+	namePos scanner.Position, assigner string) (assignment *Assignment) {
 
 	assignment = new(Assignment)
 
@@ -173,10 +176,19 @@
 
 	assignment.Name = Ident{name, namePos}
 	assignment.Value = value
+	assignment.OrigValue = value
 	assignment.Pos = pos
+	assignment.Assigner = assigner
 
 	if p.scope != nil {
-		p.scope.Add(assignment)
+		if assigner == "+=" {
+			p.scope.Append(assignment)
+		} else {
+			err := p.scope.Add(assignment)
+			if err != nil {
+				p.errorf("%s", err.Error())
+			}
+		}
 	}
 
 	return
@@ -267,17 +279,10 @@
 	}
 }
 
-func (p *parser) parseOperator(value1 Value) Value {
-	operator := p.tok
-	pos := p.scanner.Position
-	p.accept(operator)
-
-	value2 := p.parseExpression()
-
+func evaluateOperator(value1, value2 Value, operator rune, pos scanner.Position) (Value, error) {
 	if value1.Type != value2.Type {
-		p.errorf("mismatched type in operator %c: %s != %s", operator,
+		return Value{}, fmt.Errorf("mismatched type in operator %c: %s != %s", operator,
 			value1.Type, value2.Type)
-		return Value{}
 	}
 
 	value := value1
@@ -292,8 +297,8 @@
 			value.ListValue = append([]Value{}, value1.ListValue...)
 			value.ListValue = append(value.ListValue, value2.ListValue...)
 		default:
-			p.errorf("operator %c not supported on type %s", operator, value1.Type)
-			return Value{}
+			return Value{}, fmt.Errorf("operator %c not supported on type %s", operator,
+				value1.Type)
 		}
 	default:
 		panic("unknown operator " + string(operator))
@@ -305,6 +310,22 @@
 		Pos:      pos,
 	}
 
+	return value, nil
+}
+
+func (p *parser) parseOperator(value1 Value) Value {
+	operator := p.tok
+	pos := p.scanner.Position
+	p.accept(operator)
+
+	value2 := p.parseExpression()
+
+	value, err := evaluateOperator(value1, value2, operator, pos)
+	if err != nil {
+		p.errorf(err.Error())
+		return Value{}
+	}
+
 	return value
 }
 
@@ -413,6 +434,11 @@
 	Pos      scanner.Position
 }
 
+func (e *Expression) String() string {
+	return fmt.Sprintf("(%s %c %s)@%d:%s", e.Args[0].String(), e.Operator, e.Args[1].String(),
+		e.Pos.Offset, e.Pos)
+}
+
 type ValueType int
 
 const (
@@ -443,13 +469,16 @@
 }
 
 type Assignment struct {
-	Name  Ident
-	Value Value
-	Pos   scanner.Position
+	Name       Ident
+	Value      Value
+	OrigValue  Value
+	Pos        scanner.Position
+	Assigner   string
+	Referenced bool
 }
 
 func (a *Assignment) String() string {
-	return fmt.Sprintf("%s@%d:%s: %s", a.Name, a.Pos.Offset, a.Pos, a.Value)
+	return fmt.Sprintf("%s@%d:%s %s %s", a.Name, a.Pos.Offset, a.Pos, a.Assigner, a.Value)
 }
 
 func (a *Assignment) definitionTag() {}
@@ -506,28 +535,37 @@
 }
 
 func (p Value) String() string {
+	var s string
+	if p.Variable != "" {
+		s += p.Variable + " = "
+	}
+	if p.Expression != nil {
+		s += p.Expression.String()
+	}
 	switch p.Type {
 	case Bool:
-		return fmt.Sprintf("%t@%d:%s", p.BoolValue, p.Pos.Offset, p.Pos)
+		s += fmt.Sprintf("%t@%d:%s", p.BoolValue, p.Pos.Offset, p.Pos)
 	case String:
-		return fmt.Sprintf("%q@%d:%s", p.StringValue, p.Pos.Offset, p.Pos)
+		s += fmt.Sprintf("%q@%d:%s", p.StringValue, p.Pos.Offset, p.Pos)
 	case List:
 		valueStrings := make([]string, len(p.ListValue))
 		for i, value := range p.ListValue {
 			valueStrings[i] = value.String()
 		}
-		return fmt.Sprintf("@%d:%s-%d:%s[%s]", p.Pos.Offset, p.Pos, p.EndPos.Offset, p.EndPos,
+		s += fmt.Sprintf("@%d:%s-%d:%s[%s]", p.Pos.Offset, p.Pos, p.EndPos.Offset, p.EndPos,
 			strings.Join(valueStrings, ", "))
 	case Map:
 		propertyStrings := make([]string, len(p.MapValue))
 		for i, property := range p.MapValue {
 			propertyStrings[i] = property.String()
 		}
-		return fmt.Sprintf("@%d:%s-%d:%s{%s}", p.Pos.Offset, p.Pos, p.EndPos.Offset, p.EndPos,
+		s += fmt.Sprintf("@%d:%s-%d:%s{%s}", p.Pos.Offset, p.Pos, p.EndPos.Offset, p.EndPos,
 			strings.Join(propertyStrings, ", "))
 	default:
 		panic(fmt.Errorf("bad property type: %d", p.Type))
 	}
+
+	return s
 }
 
 type Scope struct {
@@ -558,12 +596,27 @@
 	return nil
 }
 
+func (s *Scope) Append(assignment *Assignment) error {
+	var err error
+	if old, ok := s.vars[assignment.Name.Name]; ok {
+		if old.Referenced {
+			return fmt.Errorf("modified variable with += after referencing")
+		}
+		old.Value, err = evaluateOperator(old.Value, assignment.Value, '+', assignment.Pos)
+	} else {
+		err = s.Add(assignment)
+	}
+
+	return err
+}
+
 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 {
+		a.Referenced = true
 		return a, nil
 	}
 
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 5007497..f1e99c7 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -292,6 +292,8 @@
 		foo = "stuff"
 		bar = foo
 		baz = foo + bar
+		boo = baz
+		boo += foo
 		`,
 		[]Definition{
 			&Assignment{
@@ -302,6 +304,13 @@
 					Pos:         mkpos(9, 2, 9),
 					StringValue: "stuff",
 				},
+				OrigValue: Value{
+					Type:        String,
+					Pos:         mkpos(9, 2, 9),
+					StringValue: "stuff",
+				},
+				Assigner:   "=",
+				Referenced: true,
 			},
 			&Assignment{
 				Name: Ident{"bar", mkpos(19, 3, 3)},
@@ -312,6 +321,14 @@
 					StringValue: "stuff",
 					Variable:    "foo",
 				},
+				OrigValue: Value{
+					Type:        String,
+					Pos:         mkpos(25, 3, 9),
+					StringValue: "stuff",
+					Variable:    "foo",
+				},
+				Assigner:   "=",
+				Referenced: true,
 			},
 			&Assignment{
 				Name: Ident{"baz", mkpos(31, 4, 3)},
@@ -339,6 +356,118 @@
 						Pos:      mkpos(41, 4, 13),
 					},
 				},
+				OrigValue: Value{
+					Type:        String,
+					Pos:         mkpos(37, 4, 9),
+					StringValue: "stuffstuff",
+					Expression: &Expression{
+						Args: [2]Value{
+							{
+								Type:        String,
+								Pos:         mkpos(37, 4, 9),
+								StringValue: "stuff",
+								Variable:    "foo",
+							},
+							{
+								Type:        String,
+								Pos:         mkpos(43, 4, 15),
+								StringValue: "stuff",
+								Variable:    "bar",
+							},
+						},
+						Operator: '+',
+						Pos:      mkpos(41, 4, 13),
+					},
+				},
+				Assigner:   "=",
+				Referenced: true,
+			},
+			&Assignment{
+				Name: Ident{"boo", mkpos(49, 5, 3)},
+				Pos:  mkpos(53, 5, 7),
+				Value: Value{
+					Type:        String,
+					Pos:         mkpos(55, 5, 9),
+					StringValue: "stuffstuffstuff",
+					Expression: &Expression{
+						Args: [2]Value{
+							{
+								Type:        String,
+								Pos:         mkpos(55, 5, 9),
+								StringValue: "stuffstuff",
+								Variable:    "baz",
+								Expression: &Expression{
+									Args: [2]Value{
+										{
+											Type:        String,
+											Pos:         mkpos(37, 4, 9),
+											StringValue: "stuff",
+											Variable:    "foo",
+										},
+										{
+											Type:        String,
+											Pos:         mkpos(43, 4, 15),
+											StringValue: "stuff",
+											Variable:    "bar",
+										},
+									},
+									Operator: '+',
+									Pos:      mkpos(41, 4, 13),
+								},
+							},
+							{
+								Variable:    "foo",
+								Type:        String,
+								Pos:         mkpos(68, 6, 10),
+								StringValue: "stuff",
+							},
+						},
+						Pos:      mkpos(66, 6, 8),
+						Operator: '+',
+					},
+				},
+				OrigValue: Value{
+					Type:        String,
+					Pos:         mkpos(55, 5, 9),
+					StringValue: "stuffstuff",
+					Variable:    "baz",
+					Expression: &Expression{
+						Args: [2]Value{
+							{
+								Type:        String,
+								Pos:         mkpos(37, 4, 9),
+								StringValue: "stuff",
+								Variable:    "foo",
+							},
+							{
+								Type:        String,
+								Pos:         mkpos(43, 4, 15),
+								StringValue: "stuff",
+								Variable:    "bar",
+							},
+						},
+						Operator: '+',
+						Pos:      mkpos(41, 4, 13),
+					},
+				},
+				Assigner: "=",
+			},
+			&Assignment{
+				Name: Ident{"boo", mkpos(61, 6, 3)},
+				Pos:  mkpos(66, 6, 8),
+				Value: Value{
+					Type:        String,
+					Pos:         mkpos(68, 6, 10),
+					StringValue: "stuff",
+					Variable:    "foo",
+				},
+				OrigValue: Value{
+					Type:        String,
+					Pos:         mkpos(68, 6, 10),
+					StringValue: "stuff",
+					Variable:    "foo",
+				},
+				Assigner: "+=",
 			},
 		},
 		nil,
diff --git a/parser/printer.go b/parser/printer.go
index 1d6cdf6..46ad34f 100644
--- a/parser/printer.go
+++ b/parser/printer.go
@@ -100,8 +100,8 @@
 
 func (p *printer) printAssignment(assignment *Assignment) {
 	p.printToken(assignment.Name.Name, assignment.Name.Pos, wsDontCare)
-	p.printToken("=", assignment.Pos, wsBoth)
-	p.printValue(assignment.Value)
+	p.printToken(assignment.Assigner, assignment.Pos, wsBoth)
+	p.printValue(assignment.OrigValue)
 	p.prev.ws = wsForceBreak
 }
 
diff --git a/parser/printer_test.go b/parser/printer_test.go
index 75dfb6f..5b752e7 100644
--- a/parser/printer_test.go
+++ b/parser/printer_test.go
@@ -119,11 +119,13 @@
 foo = "stuff"
 bar = foo
 baz = foo + bar
+baz += foo
 `,
 		output: `
 foo = "stuff"
 bar = foo
 baz = foo + bar
+baz += foo
 `,
 	},
 	{