Improve makefile parser

Improve the androidmk makefile parser based on ideas from go/ast and
friends:
   - Use type switching instead of the As* mess
   - Don't store endPos for every node, compute it based on the last
     known position in the node plus the length of the last token
   - Store positions as only the offset into the file, and then unpack
     them into Line/Column scanner.Position objects later

Change-Id: I87eb6661859951e6c2ea5a85db6229fa5561d615
diff --git a/Android.bp b/Android.bp
index 2777577..344d62b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -216,8 +216,8 @@
     name: "androidmk-parser",
     pkgPath: "android/soong/androidmk/parser",
     srcs: [
+        "androidmk/parser/ast.go",
         "androidmk/parser/make_strings.go",
-        "androidmk/parser/makething.go",
         "androidmk/parser/parser.go",
         "androidmk/parser/scope.go",
     ],
diff --git a/androidmk/cmd/androidmk/androidmk.go b/androidmk/cmd/androidmk/androidmk.go
index d925bfd..d50b5e1 100644
--- a/androidmk/cmd/androidmk/androidmk.go
+++ b/androidmk/cmd/androidmk/androidmk.go
@@ -29,8 +29,8 @@
 	inModule bool
 }
 
-func (f *bpFile) errorf(thing mkparser.MakeThing, s string, args ...interface{}) {
-	orig := thing.Dump()
+func (f *bpFile) errorf(node mkparser.Node, s string, args ...interface{}) {
+	orig := node.Dump()
 	s = fmt.Sprintf(s, args...)
 	c := bpparser.Comment{
 		Comment: []string{fmt.Sprintf("// ANDROIDMK TRANSLATION ERROR: %s", s)},
@@ -73,7 +73,7 @@
 
 	p := mkparser.NewParser(os.Args[1], bytes.NewBuffer(b))
 
-	things, errs := p.Parse()
+	nodes, errs := p.Parse()
 	if len(errs) > 0 {
 		for _, err := range errs {
 			fmt.Println("ERROR: ", err)
@@ -90,33 +90,34 @@
 	var conds []*conditional
 	var assignmentCond *conditional
 
-	for _, t := range things {
-		file.setMkPos(t.Pos(), t.EndPos())
+	for _, node := range nodes {
+		file.setMkPos(p.Unpack(node.Pos()), p.Unpack(node.End()))
 
-		if comment, ok := t.AsComment(); ok {
+		switch x := node.(type) {
+		case *mkparser.Comment:
 			file.comments = append(file.comments, bpparser.Comment{
-				Comment: []string{"//" + comment.Comment},
 				Pos:     file.bpPos,
+				Comment: []string{"//" + x.Comment},
 			})
-		} else if assignment, ok := t.AsAssignment(); ok {
-			handleAssignment(file, assignment, assignmentCond)
-		} else if directive, ok := t.AsDirective(); ok {
-			switch directive.Name {
+		case *mkparser.Assignment:
+			handleAssignment(file, x, assignmentCond)
+		case *mkparser.Directive:
+			switch x.Name {
 			case "include":
-				val := directive.Args.Value(file.scope)
+				val := x.Args.Value(file.scope)
 				switch {
 				case soongModuleTypes[val]:
-					handleModuleConditionals(file, directive, conds)
+					handleModuleConditionals(file, x, conds)
 					makeModule(file, val)
 				case val == clear_vars:
 					resetModule(file)
 				default:
-					file.errorf(directive, "unsupported include")
+					file.errorf(x, "unsupported include")
 					continue
 				}
 			case "ifeq", "ifneq", "ifdef", "ifndef":
-				args := directive.Args.Dump()
-				eq := directive.Name == "ifeq" || directive.Name == "ifdef"
+				args := x.Args.Dump()
+				eq := x.Name == "ifeq" || x.Name == "ifdef"
 				if _, ok := conditionalTranslations[args]; ok {
 					newCond := conditional{args, eq}
 					conds = append(conds, &newCond)
@@ -124,29 +125,29 @@
 						if assignmentCond == nil {
 							assignmentCond = &newCond
 						} else {
-							file.errorf(directive, "unsupported nested conditional in module")
+							file.errorf(x, "unsupported nested conditional in module")
 						}
 					}
 				} else {
-					file.errorf(directive, "unsupported conditional")
+					file.errorf(x, "unsupported conditional")
 					conds = append(conds, nil)
 					continue
 				}
 			case "else":
 				if len(conds) == 0 {
-					file.errorf(directive, "missing if before else")
+					file.errorf(x, "missing if before else")
 					continue
 				} else if conds[len(conds)-1] == nil {
-					file.errorf(directive, "else from unsupported contitional")
+					file.errorf(x, "else from unsupported contitional")
 					continue
 				}
 				conds[len(conds)-1].eq = !conds[len(conds)-1].eq
 			case "endif":
 				if len(conds) == 0 {
-					file.errorf(directive, "missing if before endif")
+					file.errorf(x, "missing if before endif")
 					continue
 				} else if conds[len(conds)-1] == nil {
-					file.errorf(directive, "endif from unsupported contitional")
+					file.errorf(x, "endif from unsupported contitional")
 					conds = conds[:len(conds)-1]
 				} else {
 					if assignmentCond == conds[len(conds)-1] {
@@ -155,11 +156,11 @@
 					conds = conds[:len(conds)-1]
 				}
 			default:
-				file.errorf(directive, "unsupported directive")
+				file.errorf(x, "unsupported directive")
 				continue
 			}
-		} else {
-			file.errorf(t, "unsupported line")
+		default:
+			file.errorf(x, "unsupported line")
 		}
 	}
 
@@ -175,7 +176,7 @@
 	fmt.Print(string(out))
 }
 
-func handleAssignment(file *bpFile, assignment mkparser.Assignment, c *conditional) {
+func handleAssignment(file *bpFile, assignment *mkparser.Assignment, c *conditional) {
 	if !assignment.Name.Const() {
 		file.errorf(assignment, "unsupported non-const variable name")
 		return
@@ -239,7 +240,7 @@
 			// This is a hack to get the LOCAL_ARM_MODE value inside
 			// of an arch: { arm: {} } block.
 			armModeAssign := assignment
-			armModeAssign.Name = mkparser.SimpleMakeString("LOCAL_ARM_MODE_HACK_arm", assignment.Name.Pos)
+			armModeAssign.Name = mkparser.SimpleMakeString("LOCAL_ARM_MODE_HACK_arm", assignment.Name.Pos())
 			handleAssignment(file, armModeAssign, c)
 		case name == "LOCAL_ADDITIONAL_DEPENDENCIES":
 			// TODO: check for only .mk files?
@@ -257,7 +258,7 @@
 	}
 }
 
-func handleModuleConditionals(file *bpFile, directive mkparser.Directive, conds []*conditional) {
+func handleModuleConditionals(file *bpFile, directive *mkparser.Directive, conds []*conditional) {
 	for _, c := range conds {
 		if c == nil {
 			continue
@@ -270,7 +271,7 @@
 		disabledPrefix := conditionalTranslations[c.cond][!c.eq]
 
 		// Create a fake assignment with enabled = false
-		val, err := makeVariableToBlueprint(file, mkparser.SimpleMakeString("false", file.bpPos), bpparser.Bool)
+		val, err := makeVariableToBlueprint(file, mkparser.SimpleMakeString("false", mkparser.NoPos), bpparser.Bool)
 		if err == nil {
 			err = setVariable(file, false, disabledPrefix, "enabled", val, true)
 		}
diff --git a/androidmk/parser/ast.go b/androidmk/parser/ast.go
new file mode 100644
index 0000000..a7fac9f
--- /dev/null
+++ b/androidmk/parser/ast.go
@@ -0,0 +1,110 @@
+package parser
+
+type Pos int
+
+const NoPos Pos = 0
+
+type Node interface {
+	Dump() string
+	Pos() Pos
+	End() Pos
+}
+
+type Assignment struct {
+	Target *MakeString
+	Name   *MakeString
+	Value  *MakeString
+	Type   string
+}
+
+func (x *Assignment) Dump() string {
+	target := ""
+	if x.Target != nil {
+		target = x.Target.Dump() + ": "
+	}
+	return target + x.Name.Dump() + x.Type + x.Value.Dump()
+}
+
+func (x *Assignment) Pos() Pos {
+	if x.Target != nil {
+		return x.Target.Pos()
+	}
+	return x.Name.Pos()
+}
+
+func (x *Assignment) End() Pos { return x.Value.End() }
+
+type Comment struct {
+	CommentPos Pos
+	Comment    string
+}
+
+func (x *Comment) Dump() string {
+	return "#" + x.Comment
+}
+
+func (x *Comment) Pos() Pos { return x.CommentPos }
+func (x *Comment) End() Pos { return Pos(int(x.CommentPos) + len(x.Comment)) }
+
+type Directive struct {
+	NamePos Pos
+	Name    string
+	Args    *MakeString
+	EndPos  Pos
+}
+
+func (x *Directive) Dump() string {
+	return x.Name + " " + x.Args.Dump()
+}
+
+func (x *Directive) Pos() Pos { return x.NamePos }
+func (x *Directive) End() Pos {
+	if x.EndPos != NoPos {
+		return x.EndPos
+	}
+	return x.Args.End()
+}
+
+type Rule struct {
+	Target        *MakeString
+	Prerequisites *MakeString
+	RecipePos     Pos
+	Recipe        string
+}
+
+func (x *Rule) Dump() string {
+	recipe := ""
+	if x.Recipe != "" {
+		recipe = "\n" + x.Recipe
+	}
+	return "rule:       " + x.Target.Dump() + ": " + x.Prerequisites.Dump() + recipe
+}
+
+func (x *Rule) Pos() Pos { return x.Target.Pos() }
+func (x *Rule) End() Pos { return Pos(int(x.RecipePos) + len(x.Recipe)) }
+
+type Variable struct {
+	Name *MakeString
+}
+
+func (x *Variable) Pos() Pos { return x.Name.Pos() }
+func (x *Variable) End() Pos { return x.Name.End() }
+
+func (x *Variable) Dump() string {
+	return "$(" + x.Name.Dump() + ")"
+}
+
+// Sort interface for []Node by position
+type byPosition []Node
+
+func (s byPosition) Len() int {
+	return len(s)
+}
+
+func (s byPosition) Swap(i, j int) {
+	s[i], s[j] = s[j], s[i]
+}
+
+func (s byPosition) Less(i, j int) bool {
+	return s[i].Pos() < s[j].Pos()
+}
diff --git a/androidmk/parser/make_strings.go b/androidmk/parser/make_strings.go
index 558853f..00d331b 100644
--- a/androidmk/parser/make_strings.go
+++ b/androidmk/parser/make_strings.go
@@ -2,7 +2,6 @@
 
 import (
 	"strings"
-	"text/scanner"
 	"unicode"
 )
 
@@ -18,18 +17,30 @@
 // of Variables.  The raw string list is always one longer than the variable
 // list.
 type MakeString struct {
-	Pos       scanner.Position
+	StringPos Pos
 	Strings   []string
 	Variables []Variable
 }
 
-func SimpleMakeString(s string, pos scanner.Position) *MakeString {
+func SimpleMakeString(s string, pos Pos) *MakeString {
 	return &MakeString{
-		Pos:     pos,
-		Strings: []string{s},
+		StringPos: pos,
+		Strings:   []string{s},
 	}
 }
 
+func (ms *MakeString) Pos() Pos {
+	return ms.StringPos
+}
+
+func (ms *MakeString) End() Pos {
+	pos := ms.StringPos
+	if len(ms.Strings) > 1 {
+		pos = ms.Variables[len(ms.Variables)-1].End()
+	}
+	return Pos(int(pos) + len(ms.Strings[len(ms.Strings)-1]))
+}
+
 func (ms *MakeString) appendString(s string) {
 	if len(ms.Strings) == 0 {
 		ms.Strings = []string{s}
@@ -97,7 +108,7 @@
 func (ms *MakeString) SplitN(sep string, n int) []*MakeString {
 	ret := []*MakeString{}
 
-	curMs := SimpleMakeString("", ms.Pos)
+	curMs := SimpleMakeString("", ms.Pos())
 
 	var i int
 	var s string
@@ -115,7 +126,7 @@
 
 			for _, r := range split[1:] {
 				ret = append(ret, curMs)
-				curMs = SimpleMakeString(r, ms.Pos)
+				curMs = SimpleMakeString(r, ms.Pos())
 			}
 		} else {
 			curMs.appendString(s)
@@ -131,7 +142,9 @@
 }
 
 func (ms *MakeString) TrimLeftSpaces() {
+	l := len(ms.Strings[0])
 	ms.Strings[0] = strings.TrimLeftFunc(ms.Strings[0], unicode.IsSpace)
+	ms.StringPos += Pos(len(ms.Strings[0]) - l)
 }
 
 func (ms *MakeString) TrimRightSpaces() {
diff --git a/androidmk/parser/make_strings_test.go b/androidmk/parser/make_strings_test.go
index cc3fd0a..5636b79 100644
--- a/androidmk/parser/make_strings_test.go
+++ b/androidmk/parser/make_strings_test.go
@@ -3,7 +3,6 @@
 import (
 	"strings"
 	"testing"
-	"text/scanner"
 )
 
 var splitNTestCases = []struct {
@@ -20,31 +19,31 @@
 				" h i j",
 			},
 			Variables: []Variable{
-				Variable{Name: SimpleMakeString("var1", scanner.Position{})},
-				Variable{Name: SimpleMakeString("var2", scanner.Position{})},
+				Variable{Name: SimpleMakeString("var1", NoPos)},
+				Variable{Name: SimpleMakeString("var2", NoPos)},
 			},
 		},
 		sep: " ",
 		n:   -1,
 		expected: []*MakeString{
-			SimpleMakeString("a", scanner.Position{}),
-			SimpleMakeString("b", scanner.Position{}),
+			SimpleMakeString("a", NoPos),
+			SimpleMakeString("b", NoPos),
 			&MakeString{
 				Strings: []string{"c", "d"},
 				Variables: []Variable{
-					Variable{Name: SimpleMakeString("var1", scanner.Position{})},
+					Variable{Name: SimpleMakeString("var1", NoPos)},
 				},
 			},
-			SimpleMakeString("e", scanner.Position{}),
+			SimpleMakeString("e", NoPos),
 			&MakeString{
 				Strings: []string{"f", ""},
 				Variables: []Variable{
-					Variable{Name: SimpleMakeString("var2", scanner.Position{})},
+					Variable{Name: SimpleMakeString("var2", NoPos)},
 				},
 			},
-			SimpleMakeString("h", scanner.Position{}),
-			SimpleMakeString("i", scanner.Position{}),
-			SimpleMakeString("j", scanner.Position{}),
+			SimpleMakeString("h", NoPos),
+			SimpleMakeString("i", NoPos),
+			SimpleMakeString("j", NoPos),
 		},
 	},
 	{
@@ -55,20 +54,20 @@
 				" h i j",
 			},
 			Variables: []Variable{
-				Variable{Name: SimpleMakeString("var1", scanner.Position{})},
-				Variable{Name: SimpleMakeString("var2", scanner.Position{})},
+				Variable{Name: SimpleMakeString("var1", NoPos)},
+				Variable{Name: SimpleMakeString("var2", NoPos)},
 			},
 		},
 		sep: " ",
 		n:   3,
 		expected: []*MakeString{
-			SimpleMakeString("a", scanner.Position{}),
-			SimpleMakeString("b", scanner.Position{}),
+			SimpleMakeString("a", NoPos),
+			SimpleMakeString("b", NoPos),
 			&MakeString{
 				Strings: []string{"c", "d e f", " h i j"},
 				Variables: []Variable{
-					Variable{Name: SimpleMakeString("var1", scanner.Position{})},
-					Variable{Name: SimpleMakeString("var2", scanner.Position{})},
+					Variable{Name: SimpleMakeString("var1", NoPos)},
+					Variable{Name: SimpleMakeString("var2", NoPos)},
 				},
 			},
 		},
diff --git a/androidmk/parser/makething.go b/androidmk/parser/makething.go
deleted file mode 100644
index 7d60a77..0000000
--- a/androidmk/parser/makething.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package parser
-
-import (
-	"text/scanner"
-)
-
-type MakeThing interface {
-	AsAssignment() (Assignment, bool)
-	AsComment() (Comment, bool)
-	AsDirective() (Directive, bool)
-	AsRule() (Rule, bool)
-	AsVariable() (Variable, bool)
-	Dump() string
-	Pos() scanner.Position
-	EndPos() scanner.Position
-}
-
-type Assignment struct {
-	makeThing
-	Name   *MakeString
-	Value  *MakeString
-	Target *MakeString
-	Type   string
-}
-
-type Comment struct {
-	makeThing
-	Comment string
-}
-
-type Directive struct {
-	makeThing
-	Name string
-	Args *MakeString
-}
-
-type Rule struct {
-	makeThing
-	Target        *MakeString
-	Prerequisites *MakeString
-	Recipe        string
-}
-
-type Variable struct {
-	makeThing
-	Name *MakeString
-}
-
-type makeThing struct {
-	pos    scanner.Position
-	endPos scanner.Position
-}
-
-func (m makeThing) Pos() scanner.Position {
-	return m.pos
-}
-
-func (m makeThing) EndPos() scanner.Position {
-	return m.endPos
-}
-
-func (makeThing) AsAssignment() (a Assignment, ok bool) {
-	return
-}
-
-func (a Assignment) AsAssignment() (Assignment, bool) {
-	return a, true
-}
-
-func (a Assignment) Dump() string {
-	target := ""
-	if a.Target != nil {
-		target = a.Target.Dump() + ": "
-	}
-	return target + a.Name.Dump() + a.Type + a.Value.Dump()
-}
-
-func (makeThing) AsComment() (c Comment, ok bool) {
-	return
-}
-
-func (c Comment) AsComment() (Comment, bool) {
-	return c, true
-}
-
-func (c Comment) Dump() string {
-	return "#" + c.Comment
-}
-
-func (makeThing) AsDirective() (d Directive, ok bool) {
-	return
-}
-
-func (d Directive) AsDirective() (Directive, bool) {
-	return d, true
-}
-
-func (d Directive) Dump() string {
-	return d.Name + " " + d.Args.Dump()
-}
-
-func (makeThing) AsRule() (r Rule, ok bool) {
-	return
-}
-
-func (r Rule) AsRule() (Rule, bool) {
-	return r, true
-}
-
-func (r Rule) Dump() string {
-	recipe := ""
-	if r.Recipe != "" {
-		recipe = "\n" + r.Recipe
-	}
-	return "rule:       " + r.Target.Dump() + ": " + r.Prerequisites.Dump() + recipe
-}
-
-func (makeThing) AsVariable() (v Variable, ok bool) {
-	return
-}
-
-func (v Variable) AsVariable() (Variable, bool) {
-	return v, true
-}
-
-func (v Variable) Dump() string {
-	return "$(" + v.Name.Dump() + ")"
-}
-
-type byPosition []MakeThing
-
-func (s byPosition) Len() int {
-	return len(s)
-}
-
-func (s byPosition) Swap(i, j int) {
-	s[i], s[j] = s[j], s[i]
-}
-
-func (s byPosition) Less(i, j int) bool {
-	return s[i].Pos().Offset < s[j].Pos().Offset
-}
diff --git a/androidmk/parser/parser.go b/androidmk/parser/parser.go
index 58e612e..ef5b492 100644
--- a/androidmk/parser/parser.go
+++ b/androidmk/parser/parser.go
@@ -21,7 +21,7 @@
 	return fmt.Sprintf("%s: %s", e.Pos, e.Err)
 }
 
-func (p *parser) Parse() ([]MakeThing, []error) {
+func (p *parser) Parse() ([]Node, []error) {
 	defer func() {
 		if r := recover(); r != nil {
 			if r == errTooManyErrors {
@@ -33,22 +33,24 @@
 
 	p.parseLines()
 	p.accept(scanner.EOF)
-	p.things = append(p.things, p.comments...)
-	sort.Sort(byPosition(p.things))
+	p.nodes = append(p.nodes, p.comments...)
+	sort.Sort(byPosition(p.nodes))
 
-	return p.things, p.errors
+	return p.nodes, p.errors
 }
 
 type parser struct {
 	scanner  scanner.Scanner
 	tok      rune
 	errors   []error
-	comments []MakeThing
-	things   []MakeThing
+	comments []Node
+	nodes    []Node
+	lines    []int
 }
 
 func NewParser(filename string, r io.Reader) *parser {
 	p := &parser{}
+	p.lines = []int{0}
 	p.scanner.Init(r)
 	p.scanner.Error = func(sc *scanner.Scanner, msg string) {
 		p.errorf(msg)
@@ -65,14 +67,29 @@
 	return p
 }
 
-func (p *parser) errorf(format string, args ...interface{}) {
+func (p *parser) Unpack(pos Pos) scanner.Position {
+	offset := int(pos)
+	line := sort.Search(len(p.lines), func(i int) bool { return p.lines[i] > offset }) - 1
+	return scanner.Position{
+		Filename: p.scanner.Filename,
+		Line:     line + 1,
+		Column:   offset - p.lines[line] + 1,
+		Offset:   offset,
+	}
+}
+
+func (p *parser) pos() Pos {
 	pos := p.scanner.Position
 	if !pos.IsValid() {
 		pos = p.scanner.Pos()
 	}
+	return Pos(pos.Offset)
+}
+
+func (p *parser) errorf(format string, args ...interface{}) {
 	err := &ParseError{
 		Err: fmt.Errorf(format, args...),
-		Pos: pos,
+		Pos: p.scanner.Position,
 	}
 	p.errors = append(p.errors, err)
 	if len(p.errors) >= maxErrors {
@@ -99,7 +116,9 @@
 			p.tok = p.scanner.Scan()
 		}
 	}
-	return
+	if p.tok == '\n' {
+		p.lines = append(p.lines, p.scanner.Position.Offset+1)
+	}
 }
 
 func (p *parser) parseLines() {
@@ -110,7 +129,7 @@
 			continue
 		}
 
-		ident, _ := p.parseExpression('=', '?', ':', '#', '\n')
+		ident := p.parseExpression('=', '?', ':', '#', '\n')
 
 		p.ignoreSpaces()
 
@@ -142,7 +161,7 @@
 		case '#', '\n', scanner.EOF:
 			ident.TrimRightSpaces()
 			if v, ok := toVariable(ident); ok {
-				p.things = append(p.things, v)
+				p.nodes = append(p.nodes, &v)
 			} else if !ident.Empty() {
 				p.errorf("expected directive, rule, or assignment after ident " + ident.Dump())
 			}
@@ -168,9 +187,9 @@
 	}
 
 	d := p.scanner.TokenText()
-	pos := p.scanner.Position
-	endPos := pos
+	pos := p.pos()
 	p.accept(scanner.Ident)
+	endPos := NoPos
 
 	expression := SimpleMakeString("", pos)
 
@@ -178,35 +197,33 @@
 	case "endif", "endef", "else":
 		// Nothing
 	case "define":
-		expression = p.parseDefine()
+		expression, endPos = p.parseDefine()
 	default:
 		p.ignoreSpaces()
-		expression, endPos = p.parseExpression()
+		expression = p.parseExpression()
 	}
 
-	p.things = append(p.things, Directive{
-		makeThing: makeThing{
-			pos:    pos,
-			endPos: endPos,
-		},
-		Name: d,
-		Args: expression,
+	p.nodes = append(p.nodes, &Directive{
+		NamePos: pos,
+		Name:    d,
+		Args:    expression,
+		EndPos:  endPos,
 	})
 	return true
 }
 
-func (p *parser) parseDefine() *MakeString {
-	value := SimpleMakeString("", p.scanner.Position)
+func (p *parser) parseDefine() (*MakeString, Pos) {
+	value := SimpleMakeString("", p.pos())
 
 loop:
 	for {
 		switch p.tok {
 		case scanner.Ident:
+			value.appendString(p.scanner.TokenText())
 			if p.scanner.TokenText() == "endef" {
 				p.accept(scanner.Ident)
 				break loop
 			}
-			value.appendString(p.scanner.TokenText())
 			p.accept(scanner.Ident)
 		case '\\':
 			p.parseEscape()
@@ -235,7 +252,7 @@
 		}
 	}
 
-	return value
+	return value, p.pos()
 }
 
 func (p *parser) parseEscape() {
@@ -244,8 +261,8 @@
 	p.scanner.Mode = scanner.ScanIdents
 }
 
-func (p *parser) parseExpression(end ...rune) (*MakeString, scanner.Position) {
-	value := SimpleMakeString("", p.scanner.Position)
+func (p *parser) parseExpression(end ...rune) *MakeString {
+	value := SimpleMakeString("", p.pos())
 
 	endParen := false
 	for _, r := range end {
@@ -255,14 +272,11 @@
 	}
 	parens := 0
 
-	endPos := p.scanner.Position
-
 loop:
 	for {
 		if endParen && parens > 0 && p.tok == ')' {
 			parens--
 			value.appendString(")")
-			endPos = p.scanner.Position
 			p.accept(')')
 			continue
 		}
@@ -278,7 +292,6 @@
 			break loop
 		case scanner.Ident:
 			value.appendString(p.scanner.TokenText())
-			endPos = p.scanner.Position
 			p.accept(scanner.Ident)
 		case '\\':
 			p.parseEscape()
@@ -288,18 +301,17 @@
 			case scanner.EOF:
 				p.errorf("expected escaped character, found %s",
 					scanner.TokenString(p.tok))
-				return value, endPos
+				return value
 			default:
 				value.appendString(`\` + string(p.tok))
 			}
-			endPos = p.scanner.Position
 			p.accept(p.tok)
 		case '#':
 			p.parseComment()
 			break loop
 		case '$':
 			var variable Variable
-			variable, endPos = p.parseVariable()
+			variable = p.parseVariable()
 			value.appendVariable(variable)
 		case scanner.EOF:
 			break loop
@@ -308,11 +320,9 @@
 				parens++
 			}
 			value.appendString("(")
-			endPos = p.scanner.Position
 			p.accept('(')
 		default:
 			value.appendString(p.scanner.TokenText())
-			endPos = p.scanner.Position
 			p.accept(p.tok)
 		}
 	}
@@ -320,12 +330,11 @@
 	if parens > 0 {
 		p.errorf("expected closing paren %s", value.Dump())
 	}
-	return value, endPos
+	return value
 }
 
-func (p *parser) parseVariable() (Variable, scanner.Position) {
-	pos := p.scanner.Position
-	endPos := pos
+func (p *parser) parseVariable() Variable {
+	pos := p.pos()
 	p.accept('$')
 	var name *MakeString
 	switch p.tok {
@@ -334,30 +343,26 @@
 	case '{':
 		return p.parseBracketedVariable('{', '}', pos)
 	case '$':
-		name = SimpleMakeString("__builtin_dollar", scanner.Position{})
+		name = SimpleMakeString("__builtin_dollar", NoPos)
 	case scanner.EOF:
 		p.errorf("expected variable name, found %s",
 			scanner.TokenString(p.tok))
 	default:
-		name, endPos = p.parseExpression(variableNameEndRunes...)
+		name = p.parseExpression(variableNameEndRunes...)
 	}
 
-	return p.nameToVariable(name, pos, endPos), endPos
+	return p.nameToVariable(name)
 }
 
-func (p *parser) parseBracketedVariable(start, end rune, pos scanner.Position) (Variable, scanner.Position) {
+func (p *parser) parseBracketedVariable(start, end rune, pos Pos) Variable {
 	p.accept(start)
-	name, endPos := p.parseExpression(end)
+	name := p.parseExpression(end)
 	p.accept(end)
-	return p.nameToVariable(name, pos, endPos), endPos
+	return p.nameToVariable(name)
 }
 
-func (p *parser) nameToVariable(name *MakeString, pos, endPos scanner.Position) Variable {
+func (p *parser) nameToVariable(name *MakeString) Variable {
 	return Variable{
-		makeThing: makeThing{
-			pos:    pos,
-			endPos: endPos,
-		},
 		Name: name,
 	}
 }
@@ -366,12 +371,11 @@
 	prerequisites, newLine := p.parseRulePrerequisites(target)
 
 	recipe := ""
-	endPos := p.scanner.Position
+	recipePos := p.pos()
 loop:
 	for {
 		if newLine {
 			if p.tok == '\t' {
-				endPos = p.scanner.Position
 				p.accept('\t')
 				newLine = false
 				continue loop
@@ -388,31 +392,25 @@
 		case '\\':
 			p.parseEscape()
 			recipe += string(p.tok)
-			endPos = p.scanner.Position
 			p.accept(p.tok)
 		case '\n':
 			newLine = true
 			recipe += "\n"
-			endPos = p.scanner.Position
 			p.accept('\n')
 		case scanner.EOF:
 			break loop
 		default:
 			recipe += p.scanner.TokenText()
-			endPos = p.scanner.Position
 			p.accept(p.tok)
 		}
 	}
 
 	if prerequisites != nil {
-		p.things = append(p.things, Rule{
-			makeThing: makeThing{
-				pos:    target.Pos,
-				endPos: endPos,
-			},
+		p.nodes = append(p.nodes, &Rule{
 			Target:        target,
 			Prerequisites: prerequisites,
 			Recipe:        recipe,
+			RecipePos:     recipePos,
 		})
 	}
 }
@@ -422,7 +420,7 @@
 
 	p.ignoreSpaces()
 
-	prerequisites, _ := p.parseExpression('#', '\n', ';', ':', '=')
+	prerequisites := p.parseExpression('#', '\n', ';', ':', '=')
 
 	switch p.tok {
 	case '\n':
@@ -439,7 +437,7 @@
 			p.parseAssignment(":=", target, prerequisites)
 			return nil, true
 		} else {
-			more, _ := p.parseExpression('#', '\n', ';')
+			more := p.parseExpression('#', '\n', ';')
 			prerequisites.appendMakeString(more)
 		}
 	case '=':
@@ -453,10 +451,9 @@
 }
 
 func (p *parser) parseComment() {
-	pos := p.scanner.Position
+	pos := p.pos()
 	p.accept('#')
 	comment := ""
-	endPos := pos
 loop:
 	for {
 		switch p.tok {
@@ -467,27 +464,21 @@
 			} else {
 				comment += "\\" + p.scanner.TokenText()
 			}
-			endPos = p.scanner.Position
 			p.accept(p.tok)
 		case '\n':
-			endPos = p.scanner.Position
 			p.accept('\n')
 			break loop
 		case scanner.EOF:
 			break loop
 		default:
 			comment += p.scanner.TokenText()
-			endPos = p.scanner.Position
 			p.accept(p.tok)
 		}
 	}
 
-	p.comments = append(p.comments, Comment{
-		makeThing: makeThing{
-			pos:    pos,
-			endPos: endPos,
-		},
-		Comment: comment,
+	p.comments = append(p.comments, &Comment{
+		CommentPos: pos,
+		Comment:    comment,
 	})
 }
 
@@ -496,7 +487,7 @@
 	// non-whitespace character after the = until the end of the logical line,
 	// which may included escaped newlines
 	p.accept('=')
-	value, endPos := p.parseExpression()
+	value := p.parseExpression()
 	value.TrimLeftSpaces()
 	if ident.EndsWith('+') && t == "=" {
 		ident.TrimRightOne()
@@ -505,11 +496,7 @@
 
 	ident.TrimRightSpaces()
 
-	p.things = append(p.things, Assignment{
-		makeThing: makeThing{
-			pos:    ident.Pos,
-			endPos: endPos,
-		},
+	p.nodes = append(p.nodes, &Assignment{
 		Name:   ident,
 		Value:  value,
 		Target: target,