Merge pull request #104 from colincross/parser

More parser cleanups
diff --git a/bpmodify/bpmodify.go b/bpmodify/bpmodify.go
index 6f7a5bf..6a3c803 100644
--- a/bpmodify/bpmodify.go
+++ b/bpmodify/bpmodify.go
@@ -124,9 +124,9 @@
 	for _, def := range file.Defs {
 		if module, ok := def.(*parser.Module); ok {
 			for _, prop := range module.Properties {
-				if prop.Name.Name == "name" && prop.Value.Type() == parser.StringType {
+				if prop.Name == "name" && prop.Value.Type() == parser.StringType {
 					if targetedModule(prop.Value.Eval().(*parser.String).Value) {
-						m, newErrs := processModule(module, prop.Name.Name, file)
+						m, newErrs := processModule(module, prop.Name, file)
 						errs = append(errs, newErrs...)
 						modified = modified || m
 					}
@@ -142,7 +142,7 @@
 	file *parser.File) (modified bool, errs []error) {
 
 	for _, prop := range module.Properties {
-		if prop.Name.Name == *parameter {
+		if prop.Name == *parameter {
 			modified, errs = processParameter(prop.Value, *parameter, moduleName, file)
 			return
 		}
diff --git a/context.go b/context.go
index b644ac5..4e05ac8 100644
--- a/context.go
+++ b/context.go
@@ -875,11 +875,11 @@
 				ret = append(ret, s.Value)
 			}
 
-			return ret, assignment.Pos, nil
+			return ret, assignment.EqualsPos, nil
 		case *parser.Bool, *parser.String:
 			return nil, scanner.Position{}, &Error{
 				Err: fmt.Errorf("%q must be a list of strings", v),
-				Pos: assignment.Pos,
+				Pos: assignment.EqualsPos,
 			}
 		default:
 			panic(fmt.Errorf("unknown value type: %d", assignment.Value.Type))
@@ -893,11 +893,11 @@
 	} else {
 		switch value := assignment.Value.Eval().(type) {
 		case *parser.String:
-			return value.Value, assignment.Pos, nil
+			return value.Value, assignment.EqualsPos, nil
 		case *parser.Bool, *parser.List:
 			return "", scanner.Position{}, &Error{
 				Err: fmt.Errorf("%q must be a string", v),
-				Pos: assignment.Pos,
+				Pos: assignment.EqualsPos,
 			}
 		default:
 			panic(fmt.Errorf("unknown value type: %d", assignment.Value.Type))
@@ -1040,8 +1040,7 @@
 func (c *Context) processModuleDef(moduleDef *parser.Module,
 	relBlueprintsFile string) (*moduleInfo, []error) {
 
-	typeName := moduleDef.Type.Name
-	factory, ok := c.moduleFactories[typeName]
+	factory, ok := c.moduleFactories[moduleDef.Type]
 	if !ok {
 		if c.ignoreUnknownModuleTypes {
 			return nil, nil
@@ -1049,8 +1048,8 @@
 
 		return nil, []error{
 			&Error{
-				Err: fmt.Errorf("unrecognized module type %q", typeName),
-				Pos: moduleDef.Type.Pos,
+				Err: fmt.Errorf("unrecognized module type %q", moduleDef.Type),
+				Pos: moduleDef.TypePos,
 			},
 		}
 	}
@@ -1059,7 +1058,7 @@
 
 	module := &moduleInfo{
 		logicModule:       logicModule,
-		typeName:          typeName,
+		typeName:          moduleDef.Type,
 		relBlueprintsFile: relBlueprintsFile,
 	}
 
@@ -1074,10 +1073,10 @@
 		return nil, errs
 	}
 
-	module.pos = moduleDef.Type.Pos
+	module.pos = moduleDef.TypePos
 	module.propertyPos = make(map[string]scanner.Position)
 	for name, propertyDef := range propertyMap {
-		module.propertyPos[name] = propertyDef.Pos
+		module.propertyPos[name] = propertyDef.ColonPos
 	}
 
 	return module, nil
diff --git a/parser/ast.go b/parser/ast.go
index 387f6d5..19a79d7 100644
--- a/parser/ast.go
+++ b/parser/ast.go
@@ -20,8 +20,16 @@
 	"text/scanner"
 )
 
+type Node interface {
+	// Pos returns the position of the first token in the Expression
+	Pos() scanner.Position
+	// End returns the position of the beginning of the last token in the Expression
+	End() scanner.Position
+}
+
 // Definition is an Assignment or a Module at the top level of a Blueprints file
 type Definition interface {
+	Node
 	String() string
 	definitionTag()
 }
@@ -29,23 +37,28 @@
 // An Assignment is a variable assignment at the top level of a Blueprints file, scoped to the
 // file and and subdirs.
 type Assignment struct {
-	Name       Ident
+	Name       string
+	NamePos    scanner.Position
 	Value      Expression
 	OrigValue  Expression
-	Pos        scanner.Position
+	EqualsPos  scanner.Position
 	Assigner   string
 	Referenced bool
 }
 
 func (a *Assignment) String() string {
-	return fmt.Sprintf("%s@%s %s %s (%s) %t", a.Name, a.Pos, a.Assigner, a.Value, a.OrigValue, a.Referenced)
+	return fmt.Sprintf("%s@%s %s %s (%s) %t", a.Name, a.EqualsPos, a.Assigner, a.Value, a.OrigValue, a.Referenced)
 }
 
+func (a *Assignment) Pos() scanner.Position { return a.NamePos }
+func (a *Assignment) End() scanner.Position { return a.Value.End() }
+
 func (a *Assignment) definitionTag() {}
 
 // A Module is a module definition at the top level of a Blueprints file
 type Module struct {
-	Type Ident
+	Type    string
+	TypePos scanner.Position
 	Map
 }
 
@@ -70,11 +83,15 @@
 
 func (m *Module) definitionTag() {}
 
+func (m *Module) Pos() scanner.Position { return m.TypePos }
+func (m *Module) End() scanner.Position { return m.Map.End() }
+
 // A Property is a name: value pair within a Map, which may be a top level Module.
 type Property struct {
-	Name  Ident
-	Value Expression
-	Pos   scanner.Position
+	Name     string
+	NamePos  scanner.Position
+	ColonPos scanner.Position
+	Value    Expression
 }
 
 func (p *Property) Copy() *Property {
@@ -84,33 +101,22 @@
 }
 
 func (p *Property) String() string {
-	return fmt.Sprintf("%s@%s: %s", p.Name, p.Pos, p.Value)
+	return fmt.Sprintf("%s@%s: %s", p.Name, p.ColonPos, p.Value)
 }
 
-// An Ident is a name identifier, the Type of a Module, the Name of a Property, or the Name of a
-// Variable.
-type Ident struct {
-	Name string
-	Pos  scanner.Position
-}
-
-func (i Ident) String() string {
-	return fmt.Sprintf("%s@%s", i.Name, i.Pos)
-}
+func (p *Property) Pos() scanner.Position { return p.NamePos }
+func (p *Property) End() scanner.Position { return p.Value.End() }
 
 // An Expression is a Value in a Property or Assignment.  It can be a literal (String or Bool), a
 // Map, a List, an Operator that combines two expressions of the same type, or a Variable that
 // references and Assignment.
 type Expression interface {
+	Node
 	// Copy returns a copy of the Expression that will not affect the original if mutated
 	Copy() Expression
 	String() string
 	// Type returns the underlying Type enum of the Expression if it were to be evalutated
 	Type() Type
-	// Pos returns the position of the first token in the Expression
-	Pos() scanner.Position
-	// End returns the position of the beginning of the last token in the Expression
-	End() scanner.Position
 	// Eval returns an expression that is fully evaluated to a simple type (List, Map, String, or
 	// Bool).  It will return the same object for every call to Eval().
 	Eval() Expression
@@ -172,8 +178,8 @@
 
 type Variable struct {
 	Name    string
-	Value   Expression
 	NamePos scanner.Position
+	Value   Expression
 }
 
 func (x *Variable) Pos() scanner.Position { return x.NamePos }
@@ -310,6 +316,13 @@
 	return BoolType
 }
 
+type CommentGroup struct {
+	Comments []*Comment
+}
+
+func (x *CommentGroup) Pos() scanner.Position { return x.Comments[0].Pos() }
+func (x *CommentGroup) End() scanner.Position { return x.Comments[len(x.Comments)-1].End() }
+
 type Comment struct {
 	Comment []string
 	Slash   scanner.Position
diff --git a/parser/parser.go b/parser/parser.go
index 6909c50..de4e8e2 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -40,8 +40,23 @@
 type File struct {
 	Name     string
 	Defs     []Definition
-	Comments []Comment
-	Lines    []scanner.Position
+	Comments []*CommentGroup
+}
+
+func (f *File) Pos() scanner.Position {
+	return scanner.Position{
+		Filename: f.Name,
+		Line:     1,
+		Column:   1,
+		Offset:   0,
+	}
+}
+
+func (f *File) End() scanner.Position {
+	if len(f.Defs) > 0 {
+		return f.Defs[len(f.Defs)-1].End()
+	}
+	return noPos
 }
 
 func parse(p *parser) (file *File, errs []error) {
@@ -88,7 +103,7 @@
 	tok      rune
 	errors   []error
 	scope    *Scope
-	comments []Comment
+	comments []*CommentGroup
 	eval     bool
 }
 
@@ -139,10 +154,18 @@
 func (p *parser) next() {
 	if p.tok != scanner.EOF {
 		p.tok = p.scanner.Scan()
-		for p.tok == scanner.Comment {
-			lines := strings.Split(p.scanner.TokenText(), "\n")
-			p.comments = append(p.comments, Comment{lines, p.scanner.Position})
-			p.tok = p.scanner.Scan()
+		if p.tok == scanner.Comment {
+			var comments []*Comment
+			for p.tok == scanner.Comment {
+				lines := strings.Split(p.scanner.TokenText(), "\n")
+				if len(comments) > 0 && p.scanner.Position.Line > comments[len(comments)-1].End().Line+1 {
+					p.comments = append(p.comments, &CommentGroup{Comments: comments})
+					comments = nil
+				}
+				comments = append(comments, &Comment{lines, p.scanner.Position})
+				p.tok = p.scanner.Scan()
+			}
+			p.comments = append(p.comments, &CommentGroup{Comments: comments})
 		}
 	}
 	return
@@ -190,23 +213,23 @@
 	}
 	value := p.parseExpression()
 
-	assignment.Name = Ident{name, namePos}
+	assignment.Name = name
+	assignment.NamePos = namePos
 	assignment.Value = value
 	assignment.OrigValue = value
-	assignment.Pos = pos
+	assignment.EqualsPos = pos
 	assignment.Assigner = assigner
 
 	if p.scope != nil {
 		if assigner == "+=" {
-			if old, local := p.scope.Get(assignment.Name.Name); old == nil {
-				p.errorf("modified non-existent variable %q with +=", assignment.Name.Name)
+			if old, local := p.scope.Get(assignment.Name); old == nil {
+				p.errorf("modified non-existent variable %q with +=", assignment.Name)
 			} else if !local {
-				p.errorf("modified non-local variable %q with +=", assignment.Name.Name)
+				p.errorf("modified non-local variable %q with +=", assignment.Name)
 			} else if old.Referenced {
-				p.errorf("modified variable %q with += after referencing",
-					assignment.Name.Name)
+				p.errorf("modified variable %q with += after referencing", assignment.Name)
 			} else {
-				val, err := p.evaluateOperator(old.Value, assignment.Value, '+', assignment.Pos)
+				val, err := p.evaluateOperator(old.Value, assignment.Value, '+', assignment.EqualsPos)
 				if err != nil {
 					p.error(err)
 				} else {
@@ -244,7 +267,8 @@
 	}
 
 	return &Module{
-		Type: Ident{typ, typPos},
+		Type:    typ,
+		TypePos: typPos,
 		Map: Map{
 			Properties: properties,
 			LBracePos:  lbracePos,
@@ -293,9 +317,10 @@
 
 	value := p.parseExpression()
 
-	property.Name = Ident{name, namePos}
+	property.Name = name
+	property.NamePos = namePos
 	property.Value = value
-	property.Pos = pos
+	property.ColonPos = pos
 
 	return
 }
@@ -362,18 +387,18 @@
 	inBoth := make(map[string]*Property)
 
 	for _, prop1 := range map1 {
-		inMap1[prop1.Name.Name] = prop1
+		inMap1[prop1.Name] = prop1
 	}
 
 	for _, prop2 := range map2 {
-		inMap2[prop2.Name.Name] = prop2
-		if _, ok := inMap1[prop2.Name.Name]; ok {
-			inBoth[prop2.Name.Name] = prop2
+		inMap2[prop2.Name] = prop2
+		if _, ok := inMap1[prop2.Name]; ok {
+			inBoth[prop2.Name] = prop2
 		}
 	}
 
 	for _, prop1 := range map1 {
-		if prop2, ok := inBoth[prop1.Name.Name]; ok {
+		if prop2, ok := inBoth[prop1.Name]; ok {
 			var err error
 			newProp := *prop1
 			newProp.Value, err = p.evaluateOperator(prop1.Value, prop2.Value, '+', pos)
@@ -387,7 +412,7 @@
 	}
 
 	for _, prop2 := range map2 {
-		if _, ok := inBoth[prop2.Name.Name]; !ok {
+		if _, ok := inBoth[prop2.Name]; !ok {
 			ret = append(ret, prop2)
 		}
 	}
@@ -550,15 +575,15 @@
 }
 
 func (s *Scope) Add(assignment *Assignment) error {
-	if old, ok := s.vars[assignment.Name.Name]; ok {
+	if old, ok := s.vars[assignment.Name]; ok {
 		return fmt.Errorf("variable already set, previous assignment: %s", old)
 	}
 
-	if old, ok := s.inheritedVars[assignment.Name.Name]; ok {
+	if old, ok := s.inheritedVars[assignment.Name]; ok {
 		return fmt.Errorf("variable already set in inherited scope, previous assignment: %s", old)
 	}
 
-	s.vars[assignment.Name.Name] = assignment
+	s.vars[assignment.Name] = assignment
 
 	return nil
 }
diff --git a/parser/parser_test.go b/parser/parser_test.go
index e93bb09..bde67e5 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -32,14 +32,15 @@
 var validParseTestCases = []struct {
 	input    string
 	defs     []Definition
-	comments []Comment
+	comments []*CommentGroup
 }{
 	{`
 		foo {}
 		`,
 		[]Definition{
 			&Module{
-				Type: Ident{"foo", mkpos(3, 2, 3)},
+				Type:    "foo",
+				TypePos: mkpos(3, 2, 3),
 				Map: Map{
 					LBracePos: mkpos(7, 2, 7),
 					RBracePos: mkpos(8, 2, 8),
@@ -56,14 +57,16 @@
 		`,
 		[]Definition{
 			&Module{
-				Type: Ident{"foo", mkpos(3, 2, 3)},
+				Type:    "foo",
+				TypePos: mkpos(3, 2, 3),
 				Map: Map{
 					LBracePos: mkpos(7, 2, 7),
 					RBracePos: mkpos(27, 4, 3),
 					Properties: []*Property{
 						{
-							Name: Ident{"name", mkpos(12, 3, 4)},
-							Pos:  mkpos(16, 3, 8),
+							Name:     "name",
+							NamePos:  mkpos(12, 3, 4),
+							ColonPos: mkpos(16, 3, 8),
 							Value: &String{
 								LiteralPos: mkpos(18, 3, 10),
 								Value:      "abc",
@@ -83,14 +86,16 @@
 		`,
 		[]Definition{
 			&Module{
-				Type: Ident{"foo", mkpos(3, 2, 3)},
+				Type:    "foo",
+				TypePos: mkpos(3, 2, 3),
 				Map: Map{
 					LBracePos: mkpos(7, 2, 7),
 					RBracePos: mkpos(28, 4, 3),
 					Properties: []*Property{
 						{
-							Name: Ident{"isGood", mkpos(12, 3, 4)},
-							Pos:  mkpos(18, 3, 10),
+							Name:     "isGood",
+							NamePos:  mkpos(12, 3, 4),
+							ColonPos: mkpos(18, 3, 10),
 							Value: &Bool{
 								LiteralPos: mkpos(20, 3, 12),
 								Value:      true,
@@ -111,14 +116,16 @@
 		`,
 		[]Definition{
 			&Module{
-				Type: Ident{"foo", mkpos(3, 2, 3)},
+				Type:    "foo",
+				TypePos: mkpos(3, 2, 3),
 				Map: Map{
 					LBracePos: mkpos(7, 2, 7),
 					RBracePos: mkpos(67, 5, 3),
 					Properties: []*Property{
 						{
-							Name: Ident{"stuff", mkpos(12, 3, 4)},
-							Pos:  mkpos(17, 3, 9),
+							Name:     "stuff",
+							NamePos:  mkpos(12, 3, 4),
+							ColonPos: mkpos(17, 3, 9),
 							Value: &List{
 								LBracePos: mkpos(19, 3, 11),
 								RBracePos: mkpos(63, 4, 19),
@@ -163,29 +170,33 @@
 		`,
 		[]Definition{
 			&Module{
-				Type: Ident{"foo", mkpos(3, 2, 3)},
+				Type:    "foo",
+				TypePos: mkpos(3, 2, 3),
 				Map: Map{
 					LBracePos: mkpos(7, 2, 7),
 					RBracePos: mkpos(62, 7, 3),
 					Properties: []*Property{
 						{
-							Name: Ident{"stuff", mkpos(12, 3, 4)},
-							Pos:  mkpos(17, 3, 9),
+							Name:     "stuff",
+							NamePos:  mkpos(12, 3, 4),
+							ColonPos: mkpos(17, 3, 9),
 							Value: &Map{
 								LBracePos: mkpos(19, 3, 11),
 								RBracePos: mkpos(58, 6, 4),
 								Properties: []*Property{
 									{
-										Name: Ident{"isGood", mkpos(25, 4, 5)},
-										Pos:  mkpos(31, 4, 11),
+										Name:     "isGood",
+										NamePos:  mkpos(25, 4, 5),
+										ColonPos: mkpos(31, 4, 11),
 										Value: &Bool{
 											LiteralPos: mkpos(33, 4, 13),
 											Value:      true,
 										},
 									},
 									{
-										Name: Ident{"name", mkpos(43, 5, 5)},
-										Pos:  mkpos(47, 5, 9),
+										Name:     "name",
+										NamePos:  mkpos(43, 5, 5),
+										ColonPos: mkpos(47, 5, 9),
 										Value: &String{
 											LiteralPos: mkpos(49, 5, 11),
 											Value:      "bar",
@@ -210,14 +221,16 @@
 		`,
 		[]Definition{
 			&Module{
-				Type: Ident{"foo", mkpos(17, 3, 3)},
+				Type:    "foo",
+				TypePos: mkpos(17, 3, 3),
 				Map: Map{
 					LBracePos: mkpos(32, 3, 18),
 					RBracePos: mkpos(81, 6, 3),
 					Properties: []*Property{
 						{
-							Name: Ident{"isGood", mkpos(52, 5, 4)},
-							Pos:  mkpos(58, 5, 10),
+							Name:     "isGood",
+							NamePos:  mkpos(52, 5, 4),
+							ColonPos: mkpos(58, 5, 10),
 							Value: &Bool{
 								LiteralPos: mkpos(60, 5, 12),
 								Value:      true,
@@ -227,22 +240,38 @@
 				},
 			},
 		},
-		[]Comment{
-			Comment{
-				Comment: []string{"// comment1"},
-				Slash:   mkpos(3, 2, 3),
+		[]*CommentGroup{
+			{
+				Comments: []*Comment{
+					&Comment{
+						Comment: []string{"// comment1"},
+						Slash:   mkpos(3, 2, 3),
+					},
+				},
 			},
-			Comment{
-				Comment: []string{"/* test */"},
-				Slash:   mkpos(21, 3, 7),
+			{
+				Comments: []*Comment{
+					&Comment{
+						Comment: []string{"/* test */"},
+						Slash:   mkpos(21, 3, 7),
+					},
+				},
 			},
-			Comment{
-				Comment: []string{"// comment2"},
-				Slash:   mkpos(37, 4, 4),
+			{
+				Comments: []*Comment{
+					&Comment{
+						Comment: []string{"// comment2"},
+						Slash:   mkpos(37, 4, 4),
+					},
+				},
 			},
-			Comment{
-				Comment: []string{"// comment3"},
-				Slash:   mkpos(67, 5, 19),
+			{
+				Comments: []*Comment{
+					&Comment{
+						Comment: []string{"// comment3"},
+						Slash:   mkpos(67, 5, 19),
+					},
+				},
 			},
 		},
 	},
@@ -258,14 +287,16 @@
 		`,
 		[]Definition{
 			&Module{
-				Type: Ident{"foo", mkpos(3, 2, 3)},
+				Type:    "foo",
+				TypePos: mkpos(3, 2, 3),
 				Map: Map{
 					LBracePos: mkpos(7, 2, 7),
 					RBracePos: mkpos(27, 4, 3),
 					Properties: []*Property{
 						{
-							Name: Ident{"name", mkpos(12, 3, 4)},
-							Pos:  mkpos(16, 3, 8),
+							Name:     "name",
+							NamePos:  mkpos(12, 3, 4),
+							ColonPos: mkpos(16, 3, 8),
 							Value: &String{
 								LiteralPos: mkpos(18, 3, 10),
 								Value:      "abc",
@@ -275,14 +306,16 @@
 				},
 			},
 			&Module{
-				Type: Ident{"bar", mkpos(32, 6, 3)},
+				Type:    "bar",
+				TypePos: mkpos(32, 6, 3),
 				Map: Map{
 					LBracePos: mkpos(36, 6, 7),
 					RBracePos: mkpos(56, 8, 3),
 					Properties: []*Property{
 						{
-							Name: Ident{"name", mkpos(41, 7, 4)},
-							Pos:  mkpos(45, 7, 8),
+							Name:     "name",
+							NamePos:  mkpos(41, 7, 4),
+							ColonPos: mkpos(45, 7, 8),
 							Value: &String{
 								LiteralPos: mkpos(47, 7, 10),
 								Value:      "def",
@@ -303,8 +336,9 @@
 		`,
 		[]Definition{
 			&Assignment{
-				Name: Ident{"foo", mkpos(3, 2, 3)},
-				Pos:  mkpos(7, 2, 7),
+				Name:      "foo",
+				NamePos:   mkpos(3, 2, 3),
+				EqualsPos: mkpos(7, 2, 7),
 				Value: &String{
 					LiteralPos: mkpos(9, 2, 9),
 					Value:      "stuff",
@@ -317,8 +351,9 @@
 				Referenced: true,
 			},
 			&Assignment{
-				Name: Ident{"bar", mkpos(19, 3, 3)},
-				Pos:  mkpos(23, 3, 7),
+				Name:      "bar",
+				NamePos:   mkpos(19, 3, 3),
+				EqualsPos: mkpos(23, 3, 7),
 				Value: &Variable{
 					Name:    "foo",
 					NamePos: mkpos(25, 3, 9),
@@ -339,8 +374,9 @@
 				Referenced: true,
 			},
 			&Assignment{
-				Name: Ident{"baz", mkpos(31, 4, 3)},
-				Pos:  mkpos(35, 4, 7),
+				Name:      "baz",
+				NamePos:   mkpos(31, 4, 3),
+				EqualsPos: mkpos(35, 4, 7),
 				Value: &Operator{
 					OperatorPos: mkpos(41, 4, 13),
 					Operator:    '+',
@@ -405,8 +441,9 @@
 				Referenced: true,
 			},
 			&Assignment{
-				Name: Ident{"boo", mkpos(49, 5, 3)},
-				Pos:  mkpos(53, 5, 7),
+				Name:      "boo",
+				NamePos:   mkpos(49, 5, 3),
+				EqualsPos: mkpos(53, 5, 7),
 				Value: &Operator{
 					Args: [2]Expression{
 						&Variable{
@@ -496,8 +533,9 @@
 				Assigner: "=",
 			},
 			&Assignment{
-				Name: Ident{"boo", mkpos(61, 6, 3)},
-				Pos:  mkpos(66, 6, 8),
+				Name:      "boo",
+				NamePos:   mkpos(61, 6, 3),
+				EqualsPos: mkpos(66, 6, 8),
 				Value: &Variable{
 					Name:    "foo",
 					NamePos: mkpos(68, 6, 10),
@@ -519,6 +557,60 @@
 		},
 		nil,
 	},
+	{`
+		// comment1
+		// comment2
+
+		/* comment3
+		   comment4 */
+		// comment5
+
+		/* comment6 */ /* comment7 */ // comment8
+		`,
+		nil,
+		[]*CommentGroup{
+			{
+				Comments: []*Comment{
+					&Comment{
+						Comment: []string{"// comment1"},
+						Slash:   mkpos(3, 2, 3),
+					},
+					&Comment{
+						Comment: []string{"// comment2"},
+						Slash:   mkpos(17, 3, 3),
+					},
+				},
+			},
+			{
+				Comments: []*Comment{
+					&Comment{
+						Comment: []string{"/* comment3", "		   comment4 */"},
+						Slash: mkpos(32, 5, 3),
+					},
+					&Comment{
+						Comment: []string{"// comment5"},
+						Slash:   mkpos(63, 7, 3),
+					},
+				},
+			},
+			{
+				Comments: []*Comment{
+					&Comment{
+						Comment: []string{"/* comment6 */"},
+						Slash:   mkpos(78, 9, 3),
+					},
+					&Comment{
+						Comment: []string{"/* comment7 */"},
+						Slash:   mkpos(93, 9, 18),
+					},
+					&Comment{
+						Comment: []string{"// comment8"},
+						Slash:   mkpos(108, 9, 33),
+					},
+				},
+			},
+		},
+	},
 }
 
 func TestParseValidInput(t *testing.T) {
diff --git a/parser/printer.go b/parser/printer.go
index 1e7bc2a..943f930 100644
--- a/parser/printer.go
+++ b/parser/printer.go
@@ -26,7 +26,7 @@
 
 type printer struct {
 	defs     []Definition
-	comments []Comment
+	comments []*CommentGroup
 
 	curComment int
 
@@ -40,7 +40,7 @@
 	indentList []int
 	wsBuf      []byte
 
-	skippedComments []Comment
+	skippedComments *CommentGroup
 }
 
 func newPrinter(file *File) *printer {
@@ -87,16 +87,16 @@
 }
 
 func (p *printer) printAssignment(assignment *Assignment) {
-	p.printToken(assignment.Name.Name, assignment.Name.Pos)
+	p.printToken(assignment.Name, assignment.NamePos)
 	p.requestSpace()
-	p.printToken(assignment.Assigner, assignment.Pos)
+	p.printToken(assignment.Assigner, assignment.EqualsPos)
 	p.requestSpace()
 	p.printExpression(assignment.OrigValue)
 	p.requestNewline()
 }
 
 func (p *printer) printModule(module *Module) {
-	p.printToken(module.Type.Name, module.Type.Pos)
+	p.printToken(module.Type, module.TypePos)
 	p.printMap(&module.Map)
 	p.requestDoubleNewline()
 }
@@ -175,8 +175,8 @@
 }
 
 func (p *printer) printProperty(property *Property) {
-	p.printToken(property.Name.Name, property.Name.Pos)
-	p.printToken(":", property.Pos)
+	p.printToken(property.Name, property.NamePos)
+	p.printToken(":", property.ColonPos)
 	p.requestSpace()
 	p.printExpression(property.Value)
 }
@@ -206,12 +206,14 @@
 
 // Print any in-line (single line /* */) comments that appear _before_ pos
 func (p *printer) printInLineCommentsBefore(pos scanner.Position) {
-	for p.curComment < len(p.comments) && p.comments[p.curComment].Slash.Offset < pos.Offset {
+	for p.curComment < len(p.comments) && p.comments[p.curComment].Pos().Offset < pos.Offset {
 		c := p.comments[p.curComment]
-		if c.Comment[0][0:2] == "//" || len(c.Comment) > 1 {
-			p.skippedComments = append(p.skippedComments, c)
+		if c.Comments[0].Comment[0][0:2] == "//" || len(c.Comments[0].Comment) > 1 {
+			if p.skippedComments != nil {
+				panic("multiple skipped comments")
+			}
+			p.skippedComments = c
 		} else {
-			p.flushSpace()
 			p.printComment(c)
 			p.requestSpace()
 		}
@@ -222,19 +224,13 @@
 // Print any comments, including end of line comments, that appear _before_ the line specified
 // by pos
 func (p *printer) printEndOfLineCommentsBefore(pos scanner.Position) {
-	for _, c := range p.skippedComments {
-		if !p.requestNewlinesForPos(c.Slash) {
-			p.requestSpace()
-		}
-		p.printComment(c)
+	if p.skippedComments != nil {
+		p.printComment(p.skippedComments)
 		p._requestNewline()
+		p.skippedComments = nil
 	}
-	p.skippedComments = []Comment{}
-	for p.curComment < len(p.comments) && p.comments[p.curComment].Slash.Line < pos.Line {
+	for p.curComment < len(p.comments) && p.comments[p.curComment].Pos().Line < pos.Line {
 		c := p.comments[p.curComment]
-		if !p.requestNewlinesForPos(c.Slash) {
-			p.requestSpace()
-		}
 		p.printComment(c)
 		p._requestNewline()
 		p.curComment++
@@ -300,39 +296,38 @@
 }
 
 // Print a single comment, which may be a multi-line comment
-func (p *printer) printComment(comment Comment) {
-	pos := comment.Slash
-	for i, line := range comment.Comment {
-		line = strings.TrimRightFunc(line, unicode.IsSpace)
-		p.flushSpace()
-		if i != 0 {
-			lineIndent := strings.IndexFunc(line, func(r rune) bool { return !unicode.IsSpace(r) })
-			lineIndent = max(lineIndent, p.curIndent())
-			p.pad(lineIndent - p.curIndent())
-			pos.Line++
+func (p *printer) printComment(cg *CommentGroup) {
+	for _, comment := range cg.Comments {
+		if !p.requestNewlinesForPos(comment.Pos()) {
+			p.requestSpace()
 		}
-		p.output = append(p.output, strings.TrimSpace(line)...)
-		if i < len(comment.Comment)-1 {
-			p._requestNewline()
+		for i, line := range comment.Comment {
+			line = strings.TrimRightFunc(line, unicode.IsSpace)
+			p.flushSpace()
+			if i != 0 {
+				lineIndent := strings.IndexFunc(line, func(r rune) bool { return !unicode.IsSpace(r) })
+				lineIndent = max(lineIndent, p.curIndent())
+				p.pad(lineIndent - p.curIndent())
+			}
+			p.output = append(p.output, strings.TrimSpace(line)...)
+			if i < len(comment.Comment)-1 {
+				p._requestNewline()
+			}
 		}
+		p.pos = comment.End()
 	}
-	p.pos = pos
 }
 
 // Print any comments that occur after the last token, and a trailing newline
 func (p *printer) flush() {
-	for _, c := range p.skippedComments {
-		if !p.requestNewlinesForPos(c.Slash) {
+	if p.skippedComments != nil {
+		if !p.requestNewlinesForPos(p.skippedComments.Pos()) {
 			p.requestSpace()
 		}
-		p.printComment(c)
+		p.printComment(p.skippedComments)
 	}
 	for p.curComment < len(p.comments) {
-		c := p.comments[p.curComment]
-		if !p.requestNewlinesForPos(c.Slash) {
-			p.requestSpace()
-		}
-		p.printComment(c)
+		p.printComment(p.comments[p.curComment])
 		p.curComment++
 	}
 	p.output = append(p.output, '\n')
diff --git a/parser/printer_test.go b/parser/printer_test.go
index 3e759cb..6c3e49c 100644
--- a/parser/printer_test.go
+++ b/parser/printer_test.go
@@ -205,8 +205,8 @@
     deps: ["libabc"],
     incs: [],
 } //test
-
 //test
+
 test2 {
 }
 
@@ -253,7 +253,7 @@
 }
 
 // This
-/* Is */
+/* Is *//* A */ // A
 // A
 
 // Multiline
@@ -279,7 +279,7 @@
 }
 
 // This
-/* Is */
+/* Is */ /* A */ // A
 // A
 
 // Multiline
diff --git a/parser/sort.go b/parser/sort.go
index 05ce5fd..c8bf74f 100644
--- a/parser/sort.go
+++ b/parser/sort.go
@@ -107,7 +107,16 @@
 	sort.Sort(l)
 
 	copyValues := append([]Expression{}, values...)
-	copyComments := append([]Comment{}, file.Comments...)
+	copyComments := make([]*CommentGroup, len(file.Comments))
+	for i := range file.Comments {
+		cg := *file.Comments[i]
+		cg.Comments = make([]*Comment, len(cg.Comments))
+		for j := range file.Comments[i].Comments {
+			c := *file.Comments[i].Comments[j]
+			cg.Comments[j] = &c
+		}
+		copyComments[i] = &cg
+	}
 
 	curPos := values[0].Pos()
 	for i, e := range l {
@@ -115,8 +124,8 @@
 		values[i].(*String).LiteralPos = curPos
 		for j, c := range copyComments {
 			if c.Pos().Offset > e.pos.Offset && c.Pos().Offset < e.nextPos.Offset {
-				file.Comments[j].Slash.Line = curPos.Line
-				file.Comments[j].Slash.Offset += values[i].Pos().Offset - e.pos.Offset
+				file.Comments[j].Comments[0].Slash.Line = curPos.Line
+				file.Comments[j].Comments[0].Slash.Offset += values[i].Pos().Offset - e.pos.Offset
 			}
 		}
 
@@ -162,7 +171,7 @@
 	return l[i].s < l[j].s
 }
 
-type commentsByOffset []Comment
+type commentsByOffset []*CommentGroup
 
 func (l commentsByOffset) Len() int {
 	return len(l)
diff --git a/unpack.go b/unpack.go
index 11718b4..f7e0a2d 100644
--- a/unpack.go
+++ b/unpack.go
@@ -65,7 +65,7 @@
 		if !packedProperty.unpacked {
 			err := &Error{
 				Err: fmt.Errorf("unrecognized property %q", name),
-				Pos: packedProperty.property.Pos,
+				Pos: packedProperty.property.ColonPos,
 			}
 			errs = append(errs, err)
 		}
@@ -82,7 +82,7 @@
 	propertyMap map[string]*packedProperty) (errs []error) {
 
 	for _, propertyDef := range propertyDefs {
-		name := namePrefix + propertyDef.Name.Name
+		name := namePrefix + propertyDef.Name
 		if first, present := propertyMap[name]; present {
 			if first.property == propertyDef {
 				// We've already added this property.
@@ -90,11 +90,11 @@
 			}
 			errs = append(errs, &Error{
 				Err: fmt.Errorf("property %q already defined", name),
-				Pos: propertyDef.Pos,
+				Pos: propertyDef.ColonPos,
 			})
 			errs = append(errs, &Error{
 				Err: fmt.Errorf("<-- previous definition here"),
-				Pos: first.property.Pos,
+				Pos: first.property.ColonPos,
 			})
 			if len(errs) >= maxErrors {
 				return errs
@@ -200,7 +200,7 @@
 			errs = append(errs,
 				&Error{
 					Err: fmt.Errorf("mutated field %s cannot be set in a Blueprint file", propertyName),
-					Pos: packedProperty.property.Pos,
+					Pos: packedProperty.property.ColonPos,
 				})
 			if len(errs) >= maxErrors {
 				return errs
@@ -212,7 +212,7 @@
 			errs = append(errs,
 				&Error{
 					Err: fmt.Errorf("filtered field %s cannot be set in a Blueprint file", propertyName),
-					Pos: packedProperty.property.Pos,
+					Pos: packedProperty.property.ColonPos,
 				})
 			if len(errs) >= maxErrors {
 				return errs