Snap for 10399941 from 797fe25cb2f2cfcb712f8de314361d662c4b8c3c to sdk-release

Change-Id: Iffa8ae7c1efbf2d29737d518d834f0a7727a5529
diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go
index bf12cd3..ead22d0 100644
--- a/bootstrap/bootstrap.go
+++ b/bootstrap/bootstrap.go
@@ -155,7 +155,7 @@
 }
 
 func pluginDeps(ctx blueprint.BottomUpMutatorContext) {
-	if pkg, ok := ctx.Module().(*goPackage); ok {
+	if pkg, ok := ctx.Module().(*GoPackage); ok {
 		if ctx.PrimaryModule() == ctx.Module() {
 			for _, plugin := range pkg.properties.PluginFor {
 				ctx.AddReverseDependency(ctx.Module(), nil, plugin)
@@ -190,18 +190,18 @@
 }
 
 func IsBootstrapModule(module blueprint.Module) bool {
-	_, isPackage := module.(*goPackage)
-	_, isBinary := module.(*goBinary)
+	_, isPackage := module.(*GoPackage)
+	_, isBinary := module.(*GoBinary)
 	return isPackage || isBinary
 }
 
 func isBootstrapBinaryModule(module blueprint.Module) bool {
-	_, isBinary := module.(*goBinary)
+	_, isBinary := module.(*GoBinary)
 	return isBinary
 }
 
-// A goPackage is a module for building Go packages.
-type goPackage struct {
+// A GoPackage is a module for building Go packages.
+type GoPackage struct {
 	blueprint.SimpleName
 	properties struct {
 		Deps      []string
@@ -231,39 +231,39 @@
 	testResultFile []string
 }
 
-var _ goPackageProducer = (*goPackage)(nil)
+var _ goPackageProducer = (*GoPackage)(nil)
 
 func newGoPackageModuleFactory() func() (blueprint.Module, []interface{}) {
 	return func() (blueprint.Module, []interface{}) {
-		module := &goPackage{}
+		module := &GoPackage{}
 		return module, []interface{}{&module.properties, &module.SimpleName.Properties}
 	}
 }
 
-func (g *goPackage) DynamicDependencies(ctx blueprint.DynamicDependerModuleContext) []string {
+func (g *GoPackage) DynamicDependencies(ctx blueprint.DynamicDependerModuleContext) []string {
 	if ctx.Module() != ctx.PrimaryModule() {
 		return nil
 	}
 	return g.properties.Deps
 }
 
-func (g *goPackage) GoPkgPath() string {
+func (g *GoPackage) GoPkgPath() string {
 	return g.properties.PkgPath
 }
 
-func (g *goPackage) GoPkgRoot() string {
+func (g *GoPackage) GoPkgRoot() string {
 	return g.pkgRoot
 }
 
-func (g *goPackage) GoPackageTarget() string {
+func (g *GoPackage) GoPackageTarget() string {
 	return g.archiveFile
 }
 
-func (g *goPackage) GoTestTargets() []string {
+func (g *GoPackage) GoTestTargets() []string {
 	return g.testResultFile
 }
 
-func (g *goPackage) IsPluginFor(name string) bool {
+func (g *GoPackage) IsPluginFor(name string) bool {
 	for _, plugin := range g.properties.PluginFor {
 		if plugin == name {
 			return true
@@ -272,11 +272,11 @@
 	return false
 }
 
-func (g *goPackage) GenerateBuildActions(ctx blueprint.ModuleContext) {
+func (g *GoPackage) GenerateBuildActions(ctx blueprint.ModuleContext) {
 	// Allow the primary builder to create multiple variants.  Any variants after the first
 	// will copy outputs from the first.
 	if ctx.Module() != ctx.PrimaryModule() {
-		primary := ctx.PrimaryModule().(*goPackage)
+		primary := ctx.PrimaryModule().(*GoPackage)
 		g.pkgRoot = primary.pkgRoot
 		g.archiveFile = primary.archiveFile
 		g.testResultFile = primary.testResultFile
@@ -340,8 +340,36 @@
 		srcs, genSrcs)
 }
 
-// A goBinary is a module for building executable binaries from Go sources.
-type goBinary struct {
+func (g *GoPackage) Srcs() []string {
+	return g.properties.Srcs
+}
+
+func (g *GoPackage) LinuxSrcs() []string {
+	return g.properties.Linux.Srcs
+}
+
+func (g *GoPackage) DarwinSrcs() []string {
+	return g.properties.Darwin.Srcs
+}
+
+func (g *GoPackage) TestSrcs() []string {
+	return g.properties.TestSrcs
+}
+
+func (g *GoPackage) LinuxTestSrcs() []string {
+	return g.properties.Linux.TestSrcs
+}
+
+func (g *GoPackage) DarwinTestSrcs() []string {
+	return g.properties.Darwin.TestSrcs
+}
+
+func (g *GoPackage) Deps() []string {
+	return g.properties.Deps
+}
+
+// A GoBinary is a module for building executable binaries from Go sources.
+type GoBinary struct {
 	blueprint.SimpleName
 	properties struct {
 		Deps           []string
@@ -363,32 +391,60 @@
 	installPath string
 }
 
-var _ GoBinaryTool = (*goBinary)(nil)
+var _ GoBinaryTool = (*GoBinary)(nil)
 
 func newGoBinaryModuleFactory() func() (blueprint.Module, []interface{}) {
 	return func() (blueprint.Module, []interface{}) {
-		module := &goBinary{}
+		module := &GoBinary{}
 		return module, []interface{}{&module.properties, &module.SimpleName.Properties}
 	}
 }
 
-func (g *goBinary) DynamicDependencies(ctx blueprint.DynamicDependerModuleContext) []string {
+func (g *GoBinary) DynamicDependencies(ctx blueprint.DynamicDependerModuleContext) []string {
 	if ctx.Module() != ctx.PrimaryModule() {
 		return nil
 	}
 	return g.properties.Deps
 }
 
-func (g *goBinary) isGoBinary() {}
-func (g *goBinary) InstallPath() string {
+func (g *GoBinary) isGoBinary() {}
+func (g *GoBinary) InstallPath() string {
 	return g.installPath
 }
 
-func (g *goBinary) GenerateBuildActions(ctx blueprint.ModuleContext) {
+func (g *GoBinary) Srcs() []string {
+	return g.properties.Srcs
+}
+
+func (g *GoBinary) LinuxSrcs() []string {
+	return g.properties.Linux.Srcs
+}
+
+func (g *GoBinary) DarwinSrcs() []string {
+	return g.properties.Darwin.Srcs
+}
+
+func (g *GoBinary) TestSrcs() []string {
+	return g.properties.TestSrcs
+}
+
+func (g *GoBinary) LinuxTestSrcs() []string {
+	return g.properties.Linux.TestSrcs
+}
+
+func (g *GoBinary) DarwinTestSrcs() []string {
+	return g.properties.Darwin.TestSrcs
+}
+
+func (g *GoBinary) Deps() []string {
+	return g.properties.Deps
+}
+
+func (g *GoBinary) GenerateBuildActions(ctx blueprint.ModuleContext) {
 	// Allow the primary builder to create multiple variants.  Any variants after the first
 	// will copy outputs from the first.
 	if ctx.Module() != ctx.PrimaryModule() {
-		primary := ctx.PrimaryModule().(*goBinary)
+		primary := ctx.PrimaryModule().(*GoBinary)
 		g.installPath = primary.installPath
 		return
 	}
@@ -619,7 +675,7 @@
 	// Find the module that's marked as the "primary builder", which means it's
 	// creating the binary that we'll use to generate the non-bootstrap
 	// build.ninja file.
-	var primaryBuilders []*goBinary
+	var primaryBuilders []*GoBinary
 	// blueprintTools contains blueprint go binaries that will be built in StageMain
 	var blueprintTools []string
 	// blueprintGoPackages contains all blueprint go packages that can be built in StageMain
@@ -627,14 +683,14 @@
 	ctx.VisitAllModulesIf(IsBootstrapModule,
 		func(module blueprint.Module) {
 			if ctx.PrimaryModule(module) == module {
-				if binaryModule, ok := module.(*goBinary); ok {
+				if binaryModule, ok := module.(*GoBinary); ok {
 					blueprintTools = append(blueprintTools, binaryModule.InstallPath())
 					if binaryModule.properties.PrimaryBuilder {
 						primaryBuilders = append(primaryBuilders, binaryModule)
 					}
 				}
 
-				if packageModule, ok := module.(*goPackage); ok {
+				if packageModule, ok := module.(*GoPackage); ok {
 					blueprintGoPackages = append(blueprintGoPackages,
 						packageModule.GoPackageTarget())
 					blueprintGoPackages = append(blueprintGoPackages,
diff --git a/bootstrap/command.go b/bootstrap/command.go
index 08a0e43..08b6a84 100644
--- a/bootstrap/command.go
+++ b/bootstrap/command.go
@@ -16,6 +16,7 @@
 
 import (
 	"bufio"
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -40,11 +41,17 @@
 	TraceFile  string
 }
 
+// RegisterGoModuleTypes adds module types to build tools written in golang
+func RegisterGoModuleTypes(ctx *blueprint.Context) {
+	ctx.RegisterModuleType("bootstrap_go_package", newGoPackageModuleFactory())
+	ctx.RegisterModuleType("blueprint_go_binary", newGoBinaryModuleFactory())
+}
+
 // RunBlueprint emits `args.OutFile` (a Ninja file) and returns the list of
 // its dependencies. These can be written to a `${args.OutFile}.d` file
 // so that it is correctly rebuilt when needed in case Blueprint is itself
 // invoked from Ninja
-func RunBlueprint(args Args, stopBefore StopBefore, ctx *blueprint.Context, config interface{}) []string {
+func RunBlueprint(args Args, stopBefore StopBefore, ctx *blueprint.Context, config interface{}) ([]string, error) {
 	runtime.GOMAXPROCS(runtime.NumCPU())
 
 	if args.NoGC {
@@ -54,7 +61,7 @@
 	if args.Cpuprofile != "" {
 		f, err := os.Create(joinPath(ctx.SrcDir(), args.Cpuprofile))
 		if err != nil {
-			fatalf("error opening cpuprofile: %s", err)
+			return nil, fmt.Errorf("error opening cpuprofile: %s", err)
 		}
 		pprof.StartCPUProfile(f)
 		defer f.Close()
@@ -64,7 +71,7 @@
 	if args.TraceFile != "" {
 		f, err := os.Create(joinPath(ctx.SrcDir(), args.TraceFile))
 		if err != nil {
-			fatalf("error opening trace: %s", err)
+			return nil, fmt.Errorf("error opening trace: %s", err)
 		}
 		trace.Start(f)
 		defer f.Close()
@@ -72,7 +79,7 @@
 	}
 
 	if args.ModuleListFile == "" {
-		fatalf("-l <moduleListFile> is required and must be nonempty")
+		return nil, fmt.Errorf("-l <moduleListFile> is required and must be nonempty")
 	}
 	ctx.SetModuleListFile(args.ModuleListFile)
 
@@ -82,50 +89,49 @@
 	ctx.BeginEvent("list_modules")
 	var filesToParse []string
 	if f, err := ctx.ListModulePaths("."); err != nil {
-		fatalf("could not enumerate files: %v\n", err.Error())
+		return nil, fmt.Errorf("could not enumerate files: %v\n", err.Error())
 	} else {
 		filesToParse = f
 	}
 	ctx.EndEvent("list_modules")
 
 	ctx.RegisterBottomUpMutator("bootstrap_plugin_deps", pluginDeps)
-	ctx.RegisterModuleType("bootstrap_go_package", newGoPackageModuleFactory())
-	ctx.RegisterModuleType("blueprint_go_binary", newGoBinaryModuleFactory())
 	ctx.RegisterSingletonType("bootstrap", newSingletonFactory(), false)
+	RegisterGoModuleTypes(ctx)
 	blueprint.RegisterPackageIncludesModuleType(ctx)
 
 	ctx.BeginEvent("parse_bp")
 	if blueprintFiles, errs := ctx.ParseFileList(".", filesToParse, config); len(errs) > 0 {
-		fatalErrors(errs)
+		return nil, fatalErrors(errs)
 	} else {
 		ctx.EndEvent("parse_bp")
 		ninjaDeps = append(ninjaDeps, blueprintFiles...)
 	}
 
 	if resolvedDeps, errs := ctx.ResolveDependencies(config); len(errs) > 0 {
-		fatalErrors(errs)
+		return nil, fatalErrors(errs)
 	} else {
 		ninjaDeps = append(ninjaDeps, resolvedDeps...)
 	}
 
 	if stopBefore == StopBeforePrepareBuildActions {
-		return ninjaDeps
+		return ninjaDeps, nil
 	}
 
 	if ctx.BeforePrepareBuildActionsHook != nil {
 		if err := ctx.BeforePrepareBuildActionsHook(); err != nil {
-			fatalErrors([]error{err})
+			return nil, fatalErrors([]error{err})
 		}
 	}
 
 	if buildActionsDeps, errs := ctx.PrepareBuildActions(config); len(errs) > 0 {
-		fatalErrors(errs)
+		return nil, fatalErrors(errs)
 	} else {
 		ninjaDeps = append(ninjaDeps, buildActionsDeps...)
 	}
 
 	if stopBefore == StopBeforeWriteNinja {
-		return ninjaDeps
+		return ninjaDeps, nil
 	}
 
 	const outFilePermissions = 0666
@@ -137,13 +143,13 @@
 	defer ctx.EndEvent("write_files")
 	if args.EmptyNinjaFile {
 		if err := os.WriteFile(joinPath(ctx.SrcDir(), args.OutFile), []byte(nil), outFilePermissions); err != nil {
-			fatalf("error writing empty Ninja file: %s", err)
+			return nil, fmt.Errorf("error writing empty Ninja file: %s", err)
 		}
 		out = io.Discard.(io.StringWriter)
 	} else {
 		f, err := os.OpenFile(joinPath(ctx.SrcDir(), args.OutFile), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, outFilePermissions)
 		if err != nil {
-			fatalf("error opening Ninja file: %s", err)
+			return nil, fmt.Errorf("error opening Ninja file: %s", err)
 		}
 		defer f.Close()
 		buf = bufio.NewWriterSize(f, 16*1024*1024)
@@ -151,40 +157,34 @@
 	}
 
 	if err := ctx.WriteBuildFile(out); err != nil {
-		fatalf("error writing Ninja file contents: %s", err)
+		return nil, fmt.Errorf("error writing Ninja file contents: %s", err)
 	}
 
 	if buf != nil {
 		if err := buf.Flush(); err != nil {
-			fatalf("error flushing Ninja file contents: %s", err)
+			return nil, fmt.Errorf("error flushing Ninja file contents: %s", err)
 		}
 	}
 
 	if f != nil {
 		if err := f.Close(); err != nil {
-			fatalf("error closing Ninja file: %s", err)
+			return nil, fmt.Errorf("error closing Ninja file: %s", err)
 		}
 	}
 
 	if args.Memprofile != "" {
 		f, err := os.Create(joinPath(ctx.SrcDir(), args.Memprofile))
 		if err != nil {
-			fatalf("error opening memprofile: %s", err)
+			return nil, fmt.Errorf("error opening memprofile: %s", err)
 		}
 		defer f.Close()
 		pprof.WriteHeapProfile(f)
 	}
 
-	return ninjaDeps
+	return ninjaDeps, nil
 }
 
-func fatalf(format string, args ...interface{}) {
-	fmt.Printf(format, args...)
-	fmt.Print("\n")
-	os.Exit(1)
-}
-
-func fatalErrors(errs []error) {
+func fatalErrors(errs []error) error {
 	red := "\x1b[31m"
 	unred := "\x1b[0m"
 
@@ -198,7 +198,8 @@
 			fmt.Printf("%sinternal error:%s %s\n", red, unred, err)
 		}
 	}
-	os.Exit(1)
+
+	return errors.New("fatal errors encountered")
 }
 
 func joinPath(base, path string) string {
diff --git a/bootstrap/glob.go b/bootstrap/glob.go
index ef68090..b2c3a2d 100644
--- a/bootstrap/glob.go
+++ b/bootstrap/glob.go
@@ -210,18 +210,21 @@
 // Writes a .ninja file that contains instructions for regenerating the glob
 // files that contain the results of every glob that was run. The list of files
 // is available as the result of GlobFileListFiles().
-func WriteBuildGlobsNinjaFile(glob *GlobSingleton, config interface{}) {
+func WriteBuildGlobsNinjaFile(glob *GlobSingleton, config interface{}) error {
 	buffer, errs := generateGlobNinjaFile(glob, config)
 	if len(errs) > 0 {
-		fatalErrors(errs)
+		return fatalErrors(errs)
 	}
 
 	const outFilePermissions = 0666
 	err := ioutil.WriteFile(joinPath(glob.SrcDir, glob.GlobFile), buffer, outFilePermissions)
 	if err != nil {
-		fatalf("error writing %s: %s", glob.GlobFile, err)
+		return fmt.Errorf("error writing %s: %s", glob.GlobFile, err)
 	}
+
+	return nil
 }
+
 func generateGlobNinjaFile(glob *GlobSingleton, config interface{}) ([]byte, []error) {
 
 	ctx := blueprint.NewContext()
diff --git a/bootstrap/writedocs.go b/bootstrap/writedocs.go
index f7314f7..d172f70 100644
--- a/bootstrap/writedocs.go
+++ b/bootstrap/writedocs.go
@@ -16,16 +16,16 @@
 	// Find the module that's marked as the "primary builder", which means it's
 	// creating the binary that we'll use to generate the non-bootstrap
 	// build.ninja file.
-	var primaryBuilders []*goBinary
+	var primaryBuilders []*GoBinary
 	ctx.VisitAllModulesIf(isBootstrapBinaryModule,
 		func(module blueprint.Module) {
-			binaryModule := module.(*goBinary)
+			binaryModule := module.(*GoBinary)
 			if binaryModule.properties.PrimaryBuilder {
 				primaryBuilders = append(primaryBuilders, binaryModule)
 			}
 		})
 
-	var primaryBuilder *goBinary
+	var primaryBuilder *GoBinary
 	switch len(primaryBuilders) {
 	case 0:
 		return nil, fmt.Errorf("no primary builder module present")
@@ -40,7 +40,7 @@
 	pkgFiles := make(map[string][]string)
 	ctx.VisitDepsDepthFirst(primaryBuilder, func(module blueprint.Module) {
 		switch m := module.(type) {
-		case (*goPackage):
+		case (*GoPackage):
 			pkgFiles[m.properties.PkgPath] = pathtools.PrefixPaths(m.properties.Srcs,
 				filepath.Join(ctx.SrcDir(), ctx.ModuleDir(m)))
 		default:
diff --git a/context.go b/context.go
index ae35dac..75ea2d6 100644
--- a/context.go
+++ b/context.go
@@ -103,15 +103,15 @@
 	// set during PrepareBuildActions
 	pkgNames        map[*packageContext]string
 	liveGlobals     *liveTracker
-	globalVariables map[Variable]ninjaString
+	globalVariables map[Variable]*ninjaString
 	globalPools     map[Pool]*poolDef
 	globalRules     map[Rule]*ruleDef
 
 	// set during PrepareBuildActions
-	outDir             ninjaString // The builddir special Ninja variable
-	requiredNinjaMajor int         // For the ninja_required_version variable
-	requiredNinjaMinor int         // For the ninja_required_version variable
-	requiredNinjaMicro int         // For the ninja_required_version variable
+	outDir             *ninjaString // The builddir special Ninja variable
+	requiredNinjaMajor int          // For the ninja_required_version variable
+	requiredNinjaMinor int          // For the ninja_required_version variable
+	requiredNinjaMicro int          // For the ninja_required_version variable
 
 	subninjas []string
 
@@ -137,8 +137,12 @@
 	// True for any mutators that have already run over all modules
 	finishedMutators map[*mutatorInfo]bool
 
-	// Can be set by tests to avoid invalidating Module values after mutators.
-	skipCloneModulesAfterMutators bool
+	// If true, RunBlueprint will skip cloning modules at the end of RunBlueprint.
+	// Cloning modules intentionally invalidates some Module values after
+	// mutators run (to ensure that mutators don't set such Module values in a way
+	// which ruins the integrity of the graph). However, keeping Module values
+	// changed by mutators may be a desirable outcome (such as for tooling or tests).
+	SkipCloneModulesAfterMutators bool
 
 	// String values that can be used to gate build graph traversal
 	includeTags *IncludeTags
@@ -1983,9 +1987,11 @@
 		}
 		deps = append(deps, mutatorDeps...)
 
-		if !c.skipCloneModulesAfterMutators {
+		c.BeginEvent("clone_modules")
+		if !c.SkipCloneModulesAfterMutators {
 			c.cloneModules()
 		}
+		defer c.EndEvent("clone_modules")
 
 		c.dependenciesReady = true
 	})
@@ -2734,6 +2740,7 @@
 type JSONAction struct {
 	Inputs  []string
 	Outputs []string
+	Desc    string
 }
 
 // JSONActionSupplier allows JSON representation of additional actions that are not registered in
@@ -2778,14 +2785,18 @@
 	}
 	var actions []JSONAction
 	for _, bDef := range m.actionDefs.buildDefs {
-		actions = append(actions, JSONAction{
+		a := JSONAction{
 			Inputs: append(
 				getNinjaStringsWithNilPkgNames(bDef.Inputs),
 				getNinjaStringsWithNilPkgNames(bDef.Implicits)...),
 			Outputs: append(
 				getNinjaStringsWithNilPkgNames(bDef.Outputs),
 				getNinjaStringsWithNilPkgNames(bDef.ImplicitOutputs)...),
-		})
+		}
+		if d, ok := bDef.Variables["description"]; ok {
+			a.Desc = d.Value(nil)
+		}
+		actions = append(actions, a)
 	}
 
 	if j, ok := m.logicModule.(JSONActionSupplier); ok {
@@ -2803,7 +2814,7 @@
 
 // Gets a list of strings from the given list of ninjaStrings by invoking ninjaString.Value with
 // nil pkgNames on each of the input ninjaStrings.
-func getNinjaStringsWithNilPkgNames(nStrs []ninjaString) []string {
+func getNinjaStringsWithNilPkgNames(nStrs []*ninjaString) []string {
 	var strs []string
 	for _, nstr := range nStrs {
 		strs = append(strs, nstr.Value(nil))
@@ -3809,7 +3820,7 @@
 	}
 }
 
-func (c *Context) setOutDir(value ninjaString) {
+func (c *Context) setOutDir(value *ninjaString) {
 	if c.outDir == nil {
 		c.outDir = value
 	}
@@ -3890,7 +3901,7 @@
 }
 
 func (c *Context) checkForVariableReferenceCycles(
-	variables map[Variable]ninjaString, pkgNames map[*packageContext]string) {
+	variables map[Variable]*ninjaString, pkgNames map[*packageContext]string) {
 
 	visited := make(map[Variable]bool)  // variables that were already checked
 	checking := make(map[Variable]bool) // variables actively being checked
@@ -4685,7 +4696,7 @@
 // keyForPhonyCandidate gives a unique identifier for a set of deps.
 // If any of the deps use a variable, we return an empty string to signal
 // that this set of deps is ineligible for extraction.
-func keyForPhonyCandidate(deps []ninjaString) string {
+func keyForPhonyCandidate(deps []*ninjaString) string {
 	hasher := sha256.New()
 	for _, d := range deps {
 		if len(d.Variables()) != 0 {
@@ -4716,7 +4727,7 @@
 			phonyCount.Add(1)
 			m.phony = &buildDef{
 				Rule:     Phony,
-				Outputs:  []ninjaString{simpleNinjaString("dedup-" + key)},
+				Outputs:  []*ninjaString{simpleNinjaString("dedup-" + key)},
 				Inputs:   m.first.OrderOnly, //we could also use b.OrderOnly
 				Optional: true,
 			}
diff --git a/context_test.go b/context_test.go
index 1a1fb0d..a10f681 100644
--- a/context_test.go
+++ b/context_test.go
@@ -1159,10 +1159,10 @@
 }
 
 func TestDeduplicateOrderOnlyDeps(t *testing.T) {
-	outputs := func(names ...string) []ninjaString {
-		r := make([]ninjaString, len(names))
+	outputs := func(names ...string) []*ninjaString {
+		r := make([]*ninjaString, len(names))
 		for i, name := range names {
-			r[i] = literalNinjaString(name)
+			r[i] = simpleNinjaString(name)
 		}
 		return r
 	}
@@ -1179,7 +1179,7 @@
 	type testcase struct {
 		modules        []*moduleInfo
 		expectedPhonys []*buildDef
-		conversions    map[string][]ninjaString
+		conversions    map[string][]*ninjaString
 	}
 	testCases := []testcase{{
 		modules: []*moduleInfo{
@@ -1189,7 +1189,7 @@
 		expectedPhonys: []*buildDef{
 			b("dedup-GKw-c0PwFokMUQ6T-TUmEWnZ4_VlQ2Qpgw-vCTT0-OQ", []string{"d"}, nil),
 		},
-		conversions: map[string][]ninjaString{
+		conversions: map[string][]*ninjaString{
 			"A": outputs("dedup-GKw-c0PwFokMUQ6T-TUmEWnZ4_VlQ2Qpgw-vCTT0-OQ"),
 			"B": outputs("dedup-GKw-c0PwFokMUQ6T-TUmEWnZ4_VlQ2Qpgw-vCTT0-OQ"),
 		},
@@ -1205,7 +1205,7 @@
 			m(b("C", nil, []string{"a"})),
 		},
 		expectedPhonys: []*buildDef{b("dedup-ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs", []string{"a"}, nil)},
-		conversions: map[string][]ninjaString{
+		conversions: map[string][]*ninjaString{
 			"A": outputs("dedup-ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs"),
 			"B": outputs("b"),
 			"C": outputs("dedup-ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs"),
@@ -1220,7 +1220,7 @@
 		expectedPhonys: []*buildDef{
 			b("dedup--44g_C5MPySMYMOb1lLzwTRymLuXe4tNWQO4UFViBgM", []string{"a", "b"}, nil),
 			b("dedup-9F3lHN7zCZFVHkHogt17VAR5lkigoAdT9E_JZuYVP8E", []string{"a", "c"}, nil)},
-		conversions: map[string][]ninjaString{
+		conversions: map[string][]*ninjaString{
 			"A": outputs("dedup--44g_C5MPySMYMOb1lLzwTRymLuXe4tNWQO4UFViBgM"),
 			"B": outputs("dedup--44g_C5MPySMYMOb1lLzwTRymLuXe4tNWQO4UFViBgM"),
 			"C": outputs("dedup-9F3lHN7zCZFVHkHogt17VAR5lkigoAdT9E_JZuYVP8E"),
diff --git a/live_tracker.go b/live_tracker.go
index 9d4f599..e2c4057 100644
--- a/live_tracker.go
+++ b/live_tracker.go
@@ -25,7 +25,7 @@
 	config interface{} // Used to evaluate variable, rule, and pool values.
 	ctx    *Context    // Used to evaluate globs
 
-	variables map[Variable]ninjaString
+	variables map[Variable]*ninjaString
 	pools     map[Pool]*poolDef
 	rules     map[Rule]*ruleDef
 }
@@ -34,7 +34,7 @@
 	return &liveTracker{
 		ctx:       ctx,
 		config:    config,
-		variables: make(map[Variable]ninjaString),
+		variables: make(map[Variable]*ninjaString),
 		pools:     make(map[Pool]*poolDef),
 		rules:     make(map[Rule]*ruleDef),
 	}
@@ -197,13 +197,13 @@
 	return nil
 }
 
-func (l *liveTracker) addNinjaStringListDeps(list []ninjaString) error {
+func (l *liveTracker) addNinjaStringListDeps(list []*ninjaString) error {
 	l.Lock()
 	defer l.Unlock()
 	return l.innerAddNinjaStringListDeps(list)
 }
 
-func (l *liveTracker) innerAddNinjaStringListDeps(list []ninjaString) error {
+func (l *liveTracker) innerAddNinjaStringListDeps(list []*ninjaString) error {
 	for _, str := range list {
 		err := l.innerAddNinjaStringDeps(str)
 		if err != nil {
@@ -213,13 +213,13 @@
 	return nil
 }
 
-func (l *liveTracker) addNinjaStringDeps(str ninjaString) error {
+func (l *liveTracker) addNinjaStringDeps(str *ninjaString) error {
 	l.Lock()
 	defer l.Unlock()
 	return l.innerAddNinjaStringDeps(str)
 }
 
-func (l *liveTracker) innerAddNinjaStringDeps(str ninjaString) error {
+func (l *liveTracker) innerAddNinjaStringDeps(str *ninjaString) error {
 	for _, v := range str.Variables() {
 		err := l.innerAddVariable(v)
 		if err != nil {
diff --git a/module_ctx.go b/module_ctx.go
index a43deed..ed03789 100644
--- a/module_ctx.go
+++ b/module_ctx.go
@@ -139,6 +139,14 @@
 	// Context.RegisterModuleType().
 	ModuleType() string
 
+	// ModuleTags returns the tags for this module that should be passed to
+	// ninja for analysis. For example:
+	// [
+	//   "module_name": "libfoo",
+	//   "module_type": "cc_library",
+	// ]
+	ModuleTags() map[string]string
+
 	// BlueprintsFile returns the name of the blueprint file that contains the definition of this
 	// module.
 	BlueprintsFile() string
@@ -406,6 +414,13 @@
 	return d.module.typeName
 }
 
+func (d *baseModuleContext) ModuleTags() map[string]string {
+	return map[string]string{
+		"module_name": d.ModuleName(),
+		"module_type": d.ModuleType(),
+	}
+}
+
 func (d *baseModuleContext) ContainsProperty(name string) bool {
 	_, ok := d.module.propertyPos[name]
 	return ok
@@ -796,7 +811,7 @@
 func (m *moduleContext) Build(pctx PackageContext, params BuildParams) {
 	m.scope.ReparentTo(pctx)
 
-	def, err := parseBuildParams(m.scope, &params)
+	def, err := parseBuildParams(m.scope, &params, m.ModuleTags())
 	if err != nil {
 		panic(err)
 	}
diff --git a/ninja_defs.go b/ninja_defs.go
index 69233c2..482c80c 100644
--- a/ninja_defs.go
+++ b/ninja_defs.go
@@ -131,11 +131,11 @@
 // A ruleDef describes a rule definition.  It does not include the name of the
 // rule.
 type ruleDef struct {
-	CommandDeps      []ninjaString
-	CommandOrderOnly []ninjaString
+	CommandDeps      []*ninjaString
+	CommandOrderOnly []*ninjaString
 	Comment          string
 	Pool             Pool
-	Variables        map[string]ninjaString
+	Variables        map[string]*ninjaString
 }
 
 func parseRuleParams(scope scope, params *RuleParams) (*ruleDef,
@@ -144,7 +144,7 @@
 	r := &ruleDef{
 		Comment:   params.Comment,
 		Pool:      params.Pool,
-		Variables: make(map[string]ninjaString),
+		Variables: make(map[string]*ninjaString),
 	}
 
 	if params.Command == "" {
@@ -264,19 +264,36 @@
 	Comment         string
 	Rule            Rule
 	RuleDef         *ruleDef
-	Outputs         []ninjaString
-	ImplicitOutputs []ninjaString
-	Inputs          []ninjaString
-	Implicits       []ninjaString
-	OrderOnly       []ninjaString
-	Validations     []ninjaString
-	Args            map[Variable]ninjaString
-	Variables       map[string]ninjaString
+	Outputs         []*ninjaString
+	ImplicitOutputs []*ninjaString
+	Inputs          []*ninjaString
+	Implicits       []*ninjaString
+	OrderOnly       []*ninjaString
+	Validations     []*ninjaString
+	Args            map[Variable]*ninjaString
+	Variables       map[string]*ninjaString
 	Optional        bool
 }
 
-func parseBuildParams(scope scope, params *BuildParams) (*buildDef,
-	error) {
+func formatTags(tags map[string]string, rule Rule) string {
+	// Maps in golang do not have a guaranteed iteration order, nor is there an
+	// ordered map type in the stdlib, but we need to deterministically generate
+	// the ninja file.
+	keys := make([]string, 0, len(tags))
+	for k := range tags {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+	pairs := make([]string, 0, len(keys))
+	for _, k := range keys {
+		pairs = append(pairs, k+"="+tags[k])
+	}
+	pairs = append(pairs, "rule_name="+rule.name())
+	return strings.Join(pairs, ";")
+}
+
+func parseBuildParams(scope scope, params *BuildParams,
+	tags map[string]string) (*buildDef, error) {
 
 	comment := params.Comment
 	rule := params.Rule
@@ -286,9 +303,9 @@
 		Rule:    rule,
 	}
 
-	setVariable := func(name string, value ninjaString) {
+	setVariable := func(name string, value *ninjaString) {
 		if b.Variables == nil {
-			b.Variables = make(map[string]ninjaString)
+			b.Variables = make(map[string]*ninjaString)
 		}
 		b.Variables[name] = value
 	}
@@ -360,10 +377,14 @@
 			simpleNinjaString(strings.Join(params.SymlinkOutputs, " ")))
 	}
 
+	if len(tags) > 0 {
+		setVariable("tags", simpleNinjaString(formatTags(tags, rule)))
+	}
+
 	argNameScope := rule.scope()
 
 	if len(params.Args) > 0 {
-		b.Args = make(map[Variable]ninjaString)
+		b.Args = make(map[Variable]*ninjaString)
 		for name, value := range params.Args {
 			if !rule.isArg(name) {
 				return nil, fmt.Errorf("unknown argument %q", name)
@@ -444,7 +465,7 @@
 	return nw.BlankLine()
 }
 
-func writeVariables(nw *ninjaWriter, variables map[string]ninjaString,
+func writeVariables(nw *ninjaWriter, variables map[string]*ninjaString,
 	pkgNames map[*packageContext]string) error {
 	var keys []string
 	for k := range variables {
diff --git a/ninja_strings.go b/ninja_strings.go
index 9e83a4d..0e565e2 100644
--- a/ninja_strings.go
+++ b/ninja_strings.go
@@ -35,19 +35,28 @@
 		":", "$:")
 )
 
-type ninjaString interface {
-	Value(pkgNames map[*packageContext]string) string
-	ValueWithEscaper(w io.StringWriter, pkgNames map[*packageContext]string, escaper *strings.Replacer)
-	Eval(variables map[Variable]ninjaString) (string, error)
-	Variables() []Variable
+// ninjaString contains the parsed result of a string that can contain references to variables (e.g. $cflags) that will
+// be propagated to the build.ninja file.  For literal strings with no variable references, the variables field will be
+// nil. For strings with variable references str contains the original, unparsed string, and variables contains a
+// pointer to a list of references, each with a span of bytes they should replace and a Variable interface.
+type ninjaString struct {
+	str       string
+	variables *[]variableReference
 }
 
-type varNinjaString struct {
-	strings   []string
-	variables []Variable
-}
+// variableReference contains information about a single reference to a variable (e.g. $cflags) inside a parsed
+// ninjaString.  start and end are int32 to reduce memory usage.  A nil variable is a special case of an inserted '$'
+// at the beginning of the string to handle leading whitespace that must not be stripped by ninja.
+type variableReference struct {
+	// start is the offset of the '$' character from the beginning of the unparsed string.
+	start int32
 
-type literalNinjaString string
+	// end is the offset of the character _after_ the final character of the variable name (or '}' if using the
+	//'${}'  syntax)
+	end int32
+
+	variable Variable
+}
 
 type scope interface {
 	LookupVariable(name string) (Variable, error)
@@ -55,36 +64,24 @@
 	IsPoolVisible(pool Pool) bool
 }
 
-func simpleNinjaString(str string) ninjaString {
-	return literalNinjaString(str)
+func simpleNinjaString(str string) *ninjaString {
+	return &ninjaString{str: str}
 }
 
 type parseState struct {
-	scope       scope
-	str         string
-	pendingStr  string
-	stringStart int
-	varStart    int
-	result      *varNinjaString
+	scope        scope
+	str          string
+	varStart     int
+	varNameStart int
+	result       *ninjaString
 }
 
-func (ps *parseState) pushVariable(v Variable) {
-	if len(ps.result.variables) == len(ps.result.strings) {
-		// Last push was a variable, we need a blank string separator
-		ps.result.strings = append(ps.result.strings, "")
+func (ps *parseState) pushVariable(start, end int, v Variable) {
+	if ps.result.variables == nil {
+		ps.result.variables = &[]variableReference{{start: int32(start), end: int32(end), variable: v}}
+	} else {
+		*ps.result.variables = append(*ps.result.variables, variableReference{start: int32(start), end: int32(end), variable: v})
 	}
-	if ps.pendingStr != "" {
-		panic("oops, pushed variable with pending string")
-	}
-	ps.result.variables = append(ps.result.variables, v)
-}
-
-func (ps *parseState) pushString(s string) {
-	if len(ps.result.strings) != len(ps.result.variables) {
-		panic("oops, pushed string after string")
-	}
-	ps.result.strings = append(ps.result.strings, ps.pendingStr+s)
-	ps.pendingStr = ""
 }
 
 type stateFunc func(*parseState, int, rune) (stateFunc, error)
@@ -92,18 +89,19 @@
 // parseNinjaString parses an unescaped ninja string (i.e. all $<something>
 // occurrences are expected to be variables or $$) and returns a list of the
 // variable names that the string references.
-func parseNinjaString(scope scope, str string) (ninjaString, error) {
-	// naively pre-allocate slices by counting $ signs
+func parseNinjaString(scope scope, str string) (*ninjaString, error) {
+	// naively pre-allocate slice by counting $ signs
 	n := strings.Count(str, "$")
 	if n == 0 {
-		if strings.HasPrefix(str, " ") {
+		if len(str) > 0 && str[0] == ' ' {
 			str = "$" + str
 		}
-		return literalNinjaString(str), nil
+		return simpleNinjaString(str), nil
 	}
-	result := &varNinjaString{
-		strings:   make([]string, 0, n+1),
-		variables: make([]Variable, 0, n),
+	variableReferences := make([]variableReference, 0, n)
+	result := &ninjaString{
+		str:       str,
+		variables: &variableReferences,
 	}
 
 	parseState := &parseState{
@@ -127,12 +125,18 @@
 		return nil, err
 	}
 
+	// All the '$' characters counted initially could have been "$$" escapes, leaving no
+	// variable references.  Deallocate the variables slice if so.
+	if len(*result.variables) == 0 {
+		result.variables = nil
+	}
+
 	return result, nil
 }
 
 func parseFirstRuneState(state *parseState, i int, r rune) (stateFunc, error) {
 	if r == ' ' {
-		state.pendingStr += "$"
+		state.pushVariable(0, 1, nil)
 	}
 	return parseStringState(state, i, r)
 }
@@ -140,11 +144,10 @@
 func parseStringState(state *parseState, i int, r rune) (stateFunc, error) {
 	switch {
 	case r == '$':
-		state.varStart = i + 1
+		state.varStart = i
 		return parseDollarStartState, nil
 
 	case r == eof:
-		state.pushString(state.str[state.stringStart:i])
 		return nil, nil
 
 	default:
@@ -156,21 +159,17 @@
 	switch {
 	case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
 		r >= '0' && r <= '9', r == '_', r == '-':
-		// The beginning of a of the variable name.  Output the string and
-		// keep going.
-		state.pushString(state.str[state.stringStart : i-1])
+		// The beginning of a of the variable name.
+		state.varNameStart = i
 		return parseDollarState, nil
 
 	case r == '$':
-		// Just a "$$".  Go back to parseStringState without changing
-		// state.stringStart.
+		// Just a "$$".  Go back to parseStringState.
 		return parseStringState, nil
 
 	case r == '{':
-		// This is a bracketted variable name (e.g. "${blah.blah}").  Output
-		// the string and keep going.
-		state.pushString(state.str[state.stringStart : i-1])
-		state.varStart = i + 1
+		// This is a bracketted variable name (e.g. "${blah.blah}").
+		state.varNameStart = i + 1
 		return parseBracketsState, nil
 
 	case r == eof:
@@ -190,45 +189,26 @@
 		r >= '0' && r <= '9', r == '_', r == '-':
 		// A part of the variable name.  Keep going.
 		return parseDollarState, nil
+	}
 
+	// The variable name has ended, output what we have.
+	v, err := state.scope.LookupVariable(state.str[state.varNameStart:i])
+	if err != nil {
+		return nil, err
+	}
+
+	state.pushVariable(state.varStart, i, v)
+
+	switch {
 	case r == '$':
-		// A dollar after the variable name (e.g. "$blah$").  Output the
-		// variable we have and start a new one.
-		v, err := state.scope.LookupVariable(state.str[state.varStart:i])
-		if err != nil {
-			return nil, err
-		}
-
-		state.pushVariable(v)
-		state.varStart = i + 1
-		state.stringStart = i
-
+		// A dollar after the variable name (e.g. "$blah$").  Start a new one.
+		state.varStart = i
 		return parseDollarStartState, nil
 
 	case r == eof:
-		// This is the end of the variable name.
-		v, err := state.scope.LookupVariable(state.str[state.varStart:i])
-		if err != nil {
-			return nil, err
-		}
-
-		state.pushVariable(v)
-
-		// We always end with a string, even if it's an empty one.
-		state.pushString("")
-
 		return nil, nil
 
 	default:
-		// We've just gone past the end of the variable name, so record what
-		// we have.
-		v, err := state.scope.LookupVariable(state.str[state.varStart:i])
-		if err != nil {
-			return nil, err
-		}
-
-		state.pushVariable(v)
-		state.stringStart = i
 		return parseStringState, nil
 	}
 }
@@ -241,20 +221,19 @@
 		return parseBracketsState, nil
 
 	case r == '}':
-		if state.varStart == i {
+		if state.varNameStart == i {
 			// The brackets were immediately closed.  That's no good.
 			return nil, fmt.Errorf("empty variable name at byte offset %d",
 				i)
 		}
 
 		// This is the end of the variable name.
-		v, err := state.scope.LookupVariable(state.str[state.varStart:i])
+		v, err := state.scope.LookupVariable(state.str[state.varNameStart:i])
 		if err != nil {
 			return nil, err
 		}
 
-		state.pushVariable(v)
-		state.stringStart = i + 1
+		state.pushVariable(state.varStart, i+1, v)
 		return parseStringState, nil
 
 	case r == eof:
@@ -267,13 +246,13 @@
 	}
 }
 
-func parseNinjaStrings(scope scope, strs []string) ([]ninjaString,
+func parseNinjaStrings(scope scope, strs []string) ([]*ninjaString,
 	error) {
 
 	if len(strs) == 0 {
 		return nil, nil
 	}
-	result := make([]ninjaString, len(strs))
+	result := make([]*ninjaString, len(strs))
 	for i, str := range strs {
 		ninjaStr, err := parseNinjaString(scope, str)
 		if err != nil {
@@ -284,62 +263,78 @@
 	return result, nil
 }
 
-func (n varNinjaString) Value(pkgNames map[*packageContext]string) string {
-	if len(n.strings) == 1 {
-		return defaultEscaper.Replace(n.strings[0])
+func (n *ninjaString) Value(pkgNames map[*packageContext]string) string {
+	if n.variables == nil || len(*n.variables) == 0 {
+		return defaultEscaper.Replace(n.str)
 	}
 	str := &strings.Builder{}
 	n.ValueWithEscaper(str, pkgNames, defaultEscaper)
 	return str.String()
 }
 
-func (n varNinjaString) ValueWithEscaper(w io.StringWriter, pkgNames map[*packageContext]string,
+func (n *ninjaString) ValueWithEscaper(w io.StringWriter, pkgNames map[*packageContext]string,
 	escaper *strings.Replacer) {
 
-	w.WriteString(escaper.Replace(n.strings[0]))
-	for i, v := range n.variables {
-		w.WriteString("${")
-		w.WriteString(v.fullName(pkgNames))
-		w.WriteString("}")
-		w.WriteString(escaper.Replace(n.strings[i+1]))
+	if n.variables == nil || len(*n.variables) == 0 {
+		w.WriteString(escaper.Replace(n.str))
+		return
 	}
-}
 
-func (n varNinjaString) Eval(variables map[Variable]ninjaString) (string, error) {
-	str := n.strings[0]
-	for i, v := range n.variables {
-		variable, ok := variables[v]
-		if !ok {
-			return "", fmt.Errorf("no such global variable: %s", v)
+	i := 0
+	for _, v := range *n.variables {
+		w.WriteString(escaper.Replace(n.str[i:v.start]))
+		if v.variable == nil {
+			w.WriteString("$ ")
+		} else {
+			w.WriteString("${")
+			w.WriteString(v.variable.fullName(pkgNames))
+			w.WriteString("}")
 		}
-		value, err := variable.Eval(variables)
-		if err != nil {
-			return "", err
-		}
-		str += value + n.strings[i+1]
+		i = int(v.end)
 	}
-	return str, nil
+	w.WriteString(escaper.Replace(n.str[i:len(n.str)]))
 }
 
-func (n varNinjaString) Variables() []Variable {
-	return n.variables
+func (n *ninjaString) Eval(variables map[Variable]*ninjaString) (string, error) {
+	if n.variables == nil || len(*n.variables) == 0 {
+		return n.str, nil
+	}
+
+	w := &strings.Builder{}
+	i := 0
+	for _, v := range *n.variables {
+		w.WriteString(n.str[i:v.start])
+		if v.variable == nil {
+			w.WriteString(" ")
+		} else {
+			variable, ok := variables[v.variable]
+			if !ok {
+				return "", fmt.Errorf("no such global variable: %s", v.variable)
+			}
+			value, err := variable.Eval(variables)
+			if err != nil {
+				return "", err
+			}
+			w.WriteString(value)
+		}
+		i = int(v.end)
+	}
+	w.WriteString(n.str[i:len(n.str)])
+	return w.String(), nil
 }
 
-func (l literalNinjaString) Value(_ map[*packageContext]string) string {
-	return defaultEscaper.Replace(string(l))
-}
+func (n *ninjaString) Variables() []Variable {
+	if n.variables == nil || len(*n.variables) == 0 {
+		return nil
+	}
 
-func (l literalNinjaString) ValueWithEscaper(w io.StringWriter, _ map[*packageContext]string,
-	escaper *strings.Replacer) {
-	w.WriteString(escaper.Replace(string(l)))
-}
-
-func (l literalNinjaString) Eval(variables map[Variable]ninjaString) (string, error) {
-	return string(l), nil
-}
-
-func (l literalNinjaString) Variables() []Variable {
-	return nil
+	variables := make([]Variable, 0, len(*n.variables))
+	for _, v := range *n.variables {
+		if v.variable != nil {
+			variables = append(variables, v.variable)
+		}
+	}
+	return variables
 }
 
 func validateNinjaName(name string) error {
@@ -399,6 +394,10 @@
 		return fmt.Errorf("%q contains a '.' character", argName)
 	}
 
+	if argName == "tags" {
+		return fmt.Errorf("\"tags\" is a reserved argument name")
+	}
+
 	for _, builtin := range builtinRuleArgs {
 		if argName == builtin {
 			return fmt.Errorf("%q conflicts with Ninja built-in", argName)
diff --git a/ninja_strings_test.go b/ninja_strings_test.go
index f400074..2fa4937 100644
--- a/ninja_strings_test.go
+++ b/ninja_strings_test.go
@@ -21,143 +21,176 @@
 	"testing"
 )
 
-var ninjaParseTestCases = []struct {
-	input   string
-	vars    []string
-	strs    []string
-	literal bool
-	err     string
-}{
-	{
-		input: "abc def $ghi jkl",
-		vars:  []string{"ghi"},
-		strs:  []string{"abc def ", " jkl"},
-	},
-	{
-		input: "abc def $ghi$jkl",
-		vars:  []string{"ghi", "jkl"},
-		strs:  []string{"abc def ", "", ""},
-	},
-	{
-		input: "foo $012_-345xyz_! bar",
-		vars:  []string{"012_-345xyz_"},
-		strs:  []string{"foo ", "! bar"},
-	},
-	{
-		input: "foo ${012_-345xyz_} bar",
-		vars:  []string{"012_-345xyz_"},
-		strs:  []string{"foo ", " bar"},
-	},
-	{
-		input: "foo ${012_-345xyz_} bar",
-		vars:  []string{"012_-345xyz_"},
-		strs:  []string{"foo ", " bar"},
-	},
-	{
-		input: "foo $$ bar",
-		vars:  nil,
-		strs:  []string{"foo $$ bar"},
-		// this is technically a literal, but not recognized as such due to the $$
-	},
-	{
-		input: "$foo${bar}",
-		vars:  []string{"foo", "bar"},
-		strs:  []string{"", "", ""},
-	},
-	{
-		input: "$foo$$",
-		vars:  []string{"foo"},
-		strs:  []string{"", "$$"},
-	},
-	{
-		input:   "foo bar",
-		vars:    nil,
-		strs:    []string{"foo bar"},
-		literal: true,
-	},
-	{
-		input:   " foo ",
-		vars:    nil,
-		strs:    []string{"$ foo "},
-		literal: true,
-	},
-	{
-		input: " $foo ",
-		vars:  []string{"foo"},
-		strs:  []string{"$ ", " "},
-	}, {
-		input: "foo $ bar",
-		err:   `error parsing ninja string "foo $ bar": invalid character after '$' at byte offset 5`,
-	},
-	{
-		input: "foo $",
-		err:   "unexpected end of string after '$'",
-	},
-	{
-		input: "foo ${} bar",
-		err:   `error parsing ninja string "foo ${} bar": empty variable name at byte offset 6`,
-	},
-	{
-		input: "foo ${abc!} bar",
-		err:   `error parsing ninja string "foo ${abc!} bar": invalid character in variable name at byte offset 9`,
-	},
-	{
-		input: "foo ${abc",
-		err:   "unexpected end of string in variable name",
-	},
+type testVariableRef struct {
+	start, end int
+	name       string
 }
 
 func TestParseNinjaString(t *testing.T) {
-	for _, testCase := range ninjaParseTestCases {
-		scope := newLocalScope(nil, "namespace")
-		expectedVars := []Variable{}
-		for _, varName := range testCase.vars {
-			v, err := scope.LookupVariable(varName)
-			if err != nil {
-				v, err = scope.AddLocalVariable(varName, "")
+	testCases := []struct {
+		input string
+		vars  []string
+		value string
+		eval  string
+		err   string
+	}{
+		{
+			input: "abc def $ghi jkl",
+			vars:  []string{"ghi"},
+			value: "abc def ${namespace.ghi} jkl",
+			eval:  "abc def GHI jkl",
+		},
+		{
+			input: "abc def $ghi$jkl",
+			vars:  []string{"ghi", "jkl"},
+			value: "abc def ${namespace.ghi}${namespace.jkl}",
+			eval:  "abc def GHIJKL",
+		},
+		{
+			input: "foo $012_-345xyz_! bar",
+			vars:  []string{"012_-345xyz_"},
+			value: "foo ${namespace.012_-345xyz_}! bar",
+			eval:  "foo 012_-345XYZ_! bar",
+		},
+		{
+			input: "foo ${012_-345xyz_} bar",
+			vars:  []string{"012_-345xyz_"},
+			value: "foo ${namespace.012_-345xyz_} bar",
+			eval:  "foo 012_-345XYZ_ bar",
+		},
+		{
+			input: "foo ${012_-345xyz_} bar",
+			vars:  []string{"012_-345xyz_"},
+			value: "foo ${namespace.012_-345xyz_} bar",
+			eval:  "foo 012_-345XYZ_ bar",
+		},
+		{
+			input: "foo $$ bar",
+			vars:  nil,
+			value: "foo $$ bar",
+			eval:  "foo $$ bar",
+		},
+		{
+			input: "$foo${bar}",
+			vars:  []string{"foo", "bar"},
+			value: "${namespace.foo}${namespace.bar}",
+			eval:  "FOOBAR",
+		},
+		{
+			input: "$foo$$",
+			vars:  []string{"foo"},
+			value: "${namespace.foo}$$",
+			eval:  "FOO$$",
+		},
+		{
+			input: "foo bar",
+			vars:  nil,
+			value: "foo bar",
+			eval:  "foo bar",
+		},
+		{
+			input: " foo ",
+			vars:  nil,
+			value: "$ foo ",
+			eval:  "$ foo ",
+		},
+		{
+			input: "\tfoo ",
+			vars:  nil,
+			value: "\tfoo ",
+			eval:  "\tfoo ",
+		},
+		{
+			input: "\nfoo ",
+			vars:  nil,
+			value: "$\nfoo ",
+			eval:  "\nfoo ",
+		},
+		{
+			input: " $foo ",
+			vars:  []string{"foo"},
+			value: "$ ${namespace.foo} ",
+			eval:  " FOO ",
+		},
+		{
+			input: "\t$foo ",
+			vars:  []string{"foo"},
+			value: "\t${namespace.foo} ",
+			eval:  "\tFOO ",
+		},
+		{
+			input: "\n$foo ",
+			vars:  []string{"foo"},
+			value: "$\n${namespace.foo} ",
+			eval:  "\nFOO ",
+		},
+		{
+			input: "foo $ bar",
+			err:   `error parsing ninja string "foo $ bar": invalid character after '$' at byte offset 5`,
+		},
+		{
+			input: "foo $",
+			err:   "unexpected end of string after '$'",
+		},
+		{
+			input: "foo ${} bar",
+			err:   `error parsing ninja string "foo ${} bar": empty variable name at byte offset 6`,
+		},
+		{
+			input: "foo ${abc!} bar",
+			err:   `error parsing ninja string "foo ${abc!} bar": invalid character in variable name at byte offset 9`,
+		},
+		{
+			input: "foo ${abc",
+			err:   "unexpected end of string in variable name",
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.input, func(t *testing.T) {
+			scope := newLocalScope(nil, "namespace.")
+			variablesMap := map[Variable]*ninjaString{}
+			for _, varName := range testCase.vars {
+				_, err := scope.LookupVariable(varName)
 				if err != nil {
-					t.Fatalf("error creating scope: %s", err)
+					v, err := scope.AddLocalVariable(varName, strings.ToUpper(varName))
+					if err != nil {
+						t.Fatalf("error creating scope: %s", err)
+					}
+					variablesMap[v] = simpleNinjaString(strings.ToUpper(varName))
 				}
 			}
-			expectedVars = append(expectedVars, v)
-		}
 
-		var expected ninjaString
-		if len(testCase.strs) > 0 {
-			if testCase.literal {
-				expected = literalNinjaString(testCase.strs[0])
-			} else {
-				expected = &varNinjaString{
-					strings:   testCase.strs,
-					variables: expectedVars,
+			output, err := parseNinjaString(scope, testCase.input)
+			if err == nil {
+				if g, w := output.Value(nil), testCase.value; g != w {
+					t.Errorf("incorrect Value output, want %q, got %q", w, g)
+				}
+
+				eval, err := output.Eval(variablesMap)
+				if err != nil {
+					t.Errorf("unexpected error in Eval: %s", err)
+				}
+				if g, w := eval, testCase.eval; g != w {
+					t.Errorf("incorrect Eval output, want %q, got %q", w, g)
 				}
 			}
-		}
-
-		output, err := parseNinjaString(scope, testCase.input)
-		if err == nil {
-			if !reflect.DeepEqual(output, expected) {
-				t.Errorf("incorrect ninja string:")
+			var errStr string
+			if err != nil {
+				errStr = err.Error()
+			}
+			if err != nil && err.Error() != testCase.err {
+				t.Errorf("unexpected error:")
 				t.Errorf("     input: %q", testCase.input)
-				t.Errorf("  expected: %#v", expected)
-				t.Errorf("       got: %#v", output)
+				t.Errorf("  expected: %q", testCase.err)
+				t.Errorf("       got: %q", errStr)
 			}
-		}
-		var errStr string
-		if err != nil {
-			errStr = err.Error()
-		}
-		if err != nil && err.Error() != testCase.err {
-			t.Errorf("unexpected error:")
-			t.Errorf("     input: %q", testCase.input)
-			t.Errorf("  expected: %q", testCase.err)
-			t.Errorf("       got: %q", errStr)
-		}
+		})
 	}
 }
 
 func TestParseNinjaStringWithImportedVar(t *testing.T) {
-	ImpVar := &staticVariable{name_: "ImpVar"}
+	ImpVar := &staticVariable{name_: "ImpVar", fullName_: "g.impPkg.ImpVar"}
 	impScope := newScope(nil)
 	impScope.AddVariable(ImpVar)
 	scope := newScope(nil)
@@ -169,13 +202,59 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 
-	expect := []Variable{ImpVar}
-	if !reflect.DeepEqual(output.(*varNinjaString).variables, expect) {
+	expect := []variableReference{{8, 24, ImpVar}}
+	if !reflect.DeepEqual(*output.variables, expect) {
 		t.Errorf("incorrect output:")
 		t.Errorf("     input: %q", input)
 		t.Errorf("  expected: %#v", expect)
-		t.Errorf("       got: %#v", output)
+		t.Errorf("       got: %#v", *output.variables)
 	}
+
+	if g, w := output.Value(nil), "abc def ${g.impPkg.ImpVar} ghi"; g != w {
+		t.Errorf("incorrect Value output, want %q got %q", w, g)
+	}
+}
+
+func Benchmark_parseNinjaString(b *testing.B) {
+	b.Run("constant", func(b *testing.B) {
+		for _, l := range []int{1, 10, 100, 1000} {
+			b.Run(strconv.Itoa(l), func(b *testing.B) {
+				b.ReportAllocs()
+				for n := 0; n < b.N; n++ {
+					_ = simpleNinjaString(strings.Repeat("a", l))
+				}
+			})
+		}
+	})
+	b.Run("variable", func(b *testing.B) {
+		for _, l := range []int{1, 10, 100, 1000} {
+			scope := newLocalScope(nil, "")
+			scope.AddLocalVariable("a", strings.Repeat("b", l/3))
+			b.Run(strconv.Itoa(l), func(b *testing.B) {
+				b.ReportAllocs()
+				for n := 0; n < b.N; n++ {
+					_, _ = parseNinjaString(scope, strings.Repeat("a", l/3)+"${a}"+strings.Repeat("a", l/3))
+				}
+			})
+		}
+	})
+	b.Run("variables", func(b *testing.B) {
+		for _, l := range []int{1, 2, 3, 4, 5, 10, 100, 1000} {
+			scope := newLocalScope(nil, "")
+			str := strings.Repeat("a", 10)
+			for i := 0; i < l; i++ {
+				scope.AddLocalVariable("a"+strconv.Itoa(i), strings.Repeat("b", 10))
+				str += "${a" + strconv.Itoa(i) + "}"
+			}
+			b.Run(strconv.Itoa(l), func(b *testing.B) {
+				b.ReportAllocs()
+				for n := 0; n < b.N; n++ {
+					_, _ = parseNinjaString(scope, str)
+				}
+			})
+		}
+	})
+
 }
 
 func BenchmarkNinjaString_Value(b *testing.B) {
@@ -183,6 +262,7 @@
 		for _, l := range []int{1, 10, 100, 1000} {
 			ns := simpleNinjaString(strings.Repeat("a", l))
 			b.Run(strconv.Itoa(l), func(b *testing.B) {
+				b.ReportAllocs()
 				for n := 0; n < b.N; n++ {
 					ns.Value(nil)
 				}
@@ -195,6 +275,7 @@
 			scope.AddLocalVariable("a", strings.Repeat("b", l/3))
 			ns, _ := parseNinjaString(scope, strings.Repeat("a", l/3)+"${a}"+strings.Repeat("a", l/3))
 			b.Run(strconv.Itoa(l), func(b *testing.B) {
+				b.ReportAllocs()
 				for n := 0; n < b.N; n++ {
 					ns.Value(nil)
 				}
@@ -211,6 +292,7 @@
 			}
 			ns, _ := parseNinjaString(scope, str)
 			b.Run(strconv.Itoa(l), func(b *testing.B) {
+				b.ReportAllocs()
 				for n := 0; n < b.N; n++ {
 					ns.Value(nil)
 				}
diff --git a/ninja_writer.go b/ninja_writer.go
index f9951b4..fbbfb9e 100644
--- a/ninja_writer.go
+++ b/ninja_writer.go
@@ -114,7 +114,7 @@
 }
 
 func (n *ninjaWriter) Build(comment string, rule string, outputs, implicitOuts,
-	explicitDeps, implicitDeps, orderOnlyDeps, validations []ninjaString,
+	explicitDeps, implicitDeps, orderOnlyDeps, validations []*ninjaString,
 	pkgNames map[*packageContext]string) error {
 
 	n.justDidBlankLine = false
@@ -235,7 +235,7 @@
 	return nil
 }
 
-func (n *ninjaWriter) Default(pkgNames map[*packageContext]string, targets ...ninjaString) error {
+func (n *ninjaWriter) Default(pkgNames map[*packageContext]string, targets ...*ninjaString) error {
 	n.justDidBlankLine = false
 
 	const lineWrapLen = len(" $")
diff --git a/ninja_writer_test.go b/ninja_writer_test.go
index 82eeee5..c70d5df 100644
--- a/ninja_writer_test.go
+++ b/ninja_writer_test.go
@@ -139,7 +139,7 @@
 	}
 }
 
-func testNinjaStrings(s ...string) []ninjaString {
+func testNinjaStrings(s ...string) []*ninjaString {
 	ret, _ := parseNinjaStrings(nil, s)
 	return ret
 }
diff --git a/package_ctx.go b/package_ctx.go
index f8a7cc4..4b48337 100644
--- a/package_ctx.go
+++ b/package_ctx.go
@@ -304,7 +304,7 @@
 	v.fullName_ = v.fullName(pkgNames)
 }
 
-func (v *staticVariable) value(VariableFuncContext, interface{}) (ninjaString, error) {
+func (v *staticVariable) value(VariableFuncContext, interface{}) (*ninjaString, error) {
 	ninjaStr, err := parseNinjaString(v.pctx.scope, v.value_)
 	if err != nil {
 		err = fmt.Errorf("error parsing variable %s value: %s", v, err)
@@ -440,7 +440,7 @@
 	v.fullName_ = v.fullName(pkgNames)
 }
 
-func (v *variableFunc) value(ctx VariableFuncContext, config interface{}) (ninjaString, error) {
+func (v *variableFunc) value(ctx VariableFuncContext, config interface{}) (*ninjaString, error) {
 	value, err := v.value_(ctx, config)
 	if err != nil {
 		return nil, err
@@ -504,7 +504,7 @@
 	// Nothing to do, full name is known at initialization.
 }
 
-func (v *argVariable) value(ctx VariableFuncContext, config interface{}) (ninjaString, error) {
+func (v *argVariable) value(ctx VariableFuncContext, config interface{}) (*ninjaString, error) {
 	return nil, errVariableIsArg
 }
 
diff --git a/provider_test.go b/provider_test.go
index 942dd31..4038eae 100644
--- a/provider_test.go
+++ b/provider_test.go
@@ -283,7 +283,7 @@
 		ctx.RegisterTopDownMutator("parent", invalidProviderUsageParentMutator)
 
 		// Don't invalidate the parent pointer and before GenerateBuildActions.
-		ctx.skipCloneModulesAfterMutators = true
+		ctx.SkipCloneModulesAfterMutators = true
 
 		var parentBP, moduleUnderTestBP, childBP string
 
diff --git a/scope.go b/scope.go
index 9585559..0ca3464 100644
--- a/scope.go
+++ b/scope.go
@@ -29,7 +29,7 @@
 	name() string                                        // "foo"
 	fullName(pkgNames map[*packageContext]string) string // "pkg.foo" or "path.to.pkg.foo"
 	memoizeFullName(pkgNames map[*packageContext]string) // precompute fullName if desired
-	value(ctx VariableFuncContext, config interface{}) (ninjaString, error)
+	value(ctx VariableFuncContext, config interface{}) (*ninjaString, error)
 	String() string
 }
 
@@ -354,7 +354,7 @@
 type localVariable struct {
 	fullName_ string
 	name_     string
-	value_    ninjaString
+	value_    *ninjaString
 }
 
 func (l *localVariable) packageContext() *packageContext {
@@ -373,7 +373,7 @@
 	// Nothing to do, full name is known at initialization.
 }
 
-func (l *localVariable) value(VariableFuncContext, interface{}) (ninjaString, error) {
+func (l *localVariable) value(VariableFuncContext, interface{}) (*ninjaString, error) {
 	return l.value_, nil
 }
 
diff --git a/singleton_ctx.go b/singleton_ctx.go
index d579b8e..95c29c4 100644
--- a/singleton_ctx.go
+++ b/singleton_ctx.go
@@ -265,7 +265,10 @@
 func (s *singletonContext) Build(pctx PackageContext, params BuildParams) {
 	s.scope.ReparentTo(pctx)
 
-	def, err := parseBuildParams(s.scope, &params)
+	def, err := parseBuildParams(s.scope, &params, map[string]string{
+		"module_name": s.name,
+		"module_type": "singleton",
+	})
 	if err != nil {
 		panic(err)
 	}