Merge upstream commit '0a2e2cae' into master

* commit '0a2e2cae7038ce519b0524c07d7135c3e520c9cd':
  Restore depfile toleration of multiple output paths on distinct lines
  Fix depfile parser handling of multiple rules
  Fix depfile parser test case line continuation
  Re-arrange depfile parser token processing logic
  Re-generate depfile parser with re2cc 1.0.1

depfile_parser.cc conflicts were handled by regenerating with re2c

The other conflicts were all fairly simple argument/option ordering.

Test: compare .ninja_deps of a full android build before and after this
      change, they're equivalent
Change-Id: I74b7fc56e035c9dbab4c987b6af17329c596a898
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..1c3ed4f
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,133 @@
+// Copyright 2016 Google Inc. All rights reserved
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_defaults {
+    name: "ninja_defaults",
+    cflags: [
+        "-Wall",
+        "-Wextra",
+        "-Wno-deprecated",
+        "-Wno-missing-field-initializers",
+        "-Wno-unused-parameter",
+        "-fno-exceptions",
+        "-fvisibility=hidden",
+        "-DNINJA_PYTHON=\"python\"",
+        "-DNINJA_HAVE_BROWSE",
+        "-UNDEBUG",
+    ],
+    target: {
+        linux_glibc: {
+            cflags: ["-DUSE_PPOLL"],
+        }
+    }
+}
+
+genrule {
+    name: "ninja_browse_py",
+
+    cmd: "$(location src/inline.sh) kBrowsePy <$(in) >$(out)",
+    tool_files: ["src/inline.sh"],
+
+    srcs: ["src/browse.py"],
+    out: ["build/browse_py.h"],
+}
+
+cc_library_host_static {
+    name: "libninja",
+    defaults: ["ninja_defaults"],
+    generated_headers: ["ninja_browse_py"],
+    srcs: [
+        "src/build.cc",
+        "src/build_log.cc",
+        "src/clean.cc",
+        "src/clparser.cc",
+        "src/debug_flags.cc",
+        "src/depfile_parser.cc",
+        "src/deps_log.cc",
+        "src/disk_interface.cc",
+        "src/edit_distance.cc",
+        "src/eval_env.cc",
+        "src/graph.cc",
+        "src/graphviz.cc",
+        "src/lexer.cc",
+        "src/line_printer.cc",
+        "src/manifest_chunk_parser.cc",
+        "src/manifest_parser.cc",
+        "src/metrics.cc",
+        "src/proto.cc",
+        "src/state.cc",
+        "src/status.cc",
+        "src/thread_pool.cc",
+        "src/util.cc",
+        "src/version.cc",
+        "src/browse.cc",
+        "src/subprocess-posix.cc",
+    ],
+}
+
+cc_binary_host {
+    name: "ninja",
+    defaults: ["ninja_defaults"],
+    srcs: ["src/ninja.cc"],
+    static_libs: ["libninja"],
+
+    // Use jemalloc for better multithreaded allocation performance. e.g. Using
+    // jemalloc can improve the overall runtime by 10x vs the default allocator.
+    target: {
+        linux_glibc: {
+            shared_libs: ["libjemalloc"],
+        },
+    },
+}
+
+cc_test_host {
+    name: "ninja_test",
+    defaults: ["ninja_defaults"],
+    static_libs: ["libninja"],
+    gtest: false,
+    srcs: [
+        "src/build_log_test.cc",
+        "src/build_test.cc",
+        "src/clean_test.cc",
+        "src/clparser_test.cc",
+        "src/depfile_parser_test.cc",
+        "src/deps_log_test.cc",
+        "src/disk_interface_test.cc",
+        "src/edit_distance_test.cc",
+        "src/graph_test.cc",
+        "src/lexer_test.cc",
+        "src/manifest_parser_test.cc",
+        "src/ninja_test.cc",
+        "src/state_test.cc",
+        "src/status_test.cc",
+        "src/subprocess_test.cc",
+        "src/test.cc",
+        "src/util_test.cc",
+    ],
+}
+
+cc_test_host {
+    name: "ninja_tests",
+    gtest: false,
+    defaults: ["ninja_defaults"],
+    test_per_src: true,
+    static_libs: ["libninja"],
+    srcs: [
+        "src/build_log_perftest.cc",
+        "src/canon_perftest.cc",
+        "src/depfile_parser_perftest.cc",
+        "src/hash_collision_bench.cc",
+        "src/manifest_parser_perftest.cc",
+    ],
+}
diff --git a/configure.py b/configure.py
index 78cd1de..0b05d06 100755
--- a/configure.py
+++ b/configure.py
@@ -272,6 +272,8 @@
 
 def src(filename):
     return os.path.join('$root', 'src', filename)
+def frontend(filename):
+    return os.path.join('$root', 'frontend', filename)
 def built(filename):
     return os.path.join('$builddir', filename)
 def doc(filename):
@@ -328,7 +330,8 @@
         cflags += ['/Ox', '/DNDEBUG', '/GL']
         ldflags += ['/LTCG', '/OPT:REF', '/OPT:ICF']
 else:
-    cflags = ['-g', '-Wall', '-Wextra',
+    cflags = ['-std=c++11',
+              '-g', '-Wall', '-Wextra',
               '-Wno-deprecated',
               '-Wno-missing-field-initializers',
               '-Wno-unused-parameter',
@@ -364,7 +367,10 @@
 
 libs = []
 
-if platform.is_mingw():
+if platform.is_linux():
+    cflags.append('-pthread')
+    ldflags.append('-pthread')
+elif platform.is_mingw():
     cflags.remove('-fvisibility=hidden');
     ldflags.append('-static')
 elif platform.is_solaris():
@@ -484,6 +490,43 @@
            "changes to src/*.in.cc will not affect your build.")
 n.newline()
 
+n.comment('the proto descriptor is generated using protoc.')
+def has_protoc():
+    try:
+        proc = subprocess.Popen(['protoc', '--version'], stdout=subprocess.PIPE)
+        return proc.communicate()[0].startswith("libprotoc")
+    except OSError:
+        return False
+
+def can_generate_proto_header():
+    try:
+        tool = os.path.join(sourcedir, 'misc', 'generate_proto_header.py')
+        proc = subprocess.Popen([tool, '--probe'], stdout=subprocess.PIPE)
+        return proc.communicate()[0].startswith("ok")
+    except OSError:
+        return False
+
+if has_protoc() and can_generate_proto_header():
+    # Use protoc to write out frontend.proto converted to a descriptor proto
+    n.rule('protoc',
+           command='protoc $in -o $out',
+           description='PROTOC $out')
+    n.build(frontend('frontend.pb'), 'protoc', src('frontend.proto'))
+
+    # Use generate_proto_header.py to read in the descriptor proto and write
+    # a header containing field numbers and types.
+    n.rule('generate_proto_header',
+           command='$tool $in $out',
+           description='GEN $out')
+    # Generate the .h file in the source directory so we can check them in.
+    tool = os.path.join(sourcedir, 'misc', 'generate_proto_header.py')
+    n.build(src('frontend.pb.h'), 'generate_proto_header', frontend('frontend.pb'),
+            implicit=[tool], variables=[('tool', tool)])
+else:
+    print("warning: A version of protoc or the python protobuf library was not found; "
+           "changes to src/frontend.proto will not affect your build.")
+n.newline()
+
 n.comment('Core source files all build into ninja library.')
 cxxvariables = []
 if platform.is_msvc():
@@ -502,13 +545,17 @@
              'graphviz',
              'lexer',
              'line_printer',
+             'manifest_chunk_parser',
              'manifest_parser',
              'metrics',
+             'proto',
              'state',
+             'status',
              'string_piece_util',
+             'thread_pool',
              'util',
              'version']:
-    objs += cxx(name, variables=cxxvariables) 
+    objs += cxx(name, order_only=src('frontend.pb.h'), variables=cxxvariables)
 if platform.is_windows():
     for name in ['subprocess-win32',
                  'includes_normalize-win32',
@@ -570,6 +617,7 @@
              'manifest_parser_test',
              'ninja_test',
              'state_test',
+             'status_test',
              'string_piece_util_test',
              'subprocess_test',
              'test',
diff --git a/doc/manual.asciidoc b/doc/manual.asciidoc
index 7b1c3ba..4f8c74f 100644
--- a/doc/manual.asciidoc
+++ b/doc/manual.asciidoc
@@ -1,5 +1,6 @@
 The Ninja build system
 ======================
+v1.7.1, Apr 2016
 
 
 Introduction
@@ -695,6 +696,8 @@
    Order-only dependencies may be tacked on the end with +||
    _dependency1_ _dependency2_+.  (See <<ref_dependencies,the reference on
    dependency types>>.)
+   Validations may be taked on the end with +|@ _validation1_ _validation2_+.
+   (See <<validations,the reference on validations>>.)
 +
 Implicit outputs _(available since Ninja 1.7)_ may be added before
 the `:` with +| _output1_ _output2_+ and do not appear in `$out`.
@@ -815,6 +818,31 @@
   the full command or its description; if a command fails, the full command
   line will always be printed before the command's output.
 
+`phony_output`:: _(Android-specific patch)_ if present, Ninja will not attempt
+  to look for them on disk. These rules are considered always dirty, and will
+  run every time they're depended upon. This behavior is very similar to Make's
+  `.PHONY` concept.
++
+This can be similar to the `phony` rule, but can have an attached `command`.
+`phony` rules are also only considered dirty in two cases: if their inputs are
+dirty, or if they have no inputs and a file with the same name does not exist on
+disk.
++
+Other build rules may not depend on `phony_output` rules unless they are also
+`phony_output`, so that it's not possible to accidentally cause everything to
+rebuild on every run.
++
+When `-o usesphonyoutputs=yes` is set on the ninja command line, it becomes an
+error for a `phony` rule to cause rebuilds, so that users can be found and
+migrated.
++
+Properly using `phony_output` and turning on `-o usesphonyoutputs=yes` allows
+the `-w outputdir={err,warn}` (consider output files that are directories as
+errors/warnings), `-w missingoutfile={err,warn}` (error/warn when an output file
+does not exist after a successful rule execution), and `-w oldoutput={err,warn}`
+(error/warn when an output file is not updated after a successful non-restat
+rule execution) flags to function.
+
 `generator`:: if present, specifies that this rule is used to
   re-invoke the generator program.  Files built using `generator`
   rules are treated specially in two ways: firstly, they will not be
@@ -946,6 +974,31 @@
 File paths are compared as is, which means that an absolute path and a
 relative path, pointing to the same file, are considered different by Ninja.
 
+[[validations]]
+Validations
+~~~~~~~~~~~
+Validations listed on the build line cause the specified files to be
+added to the top level of the build graph (as if they were specified
+on the Ninja command line) whenever the build line is a transitive
+dependency of one of the targets specified on the command line or a
+default target.
+
+Validations are added to the build graph regardless of whether the output
+files of the build statement are dirty are not, and the dirty state of
+the build statement that outputs the file being used as a validation
+has no effect on the dirty state of the build statement that requested it.
+
+A build edge can list another build edge as a validation even if the second
+edge depends on the first.
+
+Validations are designed to handle rules that perform error checking but
+don't produce any artifacts needed by the build, for example static
+analysis tools.  Marking the static analysis rule as an implicit input
+of the main build rule of the source files or of the rules that depend
+on the main build rule would slow down the critical path of the build,
+but using a validation would allow the build to proceed in parallel with
+the static analysis rule once the main build rule is complete.
+
 Variable expansion
 ~~~~~~~~~~~~~~~~~~
 
diff --git a/frontend/FRONTEND.md b/frontend/FRONTEND.md
new file mode 100644
index 0000000..5ff6e32
--- /dev/null
+++ b/frontend/FRONTEND.md
@@ -0,0 +1,51 @@
+Ninja Frontend Interface
+========================
+
+Ninja can use another program as a frontend to display build status information.
+This document describes the interface between Ninja and the frontend.
+
+Connecting
+----------
+
+The frontend is passed to Ninja using a --frontend argument.  The argument is
+executed the same as a build rule Ninja, wrapped in `sh -c` on Linux.  The
+frontend will be executed with the read end of a pipe open on file descriptor
+`3`.
+
+Ninja will pass [Protocol Buffers](https://developers.google.com/protocol-buffers/) generated from src/frontend.proto.
+
+stdin/stdout/stderr
+-------------------
+
+The frontend will have stdin, stdout, and stderr connected to the same file
+descriptors as Ninja.  The frontend MAY read from stdin, however, if it does,
+it MUST NOT read from stdin whenever a job in the console pool is running,
+from when an `EdgeStarted` message is received with the `use_console` value
+set to `true`, to when an `EdgeFinished` message is received with the same value
+for `id`.  Console rules may write directly to the same stdout/stderr as the
+frontend.
+
+Exiting
+-------
+
+The frontend MUST exit when the input pipe on fd `3` is closed.  When a build
+finishes, either successfully, due to error, or on interrupt, Ninja will close
+the pipe and then block until the frontend exits.
+
+Experimenting with frontends
+----------------------------
+
+To run Ninja with a frontend that mimics the behavior of Ninja's normal output:
+```
+$ ./ninja --frontend=frontend/native.py
+```
+
+To save serialized output to a file:
+```
+$ ./ninja --frontend='cat <&3 > ninja.pb' all
+```
+
+To run a frontend with serialized input from a file:
+```
+$ frontend/native.py 3< ninja.pb
+```
diff --git a/frontend/dump.py b/frontend/dump.py
new file mode 100755
index 0000000..d276fd0
--- /dev/null
+++ b/frontend/dump.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+from __future__ import print_function
+
+import sys
+
+import frontend
+
+def main():
+    if len(sys.argv) >= 2:
+        f = open(sys.argv[1], 'rb')
+    else:
+        f = frontend.default_reader()
+
+    for msg in frontend.Frontend(f):
+        print('---------------------------------')
+        sys.stdout.write(str(msg))
+
+if __name__ == '__main__':
+    main()
+
diff --git a/frontend/frontend.pb b/frontend/frontend.pb
new file mode 100644
index 0000000..718f173
--- /dev/null
+++ b/frontend/frontend.pb
Binary files differ
diff --git a/frontend/frontend.py b/frontend/frontend.py
new file mode 100755
index 0000000..ac8fa25
--- /dev/null
+++ b/frontend/frontend.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+
+"""Ninja frontend interface.
+
+This module implements a Ninja frontend interface that delegates handling each
+message to a handler object
+"""
+
+import os
+import sys
+
+import google.protobuf.descriptor_pb2
+import google.protobuf.message_factory
+
+def default_reader():
+    fd = 3
+    return os.fdopen(fd, 'rb', 0)
+
+class Frontend(object):
+    """Generator class that parses length-delimited ninja status messages
+    through a ninja frontend interface.
+    """
+
+    def __init__(self, reader=default_reader()):
+        self.reader = reader
+        self.status_class = self.get_status_proto()
+
+    def get_status_proto(self):
+        fd_set = google.protobuf.descriptor_pb2.FileDescriptorSet()
+        descriptor = os.path.join(os.path.dirname(__file__), 'frontend.pb')
+        with open(descriptor, 'rb') as f:
+            fd_set.ParseFromString(f.read())
+
+        if len(fd_set.file) != 1:
+            raise RuntimeError('expected exactly one file descriptor in ' + descriptor)
+
+        messages = google.protobuf.message_factory.GetMessages(fd_set.file)
+        return messages['ninja.Status']
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        return self.next()
+
+    def next(self):
+        size = 0
+        shift = 0
+        while True:
+            byte = bytearray(self.reader.read(1))
+            if len(byte) == 0:
+                raise StopIteration()
+
+            byte = byte[0]
+            size += (byte & 0x7f) << (shift * 7)
+            if (byte & 0x80) == 0:
+                break
+            shift += 1
+            if shift > 4:
+                raise RuntimeError('Expected varint32 length-delimeted message')
+
+        message = self.reader.read(size)
+        if len(message) != size:
+            raise EOFError('Unexpected EOF reading %d bytes' % size)
+
+        return self.status_class.FromString(message)
diff --git a/frontend/native.py b/frontend/native.py
new file mode 100755
index 0000000..4862268
--- /dev/null
+++ b/frontend/native.py
@@ -0,0 +1,303 @@
+#!/usr/bin/env python
+
+from __future__ import print_function
+
+import collections
+import ctypes
+import os
+import re
+import struct
+import sys
+
+import frontend
+
+class SlidingRateInfo(object):
+    def __init__(self, n=32):
+        self.rate = -1
+        self.last_update = -1
+        self.times = collections.deque(maxlen=n)
+
+    def update_rate(self, update_hint, time_millis):
+        if update_hint == self.last_update:
+            return
+
+        self.last_update = update_hint
+
+        if len(self.times) == self.times.maxlen:
+            self.times.popleft()
+        self.times.append(time_millis)
+        if self.times[-1] != self.times[0]:
+            self.rate = len(self.times) / ((self.times[-1] - self.times[0]) / 1e3)
+
+strip_ansi_re = re.compile(r'\x1B\[[^a-zA-Z]*[a-zA-Z]')
+def strip_ansi_escape_codes(output):
+    return strip_ansi_re.sub('', output)
+
+class NinjaNativeFrontend:
+    def __init__(self):
+        self.total_edges = 0
+        self.running_edges = 0
+        self.started_edges = 0
+        self.finished_edges = 0
+        self.running = {}
+
+        self.time_millis = 0
+
+        self.progress_status_format = os.getenv('NINJA_STATUS', '[%f/%t] ')
+        self.current_rate = SlidingRateInfo()
+        self.console_locked = False
+
+        self.printer = LinePrinter()
+        self.verbose = False
+
+    def handle(self, msg):
+        handled = False
+        if msg.HasField('total_edges'):
+            handled = True
+            self.total_edges = msg.total_edges.total_edges
+
+        if msg.HasField('build_started'):
+            handled = True
+            self.verbose = msg.build_started.verbose
+            self.current_rate = SlidingRateInfo(msg.build_started.parallelism)
+            self.running_edges = 0
+            self.started_edges = 0
+            self.finished_edges = 0
+            self.running = {}
+
+        if msg.HasField('build_finished'):
+            handled = True
+            self.printer.set_console_locked(False)
+            self.printer.print_on_new_line('')
+
+        if msg.HasField('edge_started'):
+            handled = True
+            self.started_edges += 1
+            self.running_edges += 1
+            self.running[msg.edge_started.id] = msg.edge_started
+            self.time_millis = msg.edge_started.start_time
+            if msg.edge_started.console or self.printer.smart_terminal:
+                self.print_status(msg.edge_started)
+            if msg.edge_started.console:
+                self.printer.set_console_locked(True)
+
+        if msg.HasField('edge_finished'):
+            handled = True
+            self.finished_edges += 1
+            self.time_millis = msg.edge_finished.end_time
+
+            edge_started = self.running[msg.edge_finished.id]
+
+            if edge_started.console:
+                self.printer.set_console_locked(False)
+
+            if not edge_started.console:
+                self.print_status(edge_started)
+
+            self.running_edges -= 1
+            del self.running[msg.edge_finished.id]
+
+            # Print the command that is spewing before printing its output.
+            if msg.edge_finished.status != 0:
+                self.printer.print_on_new_line('FAILED: ' + ' '.join(edge_started.outputs))
+                self.printer.print_on_new_line(edge_started.command)
+
+            # ninja sets stdout and stderr of subprocesses to a pipe, to be able to
+            # check if the output is empty. Some compilers, e.g. clang, check
+            # isatty(stderr) to decide if they should print colored output.
+            # To make it possible to use colored output with ninja, subprocesses should
+            # be run with a flag that forces them to always print color escape codes.
+            # To make sure these escape codes don't show up in a file if ninja's output
+            # is piped to a file, ninja strips ansi escape codes again if it's not
+            # writing to a |smart_terminal_|.
+            # (Launching subprocesses in pseudo ttys doesn't work because there are
+            # only a few hundred available on some systems, and ninja can launch
+            # thousands of parallel compile commands.)
+            # TODO: There should be a flag to disable escape code stripping.
+            if msg.edge_finished.output != '':
+                if not self.printer.smart_terminal:
+                    msg.edge_finished.output = strip_ansi_escape_codes(msg.edge_finished.output)
+                self.printer.print_on_new_line(msg.edge_finished.output)
+
+        if msg.HasField('message'):
+            handled = True
+            # TODO(colincross): get the enum values from proto
+            if msg.message.level == 0:
+                prefix = 'ninja: '
+            elif msg.message.level == 1:
+                prefix = 'ninja: warning: '
+            elif msg.message.level == 2:
+                prefix = 'ninja: error: '
+            else
+                prefix = ''
+            self.printer.print_line(prefix + msg.message.message, LinePrinter.LINE_FULL)
+
+        if not handled:
+            pass
+
+
+    def format_progress_status(self, fmt):
+        out = ''
+        fmt_iter = iter(fmt)
+        for c in fmt_iter:
+            if c == '%':
+                c = next(fmt_iter)
+                if c == '%':
+                    out += c
+                elif c == 's':
+                    out += str(self.started_edges)
+                elif c == 't':
+                    out += str(self.total_edges)
+                elif c == 'r':
+                    out += str(self.running_edges)
+                elif c == 'u':
+                    out += str(self.total_edges - self.started_edges)
+                elif c == 'f':
+                    out += str(self.finished_edges)
+                elif c == 'o':
+                    if self.time_millis > 0:
+                        rate = self.finished_edges / (self.time_millis / 1e3)
+                        out += '{:.1f}'.format(rate)
+                    else:
+                        out += '?'
+                elif c == 'c':
+                    self.current_rate.update_rate(self.finished_edges, self.time_millis)
+                    if self.current_rate.rate == -1:
+                        out += '?'
+                    else:
+                        out += '{:.1f}'.format(self.current_rate.rate)
+                elif c == 'p':
+                    out += '{:3d}%'.format((100 * self.finished_edges) // self.total_edges)
+                elif c == 'e':
+                    out += '{:.3f}'.format(self.time_millis / 1e3)
+                else:
+                    raise RuntimeError('unknown placeholder '' + c +'' in $NINJA_STATUS')
+            else:
+                out += c
+        return out
+
+    def print_status(self, edge_started):
+        to_print = edge_started.desc
+        if self.verbose or to_print == '':
+            to_print = edge_started.command
+
+        to_print = self.format_progress_status(self.progress_status_format) + to_print
+
+        self.printer.print_line(to_print, LinePrinter.LINE_FULL if self.verbose else LinePrinter.LINE_ELIDE)
+
+
+def elide_middle(status, width):
+    margin = 3 # Space for '...'.
+    if len(status) + margin > width:
+        elide_size = (width - margin) // 2
+        status = status[0:elide_size] + '...' + status[-elide_size:]
+    return status
+
+class LinePrinter(object):
+    LINE_FULL = 1
+    LINE_ELIDE = 2
+
+    def __init__(self):
+        # Whether we can do fancy terminal control codes.
+        self.smart_terminal = False
+
+        # Whether the caret is at the beginning of a blank line.
+        self.have_blank_line = True
+
+        # Whether console is locked.
+        self.console_locked = False
+
+        # Buffered current line while console is locked.
+        self.line_buffer = ''
+
+        # Buffered line type while console is locked.
+        self.line_type = self.LINE_FULL
+
+        # Buffered console output while console is locked.
+        self.output_buffer = ''
+
+        if os.name == 'windows':
+            STD_OUTPUT_HANDLE = -11
+            self.console = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE)
+            csbi = ctypes.create_string_buffer(22)
+            self.smart_terminal = ctypes.windll.kernel32.GetConsoleScreenBufferInfo(self.console, csbi)
+        else:
+            term = os.getenv('TERM')
+            self.smart_terminal = os.isatty(1) and term != '' and term != 'dumb'
+
+    def print_line(self, to_print, line_type):
+        if self.console_locked:
+            self.line_buffer = to_print
+            self.line_type = line_type
+
+        if self.smart_terminal:
+            sys.stdout.write('\r') # Print over previous line, if any.
+
+        if self.smart_terminal and line_type == self.LINE_ELIDE:
+            if os.name == 'windows':
+                csbi = ctypes.create_string_buffer(22)
+                ctypes.windll.kernel32.GetConsoleScreenBufferInfo(self.console, csbi)
+                (cols, rows) = struct.unpack('hh', csbi.raw)
+                to_print = elide_middle(to_print, cols)
+                # TODO: windows support
+                # We don't want to have the cursor spamming back and forth, so instead of
+                # printf use WriteConsoleOutput which updates the contents of the buffer,
+                # but doesn't move the cursor position.
+                sys.stdout.write(to_print)
+                sys.stdout.flush()
+            else:
+                # Limit output to width of the terminal if provided so we don't cause
+                # line-wrapping.
+                import fcntl, termios
+                winsize = fcntl.ioctl(0, termios.TIOCGWINSZ, '\0'*4)
+                (rows, cols) = struct.unpack('hh', winsize)
+                to_print = elide_middle(to_print, cols)
+                sys.stdout.write(to_print)
+                sys.stdout.write('\x1B[K')  # Clear to end of line.
+                sys.stdout.flush()
+
+            self.have_blank_line = False
+        else:
+            sys.stdout.write(to_print + '\n')
+            sys.stdout.flush()
+
+    def print_or_buffer(self, to_print):
+        if self.console_locked:
+            self.output_buffer += to_print
+        else:
+            sys.stdout.write(to_print)
+            sys.stdout.flush()
+
+    def print_on_new_line(self, to_print):
+        if self.console_locked or self.line_buffer != '':
+            self.output_buffer += self.line_buffer + '\n'
+            self.line_buffer = ''
+        if not self.have_blank_line:
+            self.print_or_buffer('\n')
+        if to_print != '':
+            self.print_or_buffer(to_print)
+        self.have_blank_line = to_print == '' or to_print[0] == '\n'
+
+    def set_console_locked(self, locked):
+        if locked == self.console_locked:
+            return
+
+        if locked:
+            self.print_on_new_line('\n')
+
+        self.console_locked = locked
+
+        if not locked:
+            self.print_on_new_line(self.output_buffer)
+            if self.line_buffer != '':
+                self.print_line(self.line_buffer, self.line_type)
+            self.output_buffer = ''
+            self.line_buffer = ''
+
+def main():
+    native = NinjaNativeFrontend()
+    for msg in frontend.Frontend():
+        native.handle(msg)
+
+if __name__ == '__main__':
+    main()
diff --git a/misc/generate_proto_header.py b/misc/generate_proto_header.py
new file mode 100755
index 0000000..a07eda6
--- /dev/null
+++ b/misc/generate_proto_header.py
@@ -0,0 +1,491 @@
+#!/usr/bin/env python
+#
+# Copyright 2017 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Write-only protobuf C++ code generator for a minimal runtime
+
+This script uses a descriptor proto generated by protoc and the descriptor_pb2
+distributed with python protobuf to iterate through the fields in a proto
+and write out simple C++ data objects with serialization methods.  The generated
+files depend on a tiny runtime implemented in src/proto.h and src/proto.cc.
+"""
+
+from __future__ import print_function
+
+import os.path
+import re
+import sys
+
+import google.protobuf.descriptor_pb2
+import google.protobuf.descriptor
+
+FieldDescriptor = google.protobuf.descriptor.FieldDescriptor
+
+CPP_TYPE_MAP = {
+    FieldDescriptor.CPPTYPE_INT32: 'int32_t',
+    FieldDescriptor.CPPTYPE_INT64: 'int64_t',
+    FieldDescriptor.CPPTYPE_UINT32: 'uint32_t',
+    FieldDescriptor.CPPTYPE_UINT64: 'uint64_t',
+    FieldDescriptor.CPPTYPE_DOUBLE: 'double',
+    FieldDescriptor.CPPTYPE_FLOAT: 'float',
+    FieldDescriptor.CPPTYPE_BOOL: 'bool',
+    FieldDescriptor.CPPTYPE_STRING: 'std::string',
+}
+
+ENCODING_MAP = {
+    FieldDescriptor.TYPE_INT32:
+        ('VarintSize32SignExtended', 'WriteVarint32SignExtended', None),
+    FieldDescriptor.TYPE_INT64:
+        ('VarintSize64', 'WriteVarint64', None),
+    FieldDescriptor.TYPE_UINT32:
+        ('VarintSize32', 'WriteVarint32', None),
+    FieldDescriptor.TYPE_UINT64:
+        ('VarintSize64', 'WriteVarint64', None),
+    FieldDescriptor.TYPE_SINT32:
+        ('VarintSize32', 'WriteVarint32', 'ZigZagEncode32'),
+    FieldDescriptor.TYPE_SINT64:
+        ('VarintSize64', 'WriteVarint64', 'ZigZagEncode64'),
+    FieldDescriptor.TYPE_BOOL:
+        ('VarintSizeBool', 'WriteVarint32', None),
+    FieldDescriptor.TYPE_ENUM:
+        ('VarintSize32SignExtended', 'WriteVarint32SignExtended',
+         'static_cast<int32_t>'),
+    FieldDescriptor.TYPE_FIXED64:
+        ('FixedSize64', 'WriteFixed64', None),
+    FieldDescriptor.TYPE_SFIXED64:
+        ('FixedSize64', 'WriteFixed64', 'static_cast<uint64_t>'),
+    FieldDescriptor.TYPE_DOUBLE:
+        ('FixedSize64', 'WriteFixed64', 'static_cast<uint64_t>'),
+    FieldDescriptor.TYPE_STRING:
+        ('StringSize', 'WriteString', None),
+    FieldDescriptor.TYPE_BYTES:
+        ('StringSize', 'WriteString', None),
+    FieldDescriptor.TYPE_FIXED32:
+        ('FixedSize32', 'WriteFixed32', None),
+    FieldDescriptor.TYPE_SFIXED32:
+        ('FixedSize32', 'WriteFixed32', 'static_cast<uint32_t>'),
+    FieldDescriptor.TYPE_FLOAT:
+        ('FixedSize32', 'WriteFixed32', 'static_cast<uint32_t>'),
+}
+
+ZIGZAG_LIST = (
+    FieldDescriptor.TYPE_SINT32,
+    FieldDescriptor.TYPE_SINT64,
+    FieldDescriptor.TYPE_SFIXED64,
+    FieldDescriptor.TYPE_SFIXED32,
+)
+
+def field_type_to_cpp_type(field):
+    """Convert a proto field object to its C++ type"""
+    if field.type_name != '':
+        cpp_type = field.type_name.replace('.', '::')
+    else:
+        cpp_type = FieldDescriptor.ProtoTypeToCppProtoType(field.type)
+        cpp_type = CPP_TYPE_MAP[cpp_type]
+    return cpp_type
+
+class Generator:
+    def __init__(self, out):
+        self.w = Writer(out)
+
+    """Class to generate C++ code for a proto descriptor"""
+    def write_enum(self, enum):
+        """Write a proto enum object to the generated file"""
+        self.w.writelines("""
+            enum %(name)s {
+        """ % {
+            'name': enum.name,
+        })
+        self.w.indent()
+        for value in enum.value:
+            self.w.writelines("""
+                %(name)s = %(value)d,
+            """ % {
+                'name': value.name,
+                'value': value.number,
+            })
+        self.w.unindent()
+        self.w.writelines("""
+        };
+
+        """)
+
+    def write_field(self, field, ctor, ser, size, clear, methods):
+        """Write a proto field object to the generated file, including necessary
+        code in the constructor and serialization methods.
+        """
+        field_cpp_type = field_type_to_cpp_type(field)
+        repeated = field.label == FieldDescriptor.LABEL_REPEATED
+
+        element_cpp_type = field_cpp_type
+        if repeated:
+            field_cpp_type = 'std::vector< %s >' % field_cpp_type
+
+        member_name = field.name + '_'
+        element_name = member_name
+
+        # Data declaration
+        self.w.writelines("""
+            %(type)s %(member_name)s;
+            bool has_%(member_name)s;
+        """ % {
+            'type': field_cpp_type,
+            'member_name': member_name,
+        })
+
+        ctor.writelines("""
+            has_%(member_name)s = false;
+        """ % {
+            'member_name': member_name,
+        })
+
+        methods.writelines("""
+                %(type)s* mutable_%(name)s() {
+                  has_%(member_name)s = true;
+                  return &%(member_name)s;
+                }
+            """ % {
+            'name': field.name,
+            'member_name': member_name,
+            'type': field_cpp_type,
+        })
+
+        if repeated:
+            loop = """
+                for (%(type)s::const_iterator it_ = %(member_name)s.begin();
+                    it_ != %(member_name)s.end(); it_++) {
+            """ % {
+                'member_name': member_name,
+                'type': field_cpp_type,
+            }
+
+            ser.writelines(loop)
+            ser.indent()
+
+            size.writelines(loop)
+            size.indent()
+
+            methods.writelines("""
+                void add_%(name)s(const %(type)s& value) {
+                  has_%(member_name)s = true;
+                  %(member_name)s.push_back(value);
+                }
+            """ % {
+                'name': field.name,
+                'member_name': member_name,
+                'type': element_cpp_type,
+            })
+
+            element_name = '*it_'
+
+        if field.type == FieldDescriptor.TYPE_MESSAGE:
+            ser.writelines("""
+                if (has_%(member_name)s) {
+                  WriteLengthDelimited(output__, %(number)s,
+                                       %(member_name)s.ByteSizeLong());
+                  %(member_name)s.SerializeToOstream(output__);
+                }
+            """ % {
+                'member_name': element_name,
+                'number': field.number,
+            })
+
+            size.writelines("""
+                if (has_%(member_name)s) {
+                  size += 1 + VarintSize32(%(member_name)s.ByteSizeLong());
+                  size += %(member_name)s.ByteSizeLong();
+                }
+            """ % {
+                'member_name': element_name,
+            })
+
+            clear.writelines("""
+                if (has_%(member_name)s) {
+                  %(member_name)s.Clear();
+                  has_%(member_name)s = false;
+                }
+            """ % {
+                'member_name': member_name,
+            })
+        else:
+            (sizer, serializer, formatter) = ENCODING_MAP[field.type]
+            if formatter != None:
+                element_name = '%s(%s)' % (formatter, element_name)
+
+            ser.writelines("""
+                %(serializer)s(output__, %(field_number)s, %(element_name)s);
+            """ % {
+                'serializer': serializer,
+                'field_number': field.number,
+                'element_name': element_name,
+            })
+
+            size.writelines("""
+                size += %(sizer)s(%(element_name)s) + 1;
+            """ % {
+                'sizer': sizer,
+                'element_name': element_name,
+            })
+
+            if repeated or field.type == FieldDescriptor.CPPTYPE_STRING:
+                clear.writelines("""
+                    %(member_name)s.clear();
+                """ % {
+                    'member_name': member_name,
+                })
+            else:
+                reset = """
+                    %(member_name)s = static_cast< %(type)s >(0);
+                """ % {
+                    'member_name': member_name,
+                    'type': field_cpp_type,
+                }
+                ctor.writelines(reset)
+                clear.writelines(reset)
+
+            methods.writelines("""
+                void set_%(name)s(const %(type)s& value) {
+                  has_%(member_name)s = true;
+                  %(member_name)s = value;
+                }
+            """ % {
+                'name': field.name,
+                'member_name': member_name,
+                'type': field_cpp_type,
+            })
+
+        if repeated:
+            ser.unindent()
+            ser.writelines('}')
+            size.unindent()
+            size.writelines('}')
+
+    def func(self, f):
+        return self.w.stringwriter(prefix=f + ' {', suffix='}\n\n')
+
+    def write_message(self, message):
+        """Write a proto message object to the generated file, recursing into
+        nested messages, enums, and fields.
+        """
+        self.w.writelines("""
+            struct %(name)s {
+        """ % {
+            'name': message.name,
+        })
+        self.w.indent()
+
+        # Constructor
+        ctor = self.func(message.name + '()')
+
+        # SerializeToOstream method
+        ser = self.func('void SerializeToOstream(std::ostream* output__) const')
+
+        size = self.func('size_t ByteSizeLong() const')
+        size.writelines("""
+            size_t size = 0;
+        """)
+
+        clear = self.func('void Clear()')
+
+        methods = self.w.stringwriter()
+
+        # Nested message type declarations
+        for nested in message.nested_type:
+            self.write_message(nested)
+
+        # Nested enum type declarations
+        for enum in message.enum_type:
+            self.write_enum(enum)
+
+        # Message fields
+        for field in message.field:
+            self.write_field(field, ctor, ser, size, clear, methods)
+        if len(message.field) > 0:
+            self.w.newline()
+
+        self.w.writelines(ctor.string())
+
+        # Disallow copy and assign constructors
+        self.w.writelines("""
+            %(name)s(const %(name)s&);
+            void operator=(const %(name)s&);
+
+        """ % {
+            'name': message.name,
+        })
+
+        # SerializeToOstream method
+        self.w.writelines(ser.string())
+
+        # ByteSizeLong method
+        size.writelines('return size;')
+        self.w.writelines(size.string())
+
+        # Clear method
+        self.w.writelines(clear.string())
+
+        # Accessor methods
+        self.w.write(methods.string())
+
+        self.w.unindent()
+        self.w.writelines("""
+            };
+
+        """)
+
+    def write_proto(self, output_file, proto):
+        header_guard = 'NINJA_' + os.path.basename(output_file).upper()
+        header_guard = re.sub('[^a-zA-Z]', '_', header_guard)
+
+        self.w.writelines("""
+            // This file is autogenerated by %(generator)s, do not edit
+
+            #ifndef %(header_guard)s
+            #define %(header_guard)s
+
+            #include <inttypes.h>
+
+            #include <iostream>
+            #include <string>
+            #include <vector>
+
+            #include "proto.h"
+
+            namespace %(namespace)s {
+        """ % {
+            'generator': os.path.basename(sys.argv[0]),
+            'header_guard': header_guard,
+            'namespace': proto.package,
+        })
+
+        for enum in proto.enum_type:
+            self.write_enum(enum)
+        for message in proto.message_type:
+            self.write_message(message)
+
+        self.w.writelines("""
+        }
+        #endif // %(header_guard)s
+        """ % {
+          'header_guard': header_guard,
+        })
+
+class Writer:
+    """Class to write code to a generated file"""
+    def __init__(self, w, indent=0):
+        self.w = w
+        self.cur_indent = indent
+
+    def write(self, s):
+        self.w.write(s)
+
+    def writeln(self, s):
+        if len(s) > 0:
+            self.write(' '*self.cur_indent + s + '\n')
+        else:
+            self.newline()
+
+    def indent(self):
+        self.cur_indent = self.cur_indent + 2
+
+    def unindent(self):
+        self.cur_indent = self.cur_indent - 2
+
+    def newline(self):
+        self.write('\n')
+
+    def writelines(self, s):
+        lines = s.split('\n')
+        if len(lines) > 0:
+            if len(lines[0].strip()) == 0:
+                lines = lines[1:]
+        if len(lines) > 0:
+            first_indent = initial_indent(lines[0])
+
+            for line in lines[:-1]:
+                indent = min(initial_indent(line), first_indent)
+                self.writeln(line[indent:])
+
+            indent = min(initial_indent(lines[-1]), first_indent)
+            if lines[-1][indent:] != '':
+                self.writeln(lines[-1][indent:])
+
+    def stringwriter(self, prefix='', suffix=''):
+        """Returns an object with the same interface as Writer that buffers
+        its writes to be written out later.
+        """
+        return StringWriter(self.cur_indent, prefix, suffix)
+
+def initial_indent(s):
+    return len(s)-len(s.lstrip(' '))
+
+class StringWriter(Writer):
+    """Subclass of Writer that buffers its writes to be written out later."""
+    def __init__(self, indent, prefix, suffix):
+        self.buf = ''
+        self.prefix = prefix
+        self.suffix = suffix
+        self.cur_indent = indent
+        if self.prefix != '':
+            self.writelines(self.prefix)
+            self.indent()
+
+    def string(self):
+        if self.prefix != '':
+            self.unindent()
+            if self.suffix != '':
+                self.writelines(self.suffix)
+        return self.buf
+
+    def write(self, s):
+        self.buf += s
+
+def main():
+    if len(sys.argv) == 2 and sys.argv[1] == '--probe':
+        print('ok')
+        return
+
+    if len(sys.argv) != 3:
+        print('usage: %s <in> <out>' % sys.argv[0])
+        sys.exit(1)
+
+    input_file = sys.argv[1]
+    output_file = sys.argv[2]
+    tmp_output_file = output_file + '.tmp'
+
+    set = google.protobuf.descriptor_pb2.FileDescriptorSet()
+    try:
+        with open(input_file, 'rb') as f:
+            set.ParseFromString(f.read())
+    except IOError:
+        print('failed to read ' + input_file)
+        sys.exit(2)
+
+    if len(set.file) != 1:
+        print('expected exactly one file descriptor in ' + input_file)
+        print(set)
+        sys.exit(3)
+
+    proto = set.file[0]
+
+    with open(tmp_output_file, 'w') as out:
+        w = Generator(out)
+
+        w.write_proto(output_file, proto)
+
+    os.rename(tmp_output_file, output_file)
+
+if __name__ == '__main__':
+    main()
diff --git a/misc/generate_proto_header_test.py b/misc/generate_proto_header_test.py
new file mode 100755
index 0000000..318e803
--- /dev/null
+++ b/misc/generate_proto_header_test.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+#
+# Copyright 2017 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Write-only protobuf C++ code generator for a minimal runtime
+
+This script uses a descriptor proto generated by protoc and the descriptor_pb2
+distributed with python protobuf to iterate through the fields in a proto
+and write out simple C++ data objects with serialization methods.  The generated
+files depend on a tiny runtime implemented in src/proto.h and src/proto.cc.
+"""
+
+from __future__ import print_function
+
+import unittest
+
+try:
+    from StringIO import StringIO
+except ImportError:
+    from io import StringIO
+
+import generate_proto_header
+
+class TestWriteLines(unittest.TestCase):
+    def setUp(self):
+        self.out = StringIO()
+        self.w = generate_proto_header.Writer(self.out)
+
+    def test_single_line(self):
+        self.w.writelines('abc')
+        self.assertEqual(self.out.getvalue(), 'abc\n')
+
+    def test_multi_line(self):
+        self.w.writelines("""
+            abc
+            def
+        """)
+        self.assertEqual(self.out.getvalue(), 'abc\ndef\n')
+
+    def test_multi_line_with_indent(self):
+        self.w.writelines("""
+            abc
+              def
+        """)
+        self.assertEqual(self.out.getvalue(), 'abc\n  def\n')
+
+    def test_string_writer(self):
+        w = self.w.stringwriter()
+        w.writelines("""
+            abc
+              def
+        """)
+        self.w.writelines(w.string())
+        self.assertEqual(self.out.getvalue(), 'abc\n  def\n')
+
+    def test_string_writer_prefix(self):
+        w = self.w.stringwriter(
+            prefix="""
+                abc
+                  def
+            """,
+            suffix="""
+                lmnop
+            """)
+        w.writelines("""
+            ghi
+              jk
+        """)
+        self.w.writelines(w.string())
+        self.assertEqual(self.out.getvalue(),
+                         'abc\n  def\n  ghi\n    jk\nlmnop\n')
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/src/build.cc b/src/build.cc
index b392803..2f8b860 100644
--- a/src/build.cc
+++ b/src/build.cc
@@ -36,7 +36,9 @@
 #include "deps_log.h"
 #include "disk_interface.h"
 #include "graph.h"
+#include "metrics.h"
 #include "state.h"
+#include "status.h"
 #include "subprocess.h"
 #include "util.h"
 
@@ -76,217 +78,6 @@
 
 }  // namespace
 
-BuildStatus::BuildStatus(const BuildConfig& config)
-    : config_(config),
-      start_time_millis_(GetTimeMillis()),
-      started_edges_(0), finished_edges_(0), total_edges_(0),
-      progress_status_format_(NULL),
-      overall_rate_(), current_rate_(config.parallelism) {
-
-  // Don't do anything fancy in verbose mode.
-  if (config_.verbosity != BuildConfig::NORMAL)
-    printer_.set_smart_terminal(false);
-
-  progress_status_format_ = getenv("NINJA_STATUS");
-  if (!progress_status_format_)
-    progress_status_format_ = "[%f/%t] ";
-}
-
-void BuildStatus::PlanHasTotalEdges(int total) {
-  total_edges_ = total;
-}
-
-void BuildStatus::BuildEdgeStarted(Edge* edge) {
-  int start_time = (int)(GetTimeMillis() - start_time_millis_);
-  running_edges_.insert(make_pair(edge, start_time));
-  ++started_edges_;
-
-  if (edge->use_console() || printer_.is_smart_terminal())
-    PrintStatus(edge, kEdgeStarted);
-
-  if (edge->use_console())
-    printer_.SetConsoleLocked(true);
-}
-
-void BuildStatus::BuildEdgeFinished(Edge* edge,
-                                    bool success,
-                                    const string& output,
-                                    int* start_time,
-                                    int* end_time) {
-  int64_t now = GetTimeMillis();
-
-  ++finished_edges_;
-
-  RunningEdgeMap::iterator i = running_edges_.find(edge);
-  *start_time = i->second;
-  *end_time = (int)(now - start_time_millis_);
-  running_edges_.erase(i);
-
-  if (edge->use_console())
-    printer_.SetConsoleLocked(false);
-
-  if (config_.verbosity == BuildConfig::QUIET)
-    return;
-
-  if (!edge->use_console())
-    PrintStatus(edge, kEdgeFinished);
-
-  // Print the command that is spewing before printing its output.
-  if (!success) {
-    string outputs;
-    for (vector<Node*>::const_iterator o = edge->outputs_.begin();
-         o != edge->outputs_.end(); ++o)
-      outputs += (*o)->path() + " ";
-
-    printer_.PrintOnNewLine("FAILED: " + outputs + "\n");
-    printer_.PrintOnNewLine(edge->EvaluateCommand() + "\n");
-  }
-
-  if (!output.empty()) {
-    // ninja sets stdout and stderr of subprocesses to a pipe, to be able to
-    // check if the output is empty. Some compilers, e.g. clang, check
-    // isatty(stderr) to decide if they should print colored output.
-    // To make it possible to use colored output with ninja, subprocesses should
-    // be run with a flag that forces them to always print color escape codes.
-    // To make sure these escape codes don't show up in a file if ninja's output
-    // is piped to a file, ninja strips ansi escape codes again if it's not
-    // writing to a |smart_terminal_|.
-    // (Launching subprocesses in pseudo ttys doesn't work because there are
-    // only a few hundred available on some systems, and ninja can launch
-    // thousands of parallel compile commands.)
-    string final_output;
-    if (!printer_.supports_color())
-      final_output = StripAnsiEscapeCodes(output);
-    else
-      final_output = output;
-
-#ifdef _WIN32
-    // Fix extra CR being added on Windows, writing out CR CR LF (#773)
-    _setmode(_fileno(stdout), _O_BINARY);  // Begin Windows extra CR fix
-#endif
-
-    printer_.PrintOnNewLine(final_output);
-
-#ifdef _WIN32
-    _setmode(_fileno(stdout), _O_TEXT);  // End Windows extra CR fix
-#endif
-  }
-}
-
-void BuildStatus::BuildStarted() {
-  overall_rate_.Restart();
-  current_rate_.Restart();
-}
-
-void BuildStatus::BuildFinished() {
-  printer_.SetConsoleLocked(false);
-  printer_.PrintOnNewLine("");
-}
-
-string BuildStatus::FormatProgressStatus(
-    const char* progress_status_format, EdgeStatus status) const {
-  string out;
-  char buf[32];
-  int percent;
-  for (const char* s = progress_status_format; *s != '\0'; ++s) {
-    if (*s == '%') {
-      ++s;
-      switch (*s) {
-      case '%':
-        out.push_back('%');
-        break;
-
-        // Started edges.
-      case 's':
-        snprintf(buf, sizeof(buf), "%d", started_edges_);
-        out += buf;
-        break;
-
-        // Total edges.
-      case 't':
-        snprintf(buf, sizeof(buf), "%d", total_edges_);
-        out += buf;
-        break;
-
-        // Running edges.
-      case 'r': {
-        int running_edges = started_edges_ - finished_edges_;
-        // count the edge that just finished as a running edge
-        if (status == kEdgeFinished)
-          running_edges++;
-        snprintf(buf, sizeof(buf), "%d", running_edges);
-        out += buf;
-        break;
-      }
-
-        // Unstarted edges.
-      case 'u':
-        snprintf(buf, sizeof(buf), "%d", total_edges_ - started_edges_);
-        out += buf;
-        break;
-
-        // Finished edges.
-      case 'f':
-        snprintf(buf, sizeof(buf), "%d", finished_edges_);
-        out += buf;
-        break;
-
-        // Overall finished edges per second.
-      case 'o':
-        overall_rate_.UpdateRate(finished_edges_);
-        SnprintfRate(overall_rate_.rate(), buf, "%.1f");
-        out += buf;
-        break;
-
-        // Current rate, average over the last '-j' jobs.
-      case 'c':
-        current_rate_.UpdateRate(finished_edges_);
-        SnprintfRate(current_rate_.rate(), buf, "%.1f");
-        out += buf;
-        break;
-
-        // Percentage
-      case 'p':
-        percent = (100 * finished_edges_) / total_edges_;
-        snprintf(buf, sizeof(buf), "%3i%%", percent);
-        out += buf;
-        break;
-
-      case 'e': {
-        double elapsed = overall_rate_.Elapsed();
-        snprintf(buf, sizeof(buf), "%.3f", elapsed);
-        out += buf;
-        break;
-      }
-
-      default:
-        Fatal("unknown placeholder '%%%c' in $NINJA_STATUS", *s);
-        return "";
-      }
-    } else {
-      out.push_back(*s);
-    }
-  }
-
-  return out;
-}
-
-void BuildStatus::PrintStatus(Edge* edge, EdgeStatus status) {
-  if (config_.verbosity == BuildConfig::QUIET)
-    return;
-
-  bool force_full_command = config_.verbosity == BuildConfig::VERBOSE;
-
-  string to_print = edge->GetBinding("description");
-  if (to_print.empty() || force_full_command)
-    to_print = edge->GetBinding("command");
-
-  to_print = FormatProgressStatus(progress_status_format_, status) + to_print;
-
-  printer_.Print(to_print,
-                 force_full_command ? LinePrinter::FULL : LinePrinter::ELIDE);
-}
-
 Plan::Plan() : command_edges_(0), wanted_edges_(0) {}
 
 void Plan::Reset() {
@@ -348,7 +139,7 @@
 Edge* Plan::FindWork() {
   if (ready_.empty())
     return NULL;
-  set<Edge*>::iterator e = ready_.begin();
+  EdgeSet::iterator e = ready_.begin();
   Edge* edge = *e;
   ready_.erase(e);
   return edge;
@@ -404,8 +195,9 @@
 
 void Plan::NodeFinished(Node* node) {
   // See if we we want any edges from this node.
-  for (vector<Edge*>::const_iterator oe = node->out_edges().begin();
-       oe != node->out_edges().end(); ++oe) {
+  const std::vector<Edge*> out_edges = node->GetOutEdges();
+  for (vector<Edge*>::const_iterator oe = out_edges.begin();
+       oe != out_edges.end(); ++oe) {
     map<Edge*, Want>::iterator want_e = want_.find(*oe);
     if (want_e == want_.end())
       continue;
@@ -426,8 +218,9 @@
 bool Plan::CleanNode(DependencyScan* scan, Node* node, string* err) {
   node->set_dirty(false);
 
-  for (vector<Edge*>::const_iterator oe = node->out_edges().begin();
-       oe != node->out_edges().end(); ++oe) {
+  const std::vector<Edge*> out_edges = node->GetOutEdges();
+  for (vector<Edge*>::const_iterator oe = out_edges.begin();
+       oe != out_edges.end(); ++oe) {
     // Don't process edges that we don't actually want.
     map<Edge*, Want>::iterator want_e = want_.find(*oe);
     if (want_e == want_.end() || want_e->second == kWantNothing)
@@ -437,6 +230,10 @@
     if ((*oe)->deps_missing_)
       continue;
 
+    // No need to clean a phony output edge, as it's always dirty
+    if ((*oe)->IsPhonyOutput())
+      continue;
+
     // If all non-order-only inputs for this edge are now clean,
     // we might have changed the dirty state of the outputs.
     vector<Node*>::iterator
@@ -543,6 +340,9 @@
   }
 
   result->status = subproc->Finish();
+#ifndef _WIN32
+  result->rusage = *subproc->GetUsage();
+#endif
   result->output = subproc->GetOutput();
 
   map<Subprocess*, Edge*>::iterator e = subproc_to_edge_.find(subproc);
@@ -555,11 +355,12 @@
 
 Builder::Builder(State* state, const BuildConfig& config,
                  BuildLog* build_log, DepsLog* deps_log,
-                 DiskInterface* disk_interface)
-    : state_(state), config_(config), disk_interface_(disk_interface),
+                 DiskInterface* disk_interface, Status* status,
+                 int64_t start_time_millis)
+    : state_(state), config_(config), status_(status),
+      start_time_millis_(start_time_millis), disk_interface_(disk_interface),
       scan_(state, build_log, deps_log, disk_interface,
-            &config_.depfile_parser_options) {
-  status_ = new BuildStatus(config);
+            &config_.depfile_parser_options, config.uses_phony_outputs) {
 }
 
 Builder::~Builder() {
@@ -573,6 +374,8 @@
 
     for (vector<Edge*>::iterator e = active_edges.begin();
          e != active_edges.end(); ++e) {
+      if ((*e)->IsPhonyOutput())
+        continue;
       string depfile = (*e)->GetUnescapedDepfile();
       for (vector<Node*>::iterator o = (*e)->outputs_.begin();
            o != (*e)->outputs_.end(); ++o) {
@@ -584,10 +387,11 @@
         // mentioned in a depfile, and the command touches its depfile
         // but is interrupted before it touches its output file.)
         string err;
-        TimeStamp new_mtime = disk_interface_->Stat((*o)->path(), &err);
-        if (new_mtime == -1)  // Log and ignore Stat() errors.
-          Error("%s", err.c_str());
-        if (!depfile.empty() || (*o)->mtime() != new_mtime)
+        bool is_dir = false;
+        TimeStamp new_mtime = disk_interface_->LStat((*o)->path(), &is_dir, &err);
+        if (new_mtime == -1)  // Log and ignore LStat() errors.
+          status_->Error("%s", err.c_str());
+        if (!is_dir && (!depfile.empty() || (*o)->mtime() != new_mtime))
           disk_interface_->RemoveFile((*o)->path());
       }
       if (!depfile.empty())
@@ -602,22 +406,41 @@
     *err = "unknown target: '" + name + "'";
     return NULL;
   }
-  if (!AddTarget(node, err))
+  if (!AddTargets({ node }, err))
     return NULL;
   return node;
 }
 
-bool Builder::AddTarget(Node* node, string* err) {
-  if (!scan_.RecomputeDirty(node, err))
+bool Builder::AddTargets(const std::vector<Node*> &nodes, string* err) {
+  std::vector<Node*> validation_nodes;
+  if (!scan_.RecomputeNodesDirty(nodes, &validation_nodes, err))
     return false;
 
-  if (Edge* in_edge = node->in_edge()) {
-    if (in_edge->outputs_ready())
-      return true;  // Nothing to do.
+  for (Node* node : nodes) {
+    std::string plan_err;
+    if (!plan_.AddTarget(node, &plan_err)) {
+      if (!plan_err.empty()) {
+        *err = plan_err;
+        return false;
+      } else {
+        // Added a target that is already up-to-date; not really
+        // an error.
+      }
+    }
   }
 
-  if (!plan_.AddTarget(node, err))
-    return false;
+  for (Node* node : validation_nodes) {
+    std::string plan_err;
+    if (!plan_.AddTarget(node, &plan_err)) {
+      if (!plan_err.empty()) {
+        *err = plan_err;
+        return false;
+      } else {
+        // Added a target that is already up-to-date; not really
+        // an error.
+      }
+    }
+  }
 
   return true;
 }
@@ -721,14 +544,29 @@
   if (edge->is_phony())
     return true;
 
-  status_->BuildEdgeStarted(edge);
+  int64_t start_time_millis = GetTimeMillis() - start_time_millis_;
+  running_edges_.insert(make_pair(edge, start_time_millis));
 
-  // Create directories necessary for outputs.
-  // XXX: this will block; do we care?
-  for (vector<Node*>::iterator o = edge->outputs_.begin();
-       o != edge->outputs_.end(); ++o) {
-    if (!disk_interface_->MakeDirs((*o)->path()))
-      return false;
+  status_->BuildEdgeStarted(edge, start_time_millis);
+
+  if (!edge->IsPhonyOutput()) {
+    for (vector<Node*>::iterator o = edge->outputs_.begin();
+         o != edge->outputs_.end(); ++o) {
+      // Create directories necessary for outputs.
+      // XXX: this will block; do we care?
+      if (!disk_interface_->MakeDirs((*o)->path()))
+        return false;
+
+      if (!(*o)->exists())
+        continue;
+
+      // Remove existing outputs for non-restat rules.
+      // XXX: this will block; do we care?
+      if (config_.pre_remove_output_files && !edge->IsRestat() && !config_.dry_run) {
+        if (disk_interface_->RemoveFile((*o)->path()) < 0)
+          return false;
+      }
+    }
   }
 
   // Create response file, if needed
@@ -753,72 +591,115 @@
   METRIC_RECORD("FinishCommand");
 
   Edge* edge = result->edge;
+  bool phony_output = edge->IsPhonyOutput();
 
-  // First try to extract dependencies from the result, if any.
-  // This must happen first as it filters the command output (we want
-  // to filter /showIncludes output, even on compile failure) and
-  // extraction itself can fail, which makes the command fail from a
-  // build perspective.
   vector<Node*> deps_nodes;
   string deps_type = edge->GetBinding("deps");
-  const string deps_prefix = edge->GetBinding("msvc_deps_prefix");
-  if (!deps_type.empty()) {
-    string extract_err;
-    if (!ExtractDeps(result, deps_type, deps_prefix, &deps_nodes,
-                     &extract_err) &&
-        result->success()) {
-      if (!result->output.empty())
-        result->output.append("\n");
-      result->output.append(extract_err);
-      result->status = ExitFailure;
+  if (!phony_output) {
+    // First try to extract dependencies from the result, if any.
+    // This must happen first as it filters the command output (we want
+    // to filter /showIncludes output, even on compile failure) and
+    // extraction itself can fail, which makes the command fail from a
+    // build perspective.
+    const string deps_prefix = edge->GetBinding("msvc_deps_prefix");
+    if (!deps_type.empty()) {
+      string extract_err;
+      if (!ExtractDeps(result, deps_type, deps_prefix, &deps_nodes,
+                       &extract_err) &&
+          result->success()) {
+        if (!result->output.empty())
+          result->output.append("\n");
+        result->output.append(extract_err);
+        result->status = ExitFailure;
+      }
     }
   }
 
-  int start_time, end_time;
-  status_->BuildEdgeFinished(edge, result->success(), result->output,
-                             &start_time, &end_time);
-
-  // The rest of this function only applies to successful commands.
-  if (!result->success()) {
-    plan_.EdgeFinished(edge, Plan::kEdgeFailed);
-    return true;
-  }
+  int64_t start_time_millis, end_time_millis;
+  RunningEdgeMap::iterator i = running_edges_.find(edge);
+  start_time_millis = i->second;
+  end_time_millis = GetTimeMillis() - start_time_millis_;
+  running_edges_.erase(i);
 
   // Restat the edge outputs
   TimeStamp output_mtime = 0;
-  bool restat = edge->GetBindingBool("restat");
-  if (!config_.dry_run) {
-    bool node_cleaned = false;
+  if (result->success() && !config_.dry_run && !phony_output) {
+    bool restat = edge->IsRestat();
+    vector<Node*> nodes_cleaned;
+
+    TimeStamp newest_input = 0;
+    Node* newest_input_node = nullptr;
+    for (vector<Node*>::iterator i = edge->inputs_.begin();
+         i != edge->inputs_.end() - edge->order_only_deps_; ++i) {
+      TimeStamp input_mtime = (*i)->mtime();
+      if (input_mtime == -1)
+        return false;
+      if (input_mtime > newest_input) {
+        newest_input = input_mtime;
+        newest_input_node = (*i);
+      }
+    }
 
     for (vector<Node*>::iterator o = edge->outputs_.begin();
          o != edge->outputs_.end(); ++o) {
-      TimeStamp new_mtime = disk_interface_->Stat((*o)->path(), err);
-      if (new_mtime == -1)
+      bool is_dir = false;
+      TimeStamp old_mtime = (*o)->mtime();
+      if (!(*o)->LStat(disk_interface_, &is_dir, err))
         return false;
+      TimeStamp new_mtime = (*o)->mtime();
+      if (config_.uses_phony_outputs) {
+        if (new_mtime == 0) {
+          if (!result->output.empty())
+            result->output.append("\n");
+          result->output.append("ninja: output file missing after successful execution: ");
+          result->output.append((*o)->path());
+          if (config_.missing_output_file_should_err) {
+            result->status = ExitFailure;
+          }
+        } else if (!restat && new_mtime < newest_input) {
+          if (!result->output.empty())
+            result->output.append("\n");
+          result->output.append("ninja: Missing `restat`? An output file is older than the most recent input:\n output: ");
+          result->output.append((*o)->path());
+          result->output.append("\n  input: ");
+          result->output.append(newest_input_node->path());
+          if (config_.old_output_should_err) {
+            result->status = ExitFailure;
+          }
+        }
+        if (is_dir) {
+          if (!result->output.empty())
+            result->output.append("\n");
+          result->output.append("ninja: outputs should be files, not directories: ");
+          result->output.append((*o)->path());
+          if (config_.output_directory_should_err) {
+            result->status = ExitFailure;
+          }
+        }
+      }
       if (new_mtime > output_mtime)
         output_mtime = new_mtime;
-      if ((*o)->mtime() == new_mtime && restat) {
+      if (old_mtime == new_mtime && restat) {
+        nodes_cleaned.push_back(*o);
+        continue;
+      }
+    }
+
+    status_->BuildEdgeFinished(edge, end_time_millis, result);
+
+    if (result->success() && !nodes_cleaned.empty()) {
+      for (vector<Node*>::iterator o = nodes_cleaned.begin();
+           o != nodes_cleaned.end(); ++o) {
         // The rule command did not change the output.  Propagate the clean
         // state through the build graph.
         // Note that this also applies to nonexistent outputs (mtime == 0).
         if (!plan_.CleanNode(&scan_, *o, err))
           return false;
-        node_cleaned = true;
       }
-    }
 
-    if (node_cleaned) {
-      TimeStamp restat_mtime = 0;
       // If any output was cleaned, find the most recent mtime of any
       // (existing) non-order-only input or the depfile.
-      for (vector<Node*>::iterator i = edge->inputs_.begin();
-           i != edge->inputs_.end() - edge->order_only_deps_; ++i) {
-        TimeStamp input_mtime = disk_interface_->Stat((*i)->path(), err);
-        if (input_mtime == -1)
-          return false;
-        if (input_mtime > restat_mtime)
-          restat_mtime = input_mtime;
-      }
+      TimeStamp restat_mtime = newest_input;
 
       string depfile = edge->GetUnescapedDepfile();
       if (restat_mtime != 0 && deps_type.empty() && !depfile.empty()) {
@@ -835,27 +716,33 @@
 
       output_mtime = restat_mtime;
     }
+  } else {
+    status_->BuildEdgeFinished(edge, end_time_millis, result);
   }
 
-  plan_.EdgeFinished(edge, Plan::kEdgeSucceeded);
+  plan_.EdgeFinished(edge, result->success() ? Plan::kEdgeSucceeded : Plan::kEdgeFailed);
+
+  // The rest of this function only applies to successful commands.
+  if (!result->success()) {
+    return true;
+  }
 
   // Delete any left over response file.
   string rspfile = edge->GetUnescapedRspfile();
   if (!rspfile.empty() && !g_keep_rsp)
     disk_interface_->RemoveFile(rspfile);
 
-  if (scan_.build_log()) {
-    if (!scan_.build_log()->RecordCommand(edge, start_time, end_time,
-                                          output_mtime)) {
+  if (scan_.build_log() && !phony_output) {
+    if (!scan_.build_log()->RecordCommand(edge, start_time_millis,
+                                          end_time_millis, output_mtime)) {
       *err = string("Error writing to build log: ") + strerror(errno);
       return false;
     }
   }
 
-  if (!deps_type.empty() && !config_.dry_run) {
-    assert(edge->outputs_.size() == 1 && "should have been rejected by parser");
+  if (!deps_type.empty() && !config_.dry_run && !phony_output) {
     Node* out = edge->outputs_[0];
-    TimeStamp deps_mtime = disk_interface_->Stat(out->path(), err);
+    TimeStamp deps_mtime = disk_interface_->LStat(out->path(), nullptr, err);
     if (deps_mtime == -1)
       return false;
     if (!scan_.deps_log()->RecordDeps(out, deps_mtime, deps_nodes)) {
@@ -900,6 +787,15 @@
       break;
     case DiskInterface::NotFound:
       err->clear();
+      // We only care if the depfile is missing when the tool succeeded.
+      if (!config_.dry_run && result->status == ExitSuccess) {
+        if (config_.missing_depfile_should_err) {
+          *err = string("depfile is missing");
+          return false;
+        } else {
+          status_->Warning("depfile is missing (%s for %s)", depfile.c_str(), result->edge->outputs_[0]->path().c_str());
+        }
+      }
       break;
     case DiskInterface::OtherError:
       return false;
diff --git a/src/build.h b/src/build.h
index a42b8d4..5c06f8b 100644
--- a/src/build.h
+++ b/src/build.h
@@ -19,23 +19,24 @@
 #include <map>
 #include <memory>
 #include <queue>
-#include <set>
 #include <string>
 #include <vector>
 
+#ifndef _WIN32
+#include <sys/resource.h>
+#endif
+
 #include "depfile_parser.h"
 #include "graph.h"  // XXX needed for DependencyScan; should rearrange.
 #include "exit_status.h"
-#include "line_printer.h"
-#include "metrics.h"
 #include "util.h"  // int64_t
 
 struct BuildLog;
-struct BuildStatus;
 struct DiskInterface;
 struct Edge;
 struct Node;
 struct State;
+struct Status;
 
 /// Plan stores the state of a build plan: what we intend to build,
 /// which steps we're ready to execute.
@@ -103,7 +104,7 @@
   /// we want for the edge.
   map<Edge*, Want> want_;
 
-  set<Edge*> ready_;
+  EdgeSet ready_;
 
   /// Total number of edges that have commands (not phony).
   int command_edges_;
@@ -125,6 +126,9 @@
     Result() : edge(NULL) {}
     Edge* edge;
     ExitStatus status;
+#ifndef _WIN32
+    struct rusage rusage;
+#endif
     string output;
     bool success() const { return status == ExitSuccess; }
   };
@@ -138,7 +142,14 @@
 /// Options (e.g. verbosity, parallelism) passed to a build.
 struct BuildConfig {
   BuildConfig() : verbosity(NORMAL), dry_run(false), parallelism(1),
-                  failures_allowed(1), max_load_average(-0.0f) {}
+                  failures_allowed(1), max_load_average(-0.0f),
+                  frontend(NULL), frontend_file(NULL),
+                  missing_depfile_should_err(false),
+                  uses_phony_outputs(false),
+                  output_directory_should_err(false),
+                  missing_output_file_should_err(false),
+                  old_output_should_err(false),
+                  pre_remove_output_files(false) {}
 
   enum Verbosity {
     NORMAL,
@@ -153,23 +164,51 @@
   /// means that we do not have any limit.
   double max_load_average;
   DepfileParserOptions depfile_parser_options;
+
+  /// Command to execute to handle build output
+  const char* frontend;
+
+  /// File to write build output to
+  const char* frontend_file;
+
+  /// Whether a missing depfile should warn or print an error.
+  bool missing_depfile_should_err;
+
+  /// Whether the generator uses 'phony_output's
+  /// Controls the warnings below
+  bool uses_phony_outputs;
+
+  /// Whether an output can be a directory
+  bool output_directory_should_err;
+
+  /// Whether a missing output file should warn or print an error.
+  bool missing_output_file_should_err;
+
+  /// Whether an output with an older timestamp than the inputs should
+  /// warn or print an error.
+  bool old_output_should_err;
+
+  /// Whether to remove outputs before executing rule commands
+  bool pre_remove_output_files;
 };
 
 /// Builder wraps the build process: starting commands, updating status.
 struct Builder {
   Builder(State* state, const BuildConfig& config,
           BuildLog* build_log, DepsLog* deps_log,
-          DiskInterface* disk_interface);
+          DiskInterface* disk_interface, Status* status,
+          int64_t start_time_millis);
   ~Builder();
 
   /// Clean up after interrupted commands by deleting output files.
   void Cleanup();
 
+  /// Used by tests.
   Node* AddTarget(const string& name, string* err);
 
-  /// Add a target to the build, scanning dependencies.
+  /// Add targets to the build, scanning dependencies.
   /// @return false on error.
-  bool AddTarget(Node* target, string* err);
+  bool AddTargets(const std::vector<Node*>& targets, string* err);
 
   /// Returns true if the build targets are already up to date.
   bool AlreadyUpToDate() const;
@@ -197,13 +236,20 @@
 #else
   unique_ptr<CommandRunner> command_runner_;  // auto_ptr was removed in C++17.
 #endif
-  BuildStatus* status_;
+  Status* status_;
 
  private:
    bool ExtractDeps(CommandRunner::Result* result, const string& deps_type,
                     const string& deps_prefix, vector<Node*>* deps_nodes,
                     string* err);
 
+  /// Map of running edge to time the edge started running.
+  typedef map<Edge*, int> RunningEdgeMap;
+  RunningEdgeMap running_edges_;
+
+  /// Time the build started.
+  int64_t start_time_millis_;
+
   DiskInterface* disk_interface_;
   DependencyScan scan_;
 
@@ -212,102 +258,4 @@
   void operator=(const Builder &other); // DO NOT IMPLEMENT
 };
 
-/// Tracks the status of a build: completion fraction, printing updates.
-struct BuildStatus {
-  explicit BuildStatus(const BuildConfig& config);
-  void PlanHasTotalEdges(int total);
-  void BuildEdgeStarted(Edge* edge);
-  void BuildEdgeFinished(Edge* edge, bool success, const string& output,
-                         int* start_time, int* end_time);
-  void BuildStarted();
-  void BuildFinished();
-
-  enum EdgeStatus {
-    kEdgeStarted,
-    kEdgeFinished,
-  };
-
-  /// Format the progress status string by replacing the placeholders.
-  /// See the user manual for more information about the available
-  /// placeholders.
-  /// @param progress_status_format The format of the progress status.
-  /// @param status The status of the edge.
-  string FormatProgressStatus(const char* progress_status_format,
-                              EdgeStatus status) const;
-
- private:
-  void PrintStatus(Edge* edge, EdgeStatus status);
-
-  const BuildConfig& config_;
-
-  /// Time the build started.
-  int64_t start_time_millis_;
-
-  int started_edges_, finished_edges_, total_edges_;
-
-  /// Map of running edge to time the edge started running.
-  typedef map<Edge*, int> RunningEdgeMap;
-  RunningEdgeMap running_edges_;
-
-  /// Prints progress output.
-  LinePrinter printer_;
-
-  /// The custom progress status format to use.
-  const char* progress_status_format_;
-
-  template<size_t S>
-  void SnprintfRate(double rate, char(&buf)[S], const char* format) const {
-    if (rate == -1)
-      snprintf(buf, S, "?");
-    else
-      snprintf(buf, S, format, rate);
-  }
-
-  struct RateInfo {
-    RateInfo() : rate_(-1) {}
-
-    void Restart() { stopwatch_.Restart(); }
-    double Elapsed() const { return stopwatch_.Elapsed(); }
-    double rate() { return rate_; }
-
-    void UpdateRate(int edges) {
-      if (edges && stopwatch_.Elapsed())
-        rate_ = edges / stopwatch_.Elapsed();
-    }
-
-  private:
-    double rate_;
-    Stopwatch stopwatch_;
-  };
-
-  struct SlidingRateInfo {
-    SlidingRateInfo(int n) : rate_(-1), N(n), last_update_(-1) {}
-
-    void Restart() { stopwatch_.Restart(); }
-    double rate() { return rate_; }
-
-    void UpdateRate(int update_hint) {
-      if (update_hint == last_update_)
-        return;
-      last_update_ = update_hint;
-
-      if (times_.size() == N)
-        times_.pop();
-      times_.push(stopwatch_.Elapsed());
-      if (times_.back() != times_.front())
-        rate_ = times_.size() / (times_.back() - times_.front());
-    }
-
-  private:
-    double rate_;
-    Stopwatch stopwatch_;
-    const size_t N;
-    queue<double> times_;
-    int last_update_;
-  };
-
-  mutable RateInfo overall_rate_;
-  mutable SlidingRateInfo current_rate_;
-};
-
 #endif  // NINJA_BUILD_H_
diff --git a/src/build_log.cc b/src/build_log.cc
index c4a08a0..0ebc45d 100644
--- a/src/build_log.cc
+++ b/src/build_log.cc
@@ -22,6 +22,7 @@
 
 #include "build_log.h"
 
+#include <assert.h>
 #include <errno.h>
 #include <stdlib.h>
 #include <string.h>
@@ -31,9 +32,13 @@
 #include <unistd.h>
 #endif
 
+#include <numeric>
+
 #include "build.h"
+#include "disk_interface.h"
 #include "graph.h"
 #include "metrics.h"
+#include "parallel_map.h"
 #include "util.h"
 #if defined(_MSC_VER) && (_MSC_VER < 1800)
 #define strtoll _strtoi64
@@ -49,9 +54,11 @@
 namespace {
 
 const char kFileSignature[] = "# ninja log v%d\n";
-const int kOldestSupportedVersion = 4;
+const int kOldestSupportedVersion = 5;
 const int kCurrentVersion = 5;
 
+const char kFieldSeparator = '\t';
+
 // 64bit MurmurHash2, by Austin Appleby
 #if defined(_MSC_VER)
 #define BIG_CONSTANT(x) (x)
@@ -105,13 +112,14 @@
 
 // static
 uint64_t BuildLog::LogEntry::HashCommand(StringPiece command) {
-  return MurmurHash64A(command.str_, command.len_);
+  METRIC_RECORD("hash command");
+  return MurmurHash64A(command.data(), command.size());
 }
 
-BuildLog::LogEntry::LogEntry(const string& output)
+BuildLog::LogEntry::LogEntry(const HashedStrView& output)
   : output(output) {}
 
-BuildLog::LogEntry::LogEntry(const string& output, uint64_t command_hash,
+BuildLog::LogEntry::LogEntry(const HashedStrView& output, uint64_t command_hash,
   int start_time, int end_time, TimeStamp restat_mtime)
   : output(output), command_hash(command_hash),
     start_time(start_time), end_time(end_time), mtime(restat_mtime)
@@ -157,13 +165,11 @@
                              TimeStamp mtime) {
   string command = edge->EvaluateCommand(true);
   uint64_t command_hash = LogEntry::HashCommand(command);
-  for (vector<Node*>::iterator out = edge->outputs_.begin();
-       out != edge->outputs_.end(); ++out) {
-    const string& path = (*out)->path();
-    Entries::iterator i = entries_.find(path);
+  for (Node* out : edge->outputs_) {
+    const HashedStr& path = out->path_hashed();
     LogEntry* log_entry;
-    if (i != entries_.end()) {
-      log_entry = i->second;
+    if (LogEntry** i = entries_.Lookup(path)) {
+      log_entry = *i;
     } else {
       log_entry = new LogEntry(path);
       entries_.insert(Entries::value_type(log_entry->output, log_entry));
@@ -190,164 +196,276 @@
   log_file_ = NULL;
 }
 
-struct LineReader {
-  explicit LineReader(FILE* file)
-    : file_(file), buf_end_(buf_), line_start_(buf_), line_end_(NULL) {
-      memset(buf_, 0, sizeof(buf_));
+/// Retrieve the next tab-delimited or LF-delimited piece in the input.
+///
+/// Specifically, the function searches for a separator, starting at the given
+/// input offset. If the function finds the separator, it removes everything up
+/// to and including the separator from |*input|, places it in |*out|, and
+/// returns true. Otherwise, it zeroes |*out| and returns false.
+static bool GetNextPiece(StringPiece* input, char sep, StringPiece* out,
+                         size_t start_offset=0) {
+  assert(input != out);
+  const char* data = input->data();
+  const char* split = nullptr;
+
+  // memchr(NULL, NULL, 0) has undefined behavior, so avoid calling memchr when
+  // input->data() is nullptr. If input->size() is non-zero, its data() will be
+  // non-null.
+  if (start_offset < input->size()) {
+    split = static_cast<const char*>(
+        memchr(data + start_offset, sep, input->size() - start_offset));
   }
 
-  // Reads a \n-terminated line from the file passed to the constructor.
-  // On return, *line_start points to the beginning of the next line, and
-  // *line_end points to the \n at the end of the line. If no newline is seen
-  // in a fixed buffer size, *line_end is set to NULL. Returns false on EOF.
-  bool ReadLine(char** line_start, char** line_end) {
-    if (line_start_ >= buf_end_ || !line_end_) {
-      // Buffer empty, refill.
-      size_t size_read = fread(buf_, 1, sizeof(buf_), file_);
-      if (!size_read)
-        return false;
-      line_start_ = buf_;
-      buf_end_ = buf_ + size_read;
-    } else {
-      // Advance to next line in buffer.
-      line_start_ = line_end_ + 1;
-    }
-
-    line_end_ = (char*)memchr(line_start_, '\n', buf_end_ - line_start_);
-    if (!line_end_) {
-      // No newline. Move rest of data to start of buffer, fill rest.
-      size_t already_consumed = line_start_ - buf_;
-      size_t size_rest = (buf_end_ - buf_) - already_consumed;
-      memmove(buf_, line_start_, size_rest);
-
-      size_t read = fread(buf_ + size_rest, 1, sizeof(buf_) - size_rest, file_);
-      buf_end_ = buf_ + size_rest + read;
-      line_start_ = buf_;
-      line_end_ = (char*)memchr(line_start_, '\n', buf_end_ - line_start_);
-    }
-
-    *line_start = line_start_;
-    *line_end = line_end_;
+  if (split != nullptr) {
+    size_t len = split + 1 - data;
+    *out = input->substr(0, len);
+    *input = input->substr(len);
     return true;
+  } else {
+    *out = {};
+    return false;
   }
+}
 
- private:
-  FILE* file_;
-  char buf_[256 << 10];
-  char* buf_end_;  // Points one past the last valid byte in |buf_|.
+struct BuildLogInput {
+  std::unique_ptr<LoadedFile> file;
+  int log_version = 0;
 
-  char* line_start_;
-  // Points at the next \n in buf_ after line_start, or NULL.
-  char* line_end_;
+  // Content excluding the file header.
+  StringPiece content;
 };
 
-bool BuildLog::Load(const string& path, string* err) {
-  METRIC_RECORD(".ninja_log load");
-  FILE* file = fopen(path.c_str(), "r");
-  if (!file) {
-    if (errno == ENOENT)
-      return true;
-    *err = strerror(errno);
+static bool OpenBuildLogForReading(const std::string& path,
+                                   BuildLogInput* log,
+                                   std::string* err) {
+  *log = {};
+
+  RealDiskInterface file_reader;
+  std::string load_err;
+  switch (file_reader.LoadFile(path, &log->file, &load_err)) {
+  case FileReader::Okay:
+    break;
+  case FileReader::NotFound:
+    return true;
+  default:
+    *err = load_err;
     return false;
   }
 
-  int log_version = 0;
-  int unique_entry_count = 0;
-  int total_entry_count = 0;
+  // We need a NUL terminator after the log file's content so that we can call
+  // atoi/atol/strtoull with a pointer to within the content.
+  log->content = log->file->content_with_nul();
+  log->content.remove_suffix(1);
 
-  LineReader reader(file);
-  char* line_start = 0;
-  char* line_end = 0;
-  while (reader.ReadLine(&line_start, &line_end)) {
-    if (!log_version) {
-      sscanf(line_start, kFileSignature, &log_version);
+  StringPiece header_line;
+  if (GetNextPiece(&log->content, '\n', &header_line)) {
+    // At least with glibc, sscanf will touch every byte of the string it scans,
+    // so make a copy of the first line for sscanf to use. (Maybe sscanf is
+    // calling strlen internally?)
+    sscanf((header_line.AsString()).c_str(), kFileSignature,
+           &log->log_version);
+  }
 
-      if (log_version < kOldestSupportedVersion) {
-        *err = ("build log version invalid, perhaps due to being too old; "
-                "starting over");
-        fclose(file);
-        unlink(path.c_str());
-        // Don't report this as a failure.  An empty build log will cause
-        // us to rebuild the outputs anyway.
-        return true;
+  if (log->log_version < kOldestSupportedVersion) {
+    *err = ("build log version invalid, perhaps due to being too old; "
+            "starting over");
+    log->content = {};
+    log->file.reset();
+    unlink(path.c_str());
+    // Don't report this as a failure.  An empty build log will cause
+    // us to rebuild the outputs anyway.
+  }
+
+  return true;
+}
+
+/// Split the build log's content (i.e. the lines excluding the header) into
+/// chunks. Each chunk is guaranteed to end with a newline character. Any output
+/// beyond the end of the last newline is quietly discarded.
+static std::vector<StringPiece> SplitBuildLog(StringPiece content) {
+  // The log file should end with an LF character, but if it doesn't, start by
+  // stripping off non-LF characters.
+  while (!content.empty() && content.back() != '\n') {
+    content.remove_suffix(1);
+  }
+
+  size_t ideal_chunk_count = GetOptimalThreadPoolJobCount();
+  size_t ideal_chunk_size = content.size() / ideal_chunk_count + 1;
+
+  std::vector<StringPiece> result;
+  StringPiece chunk;
+  while (GetNextPiece(&content, '\n', &chunk, ideal_chunk_size)) {
+    result.push_back(chunk);
+  }
+  if (!content.empty()) {
+    result.push_back(content);
+  }
+
+  return result;
+}
+
+/// Call the given function on each line in the given chunk. The chunk must end
+/// with an LF character. The LF characters are included in the string views
+/// passed to the callback.
+template <typename Func>
+static void VisitEachLineInChunk(StringPiece chunk, Func func) {
+  assert(!chunk.empty() && chunk.back() == '\n');
+  StringPiece line;
+  while (GetNextPiece(&chunk, '\n', &line)) {
+    func(line);
+  }
+}
+
+/// Count the number of LF newline characters in the string. The string is
+/// guaranteed to end with an LF.
+static size_t CountNewlinesInChunk(StringPiece chunk) {
+  size_t line_count = 0;
+  VisitEachLineInChunk(chunk, [&line_count](StringPiece line) {
+    ++line_count;
+  });
+  return line_count;
+}
+
+struct ParsedLine {
+  /// These fields are guaranteed to be followed by a whitespace character
+  /// (either a tab or an LF), but the whitespace terminator isn't explicitly
+  /// part of the string piece.
+  StringPiece start_time;
+  StringPiece end_time;
+  StringPiece mtime;
+  StringPiece path;
+  StringPiece command_hash;
+};
+
+/// Given a single line of the build log (including the terminator LF), split
+/// the line into its various tab-delimited fields.
+static inline bool SplitLine(StringPiece line, ParsedLine* out) {
+  assert(!line.empty() && line.back() == '\n');
+  line.remove_suffix(1);
+
+  auto get_next_field = [&line](StringPiece* out_piece) {
+    // Extract the next piece from the line. If we're successful, remove the
+    // field separator from the end of the piece.
+    if (!GetNextPiece(&line, kFieldSeparator, out_piece)) return false;
+    assert(!out_piece->empty() && out_piece->back() == kFieldSeparator);
+    out_piece->remove_suffix(1);
+    return true;
+  };
+
+  *out = {};
+  if (!get_next_field(&out->start_time)) return false;
+  if (!get_next_field(&out->end_time)) return false;
+  if (!get_next_field(&out->mtime)) return false;
+  if (!get_next_field(&out->path)) return false;
+  out->command_hash = line;
+
+  return true;
+}
+
+/// Given a single line of the build log (including the terminator LF), return
+/// just the path field.
+static StringPiece GetPathForLine(StringPiece line) {
+  ParsedLine parsed_line;
+  return SplitLine(line, &parsed_line) ? parsed_line.path : StringPiece();
+}
+
+bool BuildLog::Load(const string& path, string* err) {
+  METRIC_RECORD(".ninja_log load");
+  assert(entries_.empty());
+
+  BuildLogInput log;
+  if (!OpenBuildLogForReading(path, &log, err)) return false;
+  if (log.file.get() == nullptr) return true;
+
+  std::vector<StringPiece> chunks = SplitBuildLog(log.content);
+  std::unique_ptr<ThreadPool> thread_pool = CreateThreadPool();
+
+  // The concurrent hashmap doesn't automatically resize when entries are added
+  // to it, so we need to reserve space in advance. The number of newlines
+  // should exceed the number of distinct paths in the build log by a small
+  // factor.
+  size_t line_count = 0;
+  for (size_t count :
+      ParallelMap(thread_pool.get(), chunks, CountNewlinesInChunk)) {
+    line_count += count;
+  }
+  entries_.reserve(line_count);
+
+  // Construct an initial table of path -> LogEntry. Each LogEntry is
+  // initialized with the newest record for a particular path. Build a list of
+  // each chunk's new log entries.
+  std::vector<std::vector<LogEntry*>> chunk_new_entries =
+      ParallelMap(thread_pool.get(), chunks, [this, &log](StringPiece chunk) {
+    std::vector<LogEntry*> chunk_entries_list;
+    VisitEachLineInChunk(chunk, [&](StringPiece line) {
+      HashedStrView path = GetPathForLine(line);
+      if (path.empty()) return;
+      LogEntry* entry = nullptr;
+      if (LogEntry** entry_it = entries_.Lookup(path)) {
+        entry = *entry_it;
+      } else {
+        std::unique_ptr<LogEntry> new_entry(new LogEntry(path));
+        new_entry->newest_parsed_line = line.data() - log.content.data();
+        if (entries_.insert({ new_entry->output, new_entry.get() }).second) {
+          chunk_entries_list.push_back(new_entry.release());
+          return;
+        } else {
+          // Another thread beat us to it. Update the existing entry instead.
+          LogEntry** entry_it = entries_.Lookup(path);
+          assert(entry_it != nullptr);
+          entry = *entry_it;
+        }
       }
-    }
+      AtomicUpdateMaximum<size_t>(&entry->newest_parsed_line,
+                                  line.data() - log.content.data());
+    });
+    return chunk_entries_list;
+  });
 
-    // If no newline was found in this chunk, read the next.
-    if (!line_end)
-      continue;
-
-    const char kFieldSeparator = '\t';
-
-    char* start = line_start;
-    char* end = (char*)memchr(start, kFieldSeparator, line_end - start);
-    if (!end)
-      continue;
-    *end = 0;
-
-    int start_time = 0, end_time = 0;
-    TimeStamp restat_mtime = 0;
-
-    start_time = atoi(start);
-    start = end + 1;
-
-    end = (char*)memchr(start, kFieldSeparator, line_end - start);
-    if (!end)
-      continue;
-    *end = 0;
-    end_time = atoi(start);
-    start = end + 1;
-
-    end = (char*)memchr(start, kFieldSeparator, line_end - start);
-    if (!end)
-      continue;
-    *end = 0;
-    restat_mtime = strtoll(start, NULL, 10);
-    start = end + 1;
-
-    end = (char*)memchr(start, kFieldSeparator, line_end - start);
-    if (!end)
-      continue;
-    string output = string(start, end - start);
-
-    start = end + 1;
-    end = line_end;
-
-    LogEntry* entry;
-    Entries::iterator i = entries_.find(output);
-    if (i != entries_.end()) {
-      entry = i->second;
-    } else {
-      entry = new LogEntry(output);
-      entries_.insert(Entries::value_type(entry->output, entry));
-      ++unique_entry_count;
-    }
-    ++total_entry_count;
-
-    entry->start_time = start_time;
-    entry->end_time = end_time;
-    entry->mtime = restat_mtime;
-    if (log_version >= 5) {
-      char c = *end; *end = '\0';
-      entry->command_hash = (uint64_t)strtoull(start, NULL, 16);
-      *end = c;
-    } else {
-      entry->command_hash = LogEntry::HashCommand(StringPiece(start,
-                                                              end - start));
-    }
+  // Collect all the log entries into a flat vector so we can distribute the
+  // final parsing work. This list has a non-deterministic order when a node
+  // appears in multiple chunks.
+  std::vector<LogEntry*> entries_vec;
+  entries_vec.reserve(entries_.size());
+  for (auto& chunk : chunk_new_entries) {
+    std::move(chunk.begin(), chunk.end(), std::back_inserter(entries_vec));
   }
-  fclose(file);
 
-  if (!line_start) {
-    return true; // file was empty
-  }
+  // Finish parsing the log entries.
+  ParallelMap(thread_pool.get(), entries_vec, [&log](LogEntry* entry) {
+    // Locate the end of the line. The end of the line wasn't stored in the log
+    // entry because that would complicate the atomic line location updating.
+    assert(entry->newest_parsed_line < log.content.size());
+    StringPiece line_to_end = log.content.substr(entry->newest_parsed_line);
+    StringPiece line;
+    if (!GetNextPiece(&line_to_end, '\n', &line)) {
+      assert(false && "build log changed during parsing");
+      abort();
+    }
+
+    ParsedLine parsed_line {};
+    if (!SplitLine(line, &parsed_line)) {
+      assert(false && "build log changed during parsing");
+      abort();
+    }
+
+    // Initialize the entry object.
+    entry->start_time = atoi(parsed_line.start_time.data());
+    entry->end_time = atoi(parsed_line.end_time.data());
+    entry->mtime = strtoll(parsed_line.mtime.data(), nullptr, 10);
+    entry->command_hash = static_cast<uint64_t>(
+        strtoull(parsed_line.command_hash.data(), nullptr, 16));
+  });
+
+  int total_entry_count = line_count;
+  int unique_entry_count = entries_vec.size();
 
   // Decide whether it's time to rebuild the log:
   // - if we're upgrading versions
   // - if it's getting large
-  int kMinCompactionEntryCount = 100;
-  int kCompactionRatio = 3;
-  if (log_version < kCurrentVersion) {
+  const int kMinCompactionEntryCount = 100;
+  const int kCompactionRatio = 3;
+  if (log.log_version < kCurrentVersion) {
     needs_recompaction_ = true;
   } else if (total_entry_count > kMinCompactionEntryCount &&
              total_entry_count > unique_entry_count * kCompactionRatio) {
@@ -357,11 +475,10 @@
   return true;
 }
 
-BuildLog::LogEntry* BuildLog::LookupByOutput(const string& path) {
-  Entries::iterator i = entries_.find(path);
-  if (i != entries_.end())
-    return i->second;
-  return NULL;
+BuildLog::LogEntry* BuildLog::LookupByOutput(const HashedStrView& path) {
+  if (LogEntry** i = entries_.Lookup(path))
+    return *i;
+  return nullptr;
 }
 
 bool BuildLog::WriteEntry(FILE* f, const LogEntry& entry) {
@@ -388,22 +505,23 @@
     return false;
   }
 
-  vector<StringPiece> dead_outputs;
-  for (Entries::iterator i = entries_.begin(); i != entries_.end(); ++i) {
-    if (user.IsPathDead(i->first)) {
-      dead_outputs.push_back(i->first);
-      continue;
-    }
+  Entries new_entries;
+  new_entries.reserve(std::max(entries_.size(), entries_.bucket_count()));
 
-    if (!WriteEntry(f, *i->second)) {
+  for (const std::pair<HashedStrView, LogEntry*>& pair : entries_) {
+    if (user.IsPathDead(pair.first.str_view()))
+      continue;
+
+    new_entries.insert(pair);
+
+    if (!WriteEntry(f, *pair.second)) {
       *err = strerror(errno);
       fclose(f);
       return false;
     }
   }
 
-  for (size_t i = 0; i < dead_outputs.size(); ++i)
-    entries_.erase(dead_outputs[i]);
+  entries_.swap(new_entries);
 
   fclose(f);
   if (unlink(path.c_str()) < 0) {
diff --git a/src/build_log.h b/src/build_log.h
index 5268fab..79bbd74 100644
--- a/src/build_log.h
+++ b/src/build_log.h
@@ -19,7 +19,8 @@
 #include <stdio.h>
 using namespace std;
 
-#include "hash_map.h"
+#include "hashed_str_view.h"
+#include "concurrent_hash_map.h"
 #include "timestamp.h"
 #include "util.h"  // uint64_t
 
@@ -52,12 +53,15 @@
   bool Load(const string& path, string* err);
 
   struct LogEntry {
-    string output;
+    HashedStr output;
     uint64_t command_hash;
     int start_time;
     int end_time;
     TimeStamp mtime;
 
+    // Used during build log parsing.
+    std::atomic<size_t> newest_parsed_line;
+
     static uint64_t HashCommand(StringPiece command);
 
     // Used by tests.
@@ -67,13 +71,13 @@
           mtime == o.mtime;
     }
 
-    explicit LogEntry(const string& output);
-    LogEntry(const string& output, uint64_t command_hash,
+    explicit LogEntry(const HashedStrView& output);
+    LogEntry(const HashedStrView& output, uint64_t command_hash,
              int start_time, int end_time, TimeStamp restat_mtime);
   };
 
   /// Lookup a previously-run command by its output path.
-  LogEntry* LookupByOutput(const string& path);
+  LogEntry* LookupByOutput(const HashedStrView& path);
 
   /// Serialize an entry into a log file.
   bool WriteEntry(FILE* f, const LogEntry& entry);
@@ -81,7 +85,10 @@
   /// Rewrite the known log entries, throwing away old data.
   bool Recompact(const string& path, const BuildLogUser& user, string* err);
 
-  typedef ExternalStringHashMap<LogEntry*>::Type Entries;
+  /// The HashedStrView refers to memory in the LogEntry itself. This map uses
+  /// a value of LogEntry* rather than LogEntry, because moving a LogEntry would
+  /// invalidate the HashedStrView.
+  typedef ConcurrentHashMap<HashedStrView, LogEntry*> Entries;
   const Entries& entries() const { return entries_; }
 
  private:
diff --git a/src/build_log_test.cc b/src/build_log_test.cc
index ad30380..f5e8c56 100644
--- a/src/build_log_test.cc
+++ b/src/build_log_test.cc
@@ -66,7 +66,7 @@
   ASSERT_TRUE(e2);
   ASSERT_TRUE(*e1 == *e2);
   ASSERT_EQ(15, e1->start_time);
-  ASSERT_EQ("out", e1->output);
+  ASSERT_EQ("out", e1->output.str());
 }
 
 TEST_F(BuildLogTest, FirstWriteAddsSignature) {
@@ -101,9 +101,9 @@
 
 TEST_F(BuildLogTest, DoubleEntry) {
   FILE* f = fopen(kTestFilename, "wb");
-  fprintf(f, "# ninja log v4\n");
-  fprintf(f, "0\t1\t2\tout\tcommand abc\n");
-  fprintf(f, "3\t4\t5\tout\tcommand def\n");
+  fprintf(f, "# ninja log v5\n");
+  fprintf(f, "0\t1\t2\tout\t67ab\n");
+  fprintf(f, "3\t4\t5\tout\t89cd\n");
   fclose(f);
 
   string err;
@@ -113,7 +113,7 @@
 
   BuildLog::LogEntry* e = log.LookupByOutput("out");
   ASSERT_TRUE(e);
-  ASSERT_NO_FATAL_FAILURE(AssertHash("command def", e->command_hash));
+  ASSERT_EQ(0x89cd, e->command_hash);
 }
 
 TEST_F(BuildLogTest, Truncate) {
@@ -166,34 +166,19 @@
   ASSERT_NE(err.find("version"), string::npos);
 }
 
-TEST_F(BuildLogTest, SpacesInOutputV4) {
-  FILE* f = fopen(kTestFilename, "wb");
-  fprintf(f, "# ninja log v4\n");
-  fprintf(f, "123\t456\t456\tout with space\tcommand\n");
-  fclose(f);
-
-  string err;
-  BuildLog log;
-  EXPECT_TRUE(log.Load(kTestFilename, &err));
-  ASSERT_EQ("", err);
-
-  BuildLog::LogEntry* e = log.LookupByOutput("out with space");
-  ASSERT_TRUE(e);
-  ASSERT_EQ(123, e->start_time);
-  ASSERT_EQ(456, e->end_time);
-  ASSERT_EQ(456, e->mtime);
-  ASSERT_NO_FATAL_FAILURE(AssertHash("command", e->command_hash));
-}
-
 TEST_F(BuildLogTest, DuplicateVersionHeader) {
   // Old versions of ninja accidentally wrote multiple version headers to the
   // build log on Windows. This shouldn't crash, and the second version header
   // should be ignored.
+  //
+  // This test initially tested a v4 log, but support for v4 was removed. It now
+  // tests a v5 log, but that doesn't mean that the original duplicate-header
+  // bug ever affected v5 logs.
   FILE* f = fopen(kTestFilename, "wb");
-  fprintf(f, "# ninja log v4\n");
-  fprintf(f, "123\t456\t456\tout\tcommand\n");
-  fprintf(f, "# ninja log v4\n");
-  fprintf(f, "456\t789\t789\tout2\tcommand2\n");
+  fprintf(f, "# ninja log v5\n");
+  fprintf(f, "123\t456\t456\tout\t12ab\n");
+  fprintf(f, "# ninja log v5\n");
+  fprintf(f, "456\t789\t789\tout2\t34cd\n");
   fclose(f);
 
   string err;
@@ -206,26 +191,27 @@
   ASSERT_EQ(123, e->start_time);
   ASSERT_EQ(456, e->end_time);
   ASSERT_EQ(456, e->mtime);
-  ASSERT_NO_FATAL_FAILURE(AssertHash("command", e->command_hash));
+  ASSERT_EQ(0x12ab, e->command_hash);
 
   e = log.LookupByOutput("out2");
   ASSERT_TRUE(e);
   ASSERT_EQ(456, e->start_time);
   ASSERT_EQ(789, e->end_time);
   ASSERT_EQ(789, e->mtime);
-  ASSERT_NO_FATAL_FAILURE(AssertHash("command2", e->command_hash));
+  ASSERT_EQ(0x34cd, e->command_hash);
 }
 
 TEST_F(BuildLogTest, VeryLongInputLine) {
-  // Ninja's build log buffer is currently 256kB. Lines longer than that are
-  // silently ignored, but don't affect parsing of other lines.
+  // The parallelized build log parser accepts paths of arbitrary length.
+  std::string very_long_path;
+  for (size_t i = 0; i < (512 << 10) / strlen("/more_path"); ++i)
+    very_long_path += "/more_path";
+
   FILE* f = fopen(kTestFilename, "wb");
-  fprintf(f, "# ninja log v4\n");
-  fprintf(f, "123\t456\t456\tout\tcommand start");
-  for (size_t i = 0; i < (512 << 10) / strlen(" more_command"); ++i)
-    fputs(" more_command", f);
+  fprintf(f, "# ninja log v5\n");
+  fprintf(f, "123\t456\t456\t%s\t1234abcd\n", very_long_path.c_str());
   fprintf(f, "\n");
-  fprintf(f, "456\t789\t789\tout2\tcommand2\n");
+  fprintf(f, "456\t789\t789\tout2\t2345bcde\n");
   fclose(f);
 
   string err;
@@ -233,15 +219,19 @@
   EXPECT_TRUE(log.Load(kTestFilename, &err));
   ASSERT_EQ("", err);
 
-  BuildLog::LogEntry* e = log.LookupByOutput("out");
-  ASSERT_EQ(NULL, e);
+  BuildLog::LogEntry* e = log.LookupByOutput(very_long_path.c_str());
+  ASSERT_TRUE(e);
+  ASSERT_EQ(123, e->start_time);
+  ASSERT_EQ(456, e->end_time);
+  ASSERT_EQ(456, e->mtime);
+  ASSERT_EQ(0x1234abcd, e->command_hash);
 
   e = log.LookupByOutput("out2");
   ASSERT_TRUE(e);
   ASSERT_EQ(456, e->start_time);
   ASSERT_EQ(789, e->end_time);
   ASSERT_EQ(789, e->mtime);
-  ASSERT_NO_FATAL_FAILURE(AssertHash("command2", e->command_hash));
+  ASSERT_EQ(0x2345bcde, e->command_hash);
 }
 
 TEST_F(BuildLogTest, MultiTargetEdge) {
@@ -256,8 +246,8 @@
   ASSERT_TRUE(e1);
   BuildLog::LogEntry* e2 = log.LookupByOutput("out.d");
   ASSERT_TRUE(e2);
-  ASSERT_EQ("out", e1->output);
-  ASSERT_EQ("out.d", e2->output);
+  ASSERT_EQ("out", e1->output.str());
+  ASSERT_EQ("out.d", e2->output.str());
   ASSERT_EQ(21, e1->start_time);
   ASSERT_EQ(21, e2->start_time);
   ASSERT_EQ(22, e2->end_time);
@@ -297,11 +287,11 @@
 
   // "out2" is dead, it should've been removed.
   BuildLog log3;
-  EXPECT_TRUE(log2.Load(kTestFilename, &err));
+  EXPECT_TRUE(log3.Load(kTestFilename, &err));
   ASSERT_EQ("", err);
-  ASSERT_EQ(1u, log2.entries().size());
-  ASSERT_TRUE(log2.LookupByOutput("out"));
-  ASSERT_FALSE(log2.LookupByOutput("out2"));
+  ASSERT_EQ(1u, log3.entries().size());
+  ASSERT_TRUE(log3.LookupByOutput("out"));
+  ASSERT_FALSE(log3.LookupByOutput("out2"));
 }
 
 }  // anonymous namespace
diff --git a/src/build_test.cc b/src/build_test.cc
index 46ab33e..f7c8550 100644
--- a/src/build_test.cc
+++ b/src/build_test.cc
@@ -19,6 +19,7 @@
 #include "build_log.h"
 #include "deps_log.h"
 #include "graph.h"
+#include "status.h"
 #include "test.h"
 
 /// Fixture for tests involving Plan.
@@ -455,10 +456,55 @@
   VirtualFileSystem* fs_;
 };
 
+/// Fake implementation of Status, useful for tests.
+struct FakeStatus : public Status {
+  explicit FakeStatus() : last_output_(), warnings_() {}
+
+  virtual void PlanHasTotalEdges(int total) {}
+  virtual void BuildEdgeStarted(Edge* edge, int64_t start_time_millis) {}
+  virtual void BuildStarted() {}
+  virtual void BuildFinished() {}
+
+  virtual void Debug(const char* msg, ...) {}
+  virtual void Info(const char* msg, ...) {}
+  virtual void Error(const char* msg, ...) {}
+
+  virtual void BuildEdgeFinished(Edge* edge, int64_t end_time_millis,
+                                 const CommandRunner::Result* result) {
+    last_output_ = result->output;
+  }
+
+  virtual void Warning(const char* msg, ...) {
+    va_list ap, ap2;
+    va_start(ap, msg);
+    va_copy(ap2, ap);
+
+    int len = vsnprintf(NULL, 0, msg, ap2);
+    va_end(ap2);
+    if (len < 0) {
+      warnings_.push_back("vsnprintf failed");
+      return;
+    }
+
+    string buf;
+    buf.resize(len + 1);
+
+    len = vsnprintf(&buf[0], len + 1, msg, ap);
+    buf.resize(len);
+
+    warnings_.push_back(buf);
+
+    va_end(ap);
+  }
+
+  string last_output_;
+  vector<string> warnings_;
+};
+
 struct BuildTest : public StateTestWithBuiltinRules, public BuildLogUser {
   BuildTest() : config_(MakeConfig()), command_runner_(&fs_),
-                builder_(&state_, config_, NULL, NULL, &fs_),
-                status_(config_) {
+                builder_(&state_, config_, NULL, NULL, &fs_, &status_, 0),
+                status_() {
   }
 
   virtual void SetUp() {
@@ -501,7 +547,7 @@
   VirtualFileSystem fs_;
   Builder builder_;
 
-  BuildStatus status_;
+  FakeStatus status_;
 };
 
 void BuildTest::RebuildTarget(const string& target, const char* manifest,
@@ -525,12 +571,12 @@
   DepsLog deps_log, *pdeps_log = NULL;
   if (deps_path) {
     ASSERT_TRUE(deps_log.Load(deps_path, pstate, &err));
-    ASSERT_TRUE(deps_log.OpenForWrite(deps_path, &err));
+    ASSERT_TRUE(deps_log.OpenForWrite(deps_path, fs_, &err));
     ASSERT_EQ("", err);
     pdeps_log = &deps_log;
   }
 
-  Builder builder(pstate, config_, pbuild_log, pdeps_log, &fs_);
+  Builder builder(pstate, config_, pbuild_log, pdeps_log, &fs_, &status_, 0);
   EXPECT_TRUE(builder.AddTarget(target, &err));
 
   command_runner_.commands_ran_.clear();
@@ -564,7 +610,9 @@
   } else if (edge->rule().name() == "true" ||
              edge->rule().name() == "fail" ||
              edge->rule().name() == "interrupt" ||
-             edge->rule().name() == "console") {
+             edge->rule().name() == "console" ||
+             edge->rule().name() == "mkdir" ||
+             edge->rule().name() == "phony_out") {
     // Don't do anything.
   } else {
     printf("unknown command\n");
@@ -597,6 +645,18 @@
     return true;
   }
 
+  if (edge->rule().name() == "mkdir") {
+    result->status = ExitSuccess;
+    for (vector<Node*>::iterator out = edge->outputs_.begin();
+         out != edge->outputs_.end(); ++out) {
+      if (!fs_->MakeDir((*out)->path())) {
+        result->status = ExitFailure;
+      }
+    }
+    last_command_ = NULL;
+    return true;
+  }
+
   if (edge->rule().name() == "fail" ||
       (edge->rule().name() == "touch-fail-tick2" && fs_->now_ == 2))
     result->status = ExitFailure;
@@ -855,6 +915,23 @@
   ASSERT_EQ("cc foo.c", edge->EvaluateCommand());
 }
 
+TEST_F(BuildTest, DepFileOKWithPhonyOutputs) {
+  string err;
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule cc\n  command = cc $in\n  depfile = $out.d\n"
+"build foo.o: cc foo.c\n"));
+
+  config_.uses_phony_outputs = true;
+
+  fs_.Create("foo.c", "");
+  GetNode("bar.h")->MarkDirty();  // Mark bar.h as missing.
+  fs_.Create("foo.o.d", "foo.o: blah.h bar.h\n");
+  EXPECT_TRUE(builder_.AddTarget("foo.o", &err));
+  ASSERT_EQ("", err);
+
+  ASSERT_TRUE(GetNode("bar.h")->in_edge()->phony_from_depfile_);
+}
+
 TEST_F(BuildTest, DepFileParseError) {
   string err;
   ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
@@ -875,7 +952,7 @@
 "build b: touch || c\n"
 "build a: touch | b || c\n"));
 
-  vector<Edge*> c_out = GetNode("c")->out_edges();
+  vector<Edge*> c_out = GetNode("c")->GetOutEdges();
   ASSERT_EQ(2u, c_out.size());
   EXPECT_EQ("b", c_out[0]->outputs_[0]->path());
   EXPECT_EQ("a", c_out[1]->outputs_[0]->path());
@@ -1324,8 +1401,8 @@
   ASSERT_EQ("", err);
   EXPECT_TRUE(builder_.Build(&err));
   ASSERT_EQ("", err);
-  EXPECT_EQ("[3/3]", builder_.status_->FormatProgressStatus("[%s/%t]",
-      BuildStatus::kEdgeStarted));
+  EXPECT_EQ(3u, command_runner_.commands_ran_.size());
+  EXPECT_EQ(3u, builder_.plan_.command_edge_count());
   command_runner_.commands_ran_.clear();
   state_.Reset();
 
@@ -1763,20 +1840,6 @@
   ASSERT_EQ(1u, command_runner_.commands_ran_.size());
 }
 
-TEST_F(BuildTest, StatusFormatElapsed) {
-  status_.BuildStarted();
-  // Before any task is done, the elapsed time must be zero.
-  EXPECT_EQ("[%/e0.000]",
-            status_.FormatProgressStatus("[%%/e%e]",
-                BuildStatus::kEdgeStarted));
-}
-
-TEST_F(BuildTest, StatusFormatReplacePlaceholder) {
-  EXPECT_EQ("[%/s0/t0/r0/u0/f0]",
-            status_.FormatProgressStatus("[%%/s%s/t%t/r%r/u%u/f%f]",
-                BuildStatus::kEdgeStarted));
-}
-
 TEST_F(BuildTest, FailedDepsParse) {
   ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
 "build bad_deps.o: cat in1\n"
@@ -1833,10 +1896,10 @@
 
     // Run the build once, everything should be ok.
     DepsLog deps_log;
-    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err));
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
     ASSERT_EQ("", err);
 
-    Builder builder(&state, config_, NULL, &deps_log, &fs_);
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
     builder.command_runner_.reset(&command_runner_);
     EXPECT_TRUE(builder.AddTarget("out", &err));
     ASSERT_EQ("", err);
@@ -1864,9 +1927,9 @@
     // Run the build again.
     DepsLog deps_log;
     ASSERT_TRUE(deps_log.Load("ninja_deps", &state, &err));
-    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err));
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
 
-    Builder builder(&state, config_, NULL, &deps_log, &fs_);
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
     builder.command_runner_.reset(&command_runner_);
     command_runner_.commands_ran_.clear();
     EXPECT_TRUE(builder.AddTarget("out", &err));
@@ -1904,10 +1967,10 @@
 
     // Run the build once, everything should be ok.
     DepsLog deps_log;
-    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err));
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
     ASSERT_EQ("", err);
 
-    Builder builder(&state, config_, NULL, &deps_log, &fs_);
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
     builder.command_runner_.reset(&command_runner_);
     EXPECT_TRUE(builder.AddTarget("out", &err));
     ASSERT_EQ("", err);
@@ -1934,9 +1997,9 @@
 
     DepsLog deps_log;
     ASSERT_TRUE(deps_log.Load("ninja_deps", &state, &err));
-    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err));
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
 
-    Builder builder(&state, config_, NULL, &deps_log, &fs_);
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
     builder.command_runner_.reset(&command_runner_);
     command_runner_.commands_ran_.clear();
     EXPECT_TRUE(builder.AddTarget("out", &err));
@@ -1972,7 +2035,7 @@
 
   // The deps log is NULL in dry runs.
   config_.dry_run = true;
-  Builder builder(&state, config_, NULL, NULL, &fs_);
+  Builder builder(&state, config_, NULL, NULL, &fs_, &status_, 0);
   builder.command_runner_.reset(&command_runner_);
   command_runner_.commands_ran_.clear();
 
@@ -2027,10 +2090,10 @@
 
     // Run the build once, everything should be ok.
     DepsLog deps_log;
-    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err));
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
     ASSERT_EQ("", err);
 
-    Builder builder(&state, config_, NULL, &deps_log, &fs_);
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
     builder.command_runner_.reset(&command_runner_);
     EXPECT_TRUE(builder.AddTarget("out", &err));
     ASSERT_EQ("", err);
@@ -2054,9 +2117,9 @@
     // Run the build again.
     DepsLog deps_log;
     ASSERT_TRUE(deps_log.Load("ninja_deps", &state, &err));
-    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err));
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
 
-    Builder builder(&state, config_, NULL, &deps_log, &fs_);
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
     builder.command_runner_.reset(&command_runner_);
     command_runner_.commands_ran_.clear();
     EXPECT_TRUE(builder.AddTarget("out", &err));
@@ -2086,10 +2149,10 @@
 
     // Run the build once, everything should be ok.
     DepsLog deps_log;
-    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err));
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
     ASSERT_EQ("", err);
 
-    Builder builder(&state, config_, NULL, &deps_log, &fs_);
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
     builder.command_runner_.reset(&command_runner_);
     EXPECT_TRUE(builder.AddTarget("fo o.o", &err));
     ASSERT_EQ("", err);
@@ -2107,10 +2170,10 @@
 
     DepsLog deps_log;
     ASSERT_TRUE(deps_log.Load("ninja_deps", &state, &err));
-    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err));
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
     ASSERT_EQ("", err);
 
-    Builder builder(&state, config_, NULL, &deps_log, &fs_);
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
     builder.command_runner_.reset(&command_runner_);
 
     Edge* edge = state.edges_.back();
@@ -2148,10 +2211,10 @@
 
     // Run the build once, everything should be ok.
     DepsLog deps_log;
-    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err));
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
     ASSERT_EQ("", err);
 
-    Builder builder(&state, config_, NULL, &deps_log, &fs_);
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
     builder.command_runner_.reset(&command_runner_);
     EXPECT_TRUE(builder.AddTarget("a/b/c/d/e/fo o.o", &err));
     ASSERT_EQ("", err);
@@ -2171,10 +2234,10 @@
 
     DepsLog deps_log;
     ASSERT_TRUE(deps_log.Load("ninja_deps", &state, &err));
-    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", &err));
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
     ASSERT_EQ("", err);
 
-    Builder builder(&state, config_, NULL, &deps_log, &fs_);
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
     builder.command_runner_.reset(&command_runner_);
 
     Edge* edge = state.edges_.back();
@@ -2308,3 +2371,518 @@
   EXPECT_EQ("", err);
   ASSERT_EQ(1u, command_runner_.commands_ran_.size());
 }
+
+TEST_F(BuildTest, OutputDirectoryIgnored) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule mkdir\n"
+"  command = mkdir $out\n"
+"build outdir: mkdir\n"));
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("outdir", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ("", status_.last_output_);
+}
+
+TEST_F(BuildTest, OutputDirectoryWarning) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule mkdir\n"
+"  command = mkdir $out\n"
+"build outdir: mkdir\n"));
+
+  config_.uses_phony_outputs = true;
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("outdir", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ("ninja: outputs should be files, not directories: outdir", status_.last_output_);
+}
+
+TEST_F(BuildTest, OutputDirectoryError) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule mkdir\n"
+"  command = mkdir $out\n"
+"build outdir: mkdir\n"));
+
+  config_.uses_phony_outputs = true;
+  config_.output_directory_should_err = true;
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("outdir", &err));
+  EXPECT_EQ("", err);
+  EXPECT_FALSE(builder_.Build(&err));
+  EXPECT_EQ("subcommand failed", err);
+
+  EXPECT_EQ("ninja: outputs should be files, not directories: outdir", status_.last_output_);
+}
+
+TEST_F(BuildTest, OutputFileMissingIgnore) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule true\n  command = true\n"
+"build outfile: true\n"));
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("outfile", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ("", status_.last_output_);
+}
+
+TEST_F(BuildTest, OutputFileMissingWarning) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule true\n  command = true\n"
+"build outfile: true\n"));
+
+  config_.uses_phony_outputs = true;
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("outfile", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ("ninja: output file missing after successful execution: outfile", status_.last_output_);
+}
+
+TEST_F(BuildTest, OutputFileMissingError) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule true\n  command = true\n"
+"build outfile: true\n"));
+
+  config_.uses_phony_outputs = true;
+  config_.missing_output_file_should_err = true;
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("outfile", &err));
+  EXPECT_EQ("", err);
+  EXPECT_FALSE(builder_.Build(&err));
+  EXPECT_EQ("subcommand failed", err);
+
+  EXPECT_EQ("ninja: output file missing after successful execution: outfile", status_.last_output_);
+}
+
+TEST_F(BuildTest, OutputFileNotNeeded) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+"  command = echo ${out}\n"
+"  phony_output = true\n"
+"build outphony: phony_out\n"));
+
+  config_.uses_phony_outputs = true;
+  config_.missing_output_file_should_err = true;
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("outphony", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+}
+
+TEST_F(BuildWithLogTest, OldOutputFileIgnored) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule true\n  command = true\n"
+"build out: true in\n"));
+
+  fs_.Create("in", "");
+  fs_.Tick();
+  fs_.Create("out", "");
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  fs_.Tick();
+  fs_.Create("in", "");
+
+  state_.Reset();
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ("", status_.last_output_);
+}
+
+TEST_F(BuildWithLogTest, OldOutputFileWarning) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule true\n  command = true\n"
+"build out: true in\n"));
+
+  config_.uses_phony_outputs = true;
+
+  fs_.Create("in", "");
+  fs_.Tick();
+  fs_.Create("out", "");
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  fs_.Tick();
+  fs_.Create("in", "");
+
+  command_runner_.commands_ran_.clear();
+  state_.Reset();
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+  EXPECT_EQ("ninja: Missing `restat`? An output file is older than the most recent input:\n output: out\n  input: in", status_.last_output_);
+}
+
+TEST_F(BuildWithLogTest, OldOutputFileError) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule true\n  command = true\n"
+"build out: true in\n"));
+
+  config_.uses_phony_outputs = true;
+  config_.old_output_should_err = true;
+
+  fs_.Create("in", "");
+  fs_.Tick();
+  fs_.Create("out", "");
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  fs_.Tick();
+  fs_.Create("in", "");
+
+  command_runner_.commands_ran_.clear();
+  state_.Reset();
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  EXPECT_EQ("", err);
+  EXPECT_FALSE(builder_.Build(&err));
+  EXPECT_EQ("subcommand failed", err);
+
+  EXPECT_EQ("ninja: Missing `restat`? An output file is older than the most recent input:\n output: out\n  input: in", status_.last_output_);
+}
+
+TEST_F(BuildWithLogTest, OutputFileUpdated) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule touch\n  command = touch ${out}\n"
+"build out: touch in\n"));
+
+  config_.uses_phony_outputs = true;
+  config_.old_output_should_err = true;
+
+  fs_.Create("in", "");
+  fs_.Tick();
+  fs_.Create("out", "");
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  fs_.Tick();
+  fs_.Create("in", "");
+
+  command_runner_.commands_ran_.clear();
+  state_.Reset();
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ("", status_.last_output_);
+}
+
+TEST_F(BuildWithDepsLogTest, MissingDepfileWarning) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build out1: cat in1\n"
+"  deps = gcc\n"
+"  depfile = in1.d\n"));
+
+  string err;
+  DepsLog deps_log;
+  ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
+  ASSERT_EQ("", err);
+
+  Builder builder(&state_, config_, NULL, &deps_log, &fs_, &status_, 0);
+  builder.command_runner_.reset(&command_runner_);
+
+  EXPECT_TRUE(builder.AddTarget("out1", &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(builder.Build(&err));
+
+  ASSERT_EQ(1u, status_.warnings_.size());
+  ASSERT_EQ("depfile is missing (in1.d for out1)", status_.warnings_[0]);
+
+  builder.command_runner_.release();
+}
+
+TEST_F(BuildWithDepsLogTest, MissingDepfileError) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build out1: cat in1\n"
+"  deps = gcc\n"
+"  depfile = in1.d\n"));
+
+  config_.missing_depfile_should_err = true;
+
+  string err;
+  DepsLog deps_log;
+  ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
+  ASSERT_EQ("", err);
+
+  Builder builder(&state_, config_, NULL, &deps_log, &fs_, &status_, 0);
+  builder.command_runner_.reset(&command_runner_);
+
+  EXPECT_TRUE(builder.AddTarget("out1", &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_FALSE(builder.Build(&err));
+  EXPECT_EQ("subcommand failed", err);
+
+  EXPECT_EQ("depfile is missing", status_.last_output_);
+
+  builder.command_runner_.release();
+}
+
+TEST_F(BuildTest, PreRemoveOutputs) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule touch\n  command = touch ${out}\n"
+"build out: touch in\n"
+"build out2: touch out\n"));
+
+  config_.uses_phony_outputs = true;
+  config_.pre_remove_output_files = true;
+
+  fs_.Create("out", "");
+  fs_.Tick();
+  fs_.Create("in", "");
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("out2", &err));
+  EXPECT_EQ("", err);
+
+  fs_.files_created_.clear();
+
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ(2u, command_runner_.commands_ran_.size());
+  EXPECT_EQ(2u, fs_.files_created_.size());
+  EXPECT_EQ(1u, fs_.files_removed_.size());
+}
+
+TEST_F(BuildTest, PreRemoveOutputsWithPhonyOutputs) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+"  command = echo ${out}\n"
+"  phony_output = true\n"
+"build out: phony_out\n"));
+
+  config_.uses_phony_outputs = true;
+  config_.pre_remove_output_files = true;
+
+  fs_.Create("out", "");
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  EXPECT_EQ("", err);
+
+  fs_.files_created_.clear();
+
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ(1u, command_runner_.commands_ran_.size());
+  EXPECT_EQ(0u, fs_.files_created_.size());
+  EXPECT_EQ(0u, fs_.files_removed_.size());
+}
+
+TEST_F(BuildTest, Validation) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+    "build out: cat in |@ validate\n"
+    "build validate: cat in2\n"));
+
+  fs_.Create("in", "");
+  fs_.Create("in2", "");
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  EXPECT_EQ("", err);
+
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ(2u, command_runner_.commands_ran_.size());
+
+  // Test touching "in" only rebuilds "out" ("validate" doesn't depend on
+  // "out").
+  fs_.Tick();
+  fs_.Create("in", "");
+
+  err.clear();
+  command_runner_.commands_ran_.clear();
+  state_.Reset();
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ(1u, command_runner_.commands_ran_.size());
+
+  // Test touching "in2" only rebuilds "validate" ("out" doesn't depend on
+  // "validate").
+  fs_.Tick();
+  fs_.Create("in2", "");
+
+  err.clear();
+  command_runner_.commands_ran_.clear();
+  state_.Reset();
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ(1u, command_runner_.commands_ran_.size());
+}
+
+TEST_F(BuildTest, ValidationDependsOnOutput) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+    "build out: cat in |@ validate\n"
+    "build validate: cat in2 | out\n"));
+
+  fs_.Create("in", "");
+  fs_.Create("in2", "");
+
+  string err;
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  EXPECT_EQ("", err);
+
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ(2u, command_runner_.commands_ran_.size());
+
+  // Test touching "in" rebuilds "out" and "validate".
+  fs_.Tick();
+  fs_.Create("in", "");
+
+  err.clear();
+  command_runner_.commands_ran_.clear();
+  state_.Reset();
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ(2u, command_runner_.commands_ran_.size());
+
+  // Test touching "in2" only rebuilds "validate" ("out" doesn't depend on
+  // "validate").
+  fs_.Tick();
+  fs_.Create("in2", "");
+
+  err.clear();
+  command_runner_.commands_ran_.clear();
+  state_.Reset();
+  EXPECT_TRUE(builder_.AddTarget("out", &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(builder_.Build(&err));
+  EXPECT_EQ("", err);
+
+  EXPECT_EQ(1u, command_runner_.commands_ran_.size());
+}
+
+TEST_F(BuildWithDepsLogTest, ValidationThroughDepfile) {
+  const char* manifest =
+      "build out: cat in |@ validate\n"
+      "build validate: cat in2 | out\n"
+      "build out2: cat in3\n"
+      "  deps = gcc\n"
+      "  depfile = out2.d\n";
+
+  string err;
+
+  {
+    fs_.Create("in", "");
+    fs_.Create("in2", "");
+    fs_.Create("in3", "");
+    fs_.Create("out2.d", "out: out");
+
+    State state;
+    ASSERT_NO_FATAL_FAILURE(AddCatRule(&state));
+    ASSERT_NO_FATAL_FAILURE(AssertParse(&state, manifest));
+
+    DepsLog deps_log;
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
+    ASSERT_EQ("", err);
+
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
+    builder.command_runner_.reset(&command_runner_);
+
+    EXPECT_TRUE(builder.AddTarget("out2", &err));
+    ASSERT_EQ("", err);
+
+    EXPECT_TRUE(builder.Build(&err));
+    EXPECT_EQ("", err);
+
+    // On the first build, only the out2 command is run.
+    EXPECT_EQ(command_runner_.commands_ran_.size(), 1);
+
+    // The deps file should have been removed.
+    EXPECT_EQ(0, fs_.Stat("out2.d", &err));
+
+    deps_log.Close();
+    builder.command_runner_.release();
+  }
+
+  fs_.Tick();
+  command_runner_.commands_ran_.clear();
+
+  {
+    fs_.Create("in2", "");
+    fs_.Create("in3", "");
+
+    State state;
+    ASSERT_NO_FATAL_FAILURE(AddCatRule(&state));
+    ASSERT_NO_FATAL_FAILURE(AssertParse(&state, manifest));
+
+    DepsLog deps_log;
+    ASSERT_TRUE(deps_log.Load("ninja_deps", &state, &err));
+    ASSERT_TRUE(deps_log.OpenForWrite("ninja_deps", fs_, &err));
+    ASSERT_EQ("", err);
+
+    Builder builder(&state, config_, NULL, &deps_log, &fs_, &status_, 0);
+    builder.command_runner_.reset(&command_runner_);
+
+    EXPECT_TRUE(builder.AddTarget("out2", &err));
+    ASSERT_EQ("", err);
+
+    EXPECT_TRUE(builder.Build(&err));
+    EXPECT_EQ("", err);
+
+    // The out and validate actions should have been run as well as out2.
+    ASSERT_EQ(command_runner_.commands_ran_.size(), 3);
+    EXPECT_EQ(command_runner_.commands_ran_[0], "cat in > out");
+    EXPECT_EQ(command_runner_.commands_ran_[1], "cat in2 > validate");
+    EXPECT_EQ(command_runner_.commands_ran_[2], "cat in3 > out2");
+
+    deps_log.Close();
+    builder.command_runner_.release();
+  }
+}
diff --git a/src/clean.cc b/src/clean.cc
index ce6a575..d3c2d56 100644
--- a/src/clean.cc
+++ b/src/clean.cc
@@ -116,10 +116,10 @@
   for (vector<Edge*>::iterator e = state_->edges_.begin();
        e != state_->edges_.end(); ++e) {
     // Do not try to remove phony targets
-    if ((*e)->is_phony())
+    if ((*e)->is_phony() || (*e)->IsPhonyOutput())
       continue;
     // Do not remove generator's files unless generator specified.
-    if (!generator && (*e)->GetBindingBool("generator"))
+    if (!generator && (*e)->IsGenerator())
       continue;
     for (vector<Node*>::iterator out_node = (*e)->outputs_.begin();
          out_node != (*e)->outputs_.end(); ++out_node) {
@@ -135,7 +135,7 @@
 void Cleaner::DoCleanTarget(Node* target) {
   if (Edge* e = target->in_edge()) {
     // Do not try to remove phony targets
-    if (!e->is_phony()) {
+    if (!e->is_phony() && !e->IsPhonyOutput()) {
       Remove(target->path());
       RemoveEdgeFiles(e);
     }
@@ -209,6 +209,9 @@
   for (vector<Edge*>::iterator e = state_->edges_.begin();
        e != state_->edges_.end(); ++e) {
     if ((*e)->rule().name() == rule->name()) {
+      if ((*e)->IsPhonyOutput()) {
+        continue;
+      }
       for (vector<Node*>::iterator out_node = (*e)->outputs_.begin();
            out_node != (*e)->outputs_.end(); ++out_node) {
         Remove((*out_node)->path());
@@ -232,7 +235,8 @@
   assert(rule);
 
   Reset();
-  const Rule* r = state_->bindings_.LookupRule(rule);
+  ScopePosition end_of_root { &state_->root_scope_, kLastDeclIndex };
+  const Rule* r = Scope::LookupRuleAtPos(rule, end_of_root);
   if (r) {
     CleanRule(r);
   } else {
@@ -249,7 +253,8 @@
   PrintHeader();
   for (int i = 0; i < rule_count; ++i) {
     const char* rule_name = rules[i];
-    const Rule* rule = state_->bindings_.LookupRule(rule_name);
+    ScopePosition end_of_root { &state_->root_scope_, kLastDeclIndex };
+    const Rule* rule = Scope::LookupRuleAtPos(rule_name, end_of_root);
     if (rule) {
       if (IsVerbose())
         printf("Rule %s\n", rule_name);
diff --git a/src/clean_test.cc b/src/clean_test.cc
index 395343b..4fdaa96 100644
--- a/src/clean_test.cc
+++ b/src/clean_test.cc
@@ -377,6 +377,35 @@
   EXPECT_LT(0, fs_.Stat("phony", &err));
 }
 
+TEST_F(CleanTest, CleanPhonyOutput) {
+  string err;
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+"  command = echo ${out}\n"
+"  phony_output = true\n"
+"build phout: phony_out t1 t2\n"
+"build t1: cat\n"
+"build t2: cat\n"));
+
+  fs_.Create("phout", "");
+  fs_.Create("t1", "");
+  fs_.Create("t2", "");
+
+  // Check that CleanAll does not remove "phout".
+  Cleaner cleaner(&state_, config_, &fs_);
+  EXPECT_EQ(0, cleaner.CleanAll());
+  EXPECT_EQ(2, cleaner.cleaned_files_count());
+  EXPECT_LT(0, fs_.Stat("phout", &err));
+
+  fs_.Create("t1", "");
+  fs_.Create("t2", "");
+
+  // Check that CleanTarget does not remove "phony".
+  EXPECT_EQ(0, cleaner.CleanTarget("phout"));
+  EXPECT_EQ(2, cleaner.cleaned_files_count());
+  EXPECT_LT(0, fs_.Stat("phout", &err));
+}
+
 TEST_F(CleanTest, CleanDepFileAndRspFileWithSpaces) {
   ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
 "rule cc_dep\n"
diff --git a/src/concurrent_hash_map.h b/src/concurrent_hash_map.h
new file mode 100644
index 0000000..6e5d660
--- /dev/null
+++ b/src/concurrent_hash_map.h
@@ -0,0 +1,274 @@
+// Copyright 2018 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef NINJA_CONCURRENT_HASH_MAP_
+#define NINJA_CONCURRENT_HASH_MAP_
+
+#include <stdint.h>
+
+#include <atomic>
+#include <memory>
+#include <thread>
+#include <utility>
+#include <vector>
+
+#include "metrics.h"
+#include "util.h"
+
+/// A hash table that allows for concurrent lookups and insertions.
+///
+/// Resizing the table isn't thread-safe; inserting items doesn't automatically
+/// resize the table.
+template <typename K, typename V>
+class ConcurrentHashMap {
+public:
+  using key_type = K;
+  using mapped_type = V;
+  using value_type = std::pair<const K, V>;
+  using reference = value_type&;
+  using pointer = value_type*;
+  using const_reference = const value_type&;
+  using const_pointer = const value_type*;
+
+private:
+  struct Node;
+
+  struct NodeStub {
+    std::atomic<Node*> next { nullptr };
+  };
+
+  struct Node : NodeStub {
+    Node(size_t hash, const value_type& value) : hash(hash), value(value) {}
+
+    size_t hash = 0;
+    value_type value {};
+  };
+
+  /// Record the number of elements in the hash map. Assign each thread to a
+  /// separate index within the size table to avoid false sharing among CPUs.
+  struct NINJA_ALIGNAS_CACHE_LINE SizeRecord {
+    std::atomic<ssize_t> size;
+  };
+  std::vector<SizeRecord> size_ { GetThreadSlotCount() };
+
+  void IncrementSize() {
+    thread_local size_t idx = GetThreadSlotIndex();
+    size_[idx].size++;
+  }
+
+public:
+  explicit ConcurrentHashMap(size_t size=1)
+      : table_(std::max<size_t>(size, 1)) {}
+
+  ConcurrentHashMap(const ConcurrentHashMap&) = delete;
+  ConcurrentHashMap& operator=(const ConcurrentHashMap&) = delete;
+
+  ~ConcurrentHashMap() {
+    ForEachNode(table_, [](Node* node) {
+      delete node;
+    });
+  }
+
+  void swap(ConcurrentHashMap& other) {
+    size_.swap(other.size_);
+    table_.swap(other.table_);
+  }
+
+  /// Atomically lookup a key.
+  V* Lookup(const K& key) {
+    std::pair<NodeStub*, Node*> pos = Find(std::hash<K>()(key), key);
+    return pos.first ? nullptr : &pos.second->value.second;
+  }
+
+  /// Atomically lookup a key.
+  const V* Lookup(const K& key) const {
+    auto self = const_cast<ConcurrentHashMap*>(this);
+    std::pair<NodeStub*, Node*> pos = self->Find(std::hash<K>()(key), key);
+    return pos.first ? nullptr : &pos.second->value.second;
+  }
+
+  /// Atomically insert a (K, V) entry into the hash map if K isn't already in
+  /// the map. Does nothing if K is already in the map.
+  ///
+  /// Returns the address of the V entry in the map, and a bool denoting whether
+  /// or not an insertion was performed.
+  ///
+  /// This function avoids returning an iterator because insertion is
+  /// thread-safe while iteration isn't, so returning an iterator seemed
+  /// hazardous.
+  std::pair<V*, bool> insert(const value_type& value) {
+    size_t hash = std::hash<K>()(value.first);
+    std::unique_ptr<Node> node(new Node(hash, value));
+    std::pair<Node*, bool> result = InsertNode(std::move(node));
+    return { &result.first->value.second, result.second };
+  }
+
+  size_t size() const {
+    ssize_t result = 0;
+    for (const SizeRecord& rec : size_)
+      result += rec.size.load();
+    return result;
+  }
+  bool empty() const { return size() == 0; }
+  size_t bucket_count() const { return table_.size(); }
+
+  /// Unlike insertion and lookup, rehashing is *not* thread-safe. This class
+  /// does not automatically rehash the table on insertion.
+  void rehash(size_t buckets) {
+    METRIC_RECORD("ConcurrentHashMap rehash");
+
+    std::vector<NodeStub> tmp_table(std::max<size_t>(buckets, 1));
+    tmp_table.swap(table_);
+    ForEachNode(tmp_table, [this](Node* node) {
+      node->next = nullptr;
+      InsertNode(std::unique_ptr<Node>(node));
+    });
+  }
+
+  void reserve(size_t count) {
+    if (count < bucket_count())
+      return;
+    rehash(count);
+  }
+
+private:
+  template <typename Func>
+  void ForEachNode(const std::vector<NodeStub>& table, Func&& func) {
+    for (const NodeStub& stub : table) {
+      Node* node = stub.next.load();
+      while (node != nullptr) {
+        Node* next = node->next.load();
+        func(node);
+        node = next;
+      }
+    }
+  }
+
+  /// Insert a node into the hash map if it isn't already there.
+  std::pair<Node*, bool> InsertNode(std::unique_ptr<Node> node) {
+    while (true) {
+      std::pair<NodeStub*, Node*> pos = Find(node->hash, node->value.first);
+      if (pos.first == nullptr)
+        return { pos.second, false };
+
+      node->next = pos.second;
+      Node* expected = pos.second;
+      if (pos.first->next.compare_exchange_weak(expected, node.get())) {
+        IncrementSize();
+        return { node.release(), true };
+      }
+    }
+  }
+
+  std::vector<NodeStub> table_;
+
+  /// Find either a matching node or the insertion point for the node. To ensure
+  /// deterministic behavior, the nodes are kept in ascending order, first by
+  /// the hash value, then by the key itself.
+  ///
+  /// This function returns a pair of (stub, node):
+  ///  - If the node is found, stub is null and node is non-null.
+  ///  - If the node isn't found, stub is non-null, and node is the loaded value
+  ///    of the stub, which should be used in the CAS to insert the node.
+  std::pair<NodeStub*, Node*> Find(size_t hash, const K& key) {
+    NodeStub* stub = &table_[hash % table_.size()];
+    while (true) {
+      Node* ptr = stub->next.load();
+      if (ptr == nullptr || hash < ptr->hash) {
+        return { stub, ptr };
+      }
+      if (hash == ptr->hash) {
+        // In the common case, when the hash and ptr->hash are equal, we expect
+        // the keys to be equal too, so compare for equality before comparing
+        // for less-than.
+        if (key == ptr->value.first) {
+          return { nullptr, ptr };
+        } else if (key < ptr->value.first) {
+          return { stub, ptr };
+        }
+      }
+      stub = ptr;
+    }
+  }
+
+  struct MutCfg { using ValueType = value_type; };
+  struct ConstCfg { using ValueType = const value_type; };
+
+  /// Something like a ForwardIterator. Iteration over the table isn't
+  /// thread-safe.
+  template <typename Cfg>
+  class iterator_impl {
+    iterator_impl(const ConcurrentHashMap* container) : container_(container) {}
+    iterator_impl(const ConcurrentHashMap* container, size_t bucket, Node* node)
+        : container_(container),
+          bucket_(bucket),
+          node_(node) {}
+
+    const ConcurrentHashMap* container_ = nullptr;
+    size_t bucket_ = SIZE_MAX;
+    Node* node_ = nullptr;
+
+  public:
+    /// This constructor is a copy constructor for const_iterator and an
+    /// implicit conversion for const_iterator -> iterator.
+    iterator_impl(const iterator_impl<ConstCfg>& other)
+        : container_(other.container_),
+          bucket_(other.bucket_),
+          node_(other.node_) {}
+
+    typename Cfg::ValueType& operator*() { return node_->value; }
+    typename Cfg::ValueType* operator->() { return &node_->value; }
+
+    iterator_impl<Cfg>& operator++() {
+      node_ = node_->next.load();
+      if (node_ == nullptr)
+        *this = container_->FindNextOccupiedBucket(bucket_ + 1);
+      return *this;
+    }
+
+    bool operator==(const iterator_impl<Cfg>& other) const {
+      return node_ == other.node_;
+    }
+    bool operator!=(const iterator_impl<Cfg>& other) const {
+      return node_ != other.node_;
+    }
+
+    friend class ConcurrentHashMap;
+  };
+
+public:
+  using iterator = iterator_impl<MutCfg>;
+  using const_iterator = iterator_impl<ConstCfg>;
+
+  iterator begin() { return FindNextOccupiedBucket(0); }
+  iterator end() { return iterator { this }; }
+  const_iterator begin() const { return FindNextOccupiedBucket(0); }
+  const_iterator end() const { return const_iterator { this }; }
+  const_iterator cbegin() const { return FindNextOccupiedBucket(0); }
+  const_iterator cend() const { return const_iterator { this }; }
+
+private:
+  const_iterator FindNextOccupiedBucket(size_t idx) const {
+    while (idx < table_.size()) {
+      Node* node = table_[idx].next.load();
+      if (node != nullptr) {
+        return const_iterator { this, idx, node };
+      }
+      ++idx;
+    }
+    return end();
+  }
+};
+
+#endif  // NINJA_CONCURRENT_HASH_MAP_
diff --git a/src/debug_flags.cc b/src/debug_flags.cc
index 44b14c4..e77d351 100644
--- a/src/debug_flags.cc
+++ b/src/debug_flags.cc
@@ -19,3 +19,5 @@
 bool g_keep_rsp = false;
 
 bool g_experimental_statcache = true;
+
+bool g_use_threads = true;
diff --git a/src/debug_flags.h b/src/debug_flags.h
index e08a43b..d2d57ee 100644
--- a/src/debug_flags.h
+++ b/src/debug_flags.h
@@ -30,4 +30,6 @@
 
 extern bool g_experimental_statcache;
 
+extern bool g_use_threads;
+
 #endif // NINJA_EXPLAIN_H_
diff --git a/src/depfile_parser.cc b/src/depfile_parser.cc
index 405289f..16403f1 100644
--- a/src/depfile_parser.cc
+++ b/src/depfile_parser.cc
@@ -1,4 +1,4 @@
-/* Generated by re2c 1.1.1 */
+/* Generated by re2c 1.3 */
 // Copyright 2011 Google Inc. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/src/deps_log.cc b/src/deps_log.cc
index 0bb96f3..807dbd3 100644
--- a/src/deps_log.cc
+++ b/src/deps_log.cc
@@ -25,14 +25,21 @@
 typedef unsigned __int32 uint32_t;
 #endif
 
+#include <numeric>
+
+#include "disk_interface.h"
 #include "graph.h"
 #include "metrics.h"
+#include "parallel_map.h"
 #include "state.h"
 #include "util.h"
 
 // The version is stored as 4 bytes after the signature and also serves as a
 // byte order mark. Signature and version combined are 16 bytes long.
-const char kFileSignature[] = "# ninjadeps\n";
+static constexpr StringPiece kFileSignature { "# ninjadeps\n", 12 };
+static_assert(kFileSignature.size() % 4 == 0,
+              "file signature size is not a multiple of 4");
+static constexpr size_t kFileHeaderSize = kFileSignature.size() + 4;
 const int kCurrentVersion = 4;
 
 // Record size is currently limited to less than the full 32 bit, due to
@@ -43,9 +50,9 @@
   Close();
 }
 
-bool DepsLog::OpenForWrite(const string& path, string* err) {
+bool DepsLog::OpenForWrite(const string& path, const DiskInterface& disk, string* err) {
   if (needs_recompaction_) {
-    if (!Recompact(path, err))
+    if (!Recompact(path, disk, err))
       return false;
   }
   
@@ -64,7 +71,7 @@
   fseek(file_, 0, SEEK_END);
 
   if (ftell(file_) == 0) {
-    if (fwrite(kFileSignature, sizeof(kFileSignature) - 1, 1, file_) < 1) {
+    if (fwrite(kFileSignature.data(), kFileSignature.size(), 1, file_) < 1) {
       *err = strerror(errno);
       return false;
     }
@@ -167,123 +174,481 @@
   file_ = NULL;
 }
 
-bool DepsLog::Load(const string& path, State* state, string* err) {
-  METRIC_RECORD(".ninja_deps load");
-  char buf[kMaxRecordSize + 1];
-  FILE* f = fopen(path.c_str(), "rb");
-  if (!f) {
-    if (errno == ENOENT)
-      return true;
-    *err = strerror(errno);
+struct DepsLogWordSpan {
+  size_t begin; // starting index into a deps log buffer of uint32 words
+  size_t end; // stopping index
+};
+
+/// Split the v4 deps log into independently-parseable chunks using a heuristic.
+/// If the heuristic fails, we'll still load the file correctly, but it could be
+/// slower.
+///
+/// There are two kinds of records -- path and deps records. Their formats:
+///
+///    path:
+///     - uint32 size -- high bit is clear
+///     - String content. The string is padded to a multiple of 4 bytes with
+///       trailing NULs.
+///     - uint32 checksum (ones complement of the path's index / node ID)
+///
+///    deps:
+///     - uint32 size -- high bit is set
+///     - int32 output_path_id
+///     - uint32 output_path_mtime_lo
+///     - uint32 output_path_mtime_hi
+///     - int32 input_path_id[...] -- every remaining word is an input ID
+///
+/// To split the deps log into chunks, look for uint32 words with the value
+/// 0x8000xxxx, where xxxx is nonzero. Such a word is almost guaranteed to be
+/// the size field of a deps record (with fewer than ~16K dependencies):
+///  - It can't be part of a string, because paths can't have embedded NULs.
+///  - It (probably) can't be a node ID, because node IDs are represented using
+///    "int", and it would be unlikely to have more than 2 billion of them. An
+///    Android build typically has about 1 million nodes.
+///  - It's unlikely to be part of a path checksum, because that would also
+///    imply that we have at least 2 billion nodes.
+///  - It could be the upper word of a mtime from 2262, or the lower word, which
+///    wraps every ~4s. We rule these out by looking for the mtime's deps size
+///    two or three words above the split candidate.
+///
+/// This heuristic can fail in a few ways:
+///  - We only find path records in the area we scan.
+///  - The deps records all have >16K of dependencies. (Almost all deps records
+///    I've seen in the Android build have a few hundred. Only a few have ~10K.)
+///  - All deps records with <16K of dependencies are preceded by something that
+///    the mtime check rejects:
+///     - A deps record with an mtime within a 524us span every 4s, with zero or
+///       one dependency.
+///     - A deps record with an mtime from 2262, with one or two dependencies.
+///     - A path record containing "xx xx 0[0-7] 80" (little-endian) or
+///       "80 0[0-7] xx xx" (big-endian) as the last or second-to-last word. The
+///       "0[0-7]" is a control character, and "0[0-7] 80" is invalid UTF-8.
+///
+/// Maybe we can add a delimiter to the log format and replace this heuristic.
+size_t MustBeDepsRecordHeader(DepsLogData log, size_t index) {
+  assert(index < log.size);
+
+  size_t this_header = log.words[index];
+  if ((this_header & 0xffff0000) != 0x80000000) return false;
+  if ((this_header & 0x0000ffff) == 0) return false;
+
+  // We've either found a deps record or an mtime. If it's an mtime, the
+  // word two or three spaces back will be a valid deps size (0x800xxxxx).
+  auto might_be_deps_record_header = [](size_t header) {
+    return (header & 0x80000000) == 0x80000000 &&
+           (header & 0x7fffffff) <= kMaxRecordSize;
+  };
+  if (index >= 3 && might_be_deps_record_header(log.words[index - 3])) {
+    return false;
+  }
+  if (index >= 2 && might_be_deps_record_header(log.words[index - 2])) {
     return false;
   }
 
-  bool valid_header = true;
+  // Success: As long as the deps log is valid up to this point, this index
+  // must start a deps record.
+  return true;
+}
+
+static std::vector<DepsLogWordSpan>
+SplitDepsLog(DepsLogData log, ThreadPool* thread_pool) {
+  if (log.size == 0) return {};
+
+  std::vector<std::pair<size_t, size_t>> blind_splits =
+      SplitByThreads(log.size);
+  std::vector<DepsLogWordSpan> chunks;
+  size_t chunk_start = 0;
+
+  auto split_candidates = ParallelMap(thread_pool, blind_splits,
+      [log](std::pair<size_t, size_t> chunk) {
+    for (size_t i = chunk.first; i < chunk.second; ++i) {
+      if (MustBeDepsRecordHeader(log, i)) {
+        return i;
+      }
+    }
+    return SIZE_MAX;
+  });
+  for (size_t candidate : split_candidates) {
+    assert(chunk_start <= candidate);
+    if (candidate != SIZE_MAX && chunk_start < candidate) {
+      chunks.push_back({ chunk_start, candidate });
+      chunk_start = candidate;
+    }
+  }
+
+  assert(chunk_start < log.size);
+  chunks.push_back({ chunk_start, log.size });
+  return chunks;
+}
+
+struct InputRecord {
+  enum Kind { InvalidHeader, PathRecord, DepsRecord } kind = InvalidHeader;
+  size_t size = 0; // number of 32-bit words, including the header
+
+  union {
+    struct {
+      StringPiece path;
+      int checksum;
+    } path;
+
+    struct {
+      int output_id;
+      TimeStamp mtime;
+      const uint32_t* deps;
+      size_t deps_count;
+    } deps;
+  } u = {};
+};
+
+/// Parse a deps log record at the given index within the loaded deps log.
+/// If there is anything wrong with the header (e.g. the record extends past the
+/// end of the file), this function returns a blank InvalidHeader record.
+static InputRecord ParseRecord(DepsLogData log, size_t index) {
+  InputRecord result {};
+  assert(index < log.size);
+
+  // The header of a record is the size of the record, in bytes, without
+  // including the header itself. The high bit is set for a deps record and
+  // clear for a path record.
+  const uint32_t header = log.words[index];
+  const uint32_t raw_size = header & 0x7fffffff;
+  if (raw_size % sizeof(uint32_t) != 0) return result;
+  if (raw_size > kMaxRecordSize) return result;
+
+  // Add the header to the size for easier iteration over records later on.
+  const size_t size = raw_size / sizeof(uint32_t) + 1;
+  if (log.size - index < size) return result;
+
+  if ((header & 0x80000000) == 0) {
+    // Path record (header, content, checksum).
+    if (size < 3) return result;
+
+    const char* path = reinterpret_cast<const char*>(&log.words[index + 1]);
+    size_t path_size = (size - 2) * sizeof(uint32_t);
+    if (path[path_size - 1] == '\0') --path_size;
+    if (path[path_size - 1] == '\0') --path_size;
+    if (path[path_size - 1] == '\0') --path_size;
+
+    result.kind = InputRecord::PathRecord;
+    result.size = size;
+    result.u.path.path = StringPiece(path, path_size);
+    result.u.path.checksum = log.words[index + size - 1];
+    return result;
+  } else {
+    // Deps record (header, output_id, mtime_lo, mtime_hi).
+    if (size < 4) return result;
+
+    result.kind = InputRecord::DepsRecord;
+    result.size = size;
+    result.u.deps.output_id = log.words[index + 1];
+    result.u.deps.mtime =
+      (TimeStamp)(((uint64_t)(unsigned int)log.words[index + 3] << 32) |
+                  (uint64_t)(unsigned int)log.words[index + 2]);
+    result.u.deps.deps = &log.words[index + 4];
+    result.u.deps.deps_count = size - 4;
+    return result;
+  }
+}
+
+struct DepsLogInputFile {
+  std::unique_ptr<LoadedFile> file;
+  DepsLogData data;
+};
+
+static bool OpenDepsLogForReading(const std::string& path,
+                                  DepsLogInputFile* log,
+                                  std::string* err) {
+  *log = {};
+
+  RealDiskInterface file_reader;
+  std::string load_err;
+  switch (file_reader.LoadFile(path, &log->file, &load_err)) {
+  case FileReader::Okay:
+    break;
+  case FileReader::NotFound:
+    return true;
+  default:
+    *err = load_err;
+    return false;
+  }
+
+  bool valid_header = false;
   int version = 0;
-  if (!fgets(buf, sizeof(buf), f) || fread(&version, 4, 1, f) < 1)
-    valid_header = false;
+  if (log->file->content().size() >= kFileHeaderSize ||
+      log->file->content().substr(0, kFileSignature.size()) == kFileSignature) {
+    valid_header = true;
+    memcpy(&version,
+           log->file->content().data() + kFileSignature.size(),
+           sizeof(version));
+  }
+
   // Note: For version differences, this should migrate to the new format.
   // But the v1 format could sometimes (rarely) end up with invalid data, so
   // don't migrate v1 to v3 to force a rebuild. (v2 only existed for a few days,
   // and there was no release with it, so pretend that it never happened.)
-  if (!valid_header || strcmp(buf, kFileSignature) != 0 ||
-      version != kCurrentVersion) {
+  if (!valid_header || version != kCurrentVersion) {
     if (version == 1)
       *err = "deps log version change; rebuilding";
     else
       *err = "bad deps log signature or version; starting over";
-    fclose(f);
+    log->file.reset();
     unlink(path.c_str());
     // Don't report this as a failure.  An empty deps log will cause
     // us to rebuild the outputs anyway.
     return true;
   }
 
-  long offset;
-  bool read_failed = false;
-  int unique_dep_record_count = 0;
-  int total_dep_record_count = 0;
-  for (;;) {
-    offset = ftell(f);
+  log->data.words =
+      reinterpret_cast<const uint32_t*>(
+          log->file->content().data() + kFileHeaderSize);
+  log->data.size =
+      (log->file->content().size() - kFileHeaderSize) / sizeof(uint32_t);
 
-    unsigned size;
-    if (fread(&size, 4, 1, f) < 1) {
-      if (!feof(f))
-        read_failed = true;
-      break;
-    }
-    bool is_deps = (size >> 31) != 0;
-    size = size & 0x7FFFFFFF;
+  return true;
+}
 
-    if (size > kMaxRecordSize || fread(buf, size, 1, f) < 1) {
-      read_failed = true;
-      break;
-    }
+template <typename Func>
+static bool ForEachRecord(DepsLogData log, DepsLogWordSpan chunk,
+                          Func&& callback) {
+  InputRecord record {};
+  for (size_t index = chunk.begin; index < chunk.end; index += record.size) {
+    record = ParseRecord(log, index);
+    if (!callback(static_cast<const InputRecord&>(record))) return false;
+    if (record.kind == InputRecord::InvalidHeader) break;
+  }
+  return true;
+}
 
-    if (is_deps) {
-      assert(size % 4 == 0);
-      int* deps_data = reinterpret_cast<int*>(buf);
-      int out_id = deps_data[0];
-      TimeStamp mtime;
-      mtime = (TimeStamp)(((uint64_t)(unsigned int)deps_data[2] << 32) |
-                          (uint64_t)(unsigned int)deps_data[1]);
-      deps_data += 3;
-      int deps_count = (size / 4) - 3;
+struct DepsLogNodeSpan {
+  int begin; // starting node ID of span
+  int end; // stopping node ID of span
+};
 
-      Deps* deps = new Deps(mtime, deps_count);
-      for (int i = 0; i < deps_count; ++i) {
-        assert(deps_data[i] < (int)nodes_.size());
-        assert(nodes_[deps_data[i]]);
-        deps->nodes[i] = nodes_[deps_data[i]];
+/// Determine the range of node IDs for each chunk of the deps log. The range
+/// for a chunk will only be used if the preceding chunks are valid.
+static std::vector<DepsLogNodeSpan>
+FindInitialNodeSpans(DepsLogData log,
+                     const std::vector<DepsLogWordSpan>& chunk_words,
+                     ThreadPool* thread_pool) {
+  // First count the number of path records (nodes) in each chunk.
+  std::vector<int> chunk_node_counts = ParallelMap(thread_pool, chunk_words,
+      [log](DepsLogWordSpan chunk) {
+    int node_count = 0;
+    ForEachRecord(log, chunk, [&node_count](const InputRecord& record) {
+      if (record.kind == InputRecord::PathRecord) {
+        ++node_count;
       }
+      return true;
+    });
+    return node_count;
+  });
 
-      total_dep_record_count++;
-      if (!UpdateDeps(out_id, deps))
-        ++unique_dep_record_count;
+  // Compute an initial [begin, end) node ID range for each chunk.
+  std::vector<DepsLogNodeSpan> result;
+  int next_node_id = 0;
+  for (int num_nodes : chunk_node_counts) {
+    result.push_back({ next_node_id, next_node_id + num_nodes });
+    next_node_id += num_nodes;
+  }
+  return result;
+}
+
+static bool IsValidRecord(const InputRecord& record, int next_node_id) {
+  auto is_valid_id = [next_node_id](int id) {
+    return id >= 0 && id < next_node_id;
+  };
+
+  switch (record.kind) {
+  case InputRecord::InvalidHeader:
+    return false;
+  case InputRecord::PathRecord:
+    // Validate the path's checksum.
+    if (record.u.path.checksum != ~next_node_id) {
+      return false;
+    }
+    break;
+  case InputRecord::DepsRecord:
+    // Verify that input/output node IDs are valid.
+    if (!is_valid_id(record.u.deps.output_id)) {
+      return false;
+    }
+    for (size_t i = 0; i < record.u.deps.deps_count; ++i) {
+      if (!is_valid_id(record.u.deps.deps[i])) {
+        return false;
+      }
+    }
+    break;
+  }
+  return true;
+}
+
+/// Validate the deps log. If there is an invalid record, the function truncates
+/// the word+node span vectors just before the invalid record.
+static void ValidateDepsLog(DepsLogData log,
+                            std::vector<DepsLogWordSpan>* chunk_words,
+                            std::vector<DepsLogNodeSpan>* chunk_nodes,
+                            ThreadPool* thread_pool) {
+  std::atomic<size_t> num_valid_chunks { chunk_words->size() };
+
+  ParallelMap(thread_pool, IntegralRange<size_t>(0, num_valid_chunks),
+      [&](size_t chunk_index) {
+
+    size_t next_word_idx = (*chunk_words)[chunk_index].begin;
+    int next_node_id = (*chunk_nodes)[chunk_index].begin;
+
+    bool success = ForEachRecord(log, (*chunk_words)[chunk_index],
+        [&](const InputRecord& record) {
+      if (!IsValidRecord(record, next_node_id)) {
+        return false;
+      }
+      next_word_idx += record.size;
+      if (record.kind == InputRecord::PathRecord) {
+        ++next_node_id;
+      }
+      return true;
+    });
+
+    if (success) {
+      assert(next_word_idx == (*chunk_words)[chunk_index].end);
+      assert(next_node_id == (*chunk_nodes)[chunk_index].end);
     } else {
-      int path_size = size - 4;
-      assert(path_size > 0);  // CanonicalizePath() rejects empty paths.
-      // There can be up to 3 bytes of padding.
-      if (buf[path_size - 1] == '\0') --path_size;
-      if (buf[path_size - 1] == '\0') --path_size;
-      if (buf[path_size - 1] == '\0') --path_size;
-      StringPiece subpath(buf, path_size);
-      // It is not necessary to pass in a correct slash_bits here. It will
-      // either be a Node that's in the manifest (in which case it will already
-      // have a correct slash_bits that GetNode will look up), or it is an
-      // implicit dependency from a .d which does not affect the build command
-      // (and so need not have its slashes maintained).
-      Node* node = state->GetNode(subpath, 0);
+      (*chunk_words)[chunk_index].end = next_word_idx;
+      (*chunk_nodes)[chunk_index].end = next_node_id;
+      AtomicUpdateMinimum(&num_valid_chunks, chunk_index + 1);
+    }
+  });
 
-      // Check that the expected index matches the actual index. This can only
-      // happen if two ninja processes write to the same deps log concurrently.
-      // (This uses unary complement to make the checksum look less like a
-      // dependency record entry.)
-      unsigned checksum = *reinterpret_cast<unsigned*>(buf + size - 4);
-      int expected_id = ~checksum;
-      int id = nodes_.size();
-      if (id != expected_id) {
-        read_failed = true;
-        break;
+  chunk_words->resize(num_valid_chunks);
+  chunk_nodes->resize(num_valid_chunks);
+}
+
+bool DepsLog::Load(const string& path, State* state, string* err) {
+  METRIC_RECORD(".ninja_deps load");
+
+  assert(nodes_.empty());
+  DepsLogInputFile log_file;
+
+  if (!OpenDepsLogForReading(path, &log_file, err)) return false;
+  if (log_file.file.get() == nullptr) return true;
+
+  DepsLogData log = log_file.data;
+
+  std::unique_ptr<ThreadPool> thread_pool = CreateThreadPool();
+
+  std::vector<DepsLogWordSpan> chunk_words = SplitDepsLog(log, thread_pool.get());
+  std::vector<DepsLogNodeSpan> chunk_nodes =
+      FindInitialNodeSpans(log, chunk_words, thread_pool.get());
+
+  // Validate the log and truncate the vectors after an invalid record.
+  ValidateDepsLog(log, &chunk_words, &chunk_nodes, thread_pool.get());
+  assert(chunk_words.size() == chunk_nodes.size());
+
+  const size_t chunk_count = chunk_words.size();
+  const int node_count = chunk_nodes.empty() ? 0 : chunk_nodes.back().end;
+
+  // The state path hash table doesn't automatically resize, so make sure that
+  // it has at least one bucket for each node in this deps log.
+  state->paths_.reserve(node_count);
+
+  nodes_.resize(node_count);
+
+  // A map from a node ID to the final file index of the deps record outputting
+  // the given node ID.
+  std::vector<std::atomic<ssize_t>> dep_index(node_count);
+  for (auto& index : dep_index) {
+    // Write a value of -1 to indicate that no deps record outputs this ID. We
+    // don't need these stores to be synchronized with other threads, so use
+    // relaxed stores, which are much faster.
+    index.store(-1, std::memory_order_relaxed);
+  }
+
+  // Add the nodes into the build graph, find the last deps record
+  // outputting each node, and count the total number of deps records.
+  const std::vector<size_t> dep_record_counts = ParallelMap(thread_pool.get(),
+      IntegralRange<size_t>(0, chunk_count),
+      [log, state, chunk_words, chunk_nodes, &dep_index,
+        this](size_t chunk_index) {
+    size_t next_word_idx = chunk_words[chunk_index].begin;
+    int next_node_id = chunk_nodes[chunk_index].begin;
+    int stop_node_id = chunk_nodes[chunk_index].end;
+    (void)stop_node_id; // suppress unused variable compiler warning
+    size_t dep_record_count = 0;
+
+    ForEachRecord(log, chunk_words[chunk_index],
+        [&, this](const InputRecord& record) {
+      assert(record.kind != InputRecord::InvalidHeader);
+      if (record.kind == InputRecord::PathRecord) {
+        int node_id = next_node_id++;
+        assert(node_id < stop_node_id);
+        assert(record.u.path.checksum == ~node_id);
+
+        // It is not necessary to pass in a correct slash_bits here. It will
+        // either be a Node that's in the manifest (in which case it will
+        // already have a correct slash_bits that GetNode will look up), or it
+        // is an implicit dependency from a .d which does not affect the build
+        // command (and so need not have its slashes maintained).
+        Node* node = state->GetNode(record.u.path.path, 0);
+        assert(node->id() < 0);
+        node->set_id(node_id);
+        nodes_[node_id] = node;
+      } else if (record.kind == InputRecord::DepsRecord) {
+        const int output_id = record.u.deps.output_id;
+        assert(static_cast<size_t>(output_id) < dep_index.size());
+        AtomicUpdateMaximum(&dep_index[output_id],
+                            static_cast<ssize_t>(next_word_idx));
+        ++dep_record_count;
       }
+      next_word_idx += record.size;
+      return true;
+    });
+    assert(next_node_id == stop_node_id);
+    return dep_record_count;
+  });
 
-      assert(node->id() < 0);
-      node->set_id(id);
-      nodes_.push_back(node);
+  // Count the number of total and unique deps records.
+  const size_t total_dep_record_count =
+      std::accumulate(dep_record_counts.begin(), dep_record_counts.end(),
+                      static_cast<size_t>(0));
+  size_t unique_dep_record_count = 0;
+  for (auto& index : dep_index) {
+    if (index.load(std::memory_order_relaxed) != -1) {
+      ++unique_dep_record_count;
     }
   }
 
-  if (read_failed) {
-    // An error occurred while loading; try to recover by truncating the
-    // file to the last fully-read record.
-    if (ferror(f)) {
-      *err = strerror(ferror(f));
-    } else {
-      *err = "premature end of file";
-    }
-    fclose(f);
+  // Add the deps records.
+  deps_.resize(node_count);
+  ParallelMap(thread_pool.get(), IntegralRange<int>(0, node_count),
+      [this, log, &dep_index](int node_id) {
+    ssize_t index = dep_index[node_id];
+    if (index == -1) return;
 
-    if (!Truncate(path, offset, err))
+    InputRecord record = ParseRecord(log, index);
+    assert(record.kind == InputRecord::DepsRecord);
+    assert(record.u.deps.output_id == node_id);
+
+    Deps* deps = new Deps(record.u.deps.mtime, record.u.deps.deps_count);
+    for (size_t i = 0; i < record.u.deps.deps_count; ++i) {
+      const int input_id = record.u.deps.deps[i];
+      assert(static_cast<size_t>(input_id) < nodes_.size());
+      Node* node = nodes_[input_id];
+      assert(node != nullptr);
+      deps->nodes[i] = node;
+    }
+    deps_[node_id] = deps;
+  });
+
+  const size_t actual_file_size = log_file.file->content().size();
+  const size_t parsed_file_size = kFileHeaderSize +
+      (chunk_words.empty() ? 0 : chunk_words.back().end) * sizeof(uint32_t);
+  assert(parsed_file_size <= actual_file_size);
+  if (parsed_file_size < actual_file_size) {
+    // An error occurred while loading; try to recover by truncating the file to
+    // the last fully-read record.
+    *err = "premature end of file";
+    log_file.file.reset();
+
+    if (!Truncate(path, parsed_file_size, err))
       return false;
 
     // The truncate succeeded; we'll just report the load error as a
@@ -292,11 +657,9 @@
     return true;
   }
 
-  fclose(f);
-
   // Rebuild the log if there are too many dead records.
-  int kMinCompactionEntryCount = 1000;
-  int kCompactionRatio = 3;
+  const unsigned kMinCompactionEntryCount = 1000;
+  const unsigned kCompactionRatio = 3;
   if (total_dep_record_count > kMinCompactionEntryCount &&
       total_dep_record_count > unique_dep_record_count * kCompactionRatio) {
     needs_recompaction_ = true;
@@ -313,7 +676,7 @@
   return deps_[node->id()];
 }
 
-bool DepsLog::Recompact(const string& path, string* err) {
+bool DepsLog::Recompact(const string& path, const DiskInterface& disk, string* err) {
   METRIC_RECORD(".ninja_deps recompact");
 
   Close();
@@ -324,7 +687,7 @@
   unlink(temp_path.c_str());
 
   DepsLog new_log;
-  if (!new_log.OpenForWrite(temp_path, err))
+  if (!new_log.OpenForWrite(temp_path, disk, err))
     return false;
 
   // Clear all known ids so that new ones can be reassigned.  The new indices
@@ -337,8 +700,21 @@
     Deps* deps = deps_[old_id];
     if (!deps) continue;  // If nodes_[old_id] is a leaf, it has no deps.
 
-    if (!IsDepsEntryLiveFor(nodes_[old_id]))
-      continue;
+    Node* node = nodes_[old_id];
+    if (node->in_edge()) {
+      // If the current manifest defines this edge, skip if it's not dep
+      // producing.
+      if (node->in_edge()->GetBinding("deps").empty()) continue;
+    } else {
+      // If the current manifest does not define this edge, skip if it's missing
+      // from the disk.
+      string err;
+      TimeStamp mtime = disk.LStat(node->path(), nullptr, &err);
+      if (mtime == -1)
+        Error("%s", err.c_str()); // log and ignore LStat() errors
+      if (mtime == 0)
+        continue;
+    }
 
     if (!new_log.RecordDeps(nodes_[old_id], deps->mtime,
                             deps->node_count, deps->nodes)) {
@@ -368,11 +744,8 @@
 
 bool DepsLog::IsDepsEntryLiveFor(Node* node) {
   // Skip entries that don't have in-edges or whose edges don't have a
-  // "deps" attribute. They were in the deps log from previous builds, but
-  // the the files they were for were removed from the build and their deps
-  // entries are no longer needed.
-  // (Without the check for "deps", a chain of two or more nodes that each
-  // had deps wouldn't be collected in a single recompaction.)
+  // "deps" attribute. They were in the deps log from previous builds, but the
+  // files they were for were removed from the build.
   return node->in_edge() && !node->in_edge()->GetBinding("deps").empty();
 }
 
diff --git a/src/deps_log.h b/src/deps_log.h
index 3812a28..c4a60e2 100644
--- a/src/deps_log.h
+++ b/src/deps_log.h
@@ -23,9 +23,19 @@
 
 #include "timestamp.h"
 
+struct DiskInterface;
 struct Node;
 struct State;
 
+/// A buffer of uint32 words used to load a deps log. Does not include the
+/// log file's header.
+struct DepsLogData {
+  const uint32_t* words = nullptr;
+  size_t size = 0;
+};
+
+size_t MustBeDepsRecordHeader(DepsLogData log, size_t index);
+
 /// As build commands run they can output extra dependency information
 /// (e.g. header dependencies for C source) dynamically.  DepsLog collects
 /// that information at build time and uses it for subsequent builds.
@@ -70,7 +80,7 @@
   ~DepsLog();
 
   // Writing (build-time) interface.
-  bool OpenForWrite(const string& path, string* err);
+  bool OpenForWrite(const string& path, const DiskInterface& disk, string* err);
   bool RecordDeps(Node* node, TimeStamp mtime, const vector<Node*>& nodes);
   bool RecordDeps(Node* node, TimeStamp mtime, int node_count, Node** nodes);
   void Close();
@@ -88,7 +98,7 @@
   Deps* GetDeps(Node* node);
 
   /// Rewrite the known log entries, throwing away old data.
-  bool Recompact(const string& path, string* err);
+  bool Recompact(const string& path, const DiskInterface& disk, string* err);
 
   /// Returns if the deps entry for a node is still reachable from the manifest.
   ///
diff --git a/src/deps_log_test.cc b/src/deps_log_test.cc
index 0cdeb45..c43529e 100644
--- a/src/deps_log_test.cc
+++ b/src/deps_log_test.cc
@@ -41,7 +41,8 @@
   State state1;
   DepsLog log1;
   string err;
-  EXPECT_TRUE(log1.OpenForWrite(kTestFilename, &err));
+  VirtualFileSystem fs;
+  EXPECT_TRUE(log1.OpenForWrite(kTestFilename, fs, &err));
   ASSERT_EQ("", err);
 
   {
@@ -93,9 +94,14 @@
   State state1;
   DepsLog log1;
   string err;
-  EXPECT_TRUE(log1.OpenForWrite(kTestFilename, &err));
+  VirtualFileSystem fs;
+  EXPECT_TRUE(log1.OpenForWrite(kTestFilename, fs, &err));
   ASSERT_EQ("", err);
 
+  // The paths_ concurrent hash table doesn't automatically resize itself, so
+  // reserve space in advance before synthesizing paths.
+  state1.paths_.reserve(kNumDeps);
+
   {
     vector<Node*> deps;
     for (int i = 0; i < kNumDeps; ++i) {
@@ -128,7 +134,8 @@
     State state;
     DepsLog log;
     string err;
-    EXPECT_TRUE(log.OpenForWrite(kTestFilename, &err));
+    VirtualFileSystem fs;
+    EXPECT_TRUE(log.OpenForWrite(kTestFilename, fs, &err));
     ASSERT_EQ("", err);
 
     vector<Node*> deps;
@@ -148,9 +155,10 @@
     State state;
     DepsLog log;
     string err;
+    VirtualFileSystem fs;
     EXPECT_TRUE(log.Load(kTestFilename, &state, &err));
 
-    EXPECT_TRUE(log.OpenForWrite(kTestFilename, &err));
+    EXPECT_TRUE(log.OpenForWrite(kTestFilename, fs, &err));
     ASSERT_EQ("", err);
 
     vector<Node*> deps;
@@ -182,7 +190,8 @@
     ASSERT_NO_FATAL_FAILURE(AssertParse(&state, kManifest));
     DepsLog log;
     string err;
-    ASSERT_TRUE(log.OpenForWrite(kTestFilename, &err));
+    VirtualFileSystem fs;
+    ASSERT_TRUE(log.OpenForWrite(kTestFilename, fs, &err));
     ASSERT_EQ("", err);
 
     vector<Node*> deps;
@@ -210,9 +219,10 @@
     ASSERT_NO_FATAL_FAILURE(AssertParse(&state, kManifest));
     DepsLog log;
     string err;
+    VirtualFileSystem fs;
     ASSERT_TRUE(log.Load(kTestFilename, &state, &err));
 
-    ASSERT_TRUE(log.OpenForWrite(kTestFilename, &err));
+    ASSERT_TRUE(log.OpenForWrite(kTestFilename, fs, &err));
     ASSERT_EQ("", err);
 
     vector<Node*> deps;
@@ -252,7 +262,8 @@
     ASSERT_EQ("foo.h", deps->nodes[0]->path());
     ASSERT_EQ("baz.h", deps->nodes[1]->path());
 
-    ASSERT_TRUE(log.Recompact(kTestFilename, &err));
+    VirtualFileSystem fs;
+    ASSERT_TRUE(log.Recompact(kTestFilename, fs, &err));
 
     // The in-memory deps graph should still be valid after recompaction.
     deps = log.GetDeps(out);
@@ -301,17 +312,25 @@
     ASSERT_EQ("foo.h", deps->nodes[0]->path());
     ASSERT_EQ("baz.h", deps->nodes[1]->path());
 
-    ASSERT_TRUE(log.Recompact(kTestFilename, &err));
+    // Keep out.o dependencies.
+    VirtualFileSystem fs;
+    fs.Create("out.o", "");
+
+    ASSERT_TRUE(log.Recompact(kTestFilename, fs, &err));
+
+    deps = log.GetDeps(out);
+    ASSERT_TRUE(deps);
+    ASSERT_EQ(1, deps->mtime);
+    ASSERT_EQ(1, deps->node_count);
+    ASSERT_EQ("foo.h", deps->nodes[0]->path());
+    ASSERT_EQ(out, log.nodes()[out->id()]);
 
     // The previous entries should have been removed.
-    deps = log.GetDeps(out);
-    ASSERT_FALSE(deps);
-
     deps = log.GetDeps(other_out);
     ASSERT_FALSE(deps);
 
+    //ASSERT_EQ(-1, state.LookupNode("foo.h")->id());
     // The .h files pulled in via deps should no longer have ids either.
-    ASSERT_EQ(-1, state.LookupNode("foo.h")->id());
     ASSERT_EQ(-1, state.LookupNode("baz.h")->id());
 
     // The file should have shrunk more.
@@ -355,7 +374,8 @@
     State state;
     DepsLog log;
     string err;
-    EXPECT_TRUE(log.OpenForWrite(kTestFilename, &err));
+    VirtualFileSystem fs;
+    EXPECT_TRUE(log.OpenForWrite(kTestFilename, fs, &err));
     ASSERT_EQ("", err);
 
     vector<Node*> deps;
@@ -414,7 +434,8 @@
     State state;
     DepsLog log;
     string err;
-    EXPECT_TRUE(log.OpenForWrite(kTestFilename, &err));
+    VirtualFileSystem fs;
+    EXPECT_TRUE(log.OpenForWrite(kTestFilename, fs, &err));
     ASSERT_EQ("", err);
 
     vector<Node*> deps;
@@ -450,7 +471,8 @@
     // The truncated entry should've been discarded.
     EXPECT_EQ(NULL, log.GetDeps(state.GetNode("out2.o", 0)));
 
-    EXPECT_TRUE(log.OpenForWrite(kTestFilename, &err));
+    VirtualFileSystem fs;
+    EXPECT_TRUE(log.OpenForWrite(kTestFilename, fs, &err));
     ASSERT_EQ("", err);
 
     // Add a new entry.
@@ -476,4 +498,203 @@
   }
 }
 
+template <typename Func>
+static void DoLoadInvalidLogTest(Func&& func) {
+  State state;
+  DepsLog log;
+  std::string err;
+  ASSERT_TRUE(log.Load(kTestFilename, &state, &err));
+  ASSERT_EQ("premature end of file; recovering", err);
+  func(&state, &log);
+}
+
+TEST_F(DepsLogTest, LoadInvalidLog) {
+  struct Item {
+    Item(int num) : is_num(true), num(num) {}
+    Item(const char* str) : is_num(false), str(str) {}
+
+    bool is_num;
+    uint32_t num;
+    const char* str;
+  };
+
+  auto write_file = [](std::vector<Item> items) {
+    FILE* fp = fopen(kTestFilename, "wb");
+    for (const Item& item : items) {
+      if (item.is_num) {
+        ASSERT_EQ(1, fwrite(&item.num, sizeof(item.num), 1, fp));
+      } else {
+        ASSERT_EQ(strlen(item.str), fwrite(item.str, 1, strlen(item.str), fp));
+      }
+    }
+    fclose(fp);
+  };
+
+  const int kCurrentVersion = 4;
+  auto path_hdr = [](int path_len) -> int {
+    return RoundUp(path_len, 4) + 4;
+  };
+  auto deps_hdr = [](int deps_cnt) -> int {
+    return 0x80000000 | ((3 * sizeof(uint32_t)) + (deps_cnt * 4));
+  };
+
+  write_file({
+    "# ninjadeps\n", kCurrentVersion,
+    path_hdr(4), "foo0", ~0, // node #0
+    path_hdr(4), "foo1", ~2, // invalid path ID
+  });
+  DoLoadInvalidLogTest([](State* state, DepsLog* log) {
+    ASSERT_EQ(0, state->LookupNode("foo0")->id());
+    ASSERT_EQ(nullptr, state->LookupNode("foo1"));
+  });
+
+  write_file({
+    "# ninjadeps\n", kCurrentVersion,
+    path_hdr(4), "foo0", ~0, // node #0
+    deps_hdr(1), /*node*/0, /*mtime*/5, 0, /*node*/1, // invalid src ID
+    path_hdr(4), "foo1", ~1, // node #1
+  });
+  DoLoadInvalidLogTest([](State* state, DepsLog* log) {
+    ASSERT_EQ(0, state->LookupNode("foo0")->id());
+    ASSERT_EQ(nullptr, log->GetDeps(state->LookupNode("foo0")));
+    ASSERT_EQ(nullptr, state->LookupNode("foo1"));
+  });
+
+  write_file({
+    "# ninjadeps\n", kCurrentVersion,
+    path_hdr(4), "foo0", ~0, // node #0
+    deps_hdr(1), /*node*/1, /*mtime*/5, 0, /*node*/0, // invalid out ID
+    path_hdr(4), "foo1", ~1, // node #1
+  });
+  DoLoadInvalidLogTest([](State* state, DepsLog* log) {
+    ASSERT_EQ(0, state->LookupNode("foo0")->id());
+    ASSERT_EQ(nullptr, state->LookupNode("foo1"));
+  });
+
+  write_file({
+    "# ninjadeps\n", kCurrentVersion,
+    path_hdr(4), "foo0", ~0, // node #0
+    path_hdr(4), "foo1", ~1, // node #1
+    path_hdr(4), "foo2", ~2, // node #2
+    deps_hdr(1), /*node*/2, /*mtime*/5, 0, /*node*/1,
+    deps_hdr(1), /*node*/2, /*mtime*/6, 0, /*node*/3, // invalid src ID
+
+    // No records after the invalid record are parsed.
+    path_hdr(4), "foo3", ~3, // node #3
+    deps_hdr(1), /*node*/3, /*mtime*/7, 0, /*node*/0,
+    path_hdr(4), "foo4", ~4, // node #4
+    deps_hdr(1), /*node*/4, /*mtime*/8, 0, /*node*/0,
+
+    // Truncation must be handled before looking for the last deps record
+    // that outputs a given node.
+    deps_hdr(1), /*node*/2, /*mtime*/9, 0, /*node*/0,
+    deps_hdr(1), /*node*/2, /*mtime*/9, 0, /*node*/3,
+  });
+  DoLoadInvalidLogTest([](State* state, DepsLog* log) {
+    ASSERT_EQ(0, state->LookupNode("foo0")->id());
+    ASSERT_EQ(1, state->LookupNode("foo1")->id());
+    ASSERT_EQ(2, state->LookupNode("foo2")->id());
+    ASSERT_EQ(nullptr, state->LookupNode("foo3"));
+    ASSERT_EQ(nullptr, state->LookupNode("foo4"));
+
+    ASSERT_EQ(nullptr, log->GetDeps(state->LookupNode("foo1")));
+
+    DepsLog::Deps* deps = log->GetDeps(state->LookupNode("foo2"));
+    ASSERT_EQ(5, deps->mtime);
+    ASSERT_EQ(1, deps->node_count);
+    ASSERT_EQ(1, deps->nodes[0]->id());
+  });
+}
+
+TEST_F(DepsLogTest, MustBeDepsRecordHeader) {
+  // Mark a word as a candidate.
+  static constexpr uint64_t kCandidate = 0x100000000;
+
+  // Verifies that MustBeDepsRecordHeader returns the expected value. Returns
+  // true on success.
+  auto do_test = [](std::vector<uint64_t> words) -> bool {
+    std::vector<uint32_t> data;
+    for (uint64_t word : words) {
+      // Coerce from uint64_t to uint32_t to mask off the kCandidate flag.
+      data.push_back(word);
+    }
+    DepsLogData log;
+    log.words = data.data();
+    log.size = data.size();
+    for (size_t i = 0; i < words.size(); ++i) {
+      const bool expected = (words[i] & kCandidate) == kCandidate;
+      if (expected != MustBeDepsRecordHeader(log, i)) {
+        printf("\n%s,%d: bad index: %zu\n", __FILE__, __LINE__, i);
+        return false;
+      }
+    }
+    return true;
+  };
+
+  // Two valid deps records with no dependencies. Each record's header is
+  // recognized as the start of a deps record. The first record has an mtime_hi
+  // from 2262.
+  EXPECT_TRUE(do_test({
+    // header               output_id   mtime_lo    mtime_hi
+    0x8000000c|kCandidate,  1,          2,          0x80000100,
+    0x8000000c|kCandidate,  3,          4,          5,
+  }));
+
+  // The first record's mtime_lo is within a 524us window. The second record's
+  // header looks like a potential mtime_lo for 0x8007fffc.
+  EXPECT_TRUE(do_test({
+    // header               output_id   mtime_lo    mtime_hi
+    0x8000000c|kCandidate,  1,          0x8007fffc, 2,
+    0x8000000c,             3,          4,          5,
+  }));
+
+  // 0x80080000 is above the maximum record size, so it is rejected as a
+  // possible header.
+  EXPECT_TRUE(do_test({
+    // header               output_id   mtime_lo    mtime_hi
+    0x8000000c|kCandidate,  1,          0x80080000, 2,
+    0x8000000c|kCandidate,  3,          4,          5,
+  }));
+
+  // Two deps records with >16K inputs each. The header could be confused with a
+  // path string containing control characters, so it's not a candidate.
+  EXPECT_TRUE(do_test({
+    // header               output_id   mtime_lo    mtime_hi
+    0x80010101,             1,          2,          3,    // input IDs elided...
+    0x80010101,             4,          5,          6,    // input IDs elided...
+  }));
+
+  // The first record has a single dependency and an mtime_hi from 2262. The
+  // second deps record's header looks like a potential mtime_lo for 0x80000100.
+  EXPECT_TRUE(do_test({
+    // header               output_id   mtime_lo    mtime_hi
+    0x80000010|kCandidate,  1,          2,          0x80000100, 3,
+    0x8000000c,             4,          5,          6,
+  }));
+
+  // The first deps record's mtime_lo is within a 524us window, and the second
+  // record's header looks like a potential mtime_hi for 0x80000100.
+  EXPECT_TRUE(do_test({
+    // header               output_id   mtime_lo    mtime_hi
+    0x80000010|kCandidate,  1,          0x80000100, 2,          3,
+    0x8000000c,             4,          5,          6,
+  }));
+
+  // The first record has two dependencies, so its mtime_lo doesn't disqualify
+  // the next record's header.
+  EXPECT_TRUE(do_test({
+    // header               output_id   mtime_lo    mtime_hi
+    0x80000014|kCandidate,  1,          0x80000100, 2,          3,          4,
+    0x8000000c|kCandidate,  5,          6,          7,
+  }));
+
+  // The first deps record's mtime_hi is from 2262, and the second record's
+  // header looks like a potential mtime_hi for 0x80000100.
+  EXPECT_TRUE(do_test({
+    // header               output_id   mtime_lo    mtime_hi
+    0x80000014|kCandidate,  1,          2,          0x80000100, 3,          4,
+    0x8000000c,             5,          6,          7,
+  }));
+}
+
 }  // anonymous namespace
diff --git a/src/disk_interface.cc b/src/disk_interface.cc
index d4c2fb0..0044c5b 100644
--- a/src/disk_interface.cc
+++ b/src/disk_interface.cc
@@ -153,6 +153,34 @@
 
 // RealDiskInterface -----------------------------------------------------------
 
+#ifndef _WIN32
+static TimeStamp StatTimestamp(const struct stat& st) {
+  // Some users (Flatpak) set mtime to 0, this should be harmless
+  // and avoids conflicting with our return value of 0 meaning
+  // that it doesn't exist.
+  if (st.st_mtime == 0)
+    return 1;
+#if defined(__APPLE__) && !defined(_POSIX_C_SOURCE)
+  return ((int64_t)st.st_mtimespec.tv_sec * 1000000000LL +
+          st.st_mtimespec.tv_nsec);
+#elif (_POSIX_C_SOURCE >= 200809L || _XOPEN_SOURCE >= 700 || defined(_BSD_SOURCE) || defined(_SVID_SOURCE) || \
+       defined(__BIONIC__) || (defined (__SVR4) && defined (__sun)) || defined(__FreeBSD__))
+  // For glibc, see "Timestamp files" in the Notes of http://www.kernel.org/doc/man-pages/online/pages/man2/stat.2.html
+  // newlib, uClibc and musl follow the kernel (or Cygwin) headers and define the right macro values above.
+  // For bsd, see https://github.com/freebsd/freebsd/blob/master/sys/sys/stat.h and similar
+  // For bionic, C and POSIX API is always enabled.
+  // For solaris, see https://docs.oracle.com/cd/E88353_01/html/E37841/stat-2.html.
+  return (int64_t)st.st_mtim.tv_sec * 1000000000LL + st.st_mtim.tv_nsec;
+#elif defined(_AIX)
+  return (int64_t)st.st_mtime * 1000000000LL + st.st_mtime_n;
+#else
+  return (int64_t)st.st_mtime * 1000000000LL + st.st_mtimensec;
+#endif
+}
+#endif
+
+/// This function is thread-safe on Unix but not on Windows. See
+/// RealDiskInterface::IsStatThreadSafe.
 TimeStamp RealDiskInterface::Stat(const string& path, string* err) const {
   METRIC_RECORD("node stat");
 #ifdef _WIN32
@@ -197,27 +225,34 @@
     *err = "stat(" + path + "): " + strerror(errno);
     return -1;
   }
-  // Some users (Flatpak) set mtime to 0, this should be harmless
-  // and avoids conflicting with our return value of 0 meaning
-  // that it doesn't exist.
-  if (st.st_mtime == 0)
-    return 1;
-#if defined(__APPLE__) && !defined(_POSIX_C_SOURCE)
-  return ((int64_t)st.st_mtimespec.tv_sec * 1000000000LL +
-          st.st_mtimespec.tv_nsec);
-#elif (_POSIX_C_SOURCE >= 200809L || _XOPEN_SOURCE >= 700 || defined(_BSD_SOURCE) || defined(_SVID_SOURCE) || \
-       defined(__BIONIC__) || (defined (__SVR4) && defined (__sun)) || defined(__FreeBSD__))
-  // For glibc, see "Timestamp files" in the Notes of http://www.kernel.org/doc/man-pages/online/pages/man2/stat.2.html
-  // newlib, uClibc and musl follow the kernel (or Cygwin) headers and define the right macro values above.
-  // For bsd, see https://github.com/freebsd/freebsd/blob/master/sys/sys/stat.h and similar
-  // For bionic, C and POSIX API is always enabled.
-  // For solaris, see https://docs.oracle.com/cd/E88353_01/html/E37841/stat-2.html.
-  return (int64_t)st.st_mtim.tv_sec * 1000000000LL + st.st_mtim.tv_nsec;
-#elif defined(_AIX)
-  return (int64_t)st.st_mtime * 1000000000LL + st.st_mtime_n;
-#else
-  return (int64_t)st.st_mtime * 1000000000LL + st.st_mtimensec;
+  return StatTimestamp(st);
 #endif
+}
+
+TimeStamp RealDiskInterface::LStat(const string& path, bool* is_dir, string* err) const {
+  METRIC_RECORD("node lstat");
+#ifdef _WIN32
+#error unimplemented
+#else
+  struct stat st;
+  if (lstat(path.c_str(), &st) < 0) {
+    if (errno == ENOENT || errno == ENOTDIR)
+      return 0;
+    *err = "lstat(" + path + "): " + strerror(errno);
+    return -1;
+  }
+  if (is_dir != nullptr) {
+    *is_dir = S_ISDIR(st.st_mode);
+  }
+  return StatTimestamp(st);
+#endif
+}
+
+bool RealDiskInterface::IsStatThreadSafe() const {
+#ifdef _WIN32
+  return false;
+#else
+  return true;
 #endif
 }
 
@@ -266,6 +301,35 @@
   }
 }
 
+FileReader::Status RealDiskInterface::LoadFile(const std::string& path,
+                                               std::unique_ptr<LoadedFile>* result,
+                                               std::string* err) {
+#ifdef _WIN32
+#error "LoadFile is not implemented yet on Windows"
+#else
+  struct MappedFile : LoadedFile {
+    MappedFile(const std::string& filename, std::unique_ptr<Mapping> mapping)
+        : mapping_(std::move(mapping)) {
+      filename_ = filename;
+      content_with_nul_ = { mapping_->base(), mapping_->file_size() + 1 };
+    }
+    std::unique_ptr<Mapping> mapping_;
+  };
+
+  std::unique_ptr<Mapping> mapping;
+  switch (::MapFile(path, &mapping, err)) {
+  case 0:
+    *result = std::unique_ptr<MappedFile>(new MappedFile(path,
+                                                         std::move(mapping)));
+    return Okay;
+  case -ENOENT:
+    return NotFound;
+  default:
+    return OtherError;
+  }
+#endif
+}
+
 int RealDiskInterface::RemoveFile(const string& path) {
   if (remove(path.c_str()) < 0) {
     switch (errno) {
diff --git a/src/disk_interface.h b/src/disk_interface.h
index 145e089..f8b1b0d 100644
--- a/src/disk_interface.h
+++ b/src/disk_interface.h
@@ -16,11 +16,64 @@
 #define NINJA_DISK_INTERFACE_H_
 
 #include <map>
+#include <memory>
 #include <string>
 using namespace std;
 
+#include "string_piece.h"
 #include "timestamp.h"
 
+/// A file whose content has been loaded into memory.
+struct LoadedFile {
+  LoadedFile() {}
+
+  LoadedFile(const LoadedFile&) = delete;
+  LoadedFile& operator=(const LoadedFile&) = delete;
+
+  virtual ~LoadedFile() {}
+
+  const std::string& filename() const { return filename_; }
+
+  /// Return the size of the file's content, excluding the extra NUL at the end.
+  size_t size() const { return content_with_nul_.size() - 1; }
+
+  /// Return the file content, excluding the extra NUL at the end.
+  StringPiece content() const {
+    return content_with_nul_.substr(0, content_with_nul_.size() - 1);
+  }
+
+  /// The last character of this StringPiece will be NUL. (i.e. The size of this
+  /// view will be 1 greater than the true file size.)
+  StringPiece content_with_nul() const { return content_with_nul_; }
+
+protected:
+  std::string filename_;
+
+  /// The start of the string view must be aligned sufficiently for any basic
+  /// type it may contain (e.g. a minimum alignment of 16 bytes is typical). The
+  /// alignment minimum is necessary for parsing the 4-byte aligned binary
+  /// .ninja_deps log.
+  StringPiece content_with_nul_;
+};
+
+/// An implementation of LoadedFile where the file's content is copied into a
+/// char[] array. This class adds a NUL to the given string.
+struct HeapLoadedFile : LoadedFile {
+  HeapLoadedFile(const std::string& filename, StringPiece content) {
+    filename_ = filename;
+    // An array allocated with new char[N] is guaranteed to be sufficiently
+    // aligned for any fundamental type.
+    storage_.reset(new char[content.size() + 1]);
+    memcpy(storage_.get(), content.data(), content.size());
+    storage_[content.size()] = '\0';
+    content_with_nul_ = StringPiece(storage_.get(), content.size() + 1);
+  }
+
+private:
+  /// Use new char[] to provide the alignment guarantee this class needs.
+  std::unique_ptr<char[]> storage_;
+};
+
 /// Interface for reading files from disk.  See DiskInterface for details.
 /// This base offers the minimum interface needed just to read files.
 struct FileReader {
@@ -37,6 +90,12 @@
   /// On error, return another Status and fill |err|.
   virtual Status ReadFile(const string& path, string* contents,
                           string* err) = 0;
+
+  /// Open the file for reading and return an abstract LoadedFile. On success,
+  /// return Okay. On error, return another Status and fill |err|.
+  virtual Status LoadFile(const std::string& path,
+                          std::unique_ptr<LoadedFile>* result,
+                          std::string* err) = 0;
 };
 
 /// Interface for accessing the disk.
@@ -45,9 +104,18 @@
 /// is RealDiskInterface.
 struct DiskInterface: public FileReader {
   /// stat() a file, returning the mtime, or 0 if missing and -1 on
-  /// other errors.
+  /// other errors. Thread-safe iff IsStatThreadSafe returns true.
   virtual TimeStamp Stat(const string& path, string* err) const = 0;
 
+  /// lstat() a path, returning the mtime, or 0 if missing and 01 on
+  /// other errors. Does not traverse symlinks, and returns whether the
+  /// path represents a directory. Thread-safe iff IsStatThreadSafe
+  /// returns true.
+  virtual TimeStamp LStat(const string& path, bool* is_dir, string* err) const = 0;
+
+  /// True if Stat() can be called from multiple threads concurrently.
+  virtual bool IsStatThreadSafe() const = 0;
+
   /// Create a directory, returning false on failure.
   virtual bool MakeDir(const string& path) = 0;
 
@@ -76,9 +144,14 @@
                       {}
   virtual ~RealDiskInterface() {}
   virtual TimeStamp Stat(const string& path, string* err) const;
+  virtual TimeStamp LStat(const string& path, bool* is_dir, string* err) const;
+  virtual bool IsStatThreadSafe() const;
   virtual bool MakeDir(const string& path);
   virtual bool WriteFile(const string& path, const string& contents);
   virtual Status ReadFile(const string& path, string* contents, string* err);
+  virtual Status LoadFile(const std::string& path,
+                          std::unique_ptr<LoadedFile>* result,
+                          std::string* err);
   virtual int RemoveFile(const string& path);
 
   /// Whether stat information can be cached.  Only has an effect on Windows.
diff --git a/src/disk_interface_test.cc b/src/disk_interface_test.cc
index bac515d..090adc4 100644
--- a/src/disk_interface_test.cc
+++ b/src/disk_interface_test.cc
@@ -213,10 +213,12 @@
 
 struct StatTest : public StateTestWithBuiltinRules,
                   public DiskInterface {
-  StatTest() : scan_(&state_, NULL, NULL, this, NULL) {}
+  StatTest() : scan_(&state_, NULL, NULL, this, NULL, false) {}
 
   // DiskInterface implementation.
   virtual TimeStamp Stat(const string& path, string* err) const;
+  virtual TimeStamp LStat(const string& path, bool* is_dir, string* err) const;
+  virtual bool IsStatThreadSafe() const;
   virtual bool WriteFile(const string& path, const string& contents) {
     assert(false);
     return true;
@@ -229,6 +231,12 @@
     assert(false);
     return NotFound;
   }
+  virtual Status LoadFile(const std::string& path,
+                          std::unique_ptr<LoadedFile>* result,
+                          std::string* err) {
+    assert(false);
+    return NotFound;
+  }
   virtual int RemoveFile(const string& path) {
     assert(false);
     return 0;
@@ -240,13 +248,23 @@
 };
 
 TimeStamp StatTest::Stat(const string& path, string* err) const {
+  return LStat(path, nullptr, err);
+}
+
+TimeStamp StatTest::LStat(const string& path, bool* is_dir, string* err) const {
   stats_.push_back(path);
   map<string, TimeStamp>::const_iterator i = mtimes_.find(path);
   if (i == mtimes_.end())
     return 0;  // File not found.
+  if (is_dir != nullptr)
+    *is_dir = false;
   return i->second;
 }
 
+bool StatTest::IsStatThreadSafe() const {
+  return false;
+}
+
 TEST_F(StatTest, Simple) {
   ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
 "build out: cat in\n"));
@@ -256,7 +274,7 @@
   EXPECT_TRUE(out->Stat(this, &err));
   EXPECT_EQ("", err);
   ASSERT_EQ(1u, stats_.size());
-  scan_.RecomputeDirty(out, NULL);
+  scan_.RecomputeDirty(out, NULL, NULL);
   ASSERT_EQ(2u, stats_.size());
   ASSERT_EQ("out", stats_[0]);
   ASSERT_EQ("in",  stats_[1]);
@@ -272,7 +290,7 @@
   EXPECT_TRUE(out->Stat(this, &err));
   EXPECT_EQ("", err);
   ASSERT_EQ(1u, stats_.size());
-  scan_.RecomputeDirty(out, NULL);
+  scan_.RecomputeDirty(out, NULL, NULL);
   ASSERT_EQ(3u, stats_.size());
   ASSERT_EQ("out", stats_[0]);
   ASSERT_TRUE(GetNode("out")->dirty());
@@ -292,7 +310,7 @@
   EXPECT_TRUE(out->Stat(this, &err));
   EXPECT_EQ("", err);
   ASSERT_EQ(1u, stats_.size());
-  scan_.RecomputeDirty(out, NULL);
+  scan_.RecomputeDirty(out, NULL, NULL);
   ASSERT_EQ(1u + 6u, stats_.size());
   ASSERT_EQ("mid1", stats_[1]);
   ASSERT_TRUE(GetNode("mid1")->dirty());
@@ -313,7 +331,7 @@
   EXPECT_TRUE(out->Stat(this, &err));
   EXPECT_EQ("", err);
   ASSERT_EQ(1u, stats_.size());
-  scan_.RecomputeDirty(out, NULL);
+  scan_.RecomputeDirty(out, NULL, NULL);
   ASSERT_FALSE(GetNode("in")->dirty());
   ASSERT_TRUE(GetNode("mid")->dirty());
   ASSERT_TRUE(GetNode("out")->dirty());
diff --git a/src/eval_env.cc b/src/eval_env.cc
index 8817a87..3d9f1c8 100644
--- a/src/eval_env.cc
+++ b/src/eval_env.cc
@@ -12,57 +12,24 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <assert.h>
-
 #include "eval_env.h"
 
-string BindingEnv::LookupVariable(const string& var) {
-  map<string, string>::iterator i = bindings_.find(var);
-  if (i != bindings_.end())
-    return i->second;
-  if (parent_)
-    return parent_->LookupVariable(var);
-  return "";
+#include <assert.h>
+
+#include "graph.h"
+#include "state.h"
+
+Rule::Rule(const HashedStrView& name) : name_(name) {
+  pos_.base = new BasePosition {{ &State::kBuiltinScope, 0 }}; // leaked
 }
 
-void BindingEnv::AddBinding(const string& key, const string& val) {
-  bindings_[key] = val;
-}
+bool Rule::IsReservedBinding(StringPiece var) {
+  // Cycle detection for rule variable evaluation uses a fixed recursion depth
+  // that's guaranteed to be larger than the number of reserved binding names
+  // listed below.
+  static_assert(EdgeEval::kEvalRecursionLimit == 16,
+                "Unexpected rule variable evaluation recursion limit");
 
-void BindingEnv::AddRule(const Rule* rule) {
-  assert(LookupRuleCurrentScope(rule->name()) == NULL);
-  rules_[rule->name()] = rule;
-}
-
-const Rule* BindingEnv::LookupRuleCurrentScope(const string& rule_name) {
-  map<string, const Rule*>::iterator i = rules_.find(rule_name);
-  if (i == rules_.end())
-    return NULL;
-  return i->second;
-}
-
-const Rule* BindingEnv::LookupRule(const string& rule_name) {
-  map<string, const Rule*>::iterator i = rules_.find(rule_name);
-  if (i != rules_.end())
-    return i->second;
-  if (parent_)
-    return parent_->LookupRule(rule_name);
-  return NULL;
-}
-
-void Rule::AddBinding(const string& key, const EvalString& val) {
-  bindings_[key] = val;
-}
-
-const EvalString* Rule::GetBinding(const string& key) const {
-  Bindings::const_iterator i = bindings_.find(key);
-  if (i == bindings_.end())
-    return NULL;
-  return &i->second;
-}
-
-// static
-bool Rule::IsReservedBinding(const string& var) {
   return var == "command" ||
       var == "depfile" ||
       var == "description" ||
@@ -72,61 +39,133 @@
       var == "restat" ||
       var == "rspfile" ||
       var == "rspfile_content" ||
+      var == "phony_output" ||
       var == "msvc_deps_prefix";
 }
 
-const map<string, const Rule*>& BindingEnv::GetRules() const {
-  return rules_;
+void Binding::Evaluate(std::string* out_append) {
+  if (is_evaluated_.load()) {
+    out_append->append(final_value_);
+    return;
+  }
+
+  std::string str;
+  EvaluateBindingInScope(&str, parsed_value_, pos_.scope_pos());
+  out_append->append(str);
+
+  // Try to store the result so we can use it again later. If we can't acquire
+  // the lock, then another thread has already acquired it and will set the
+  // final binding value.
+  std::unique_lock<std::mutex> lock(mutex_, std::try_to_lock);
+  if (!lock.owns_lock())
+    return;
+
+  // Check the flag again. Another thread could have set it before this thread
+  // acquired the lock, and if it had, then other threads could already be using
+  // this binding's saved value.
+  if (is_evaluated_.load())
+    return;
+
+  final_value_ = std::move(str);
+  is_evaluated_.store(true);
 }
 
-string BindingEnv::LookupWithFallback(const string& var,
-                                      const EvalString* eval,
-                                      Env* env) {
-  map<string, string>::iterator i = bindings_.find(var);
-  if (i != bindings_.end())
-    return i->second;
+template <typename K, typename V>
+static void ExpandTable(std::unordered_map<K, V>& table, size_t extra_size) {
+  size_t needed_buckets = table.size() + extra_size + 1;
+  if (table.bucket_count() >= needed_buckets)
+    return;
+  table.rehash(needed_buckets * 2);
+};
 
-  if (eval)
-    return eval->Evaluate(env);
-
-  if (parent_)
-    return parent_->LookupVariable(var);
-
-  return "";
+void Scope::ReserveTableSpace(size_t new_bindings, size_t new_rules) {
+  ExpandTable(bindings_, new_bindings);
+  ExpandTable(rules_, new_rules);
 }
 
-string EvalString::Evaluate(Env* env) const {
-  string result;
-  for (TokenList::const_iterator i = parsed_.begin(); i != parsed_.end(); ++i) {
-    if (i->second == RAW)
-      result.append(i->first);
-    else
-      result.append(env->LookupVariable(i->first));
+std::map<std::string, const Rule*> Scope::GetRules() const {
+  std::map<std::string, const Rule*> result;
+  for (std::pair<HashedStrView, Rule*> pair : rules_) {
+    Rule* rule = pair.second;
+    result[rule->name()] = rule;
   }
   return result;
 }
 
-void EvalString::AddText(StringPiece text) {
-  // Add it to the end of an existing RAW token if possible.
-  if (!parsed_.empty() && parsed_.back().second == RAW) {
-    parsed_.back().first.append(text.str_, text.len_);
-  } else {
-    parsed_.push_back(make_pair(text.AsString(), RAW));
-  }
-}
-void EvalString::AddSpecial(StringPiece text) {
-  parsed_.push_back(make_pair(text.AsString(), SPECIAL));
+Binding* Scope::LookupBindingAtPos(const HashedStrView& var, ScopePosition pos) {
+  Scope* scope = pos.scope;
+  if (scope == nullptr) return nullptr;
+
+  auto it = scope->bindings_.find(var);
+  if (it == scope->bindings_.end())
+    return LookupBindingAtPos(var, scope->parent_);
+
+  struct BindingCmp {
+    bool operator()(Binding* x, DeclIndex y) const {
+      return x->pos_.scope_index() < y;
+    }
+  };
+
+  // A binding "Foo = $Foo" is valid; the "$Foo" is not a self-reference but
+  // a reference to the previous binding of Foo. The evaluation of "$Foo"
+  // happens with the same DeclIndex as the "Foo = $Foo" binding, so we want
+  // to find a binding whose ScopePosition is strictly less than the one we're
+  // searching for, not equal to it.
+  std::vector<Binding*>& decls = it->second;
+  auto it2 = std::lower_bound(
+    decls.begin(), decls.end(),
+    pos.index, BindingCmp());
+  if (it2 == decls.begin())
+    return LookupBindingAtPos(var, scope->parent_);
+  return *(it2 - 1);
 }
 
-string EvalString::Serialize() const {
-  string result;
-  for (TokenList::const_iterator i = parsed_.begin();
-       i != parsed_.end(); ++i) {
-    result.append("[");
-    if (i->second == SPECIAL)
-      result.append("$");
-    result.append(i->first);
-    result.append("]");
+Binding* Scope::LookupBinding(const HashedStrView& var, Scope* scope) {
+  if (scope == nullptr) return nullptr;
+
+  auto it = scope->bindings_.find(var);
+  if (it == scope->bindings_.end()) {
+    // When we delegate to the parent scope, we match bindings at the end of the
+    // parent's scope, even if they weren't in scope when the subninja scope was
+    // parsed. The behavior matters after parsing is complete and we're
+    // evaluating edge bindings that involve rule variable expansions.
+    return LookupBinding(var, scope->parent_.scope);
   }
+
+  std::vector<Binding*>& decls = it->second;
+  assert(!decls.empty());
+
+  // Evaluate the binding.
+  return decls.back();
+}
+
+void Scope::EvaluateVariableAtPos(std::string* out_append,
+                                  const HashedStrView& var, ScopePosition pos) {
+  if (Binding* binding = LookupBindingAtPos(var, pos))
+    binding->Evaluate(out_append);
+}
+
+void Scope::EvaluateVariable(std::string* out_append, const HashedStrView& var,
+                             Scope* scope) {
+  if (Binding* binding = LookupBinding(var, scope))
+    binding->Evaluate(out_append);
+}
+
+std::string Scope::LookupVariable(const HashedStrView& var) {
+  std::string result;
+  EvaluateVariable(&result, var, this);
   return result;
 }
+
+Rule* Scope::LookupRuleAtPos(const HashedStrView& rule_name,
+                             ScopePosition pos) {
+  Scope* scope = pos.scope;
+  if (scope == nullptr) return nullptr;
+  auto it = scope->rules_.find(rule_name);
+  if (it != scope->rules_.end()) {
+    Rule* rule = it->second;
+    if (rule->pos_.scope_index() < pos.index)
+      return rule;
+  }
+  return LookupRuleAtPos(rule_name, scope->parent_);
+}
diff --git a/src/eval_env.h b/src/eval_env.h
index 999ce42..1930226 100644
--- a/src/eval_env.h
+++ b/src/eval_env.h
@@ -15,91 +15,197 @@
 #ifndef NINJA_EVAL_ENV_H_
 #define NINJA_EVAL_ENV_H_
 
+#include <assert.h>
+#include <stdint.h>
+
 #include <map>
+#include <mutex>
 #include <string>
+#include <unordered_map>
 #include <vector>
 using namespace std;
 
+#include "hashed_str_view.h"
+#include "lexer.h"
 #include "string_piece.h"
 
-struct Rule;
+using DeclIndex = uint32_t;
+const DeclIndex kLastDeclIndex = static_cast<DeclIndex>(-1);
 
-/// An interface for a scope for variable (e.g. "$foo") lookups.
-struct Env {
-  virtual ~Env() {}
-  virtual string LookupVariable(const string& var) = 0;
+struct Scope;
+
+/// Position of a declaration within a scope.
+struct ScopePosition {
+  ScopePosition(Scope* scope=nullptr, DeclIndex index=0)
+      : scope(scope), index(index) {}
+
+  Scope* scope = nullptr;
+  DeclIndex index = 0;
 };
 
-/// A tokenized string that contains variable references.
-/// Can be evaluated relative to an Env.
-struct EvalString {
-  string Evaluate(Env* env) const;
+/// Position of a parsed manifest "clump" within its containing scope and within
+/// all manifest files, in DFS order. This field is updated after parallelized
+/// parsing is complete.
+struct BasePosition {
+  BasePosition(ScopePosition scope={}, DeclIndex dfs_location=0)
+      : scope(scope), dfs_location(dfs_location) {}
 
-  void Clear() { parsed_.clear(); }
-  bool empty() const { return parsed_.empty(); }
+  /// The scope location corresponding to this chunk. (Updated during DFS
+  /// manifest parsing.)
+  ScopePosition scope = {};
 
-  void AddText(StringPiece text);
-  void AddSpecial(StringPiece text);
+  /// Location of the chunk's declarations within DFS order of all the ninja
+  /// files. This field is useful for verifying that pool references and default
+  /// target references appear *after* their referents have been declared.
+  DeclIndex dfs_location = 0;
 
-  /// Construct a human-readable representation of the parsed state,
-  /// for use in tests.
-  string Serialize() const;
+  /// Check that the position is valid. (i.e. The clump it belongs to has been
+  /// assigned a scope and a DFS position.)
+  bool initialized() const {
+    return scope.scope != nullptr;
+  }
+};
+
+/// Offset of a manifest declaration relative to the beginning of a "clump" of
+/// declarations.
+struct RelativePosition {
+  RelativePosition(BasePosition* base=nullptr, DeclIndex offset=0)
+      : base(base), offset(offset) {}
+
+  BasePosition* base = nullptr;
+  DeclIndex offset = 0;
+
+  DeclIndex dfs_location() const { Validate(); return base->dfs_location + offset; }
+  DeclIndex scope_index() const { Validate(); return base->scope.index + offset; }
+  Scope* scope() const { Validate(); return base->scope.scope; }
+  ScopePosition scope_pos() const { Validate(); return { base->scope.scope, scope_index() }; }
 
 private:
-  enum TokenType { RAW, SPECIAL };
-  typedef vector<pair<string, TokenType> > TokenList;
-  TokenList parsed_;
+  void Validate() const {
+    assert(base->initialized() && "clump position hasn't been set yet");
+  }
 };
 
 /// An invokable build command and associated metadata (description, etc.).
 struct Rule {
-  explicit Rule(const string& name) : name_(name) {}
+  Rule() {}
 
-  const string& name() const { return name_; }
+  /// This constructor is used to construct built-in rules.
+  explicit Rule(const HashedStrView& name);
 
-  void AddBinding(const string& key, const EvalString& val);
+  const std::string& name() const { return name_.str(); }
+  const HashedStr& name_hashed() const { return name_; }
 
-  static bool IsReservedBinding(const string& var);
+  static bool IsReservedBinding(StringPiece var);
 
-  const EvalString* GetBinding(const string& key) const;
+  const std::string* GetBinding(const HashedStrView& name) const {
+    for (auto it = bindings_.rbegin(); it != bindings_.rend(); ++it) {
+      if (it->first == name)
+        return &it->second;
+    }
+    return nullptr;
+  }
 
- private:
-  // Allow the parsers to reach into this object and fill out its fields.
-  friend struct ManifestParser;
+  /// Temporary fields used only during manifest parsing.
+  struct {
+    /// The position of the rule in its source file. Used for diagnostics.
+    size_t rule_name_diag_pos = 0;
+  } parse_state_;
 
-  string name_;
-  typedef map<string, EvalString> Bindings;
-  Bindings bindings_;
+  RelativePosition pos_;
+  HashedStr name_;
+  std::vector<std::pair<HashedStr, std::string>> bindings_;
 };
 
-/// An Env which contains a mapping of variables to values
-/// as well as a pointer to a parent scope.
-struct BindingEnv : public Env {
-  BindingEnv() : parent_(NULL) {}
-  explicit BindingEnv(BindingEnv* parent) : parent_(parent) {}
+struct Binding {
+  /// This function is thread-safe. It stores a copy of the evaluated binding in
+  /// the Binding object if it hasn't been evaluated yet.
+  void Evaluate(std::string* out_append);
 
-  virtual ~BindingEnv() {}
-  virtual string LookupVariable(const string& var);
+  const std::string& name() { return name_.str(); }
+  const HashedStr& name_hashed() { return name_; }
 
-  void AddRule(const Rule* rule);
-  const Rule* LookupRule(const string& rule_name);
-  const Rule* LookupRuleCurrentScope(const string& rule_name);
-  const map<string, const Rule*>& GetRules() const;
+  RelativePosition pos_;
+  HashedStr name_;
 
-  void AddBinding(const string& key, const string& val);
-
-  /// This is tricky.  Edges want lookup scope to go in this order:
-  /// 1) value set on edge itself (edge_->env_)
-  /// 2) value set on rule, with expansion in the edge's scope
-  /// 3) value set on enclosing scope of edge (edge_->env_->parent_)
-  /// This function takes as parameters the necessary info to do (2).
-  string LookupWithFallback(const string& var, const EvalString* eval,
-                            Env* env);
+  /// The manifest parser initializes this field with the location of the
+  /// binding's unevaluated memory in the loaded manifest file.
+  StringPiece parsed_value_;
 
 private:
-  map<string, string> bindings_;
-  map<string, const Rule*> rules_;
-  BindingEnv* parent_;
+  /// This binding's value is evaluated lazily, but it must be evaluated before
+  /// the manifest files are unloaded.
+  std::atomic<bool> is_evaluated_;
+  std::mutex mutex_;
+  std::string final_value_;
+};
+
+struct Scope {
+  Scope(ScopePosition parent) : parent_(parent) {}
+
+  /// Preallocate space in the hash tables so adding bindings and rules is more
+  /// efficient.
+  void ReserveTableSpace(size_t new_bindings, size_t new_rules);
+
+  void AddBinding(Binding* binding) {
+    bindings_[binding->name_hashed()].push_back(binding);
+  }
+
+  /// Searches for a binding using a scope position (i.e. at parse time).
+  /// Returns nullptr if the binding doesn't exist.
+  static Binding* LookupBindingAtPos(const HashedStrView& var, ScopePosition pos);
+
+  /// Searches for a binding in a scope and its ancestors. The position of a
+  /// binding within its containing scope is ignored (i.e. post-parse lookup
+  /// semantics). Returns nullptr if no binding is found. The Scope pointer may
+  /// be nullptr.
+  static Binding* LookupBinding(const HashedStrView& var, Scope* scope);
+
+  /// Append a ${var} reference using a parse-time scope position.
+  static void EvaluateVariableAtPos(std::string* out_append,
+                                    const HashedStrView& var,
+                                    ScopePosition pos);
+
+  /// Append a ${var} reference using a post-parse scope, which may be nullptr.
+  static void EvaluateVariable(std::string* out_append,
+                               const HashedStrView& var,
+                               Scope* scope);
+
+  /// Convenience method for looking up the value of a binding after manifest
+  /// parsing is finished.
+  std::string LookupVariable(const HashedStrView& var);
+
+  bool AddRule(Rule* rule) {
+    return rules_.insert({ rule->name_hashed(), rule }).second;
+  }
+
+  static Rule* LookupRuleAtPos(const HashedStrView& rule_name,
+                               ScopePosition pos);
+
+  /// Tests use this function to verify that a scope's rules are correct.
+  std::map<std::string, const Rule*> GetRules() const;
+
+  ScopePosition AllocDecls(DeclIndex count) {
+    ScopePosition result = GetCurrentEndOfScope();
+    pos_ += count;
+    return result;
+  }
+
+  ScopePosition GetCurrentEndOfScope() { return { this, pos_ }; }
+
+private:
+  /// The position of this scope within its parent scope.
+  /// (ScopePosition::parent will be nullptr for the root scope.)
+  ScopePosition parent_;
+
+  DeclIndex pos_ = 0;
+
+  std::unordered_map<HashedStrView, std::vector<Binding*>> bindings_;
+
+  /// A scope can only declare a rule with a particular name once. Even if a
+  /// scope has a rule of a given name, a lookup for that name can still find a
+  /// parent scope's rule if the search position comes before this scope's rule.
+  std::unordered_map<HashedStrView, Rule*> rules_;
 };
 
 #endif  // NINJA_EVAL_ENV_H_
diff --git a/src/filebuf.h b/src/filebuf.h
new file mode 100644
index 0000000..335c594
--- /dev/null
+++ b/src/filebuf.h
@@ -0,0 +1,49 @@
+// Copyright 2016 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef NINJA_FILEBUF_H_
+#define NINJA_FILEBUF_H_
+
+#include <stdio.h>
+
+#include <streambuf>
+
+// A non-buffering std::streambuf implementation that allows using
+// a FILE* as an ostream.
+class ofilebuf : public std::streambuf {
+ public:
+  ofilebuf(FILE* f) : f_(f) { }
+  ~ofilebuf() { }
+
+ private:
+  int_type overflow(int_type c) {
+    if (c != traits_type::eof()) {
+      int ret = fputc(c, f_);
+      if (ret == EOF) {
+        return traits_type::eof();
+      }
+    }
+
+    return c;
+  }
+
+  std::streamsize xsputn(const char* s, std::streamsize count) {
+    return fwrite(s, 1, count, f_);
+  }
+
+ private:
+  FILE* f_;
+};
+
+#endif // NINJA_FILEBUF_H_
diff --git a/src/frontend.pb.h b/src/frontend.pb.h
new file mode 100644
index 0000000..53d2431
--- /dev/null
+++ b/src/frontend.pb.h
@@ -0,0 +1,573 @@
+// This file is autogenerated by generate_proto_header.py, do not edit
+
+#ifndef NINJA_FRONTEND_PB_H
+#define NINJA_FRONTEND_PB_H
+
+#include <inttypes.h>
+
+#include <iostream>
+#include <string>
+#include <vector>
+
+#include "proto.h"
+
+namespace ninja {
+struct Status {
+  struct TotalEdges {
+    uint32_t total_edges_;
+    bool has_total_edges_;
+
+    TotalEdges() {
+      has_total_edges_ = false;
+      total_edges_ = static_cast< uint32_t >(0);
+    }
+
+    TotalEdges(const TotalEdges&);
+    void operator=(const TotalEdges&);
+
+    void SerializeToOstream(std::ostream* output__) const {
+      WriteVarint32(output__, 1, total_edges_);
+    }
+
+    size_t ByteSizeLong() const {
+      size_t size = 0;
+      size += VarintSize32(total_edges_) + 1;
+      return size;
+    }
+
+    void Clear() {
+      total_edges_ = static_cast< uint32_t >(0);
+    }
+
+    uint32_t* mutable_total_edges() {
+      has_total_edges_ = true;
+      return &total_edges_;
+    }
+    void set_total_edges(const uint32_t& value) {
+      has_total_edges_ = true;
+      total_edges_ = value;
+    }
+  };
+
+  struct BuildStarted {
+    uint32_t parallelism_;
+    bool has_parallelism_;
+    bool verbose_;
+    bool has_verbose_;
+
+    BuildStarted() {
+      has_parallelism_ = false;
+      parallelism_ = static_cast< uint32_t >(0);
+      has_verbose_ = false;
+      verbose_ = static_cast< bool >(0);
+    }
+
+    BuildStarted(const BuildStarted&);
+    void operator=(const BuildStarted&);
+
+    void SerializeToOstream(std::ostream* output__) const {
+      WriteVarint32(output__, 1, parallelism_);
+      WriteVarint32(output__, 2, verbose_);
+    }
+
+    size_t ByteSizeLong() const {
+      size_t size = 0;
+      size += VarintSize32(parallelism_) + 1;
+      size += VarintSizeBool(verbose_) + 1;
+      return size;
+    }
+
+    void Clear() {
+      parallelism_ = static_cast< uint32_t >(0);
+      verbose_ = static_cast< bool >(0);
+    }
+
+    uint32_t* mutable_parallelism() {
+      has_parallelism_ = true;
+      return &parallelism_;
+    }
+    void set_parallelism(const uint32_t& value) {
+      has_parallelism_ = true;
+      parallelism_ = value;
+    }
+    bool* mutable_verbose() {
+      has_verbose_ = true;
+      return &verbose_;
+    }
+    void set_verbose(const bool& value) {
+      has_verbose_ = true;
+      verbose_ = value;
+    }
+  };
+
+  struct BuildFinished {
+    BuildFinished() {
+    }
+
+    BuildFinished(const BuildFinished&);
+    void operator=(const BuildFinished&);
+
+    void SerializeToOstream(std::ostream* output__) const {
+    }
+
+    size_t ByteSizeLong() const {
+      size_t size = 0;
+      return size;
+    }
+
+    void Clear() {
+    }
+
+  };
+
+  struct EdgeStarted {
+    uint32_t id_;
+    bool has_id_;
+    uint32_t start_time_;
+    bool has_start_time_;
+    std::vector< std::string > inputs_;
+    bool has_inputs_;
+    std::vector< std::string > outputs_;
+    bool has_outputs_;
+    std::string desc_;
+    bool has_desc_;
+    std::string command_;
+    bool has_command_;
+    bool console_;
+    bool has_console_;
+
+    EdgeStarted() {
+      has_id_ = false;
+      id_ = static_cast< uint32_t >(0);
+      has_start_time_ = false;
+      start_time_ = static_cast< uint32_t >(0);
+      has_inputs_ = false;
+      has_outputs_ = false;
+      has_desc_ = false;
+      has_command_ = false;
+      has_console_ = false;
+      console_ = static_cast< bool >(0);
+    }
+
+    EdgeStarted(const EdgeStarted&);
+    void operator=(const EdgeStarted&);
+
+    void SerializeToOstream(std::ostream* output__) const {
+      WriteVarint32(output__, 1, id_);
+      WriteVarint32(output__, 2, start_time_);
+      for (std::vector< std::string >::const_iterator it_ = inputs_.begin();
+          it_ != inputs_.end(); it_++) {
+        WriteString(output__, 3, *it_);
+      }
+      for (std::vector< std::string >::const_iterator it_ = outputs_.begin();
+          it_ != outputs_.end(); it_++) {
+        WriteString(output__, 4, *it_);
+      }
+      WriteString(output__, 5, desc_);
+      WriteString(output__, 6, command_);
+      WriteVarint32(output__, 7, console_);
+    }
+
+    size_t ByteSizeLong() const {
+      size_t size = 0;
+      size += VarintSize32(id_) + 1;
+      size += VarintSize32(start_time_) + 1;
+      for (std::vector< std::string >::const_iterator it_ = inputs_.begin();
+          it_ != inputs_.end(); it_++) {
+        size += StringSize(*it_) + 1;
+      }
+      for (std::vector< std::string >::const_iterator it_ = outputs_.begin();
+          it_ != outputs_.end(); it_++) {
+        size += StringSize(*it_) + 1;
+      }
+      size += StringSize(desc_) + 1;
+      size += StringSize(command_) + 1;
+      size += VarintSizeBool(console_) + 1;
+      return size;
+    }
+
+    void Clear() {
+      id_ = static_cast< uint32_t >(0);
+      start_time_ = static_cast< uint32_t >(0);
+      inputs_.clear();
+      outputs_.clear();
+      desc_.clear();
+      command_.clear();
+      console_ = static_cast< bool >(0);
+    }
+
+    uint32_t* mutable_id() {
+      has_id_ = true;
+      return &id_;
+    }
+    void set_id(const uint32_t& value) {
+      has_id_ = true;
+      id_ = value;
+    }
+    uint32_t* mutable_start_time() {
+      has_start_time_ = true;
+      return &start_time_;
+    }
+    void set_start_time(const uint32_t& value) {
+      has_start_time_ = true;
+      start_time_ = value;
+    }
+    std::vector< std::string >* mutable_inputs() {
+      has_inputs_ = true;
+      return &inputs_;
+    }
+    void add_inputs(const std::string& value) {
+      has_inputs_ = true;
+      inputs_.push_back(value);
+    }
+    void set_inputs(const std::vector< std::string >& value) {
+      has_inputs_ = true;
+      inputs_ = value;
+    }
+    std::vector< std::string >* mutable_outputs() {
+      has_outputs_ = true;
+      return &outputs_;
+    }
+    void add_outputs(const std::string& value) {
+      has_outputs_ = true;
+      outputs_.push_back(value);
+    }
+    void set_outputs(const std::vector< std::string >& value) {
+      has_outputs_ = true;
+      outputs_ = value;
+    }
+    std::string* mutable_desc() {
+      has_desc_ = true;
+      return &desc_;
+    }
+    void set_desc(const std::string& value) {
+      has_desc_ = true;
+      desc_ = value;
+    }
+    std::string* mutable_command() {
+      has_command_ = true;
+      return &command_;
+    }
+    void set_command(const std::string& value) {
+      has_command_ = true;
+      command_ = value;
+    }
+    bool* mutable_console() {
+      has_console_ = true;
+      return &console_;
+    }
+    void set_console(const bool& value) {
+      has_console_ = true;
+      console_ = value;
+    }
+  };
+
+  struct EdgeFinished {
+    uint32_t id_;
+    bool has_id_;
+    uint32_t end_time_;
+    bool has_end_time_;
+    int32_t status_;
+    bool has_status_;
+    std::string output_;
+    bool has_output_;
+    uint32_t user_time_;
+    bool has_user_time_;
+    uint32_t system_time_;
+    bool has_system_time_;
+
+    EdgeFinished() {
+      has_id_ = false;
+      id_ = static_cast< uint32_t >(0);
+      has_end_time_ = false;
+      end_time_ = static_cast< uint32_t >(0);
+      has_status_ = false;
+      status_ = static_cast< int32_t >(0);
+      has_output_ = false;
+      has_user_time_ = false;
+      user_time_ = static_cast< uint32_t >(0);
+      has_system_time_ = false;
+      system_time_ = static_cast< uint32_t >(0);
+    }
+
+    EdgeFinished(const EdgeFinished&);
+    void operator=(const EdgeFinished&);
+
+    void SerializeToOstream(std::ostream* output__) const {
+      WriteVarint32(output__, 1, id_);
+      WriteVarint32(output__, 2, end_time_);
+      WriteVarint32(output__, 3, ZigZagEncode32(status_));
+      WriteString(output__, 4, output_);
+      WriteVarint32(output__, 5, user_time_);
+      WriteVarint32(output__, 6, system_time_);
+    }
+
+    size_t ByteSizeLong() const {
+      size_t size = 0;
+      size += VarintSize32(id_) + 1;
+      size += VarintSize32(end_time_) + 1;
+      size += VarintSize32(ZigZagEncode32(status_)) + 1;
+      size += StringSize(output_) + 1;
+      size += VarintSize32(user_time_) + 1;
+      size += VarintSize32(system_time_) + 1;
+      return size;
+    }
+
+    void Clear() {
+      id_ = static_cast< uint32_t >(0);
+      end_time_ = static_cast< uint32_t >(0);
+      status_ = static_cast< int32_t >(0);
+      output_.clear();
+      user_time_ = static_cast< uint32_t >(0);
+      system_time_ = static_cast< uint32_t >(0);
+    }
+
+    uint32_t* mutable_id() {
+      has_id_ = true;
+      return &id_;
+    }
+    void set_id(const uint32_t& value) {
+      has_id_ = true;
+      id_ = value;
+    }
+    uint32_t* mutable_end_time() {
+      has_end_time_ = true;
+      return &end_time_;
+    }
+    void set_end_time(const uint32_t& value) {
+      has_end_time_ = true;
+      end_time_ = value;
+    }
+    int32_t* mutable_status() {
+      has_status_ = true;
+      return &status_;
+    }
+    void set_status(const int32_t& value) {
+      has_status_ = true;
+      status_ = value;
+    }
+    std::string* mutable_output() {
+      has_output_ = true;
+      return &output_;
+    }
+    void set_output(const std::string& value) {
+      has_output_ = true;
+      output_ = value;
+    }
+    uint32_t* mutable_user_time() {
+      has_user_time_ = true;
+      return &user_time_;
+    }
+    void set_user_time(const uint32_t& value) {
+      has_user_time_ = true;
+      user_time_ = value;
+    }
+    uint32_t* mutable_system_time() {
+      has_system_time_ = true;
+      return &system_time_;
+    }
+    void set_system_time(const uint32_t& value) {
+      has_system_time_ = true;
+      system_time_ = value;
+    }
+  };
+
+  struct Message {
+    enum Level {
+      INFO = 0,
+      WARNING = 1,
+      ERROR = 2,
+      DEBUG = 3,
+    };
+
+    ::ninja::Status::Message::Level level_;
+    bool has_level_;
+    std::string message_;
+    bool has_message_;
+
+    Message() {
+      has_level_ = false;
+      level_ = static_cast< ::ninja::Status::Message::Level >(0);
+      has_message_ = false;
+    }
+
+    Message(const Message&);
+    void operator=(const Message&);
+
+    void SerializeToOstream(std::ostream* output__) const {
+      WriteVarint32SignExtended(output__, 1, static_cast<int32_t>(level_));
+      WriteString(output__, 2, message_);
+    }
+
+    size_t ByteSizeLong() const {
+      size_t size = 0;
+      size += VarintSize32SignExtended(static_cast<int32_t>(level_)) + 1;
+      size += StringSize(message_) + 1;
+      return size;
+    }
+
+    void Clear() {
+      level_ = static_cast< ::ninja::Status::Message::Level >(0);
+      message_.clear();
+    }
+
+    ::ninja::Status::Message::Level* mutable_level() {
+      has_level_ = true;
+      return &level_;
+    }
+    void set_level(const ::ninja::Status::Message::Level& value) {
+      has_level_ = true;
+      level_ = value;
+    }
+    std::string* mutable_message() {
+      has_message_ = true;
+      return &message_;
+    }
+    void set_message(const std::string& value) {
+      has_message_ = true;
+      message_ = value;
+    }
+  };
+
+  ::ninja::Status::TotalEdges total_edges_;
+  bool has_total_edges_;
+  ::ninja::Status::BuildStarted build_started_;
+  bool has_build_started_;
+  ::ninja::Status::BuildFinished build_finished_;
+  bool has_build_finished_;
+  ::ninja::Status::EdgeStarted edge_started_;
+  bool has_edge_started_;
+  ::ninja::Status::EdgeFinished edge_finished_;
+  bool has_edge_finished_;
+  ::ninja::Status::Message message_;
+  bool has_message_;
+
+  Status() {
+    has_total_edges_ = false;
+    has_build_started_ = false;
+    has_build_finished_ = false;
+    has_edge_started_ = false;
+    has_edge_finished_ = false;
+    has_message_ = false;
+  }
+
+  Status(const Status&);
+  void operator=(const Status&);
+
+  void SerializeToOstream(std::ostream* output__) const {
+    if (has_total_edges_) {
+      WriteLengthDelimited(output__, 1,
+                           total_edges_.ByteSizeLong());
+      total_edges_.SerializeToOstream(output__);
+    }
+    if (has_build_started_) {
+      WriteLengthDelimited(output__, 2,
+                           build_started_.ByteSizeLong());
+      build_started_.SerializeToOstream(output__);
+    }
+    if (has_build_finished_) {
+      WriteLengthDelimited(output__, 3,
+                           build_finished_.ByteSizeLong());
+      build_finished_.SerializeToOstream(output__);
+    }
+    if (has_edge_started_) {
+      WriteLengthDelimited(output__, 4,
+                           edge_started_.ByteSizeLong());
+      edge_started_.SerializeToOstream(output__);
+    }
+    if (has_edge_finished_) {
+      WriteLengthDelimited(output__, 5,
+                           edge_finished_.ByteSizeLong());
+      edge_finished_.SerializeToOstream(output__);
+    }
+    if (has_message_) {
+      WriteLengthDelimited(output__, 6,
+                           message_.ByteSizeLong());
+      message_.SerializeToOstream(output__);
+    }
+  }
+
+  size_t ByteSizeLong() const {
+    size_t size = 0;
+    if (has_total_edges_) {
+      size += 1 + VarintSize32(total_edges_.ByteSizeLong());
+      size += total_edges_.ByteSizeLong();
+    }
+    if (has_build_started_) {
+      size += 1 + VarintSize32(build_started_.ByteSizeLong());
+      size += build_started_.ByteSizeLong();
+    }
+    if (has_build_finished_) {
+      size += 1 + VarintSize32(build_finished_.ByteSizeLong());
+      size += build_finished_.ByteSizeLong();
+    }
+    if (has_edge_started_) {
+      size += 1 + VarintSize32(edge_started_.ByteSizeLong());
+      size += edge_started_.ByteSizeLong();
+    }
+    if (has_edge_finished_) {
+      size += 1 + VarintSize32(edge_finished_.ByteSizeLong());
+      size += edge_finished_.ByteSizeLong();
+    }
+    if (has_message_) {
+      size += 1 + VarintSize32(message_.ByteSizeLong());
+      size += message_.ByteSizeLong();
+    }
+    return size;
+  }
+
+  void Clear() {
+    if (has_total_edges_) {
+      total_edges_.Clear();
+      has_total_edges_ = false;
+    }
+    if (has_build_started_) {
+      build_started_.Clear();
+      has_build_started_ = false;
+    }
+    if (has_build_finished_) {
+      build_finished_.Clear();
+      has_build_finished_ = false;
+    }
+    if (has_edge_started_) {
+      edge_started_.Clear();
+      has_edge_started_ = false;
+    }
+    if (has_edge_finished_) {
+      edge_finished_.Clear();
+      has_edge_finished_ = false;
+    }
+    if (has_message_) {
+      message_.Clear();
+      has_message_ = false;
+    }
+  }
+
+  ::ninja::Status::TotalEdges* mutable_total_edges() {
+    has_total_edges_ = true;
+    return &total_edges_;
+  }
+  ::ninja::Status::BuildStarted* mutable_build_started() {
+    has_build_started_ = true;
+    return &build_started_;
+  }
+  ::ninja::Status::BuildFinished* mutable_build_finished() {
+    has_build_finished_ = true;
+    return &build_finished_;
+  }
+  ::ninja::Status::EdgeStarted* mutable_edge_started() {
+    has_edge_started_ = true;
+    return &edge_started_;
+  }
+  ::ninja::Status::EdgeFinished* mutable_edge_finished() {
+    has_edge_finished_ = true;
+    return &edge_finished_;
+  }
+  ::ninja::Status::Message* mutable_message() {
+    has_message_ = true;
+    return &message_;
+  }
+};
+
+}
+#endif // NINJA_FRONTEND_PB_H
diff --git a/src/frontend.proto b/src/frontend.proto
new file mode 100644
index 0000000..57423d8
--- /dev/null
+++ b/src/frontend.proto
@@ -0,0 +1,88 @@
+// Copyright 2017 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package ninja;
+
+message Status {
+  message TotalEdges {
+    // New value for total edges in the build.
+    optional uint32 total_edges = 1;
+  }
+
+  message BuildStarted {
+    // Number of jobs Ninja will run in parallel.
+    optional uint32 parallelism = 1;
+    // Verbose value passed to ninja.
+    optional bool verbose = 2;
+  }
+
+  message BuildFinished {
+  }
+
+  message EdgeStarted {
+    // Edge identification number, unique to a Ninja run.
+    optional uint32 id = 1;
+    // Edge start time in milliseconds since Ninja started.
+    optional uint32 start_time = 2;
+    // List of edge inputs.
+    repeated string inputs = 3;
+    // List of edge outputs.
+    repeated string outputs = 4;
+    // Description field from the edge.
+    optional string desc = 5;
+    // Command field from the edge.
+    optional string command = 6;
+    // Edge uses console.
+    optional bool console = 7;
+  }
+
+  message EdgeFinished {
+    // Edge identification number, unique to a Ninja run.
+    optional uint32 id = 1;
+    // Edge end time in milliseconds since Ninja started.
+    optional uint32 end_time = 2;
+    // Exit status (0 for success).
+    optional sint32 status = 3;
+    // Edge output, may contain ANSI codes.
+    optional string output = 4;
+    // Number of milliseconds spent executing in user mode
+    optional uint32 user_time = 5;
+    // Number of milliseconds spent executing in kernel mode
+    optional uint32 system_time = 6;
+  }
+
+  message Message {
+    enum Level {
+      INFO = 0;
+      WARNING = 1;
+      ERROR = 2;
+      DEBUG = 3;
+    }
+    // Message priority level (DEBUG, INFO, WARNING, ERROR).
+    optional Level level = 1 [default = INFO];
+    // Info/warning/error message from Ninja.
+    optional string message = 2;
+  }
+
+  optional TotalEdges total_edges = 1;
+  optional BuildStarted build_started = 2;
+  optional BuildFinished build_finished = 3;
+  optional EdgeStarted edge_started = 4;
+  optional EdgeFinished edge_finished = 5;
+  optional Message message = 6;
+}
diff --git a/src/graph.cc b/src/graph.cc
index 9c2f784..6c773aa 100644
--- a/src/graph.cc
+++ b/src/graph.cc
@@ -14,6 +14,7 @@
 
 #include "graph.h"
 
+#include <deque>
 #include <assert.h>
 #include <stdio.h>
 
@@ -22,22 +23,187 @@
 #include "depfile_parser.h"
 #include "deps_log.h"
 #include "disk_interface.h"
-#include "manifest_parser.h"
 #include "metrics.h"
+#include "parallel_map.h"
 #include "state.h"
 #include "util.h"
 
+bool Node::PrecomputeStat(DiskInterface* disk_interface, std::string* err) {
+  if (in_edge() != nullptr) {
+    if (in_edge()->IsPhonyOutput()) {
+      return true;
+    }
+    return (precomputed_mtime_ = disk_interface->LStat(path_.str(), nullptr, err)) != -1;
+  } else {
+    return (precomputed_mtime_ = disk_interface->Stat(path_.str(), err)) != -1;
+  }
+}
+
 bool Node::Stat(DiskInterface* disk_interface, string* err) {
-  return (mtime_ = disk_interface->Stat(path_, err)) != -1;
+  if (in_edge() != nullptr) {
+    assert(!in_edge()->IsPhonyOutput());
+    return (mtime_ = disk_interface->LStat(path_.str(), nullptr, err)) != -1;
+  } else {
+    return (mtime_ = disk_interface->Stat(path_.str(), err)) != -1;
+  }
 }
 
-bool DependencyScan::RecomputeDirty(Node* node, string* err) {
-  vector<Node*> stack;
-  return RecomputeDirty(node, &stack, err);
+bool Node::LStat(DiskInterface* disk_interface, bool* is_dir, string* err) {
+  assert(in_edge() != nullptr);
+  assert(!in_edge()->IsPhonyOutput());
+  return (mtime_ = disk_interface->LStat(path_.str(), is_dir, err)) != -1;
 }
 
-bool DependencyScan::RecomputeDirty(Node* node, vector<Node*>* stack,
-                                    string* err) {
+bool DependencyScan::RecomputeNodesDirty(const std::vector<Node*>& initial_nodes,
+                                         std::vector<Node*>* validation_nodes,
+                                         std::string* err) {
+  METRIC_RECORD("dep scan");
+  std::vector<Node*> all_nodes;
+  std::vector<Edge*> all_edges;
+  std::unique_ptr<ThreadPool> thread_pool = CreateThreadPool();
+
+  {
+    METRIC_RECORD("dep scan : collect nodes+edges");
+    for (Node* node : initial_nodes)
+      CollectPrecomputeLists(node, &all_nodes, &all_edges);
+  }
+
+  bool success = true;
+  if (!PrecomputeNodesDirty(all_nodes, all_edges, thread_pool.get(), err))
+    success = false;
+
+
+  std::deque<Node*> nodes(initial_nodes.begin(), initial_nodes.end());
+
+  if (success) {
+    METRIC_RECORD("dep scan : main pass");
+    std::vector<Node*> stack;
+    std::vector<Node*> new_validation_nodes;
+    while (!nodes.empty()) {
+      Node* node = nodes.front();
+      nodes.pop_front();
+
+      stack.clear();
+      new_validation_nodes.clear();
+      if (!RecomputeNodeDirty(node, &stack, &new_validation_nodes, err)) {
+        success = false;
+        break;
+      }
+      nodes.insert(nodes.end(), new_validation_nodes.begin(),
+                                new_validation_nodes.end());
+      if (!new_validation_nodes.empty()) {
+        assert(validation_nodes &&
+               "validations require RecomputeDirty to be called with validation_nodes");
+        validation_nodes->insert(validation_nodes->end(),
+                                 new_validation_nodes.begin(),
+                                 new_validation_nodes.end());
+      }
+    }
+  }
+
+  {
+    // Ensure that the precomputed mtime information can't be used after this
+    // dependency scan finishes.
+    METRIC_RECORD("dep scan : clear pre-stat");
+    ParallelMap(thread_pool.get(), all_nodes, [](Node* node) {
+      node->ClearPrecomputedStat();
+    });
+  }
+
+  return success;
+}
+
+void DependencyScan::CollectPrecomputeLists(Node* node,
+                                            std::vector<Node*>* nodes,
+                                            std::vector<Edge*>* edges) {
+  if (node->precomputed_dirtiness())
+    return;
+  node->set_precomputed_dirtiness(true);
+  nodes->push_back(node);
+
+  Edge* edge = node->in_edge();
+  if (edge && !edge->precomputed_dirtiness_) {
+    edge->precomputed_dirtiness_ = true;
+    edges->push_back(edge);
+
+    for (Node* node : edge->inputs_) {
+      // Duplicate the dirtiness check here to avoid an unnecessary function
+      // call. (The precomputed_dirtiness() will be inlined, but the recursive
+      // call can't be.)
+      if (!node->precomputed_dirtiness())
+        CollectPrecomputeLists(node, nodes, edges);
+    }
+
+    if (!edge->validations_.empty()) {
+      for (Node* node : edge->validations_) {
+        // Duplicate the dirtiness check here to avoid an unnecessary function
+        // call. (The precomputed_dirtiness() will be inlined, but the recursive
+        // call can't be.)
+        if (!node->precomputed_dirtiness())
+          CollectPrecomputeLists(node, nodes, edges);
+      }
+    }
+  }
+
+  // Collect dependencies from the deps log. This pass could also examine
+  // depfiles, but it would be a more intrusive design change, because we don't
+  // want to parse a depfile twice.
+  if (DepsLog::Deps* deps = deps_log()->GetDeps(node)) {
+    for (int i = 0; i < deps->node_count; ++i) {
+      Node* node = deps->nodes[i];
+      // Duplicate the dirtiness check here to avoid an unnecessary function
+      // call.
+      if (!node->precomputed_dirtiness()) {
+        CollectPrecomputeLists(node, nodes, edges);
+      }
+    }
+  }
+}
+
+bool DependencyScan::PrecomputeNodesDirty(const std::vector<Node*>& nodes,
+                                          const std::vector<Edge*>& edges,
+                                          ThreadPool* thread_pool,
+                                          std::string* err) {
+  // Optimize the "null build" case by calling Stat in parallel on every node in
+  // the transitive closure.
+  //
+  // The Windows RealDiskInterface::Stat uses a directory-based cache that isn't
+  // thread-safe. Various tests also uses a non-thread-safe Stat, so disable the
+  // parallelized stat'ing for them as well.
+  if (disk_interface_->IsStatThreadSafe() &&
+      GetOptimalThreadPoolJobCount() > 1) {
+    METRIC_RECORD("dep scan : pre-stat nodes");
+    if (!PropagateError(err, ParallelMap(thread_pool, nodes,
+        [this](Node* node) {
+      // Each node is guaranteed to appear at most once in the collected list
+      // of nodes, so it's safe to modify the nodes from worker threads.
+      std::string err;
+      node->PrecomputeStat(disk_interface_, &err);
+      return err;
+    }))) {
+      return false;
+    }
+  }
+
+  {
+    METRIC_RECORD("dep scan : precompute edge info");
+    if (!PropagateError(err, ParallelMap(thread_pool, edges, [](Edge* edge) {
+      // As with the node list, each edge appears at most once in the
+      // collected list, so it's safe to modify the edges from worker threads.
+      std::string err;
+      edge->PrecomputeDepScanInfo(&err);
+      return err;
+    }))) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+bool DependencyScan::RecomputeNodeDirty(Node* node, std::vector<Node*>* stack,
+                                        std::vector<Node*>* validation_nodes,
+                                        std::string* err) {
   Edge* edge = node->in_edge();
   if (!edge) {
     // If we already visited this leaf node then we are done.
@@ -65,39 +231,66 @@
   stack->push_back(node);
 
   bool dirty = false;
+  bool phony_output = edge->IsPhonyOutput();
   edge->outputs_ready_ = true;
   edge->deps_missing_ = false;
 
-  // Load output mtimes so we can compare them to the most recent input below.
-  for (vector<Node*>::iterator o = edge->outputs_.begin();
-       o != edge->outputs_.end(); ++o) {
-    if (!(*o)->StatIfNecessary(disk_interface_, err))
+  if (phony_output) {
+    EXPLAIN("edge with output %s is a phony output, so is always dirty",
+            node->path().c_str());
+    dirty = true;
+
+    if (edge->UsesDepsLog() || edge->UsesDepfile()) {
+      *err = "phony output " + node->path() + " has deps, which does not make sense.";
       return false;
+    }
+  } else {
+    // Load output mtimes so we can compare them to the most recent input below.
+    for (vector<Node*>::iterator o = edge->outputs_.begin();
+         o != edge->outputs_.end(); ++o) {
+      if (!(*o)->StatIfNecessary(disk_interface_, err))
+        return false;
+    }
+
+    if (!dep_loader_.LoadDeps(edge, err)) {
+      if (!err->empty())
+        return false;
+      // Failed to load dependency info: rebuild to regenerate it.
+      // LoadDeps() did EXPLAIN() already, no need to do it here.
+      dirty = edge->deps_missing_ = true;
+    }
   }
 
-  if (!dep_loader_.LoadDeps(edge, err)) {
-    if (!err->empty())
-      return false;
-    // Failed to load dependency info: rebuild to regenerate it.
-    // LoadDeps() did EXPLAIN() already, no need to do it here.
-    dirty = edge->deps_missing_ = true;
-  }
+  // Store any validation nodes from the edge for adding to the initial
+  // nodes.  Don't recurse into them, that would trigger the dependency
+  // cycle detector if the validation node depends on this node.
+  // RecomputeNodesDirty will add the validation nodes to the initial nodes
+  // and recurse into them.
+  validation_nodes->insert(validation_nodes->end(),
+      edge->validations_.begin(), edge->validations_.end());
 
   // Visit all inputs; we're dirty if any of the inputs are dirty.
   Node* most_recent_input = NULL;
   for (vector<Node*>::iterator i = edge->inputs_.begin();
        i != edge->inputs_.end(); ++i) {
     // Visit this input.
-    if (!RecomputeDirty(*i, stack, err))
+    if (!RecomputeNodeDirty(*i, stack, validation_nodes, err))
       return false;
 
     // If an input is not ready, neither are our outputs.
-    if (Edge* in_edge = (*i)->in_edge()) {
+    Edge* in_edge = (*i)->in_edge();
+    if (in_edge != nullptr) {
       if (!in_edge->outputs_ready_)
         edge->outputs_ready_ = false;
     }
 
-    if (!edge->is_order_only(i - edge->inputs_.begin())) {
+    if (!phony_output && !edge->is_order_only(i - edge->inputs_.begin())) {
+      if (in_edge != nullptr && in_edge->IsPhonyOutput()) {
+        *err = "real file '" + node->path() +
+               "' depends on phony output '" + (*i)->path() + "'\n";
+        return false;
+      }
+
       // If a regular input is dirty (or missing), we're dirty.
       // Otherwise consider mtime.
       if ((*i)->dirty()) {
@@ -182,10 +375,30 @@
 
 bool DependencyScan::RecomputeOutputsDirty(Edge* edge, Node* most_recent_input,
                                            bool* outputs_dirty, string* err) {
-  string command = edge->EvaluateCommand(/*incl_rsp_file=*/true);
+  assert(!edge->IsPhonyOutput());
+
+  uint64_t command_hash = edge->GetCommandHash();
   for (vector<Node*>::iterator o = edge->outputs_.begin();
        o != edge->outputs_.end(); ++o) {
-    if (RecomputeOutputDirty(edge, most_recent_input, command, *o)) {
+    if (edge->is_phony()) {
+      // Phony edges don't write any output.  Outputs are only dirty if
+      // there are no inputs and we're missing the output.
+      if (edge->inputs_.empty() && !(*o)->exists()) {
+        // For phony targets defined in the ninja file, error when using dirty phony edges.
+        // The phony edges automatically created from depfiles still need the old behavior.
+        if (missing_phony_is_err_ && !edge->phony_from_depfile_) {
+          *err = "output " + (*o)->path() + " of phony edge doesn't exist. Missing 'phony_output = true'?";
+          return false;
+        } else {
+          EXPLAIN("output %s of phony edge with no inputs doesn't exist",
+                  (*o)->path().c_str());
+          *outputs_dirty = true;
+          return true;
+        }
+      }
+      continue;
+    }
+    if (RecomputeOutputDirty(edge, most_recent_input, command_hash, *o)) {
       *outputs_dirty = true;
       return true;
     }
@@ -195,18 +408,9 @@
 
 bool DependencyScan::RecomputeOutputDirty(Edge* edge,
                                           Node* most_recent_input,
-                                          const string& command,
+                                          uint64_t command_hash,
                                           Node* output) {
-  if (edge->is_phony()) {
-    // Phony edges don't write any output.  Outputs are only dirty if
-    // there are no inputs and we're missing the output.
-    if (edge->inputs_.empty() && !output->exists()) {
-      EXPLAIN("output %s of phony edge with no inputs doesn't exist",
-              output->path().c_str());
-      return true;
-    }
-    return false;
-  }
+  assert(!edge->is_phony());
 
   BuildLog::LogEntry* entry = 0;
 
@@ -225,8 +429,8 @@
     // build log.  Use that mtime instead, so that the file will only be
     // considered dirty if an input was modified since the previous run.
     bool used_restat = false;
-    if (edge->GetBindingBool("restat") && build_log() &&
-        (entry = build_log()->LookupByOutput(output->path()))) {
+    if (edge->IsRestat() && build_log() &&
+        (entry = build_log()->LookupByOutput(output->path_hashed()))) {
       output_mtime = entry->mtime;
       used_restat = true;
     }
@@ -242,10 +446,10 @@
   }
 
   if (build_log()) {
-    bool generator = edge->GetBindingBool("generator");
-    if (entry || (entry = build_log()->LookupByOutput(output->path()))) {
+    bool generator = edge->IsGenerator();
+    if (entry || (entry = build_log()->LookupByOutput(output->path_hashed()))) {
       if (!generator &&
-          BuildLog::LogEntry::HashCommand(command) != entry->command_hash) {
+          command_hash != entry->command_hash) {
         // May also be dirty due to the command changing since the last build.
         // But if this is a generator rule, the command changing does not make us
         // dirty.
@@ -281,111 +485,180 @@
   return true;
 }
 
-/// An Env for an Edge, providing $in and $out.
-struct EdgeEnv : public Env {
-  enum EscapeKind { kShellEscape, kDoNotEscape };
+static const HashedStrView kIn        { "in" };
+static const HashedStrView kInNewline { "in_newline" };
+static const HashedStrView kOut       { "out" };
 
-  EdgeEnv(Edge* edge, EscapeKind escape)
-      : edge_(edge), escape_in_out_(escape), recursive_(false) {}
-  virtual string LookupVariable(const string& var);
-
-  /// Given a span of Nodes, construct a list of paths suitable for a command
-  /// line.
-  string MakePathList(vector<Node*>::iterator begin,
-                      vector<Node*>::iterator end,
-                      char sep);
-
- private:
-  vector<string> lookups_;
-  Edge* edge_;
-  EscapeKind escape_in_out_;
-  bool recursive_;
-};
-
-string EdgeEnv::LookupVariable(const string& var) {
-  if (var == "in" || var == "in_newline") {
+bool EdgeEval::EvaluateVariable(std::string* out_append,
+                                const HashedStrView& var,
+                                std::string* err) {
+  if (var == kIn || var == kInNewline) {
     int explicit_deps_count = edge_->inputs_.size() - edge_->implicit_deps_ -
       edge_->order_only_deps_;
-    return MakePathList(edge_->inputs_.begin(),
-                        edge_->inputs_.begin() + explicit_deps_count,
-                        var == "in" ? ' ' : '\n');
-  } else if (var == "out") {
+    AppendPathList(out_append,
+                   edge_->inputs_.begin(),
+                   edge_->inputs_.begin() + explicit_deps_count,
+                   var == kIn ? ' ' : '\n');
+    return true;
+  } else if (var == kOut) {
     int explicit_outs_count = edge_->outputs_.size() - edge_->implicit_outs_;
-    return MakePathList(edge_->outputs_.begin(),
-                        edge_->outputs_.begin() + explicit_outs_count,
-                        ' ');
+    AppendPathList(out_append,
+                   edge_->outputs_.begin(),
+                   edge_->outputs_.begin() + explicit_outs_count,
+                   ' ');
+    return true;
   }
 
-  if (recursive_) {
-    vector<string>::const_iterator it;
-    if ((it = find(lookups_.begin(), lookups_.end(), var)) != lookups_.end()) {
-      string cycle;
-      for (; it != lookups_.end(); ++it)
-        cycle.append(*it + " -> ");
-      cycle.append(var);
-      Fatal(("cycle in rule variables: " + cycle).c_str());
+  if (edge_->EvaluateVariableSelfOnly(out_append, var))
+    return true;
+
+  // Search for a matching rule binding.
+  if (const std::string* binding_pattern = edge_->rule().GetBinding(var)) {
+    // Detect recursive rule variable usage.
+    if (recursion_count_ == kEvalRecursionLimit) {
+      std::string cycle = recursion_vars_[0].AsString();
+      for (int i = 1; i < kEvalRecursionLimit; ++i) {
+        cycle += " -> " + recursion_vars_[i].AsString();
+        if (recursion_vars_[i] == recursion_vars_[0])
+          break;
+      }
+      *err = "cycle in rule variables: " + cycle;
+      return false;
     }
+    recursion_vars_[recursion_count_++] = var.str_view();
+
+    return EvaluateBindingOnRule(out_append, *binding_pattern, this, err);
   }
 
-  // See notes on BindingEnv::LookupWithFallback.
-  const EvalString* eval = edge_->rule_->GetBinding(var);
-  if (recursive_ && eval)
-    lookups_.push_back(var);
-
-  // In practice, variables defined on rules never use another rule variable.
-  // For performance, only start checking for cycles after the first lookup.
-  recursive_ = true;
-  return edge_->env_->LookupWithFallback(var, eval, this);
+  // Fall back to the edge's enclosing scope.
+  if (eval_phase_ == EdgeEval::kParseTime) {
+    Scope::EvaluateVariableAtPos(out_append, var, edge_->pos_.scope_pos());
+  } else {
+    Scope::EvaluateVariable(out_append, var, edge_->pos_.scope());
+  }
+  return true;
 }
 
-string EdgeEnv::MakePathList(vector<Node*>::iterator begin,
-                             vector<Node*>::iterator end,
-                             char sep) {
-  string result;
-  for (vector<Node*>::iterator i = begin; i != end; ++i) {
-    if (!result.empty())
-      result.push_back(sep);
-    const string& path = (*i)->PathDecanonicalized();
+void EdgeEval::AppendPathList(std::string* out_append,
+                              std::vector<Node*>::iterator begin,
+                              std::vector<Node*>::iterator end,
+                              char sep) {
+  for (auto it = begin; it != end; ++it) {
+    if (it != begin)
+      out_append->push_back(sep);
+
+    const string& path = (*it)->PathDecanonicalized();
     if (escape_in_out_ == kShellEscape) {
 #if _WIN32
-      GetWin32EscapedString(path, &result);
+      GetWin32EscapedString(path, out_append);
 #else
-      GetShellEscapedString(path, &result);
+      GetShellEscapedString(path, out_append);
 #endif
     } else {
-      result.append(path);
+      out_append->append(path);
     }
   }
-  return result;
 }
 
-string Edge::EvaluateCommand(bool incl_rsp_file) {
-  string command = GetBinding("command");
+static const HashedStrView kCommand         { "command" };
+static const HashedStrView kDepfile         { "depfile" };
+static const HashedStrView kRspfile         { "rspfile" };
+static const HashedStrView kRspFileContent  { "rspfile_content" };
+
+bool Edge::EvaluateCommand(std::string* out_append, bool incl_rsp_file,
+                           std::string* err) {
+  METRIC_RECORD("eval command");
+  if (!EvaluateVariable(out_append, kCommand, err))
+    return false;
   if (incl_rsp_file) {
-    string rspfile_content = GetBinding("rspfile_content");
-    if (!rspfile_content.empty())
-      command += ";rspfile=" + rspfile_content;
+    std::string rspfile_content;
+    if (!EvaluateVariable(&rspfile_content, kRspFileContent, err))
+      return false;
+    if (!rspfile_content.empty()) {
+      out_append->append(";rspfile=");
+      out_append->append(rspfile_content);
+    }
   }
+  return true;
+}
+
+std::string Edge::EvaluateCommand(bool incl_rsp_file) {
+  std::string command;
+  std::string err;
+  if (!EvaluateCommand(&command, incl_rsp_file, &err))
+    Fatal("%s", err.c_str());
   return command;
 }
 
-string Edge::GetBinding(const string& key) {
-  EdgeEnv env(this, EdgeEnv::kShellEscape);
-  return env.LookupVariable(key);
+static const HashedStrView kRestat      { "restat" };
+static const HashedStrView kGenerator   { "generator" };
+static const HashedStrView kDeps        { "deps" };
+static const HashedStrView kPhonyOutput  { "phony_output" };
+
+bool Edge::PrecomputeDepScanInfo(std::string* err) {
+  if (dep_scan_info_.valid)
+    return true;
+
+  // Precompute boolean flags.
+  auto get_bool_var = [this, err](const HashedStrView& var,
+                                  EdgeEval::EscapeKind escape, bool* out) {
+    std::string value;
+    if (!EvaluateVariable(&value, var, err, EdgeEval::kFinalScope, escape))
+      return false;
+    *out = !value.empty();
+    return true;
+  };
+  if (!get_bool_var(kRestat,      EdgeEval::kShellEscape, &dep_scan_info_.restat))       return false;
+  if (!get_bool_var(kGenerator,   EdgeEval::kShellEscape, &dep_scan_info_.generator))    return false;
+  if (!get_bool_var(kDeps,        EdgeEval::kShellEscape, &dep_scan_info_.deps))         return false;
+  if (!get_bool_var(kDepfile,     EdgeEval::kDoNotEscape, &dep_scan_info_.depfile))      return false;
+  if (!get_bool_var(kPhonyOutput, EdgeEval::kShellEscape, &dep_scan_info_.phony_output)) return false;
+
+  // Precompute the command hash.
+  std::string command;
+  if (!EvaluateCommand(&command, /*incl_rsp_file=*/true, err))
+    return false;
+  dep_scan_info_.command_hash = BuildLog::LogEntry::HashCommand(command);
+
+  dep_scan_info_.valid = true;
+  return true;
 }
 
-bool Edge::GetBindingBool(const string& key) {
-  return !GetBinding(key).empty();
+/// Returns dependency-scanning info or exits with a fatal error.
+const Edge::DepScanInfo& Edge::ComputeDepScanInfo() {
+  std::string err;
+  if (!PrecomputeDepScanInfo(&err))
+    Fatal("%s", err.c_str());
+  return dep_scan_info_;
 }
 
-string Edge::GetUnescapedDepfile() {
-  EdgeEnv env(this, EdgeEnv::kDoNotEscape);
-  return env.LookupVariable("depfile");
+bool Edge::EvaluateVariable(std::string* out_append, const HashedStrView& key,
+                            std::string* err, EdgeEval::EvalPhase phase,
+                            EdgeEval::EscapeKind escape) {
+  EdgeEval eval(this, phase, escape);
+  return eval.EvaluateVariable(out_append, key, err);
 }
 
-string Edge::GetUnescapedRspfile() {
-  EdgeEnv env(this, EdgeEnv::kDoNotEscape);
-  return env.LookupVariable("rspfile");
+std::string Edge::GetBindingImpl(const HashedStrView& key,
+                                 EdgeEval::EvalPhase phase,
+                                 EdgeEval::EscapeKind escape) {
+  std::string result;
+  std::string err;
+  if (!EvaluateVariable(&result, key, &err, phase, escape))
+    Fatal("%s", err.c_str());
+  return result;
+}
+
+std::string Edge::GetBinding(const HashedStrView& key) {
+  return GetBindingImpl(key, EdgeEval::kFinalScope, EdgeEval::kShellEscape);
+}
+
+std::string Edge::GetUnescapedDepfile() {
+  return GetBindingImpl(kDepfile, EdgeEval::kFinalScope, EdgeEval::kDoNotEscape);
+}
+
+std::string Edge::GetUnescapedRspfile() {
+  return GetBindingImpl(kRspfile, EdgeEval::kFinalScope, EdgeEval::kDoNotEscape);
 }
 
 void Edge::Dump(const char* prefix) const {
@@ -399,6 +672,13 @@
        i != outputs_.end() && *i != NULL; ++i) {
     printf("%s ", (*i)->path().c_str());
   }
+  if (!validations_.empty()) {
+    printf(" validations ");
+    for (vector<Node*>::const_iterator i = validations_.begin();
+         i != validations_.end() && *i != NULL; ++i) {
+      printf("%s ", (*i)->path().c_str());
+    }
+  }
   if (pool_) {
     if (!pool_->name().empty()) {
       printf("(in pool '%s')", pool_->name().c_str());
@@ -422,7 +702,21 @@
   // of the form "build a: phony ... a ...".   Restrict our
   // "phonycycle" diagnostic option to the form it used.
   return is_phony() && outputs_.size() == 1 && implicit_outs_ == 0 &&
-      implicit_deps_ == 0;
+      implicit_deps_ == 0 && order_only_deps_ == 0;
+}
+
+bool Edge::EvaluateVariableSelfOnly(std::string* out_append,
+                                    const HashedStrView& var) const {
+  // ninja allows declaring the same binding repeatedly on an edge. Use the
+  // last matching binding.
+  const auto it_end = unevaled_bindings_.rend();
+  for (auto it = unevaled_bindings_.rbegin(); it != it_end; ++it) {
+    if (var == it->first) {
+      EvaluateBindingInScope(out_append, it->second, pos_.scope_pos());
+      return true;
+    }
+  }
+  return false;
 }
 
 // static
@@ -440,6 +734,66 @@
   return result;
 }
 
+Node::~Node() {
+  EdgeList* node = out_edges_.load();
+  while (node != nullptr) {
+    EdgeList* next = node->next;
+    delete node;
+    node = next;
+  }
+}
+
+// Does the node have at least one out edge?
+bool Node::has_out_edge() const {
+  return out_edges_.load() != nullptr;
+}
+
+std::vector<Edge*> Node::GetOutEdges() const {
+  // Include out-edges from the manifest.
+  std::vector<Edge*> result;
+  for (EdgeList* node = out_edges_.load(); node != nullptr; node = node->next) {
+    result.push_back(node->edge);
+  }
+  std::sort(result.begin(), result.end(), EdgeCmp());
+
+  // Add extra out-edges from depfiles and the deps log. Preserve the order
+  // of these extra edges; don't sort them.
+  std::copy(dep_scan_out_edges_.begin(), dep_scan_out_edges_.end(),
+            std::back_inserter(result));
+
+  return result;
+}
+
+std::vector<Edge*> Node::GetValidationOutEdges() const {
+  std::vector<Edge*> result;
+  for (EdgeList* node = validation_out_edges_.load(); node != nullptr; node = node->next) {
+    result.push_back(node->edge);
+  }
+  std::sort(result.begin(), result.end(), EdgeCmp());
+
+  return result;
+}
+
+void Node::AddOutEdge(Edge* edge) {
+  EdgeList* new_node = new EdgeList { edge };
+  while (true) {
+    EdgeList* cur_head = out_edges_.load();
+    new_node->next = cur_head;
+    if (out_edges_.compare_exchange_weak(cur_head, new_node))
+      break;
+  }
+}
+
+void Node::AddValidationOutEdge(Edge* edge) {
+  EdgeList* new_node = new EdgeList { edge };
+  while (true) {
+    EdgeList* cur_head = validation_out_edges_.load();
+    new_node->next = cur_head;
+    if (validation_out_edges_.compare_exchange_weak(cur_head, new_node))
+      break;
+  }
+}
+
 void Node::Dump(const char* prefix) const {
   printf("%s <%s 0x%p> mtime: %" PRId64 "%s, (:%s), ",
          prefix, path().c_str(), this,
@@ -451,20 +805,31 @@
     printf("no in-edge\n");
   }
   printf(" out edges:\n");
-  for (vector<Edge*>::const_iterator e = out_edges().begin();
-       e != out_edges().end() && *e != NULL; ++e) {
+  const std::vector<Edge*> out_edges = GetOutEdges();
+  for (vector<Edge*>::const_iterator e = out_edges.begin();
+       e != out_edges.end() && *e != NULL; ++e) {
     (*e)->Dump(" +- ");
   }
+  const std::vector<Edge*> validation_out_edges = GetValidationOutEdges();
+  if (!validation_out_edges.empty()) {
+    printf(" validation out edges:\n");
+    for (vector<Edge*>::const_iterator e = validation_out_edges.begin();
+         e != validation_out_edges.end() && *e != NULL; ++e) {
+      (*e)->Dump(" +- ");
+    }
+  }
 }
 
 bool ImplicitDepLoader::LoadDeps(Edge* edge, string* err) {
-  string deps_type = edge->GetBinding("deps");
-  if (!deps_type.empty())
+  if (edge->UsesDepsLog())
     return LoadDepsFromLog(edge, err);
 
-  string depfile = edge->GetUnescapedDepfile();
-  if (!depfile.empty())
+  if (edge->UsesDepfile()) {
+    std::string depfile = edge->GetUnescapedDepfile();
+    assert(!depfile.empty() &&
+           "UsesDepfile was set, so the depfile should be non-empty");
     return LoadDepFile(edge, depfile, err);
+  }
 
   // No deps to load.
   return true;
@@ -510,8 +875,7 @@
   // Check that this depfile matches the edge's output, if not return false to
   // mark the edge as dirty.
   Node* first_output = edge->outputs_[0];
-  StringPiece opath = StringPiece(first_output->path());
-  if (opath != depfile.out_) {
+  if (first_output->path() != depfile.out_) {
     EXPLAIN("expected depfile '%s' to mention '%s', got '%s'", path.c_str(),
             first_output->path().c_str(), depfile.out_.AsString().c_str());
     return false;
@@ -531,7 +895,7 @@
 
     Node* node = state_->GetNode(*i, slash_bits);
     *implicit_dep = node;
-    node->AddOutEdge(edge);
+    node->AddOutEdgeDepScan(edge);
     CreatePhonyInEdge(node);
   }
 
@@ -559,7 +923,7 @@
   for (int i = 0; i < deps->node_count; ++i, ++implicit_dep) {
     Node* node = deps->nodes[i];
     *implicit_dep = node;
-    node->AddOutEdge(edge);
+    node->AddOutEdgeDepScan(edge);
     CreatePhonyInEdge(node);
   }
   return true;
@@ -580,6 +944,7 @@
   Edge* phony_edge = state_->AddEdge(&State::kPhonyRule);
   node->set_in_edge(phony_edge);
   phony_edge->outputs_.push_back(node);
+  ++phony_edge->explicit_outs_;
 
   // RecomputeDirty might not be called for phony_edge if a previous call
   // to RecomputeDirty had caused the file to be stat'ed.  Because previous
@@ -588,4 +953,6 @@
   // to avoid a potential stuck build.  If we do call RecomputeDirty for
   // this node, it will simply set outputs_ready_ to the correct value.
   phony_edge->outputs_ready_ = true;
+
+  phony_edge->phony_from_depfile_ = true;
 }
diff --git a/src/graph.h b/src/graph.h
index d58fecd..b648947 100644
--- a/src/graph.h
+++ b/src/graph.h
@@ -15,6 +15,8 @@
 #ifndef NINJA_GRAPH_H_
 #define NINJA_GRAPH_H_
 
+#include <atomic>
+#include <set>
 #include <string>
 #include <vector>
 using namespace std;
@@ -31,32 +33,108 @@
 struct Node;
 struct Pool;
 struct State;
+struct ThreadPool;
+
+/// A reference to a path from the lexer. This path is unevaluated, stored in
+/// mmap'ed memory, and is guaranteed to be followed by a terminating character
+/// (e.g. whitespace, a colon or pipe, etc).
+struct LexedPath {
+  StringPiece str_;
+};
+
+#ifdef _WIN32
+
+/// On Windows, we record the '/'-vs-'\' slash direction in the first reference
+/// to a path, which determines the path strings used in command-lines.
+struct NodeSlashBits {
+  NodeSlashBits() {}
+  NodeSlashBits(uint64_t value) : slash_bits_(value) {}
+  uint64_t slash_bits() const { return slash_bits_; }
+private:
+  uint64_t slash_bits_ = 0;
+};
+
+#else
+
+/// By default, '\' is not recognized as a path separator, and slash_bits is
+/// always 0.
+struct NodeSlashBits {
+  NodeSlashBits() {}
+  NodeSlashBits(uint64_t /*value*/) {}
+  uint64_t slash_bits() const { return 0; }
+};
+
+#endif  // _WIN32
+
+/// Record the earliest reference to the node, which is needed for two uses:
+///
+///  - To verify that a node exists before a "default" declaration references
+///    it.
+///  - On Windows, the earliest reference to the node determines the slash_bits
+///    value for decanonicalizing the node's path.
+///
+/// On Windows, atomic operations on this struct probably use a spin lock, but
+/// it can be configured to use a DWCAS (e.g. -mcx16 with Clang/libc++). On
+/// other targets, the empty base class occupies 0 bytes, and atomic operations
+/// will be lock-free.
+struct NodeFirstReference : NodeSlashBits {
+  NodeFirstReference() {}
+  NodeFirstReference(DeclIndex loc, uint64_t slash_bits)
+      : NodeSlashBits(slash_bits), loc_(loc) {}
+  DeclIndex dfs_location() const { return loc_; }
+private:
+  DeclIndex loc_ = kLastDeclIndex;
+};
+
+inline bool operator<(const NodeFirstReference& x,
+                      const NodeFirstReference& y) {
+  if (x.dfs_location() < y.dfs_location()) return true;
+  if (x.dfs_location() > y.dfs_location()) return false;
+  return x.slash_bits() < y.slash_bits();
+}
 
 /// Information about a node in the dependency graph: the file, whether
 /// it's dirty, mtime, etc.
 struct Node {
-  Node(const string& path, uint64_t slash_bits)
+  Node(const HashedStrView& path, uint64_t initial_slash_bits)
       : path_(path),
-        slash_bits_(slash_bits),
-        mtime_(-1),
-        dirty_(false),
-        in_edge_(NULL),
-        id_(-1) {}
+        first_reference_({ kLastDeclIndex, initial_slash_bits }) {}
+  ~Node();
+
+  /// Precompute the node's Stat() call from a worker thread with exclusive
+  /// access to this node. Returns false on error.
+  bool PrecomputeStat(DiskInterface* disk_interface, string* err);
+
+  /// After the dependency scan is complete, reset the precomputed mtime so it
+  /// can't affect later StatIfNecessary() calls.
+  void ClearPrecomputedStat() {
+    precomputed_mtime_ = -1;
+  }
 
   /// Return false on error.
+  /// Uses stat() or lstat() as appropriate.
   bool Stat(DiskInterface* disk_interface, string* err);
 
+  /// Only use when lstat() is desired (output files)
+  bool LStat(DiskInterface* disk_interface, bool* is_dir, string* err);
+
   /// Return false on error.
   bool StatIfNecessary(DiskInterface* disk_interface, string* err) {
     if (status_known())
       return true;
+    if (precomputed_mtime_ >= 0) {
+      mtime_ = precomputed_mtime_;
+      return true;
+    }
     return Stat(disk_interface, err);
   }
 
   /// Mark as not-yet-stat()ed and not dirty.
   void ResetState() {
     mtime_ = -1;
+    precomputed_mtime_ = -1;
     dirty_ = false;
+    precomputed_dirtiness_ = false;
   }
 
   /// Mark the Node as already-stat()ed and missing.
@@ -72,14 +150,14 @@
     return mtime_ != -1;
   }
 
-  const string& path() const { return path_; }
+  const std::string& path() const { return path_.str(); }
+  const HashedStr& path_hashed() const { return path_; }
   /// Get |path()| but use slash_bits to convert back to original slash styles.
   string PathDecanonicalized() const {
-    return PathDecanonicalized(path_, slash_bits_);
+    return PathDecanonicalized(path_.str(), slash_bits());
   }
   static string PathDecanonicalized(const string& path,
                                     uint64_t slash_bits);
-  uint64_t slash_bits() const { return slash_bits_; }
 
   TimeStamp mtime() const { return mtime_; }
 
@@ -87,44 +165,131 @@
   void set_dirty(bool dirty) { dirty_ = dirty; }
   void MarkDirty() { dirty_ = true; }
 
+  bool precomputed_dirtiness() const { return precomputed_dirtiness_; }
+  void set_precomputed_dirtiness(bool value) { precomputed_dirtiness_ = value; }
+
   Edge* in_edge() const { return in_edge_; }
   void set_in_edge(Edge* edge) { in_edge_ = edge; }
 
   int id() const { return id_; }
   void set_id(int id) { id_ = id; }
 
-  const vector<Edge*>& out_edges() const { return out_edges_; }
-  void AddOutEdge(Edge* edge) { out_edges_.push_back(edge); }
+  // Thread-safe properties.
+  DeclIndex dfs_location() const {
+    return first_reference_.load().dfs_location();
+  }
+  uint64_t slash_bits() const {
+    return first_reference_.load().slash_bits();
+  }
+  void UpdateFirstReference(DeclIndex dfs_location, uint64_t slash_bits) {
+    AtomicUpdateMinimum(&first_reference_, { dfs_location, slash_bits });
+  }
+
+  // Thread-safe properties.
+  bool has_out_edge() const;
+  std::vector<Edge*> GetOutEdges() const;
+  std::vector<Edge*> GetValidationOutEdges() const;
+  void AddOutEdge(Edge* edge);
+  void AddValidationOutEdge(Edge* edge);
+
+  /// Add an out-edge from the dependency scan. This function differs from
+  /// AddOutEdge in several ways:
+  ///  - It uses a simple vector, which is faster for single-threaded use.
+  ///  - It's not thread-safe.
+  ///  - It preserves edge order. Edges added with AddOutEdge come from the
+  ///    manifest and are ordered by their position within the manifest
+  ///    (represented with the edge ID). Dep scan edges, on the other hand,
+  ///    are ordered by a DFS walk from target nodes to their dependencies.
+  ///    (I'm not sure whether this order is practically important.)
+  void AddOutEdgeDepScan(Edge* edge) { dep_scan_out_edges_.push_back(edge); }
 
   void Dump(const char* prefix="") const;
 
-private:
-  string path_;
+  // Used in the inputs debug tool.
+  bool InputsChecked() const { return inputs_checked_; }
+  void MarkInputsChecked() { inputs_checked_ = true; }
 
-  /// Set bits starting from lowest for backslashes that were normalized to
-  /// forward slashes by CanonicalizePath. See |PathDecanonicalized|.
-  uint64_t slash_bits_;
+private:
+  const HashedStr path_;
 
   /// Possible values of mtime_:
   ///   -1: file hasn't been examined
   ///   0:  we looked, and file doesn't exist
   ///   >0: actual file's mtime
-  TimeStamp mtime_;
+  TimeStamp mtime_ = -1;
+
+  /// If this value is >= 0, it represents a precomputed mtime for the node.
+  TimeStamp precomputed_mtime_ = -1;
 
   /// Dirty is true when the underlying file is out-of-date.
   /// But note that Edge::outputs_ready_ is also used in judging which
   /// edges to build.
-  bool dirty_;
+  bool dirty_ = false;
 
-  /// The Edge that produces this Node, or NULL when there is no
-  /// known edge to produce it.
-  Edge* in_edge_;
-
-  /// All Edges that use this Node as an input.
-  vector<Edge*> out_edges_;
+  /// Set to true once the node's stat and command-hash info have been
+  /// precomputed.
+  bool precomputed_dirtiness_ = false;
 
   /// A dense integer id for the node, assigned and used by DepsLog.
-  int id_;
+  int id_ = -1;
+
+  std::atomic<NodeFirstReference> first_reference_;
+
+  Edge* in_edge_ = nullptr;
+
+  struct EdgeList {
+    EdgeList(Edge* edge=nullptr, EdgeList* next=nullptr)
+        : edge(edge), next(next) {}
+
+    Edge* edge = nullptr;
+    EdgeList* next = nullptr;
+  };
+
+  /// All Edges that use this Node as an input. The order of this list is
+  /// non-deterministic. An accessor function sorts it each time it's used.
+  std::atomic<EdgeList*> out_edges_ { nullptr };
+  std::atomic<EdgeList*> validation_out_edges_ { nullptr };
+
+  std::vector<Edge*> dep_scan_out_edges_;
+
+  /// Stores if this node's inputs have been already computed. Used in the
+  /// inputs debug tool.
+  bool inputs_checked_ = false;
+};
+
+struct EdgeEval {
+  enum EvalPhase { kParseTime, kFinalScope };
+  enum EscapeKind { kShellEscape, kDoNotEscape };
+
+  EdgeEval(Edge* edge, EvalPhase eval_phase, EscapeKind escape)
+      : edge_(edge),
+        eval_phase_(eval_phase),
+        escape_in_out_(escape) {}
+
+  /// Looks up the variable and appends its value to the output buffer. Returns
+  /// false on error (i.e. a cycle in rule variable expansion).
+  bool EvaluateVariable(std::string* out_append, const HashedStrView& var,
+                        std::string* err);
+
+  /// There are only a small number of bindings allowed on a rule. If we recurse
+  /// enough times, we're guaranteed to repeat a variable.
+  static constexpr int kEvalRecursionLimit = 16;
+
+private:
+  Edge* edge_ = nullptr;
+
+  EvalPhase eval_phase_ = kFinalScope;
+
+  /// The kind of escaping to do on $in and $out path variables.
+  EscapeKind escape_in_out_ = kShellEscape;
+
+  int recursion_count_ = 0;
+  StringPiece recursion_vars_[kEvalRecursionLimit];
+
+  void AppendPathList(std::string* out_append,
+                      std::vector<Node*>::iterator begin,
+                      std::vector<Node*>::iterator end,
+                      char sep);
 };
 
 /// An edge in the dependency graph; links between Nodes using Rules.
@@ -135,9 +300,15 @@
     VisitDone
   };
 
-  Edge() : rule_(NULL), pool_(NULL), env_(NULL), mark_(VisitNone),
-           outputs_ready_(false), deps_missing_(false),
-           implicit_deps_(0), order_only_deps_(0), implicit_outs_(0) {}
+  struct DepScanInfo {
+    bool valid = false;
+    bool restat = false;
+    bool generator = false;
+    bool deps = false;
+    bool depfile = false;
+    bool phony_output = false;
+    uint64_t command_hash = 0;
+  };
 
   /// Return true if all inputs' in-edges are ready.
   bool AllInputsReady() const;
@@ -145,12 +316,45 @@
   /// Expand all variables in a command and return it as a string.
   /// If incl_rsp_file is enabled, the string will also contain the
   /// full contents of a response file (if applicable)
-  string EvaluateCommand(bool incl_rsp_file = false);
+  bool EvaluateCommand(std::string* out_append, bool incl_rsp_file,
+                       std::string* err);
 
-  /// Returns the shell-escaped value of |key|.
-  string GetBinding(const string& key);
-  bool GetBindingBool(const string& key);
+  /// Convenience method. This method must not be called from a worker thread,
+  /// because it could abort with a fatal error. (For consistency with other
+  /// Get*/Evaluate* methods, a better name might be GetCommand.)
+  std::string EvaluateCommand(bool incl_rsp_file = false);
 
+  /// Attempts to evaluate info needed for scanning dependencies.
+  bool PrecomputeDepScanInfo(std::string* err);
+
+  /// Returns dependency-scanning info or exits with a fatal error. These
+  /// methods must not be called until after the manifest has been loaded.
+  const DepScanInfo& ComputeDepScanInfo();
+  uint64_t GetCommandHash()   { return ComputeDepScanInfo().command_hash; }
+  bool IsRestat()             { return ComputeDepScanInfo().restat;       }
+  bool IsGenerator()          { return ComputeDepScanInfo().generator;    }
+  bool IsPhonyOutput()         { return ComputeDepScanInfo().phony_output;  }
+  bool UsesDepsLog()          { return ComputeDepScanInfo().deps;         }
+  bool UsesDepfile()          { return ComputeDepScanInfo().depfile;      }
+
+  /// Appends the value of |key| to the output buffer. On error, returns false,
+  /// and the content of the output buffer is unspecified.
+  bool EvaluateVariable(std::string* out_append, const HashedStrView& key,
+                        std::string* err,
+                        EdgeEval::EvalPhase phase=EdgeEval::kFinalScope,
+                        EdgeEval::EscapeKind escape=EdgeEval::kShellEscape);
+
+private:
+  std::string GetBindingImpl(const HashedStrView& key,
+                             EdgeEval::EvalPhase phase,
+                             EdgeEval::EscapeKind escape);
+
+public:
+  /// Convenience method for EvaluateVariable. On failure, it issues a fatal
+  /// error. This function must not be called from a worker thread because:
+  ///  - Error reporting should be deterministic, and
+  ///  - Fatal() destructs static globals, which a worker thread could be using.
+  std::string GetBinding(const HashedStrView& key);
   /// Like GetBinding("depfile"), but without shell escaping.
   string GetUnescapedDepfile();
   /// Like GetBinding("rspfile"), but without shell escaping.
@@ -158,15 +362,45 @@
 
   void Dump(const char* prefix="") const;
 
-  const Rule* rule_;
-  Pool* pool_;
+  /// Temporary fields used only during manifest parsing.
+  struct DeferredPathList {
+    enum Type {
+      INPUT = 0,
+      OUTPUT = 1,
+      VALIDATION = 2,
+    };
+
+    DeferredPathList(const char* lexer_pos=nullptr,
+        Type type = INPUT, int count=0)
+        : lexer_pos(lexer_pos), type(type), count(count) {}
+
+    const char* lexer_pos = nullptr;
+    Type type;
+    int count = 0;
+  };
+  struct {
+    StringPiece rule_name;
+    size_t rule_name_diag_pos = 0;
+    size_t final_diag_pos = 0;
+    std::vector<DeferredPathList> deferred_path_lists;
+  } parse_state_;
+
+  RelativePosition pos_;
+  const Rule* rule_ = nullptr;
+  Pool* pool_ = nullptr;
   vector<Node*> inputs_;
   vector<Node*> outputs_;
-  BindingEnv* env_;
-  VisitMark mark_;
-  bool outputs_ready_;
-  bool deps_missing_;
+  vector<Node*> validations_;
+  std::vector<std::pair<HashedStr, std::string>> unevaled_bindings_;
+  VisitMark mark_ = VisitNone;
+  bool precomputed_dirtiness_ = false;
+  size_t id_ = 0;
+  bool outputs_ready_ = false;
+  bool deps_missing_ = false;
+  bool phony_from_depfile_ = false;
+  DepScanInfo dep_scan_info_;
 
+  DeclIndex dfs_location() const { return pos_.dfs_location(); }
   const Rule& rule() const { return *rule_; }
   Pool* pool() const { return pool_; }
   int weight() const { return 1; }
@@ -180,8 +414,9 @@
   //                     don't cause the target to rebuild.
   // These are stored in inputs_ in that order, and we keep counts of
   // #2 and #3 when we need to access the various subsets.
-  int implicit_deps_;
-  int order_only_deps_;
+  int explicit_deps_ = 0;
+  int implicit_deps_ = 0;
+  int order_only_deps_ = 0;
   bool is_implicit(size_t index) {
     return index >= inputs_.size() - order_only_deps_ - implicit_deps_ &&
         !is_order_only(index);
@@ -195,16 +430,32 @@
   // 2) implicit outs, which the target generates but are not part of $out.
   // These are stored in outputs_ in that order, and we keep a count of
   // #2 to use when we need to access the various subsets.
-  int implicit_outs_;
+  int explicit_outs_ = 0;
+  int implicit_outs_ = 0;
   bool is_implicit_out(size_t index) const {
     return index >= outputs_.size() - implicit_outs_;
   }
 
+  int validation_deps_ = 0;
+
   bool is_phony() const;
   bool use_console() const;
   bool maybe_phonycycle_diagnostic() const;
+
+  /// Search for a binding on this edge and append its value to the output
+  /// string. Does not search the enclosing scope. Use other functions to
+  /// include ancestor scopes and rule bindings.
+  bool EvaluateVariableSelfOnly(std::string* out_append,
+                                const HashedStrView& var) const;
 };
 
+struct EdgeCmp {
+  bool operator()(const Edge* a, const Edge* b) const {
+    return a->id_ < b->id_;
+  }
+};
+
+typedef set<Edge*, EdgeCmp> EdgeSet;
 
 /// ImplicitDepLoader loads implicit dependencies, as referenced via the
 /// "depfile" attribute in build files.
@@ -254,17 +505,30 @@
 struct DependencyScan {
   DependencyScan(State* state, BuildLog* build_log, DepsLog* deps_log,
                  DiskInterface* disk_interface,
-                 DepfileParserOptions const* depfile_parser_options)
+                 DepfileParserOptions const* depfile_parser_options,
+                 bool missing_phony_is_err)
       : build_log_(build_log),
         disk_interface_(disk_interface),
-        dep_loader_(state, deps_log, disk_interface, depfile_parser_options) {}
+        dep_loader_(state, deps_log, disk_interface, depfile_parser_options),
+        missing_phony_is_err_(missing_phony_is_err) {}
 
-  /// Update the |dirty_| state of the given node by inspecting its input edge.
+  /// Used for tests.
+  bool RecomputeDirty(Node* node, std::vector<Node*>* validation_nodes,
+      std::string* err) {
+    std::vector<Node*> nodes = {node};
+    return RecomputeNodesDirty(nodes, validation_nodes, err);
+  }
+
+  /// Update the |dirty_| state of the given nodes by transitively inspecting
+  /// their input edges.
   /// Examine inputs, outputs, and command lines to judge whether an edge
   /// needs to be re-run, and update outputs_ready_ and each outputs' |dirty_|
   /// state accordingly.
+  /// Appends any validation nodes found to the nodes parameter.
   /// Returns false on failure.
-  bool RecomputeDirty(Node* node, string* err);
+  bool RecomputeNodesDirty(const std::vector<Node*>& initial_nodes,
+                           std::vector<Node*>* validation_nodes,
+                           std::string* err);
 
   /// Recompute whether any output of the edge is dirty, if so sets |*dirty|.
   /// Returns false on failure.
@@ -283,17 +547,32 @@
   }
 
  private:
-  bool RecomputeDirty(Node* node, vector<Node*>* stack, string* err);
+  /// Find the transitive closure of edges and nodes that the given node depends
+  /// on. Each Node and Edge is guaranteed to appear at most once in an output
+  /// vector. The returned lists are not guaranteed to be a superset or a subset
+  /// of the nodes and edges that RecomputeNodesDirty will initialize.
+  void CollectPrecomputeLists(Node* node, std::vector<Node*>* nodes,
+                              std::vector<Edge*>* edges);
+
+  bool PrecomputeNodesDirty(const std::vector<Node*>& nodes,
+                            const std::vector<Edge*>& edges,
+                            ThreadPool* thread_pool, std::string* err);
+
+  bool RecomputeNodeDirty(Node* node, vector<Node*>* stack,
+                          vector<Node*>* validation_nodes, string* err);
+
   bool VerifyDAG(Node* node, vector<Node*>* stack, string* err);
 
   /// Recompute whether a given single output should be marked dirty.
   /// Returns true if so.
   bool RecomputeOutputDirty(Edge* edge, Node* most_recent_input,
-                            const string& command, Node* output);
+                            uint64_t command_hash, Node* output);
 
   BuildLog* build_log_;
   DiskInterface* disk_interface_;
   ImplicitDepLoader dep_loader_;
+
+  bool missing_phony_is_err_;
 };
 
 #endif  // NINJA_GRAPH_H_
diff --git a/src/graph_test.cc b/src/graph_test.cc
index 4a66831..a957513 100644
--- a/src/graph_test.cc
+++ b/src/graph_test.cc
@@ -18,7 +18,7 @@
 #include "test.h"
 
 struct GraphTest : public StateTestWithBuiltinRules {
-  GraphTest() : scan_(&state_, NULL, NULL, &fs_, NULL) {}
+  GraphTest() : scan_(&state_, NULL, NULL, &fs_, NULL, false) {}
 
   VirtualFileSystem fs_;
   DependencyScan scan_;
@@ -31,7 +31,7 @@
   fs_.Create("out", "");
 
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
   ASSERT_EQ("", err);
 
   // A missing implicit dep *should* make the output dirty.
@@ -49,7 +49,7 @@
   fs_.Create("implicit", "");
 
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
   ASSERT_EQ("", err);
 
   // A modified implicit dep should make the output dirty.
@@ -69,7 +69,7 @@
   fs_.Create("implicit.h", "");
 
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), NULL, &err));
   ASSERT_EQ("", err);
 
   // implicit.h has changed, though our depfile refers to it with a
@@ -92,7 +92,7 @@
   fs_.Create("data", "");
 
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), NULL, &err));
   ASSERT_EQ("", err);
 
   // We have both an implicit and an explicit dep on implicit.h.
@@ -120,7 +120,7 @@
   fs_.Create("out", "");
 
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
   ASSERT_EQ("", err);
 
   EXPECT_TRUE(GetNode("out")->dirty());
@@ -136,7 +136,7 @@
   fs_.Create("out", "");
 
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
   ASSERT_EQ("", err);
 
   EXPECT_TRUE(GetNode("out")->dirty());
@@ -160,7 +160,7 @@
   fs_.Create("in", "");
 
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.imp"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.imp"), NULL, &err));
   ASSERT_EQ("", err);
 
   EXPECT_TRUE(GetNode("out.imp")->dirty());
@@ -174,7 +174,7 @@
   fs_.Create("in", "");
 
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.imp"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.imp"), NULL, &err));
   ASSERT_EQ("", err);
 
   EXPECT_TRUE(GetNode("out.imp")->dirty());
@@ -191,7 +191,7 @@
   fs_.Create("out.o", "");
 
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), NULL, &err));
   ASSERT_EQ("", err);
 
   EXPECT_FALSE(GetNode("out.o")->dirty());
@@ -239,7 +239,7 @@
   fs_.Create("out.o", "");
 
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), NULL, &err));
   ASSERT_EQ("", err);
 
   EXPECT_FALSE(GetNode("out.o")->dirty());
@@ -259,13 +259,13 @@
   fs_.Create("out.o", "");
 
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), NULL, &err));
   ASSERT_EQ("", err);
   EXPECT_FALSE(GetNode("out.o")->dirty());
 
   state_.Reset();
   fs_.RemoveFile("out.o.d");
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out.o"), NULL, &err));
   ASSERT_EQ("", err);
   EXPECT_TRUE(GetNode("out.o")->dirty());
 }
@@ -312,7 +312,7 @@
 "build n2: phony n1\n"
   );
   string err;
-  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("n2"), &err));
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("n2"), NULL, &err));
   ASSERT_EQ("", err);
 
   Plan plan_;
@@ -331,10 +331,190 @@
   parser_opts);
 
   string err;
-  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("a"), &err));
+  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("a"), NULL, &err));
   ASSERT_EQ("dependency cycle: a -> a [-w phonycycle=err]", err);
 }
 
+TEST_F(GraphTest, OutputSymlinkSourceUpdate) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build other: cat\n"
+"build sym: cat\n"
+"build out: cat | sym\n"));
+
+  fs_.Create("out", "");
+  fs_.CreateSymlink("sym", "other");
+  fs_.Tick();
+  fs_.Create("other", "");
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_FALSE(GetNode("out")->dirty());
+}
+
+TEST_F(GraphTest, OutputSymlinkDangling) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build sym: cat\n"
+"build out: cat | sym\n"));
+
+  fs_.CreateSymlink("sym", "dangling");
+  fs_.Create("out", "");
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_FALSE(GetNode("out")->dirty());
+}
+
+TEST_F(GraphTest, InputSymlink) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build out: cat sym\n"));
+
+  fs_.Create("out", "");
+  fs_.CreateSymlink("sym", "in");
+  fs_.Tick();
+  fs_.Create("in", "");
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(GetNode("out")->dirty());
+}
+
+TEST_F(GraphTest, InputSymlinkUpdate) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build out: cat sym\n"));
+
+  fs_.Create("out", "");
+  fs_.Create("in", "");
+  fs_.Tick();
+  fs_.CreateSymlink("sym", "in");
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
+  ASSERT_EQ("", err);
+
+  // This can be incorrect if the destination of the symlink changed to
+  // a file with an equal or older timestamp.
+  EXPECT_FALSE(GetNode("out")->dirty());
+}
+
+TEST_F(GraphTest, InputSymlinkDangling) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build out: cat sym\n"));
+
+  fs_.Create("out", "");
+  fs_.CreateSymlink("sym", "in");
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(GetNode("out")->dirty());
+}
+
+TEST_F(GraphTest, InputDirectoryUpToDate) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build out: cat inputs\n"));
+
+  fs_.Create("out", "");
+  EXPECT_TRUE(fs_.MakeDir("inputs"));
+  EXPECT_TRUE(fs_.WriteFile("inputs/foo", ""));
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_FALSE(GetNode("out")->dirty());
+}
+
+TEST_F(GraphTest, InputDirectoryChanged) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build out: cat inputs\n"));
+
+  fs_.Create("out", "");
+  EXPECT_TRUE(fs_.MakeDir("inputs"));
+  fs_.Tick();
+  EXPECT_TRUE(fs_.WriteFile("inputs/foo", ""));
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(GetNode("out")->dirty());
+}
+
+TEST_F(GraphTest, PhonyOutput) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+"  command = echo ${out}\n"
+"  phony_output = true\n"
+"build foo: phony_out\n"));
+
+  Node* node = state_.LookupNode("foo");
+  Edge* edge = node->in_edge();
+  ASSERT_TRUE(edge->IsPhonyOutput());
+}
+
+TEST_F(GraphTest, PhonyOutputDependsOnPhonyOutput) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+"  command = echo ${out}\n"
+"  phony_output = true\n"
+"build foo: phony_out\n"
+"build bar: phony_out foo\n"));
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("bar"), NULL, &err));
+  ASSERT_EQ("", err);
+}
+
+TEST_F(GraphTest, RealDependsOnPhonyOutput) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+"  command = echo ${out}\n"
+"  phony_output = true\n"
+"rule touch\n"
+"  command = touch ${out}\n"
+"build foo: phony_out\n"
+"build bar: touch foo\n"));
+
+  string err;
+  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("bar"), NULL, &err));
+  EXPECT_EQ("real file 'bar' depends on phony output 'foo'\n", err);
+}
+
+TEST_F(GraphTest, PhonyDependsOnPhonyOutput) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+"  command = echo ${out}\n"
+"  phony_output = true\n"
+"build foo: phony_out\n"
+"build bar: phony foo\n"));
+
+  string err;
+  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("bar"), NULL, &err));
+  EXPECT_EQ("real file 'bar' depends on phony output 'foo'\n", err);
+}
+
+TEST_F(GraphTest, MissingPhonyWithPhonyOutputs) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build foo: phony\n"));
+
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("foo"), NULL, &err));
+  EXPECT_EQ("", err);
+  EXPECT_TRUE(GetNode("foo")->dirty());
+
+  state_.Reset();
+  DependencyScan scan(&state_, NULL, NULL, &fs_, NULL, true);
+  EXPECT_FALSE(scan.RecomputeDirty(GetNode("foo"), NULL, &err));
+  EXPECT_EQ("output foo of phony edge doesn't exist. Missing 'phony_output = true'?", err);
+}
+
 TEST_F(GraphTest, DependencyCycle) {
   AssertParse(&state_,
 "build out: cat mid\n"
@@ -343,7 +523,7 @@
 "build pre: cat out\n");
 
   string err;
-  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("out"), &err));
+  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("out"), NULL, &err));
   ASSERT_EQ("dependency cycle: out -> mid -> in -> pre -> out", err);
 }
 
@@ -351,7 +531,7 @@
   string err;
   AssertParse(&state_,
 "build a b: cat a\n");
-  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("b"), &err));
+  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("b"), NULL, &err));
   ASSERT_EQ("dependency cycle: a -> a", err);
 }
 
@@ -359,7 +539,7 @@
   string err;
   ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
 "build b a: cat a\n"));
-  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("b"), &err));
+  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("b"), NULL, &err));
   ASSERT_EQ("dependency cycle: a -> a", err);
 }
 
@@ -368,7 +548,7 @@
   ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
 "build a b: cat c\n"
 "build c: cat a\n"));
-  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("b"), &err));
+  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("b"), NULL, &err));
   ASSERT_EQ("dependency cycle: a -> c -> a", err);
 }
 
@@ -380,7 +560,7 @@
 "build b: cat a\n"
 "build a e: cat d\n"
 "build f: cat e\n"));
-  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("f"), &err));
+  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("f"), NULL, &err));
   ASSERT_EQ("dependency cycle: a -> d -> c -> b -> a", err);
 }
 
@@ -396,7 +576,7 @@
   fs_.Create("dep.d", "a: b\n");
 
   string err;
-  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("a"), &err));
+  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("a"), NULL, &err));
   ASSERT_EQ("dependency cycle: b -> b", err);
 
   // Despite the depfile causing edge to be a cycle (it has outputs a and b,
@@ -421,7 +601,7 @@
   fs_.Create("dep.d", "a: c\n");
 
   string err;
-  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("a"), &err));
+  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("a"), NULL, &err));
   ASSERT_EQ("dependency cycle: b -> c -> b", err);
 
   // Despite the depfile causing edge to be a cycle (|edge| has outputs a and b,
@@ -448,7 +628,7 @@
   fs_.Create("dep.d", "a: c\n");
 
   string err;
-  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("d"), &err));
+  EXPECT_FALSE(scan_.RecomputeDirty(GetNode("d"), NULL, &err));
   ASSERT_EQ("dependency cycle: b -> c -> b", err);
 
   // Despite the depfile causing edge to be a cycle (|edge| has outputs a and b,
@@ -479,3 +659,68 @@
   EXPECT_EQ(root_nodes[3]->PathDecanonicalized(), "out4\\foo");
 }
 #endif
+
+TEST_F(GraphTest, EdgeVarEvalPhase) {
+  // Variable lookups on edges can happen in one of two phases:
+  //  - Parse-time: Only bindings declared before the reference are visible.
+  //  - Final-scope: All bindings in the current scope (and ancestor scopes)
+  //    are visible.
+  //
+  // An edge's pool is determined at parse-time, while most other bindings are
+  // looked up after manifest parsing is finished.
+
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"foo = A\n"
+"bar = B\n"
+"pool A\n"
+"  depth = 3\n"
+"pool C\n"
+"  depth = 3\n"
+"rule echo\n"
+"  command = replaced by next line\n"
+"  command = echo $foo,$bar\n"
+"  pool = $foo\n"
+"build a: echo\n"
+"  bar = replaced by next line\n"
+"  bar = edge:$foo\n"
+"foo = C\n"
+"bar = D\n"));
+
+  Edge* edge = GetNode("a")->in_edge();
+  EXPECT_EQ("echo C,edge:A", edge->GetBinding("command"));
+  EXPECT_EQ("A", edge->pool()->name());
+  EXPECT_EQ("C", edge->GetBinding("pool"));
+}
+
+TEST_F(GraphTest, PhonyOutputAlwaysDirty) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"rule phony_out\n"
+"  command = echo ${out}\n"
+"  phony_output = true\n"
+"build foo: phony_out\n"));
+
+  fs_.Create("foo", "");
+  string err;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("foo"), NULL, &err));
+  ASSERT_EQ("", err);
+
+  EXPECT_TRUE(GetNode("foo")->dirty());
+}
+
+TEST_F(GraphTest, Validation) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(&state_,
+"build out: cat in |@ validate\n"
+"build validate: cat in\n"));
+
+  fs_.Create("in", "");
+  string err;
+  std::vector<Node*> validation_nodes;
+  EXPECT_TRUE(scan_.RecomputeDirty(GetNode("out"), &validation_nodes, &err));
+  ASSERT_EQ("", err);
+
+  ASSERT_EQ(validation_nodes.size(), 1);
+  EXPECT_EQ(validation_nodes[0]->path(), "validate");
+
+  EXPECT_TRUE(GetNode("out")->dirty());
+  EXPECT_TRUE(GetNode("validate")->dirty());
+}
diff --git a/src/graphviz.h b/src/graphviz.h
index 408496d..6dd08be 100644
--- a/src/graphviz.h
+++ b/src/graphviz.h
@@ -17,6 +17,8 @@
 
 #include <set>
 
+#include "graph.h"
+
 struct Node;
 struct Edge;
 
@@ -27,7 +29,7 @@
   void Finish();
 
   std::set<Node*> visited_nodes_;
-  std::set<Edge*> visited_edges_;
+  EdgeSet visited_edges_;
 };
 
 #endif  // NINJA_GRAPHVIZ_H_
diff --git a/src/hash_map.h b/src/hash_map.h
index 55d2c9d..717a7c9 100644
--- a/src/hash_map.h
+++ b/src/hash_map.h
@@ -12,12 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef NINJA_MAP_H_
-#define NINJA_MAP_H_
+#ifndef NINJA_HASH_MAP_H_
+#define NINJA_HASH_MAP_H_
 
-#include <algorithm>
 #include <string.h>
-#include "string_piece.h"
 #include "util.h"
 
 // MurmurHash2, by Austin Appleby
@@ -53,71 +51,4 @@
   return h;
 }
 
-#if (__cplusplus >= 201103L) || (_MSC_VER >= 1900)
-#include <unordered_map>
-
-namespace std {
-template<>
-struct hash<StringPiece> {
-  typedef StringPiece argument_type;
-  typedef size_t result_type;
-
-  size_t operator()(StringPiece key) const {
-    return MurmurHash2(key.str_, key.len_);
-  }
-};
-}
-
-#elif defined(_MSC_VER)
-#include <hash_map>
-
-using stdext::hash_map;
-using stdext::hash_compare;
-
-struct StringPieceCmp : public hash_compare<StringPiece> {
-  size_t operator()(const StringPiece& key) const {
-    return MurmurHash2(key.str_, key.len_);
-  }
-  bool operator()(const StringPiece& a, const StringPiece& b) const {
-    int cmp = memcmp(a.str_, b.str_, min(a.len_, b.len_));
-    if (cmp < 0) {
-      return true;
-    } else if (cmp > 0) {
-      return false;
-    } else {
-      return a.len_ < b.len_;
-    }
-  }
-};
-
-#else
-#include <ext/hash_map>
-
-using __gnu_cxx::hash_map;
-
-namespace __gnu_cxx {
-template<>
-struct hash<StringPiece> {
-  size_t operator()(StringPiece key) const {
-    return MurmurHash2(key.str_, key.len_);
-  }
-};
-}
-#endif
-
-/// A template for hash_maps keyed by a StringPiece whose string is
-/// owned externally (typically by the values).  Use like:
-/// ExternalStringHash<Foo*>::Type foos; to make foos into a hash
-/// mapping StringPiece => Foo*.
-template<typename V>
-struct ExternalStringHashMap {
-#if (__cplusplus >= 201103L) || (_MSC_VER >= 1900)
-  typedef std::unordered_map<StringPiece, V> Type;
-#elif defined(_MSC_VER)
-  typedef hash_map<StringPiece, V, StringPieceCmp> Type;
-#else
-  typedef hash_map<StringPiece, V> Type;
-#endif
-};
-
-#endif // NINJA_MAP_H_
+#endif // NINJA_HASH_MAP_H_
diff --git a/src/hashed_str_view.h b/src/hashed_str_view.h
new file mode 100644
index 0000000..ea19a4f
--- /dev/null
+++ b/src/hashed_str_view.h
@@ -0,0 +1,163 @@
+// Copyright 2018 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef NINJA_HASHED_STR_VIEW_
+#define NINJA_HASHED_STR_VIEW_
+
+#include <stdint.h>
+
+#include "string_piece.h"
+
+class HashedStr;
+
+using StrHashType = size_t;
+
+class HashedStrView {
+public:
+  constexpr HashedStrView() {}
+
+  HashedStrView(const char* str) : str_(str), hash_(HashStr(str_)) {}
+  HashedStrView(const std::string& str) : str_(str), hash_(HashStr(str_)) {}
+  HashedStrView(StringPiece str) : str_(str), hash_(HashStr(str_)) {}
+  inline HashedStrView(const HashedStr& str);
+
+  // Bypass the hashing step when necessary for better performance.
+  explicit HashedStrView(const StringPiece& str, StrHashType hash)
+      : str_(str), hash_(hash) {}
+
+  const StringPiece& str_view() const { return str_; }
+
+  StrHashType hash() const { return hash_; }
+  const char* data() const { return str_.data(); }
+  size_t size() const { return str_.size(); }
+  bool empty() const { return str_.empty(); }
+
+private:
+  // Reversing the order of these fields would break the constructors, most of
+  // which initialize hash_ using str_.
+  StringPiece str_;
+  StrHashType hash_ = 0;
+};
+
+class HashedStr {
+public:
+  HashedStr() {}
+
+  HashedStr(const char* str) : str_(str), hash_(HashStr(str_)) {}
+  HashedStr(const std::string& str) : str_(str), hash_(HashStr(str_)) {}
+  HashedStr(std::string&& str) : str_(std::move(str)), hash_(HashStr(str_)) {}
+  explicit HashedStr(StringPiece str) : str_(NonNullData(str), str.size()), hash_(HashStr(str_)) {}
+  explicit HashedStr(const HashedStrView& str) : str_(NonNullData(str.str_view()), str.size()), hash_(str.hash()) {}
+
+  HashedStr& operator=(std::string&& str) {
+    str_ = std::move(str);
+    hash_ = HashStr(str_);
+    return *this;
+  }
+
+  HashedStr& operator=(const std::string& str) {
+    str_ = str;
+    hash_ = HashStr(str_);
+    return *this;
+  }
+
+  HashedStr& operator=(StringPiece str) {
+    str_.assign(NonNullData(str), str.size());
+    hash_ = HashStr(str_);
+    return *this;
+  }
+
+  HashedStr& operator=(const HashedStrView& str) {
+    str_.assign(NonNullData(str.str_view()), str.size());
+    hash_ = str.hash();
+    return *this;
+  }
+
+  const std::string& str() const { return str_; }
+
+  StrHashType hash() const { return hash_; }
+  const char* c_str() const { return str_.c_str(); }
+  const char* data() const { return str_.data(); }
+  size_t size() const { return str_.size(); }
+  bool empty() const { return str_.empty(); }
+
+private:
+  static const char* NonNullData(const StringPiece& piece) {
+    return piece.data() == nullptr ? "" : piece.data();
+  }
+
+  // Reversing the order of these fields would break the constructors, most of
+  // which initialize hash_ using str_.
+  std::string str_;
+  StrHashType hash_ = 0;
+};
+
+inline HashedStrView::HashedStrView(const HashedStr& str) : str_(str.str()), hash_(str.hash()) {}
+
+inline bool operator==(const HashedStr& x, const HashedStr& y) {
+  return x.hash() == y.hash() && x.str() == y.str();
+}
+
+inline bool operator==(const HashedStr& x, const HashedStrView& y) {
+  return x.hash() == y.hash() && x.str() == y.str_view();
+}
+
+inline bool operator==(const HashedStrView& x, const HashedStr& y) {
+  return x.hash() == y.hash() && x.str_view() == y.str();
+}
+
+inline bool operator==(const HashedStrView& x, const HashedStrView& y) {
+  return x.hash() == y.hash() && x.str_view() == y.str_view();
+}
+
+inline bool operator!=(const HashedStr& x, const HashedStr& y) { return !(x == y); }
+inline bool operator!=(const HashedStr& x, const HashedStrView& y) { return !(x == y); }
+inline bool operator!=(const HashedStrView& x, const HashedStr& y) { return !(x == y); }
+inline bool operator!=(const HashedStrView& x, const HashedStrView& y) { return !(x == y); }
+
+namespace std {
+  template<> struct hash<HashedStrView> {
+    size_t operator()(const HashedStrView& x) const { return x.hash(); }
+  };
+
+  template<> struct hash<HashedStr> {
+    size_t operator()(const HashedStr& x) const { return x.hash(); }
+  };
+}
+
+// The comparison operators ignore the hash code. For efficiency, the
+// ConcurrentHashMap orders keys by their hash code first, then by their
+// operator< functions.
+
+inline bool operator<(const HashedStr& x, const HashedStr& y) { return x.str() < y.str(); }
+inline bool operator>(const HashedStr& x, const HashedStr& y) { return x.str() > y.str(); }
+inline bool operator<=(const HashedStr& x, const HashedStr& y) { return x.str() <= y.str(); }
+inline bool operator>=(const HashedStr& x, const HashedStr& y) { return x.str() >= y.str(); }
+
+inline bool operator<(const HashedStr& x, const HashedStrView& y) { return x.str() < y.str_view(); }
+inline bool operator>(const HashedStr& x, const HashedStrView& y) { return x.str() > y.str_view(); }
+inline bool operator<=(const HashedStr& x, const HashedStrView& y) { return x.str() <= y.str_view(); }
+inline bool operator>=(const HashedStr& x, const HashedStrView& y) { return x.str() >= y.str_view(); }
+
+inline bool operator<(const HashedStrView& x, const HashedStr& y) { return x.str_view() < y.str(); }
+inline bool operator>(const HashedStrView& x, const HashedStr& y) { return x.str_view() > y.str(); }
+inline bool operator<=(const HashedStrView& x, const HashedStr& y) { return x.str_view() <= y.str(); }
+inline bool operator>=(const HashedStrView& x, const HashedStr& y) { return x.str_view() >= y.str(); }
+
+inline bool operator<(const HashedStrView& x, const HashedStrView& y) { return x.str_view() < y.str_view(); }
+inline bool operator>(const HashedStrView& x, const HashedStrView& y) { return x.str_view() > y.str_view(); }
+inline bool operator<=(const HashedStrView& x, const HashedStrView& y) { return x.str_view() <= y.str_view(); }
+inline bool operator>=(const HashedStrView& x, const HashedStrView& y) { return x.str_view() >= y.str_view(); }
+
+#endif  // NINJA_HASHED_STR_VIEW_
diff --git a/src/lexer.cc b/src/lexer.cc
index 35ae97b..a5c3c27 100644
--- a/src/lexer.cc
+++ b/src/lexer.cc
@@ -1,4 +1,4 @@
-/* Generated by re2c 0.16 */
+/* Generated by re2c 1.3 */
 // Copyright 2011 Google Inc. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,24 +18,79 @@
 #include <stdio.h>
 
 #include "eval_env.h"
+#include "graph.h"
 #include "util.h"
 
-bool Lexer::Error(const string& message, string* err) {
+size_t AdvanceToNextManifestChunk(StringPiece content, size_t idx) {
+  assert(idx <= content.size());
+
+  // Iterate over each LF in the manifest, starting at the given index.
+  while (true) {
+    const void* next_line = memchr(content.data() + idx, '\n',
+                                   content.size() - idx);
+    if (next_line == nullptr) {
+      break;
+    }
+    idx = static_cast<const char*>(next_line) - content.data();
+    ++idx; // step over the LF
+
+    // The line must not be preceded by a line continuator. This logic can
+    // filter out more split candidates than strictly necessary:
+    //  - The preceding line could have a comment that ends with a "$": "# $\n"
+    //  - The preceding line could end with an escaped-dollar: "X=$$\n"
+    if ((idx >= 2 && content.substr(idx - 2, 2) == "$\n") ||
+        (idx >= 3 && content.substr(idx - 3, 3) == "$\r\n")) {
+      continue;
+    }
+
+    // Skip an indented line or a comment line, either of which could be part of
+    // an earlier declaration. Ninja allows unindented comments (as well as
+    // indented comments) inside a binding block, e.g.:
+    //
+    //   build foo: cc
+    //   # comment-line
+    //     pool = link_pool
+    //
+    // Ninja doesn't allow blank lines in a binding block. This code could
+    // probably allow a chunk to start with a blank line, but it seems better if
+    // it doesn't.
+    if (idx >= content.size() ||
+        content[idx] == ' ' || content[idx] == '#' ||
+        content[idx] == '\r' || content[idx] == '\n') {
+      continue;
+    }
+
+    return idx;
+  }
+
+  return content.size();
+}
+
+bool DecorateErrorWithLocation(const std::string& filename,
+                               const char* file_start,
+                               size_t file_offset,
+                               const std::string& message,
+                               std::string* err) {
+  // Make a copy in case message and err alias.
+  std::string message_tmp = message;
+
   // Compute line/column.
   int line = 1;
-  const char* line_start = input_.str_;
-  for (const char* p = input_.str_; p < last_token_; ++p) {
+  const char* line_start = file_start;
+  const char* file_pos = file_start + file_offset;
+
+  for (const char* p = line_start; p < file_pos; ++p) {
     if (*p == '\n') {
       ++line;
       line_start = p + 1;
     }
   }
-  int col = last_token_ ? (int)(last_token_ - line_start) : 0;
+  int col = (int)(file_pos - line_start);
 
   char buf[1024];
-  snprintf(buf, sizeof(buf), "%s:%d: ", filename_.AsString().c_str(), line);
+  snprintf(buf, sizeof(buf), "%s:%d: ", filename.c_str(), line);
   *err = buf;
-  *err += message + "\n";
+  *err += message_tmp + "\n";
 
   // Add some context to the message.
   const int kTruncateColumn = 72;
@@ -59,15 +114,16 @@
   return false;
 }
 
-Lexer::Lexer(const char* input) {
-  Start("input", input);
+bool Lexer::Error(const std::string& message, std::string* err) {
+  return DecorateErrorWithLocation(filename_, input_.data(),
+                                   GetLastTokenOffset(), message, err);
 }
 
-void Lexer::Start(StringPiece filename, StringPiece input) {
-  filename_ = filename;
-  input_ = input;
-  ofs_ = input_.str_;
-  last_token_ = NULL;
+bool Lexer::UnexpectedNulError(const char* pos, std::string* err) {
+  assert(*pos == '\0');
+  const char* msg = (pos == EndOfFile()) ? "unexpected EOF"
+                                         : "unexpected NUL byte";
+  return Error(msg, err);
 }
 
 const char* Lexer::TokenName(Token t) {
@@ -83,9 +139,11 @@
   case NEWLINE:  return "newline";
   case PIPE2:    return "'||'";
   case PIPE:     return "'|'";
+  case PIPEAT:   return "'|@'";
   case POOL:     return "'pool'";
   case RULE:     return "'rule'";
   case SUBNINJA: return "'subninja'";
+  case TNUL:     return "nul byte";
   case TEOF:     return "eof";
   }
   return NULL;  // not reached
@@ -117,6 +175,7 @@
 Lexer::Token Lexer::ReadToken() {
   const char* p = ofs_;
   const char* q;
+  const char* r;
   const char* start;
   Lexer::Token token;
   for (;;) {
@@ -127,7 +186,7 @@
 	unsigned int yyaccept = 0;
 	static const unsigned char yybm[] = {
 		  0, 128, 128, 128, 128, 128, 128, 128, 
-		128, 128,   0, 128, 128, 128, 128, 128, 
+		128, 128,   0, 128, 128,   0, 128, 128, 
 		128, 128, 128, 128, 128, 128, 128, 128, 
 		128, 128, 128, 128, 128, 128, 128, 128, 
 		160, 128, 128, 128, 128, 128, 128, 128, 
@@ -219,7 +278,7 @@
 	}
 yy2:
 	++p;
-	{ token = TEOF;     break; }
+	{ token = (start == EndOfFile()) ? TEOF : TNUL; break; }
 yy4:
 	++p;
 yy5:
@@ -229,20 +288,19 @@
 	{ token = NEWLINE;  break; }
 yy8:
 	yych = *++p;
-	if (yych == '\n') goto yy28;
+	if (yych == '\n') goto yy6;
 	goto yy5;
 yy9:
 	yyaccept = 0;
-	q = ++p;
-	yych = *p;
+	yych = *(q = ++p);
 	if (yybm[0+yych] & 32) {
 		goto yy9;
 	}
 	if (yych <= '\f') {
 		if (yych == '\n') goto yy6;
 	} else {
-		if (yych <= '\r') goto yy30;
-		if (yych == '#') goto yy32;
+		if (yych <= '\r') goto yy28;
+		if (yych == '#') goto yy30;
 	}
 yy11:
 	{ token = INDENT;   break; }
@@ -250,10 +308,9 @@
 	yyaccept = 1;
 	yych = *(q = ++p);
 	if (yych <= 0x00) goto yy5;
-	goto yy33;
+	goto yy31;
 yy13:
-	++p;
-	yych = *p;
+	yych = *++p;
 yy14:
 	if (yybm[0+yych] & 64) {
 		goto yy13;
@@ -267,93 +324,100 @@
 	{ token = EQUALS;   break; }
 yy20:
 	yych = *++p;
-	if (yych == 'u') goto yy36;
+	if (yych == 'u') goto yy35;
 	goto yy14;
 yy21:
 	yych = *++p;
-	if (yych == 'e') goto yy37;
+	if (yych == 'e') goto yy36;
 	goto yy14;
 yy22:
 	yych = *++p;
-	if (yych == 'n') goto yy38;
+	if (yych == 'n') goto yy37;
 	goto yy14;
 yy23:
 	yych = *++p;
-	if (yych == 'o') goto yy39;
+	if (yych == 'o') goto yy38;
 	goto yy14;
 yy24:
 	yych = *++p;
-	if (yych == 'u') goto yy40;
+	if (yych == 'u') goto yy39;
 	goto yy14;
 yy25:
 	yych = *++p;
-	if (yych == 'u') goto yy41;
+	if (yych == 'u') goto yy40;
 	goto yy14;
 yy26:
-	++p;
-	if ((yych = *p) == '|') goto yy42;
+	yych = *++p;
+	if (yych == '@') goto yy41;
+	if (yych == '|') goto yy43;
 	{ token = PIPE;     break; }
 yy28:
-	++p;
-	{ token = NEWLINE;  break; }
-yy30:
 	yych = *++p;
-	if (yych == '\n') goto yy28;
-yy31:
+	if (yych == '\n') goto yy6;
+yy29:
 	p = q;
 	if (yyaccept == 0) {
 		goto yy11;
 	} else {
 		goto yy5;
 	}
+yy30:
+	yych = *++p;
+yy31:
+	if (yybm[0+yych] & 128) {
+		goto yy30;
+	}
+	if (yych <= 0x00) goto yy29;
+	if (yych >= '\v') {
+		r = p;
+		goto yy34;
+	}
+	r = p;
 yy32:
 	++p;
-	yych = *p;
-yy33:
-	if (yybm[0+yych] & 128) {
-		goto yy32;
-	}
-	if (yych <= 0x00) goto yy31;
-	++p;
+	p = r;
 	{ continue; }
+yy34:
+	yych = *++p;
+	if (yych == '\n') goto yy32;
+	goto yy29;
+yy35:
+	yych = *++p;
+	if (yych == 'i') goto yy45;
+	goto yy14;
 yy36:
 	yych = *++p;
-	if (yych == 'i') goto yy44;
+	if (yych == 'f') goto yy46;
 	goto yy14;
 yy37:
 	yych = *++p;
-	if (yych == 'f') goto yy45;
+	if (yych == 'c') goto yy47;
 	goto yy14;
 yy38:
 	yych = *++p;
-	if (yych == 'c') goto yy46;
+	if (yych == 'o') goto yy48;
 	goto yy14;
 yy39:
 	yych = *++p;
-	if (yych == 'o') goto yy47;
+	if (yych == 'l') goto yy49;
 	goto yy14;
 yy40:
 	yych = *++p;
-	if (yych == 'l') goto yy48;
+	if (yych == 'b') goto yy50;
 	goto yy14;
 yy41:
-	yych = *++p;
-	if (yych == 'b') goto yy49;
-	goto yy14;
-yy42:
+	++p;
+	{ token = PIPEAT;   break; }
+yy43:
 	++p;
 	{ token = PIPE2;    break; }
-yy44:
-	yych = *++p;
-	if (yych == 'l') goto yy50;
-	goto yy14;
 yy45:
 	yych = *++p;
-	if (yych == 'a') goto yy51;
+	if (yych == 'l') goto yy51;
 	goto yy14;
 yy46:
 	yych = *++p;
-	if (yych == 'l') goto yy52;
+	if (yych == 'a') goto yy52;
 	goto yy14;
 yy47:
 	yych = *++p;
@@ -361,87 +425,91 @@
 	goto yy14;
 yy48:
 	yych = *++p;
-	if (yych == 'e') goto yy55;
+	if (yych == 'l') goto yy54;
 	goto yy14;
 yy49:
 	yych = *++p;
-	if (yych == 'n') goto yy57;
+	if (yych == 'e') goto yy56;
 	goto yy14;
 yy50:
 	yych = *++p;
-	if (yych == 'd') goto yy58;
+	if (yych == 'n') goto yy58;
 	goto yy14;
 yy51:
 	yych = *++p;
-	if (yych == 'u') goto yy60;
+	if (yych == 'd') goto yy59;
 	goto yy14;
 yy52:
 	yych = *++p;
 	if (yych == 'u') goto yy61;
 	goto yy14;
 yy53:
-	++p;
-	if (yybm[0+(yych = *p)] & 64) {
+	yych = *++p;
+	if (yych == 'u') goto yy62;
+	goto yy14;
+yy54:
+	yych = *++p;
+	if (yybm[0+yych] & 64) {
 		goto yy13;
 	}
 	{ token = POOL;     break; }
-yy55:
-	++p;
-	if (yybm[0+(yych = *p)] & 64) {
+yy56:
+	yych = *++p;
+	if (yybm[0+yych] & 64) {
 		goto yy13;
 	}
 	{ token = RULE;     break; }
-yy57:
-	yych = *++p;
-	if (yych == 'i') goto yy62;
-	goto yy14;
 yy58:
-	++p;
-	if (yybm[0+(yych = *p)] & 64) {
+	yych = *++p;
+	if (yych == 'i') goto yy63;
+	goto yy14;
+yy59:
+	yych = *++p;
+	if (yybm[0+yych] & 64) {
 		goto yy13;
 	}
 	{ token = BUILD;    break; }
-yy60:
-	yych = *++p;
-	if (yych == 'l') goto yy63;
-	goto yy14;
 yy61:
 	yych = *++p;
-	if (yych == 'd') goto yy64;
+	if (yych == 'l') goto yy64;
 	goto yy14;
 yy62:
 	yych = *++p;
-	if (yych == 'n') goto yy65;
+	if (yych == 'd') goto yy65;
 	goto yy14;
 yy63:
 	yych = *++p;
-	if (yych == 't') goto yy66;
+	if (yych == 'n') goto yy66;
 	goto yy14;
 yy64:
 	yych = *++p;
-	if (yych == 'e') goto yy68;
+	if (yych == 't') goto yy67;
 	goto yy14;
 yy65:
 	yych = *++p;
-	if (yych == 'j') goto yy70;
+	if (yych == 'e') goto yy69;
 	goto yy14;
 yy66:
-	++p;
-	if (yybm[0+(yych = *p)] & 64) {
+	yych = *++p;
+	if (yych == 'j') goto yy71;
+	goto yy14;
+yy67:
+	yych = *++p;
+	if (yybm[0+yych] & 64) {
 		goto yy13;
 	}
 	{ token = DEFAULT;  break; }
-yy68:
-	++p;
-	if (yybm[0+(yych = *p)] & 64) {
+yy69:
+	yych = *++p;
+	if (yybm[0+yych] & 64) {
 		goto yy13;
 	}
 	{ token = INCLUDE;  break; }
-yy70:
+yy71:
 	yych = *++p;
 	if (yych != 'a') goto yy14;
-	++p;
-	if (yybm[0+(yych = *p)] & 64) {
+	yych = *++p;
+	if (yybm[0+yych] & 64) {
 		goto yy13;
 	}
 	{ token = SUBNINJA; break; }
@@ -456,6 +524,119 @@
   return token;
 }
 
+bool Lexer::PeekIndent() {
+  const char* p = ofs_;
+  const char* q;
+  const char* start;
+  for (;;) {
+    start = p;
+    
+{
+	unsigned char yych;
+	unsigned int yyaccept = 0;
+	static const unsigned char yybm[] = {
+		  0, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128,   0, 128, 128,   0, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		192, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+	};
+	yych = *p;
+	if (yybm[0+yych] & 64) {
+		goto yy81;
+	}
+	if (yych <= '\f') {
+		if (yych == '\n') goto yy78;
+	} else {
+		if (yych <= '\r') goto yy80;
+		if (yych == '#') goto yy84;
+	}
+	++p;
+yy77:
+	{ last_token_ = ofs_ = start; return false; }
+yy78:
+	++p;
+	{ last_token_ = ofs_ = start; return false; }
+yy80:
+	yych = *++p;
+	if (yych == '\n') goto yy78;
+	goto yy77;
+yy81:
+	yyaccept = 0;
+	yych = *(q = ++p);
+	if (yybm[0+yych] & 64) {
+		goto yy81;
+	}
+	if (yych <= '\f') {
+		if (yych == '\n') goto yy78;
+	} else {
+		if (yych <= '\r') goto yy85;
+		if (yych == '#') goto yy87;
+	}
+yy83:
+	{ last_token_ = start; ofs_ = p; return true; }
+yy84:
+	yyaccept = 1;
+	yych = *(q = ++p);
+	if (yych <= 0x00) goto yy77;
+	goto yy88;
+yy85:
+	yych = *++p;
+	if (yych == '\n') goto yy78;
+yy86:
+	p = q;
+	if (yyaccept == 0) {
+		goto yy83;
+	} else {
+		goto yy77;
+	}
+yy87:
+	yych = *++p;
+yy88:
+	if (yybm[0+yych] & 128) {
+		goto yy87;
+	}
+	if (yych <= 0x00) goto yy86;
+	if (yych >= '\v') goto yy91;
+yy89:
+	++p;
+	{ continue; }
+yy91:
+	yych = *++p;
+	if (yych == '\n') goto yy89;
+	goto yy86;
+}
+
+  }
+}
+
 bool Lexer::PeekToken(Token token) {
   Token t = ReadToken();
   if (t == token)
@@ -508,47 +689,43 @@
 	};
 	yych = *p;
 	if (yybm[0+yych] & 128) {
-		goto yy79;
+		goto yy98;
 	}
-	if (yych <= 0x00) goto yy75;
-	if (yych == '$') goto yy82;
-	goto yy77;
-yy75:
+	if (yych <= 0x00) goto yy94;
+	if (yych == '$') goto yy101;
+	goto yy96;
+yy94:
 	++p;
 	{ break; }
-yy77:
+yy96:
 	++p;
-yy78:
+yy97:
 	{ break; }
-yy79:
-	++p;
-	yych = *p;
-	if (yybm[0+yych] & 128) {
-		goto yy79;
-	}
-	{ continue; }
-yy82:
-	yych = *(q = ++p);
-	if (yych == '\n') goto yy83;
-	if (yych == '\r') goto yy85;
-	goto yy78;
-yy83:
-	++p;
-	{ continue; }
-yy85:
+yy98:
 	yych = *++p;
-	if (yych == '\n') goto yy87;
-	p = q;
-	goto yy78;
-yy87:
+	if (yybm[0+yych] & 128) {
+		goto yy98;
+	}
+	{ continue; }
+yy101:
+	yych = *(q = ++p);
+	if (yych == '\n') goto yy102;
+	if (yych == '\r') goto yy104;
+	goto yy97;
+yy102:
 	++p;
 	{ continue; }
+yy104:
+	yych = *++p;
+	if (yych == '\n') goto yy102;
+	p = q;
+	goto yy97;
 }
 
   }
 }
 
-bool Lexer::ReadIdent(string* out) {
+bool Lexer::ReadIdent(StringPiece* out) {
   const char* p = ofs_;
   const char* start;
   for (;;) {
@@ -592,21 +769,20 @@
 	};
 	yych = *p;
 	if (yybm[0+yych] & 128) {
-		goto yy93;
+		goto yy110;
 	}
 	++p;
 	{
       last_token_ = start;
       return false;
     }
-yy93:
-	++p;
-	yych = *p;
+yy110:
+	yych = *++p;
 	if (yybm[0+yych] & 128) {
-		goto yy93;
+		goto yy110;
 	}
 	{
-      out->assign(start, p - start);
+      *out = StringPiece(start, p - start);
       break;
     }
 }
@@ -618,7 +794,7 @@
   return true;
 }
 
-bool Lexer::ReadEvalString(EvalString* eval, bool path, string* err) {
+bool Lexer::ReadBindingValue(StringPiece* out, string* err) {
   const char* p = ofs_;
   const char* q;
   const char* start;
@@ -627,6 +803,1126 @@
     
 {
 	unsigned char yych;
+	unsigned int yyaccept = 0;
+	static const unsigned char yybm[] = {
+		  0,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,   0,  64,  64,   0,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,   0,  64,  64,  64, 
+		 64,  64,  64,  64,  64, 192, 192,  64, 
+		192, 192, 192, 192, 192, 192, 192, 192, 
+		192, 192,  64,  64,  64,  64,  64,  64, 
+		 64, 192, 192, 192, 192, 192, 192, 192, 
+		192, 192, 192, 192, 192, 192, 192, 192, 
+		192, 192, 192, 192, 192, 192, 192, 192, 
+		192, 192, 192,  64,  64,  64,  64, 192, 
+		 64, 192, 192, 192, 192, 192, 192, 192, 
+		192, 192, 192, 192, 192, 192, 192, 192, 
+		192, 192, 192, 192, 192, 192, 192, 192, 
+		192, 192, 192,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+		 64,  64,  64,  64,  64,  64,  64,  64, 
+	};
+	yych = *p;
+	if (yybm[0+yych] & 64) {
+		goto yy117;
+	}
+	if (yych <= 0x00) goto yy115;
+	if (yych <= '\n') goto yy120;
+	if (yych <= '\r') goto yy122;
+	goto yy124;
+yy115:
+	++p;
+	{
+      last_token_ = start;
+      return UnexpectedNulError(start, err);
+    }
+yy117:
+	yyaccept = 0;
+	yych = *(q = ++p);
+	if (yybm[0+yych] & 64) {
+		goto yy117;
+	}
+	if (yych >= 0x0E) goto yy125;
+yy119:
+	{
+      continue;
+    }
+yy120:
+	++p;
+	{
+      break;
+    }
+yy122:
+	yych = *++p;
+	if (yych == '\n') goto yy120;
+	{
+      last_token_ = start;
+      return Error(DescribeLastError(), err);
+    }
+yy124:
+	yych = *++p;
+	if (yych <= '-') {
+		if (yych <= 0x1F) {
+			if (yych <= '\n') {
+				if (yych <= '\t') goto yy127;
+				goto yy117;
+			} else {
+				if (yych == '\r') goto yy129;
+				goto yy127;
+			}
+		} else {
+			if (yych <= '#') {
+				if (yych <= ' ') goto yy117;
+				goto yy127;
+			} else {
+				if (yych <= '$') goto yy117;
+				if (yych <= ',') goto yy127;
+				goto yy117;
+			}
+		}
+	} else {
+		if (yych <= '^') {
+			if (yych <= ':') {
+				if (yych <= '/') goto yy127;
+				goto yy117;
+			} else {
+				if (yych <= '@') goto yy127;
+				if (yych <= 'Z') goto yy117;
+				goto yy127;
+			}
+		} else {
+			if (yych <= '`') {
+				if (yych <= '_') goto yy117;
+				goto yy127;
+			} else {
+				if (yych <= 'z') goto yy117;
+				if (yych <= '{') goto yy130;
+				goto yy127;
+			}
+		}
+	}
+yy125:
+	yych = *++p;
+	if (yych <= '-') {
+		if (yych <= 0x1F) {
+			if (yych <= '\n') {
+				if (yych >= '\n') goto yy117;
+			} else {
+				if (yych == '\r') goto yy131;
+			}
+		} else {
+			if (yych <= '#') {
+				if (yych <= ' ') goto yy117;
+			} else {
+				if (yych <= '$') goto yy117;
+				if (yych >= '-') goto yy117;
+			}
+		}
+	} else {
+		if (yych <= '^') {
+			if (yych <= ':') {
+				if (yych >= '0') goto yy117;
+			} else {
+				if (yych <= '@') goto yy126;
+				if (yych <= 'Z') goto yy117;
+			}
+		} else {
+			if (yych <= '`') {
+				if (yych <= '_') goto yy117;
+			} else {
+				if (yych <= 'z') goto yy117;
+				if (yych <= '{') goto yy132;
+			}
+		}
+	}
+yy126:
+	p = q;
+	if (yyaccept == 0) {
+		goto yy119;
+	} else {
+		goto yy128;
+	}
+yy127:
+	++p;
+yy128:
+	{
+      last_token_ = start;
+      return Error("bad $-escape (literal $ must be written as $$)", err);
+    }
+yy129:
+	yych = *++p;
+	if (yych == '\n') goto yy117;
+	goto yy128;
+yy130:
+	yyaccept = 1;
+	yych = *(q = ++p);
+	if (yybm[0+yych] & 128) {
+		goto yy133;
+	}
+	goto yy128;
+yy131:
+	yych = *++p;
+	if (yych == '\n') goto yy117;
+	goto yy126;
+yy132:
+	yych = *++p;
+	if (yybm[0+yych] & 128) {
+		goto yy133;
+	}
+	goto yy126;
+yy133:
+	yych = *++p;
+	if (yybm[0+yych] & 128) {
+		goto yy133;
+	}
+	if (yych == '}') goto yy117;
+	goto yy126;
+}
+
+  }
+  *out = StringPiece(ofs_, p - ofs_);
+  last_token_ = start;
+  ofs_ = p;
+  // Non-path strings end in newlines, so there's no whitespace to eat.
+  return true;
+}
+
+StringPiece Lexer::PeekCanonicalPath() {
+  auto finish = [this](const char* start, const char* end) {
+    ofs_ = end;
+    EatWhitespace();
+    return StringPiece(start, end - start);
+  };
+
+  const char* p = ofs_;
+  const char* q;
+  const char* r;
+  last_token_ = ofs_;
+
+  do {
+    
+{
+	unsigned char yych;
+	static const unsigned char yybm[] = {
+		  0, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128,   0, 128, 128,   0, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		  0, 128, 128, 128,   0, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128,   0, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128,   0, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128,   0, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+		128, 128, 128, 128, 128, 128, 128, 128, 
+	};
+	yych = *p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy137;
+			if (yych != '\n') goto yy139;
+		} else {
+			if (yych <= '\r') goto yy137;
+			if (yych != ' ') goto yy139;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych <= '$') goto yy137;
+			if (yych <= '-') goto yy139;
+			if (yych <= '.') goto yy140;
+			goto yy141;
+		} else {
+			if (yych <= ':') {
+				if (yych <= '9') goto yy139;
+			} else {
+				if (yych != '|') goto yy139;
+			}
+		}
+	}
+yy137:
+	++p;
+yy138:
+	{ break;                      }
+yy139:
+	yych = *(q = ++p);
+	if (yych <= 0x00) goto yy138;
+	if (yych == '$') goto yy138;
+	goto yy143;
+yy140:
+	yych = *(q = ++p);
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy138;
+			if (yych == '\n') goto yy138;
+			goto yy142;
+		} else {
+			if (yych <= '\r') goto yy138;
+			if (yych == ' ') goto yy138;
+			goto yy142;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych <= '$') goto yy138;
+			if (yych <= '-') goto yy142;
+			if (yych <= '.') goto yy149;
+			goto yy150;
+		} else {
+			if (yych <= ':') {
+				if (yych <= '9') goto yy142;
+				goto yy138;
+			} else {
+				if (yych == '|') goto yy138;
+				goto yy142;
+			}
+		}
+	}
+yy141:
+	yych = *(q = ++p);
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy138;
+			if (yych == '\n') goto yy138;
+		} else {
+			if (yych <= '\r') goto yy138;
+			if (yych == ' ') goto yy138;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych <= '$') goto yy138;
+			if (yych <= '-') goto yy142;
+			if (yych <= '.') goto yy151;
+			goto yy138;
+		} else {
+			if (yych <= ':') {
+				if (yych >= ':') goto yy138;
+			} else {
+				if (yych == '|') goto yy138;
+			}
+		}
+	}
+yy142:
+	yych = *++p;
+yy143:
+	if (yybm[0+yych] & 128) {
+		goto yy142;
+	}
+	if (yych <= '\r') {
+		if (yych <= 0x00) goto yy144;
+		if (yych <= '\n') {
+			r = p;
+			goto yy145;
+		}
+		r = p;
+		goto yy147;
+	} else {
+		if (yych <= ' ') {
+			r = p;
+			goto yy145;
+		}
+		if (yych <= '$') goto yy144;
+		if (yych <= '/') goto yy148;
+		r = p;
+		goto yy145;
+	}
+yy144:
+	p = q;
+	goto yy138;
+yy145:
+	++p;
+	p = r;
+	{ return finish(ofs_, p);     }
+yy147:
+	yych = *++p;
+	if (yych == '\n') goto yy145;
+	goto yy144;
+yy148:
+	yych = *++p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy144;
+			if (yych == '\n') goto yy144;
+			goto yy142;
+		} else {
+			if (yych <= '\r') goto yy144;
+			if (yych == ' ') goto yy144;
+			goto yy142;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych <= '$') goto yy144;
+			if (yych <= '-') goto yy142;
+			if (yych <= '.') goto yy151;
+			goto yy144;
+		} else {
+			if (yych <= ':') {
+				if (yych <= '9') goto yy142;
+				goto yy144;
+			} else {
+				if (yych == '|') goto yy144;
+				goto yy142;
+			}
+		}
+	}
+yy149:
+	yych = *++p;
+	if (yybm[0+yych] & 128) {
+		goto yy142;
+	}
+	if (yych <= '$') goto yy144;
+	if (yych <= '/') goto yy152;
+	goto yy144;
+yy150:
+	yych = *++p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy144;
+			if (yych == '\n') goto yy144;
+			goto yy153;
+		} else {
+			if (yych <= '\r') goto yy144;
+			if (yych == ' ') goto yy144;
+			goto yy153;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych <= '$') goto yy144;
+			if (yych <= '-') goto yy153;
+			if (yych <= '.') goto yy155;
+			goto yy144;
+		} else {
+			if (yych <= ':') {
+				if (yych <= '9') goto yy153;
+				goto yy144;
+			} else {
+				if (yych == '|') goto yy144;
+				goto yy153;
+			}
+		}
+	}
+yy151:
+	yych = *++p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy144;
+			if (yych == '\n') goto yy144;
+			goto yy142;
+		} else {
+			if (yych <= '\r') goto yy144;
+			if (yych == ' ') goto yy144;
+			goto yy142;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych <= '$') goto yy144;
+			if (yych <= '-') goto yy142;
+			if (yych <= '.') goto yy156;
+			goto yy144;
+		} else {
+			if (yych <= ':') {
+				if (yych <= '9') goto yy142;
+				goto yy144;
+			} else {
+				if (yych == '|') goto yy144;
+				goto yy142;
+			}
+		}
+	}
+yy152:
+	yych = *++p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy144;
+			if (yych == '\n') goto yy144;
+			goto yy142;
+		} else {
+			if (yych <= '\r') goto yy144;
+			if (yych == ' ') goto yy144;
+			goto yy142;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych <= '$') goto yy144;
+			if (yych <= '-') goto yy142;
+			if (yych <= '.') goto yy157;
+			goto yy144;
+		} else {
+			if (yych <= ':') {
+				if (yych <= '9') goto yy142;
+				goto yy144;
+			} else {
+				if (yych == '|') goto yy144;
+				goto yy142;
+			}
+		}
+	}
+yy153:
+	yych = *++p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy144;
+			if (yych == '\n') {
+				r = p;
+				goto yy158;
+			}
+			goto yy153;
+		} else {
+			if (yych <= '\r') {
+				r = p;
+				goto yy160;
+			}
+			if (yych == ' ') {
+				r = p;
+				goto yy158;
+			}
+			goto yy153;
+		}
+	} else {
+		if (yych <= '9') {
+			if (yych <= '$') goto yy144;
+			if (yych == '/') goto yy161;
+			goto yy153;
+		} else {
+			if (yych <= ':') {
+				r = p;
+				goto yy158;
+			}
+			if (yych == '|') {
+				r = p;
+				goto yy158;
+			}
+			goto yy153;
+		}
+	}
+yy155:
+	yych = *++p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy144;
+			if (yych == '\n') goto yy144;
+			goto yy153;
+		} else {
+			if (yych <= '\r') goto yy144;
+			if (yych == ' ') goto yy144;
+			goto yy153;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych <= '$') goto yy144;
+			if (yych <= '-') goto yy153;
+			if (yych <= '.') goto yy162;
+			goto yy144;
+		} else {
+			if (yych <= ':') {
+				if (yych <= '9') goto yy153;
+				goto yy144;
+			} else {
+				if (yych == '|') goto yy144;
+				goto yy153;
+			}
+		}
+	}
+yy156:
+	yych = *++p;
+	if (yybm[0+yych] & 128) {
+		goto yy142;
+	}
+	goto yy144;
+yy157:
+	yych = *++p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy144;
+			if (yych == '\n') goto yy144;
+			goto yy142;
+		} else {
+			if (yych <= '\r') goto yy144;
+			if (yych == ' ') goto yy144;
+			goto yy142;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych <= '$') goto yy144;
+			if (yych <= '-') goto yy142;
+			if (yych <= '.') goto yy149;
+			goto yy144;
+		} else {
+			if (yych <= ':') {
+				if (yych <= '9') goto yy142;
+				goto yy144;
+			} else {
+				if (yych == '|') goto yy144;
+				goto yy142;
+			}
+		}
+	}
+yy158:
+	++p;
+	p = r;
+	{ return finish(ofs_ + 2, p); }
+yy160:
+	yych = *++p;
+	if (yych == '\n') goto yy158;
+	goto yy144;
+yy161:
+	yych = *++p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy144;
+			if (yych == '\n') goto yy144;
+			goto yy153;
+		} else {
+			if (yych <= '\r') goto yy144;
+			if (yych == ' ') goto yy144;
+			goto yy153;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych <= '$') goto yy144;
+			if (yych <= '-') goto yy153;
+			if (yych <= '.') goto yy163;
+			goto yy144;
+		} else {
+			if (yych <= ':') {
+				if (yych <= '9') goto yy153;
+				goto yy144;
+			} else {
+				if (yych == '|') goto yy144;
+				goto yy153;
+			}
+		}
+	}
+yy162:
+	yych = *++p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy144;
+			if (yych == '\n') goto yy144;
+			goto yy153;
+		} else {
+			if (yych <= '\r') goto yy144;
+			if (yych == ' ') goto yy144;
+			goto yy153;
+		}
+	} else {
+		if (yych <= '9') {
+			if (yych <= '$') goto yy144;
+			if (yych == '/') goto yy150;
+			goto yy153;
+		} else {
+			if (yych <= ':') goto yy144;
+			if (yych == '|') goto yy144;
+			goto yy153;
+		}
+	}
+yy163:
+	yych = *++p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy144;
+			if (yych == '\n') goto yy144;
+			goto yy153;
+		} else {
+			if (yych <= '\r') goto yy144;
+			if (yych == ' ') goto yy144;
+			goto yy153;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych <= '$') goto yy144;
+			if (yych <= '-') goto yy153;
+			if (yych >= '/') goto yy144;
+		} else {
+			if (yych <= ':') {
+				if (yych <= '9') goto yy153;
+				goto yy144;
+			} else {
+				if (yych == '|') goto yy144;
+				goto yy153;
+			}
+		}
+	}
+	yych = *++p;
+	if (yych <= '#') {
+		if (yych <= '\f') {
+			if (yych <= 0x00) goto yy144;
+			if (yych == '\n') goto yy144;
+			goto yy153;
+		} else {
+			if (yych <= '\r') goto yy144;
+			if (yych == ' ') goto yy144;
+			goto yy153;
+		}
+	} else {
+		if (yych <= '9') {
+			if (yych <= '$') goto yy144;
+			if (yych == '/') goto yy144;
+			goto yy153;
+		} else {
+			if (yych <= ':') goto yy144;
+			if (yych == '|') goto yy144;
+			goto yy153;
+		}
+	}
+}
+
+  } while (false);
+
+  return {};
+}
+
+bool Lexer::ReadPath(LexedPath* out, std::string* err) {
+  const char* p = ofs_;
+  const char* q;
+  const char* start;
+  for (;;) {
+    start = p;
+    
+{
+	unsigned char yych;
+	unsigned int yyaccept = 0;
+	static const unsigned char yybm[] = {
+		  0,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,   0,  32,  32,   0,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 64,  32,  32,  32,   0,  32,  32,  32, 
+		 32,  32,  32,  32,  32, 160, 160,  32, 
+		160, 160, 160, 160, 160, 160, 160, 160, 
+		160, 160,   0,  32,  32,  32,  32,  32, 
+		 32, 160, 160, 160, 160, 160, 160, 160, 
+		160, 160, 160, 160, 160, 160, 160, 160, 
+		160, 160, 160, 160, 160, 160, 160, 160, 
+		160, 160, 160,  32,  32,  32,  32, 160, 
+		 32, 160, 160, 160, 160, 160, 160, 160, 
+		160, 160, 160, 160, 160, 160, 160, 160, 
+		160, 160, 160, 160, 160, 160, 160, 160, 
+		160, 160, 160,  32,   0,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+		 32,  32,  32,  32,  32,  32,  32,  32, 
+	};
+	yych = *p;
+	if (yybm[0+yych] & 32) {
+		goto yy169;
+	}
+	if (yych <= '\r') {
+		if (yych <= 0x00) goto yy167;
+		if (yych <= '\n') goto yy172;
+		goto yy174;
+	} else {
+		if (yych <= ' ') goto yy172;
+		if (yych <= '$') goto yy176;
+		goto yy172;
+	}
+yy167:
+	++p;
+	{
+      last_token_ = start;
+      return UnexpectedNulError(start, err);
+    }
+yy169:
+	yyaccept = 0;
+	yych = *(q = ++p);
+	if (yybm[0+yych] & 32) {
+		goto yy169;
+	}
+	if (yych <= ' ') goto yy171;
+	if (yych <= '$') goto yy177;
+yy171:
+	{
+      continue;
+    }
+yy172:
+	++p;
+	{
+      p = start;
+      break;
+    }
+yy174:
+	yych = *++p;
+	if (yych == '\n') goto yy172;
+	{
+      last_token_ = start;
+      return Error(DescribeLastError(), err);
+    }
+yy176:
+	yych = *++p;
+	if (yych <= '-') {
+		if (yych <= 0x1F) {
+			if (yych <= '\n') {
+				if (yych <= '\t') goto yy179;
+				goto yy181;
+			} else {
+				if (yych == '\r') goto yy183;
+				goto yy179;
+			}
+		} else {
+			if (yych <= '#') {
+				if (yych <= ' ') goto yy169;
+				goto yy179;
+			} else {
+				if (yych <= '$') goto yy169;
+				if (yych <= ',') goto yy179;
+				goto yy169;
+			}
+		}
+	} else {
+		if (yych <= '^') {
+			if (yych <= ':') {
+				if (yych <= '/') goto yy179;
+				goto yy169;
+			} else {
+				if (yych <= '@') goto yy179;
+				if (yych <= 'Z') goto yy169;
+				goto yy179;
+			}
+		} else {
+			if (yych <= '`') {
+				if (yych <= '_') goto yy169;
+				goto yy179;
+			} else {
+				if (yych <= 'z') goto yy169;
+				if (yych <= '{') goto yy184;
+				goto yy179;
+			}
+		}
+	}
+yy177:
+	yych = *++p;
+	if (yych <= '-') {
+		if (yych <= 0x1F) {
+			if (yych <= '\n') {
+				if (yych >= '\n') goto yy181;
+			} else {
+				if (yych == '\r') goto yy185;
+			}
+		} else {
+			if (yych <= '#') {
+				if (yych <= ' ') goto yy169;
+			} else {
+				if (yych <= '$') goto yy169;
+				if (yych >= '-') goto yy169;
+			}
+		}
+	} else {
+		if (yych <= '^') {
+			if (yych <= ':') {
+				if (yych >= '0') goto yy169;
+			} else {
+				if (yych <= '@') goto yy178;
+				if (yych <= 'Z') goto yy169;
+			}
+		} else {
+			if (yych <= '`') {
+				if (yych <= '_') goto yy169;
+			} else {
+				if (yych <= 'z') goto yy169;
+				if (yych <= '{') goto yy186;
+			}
+		}
+	}
+yy178:
+	p = q;
+	if (yyaccept == 0) {
+		goto yy171;
+	} else {
+		goto yy180;
+	}
+yy179:
+	++p;
+yy180:
+	{
+      last_token_ = start;
+      return Error("bad $-escape (literal $ must be written as $$)", err);
+    }
+yy181:
+	yyaccept = 0;
+	yych = *(q = ++p);
+	if (yybm[0+yych] & 32) {
+		goto yy169;
+	}
+	if (yych <= '\r') goto yy171;
+	if (yych <= ' ') goto yy181;
+	if (yych <= '$') goto yy177;
+	goto yy171;
+yy183:
+	yych = *++p;
+	if (yych == '\n') goto yy181;
+	goto yy180;
+yy184:
+	yyaccept = 1;
+	yych = *(q = ++p);
+	if (yybm[0+yych] & 128) {
+		goto yy187;
+	}
+	goto yy180;
+yy185:
+	yych = *++p;
+	if (yych == '\n') goto yy181;
+	goto yy178;
+yy186:
+	yych = *++p;
+	if (yybm[0+yych] & 128) {
+		goto yy187;
+	}
+	goto yy178;
+yy187:
+	yych = *++p;
+	if (yybm[0+yych] & 128) {
+		goto yy187;
+	}
+	if (yych == '}') goto yy169;
+	goto yy178;
+}
+
+  }
+  *out = {};
+  out->str_ = StringPiece(ofs_, p - ofs_);
+  last_token_ = start;
+  ofs_ = p;
+  EatWhitespace();
+  return true;
+}
+
+/// Append the let binding's evaluated value to the output string. The input
+/// StringPiece must include a valid binding terminator.
+template <typename EvalVar>
+static inline void EvaluateBinding(std::string* out_append, StringPiece value,
+                                   EvalVar&& eval_var) {
+  auto expand = [&eval_var](const char* start, const char* end) {
+    StringPiece var(start, end - start);
+    eval_var(var);
+  };
+
+  const char* p = value.data();
+  const char* q;
+
+  for (;;) {
+    const char* start = p;
+    
+{
+	unsigned char yych;
+	static const unsigned char yybm[] = {
+		  0,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,   0,  16,  16,   0,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 48,  16,  16,  16,   0,  16,  16,  16, 
+		 16,  16,  16,  16,  16, 208, 144,  16, 
+		208, 208, 208, 208, 208, 208, 208, 208, 
+		208, 208,  16,  16,  16,  16,  16,  16, 
+		 16, 208, 208, 208, 208, 208, 208, 208, 
+		208, 208, 208, 208, 208, 208, 208, 208, 
+		208, 208, 208, 208, 208, 208, 208, 208, 
+		208, 208, 208,  16,  16,  16,  16, 208, 
+		 16, 208, 208, 208, 208, 208, 208, 208, 
+		208, 208, 208, 208, 208, 208, 208, 208, 
+		208, 208, 208, 208, 208, 208, 208, 208, 
+		208, 208, 208,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+		 16,  16,  16,  16,  16,  16,  16,  16, 
+	};
+	yych = *p;
+	if (yybm[0+yych] & 16) {
+		goto yy193;
+	}
+	if (yych <= 0x00) goto yy191;
+	if (yych <= '\n') goto yy196;
+	if (yych <= '\r') goto yy198;
+	goto yy199;
+yy191:
+	++p;
+yy192:
+	{ assert(false && "bad input in EvaluateBinding"); abort(); }
+yy193:
+	yych = *++p;
+	if (yybm[0+yych] & 16) {
+		goto yy193;
+	}
+	{ out_append->append(start, p - start);   continue; }
+yy196:
+	++p;
+	{                                         break;    }
+yy198:
+	yych = *++p;
+	if (yych == '\n') goto yy196;
+	goto yy192;
+yy199:
+	yych = *(q = ++p);
+	if (yybm[0+yych] & 64) {
+		goto yy207;
+	}
+	if (yych <= ' ') {
+		if (yych <= '\f') {
+			if (yych != '\n') goto yy192;
+		} else {
+			if (yych <= '\r') goto yy203;
+			if (yych <= 0x1F) goto yy192;
+			goto yy205;
+		}
+	} else {
+		if (yych <= '/') {
+			if (yych == '$') goto yy205;
+			goto yy192;
+		} else {
+			if (yych <= ':') goto yy205;
+			if (yych <= '`') goto yy192;
+			if (yych <= '{') goto yy210;
+			goto yy192;
+		}
+	}
+yy200:
+	yych = *++p;
+	if (yybm[0+yych] & 32) {
+		goto yy200;
+	}
+	{                                         continue; }
+yy203:
+	yych = *++p;
+	if (yych == '\n') goto yy200;
+yy204:
+	p = q;
+	goto yy192;
+yy205:
+	++p;
+	{ out_append->push_back(start[1]);        continue; }
+yy207:
+	yych = *++p;
+	if (yybm[0+yych] & 64) {
+		goto yy207;
+	}
+	{ expand(start + 1, p);                   continue; }
+yy210:
+	yych = *++p;
+	if (yych == '}') goto yy204;
+	goto yy212;
+yy211:
+	yych = *++p;
+yy212:
+	if (yybm[0+yych] & 128) {
+		goto yy211;
+	}
+	if (yych != '}') goto yy204;
+	++p;
+	{ expand(start + 2, p - 1);               continue; }
+}
+
+  }
+  assert((p == value.data() + value.size()) &&
+         "bad end pos in EvaluateBinding");
+}
+
+void EvaluateBindingInScope(std::string* out_append, StringPiece value,
+                            ScopePosition pos) {
+  EvaluateBinding(out_append, value,
+      [out_append, &pos](const HashedStrView& var) {
+    Scope::EvaluateVariableAtPos(out_append, var, pos);
+  });
+}
+
+bool EvaluateBindingOnRule(std::string* out_append, StringPiece value,
+                           EdgeEval* edge_eval, std::string* err) {
+  bool result = true;
+  EvaluateBinding(out_append, value,
+      [out_append, edge_eval, &result, err](const HashedStrView& var) {
+    result = result && edge_eval->EvaluateVariable(out_append, var, err);
+  });
+  return result;
+}
+
+std::string EvaluateBindingForTesting(StringPiece value) {
+  std::string result;
+  EvaluateBinding(&result, value, [&result](StringPiece var) {
+    result += "[$" + var.AsString() + "]";
+  });
+  return result;
+}
+
+/// Append an evaluated path to the output string.
+///
+/// This function does not canonicalize the output. Ninja canonicalizes paths for
+/// build nodes, but not all paths (e.g. It doesn't canonicalize paths to
+/// included ninja files.)
+template <typename EvalVar>
+static inline void EvaluatePath(std::string* out_append, const LexedPath& path,
+                                EvalVar&& eval_var) {
+  auto expand = [&eval_var](const char* start, const char* end) {
+    StringPiece var(start, end - start);
+    eval_var(var);
+  };
+
+  const char* p = path.str_.data();
+  const char* q;
+
+  for (;;) {
+    const char* start = p;
+    
+{
+	unsigned char yych;
 	static const unsigned char yybm[] = {
 		  0,  16,  16,  16,  16,  16,  16,  16, 
 		 16,  16,   0,  16,  16,   0,  16,  16, 
@@ -663,168 +1959,121 @@
 	};
 	yych = *p;
 	if (yybm[0+yych] & 16) {
-		goto yy100;
+		goto yy219;
 	}
 	if (yych <= '\r') {
-		if (yych <= 0x00) goto yy98;
-		if (yych <= '\n') goto yy103;
-		goto yy105;
+		if (yych <= 0x00) goto yy217;
+		if (yych <= '\n') goto yy222;
+		goto yy224;
 	} else {
-		if (yych <= ' ') goto yy103;
-		if (yych <= '$') goto yy107;
-		goto yy103;
+		if (yych <= ' ') goto yy222;
+		if (yych <= '$') goto yy225;
+		goto yy222;
 	}
-yy98:
+yy217:
 	++p;
-	{
-      last_token_ = start;
-      return Error("unexpected EOF", err);
-    }
-yy100:
-	++p;
-	yych = *p;
-	if (yybm[0+yych] & 16) {
-		goto yy100;
-	}
-	{
-      eval->AddText(StringPiece(start, p - start));
-      continue;
-    }
-yy103:
-	++p;
-	{
-      if (path) {
-        p = start;
-        break;
-      } else {
-        if (*start == '\n')
-          break;
-        eval->AddText(StringPiece(start, 1));
-        continue;
-      }
-    }
-yy105:
-	++p;
-	if ((yych = *p) == '\n') goto yy108;
-	{
-      last_token_ = start;
-      return Error(DescribeLastError(), err);
-    }
-yy107:
+yy218:
+	{ assert(false && "bad input in EvaluatePath"); abort(); }
+yy219:
 	yych = *++p;
+	if (yybm[0+yych] & 16) {
+		goto yy219;
+	}
+	{ out_append->append(start, p - start);   continue; }
+yy222:
+	++p;
+	{ p = start;                              break;    }
+yy224:
+	yych = *++p;
+	if (yych == '\n') goto yy222;
+	goto yy218;
+yy225:
+	yych = *(q = ++p);
 	if (yybm[0+yych] & 64) {
-		goto yy120;
+		goto yy233;
 	}
 	if (yych <= ' ') {
 		if (yych <= '\f') {
-			if (yych == '\n') goto yy112;
-			goto yy110;
+			if (yych != '\n') goto yy218;
 		} else {
-			if (yych <= '\r') goto yy115;
-			if (yych <= 0x1F) goto yy110;
-			goto yy116;
+			if (yych <= '\r') goto yy229;
+			if (yych <= 0x1F) goto yy218;
+			goto yy231;
 		}
 	} else {
 		if (yych <= '/') {
-			if (yych == '$') goto yy118;
-			goto yy110;
+			if (yych == '$') goto yy231;
+			goto yy218;
 		} else {
-			if (yych <= ':') goto yy123;
-			if (yych <= '`') goto yy110;
-			if (yych <= '{') goto yy125;
-			goto yy110;
+			if (yych <= ':') goto yy231;
+			if (yych <= '`') goto yy218;
+			if (yych <= '{') goto yy236;
+			goto yy218;
 		}
 	}
-yy108:
-	++p;
-	{
-      if (path)
-        p = start;
-      break;
-    }
-yy110:
-	++p;
-yy111:
-	{
-      last_token_ = start;
-      return Error("bad $-escape (literal $ must be written as $$)", err);
-    }
-yy112:
-	++p;
-	yych = *p;
-	if (yybm[0+yych] & 32) {
-		goto yy112;
-	}
-	{
-      continue;
-    }
-yy115:
+yy226:
 	yych = *++p;
-	if (yych == '\n') goto yy126;
-	goto yy111;
-yy116:
-	++p;
-	{
-      eval->AddText(StringPiece(" ", 1));
-      continue;
-    }
-yy118:
-	++p;
-	{
-      eval->AddText(StringPiece("$", 1));
-      continue;
-    }
-yy120:
-	++p;
-	yych = *p;
-	if (yybm[0+yych] & 64) {
-		goto yy120;
+	if (yybm[0+yych] & 32) {
+		goto yy226;
 	}
-	{
-      eval->AddSpecial(StringPiece(start + 1, p - start - 1));
-      continue;
-    }
-yy123:
-	++p;
-	{
-      eval->AddText(StringPiece(":", 1));
-      continue;
-    }
-yy125:
-	yych = *(q = ++p);
-	if (yybm[0+yych] & 128) {
-		goto yy129;
-	}
-	goto yy111;
-yy126:
-	++p;
-	yych = *p;
-	if (yych == ' ') goto yy126;
-	{
-      continue;
-    }
-yy129:
-	++p;
-	yych = *p;
-	if (yybm[0+yych] & 128) {
-		goto yy129;
-	}
-	if (yych == '}') goto yy132;
+	{                                         continue; }
+yy229:
+	yych = *++p;
+	if (yych == '\n') goto yy226;
+yy230:
 	p = q;
-	goto yy111;
-yy132:
+	goto yy218;
+yy231:
 	++p;
-	{
-      eval->AddSpecial(StringPiece(start + 2, p - start - 3));
-      continue;
-    }
+	{ out_append->push_back(start[1]);        continue; }
+yy233:
+	yych = *++p;
+	if (yybm[0+yych] & 64) {
+		goto yy233;
+	}
+	{ expand(start + 1, p);                   continue; }
+yy236:
+	yych = *++p;
+	if (yych == '}') goto yy230;
+	goto yy238;
+yy237:
+	yych = *++p;
+yy238:
+	if (yybm[0+yych] & 128) {
+		goto yy237;
+	}
+	if (yych != '}') goto yy230;
+	++p;
+	{ expand(start + 2, p - 1);               continue; }
 }
 
   }
-  last_token_ = start;
-  ofs_ = p;
-  if (path)
-    EatWhitespace();
-  // Non-path strings end in newlines, so there's no whitespace to eat.
-  return true;
+  assert((p == path.str_.data() + path.str_.size()) &&
+         "bad end pos in EvaluatePath");
+}
+
+void EvaluatePathInScope(std::string* out_append, const LexedPath& path,
+                         ScopePosition pos) {
+  EvaluatePath(out_append, path, [out_append, &pos](const HashedStrView& var) {
+    Scope::EvaluateVariableAtPos(out_append, var, pos);
+  });
+}
+
+void EvaluatePathOnEdge(std::string* out_append, const LexedPath& path,
+                        const Edge& edge) {
+  EvaluatePath(out_append, path, [out_append, &edge](const HashedStrView& var) {
+    // First look for a binding on the edge itself, then fall back to the
+    // edge's enclosing scope.
+    if (edge.EvaluateVariableSelfOnly(out_append, var))
+      return;
+    Scope::EvaluateVariableAtPos(out_append, var, edge.pos_.scope_pos());
+  });
+}
+
+std::string EvaluatePathForTesting(const LexedPath& path) {
+  std::string result;
+  EvaluatePath(&result, path, [&result](StringPiece var) {
+    result += "[$" + var.AsString() + "]";
+  });
+  return result;
 }
diff --git a/src/lexer.h b/src/lexer.h
index f366556..42581a7 100644
--- a/src/lexer.h
+++ b/src/lexer.h
@@ -15,19 +15,46 @@
 #ifndef NINJA_LEXER_H_
 #define NINJA_LEXER_H_
 
+#include <assert.h>
+
+#include <string>
+
 #include "string_piece.h"
+#include "util.h"
 
 // Windows may #define ERROR.
 #ifdef ERROR
 #undef ERROR
 #endif
 
+struct Edge;
+struct EdgeEval;
 struct EvalString;
+struct LexedPath;
+struct ScopePosition;
+
+/// Search the manifest for the next acceptable point to split the manifest.
+/// Assuming the manifest is syntactically valid to the returned position, the
+/// chunk parser will finish parsing a declaration just before the returned
+/// position.
+size_t AdvanceToNextManifestChunk(StringPiece content, size_t idx);
+
+bool DecorateErrorWithLocation(const std::string& filename,
+                               const char* file_start,
+                               size_t file_offset,
+                               const std::string& message,
+                               std::string* err);
 
 struct Lexer {
-  Lexer() {}
+  /// Ordinary lexer constructor.
+  Lexer(const std::string& filename, StringPiece file_content, const char* pos)
+      : filename_(filename), input_(file_content), ofs_(pos) {
+    assert(pos >= &file_content[0] &&
+           pos <= file_content.data() + file_content.size());
+  }
+
   /// Helper ctor useful for tests.
-  explicit Lexer(const char* input);
+  explicit Lexer(const char* input) : Lexer("input", input, input) {}
 
   enum Token {
     ERROR,
@@ -41,9 +68,11 @@
     NEWLINE,
     PIPE,
     PIPE2,
+    PIPEAT,
     POOL,
     RULE,
     SUBNINJA,
+    TNUL,
     TEOF,
   };
 
@@ -55,10 +84,17 @@
 
   /// If the last token read was an ERROR token, provide more info
   /// or the empty string.
-  string DescribeLastError();
+  std::string DescribeLastError();
 
-  /// Start parsing some input.
-  void Start(StringPiece filename, StringPiece input);
+  // XXX: Decide whether to use char* or size_t to represent lexer position.
+  // (We're using size_t for diagnostic reporting -- GetLastTokenOffset, but
+  // we're using char* for checking for the end-of-chunk.)
+  const char* GetPos() { return ofs_; }
+  void ResetPos(const char* pos) { ofs_ = pos; last_token_ = nullptr; }
+
+  size_t GetLastTokenOffset() {
+    return last_token_ ? last_token_ - input_.data() : 0;
+  }
 
   /// Read a Token from the Token enum.
   Token ReadToken();
@@ -66,40 +102,86 @@
   /// Rewind to the last read Token.
   void UnreadToken();
 
+  /// If the next token is a binding's indentation, this function reads it and
+  /// returns true. This function skips lines containing only a comment, which
+  /// are allowed in a block of indented bindings. (A completely blank line, on
+  /// the other hand, ends the block.)
+  bool PeekIndent();
+
   /// If the next token is \a token, read it and return true.
   bool PeekToken(Token token);
 
   /// Read a simple identifier (a rule or variable name).
   /// Returns false if a name can't be read.
-  bool ReadIdent(string* out);
+  bool ReadIdent(StringPiece* out);
 
-  /// Read a path (complete with $escapes).
-  /// Returns false only on error, returned path may be empty if a delimiter
-  /// (space, newline) is hit.
-  bool ReadPath(EvalString* path, string* err) {
-    return ReadEvalString(path, true, err);
-  }
+  /// Parse the value in a "var = value" let binding and return the unprocessed
+  /// span of the value (escapes and variable expansions are left as-is). The
+  /// binding must end with a newline, which is consumed.
+  ///
+  /// The returned StringPiece includes the EOL terminator, "\r\n" or "\n".
+  /// Copying the StringPiece copies the terminator.
+  bool ReadBindingValue(StringPiece* out, std::string* err);
 
-  /// Read the value side of a var = value line (complete with $escapes).
-  /// Returns false only on error.
-  bool ReadVarValue(EvalString* value, string* err) {
-    return ReadEvalString(value, false, err);
-  }
+  /// Try to parse a path from the manifest that's already canonical and doesn't
+  /// need evaluation. Returns the string if successful and return a 0-size
+  /// string otherwise. This function is a performance optimization.
+  StringPiece PeekCanonicalPath();
+
+  /// Read a ninja path (e.g. build/include declarations) and output a LexedPath
+  /// object with the position of the path in the loaded file's buffer. If there
+  /// is no path before the lexer sees a delimiter (e.g. space, newline, colon),
+  /// it returns true and outputs an empty string.
+  ///
+  /// On error, the function returns false and writes an error to *err.
+  ///
+  /// The delimiter is not included in the path's StringPiece, and (except for
+  /// space characters), the delimiter is left in the lexer's input stream.
+  ///
+  /// Because path evaluation requires the delimiter's presence, the path should
+  /// be evaluated before its underlying memory is freed, and the StringPiece
+  /// should not be duplicated. (e.g. Don't copy it into an std::string first.)
+  bool ReadPath(LexedPath* out, std::string* err);
 
   /// Construct an error message with context.
-  bool Error(const string& message, string* err);
+  bool Error(const std::string& message, std::string* err);
 
 private:
+  /// Return the end of the file input (which points at a NUL byte).
+  const char* EndOfFile() { return input_.data() + input_.size(); }
+
+  /// Report an error on a NUL character, which could indicate EOF, but could
+  /// also be a stray NUL character in the file's content.
+  bool UnexpectedNulError(const char* pos, std::string* err);
+
   /// Skip past whitespace (called after each read token/ident/etc.).
   void EatWhitespace();
 
-  /// Read a $-escaped string.
-  bool ReadEvalString(EvalString* eval, bool path, string* err);
-
-  StringPiece filename_;
+  std::string filename_;
   StringPiece input_;
-  const char* ofs_;
-  const char* last_token_;
+  const char* ofs_ = nullptr;
+  const char* last_token_ = nullptr;
 };
 
+void EvaluateBindingInScope(std::string* out_append,
+                            StringPiece value,
+                            ScopePosition pos);
+
+bool EvaluateBindingOnRule(std::string* out_append,
+                           StringPiece value,
+                           EdgeEval* edge_eval,
+                           std::string* err);
+
+/// Used for lexer tests.
+std::string EvaluateBindingForTesting(StringPiece value);
+
+void EvaluatePathInScope(std::string* out_append, const LexedPath& path,
+                         ScopePosition pos);
+
+void EvaluatePathOnEdge(std::string* out_append, const LexedPath& path,
+                        const Edge& edge);
+
+/// Used for lexer tests.
+std::string EvaluatePathForTesting(const LexedPath& path);
+
 #endif // NINJA_LEXER_H_
diff --git a/src/lexer.in.cc b/src/lexer.in.cc
index c1fb822..63fa937 100644
--- a/src/lexer.in.cc
+++ b/src/lexer.in.cc
@@ -17,24 +17,79 @@
 #include <stdio.h>
 
 #include "eval_env.h"
+#include "graph.h"
 #include "util.h"
 
-bool Lexer::Error(const string& message, string* err) {
+size_t AdvanceToNextManifestChunk(StringPiece content, size_t idx) {
+  assert(idx <= content.size());
+
+  // Iterate over each LF in the manifest, starting at the given index.
+  while (true) {
+    const void* next_line = memchr(content.data() + idx, '\n',
+                                   content.size() - idx);
+    if (next_line == nullptr) {
+      break;
+    }
+    idx = static_cast<const char*>(next_line) - content.data();
+    ++idx; // step over the LF
+
+    // The line must not be preceded by a line continuator. This logic can
+    // filter out more split candidates than strictly necessary:
+    //  - The preceding line could have a comment that ends with a "$": "# $\n"
+    //  - The preceding line could end with an escaped-dollar: "X=$$\n"
+    if ((idx >= 2 && content.substr(idx - 2, 2) == "$\n") ||
+        (idx >= 3 && content.substr(idx - 3, 3) == "$\r\n")) {
+      continue;
+    }
+
+    // Skip an indented line or a comment line, either of which could be part of
+    // an earlier declaration. Ninja allows unindented comments (as well as
+    // indented comments) inside a binding block, e.g.:
+    //
+    //   build foo: cc
+    //   # comment-line
+    //     pool = link_pool
+    //
+    // Ninja doesn't allow blank lines in a binding block. This code could
+    // probably allow a chunk to start with a blank line, but it seems better if
+    // it doesn't.
+    if (idx >= content.size() ||
+        content[idx] == ' ' || content[idx] == '#' ||
+        content[idx] == '\r' || content[idx] == '\n') {
+      continue;
+    }
+
+    return idx;
+  }
+
+  return content.size();
+}
+
+bool DecorateErrorWithLocation(const std::string& filename,
+                               const char* file_start,
+                               size_t file_offset,
+                               const std::string& message,
+                               std::string* err) {
+  // Make a copy in case message and err alias.
+  std::string message_tmp = message;
+
   // Compute line/column.
   int line = 1;
-  const char* line_start = input_.str_;
-  for (const char* p = input_.str_; p < last_token_; ++p) {
+  const char* line_start = file_start;
+  const char* file_pos = file_start + file_offset;
+
+  for (const char* p = line_start; p < file_pos; ++p) {
     if (*p == '\n') {
       ++line;
       line_start = p + 1;
     }
   }
-  int col = last_token_ ? (int)(last_token_ - line_start) : 0;
+  int col = (int)(file_pos - line_start);
 
   char buf[1024];
-  snprintf(buf, sizeof(buf), "%s:%d: ", filename_.AsString().c_str(), line);
+  snprintf(buf, sizeof(buf), "%s:%d: ", filename.c_str(), line);
   *err = buf;
-  *err += message + "\n";
+  *err += message_tmp + "\n";
 
   // Add some context to the message.
   const int kTruncateColumn = 72;
@@ -58,15 +113,16 @@
   return false;
 }
 
-Lexer::Lexer(const char* input) {
-  Start("input", input);
+bool Lexer::Error(const std::string& message, std::string* err) {
+  return DecorateErrorWithLocation(filename_, input_.data(),
+                                   GetLastTokenOffset(), message, err);
 }
 
-void Lexer::Start(StringPiece filename, StringPiece input) {
-  filename_ = filename;
-  input_ = input;
-  ofs_ = input_.str_;
-  last_token_ = NULL;
+bool Lexer::UnexpectedNulError(const char* pos, std::string* err) {
+  assert(*pos == '\0');
+  const char* msg = (pos == EndOfFile()) ? "unexpected EOF"
+                                         : "unexpected NUL byte";
+  return Error(msg, err);
 }
 
 const char* Lexer::TokenName(Token t) {
@@ -82,9 +138,11 @@
   case NEWLINE:  return "newline";
   case PIPE2:    return "'||'";
   case PIPE:     return "'|'";
+  case PIPEAT:   return "'|@'";
   case POOL:     return "'pool'";
   case RULE:     return "'rule'";
   case SUBNINJA: return "'subninja'";
+  case TNUL:     return "nul byte";
   case TEOF:     return "eof";
   }
   return NULL;  // not reached
@@ -116,6 +174,7 @@
 Lexer::Token Lexer::ReadToken() {
   const char* p = ofs_;
   const char* q;
+  const char* r;
   const char* start;
   Lexer::Token token;
   for (;;) {
@@ -124,15 +183,17 @@
     re2c:define:YYCTYPE = "unsigned char";
     re2c:define:YYCURSOR = p;
     re2c:define:YYMARKER = q;
+    re2c:define:YYCTXMARKER = r;
     re2c:yyfill:enable = 0;
 
     nul = "\000";
     simple_varname = [a-zA-Z0-9_-]+;
     varname = [a-zA-Z0-9_.-]+;
+    eol = "\n" | "\r\n";
+    comment = "#"[^\000\r\n]*;
 
-    [ ]*"#"[^\000\n]*"\n" { continue; }
-    [ ]*"\r\n" { token = NEWLINE;  break; }
-    [ ]*"\n"   { token = NEWLINE;  break; }
+    [ ]*comment / eol { continue; }
+    [ ]*eol    { token = NEWLINE;  break; }
     [ ]+       { token = INDENT;   break; }
     "build"    { token = BUILD;    break; }
     "pool"     { token = POOL;     break; }
@@ -140,12 +201,13 @@
     "default"  { token = DEFAULT;  break; }
     "="        { token = EQUALS;   break; }
     ":"        { token = COLON;    break; }
+    "|@"       { token = PIPEAT;   break; }
     "||"       { token = PIPE2;    break; }
     "|"        { token = PIPE;     break; }
     "include"  { token = INCLUDE;  break; }
     "subninja" { token = SUBNINJA; break; }
     varname    { token = IDENT;    break; }
-    nul        { token = TEOF;     break; }
+    nul        { token = (start == EndOfFile()) ? TEOF : TNUL; break; }
     [^]        { token = ERROR;    break; }
     */
   }
@@ -157,6 +219,21 @@
   return token;
 }
 
+bool Lexer::PeekIndent() {
+  const char* p = ofs_;
+  const char* q;
+  const char* start;
+  for (;;) {
+    start = p;
+    /*!re2c
+    [ ]*comment eol { continue; }
+    [ ]*eol         { last_token_ = ofs_ = start; return false; }
+    [ ]+            { last_token_ = start; ofs_ = p; return true; }
+    [^]             { last_token_ = ofs_ = start; return false; }
+    */
+  }
+}
+
 bool Lexer::PeekToken(Token token) {
   Token t = ReadToken();
   if (t == token)
@@ -172,22 +249,21 @@
     ofs_ = p;
     /*!re2c
     [ ]+    { continue; }
-    "$\r\n" { continue; }
-    "$\n"   { continue; }
+    "$" eol { continue; }
     nul     { break; }
     [^]     { break; }
     */
   }
 }
 
-bool Lexer::ReadIdent(string* out) {
+bool Lexer::ReadIdent(StringPiece* out) {
   const char* p = ofs_;
   const char* start;
   for (;;) {
     start = p;
     /*!re2c
     varname {
-      out->assign(start, p - start);
+      *out = StringPiece(start, p - start);
       break;
     }
     [^] {
@@ -202,66 +278,30 @@
   return true;
 }
 
-bool Lexer::ReadEvalString(EvalString* eval, bool path, string* err) {
+bool Lexer::ReadBindingValue(StringPiece* out, string* err) {
   const char* p = ofs_;
   const char* q;
   const char* start;
   for (;;) {
     start = p;
     /*!re2c
-    [^$ :\r\n|\000]+ {
-      eval->AddText(StringPiece(start, p - start));
+    ( [^$\r\n\000]
+        | "$" [$: ]
+        | "$" eol
+        | "${" varname "}"
+        | "$" simple_varname )+ {
       continue;
     }
-    "\r\n" {
-      if (path)
-        p = start;
+    eol {
       break;
     }
-    [ :|\n] {
-      if (path) {
-        p = start;
-        break;
-      } else {
-        if (*start == '\n')
-          break;
-        eval->AddText(StringPiece(start, 1));
-        continue;
-      }
-    }
-    "$$" {
-      eval->AddText(StringPiece("$", 1));
-      continue;
-    }
-    "$ " {
-      eval->AddText(StringPiece(" ", 1));
-      continue;
-    }
-    "$\r\n"[ ]* {
-      continue;
-    }
-    "$\n"[ ]* {
-      continue;
-    }
-    "${"varname"}" {
-      eval->AddSpecial(StringPiece(start + 2, p - start - 3));
-      continue;
-    }
-    "$"simple_varname {
-      eval->AddSpecial(StringPiece(start + 1, p - start - 1));
-      continue;
-    }
-    "$:" {
-      eval->AddText(StringPiece(":", 1));
-      continue;
-    }
     "$". {
       last_token_ = start;
       return Error("bad $-escape (literal $ must be written as $$)", err);
     }
     nul {
       last_token_ = start;
-      return Error("unexpected EOF", err);
+      return UnexpectedNulError(start, err);
     }
     [^] {
       last_token_ = start;
@@ -269,10 +309,192 @@
     }
     */
   }
+  *out = StringPiece(ofs_, p - ofs_);
   last_token_ = start;
   ofs_ = p;
-  if (path)
-    EatWhitespace();
   // Non-path strings end in newlines, so there's no whitespace to eat.
   return true;
 }
+
+StringPiece Lexer::PeekCanonicalPath() {
+  auto finish = [this](const char* start, const char* end) {
+    ofs_ = end;
+    EatWhitespace();
+    return StringPiece(start, end - start);
+  };
+
+  const char* p = ofs_;
+  const char* q;
+  const char* r;
+  last_token_ = ofs_;
+
+  do {
+    /*!re2c
+    canon_no_dot = [^$ :|/.\r\n\000];
+    canon_any = canon_no_dot | ".";
+    canon_piece = ( canon_no_dot | "." canon_no_dot | ".." canon_any ) canon_any*;
+    canon_pieces = canon_piece ( "/" canon_piece )*;
+
+    // The Chromium gn manifests have many paths that start with "./" but are
+    // otherwise canonical. Allow them and strip off the leading "./".
+    ( "/" | "../"* ) canon_pieces / ([ :|] | eol)   { return finish(ofs_, p);     }
+    "./" "../"*      canon_pieces / ([ :|] | eol)   { return finish(ofs_ + 2, p); }
+    [^]                                             { break;                      }
+    */
+  } while (false);
+
+  return {};
+}
+
+bool Lexer::ReadPath(LexedPath* out, std::string* err) {
+  const char* p = ofs_;
+  const char* q;
+  const char* start;
+  for (;;) {
+    start = p;
+    /*!re2c
+    ( [^$ :|\r\n\000]
+        | "$" [$: ]
+        | "$" eol [ ]*
+        | "${" varname "}"
+        | "$" simple_varname )+ {
+      continue;
+    }
+    [ :|] | eol {
+      p = start;
+      break;
+    }
+    "$". {
+      last_token_ = start;
+      return Error("bad $-escape (literal $ must be written as $$)", err);
+    }
+    nul {
+      last_token_ = start;
+      return UnexpectedNulError(start, err);
+    }
+    [^] {
+      last_token_ = start;
+      return Error(DescribeLastError(), err);
+    }
+    */
+  }
+  *out = {};
+  out->str_ = StringPiece(ofs_, p - ofs_);
+  last_token_ = start;
+  ofs_ = p;
+  EatWhitespace();
+  return true;
+}
+
+/// Append the let binding's evaluated value to the output string. The input
+/// StringPiece must include a valid binding terminator.
+template <typename EvalVar>
+static inline void EvaluateBinding(std::string* out_append, StringPiece value,
+                                   EvalVar&& eval_var) {
+  auto expand = [&eval_var](const char* start, const char* end) {
+    StringPiece var(start, end - start);
+    eval_var(var);
+  };
+
+  const char* p = value.data();
+  const char* q;
+
+  for (;;) {
+    const char* start = p;
+    /*!re2c
+    [^$\r\n\000]+       { out_append->append(start, p - start);   continue; }
+    "$" [$: ]           { out_append->push_back(start[1]);        continue; }
+    "$" eol [ ]*        {                                         continue; }
+    "${" varname "}"    { expand(start + 2, p - 1);               continue; }
+    "$" simple_varname  { expand(start + 1, p);                   continue; }
+    eol                 {                                         break;    }
+    [^]                 { assert(false && "bad input in EvaluateBinding"); abort(); }
+    */
+  }
+  assert((p == value.data() + value.size()) &&
+         "bad end pos in EvaluateBinding");
+}
+
+void EvaluateBindingInScope(std::string* out_append, StringPiece value,
+                            ScopePosition pos) {
+  EvaluateBinding(out_append, value,
+      [out_append, &pos](const HashedStrView& var) {
+    Scope::EvaluateVariableAtPos(out_append, var, pos);
+  });
+}
+
+bool EvaluateBindingOnRule(std::string* out_append, StringPiece value,
+                           EdgeEval* edge_eval, std::string* err) {
+  bool result = true;
+  EvaluateBinding(out_append, value,
+      [out_append, edge_eval, &result, err](const HashedStrView& var) {
+    result = result && edge_eval->EvaluateVariable(out_append, var, err);
+  });
+  return result;
+}
+
+std::string EvaluateBindingForTesting(StringPiece value) {
+  std::string result;
+  EvaluateBinding(&result, value, [&result](StringPiece var) {
+    result += "[$" + var.AsString() + "]";
+  });
+  return result;
+}
+
+/// Append an evaluated path to the output string.
+///
+/// This function does not canonicalize the output. Ninja canonicalizes paths for
+/// build nodes, but not all paths (e.g. It doesn't canonicalize paths to
+/// included ninja files.)
+template <typename EvalVar>
+static inline void EvaluatePath(std::string* out_append, const LexedPath& path,
+                                EvalVar&& eval_var) {
+  auto expand = [&eval_var](const char* start, const char* end) {
+    StringPiece var(start, end - start);
+    eval_var(var);
+  };
+
+  const char* p = path.str_.data();
+  const char* q;
+
+  for (;;) {
+    const char* start = p;
+    /*!re2c
+    [^$ :|\r\n\000]+        { out_append->append(start, p - start);   continue; }
+    "$" [$: ]               { out_append->push_back(start[1]);        continue; }
+    "$" eol [ ]*            {                                         continue; }
+    "${" varname "}"        { expand(start + 2, p - 1);               continue; }
+    "$" simple_varname      { expand(start + 1, p);                   continue; }
+    [ :|] | eol             { p = start;                              break;    }
+    [^]                     { assert(false && "bad input in EvaluatePath"); abort(); }
+    */
+  }
+  assert((p == path.str_.data() + path.str_.size()) &&
+         "bad end pos in EvaluatePath");
+}
+
+void EvaluatePathInScope(std::string* out_append, const LexedPath& path,
+                         ScopePosition pos) {
+  EvaluatePath(out_append, path, [out_append, &pos](const HashedStrView& var) {
+    Scope::EvaluateVariableAtPos(out_append, var, pos);
+  });
+}
+
+void EvaluatePathOnEdge(std::string* out_append, const LexedPath& path,
+                        const Edge& edge) {
+  EvaluatePath(out_append, path, [out_append, &edge](const HashedStrView& var) {
+    // First look for a binding on the edge itself, then fall back to the
+    // edge's enclosing scope.
+    if (edge.EvaluateVariableSelfOnly(out_append, var))
+      return;
+    Scope::EvaluateVariableAtPos(out_append, var, edge.pos_.scope_pos());
+  });
+}
+
+std::string EvaluatePathForTesting(const LexedPath& path) {
+  std::string result;
+  EvaluatePath(&result, path, [&result](StringPiece var) {
+    result += "[$" + var.AsString() + "]";
+  });
+  return result;
+}
diff --git a/src/lexer_test.cc b/src/lexer_test.cc
index 331d8e1..1b69bed 100644
--- a/src/lexer_test.cc
+++ b/src/lexer_test.cc
@@ -18,28 +18,30 @@
 #include "test.h"
 
 TEST(Lexer, ReadVarValue) {
-  Lexer lexer("plain text $var $VaR ${x}\n");
-  EvalString eval;
-  string err;
-  EXPECT_TRUE(lexer.ReadVarValue(&eval, &err));
+  Lexer lexer("plain text $var $VaR ${x}\nmore text");
+  StringPiece uneval;
+  std::string err;
+  EXPECT_TRUE(lexer.ReadBindingValue(&uneval, &err));
   EXPECT_EQ("", err);
-  EXPECT_EQ("[plain text ][$var][ ][$VaR][ ][$x]",
-            eval.Serialize());
+  EXPECT_EQ("plain text $var $VaR ${x}\n", uneval.AsString());
+  std::string eval = EvaluateBindingForTesting(uneval);
+  EXPECT_EQ("plain text [$var] [$VaR] [$x]", eval);
 }
 
 TEST(Lexer, ReadEvalStringEscapes) {
-  Lexer lexer("$ $$ab c$: $\ncde\n");
-  EvalString eval;
-  string err;
-  EXPECT_TRUE(lexer.ReadVarValue(&eval, &err));
+  Lexer lexer("$ $$ab c$: $\ncde\nmore text");
+  StringPiece uneval;
+  std::string err;
+  EXPECT_TRUE(lexer.ReadBindingValue(&uneval, &err));
   EXPECT_EQ("", err);
-  EXPECT_EQ("[ $ab c: cde]",
-            eval.Serialize());
+  EXPECT_EQ("$ $$ab c$: $\ncde\n", uneval.AsString());
+  std::string eval = EvaluateBindingForTesting(uneval);
+  EXPECT_EQ(" $ab c: cde", eval);
 }
 
 TEST(Lexer, ReadIdent) {
   Lexer lexer("foo baR baz_123 foo-bar");
-  string ident;
+  StringPiece ident;
   EXPECT_TRUE(lexer.ReadIdent(&ident));
   EXPECT_EQ("foo", ident);
   EXPECT_TRUE(lexer.ReadIdent(&ident));
@@ -54,23 +56,71 @@
   // Verify that ReadIdent includes dots in the name,
   // but in an expansion $bar.dots stops at the dot.
   Lexer lexer("foo.dots $bar.dots ${bar.dots}\n");
-  string ident;
+  StringPiece ident;
   EXPECT_TRUE(lexer.ReadIdent(&ident));
   EXPECT_EQ("foo.dots", ident);
 
-  EvalString eval;
+  StringPiece uneval;
   string err;
-  EXPECT_TRUE(lexer.ReadVarValue(&eval, &err));
+  EXPECT_TRUE(lexer.ReadBindingValue(&uneval, &err));
   EXPECT_EQ("", err);
-  EXPECT_EQ("[$bar][.dots ][$bar.dots]",
-            eval.Serialize());
+  EXPECT_EQ("$bar.dots ${bar.dots}\n", uneval.AsString());
+  std::string eval = EvaluateBindingForTesting(uneval);
+  EXPECT_EQ("[$bar].dots [$bar.dots]", eval);
+}
+
+TEST(Lexer, PeekCanonicalPath) {
+  auto peek_canon = [](const char* pattern) {
+    Lexer lexer(pattern);
+    return lexer.PeekCanonicalPath().AsString();
+  };
+  EXPECT_EQ("", peek_canon(""));
+  EXPECT_EQ("", peek_canon(" : "));
+  EXPECT_EQ("", peek_canon("/ : "));
+  EXPECT_EQ("", peek_canon("/foo/ : "));
+  EXPECT_EQ("", peek_canon("/foo/../bar : "));
+  EXPECT_EQ("", peek_canon("/../foo/bar : "));
+
+  EXPECT_EQ("", peek_canon("foo$$bar : "));
+  EXPECT_EQ("", peek_canon("foo${bar}baz : "));
+
+  EXPECT_EQ("/foo", peek_canon("/foo : "));
+  EXPECT_EQ("/foo/bar", peek_canon("/foo/bar : "));
+  EXPECT_EQ("foo", peek_canon("foo : "));
+  EXPECT_EQ("foo/bar", peek_canon("foo/bar : "));
+  EXPECT_EQ("../foo/bar", peek_canon("../foo/bar : "));
+  EXPECT_EQ("../../foo/bar", peek_canon("../../foo/bar : "));
+
+  EXPECT_EQ("foo", peek_canon("./foo : "));
+  EXPECT_EQ("../../foo/bar", peek_canon("./../../foo/bar : "));
+}
+
+TEST(Lexer, ReadPath) {
+  Lexer lexer("x$$y$ z$:: $bar.dots\n");
+  std::string err;
+  LexedPath uneval;
+  std::string eval;
+
+  EXPECT_TRUE(lexer.ReadPath(&uneval, &err));
+  EXPECT_EQ("", err);
+  EXPECT_EQ("x$$y$ z$:", uneval.str_.AsString());
+  eval = EvaluatePathForTesting(uneval);
+  EXPECT_EQ("x$y z:", eval);
+
+  EXPECT_EQ(Lexer::COLON, lexer.ReadToken());
+
+  EXPECT_TRUE(lexer.ReadPath(&uneval, &err));
+  EXPECT_EQ("", err);
+  EXPECT_EQ("$bar.dots", uneval.str_.AsString());
+  eval = EvaluatePathForTesting(uneval);
+  EXPECT_EQ("[$bar].dots", eval);
 }
 
 TEST(Lexer, Error) {
   Lexer lexer("foo$\nbad $");
-  EvalString eval;
+  StringPiece uneval;
   string err;
-  ASSERT_FALSE(lexer.ReadVarValue(&eval, &err));
+  ASSERT_FALSE(lexer.ReadBindingValue(&uneval, &err));
   EXPECT_EQ("input:2: bad $-escape (literal $ must be written as $$)\n"
             "bad $\n"
             "    ^ near here"
@@ -94,3 +144,40 @@
   EXPECT_EQ(Lexer::ERROR, token);
   EXPECT_EQ("tabs are not allowed, use spaces", lexer.DescribeLastError());
 }
+
+TEST(Lexer, AdvanceToNextManifestChunk) {
+  auto advance = [](std::string input) -> std::string {
+    // The input string may optionally have a '^' marking where the lexer should
+    // start scanning for a chunk.
+    size_t idx = input.find_first_of('^');
+    if (idx == std::string::npos) {
+      idx = 0;
+    } else {
+      input = input.substr(0, idx) + input.substr(idx + 1);
+    }
+    idx = AdvanceToNextManifestChunk(input, idx);
+    // The return string has a '^' marking where the manifest was split.
+    return input.substr(0, idx) + "^" + input.substr(idx);
+  };
+
+  EXPECT_EQ("^", advance(""));
+  EXPECT_EQ("A\n^B\n", advance("A\nB\n"));
+  EXPECT_EQ("\n^B\n", advance("\nB\n"));
+  EXPECT_EQ("\0\0\0^"_s, advance("\0\0\0"_s));
+
+  // An LF preceded by "$" or "$\r" might be part of a line continuator, so we
+  // skip it when looking for a place to split the manifest.
+  EXPECT_EQ("A$\nB\n^C", advance("^A$\nB\nC"));
+  EXPECT_EQ("A$\nB\n^C", advance("A^$\nB\nC"));
+  EXPECT_EQ("A$\nB\n^C", advance("A$^\nB\nC"));
+  EXPECT_EQ("A$\r\nB\n^C", advance("^A$\r\nB\nC"));
+  EXPECT_EQ("A$\r\nB\n^C", advance("A^$\r\nB\nC"));
+  EXPECT_EQ("A$\r\nB\n^C", advance("A$^\r\nB\nC"));
+  EXPECT_EQ("A$\r\nB\n^C", advance("A$\r^\nB\nC"));
+
+  // Skip over blank lines and comment lines.
+  EXPECT_EQ("\n^", advance("\n"));
+  EXPECT_EQ("\n\n^A\n", advance("\n\nA\n"));
+  EXPECT_EQ("\n \n^A", advance("\n \nA"));
+  EXPECT_EQ("\n#\n^A", advance("\n#\nA"));
+}
diff --git a/src/manifest_chunk_parser.cc b/src/manifest_chunk_parser.cc
new file mode 100644
index 0000000..ff36752
--- /dev/null
+++ b/src/manifest_chunk_parser.cc
@@ -0,0 +1,433 @@
+// Copyright 2018 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "manifest_chunk_parser.h"
+
+#include <assert.h>
+
+#include <thread>
+#include <unordered_map>
+
+#include "lexer.h"
+#include "metrics.h"
+#include "thread_pool.h"
+#include "util.h"
+
+namespace manifest_chunk {
+
+class ChunkParser {
+  const LoadedFile& file_;
+  Lexer lexer_;
+  const char* chunk_end_ = nullptr;
+  std::vector<ParserItem>* out_ = nullptr;
+  Clump* current_clump_ = nullptr;
+
+  Clump* MakeClump() {
+    if (current_clump_)
+      return current_clump_;
+    current_clump_ = new Clump { file_ };
+    out_->push_back(current_clump_);
+    return current_clump_;
+  }
+
+  void OutItem(ParserItem item) {
+    current_clump_ = nullptr;
+    out_->push_back(item);
+  }
+
+  bool OutError(const std::string& err) {
+    OutItem(new Error { err });
+    return false;
+  }
+
+  bool LexerError(const std::string& message);
+  bool ExpectToken(Lexer::Token expected);
+  bool ParseLet(StringPiece* key, StringPiece* value);
+  bool ParseFileInclude(bool new_scope);
+  bool ParsePool();
+  bool ParseDefault();
+  bool ParseBinding();
+  bool ParseRule();
+  bool ParseEdge();
+
+public:
+  ChunkParser(const LoadedFile& file,
+              StringPiece chunk_content,
+              std::vector<ParserItem>* out)
+      : file_(file),
+        lexer_(file.filename(), file.content(), chunk_content.data()),
+        chunk_end_(chunk_content.data() + chunk_content.size()),
+        out_(out) {}
+
+  bool ParseChunk();
+};
+
+bool ChunkParser::LexerError(const std::string& message) {
+  std::string err;
+  lexer_.Error(message, &err);
+  return OutError(err);
+}
+
+bool ChunkParser::ExpectToken(Lexer::Token expected) {
+  Lexer::Token token = lexer_.ReadToken();
+  if (token != expected) {
+    std::string message = string("expected ") + Lexer::TokenName(expected);
+    message += string(", got ") + Lexer::TokenName(token);
+    message += Lexer::TokenErrorHint(expected);
+    return LexerError(message);
+  }
+  return true;
+}
+
+bool ChunkParser::ParseLet(StringPiece* key, StringPiece* value) {
+  if (!lexer_.ReadIdent(key))
+    return LexerError("expected variable name");
+  if (!ExpectToken(Lexer::EQUALS))
+    return false;
+  std::string err;
+  if (!lexer_.ReadBindingValue(value, &err))
+    return OutError(err);
+  return true;
+}
+
+bool ChunkParser::ParseFileInclude(bool new_scope) {
+  Include* include = new Include();
+  include->new_scope_ = new_scope;
+  std::string err;
+  if (!lexer_.ReadPath(&include->path_, &err))
+    return OutError(err);
+  include->diag_pos_ = lexer_.GetLastTokenOffset();
+  if (!ExpectToken(Lexer::NEWLINE))
+    return false;
+  OutItem(include);
+  return true;
+}
+
+bool ChunkParser::ParsePool() {
+  Pool* pool = new Pool();
+
+  StringPiece name;
+  if (!lexer_.ReadIdent(&name))
+    return LexerError("expected pool name");
+  pool->name_ = name;
+
+  if (!ExpectToken(Lexer::NEWLINE))
+    return false;
+
+  pool->parse_state_.pool_name_diag_pos = lexer_.GetLastTokenOffset();
+
+  while (lexer_.PeekIndent()) {
+    StringPiece key;
+    StringPiece value;
+    if (!ParseLet(&key, &value))
+      return false;
+
+    if (key == "depth") {
+      pool->parse_state_.depth = value;
+      pool->parse_state_.depth_diag_pos = lexer_.GetLastTokenOffset();
+    } else {
+      return LexerError("unexpected variable '" + key.AsString() + "'");
+    }
+  }
+
+  if (pool->parse_state_.depth.empty())
+    return LexerError("expected 'depth =' line");
+
+  Clump* clump = MakeClump();
+  pool->pos_ = clump->AllocNextPos();
+  clump->pools_.push_back(pool);
+  return true;
+}
+
+bool ChunkParser::ParseDefault() {
+  bool first = true;
+  while (true) {
+    LexedPath path;
+    std::string err;
+    if (!lexer_.ReadPath(&path, &err))
+      return OutError(err);
+    if (path.str_.empty()) {
+      if (first)
+        return LexerError("expected target name");
+      else
+        break;
+    }
+    first = false;
+
+    DefaultTarget* target = new DefaultTarget();
+    target->parsed_path_ = std::move(path);
+    target->diag_pos_ = lexer_.GetLastTokenOffset();
+
+    Clump* clump = MakeClump();
+    target->pos_ = clump->AllocNextPos();
+    clump->default_targets_.push_back(target);
+  }
+  return ExpectToken(Lexer::NEWLINE);
+}
+
+static const HashedStrView kNinjaRequiredVersion { "ninja_required_version" };
+
+bool ChunkParser::ParseBinding() {
+  Binding* binding = new Binding();
+
+  lexer_.UnreadToken();
+  StringPiece name;
+  if (!ParseLet(&name, &binding->parsed_value_))
+    return false;
+  binding->name_ = name;
+
+  // Record a ninja_required_version binding specially. We want to do this
+  // version check ASAP in case we encounter unexpected syntax. We can't do the
+  // check immediately because the version string is evaluated and might need
+  // bindings from an earlier chunk. We could probably do this version check as
+  // part of ordinary scope setup while processing a Clump, but keeping it
+  // separate guarantees that the version check is done early enough.
+  if (binding->name_ == kNinjaRequiredVersion)
+    OutItem(new RequiredVersion { binding->parsed_value_ });
+
+  Clump* clump = MakeClump();
+  binding->pos_ = clump->AllocNextPos();
+  clump->bindings_.push_back(binding);
+  return true;
+}
+
+static const HashedStrView kRspFile         { "rspfile" };
+static const HashedStrView kRspFileContent  { "rspfile_content" };
+static const HashedStrView kCommand         { "command" };
+
+bool ChunkParser::ParseRule() {
+  Rule* rule = new Rule();
+
+  StringPiece rule_name;
+  if (!lexer_.ReadIdent(&rule_name))
+    return LexerError("expected rule name");
+  rule->name_ = rule_name;
+
+  if (!ExpectToken(Lexer::NEWLINE))
+    return false;
+
+  rule->parse_state_.rule_name_diag_pos = lexer_.GetLastTokenOffset();
+
+  while (lexer_.PeekIndent()) {
+    StringPiece key;
+    StringPiece value;
+    if (!ParseLet(&key, &value))
+      return false;
+
+    if (!Rule::IsReservedBinding(key)) {
+      // Die on other keyvals for now; revisit if we want to add a scope here.
+      // If we allow arbitrary key values here, we'll need to revisit how cycle
+      // detection works when evaluating a rule variable.
+      return LexerError("unexpected variable '" + key.AsString() + "'");
+    }
+
+    rule->bindings_.emplace_back(std::piecewise_construct,
+                                 std::tuple<StringPiece>(key),
+                                 std::tuple<const char*, size_t>(value.data(),
+                                                                 value.size()));
+  }
+
+  if (static_cast<bool>(rule->GetBinding(kRspFile)) !=
+      static_cast<bool>(rule->GetBinding(kRspFileContent))) {
+    return LexerError("rspfile and rspfile_content need to be both specified");
+  }
+
+  if (rule->GetBinding(kCommand) == nullptr)
+    return LexerError("expected 'command =' line");
+
+  Clump* clump = MakeClump();
+  rule->pos_ = clump->AllocNextPos();
+  clump->rules_.push_back(rule);
+  return true;
+}
+
+bool ChunkParser::ParseEdge() {
+  Edge* edge = new Edge();
+
+  auto parse_path_list = [this, edge](Edge::DeferredPathList::Type type, int& count) -> bool {
+    const char* start_pos = lexer_.GetPos();
+    while (true) {
+      LexedPath path;
+      std::string err;
+      if (!lexer_.ReadPath(&path, &err))
+        return OutError(err);
+      if (path.str_.empty()) {
+        // In the initial parsing pass, the manifest's bindings aren't ready
+        // yet, so paths can't be evaluated. Rather than store the path itself
+        // (wasting memory), store just enough information to parse the path
+        // lists again once bindings are ready.
+        edge->parse_state_.deferred_path_lists.push_back({
+          start_pos, type, count,
+        });
+        return true;
+      }
+      count++;
+    }
+  };
+
+  if (!parse_path_list(Edge::DeferredPathList::OUTPUT,
+      edge->explicit_outs_))
+    return false;
+
+  // Add all implicit outs, counting how many as we go.
+  if (lexer_.PeekToken(Lexer::PIPE)) {
+    if (!parse_path_list(Edge::DeferredPathList::OUTPUT,
+        edge->implicit_outs_))
+      return false;
+  }
+
+  if (edge->explicit_outs_ + edge->implicit_outs_ == 0)
+    return LexerError("expected path");
+
+  if (!ExpectToken(Lexer::COLON))
+    return false;
+
+  StringPiece rule_name;
+  if (!lexer_.ReadIdent(&rule_name))
+    return LexerError("expected build command name");
+  edge->parse_state_.rule_name = rule_name;
+  edge->parse_state_.rule_name_diag_pos = lexer_.GetLastTokenOffset();
+
+  if (!parse_path_list(Edge::DeferredPathList::INPUT,
+      edge->explicit_deps_))
+    return false;
+
+  // Add all implicit deps, counting how many as we go.
+  if (lexer_.PeekToken(Lexer::PIPE)) {
+    if (!parse_path_list(Edge::DeferredPathList::INPUT,
+        edge->implicit_deps_))
+      return false;
+  }
+
+  // Add all order-only deps, counting how many as we go.
+  if (lexer_.PeekToken(Lexer::PIPE2)) {
+    if (!parse_path_list(Edge::DeferredPathList::INPUT,
+        edge->order_only_deps_))
+      return false;
+  }
+
+  // Add all validation deps, counting how many as we go.
+  if (lexer_.PeekToken(Lexer::PIPEAT)) {
+    if (!parse_path_list(Edge::DeferredPathList::VALIDATION,
+        edge->validation_deps_))
+      return false;
+  }
+
+
+  if (!ExpectToken(Lexer::NEWLINE))
+    return false;
+
+  while (lexer_.PeekIndent()) {
+    StringPiece key;
+    StringPiece val;
+    if (!ParseLet(&key, &val))
+      return false;
+
+    std::tuple<const char*, size_t> val_ctor_params(val.data(), val.size());
+    edge->unevaled_bindings_.emplace_back(std::piecewise_construct,
+                                          std::tuple<StringPiece>(key),
+                                          val_ctor_params);
+  }
+
+  edge->parse_state_.final_diag_pos = lexer_.GetLastTokenOffset();
+
+  Clump* clump = MakeClump();
+  edge->pos_ = clump->AllocNextPos();
+  clump->edges_.push_back(edge);
+  clump->edge_output_count_ += edge->explicit_outs_;
+  return true;
+}
+
+bool ChunkParser::ParseChunk() {
+  while (true) {
+    if (lexer_.GetPos() >= chunk_end_) {
+      assert(lexer_.GetPos() == chunk_end_ &&
+             "lexer scanned beyond the end of a manifest chunk");
+      return true;
+    }
+
+    Lexer::Token token = lexer_.ReadToken();
+    bool success = true;
+    switch (token) {
+    case Lexer::INCLUDE:  success = ParseFileInclude(false); break;
+    case Lexer::SUBNINJA: success = ParseFileInclude(true); break;
+    case Lexer::POOL:     success = ParsePool(); break;
+    case Lexer::DEFAULT:  success = ParseDefault(); break;
+    case Lexer::IDENT:    success = ParseBinding(); break;
+    case Lexer::RULE:     success = ParseRule(); break;
+    case Lexer::BUILD:    success = ParseEdge(); break;
+    case Lexer::NEWLINE:  break;
+    case Lexer::ERROR:    return LexerError(lexer_.DescribeLastError());
+    case Lexer::TNUL:     return LexerError("unexpected NUL byte");
+    case Lexer::TEOF:
+      assert(false && "EOF should have been detected before reading a token");
+      break;
+    default:
+      return LexerError(std::string("unexpected ") + Lexer::TokenName(token));
+    }
+    if (!success) return false;
+  }
+  return false;  // not reached
+}
+
+void ParseChunk(const LoadedFile& file, StringPiece chunk_content,
+                std::vector<ParserItem>* out) {
+  ChunkParser parser(file, chunk_content, out);
+  if (!parser.ParseChunk()) {
+    assert(!out->empty());
+    assert(out->back().kind == ParserItem::kError);
+    assert(!out->back().u.error->msg_.empty());
+  }
+}
+
+std::vector<StringPiece> SplitManifestIntoChunks(StringPiece input) {
+  METRIC_RECORD(".ninja load : split manifest");
+
+  // The lexer requires that a NUL character appear after the file's content.
+  // Make the NUL implicit for the rest of the manifest parsing.
+  //
+  // In general, parsing a manifest chunk examines the terminating text after
+  // the chunk's explicit end. For example, suppose we have two consecutive
+  // chunks:
+  //  - "build foo: gen\n  pool = mypool\n"
+  //  - "build bar: gen\n"
+  // The parser for the "foo" chunk will examine the start of "build" from the
+  // "bar" chunk when it looks for another indented edge binding.
+  assert(!input.empty() && input.back() == '\0' &&
+         "input lacks a NUL terminator");
+  input.remove_suffix(1);
+
+  // For efficiency, avoid splitting small files. kChunkMinSize can be lowered
+  // to 0 for testing.
+  const size_t kChunkMinSize = 1024 * 1024;
+
+  const size_t chunk_count = GetOptimalThreadPoolJobCount();
+  const size_t chunk_size = std::max(kChunkMinSize,
+                                     input.size() / chunk_count + 1);
+
+  std::vector<StringPiece> result;
+  result.reserve(chunk_count);
+
+  size_t start = 0;
+  while (start < input.size()) {
+    size_t next = std::min(start + chunk_size, input.size());
+    next = AdvanceToNextManifestChunk(input, next);
+    result.push_back(input.substr(start, next - start));
+    start = next;
+  }
+
+  return result;
+}
+
+} // namespace manifest_chunk
diff --git a/src/manifest_chunk_parser.h b/src/manifest_chunk_parser.h
new file mode 100644
index 0000000..665a805
--- /dev/null
+++ b/src/manifest_chunk_parser.h
@@ -0,0 +1,112 @@
+// Copyright 2018 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef NINJA_MANIFEST_CHUNK_PARSER_H_
+#define NINJA_MANIFEST_CHUNK_PARSER_H_
+
+#include <stdlib.h>
+
+#include <string>
+#include <vector>
+
+#include "disk_interface.h"
+#include "eval_env.h"
+#include "graph.h"
+#include "state.h"
+#include "string_piece.h"
+
+struct Lexer;
+
+namespace manifest_chunk {
+
+struct Error {
+  std::string msg_;
+};
+
+struct RequiredVersion {
+  StringPiece version_;
+};
+
+struct Include {
+  LexedPath path_;
+  bool new_scope_ = false;
+  size_t diag_pos_ = 0;
+};
+
+struct DefaultTarget {
+  RelativePosition pos_;
+  LexedPath parsed_path_;
+  size_t diag_pos_ = 0;
+};
+
+/// A group of manifest declarations generated while parsing a chunk.
+struct Clump {
+  Clump(const LoadedFile& file) : file_(file) {}
+
+  const LoadedFile& file_;
+  BasePosition pos_;
+
+  std::vector<Binding*> bindings_;
+  std::vector<Rule*> rules_;
+  std::vector<Pool*> pools_;
+  std::vector<Edge*> edges_;
+  std::vector<DefaultTarget*> default_targets_;
+
+  /// A count of non-implicit outputs across all edges.
+  size_t edge_output_count_ = 0;
+
+  DeclIndex decl_count() const { return next_index_; }
+
+  /// Allocate an index within the clump. Once the parallelized chunk parsing is
+  /// finished, each clump's base position will be computed, giving every clump
+  /// item both a DFS "depth-first-search" position and a position within the
+  /// tree of scopes.
+  RelativePosition AllocNextPos() { return { &pos_, next_index_++ }; }
+
+private:
+  DeclIndex next_index_ = 0;
+};
+
+/// This class could be replaced with std::variant from C++17.
+struct ParserItem {
+  enum Kind {
+    kError, kRequiredVersion, kInclude, kClump
+  };
+
+  Kind kind;
+
+  union {
+    Error* error;
+    RequiredVersion* required_version;
+    Include* include;
+    Clump* clump;
+  } u;
+
+  ParserItem(Error* val)            : kind(kError)            { u.error             = val; }
+  ParserItem(RequiredVersion* val)  : kind(kRequiredVersion)  { u.required_version  = val; }
+  ParserItem(Include* val)          : kind(kInclude)          { u.include           = val; }
+  ParserItem(Clump* val)            : kind(kClump)            { u.clump             = val; }
+};
+
+/// Parse a chunk of a manifest. If the parser encounters a syntactic error, the
+/// final item will be an error object.
+void ParseChunk(const LoadedFile& file, StringPiece chunk_content,
+                std::vector<ParserItem>* out);
+
+/// Split the input into chunks of declarations.
+std::vector<StringPiece> SplitManifestIntoChunks(StringPiece input);
+
+} // namespace manifest_chunk
+
+#endif  // NINJA_MANIFEST_CHUNK_PARSER_H_
diff --git a/src/manifest_parser.cc b/src/manifest_parser.cc
index 27c423b..1e4246c 100644
--- a/src/manifest_parser.cc
+++ b/src/manifest_parser.cc
@@ -1,4 +1,4 @@
-// Copyright 2011 Google Inc. All Rights Reserved.
+// Copyright 2018 Google Inc. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,434 +14,597 @@
 
 #include "manifest_parser.h"
 
-#include <stdio.h>
-#include <stdlib.h>
+#include <assert.h>
+
+#include <unordered_map>
 #include <vector>
 
 #include "disk_interface.h"
-#include "graph.h"
+#include "eval_env.h"
+#include "lexer.h"
 #include "metrics.h"
+#include "parallel_map.h"
 #include "state.h"
 #include "util.h"
 #include "version.h"
+#include "manifest_chunk_parser.h"
 
-ManifestParser::ManifestParser(State* state, FileReader* file_reader,
-                               ManifestParserOptions options)
-    : state_(state), file_reader_(file_reader),
-      options_(options), quiet_(false) {
-  env_ = &state->bindings_;
+using manifest_chunk::Clump;
+using manifest_chunk::DefaultTarget;
+using manifest_chunk::Include;
+using manifest_chunk::ParserItem;
+using manifest_chunk::RequiredVersion;
+
+namespace {
+
+static bool DecorateError(const LoadedFile& file, size_t file_offset,
+                          const std::string& message, std::string* err) {
+  assert(file_offset <= file.content().size());
+  return DecorateErrorWithLocation(file.filename(), file.content().data(),
+                                   file_offset, message, err);
 }
 
-bool ManifestParser::Load(const string& filename, string* err, Lexer* parent) {
-  METRIC_RECORD(".ninja parse");
-  string contents;
-  string read_err;
-  if (file_reader_->ReadFile(filename, &contents, &read_err) != FileReader::Okay) {
-    *err = "loading '" + filename + "': " + read_err;
-    if (parent)
-      parent->Error(string(*err), err);
+/// Split a single manifest file into chunks, parse the chunks in parallel, and
+/// return the resulting parser output.
+static std::vector<ParserItem> ParseManifestChunks(const LoadedFile& file,
+                                                   ThreadPool* thread_pool) {
+  std::vector<ParserItem> result;
+  const std::vector<StringPiece> chunk_views =
+    manifest_chunk::SplitManifestIntoChunks(file.content_with_nul());
+
+  METRIC_RECORD(".ninja load : parse chunks");
+
+  for (std::vector<ParserItem>& chunk_items :
+      ParallelMap(thread_pool, chunk_views, [&file](StringPiece view) {
+    std::vector<ParserItem> chunk_items;
+    manifest_chunk::ParseChunk(file, view, &chunk_items);
+    return chunk_items;
+  })) {
+    std::move(chunk_items.begin(), chunk_items.end(),
+              std::back_inserter(result));
+  }
+  return result;
+}
+
+static void ReserveSpaceInScopeTables(Scope* scope,
+                                      const std::vector<ParserItem>& items) {
+  // Reserve extra space in the scope's bindings/rules hash tables.
+  METRIC_RECORD(".ninja load : alloc scope tables");
+  size_t new_bindings = 0;
+  size_t new_rules = 0;
+  for (const auto& item : items) {
+    if (item.kind == ParserItem::kClump) {
+      Clump* clump = item.u.clump;
+      new_bindings += clump->bindings_.size();
+      new_rules += clump->rules_.size();
+    }
+  }
+  scope->ReserveTableSpace(new_bindings, new_rules);
+}
+
+struct ManifestFileSet {
+  ManifestFileSet(FileReader* file_reader) : file_reader_(file_reader) {}
+
+  bool LoadFile(const std::string& filename, const LoadedFile** result,
+                std::string* err);
+
+  ~ManifestFileSet() {
+    // It can take a surprising long time to unmap all the manifest files
+    // (e.g. ~70 ms for Android, 25 ms for Chromium).
+    METRIC_RECORD(".ninja load : unload files");
+    loaded_files_.clear();
+  }
+
+private:
+  FileReader *file_reader_ = nullptr;
+
+  /// Keep all the manifests in memory until all the manifest parsing work is
+  /// complete, because intermediate parsing results (i.e. StringPiece) may
+  /// point into the loaded files.
+  std::vector<std::unique_ptr<LoadedFile>> loaded_files_;
+};
+
+bool ManifestFileSet::LoadFile(const std::string& filename,
+                               const LoadedFile** result,
+                               std::string* err) {
+  std::unique_ptr<LoadedFile> loaded_file;
+  if (file_reader_->LoadFile(filename, &loaded_file, err) != FileReader::Okay) {
+    *err = "loading '" + filename + "': " + *err;
     return false;
   }
-
-  // The lexer needs a nul byte at the end of its input, to know when it's done.
-  // It takes a StringPiece, and StringPiece's string constructor uses
-  // string::data().  data()'s return value isn't guaranteed to be
-  // null-terminated (although in practice - libc++, libstdc++, msvc's stl --
-  // it is, and C++11 demands that too), so add an explicit nul byte.
-  contents.resize(contents.size() + 1);
-
-  return Parse(filename, contents, err);
-}
-
-bool ManifestParser::Parse(const string& filename, const string& input,
-                           string* err) {
-  lexer_.Start(filename, input);
-
-  for (;;) {
-    Lexer::Token token = lexer_.ReadToken();
-    switch (token) {
-    case Lexer::POOL:
-      if (!ParsePool(err))
-        return false;
-      break;
-    case Lexer::BUILD:
-      if (!ParseEdge(err))
-        return false;
-      break;
-    case Lexer::RULE:
-      if (!ParseRule(err))
-        return false;
-      break;
-    case Lexer::DEFAULT:
-      if (!ParseDefault(err))
-        return false;
-      break;
-    case Lexer::IDENT: {
-      lexer_.UnreadToken();
-      string name;
-      EvalString let_value;
-      if (!ParseLet(&name, &let_value, err))
-        return false;
-      string value = let_value.Evaluate(env_);
-      // Check ninja_required_version immediately so we can exit
-      // before encountering any syntactic surprises.
-      if (name == "ninja_required_version")
-        CheckNinjaVersion(value);
-      env_->AddBinding(name, value);
-      break;
-    }
-    case Lexer::INCLUDE:
-      if (!ParseFileInclude(false, err))
-        return false;
-      break;
-    case Lexer::SUBNINJA:
-      if (!ParseFileInclude(true, err))
-        return false;
-      break;
-    case Lexer::ERROR: {
-      return lexer_.Error(lexer_.DescribeLastError(), err);
-    }
-    case Lexer::TEOF:
-      return true;
-    case Lexer::NEWLINE:
-      break;
-    default:
-      return lexer_.Error(string("unexpected ") + Lexer::TokenName(token),
-                          err);
-    }
-  }
-  return false;  // not reached
-}
-
-
-bool ManifestParser::ParsePool(string* err) {
-  string name;
-  if (!lexer_.ReadIdent(&name))
-    return lexer_.Error("expected pool name", err);
-
-  if (!ExpectToken(Lexer::NEWLINE, err))
-    return false;
-
-  if (state_->LookupPool(name) != NULL)
-    return lexer_.Error("duplicate pool '" + name + "'", err);
-
-  int depth = -1;
-
-  while (lexer_.PeekToken(Lexer::INDENT)) {
-    string key;
-    EvalString value;
-    if (!ParseLet(&key, &value, err))
-      return false;
-
-    if (key == "depth") {
-      string depth_string = value.Evaluate(env_);
-      depth = atol(depth_string.c_str());
-      if (depth < 0)
-        return lexer_.Error("invalid pool depth", err);
-    } else {
-      return lexer_.Error("unexpected variable '" + key + "'", err);
-    }
-  }
-
-  if (depth < 0)
-    return lexer_.Error("expected 'depth =' line", err);
-
-  state_->AddPool(new Pool(name, depth));
+  loaded_files_.push_back(std::move(loaded_file));
+  *result = loaded_files_.back().get();
   return true;
 }
 
+struct DfsParser {
+  DfsParser(ManifestFileSet* file_set, State* state, ThreadPool* thread_pool)
+      : file_set_(file_set), state_(state), thread_pool_(thread_pool) {}
 
-bool ManifestParser::ParseRule(string* err) {
-  string name;
-  if (!lexer_.ReadIdent(&name))
-    return lexer_.Error("expected rule name", err);
+private:
+  void HandleRequiredVersion(const RequiredVersion& item, Scope* scope);
+  bool HandleInclude(const Include& item, const LoadedFile& file, Scope* scope,
+                     const LoadedFile** child_file, Scope** child_scope,
+                     std::string* err);
+  bool HandlePool(Pool* pool, const LoadedFile& file, std::string* err);
+  bool HandleClump(Clump* clump, const LoadedFile& file, Scope* scope,
+                   std::string* err);
 
-  if (!ExpectToken(Lexer::NEWLINE, err))
-    return false;
+public:
+  /// Load the tree of manifest files and do initial parsing of all the chunks.
+  /// This function always runs on the main thread.
+  bool LoadManifestTree(const LoadedFile& file, Scope* scope,
+                        std::vector<Clump*>* out_clumps, std::string* err);
 
-  if (env_->LookupRuleCurrentScope(name) != NULL)
-    return lexer_.Error("duplicate rule '" + name + "'", err);
+private:
+  ManifestFileSet* file_set_;
+  State* state_;
+  ThreadPool* thread_pool_;
+};
 
-  Rule* rule = new Rule(name);  // XXX scoped_ptr
+void DfsParser::HandleRequiredVersion(const RequiredVersion& item,
+                                      Scope* scope) {
+  std::string version;
+  EvaluateBindingInScope(&version, item.version_,
+                         scope->GetCurrentEndOfScope());
+  CheckNinjaVersion(version);
+}
 
-  while (lexer_.PeekToken(Lexer::INDENT)) {
-    string key;
-    EvalString value;
-    if (!ParseLet(&key, &value, err))
-      return false;
-
-    if (Rule::IsReservedBinding(key)) {
-      rule->AddBinding(key, value);
-    } else {
-      // Die on other keyvals for now; revisit if we want to add a
-      // scope here.
-      return lexer_.Error("unexpected variable '" + key + "'", err);
-    }
+bool DfsParser::HandleInclude(const Include& include, const LoadedFile& file,
+                              Scope* scope, const LoadedFile** child_file,
+                              Scope** child_scope, std::string* err) {
+  std::string path;
+  EvaluatePathInScope(&path, include.path_, scope->GetCurrentEndOfScope());
+  if (!file_set_->LoadFile(path, child_file, err)) {
+    return DecorateError(file, include.diag_pos_, std::string(*err), err);
   }
-
-  if (rule->bindings_["rspfile"].empty() !=
-      rule->bindings_["rspfile_content"].empty()) {
-    return lexer_.Error("rspfile and rspfile_content need to be "
-                        "both specified", err);
+  if (include.new_scope_) {
+    *child_scope = new Scope(scope->GetCurrentEndOfScope());
+  } else {
+    *child_scope = scope;
   }
-
-  if (rule->bindings_["command"].empty())
-    return lexer_.Error("expected 'command =' line", err);
-
-  env_->AddRule(rule);
   return true;
 }
 
-bool ManifestParser::ParseLet(string* key, EvalString* value, string* err) {
-  if (!lexer_.ReadIdent(key))
-    return lexer_.Error("expected variable name", err);
-  if (!ExpectToken(Lexer::EQUALS, err))
-    return false;
-  if (!lexer_.ReadVarValue(value, err))
-    return false;
+bool DfsParser::HandlePool(Pool* pool, const LoadedFile& file,
+                           std::string* err) {
+  std::string depth_string;
+  EvaluateBindingInScope(&depth_string, pool->parse_state_.depth,
+                         pool->pos_.scope_pos());
+  pool->depth_ = atol(depth_string.c_str());
+  if (pool->depth_ < 0) {
+    return DecorateError(file, pool->parse_state_.depth_diag_pos,
+                         "invalid pool depth", err);
+  }
+  if (!state_->AddPool(pool)) {
+    return DecorateError(file, pool->parse_state_.pool_name_diag_pos,
+                         "duplicate pool '" + pool->name() + "'", err);
+  }
   return true;
 }
 
-bool ManifestParser::ParseDefault(string* err) {
-  EvalString eval;
-  if (!lexer_.ReadPath(&eval, err))
-    return false;
-  if (eval.empty())
-    return lexer_.Error("expected target name", err);
-
-  do {
-    string path = eval.Evaluate(env_);
-    string path_err;
-    uint64_t slash_bits;  // Unused because this only does lookup.
-    if (!CanonicalizePath(&path, &slash_bits, &path_err))
-      return lexer_.Error(path_err, err);
-    if (!state_->AddDefault(path, &path_err))
-      return lexer_.Error(path_err, err);
-
-    eval.Clear();
-    if (!lexer_.ReadPath(&eval, err))
-      return false;
-  } while (!eval.empty());
-
-  if (!ExpectToken(Lexer::NEWLINE, err))
-    return false;
-
-  return true;
-}
-
-bool ManifestParser::ParseEdge(string* err) {
-  vector<EvalString> ins, outs;
-
+bool DfsParser::HandleClump(Clump* clump, const LoadedFile& file, Scope* scope,
+                            std::string* err) {
+  METRIC_RECORD(".ninja load : scope setup");
+  // Allocate DFS and scope positions for the clump.
+  clump->pos_.scope = scope->AllocDecls(clump->decl_count());
+  clump->pos_.dfs_location = state_->AllocDfsLocation(clump->decl_count());
   {
-    EvalString out;
-    if (!lexer_.ReadPath(&out, err))
-      return false;
-    while (!out.empty()) {
-      outs.push_back(out);
-
-      out.Clear();
-      if (!lexer_.ReadPath(&out, err))
-        return false;
+    METRIC_RECORD(".ninja load : scope setup : bindings");
+    for (Binding* binding : clump->bindings_) {
+      scope->AddBinding(binding);
     }
   }
-
-  // Add all implicit outs, counting how many as we go.
-  int implicit_outs = 0;
-  if (lexer_.PeekToken(Lexer::PIPE)) {
-    for (;;) {
-      EvalString out;
-      if (!lexer_.ReadPath(&out, err))
-        return err;
-      if (out.empty())
-        break;
-      outs.push_back(out);
-      ++implicit_outs;
-    }
-  }
-
-  if (outs.empty())
-    return lexer_.Error("expected path", err);
-
-  if (!ExpectToken(Lexer::COLON, err))
-    return false;
-
-  string rule_name;
-  if (!lexer_.ReadIdent(&rule_name))
-    return lexer_.Error("expected build command name", err);
-
-  const Rule* rule = env_->LookupRule(rule_name);
-  if (!rule)
-    return lexer_.Error("unknown build rule '" + rule_name + "'", err);
-
-  for (;;) {
-    // XXX should we require one path here?
-    EvalString in;
-    if (!lexer_.ReadPath(&in, err))
-      return false;
-    if (in.empty())
-      break;
-    ins.push_back(in);
-  }
-
-  // Add all implicit deps, counting how many as we go.
-  int implicit = 0;
-  if (lexer_.PeekToken(Lexer::PIPE)) {
-    for (;;) {
-      EvalString in;
-      if (!lexer_.ReadPath(&in, err))
-        return err;
-      if (in.empty())
-        break;
-      ins.push_back(in);
-      ++implicit;
-    }
-  }
-
-  // Add all order-only deps, counting how many as we go.
-  int order_only = 0;
-  if (lexer_.PeekToken(Lexer::PIPE2)) {
-    for (;;) {
-      EvalString in;
-      if (!lexer_.ReadPath(&in, err))
-        return false;
-      if (in.empty())
-        break;
-      ins.push_back(in);
-      ++order_only;
-    }
-  }
-
-  if (!ExpectToken(Lexer::NEWLINE, err))
-    return false;
-
-  // Bindings on edges are rare, so allocate per-edge envs only when needed.
-  bool has_indent_token = lexer_.PeekToken(Lexer::INDENT);
-  BindingEnv* env = has_indent_token ? new BindingEnv(env_) : env_;
-  while (has_indent_token) {
-    string key;
-    EvalString val;
-    if (!ParseLet(&key, &val, err))
-      return false;
-
-    env->AddBinding(key, val.Evaluate(env_));
-    has_indent_token = lexer_.PeekToken(Lexer::INDENT);
-  }
-
-  Edge* edge = state_->AddEdge(rule);
-  edge->env_ = env;
-
-  string pool_name = edge->GetBinding("pool");
-  if (!pool_name.empty()) {
-    Pool* pool = state_->LookupPool(pool_name);
-    if (pool == NULL)
-      return lexer_.Error("unknown pool name '" + pool_name + "'", err);
-    edge->pool_ = pool;
-  }
-
-  edge->outputs_.reserve(outs.size());
-  for (size_t i = 0, e = outs.size(); i != e; ++i) {
-    string path = outs[i].Evaluate(env);
-    string path_err;
-    uint64_t slash_bits;
-    if (!CanonicalizePath(&path, &slash_bits, &path_err))
-      return lexer_.Error(path_err, err);
-    if (!state_->AddOut(edge, path, slash_bits)) {
-      if (options_.dupe_edge_action_ == kDupeEdgeActionError) {
-        lexer_.Error("multiple rules generate " + path + " [-w dupbuild=err]",
-                     err);
-        return false;
-      } else {
-        if (!quiet_) {
-          Warning("multiple rules generate %s. "
-                  "builds involving this target will not be correct; "
-                  "continuing anyway [-w dupbuild=warn]",
-                  path.c_str());
-        }
-        if (e - i <= static_cast<size_t>(implicit_outs))
-          --implicit_outs;
+  {
+    METRIC_RECORD(".ninja load : scope setup : rules");
+    for (Rule* rule : clump->rules_) {
+      if (!scope->AddRule(rule)) {
+        return DecorateError(file, rule->parse_state_.rule_name_diag_pos,
+                             "duplicate rule '" + rule->name() + "'", err);
       }
     }
   }
-  if (edge->outputs_.empty()) {
-    // All outputs of the edge are already created by other edges. Don't add
-    // this edge.  Do this check before input nodes are connected to the edge.
-    state_->edges_.pop_back();
-    delete edge;
-    return true;
+  for (Pool* pool : clump->pools_) {
+    if (!HandlePool(pool, file, err)) {
+      return false;
+    }
   }
-  edge->implicit_outs_ = implicit_outs;
+  return true;
+}
 
-  edge->inputs_.reserve(ins.size());
-  for (vector<EvalString>::iterator i = ins.begin(); i != ins.end(); ++i) {
-    string path = i->Evaluate(env);
-    string path_err;
-    uint64_t slash_bits;
-    if (!CanonicalizePath(&path, &slash_bits, &path_err))
-      return lexer_.Error(path_err, err);
-    state_->AddIn(edge, path, slash_bits);
+bool DfsParser::LoadManifestTree(const LoadedFile& file, Scope* scope,
+                                 std::vector<Clump*>* out_clumps,
+                                 std::string* err) {
+  std::vector<ParserItem> items = ParseManifestChunks(file, thread_pool_);
+  ReserveSpaceInScopeTables(scope, items);
+
+  // With the chunks parsed, do a depth-first parse of the ninja manifest using
+  // the results of the parallel parse.
+  for (const auto& item : items) {
+    switch (item.kind) {
+    case ParserItem::kError:
+      *err = item.u.error->msg_;
+      return false;
+
+    case ParserItem::kRequiredVersion:
+      HandleRequiredVersion(*item.u.required_version, scope);
+      break;
+
+    case ParserItem::kInclude: {
+      const Include& include = *item.u.include;
+      const LoadedFile* child_file = nullptr;
+      Scope* child_scope = nullptr;
+      if (!HandleInclude(include, file, scope, &child_file, &child_scope, err))
+        return false;
+      if (!LoadManifestTree(*child_file, child_scope, out_clumps, err))
+        return false;
+      break;
+    }
+
+    case ParserItem::kClump:
+      if (!HandleClump(item.u.clump, file, scope, err))
+        return false;
+      out_clumps->push_back(item.u.clump);
+      break;
+
+    default:
+      assert(false && "unrecognized kind of ParserItem");
+      abort();
+    }
   }
-  edge->implicit_deps_ = implicit;
-  edge->order_only_deps_ = order_only;
 
+  return true;
+}
+
+/// Parse an edge's path and add it to a vector on the Edge object.
+static inline bool AddPathToEdge(State* state, const Edge& edge,
+                                 std::vector<Node*>* out_vec,
+                                 const LoadedFile& file, Lexer& lexer,
+                                 std::string* err) {
+  HashedStrView key;
+  uint64_t slash_bits = 0;
+
+  StringPiece canon_path = lexer.PeekCanonicalPath();
+  if (!canon_path.empty()) {
+    key = canon_path;
+  } else {
+    LexedPath path;
+    if (!lexer.ReadPath(&path, err) || path.str_.empty()) {
+      assert(false && "manifest file apparently changed during parsing");
+      abort();
+    }
+
+    thread_local std::string tls_work_buf;
+    std::string& work_buf = tls_work_buf;
+    work_buf.clear();
+    EvaluatePathOnEdge(&work_buf, path, edge);
+
+    std::string path_err;
+    if (!CanonicalizePath(&work_buf, &slash_bits, &path_err)) {
+      return DecorateError(file, edge.parse_state_.final_diag_pos, path_err,
+                           err);
+    }
+    key = work_buf;
+  }
+
+  Node* node = state->GetNode(key, 0);
+  node->UpdateFirstReference(edge.dfs_location(), slash_bits);
+  out_vec->push_back(node);
+  return true;
+}
+
+static const HashedStrView kPool { "pool" };
+static const HashedStrView kDeps { "deps" };
+
+struct ManifestLoader {
+private:
+  State* const state_ = nullptr;
+  ThreadPool* const thread_pool_ = nullptr;
+  const ManifestParserOptions options_;
+  const bool quiet_ = false;
+
+  bool AddEdgeToGraph(Edge* edge, const LoadedFile& file, std::string* err);
+
+  /// This function runs on a worker thread and adds a clump's declarations to
+  /// the build graph.
+  bool FinishAddingClumpToGraph(Clump* clump, std::string* err);
+
+  bool FinishLoading(const std::vector<Clump*>& clumps, std::string* err);
+
+public:
+  ManifestLoader(State* state, ThreadPool* thread_pool,
+                 ManifestParserOptions options, bool quiet)
+      : state_(state), thread_pool_(thread_pool), options_(options),
+        quiet_(quiet) {}
+
+  bool Load(ManifestFileSet* file_set, const LoadedFile& root_manifest,
+            std::string* err);
+};
+
+bool ManifestLoader::AddEdgeToGraph(Edge* edge, const LoadedFile& file,
+                                    std::string* err) {
+
+  const ScopePosition edge_pos = edge->pos_.scope_pos();
+
+  // Look up the edge's rule.
+  edge->rule_ = Scope::LookupRuleAtPos(edge->parse_state_.rule_name, edge_pos);
+  if (edge->rule_ == nullptr) {
+    std::string msg = "unknown build rule '" +
+        edge->parse_state_.rule_name.AsString() + "'";
+    return DecorateError(file, edge->parse_state_.rule_name_diag_pos, msg, err);
+  }
+
+  // Now that the edge's bindings are available, check whether the edge has a
+  // pool. This check requires the full edge+rule evaluation system.
+  std::string pool_name;
+  if (!edge->EvaluateVariable(&pool_name, kPool, err, EdgeEval::kParseTime))
+    return false;
+  if (pool_name.empty()) {
+    edge->pool_ = &State::kDefaultPool;
+  } else {
+    edge->pool_ = state_->LookupPoolAtPos(pool_name, edge->pos_.dfs_location());
+    if (edge->pool_ == nullptr) {
+      return DecorateError(file, edge->parse_state_.final_diag_pos,
+                           "unknown pool name '" + pool_name + "'", err);
+    }
+  }
+
+  edge->outputs_.reserve(edge->explicit_outs_ + edge->implicit_outs_);
+  edge->inputs_.reserve(edge->explicit_deps_ + edge->implicit_deps_ +
+                        edge->order_only_deps_);
+  edge->validations_.reserve(edge->validation_deps_);
+
+  // Add the input and output nodes. We already lexed them in the first pass,
+  // but we couldn't add them because scope bindings weren't available. To save
+  // memory, the first pass only recorded the lexer position of each category
+  // of input/output nodes, rather than each path's location.
+  Lexer lexer(file.filename(), file.content(), file.content().data());
+  for (const Edge::DeferredPathList& path_list :
+      edge->parse_state_.deferred_path_lists) {
+    std::vector<Node*>* vec =
+        path_list.type == Edge::DeferredPathList::INPUT ? &edge->inputs_ :
+        path_list.type == Edge::DeferredPathList::OUTPUT ? &edge->outputs_ :
+        &edge->validations_;
+    lexer.ResetPos(path_list.lexer_pos);
+    for (int i = 0; i < path_list.count; ++i) {
+      if (!AddPathToEdge(state_, *edge, vec, file, lexer, err))
+        return false;
+    }
+    // Verify that there are no more paths to parse.
+    LexedPath path;
+    if (!lexer.ReadPath(&path, err) || !path.str_.empty()) {
+      assert(false && "manifest file apparently changed during parsing");
+      abort();
+    }
+  }
+
+  // This compatibility mode filters nodes from the edge->inputs_ list; do it
+  // before linking the edge inputs and nodes.
   if (options_.phony_cycle_action_ == kPhonyCycleActionWarn &&
       edge->maybe_phonycycle_diagnostic()) {
-    // CMake 2.8.12.x and 3.0.x incorrectly write phony build statements
-    // that reference themselves.  Ninja used to tolerate these in the
-    // build graph but that has since been fixed.  Filter them out to
-    // support users of those old CMake versions.
+    // CMake 2.8.12.x and 3.0.x incorrectly write phony build statements that
+    // reference themselves.  Ninja used to tolerate these in the build graph
+    // but that has since been fixed.  Filter them out to support users of those
+    // old CMake versions.
     Node* out = edge->outputs_[0];
-    vector<Node*>::iterator new_end =
-        remove(edge->inputs_.begin(), edge->inputs_.end(), out);
+    std::vector<Node*>::iterator new_end =
+        std::remove(edge->inputs_.begin(), edge->inputs_.end(), out);
     if (new_end != edge->inputs_.end()) {
       edge->inputs_.erase(new_end, edge->inputs_.end());
+      --edge->explicit_deps_;
       if (!quiet_) {
-        Warning("phony target '%s' names itself as an input; "
-                "ignoring [-w phonycycle=warn]",
-                out->path().c_str());
+        Warning("phony target '%s' names itself as an input; ignoring "
+                "[-w phonycycle=warn]", out->path().c_str());
       }
     }
   }
 
   // Multiple outputs aren't (yet?) supported with depslog.
-  string deps_type = edge->GetBinding("deps");
-  if (!deps_type.empty() && edge->outputs_.size() > 1) {
-    return lexer_.Error("multiple outputs aren't (yet?) supported by depslog; "
-                        "bring this up on the mailing list if it affects you",
-                        err);
+  std::string deps_type;
+  if (!edge->EvaluateVariable(&deps_type, kDeps, err, EdgeEval::kParseTime))
+    return false;
+  if (!deps_type.empty() && edge->outputs_.size() - edge->implicit_outs_ > 1) {
+    return DecorateError(file, edge->parse_state_.final_diag_pos,
+                         "multiple outputs aren't (yet?) supported by depslog; "
+                         "bring this up on the mailing list if it affects you",
+                         err);
   }
 
   return true;
 }
 
-bool ManifestParser::ParseFileInclude(bool new_scope, string* err) {
-  EvalString eval;
-  if (!lexer_.ReadPath(&eval, err))
-    return false;
-  string path = eval.Evaluate(env_);
+bool ManifestLoader::FinishAddingClumpToGraph(Clump* clump, std::string* err) {
+  std::string work_buf;
 
-  ManifestParser subparser(state_, file_reader_, options_);
-  if (new_scope) {
-    subparser.env_ = new BindingEnv(env_);
-  } else {
-    subparser.env_ = env_;
+  // Precompute all binding values. Discard each evaluated string -- we just
+  // need to make sure each binding's value isn't coming from the mmap'ed
+  // manifest anymore.
+  for (Binding* binding : clump->bindings_) {
+    work_buf.clear();
+    binding->Evaluate(&work_buf);
   }
 
-  if (!subparser.Load(path, err, &lexer_))
-    return false;
-
-  if (!ExpectToken(Lexer::NEWLINE, err))
-    return false;
+  for (Edge* edge : clump->edges_) {
+    if (!AddEdgeToGraph(edge, clump->file_, err))
+      return false;
+  }
 
   return true;
 }
 
-bool ManifestParser::ExpectToken(Lexer::Token expected, string* err) {
-  Lexer::Token token = lexer_.ReadToken();
-  if (token != expected) {
-    string message = string("expected ") + Lexer::TokenName(expected);
-    message += string(", got ") + Lexer::TokenName(token);
-    message += Lexer::TokenErrorHint(expected);
-    return lexer_.Error(message, err);
+bool ManifestLoader::FinishLoading(const std::vector<Clump*>& clumps,
+                                   std::string* err) {
+  {
+    // Most of this pass's time is spent adding the edges. (i.e. The time spent
+    // evaluating the bindings is negligible.)
+    METRIC_RECORD(".ninja load : edge setup");
+
+    size_t output_count = 0;
+    for (Clump* clump : clumps)
+      output_count += clump->edge_output_count_;
+
+    // Construct the initial graph of input/output nodes. Select an initial size
+    // that's likely to keep the number of collisions low. The number of edges'
+    // non-implicit outputs is a decent enough proxy for the final number of
+    // nodes. (I see acceptable performance even with a much lower number of
+    // buckets, e.g. 100 times fewer.)
+    state_->paths_.reserve(state_->paths_.size() + output_count * 3);
+
+    if (!PropagateError(err, ParallelMap(thread_pool_, clumps,
+        [this](Clump* clump) {
+      std::string err;
+      FinishAddingClumpToGraph(clump, &err);
+      return err;
+    }))) {
+      return false;
+    }
   }
+  {
+    // Record the in-edge for each node that's built by an edge. Detect
+    // duplicate edges.
+    //
+    // With dupbuild=warn (the default until 1.9.0), when two edges generate the
+    // same node, remove the duplicate node from the output list of the later
+    // edge. If all of an edge's outputs are removed, remove the edge from the
+    // graph.
+    METRIC_RECORD(".ninja load : link edge outputs");
+    for (Clump* clump : clumps) {
+      for (size_t edge_idx = 0; edge_idx < clump->edges_.size(); ) {
+        // Scan all Edge outputs and link them to the Node objects.
+        Edge* edge = clump->edges_[edge_idx];
+        for (size_t i = 0; i < edge->outputs_.size(); ) {
+          Node* output = edge->outputs_[i];
+          if (output->in_edge() == nullptr) {
+            output->set_in_edge(edge);
+            ++i;
+            continue;
+          }
+          // Two edges produce the same output node.
+          if (options_.dupe_edge_action_ == kDupeEdgeActionError) {
+            return DecorateError(clump->file_,
+                                 edge->parse_state_.final_diag_pos,
+                                 "multiple rules generate " + output->path() +
+                                 " [-w dupbuild=err]", err);
+          } else {
+            if (!quiet_) {
+              Warning("multiple rules generate %s. "
+                      "builds involving this target will not be correct; "
+                      "continuing anyway [-w dupbuild=warn]",
+                      output->path().c_str());
+            }
+            if (edge->is_implicit_out(i))
+              --edge->implicit_outs_;
+            else
+              --edge->explicit_outs_;
+            edge->outputs_.erase(edge->outputs_.begin() + i);
+          }
+        }
+        if (edge->outputs_.empty()) {
+          // All outputs of the edge are already created by other edges. Remove
+          // this edge from the graph. This removal happens before the edge's
+          // inputs are linked to nodes.
+          clump->edges_.erase(clump->edges_.begin() + edge_idx);
+          continue;
+        }
+        ++edge_idx;
+      }
+    }
+  }
+  {
+    // Now that all invalid edges are removed from the graph, record an out-edge
+    // on each node that's needed by an edge.
+    METRIC_RECORD(".ninja load : link edge inputs");
+    ParallelMap(thread_pool_, clumps, [](Clump* clump) {
+      for (Edge* edge : clump->edges_) {
+        for (Node* input : edge->inputs_) {
+          input->AddOutEdge(edge);
+        }
+        for (Node* validation : edge->validations_) {
+          validation->AddValidationOutEdge(edge);
+        }
+      }
+    });
+  }
+  {
+    METRIC_RECORD(".ninja load : default targets");
+
+    for (Clump* clump : clumps) {
+      for (DefaultTarget* target : clump->default_targets_) {
+        std::string path;
+        EvaluatePathInScope(&path, target->parsed_path_,
+                            target->pos_.scope_pos());
+        uint64_t slash_bits;  // Unused because this only does lookup.
+        std::string path_err;
+        if (!CanonicalizePath(&path, &slash_bits, &path_err))
+          return DecorateError(clump->file_, target->diag_pos_, path_err, err);
+
+        Node* node = state_->LookupNodeAtPos(path, target->pos_.dfs_location());
+        if (node == nullptr) {
+          return DecorateError(clump->file_, target->diag_pos_,
+                               "unknown target '" + path + "'", err);
+        }
+        state_->AddDefault(node);
+      }
+    }
+  }
+  {
+    // Add the clump edges into the global edge vector, and assign edge IDs.
+    // Edge IDs are used for custom protobuf-based Ninja frontends. An edge's ID
+    // is equal to its index in the global edge vector, so delay the assignment
+    // of edge IDs until we've removed duplicate edges above (dupbuild=warn).
+
+    METRIC_RECORD(".ninja load : build edge table");
+
+    // Copy edges to the global edge table.
+    size_t old_size = state_->edges_.size();
+    size_t new_size = old_size;
+    for (Clump* clump : clumps) {
+      new_size += clump->edges_.size();
+    }
+    state_->edges_.reserve(new_size);
+    for (Clump* clump : clumps) {
+      std::copy(clump->edges_.begin(), clump->edges_.end(),
+                std::back_inserter(state_->edges_));
+    }
+    // Assign edge IDs.
+    ParallelMap(thread_pool_, IntegralRange<size_t>(old_size, new_size),
+        [this](size_t idx) {
+      state_->edges_[idx]->id_ = idx;
+    });
+  }
+
   return true;
 }
+
+bool ManifestLoader::Load(ManifestFileSet* file_set,
+                          const LoadedFile& root_manifest, std::string* err) {
+  DfsParser dfs_parser(file_set, state_, thread_pool_);
+  std::vector<Clump*> clumps;
+  if (!dfs_parser.LoadManifestTree(root_manifest, &state_->root_scope_, &clumps,
+                                   err)) {
+    return false;
+  }
+  return FinishLoading(clumps, err);
+}
+
+} // anonymous namespace
+
+bool ManifestParser::Load(const string& filename, string* err) {
+  METRIC_RECORD(".ninja load");
+
+  ManifestFileSet file_set(file_reader_);
+  const LoadedFile* file = nullptr;
+  if (!file_set.LoadFile(filename, &file, err))
+    return false;
+
+  std::unique_ptr<ThreadPool> thread_pool = CreateThreadPool();
+  ManifestLoader loader(state_, thread_pool.get(), options_, false);
+  return loader.Load(&file_set, *file, err);
+}
+
+bool ManifestParser::ParseTest(const string& input, string* err) {
+  ManifestFileSet file_set(file_reader_);
+  std::unique_ptr<ThreadPool> thread_pool = CreateThreadPool();
+  ManifestLoader loader(state_, thread_pool.get(), options_, true);
+  return loader.Load(&file_set, HeapLoadedFile("input", input), err);
+}
diff --git a/src/manifest_parser.h b/src/manifest_parser.h
index 2136018..3f1895d 100644
--- a/src/manifest_parser.h
+++ b/src/manifest_parser.h
@@ -1,4 +1,4 @@
-// Copyright 2011 Google Inc. All Rights Reserved.
+// Copyright 2018 Google Inc. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,12 +17,6 @@
 
 #include <string>
 
-using namespace std;
-
-#include "lexer.h"
-
-struct BindingEnv;
-struct EvalString;
 struct FileReader;
 struct State;
 
@@ -37,51 +31,30 @@
 };
 
 struct ManifestParserOptions {
-  ManifestParserOptions()
-      : dupe_edge_action_(kDupeEdgeActionWarn),
-        phony_cycle_action_(kPhonyCycleActionWarn) {}
-  DupeEdgeAction dupe_edge_action_;
-  PhonyCycleAction phony_cycle_action_;
+  DupeEdgeAction dupe_edge_action_ = kDupeEdgeActionWarn;
+  PhonyCycleAction phony_cycle_action_ = kPhonyCycleActionWarn;
 };
 
-/// Parses .ninja files.
 struct ManifestParser {
   ManifestParser(State* state, FileReader* file_reader,
-                 ManifestParserOptions options = ManifestParserOptions());
+                 ManifestParserOptions options = ManifestParserOptions())
+      : state_(state),
+        file_reader_(file_reader),
+        options_(options) {}
 
   /// Load and parse a file.
-  bool Load(const string& filename, string* err, Lexer* parent = NULL);
+  bool Load(const std::string& filename, std::string* err);
 
-  /// Parse a text string of input.  Used by tests.
-  bool ParseTest(const string& input, string* err) {
-    quiet_ = true;
-    return Parse("input", input, err);
-  }
+  /// Parse a text string of input. Used by tests.
+  ///
+  /// Some tests may call ParseTest multiple times with the same State object.
+  /// Each call adds to the previous state; it doesn't replace it.
+  bool ParseTest(const std::string& input, std::string* err);
 
 private:
-  /// Parse a file, given its contents as a string.
-  bool Parse(const string& filename, const string& input, string* err);
-
-  /// Parse various statement types.
-  bool ParsePool(string* err);
-  bool ParseRule(string* err);
-  bool ParseLet(string* key, EvalString* val, string* err);
-  bool ParseEdge(string* err);
-  bool ParseDefault(string* err);
-
-  /// Parse either a 'subninja' or 'include' line.
-  bool ParseFileInclude(bool new_scope, string* err);
-
-  /// If the next token is not \a expected, produce an error string
-  /// saying "expectd foo, got bar".
-  bool ExpectToken(Lexer::Token expected, string* err);
-
-  State* state_;
-  BindingEnv* env_;
-  FileReader* file_reader_;
-  Lexer lexer_;
+  State* state_ = nullptr;
+  FileReader* file_reader_ = nullptr;
   ManifestParserOptions options_;
-  bool quiet_;
 };
 
 #endif  // NINJA_MANIFEST_PARSER_H_
diff --git a/src/manifest_parser_test.cc b/src/manifest_parser_test.cc
index c91d8d1..0732129 100644
--- a/src/manifest_parser_test.cc
+++ b/src/manifest_parser_test.cc
@@ -48,11 +48,10 @@
 "\n"
 "build result: cat in_1.cc in-2.O\n"));
 
-  ASSERT_EQ(3u, state.bindings_.GetRules().size());
-  const Rule* rule = state.bindings_.GetRules().begin()->second;
+  ASSERT_EQ(3u, state.root_scope_.GetRules().size());
+  const Rule* rule = state.root_scope_.GetRules().begin()->second;
   EXPECT_EQ("cat", rule->name());
-  EXPECT_EQ("[cat ][$in][ > ][$out]",
-            rule->GetBinding("command")->Serialize());
+  EXPECT_EQ("cat $in > $out\n", *rule->GetBinding("command"));
 }
 
 TEST_F(ParserTest, RuleAttributes) {
@@ -63,6 +62,7 @@
 "  depfile = a\n"
 "  deps = a\n"
 "  description = a\n"
+"  phony_output = a\n"
 "  generator = a\n"
 "  restat = a\n"
 "  rspfile = a\n"
@@ -81,12 +81,12 @@
 "build result: cat in_1.cc in-2.O\n"
 "  #comment\n"));
 
-  ASSERT_EQ(2u, state.bindings_.GetRules().size());
-  const Rule* rule = state.bindings_.GetRules().begin()->second;
+  ASSERT_EQ(2u, state.root_scope_.GetRules().size());
+  const Rule* rule = state.root_scope_.GetRules().begin()->second;
   EXPECT_EQ("cat", rule->name());
   Edge* edge = state.GetNode("result", 0)->in_edge();
-  EXPECT_TRUE(edge->GetBindingBool("restat"));
-  EXPECT_FALSE(edge->GetBindingBool("generator"));
+  EXPECT_TRUE(edge->IsRestat());
+  EXPECT_FALSE(edge->IsGenerator());
 }
 
 TEST_F(ParserTest, IgnoreIndentedBlankLines) {
@@ -101,7 +101,7 @@
 "variable=1\n"));
 
   // the variable must be in the top level environment
-  EXPECT_EQ("1", state.bindings_.LookupVariable("variable"));
+  EXPECT_EQ("1", state.root_scope_.LookupVariable("variable"));
 }
 
 TEST_F(ParserTest, ResponseFiles) {
@@ -114,13 +114,12 @@
 "build out: cat_rsp in\n"
 "  rspfile=out.rsp\n"));
 
-  ASSERT_EQ(2u, state.bindings_.GetRules().size());
-  const Rule* rule = state.bindings_.GetRules().begin()->second;
+  ASSERT_EQ(2u, state.root_scope_.GetRules().size());
+  const Rule* rule = state.root_scope_.GetRules().begin()->second;
   EXPECT_EQ("cat_rsp", rule->name());
-  EXPECT_EQ("[cat ][$rspfile][ > ][$out]",
-            rule->GetBinding("command")->Serialize());
-  EXPECT_EQ("[$rspfile]", rule->GetBinding("rspfile")->Serialize());
-  EXPECT_EQ("[$in]", rule->GetBinding("rspfile_content")->Serialize());
+  EXPECT_EQ("cat $rspfile > $out\n", *rule->GetBinding("command"));
+  EXPECT_EQ("$rspfile\n", *rule->GetBinding("rspfile"));
+  EXPECT_EQ("$in\n", *rule->GetBinding("rspfile_content"));
 }
 
 TEST_F(ParserTest, InNewline) {
@@ -131,11 +130,10 @@
 "build out: cat_rsp in in2\n"
 "  rspfile=out.rsp\n"));
 
-  ASSERT_EQ(2u, state.bindings_.GetRules().size());
-  const Rule* rule = state.bindings_.GetRules().begin()->second;
+  ASSERT_EQ(2u, state.root_scope_.GetRules().size());
+  const Rule* rule = state.root_scope_.GetRules().begin()->second;
   EXPECT_EQ("cat_rsp", rule->name());
-  EXPECT_EQ("[cat ][$in_newline][ > ][$out]",
-            rule->GetBinding("command")->Serialize());
+  EXPECT_EQ("cat $in_newline > $out\n", *rule->GetBinding("command"));
 
   Edge* edge = state.edges_[0];
   EXPECT_EQ("cat in\nin2 > out", edge->EvaluateCommand());
@@ -159,7 +157,7 @@
   Edge* edge = state.edges_[0];
   EXPECT_EQ("ld one-letter-test -pthread -under -o a b c",
             edge->EvaluateCommand());
-  EXPECT_EQ("1/2", state.bindings_.LookupVariable("nested2"));
+  EXPECT_EQ("1/2", state.root_scope_.LookupVariable("nested2"));
 
   edge = state.edges_[1];
   EXPECT_EQ("ld one-letter-test 1/2/3 -under -o supernested x",
@@ -192,10 +190,12 @@
 "build a: link c $\n"
 " d e f\n"));
 
-  ASSERT_EQ(2u, state.bindings_.GetRules().size());
-  const Rule* rule = state.bindings_.GetRules().begin()->second;
+  ASSERT_EQ(2u, state.root_scope_.GetRules().size());
+  const Rule* rule = state.root_scope_.GetRules().begin()->second;
   EXPECT_EQ("link", rule->name());
-  EXPECT_EQ("[foo bar baz]", rule->GetBinding("command")->Serialize());
+  std::string command = *rule->GetBinding("command");
+  EXPECT_EQ("foo bar $\n    baz\n", command);
+  EXPECT_EQ("foo bar baz", EvaluateBindingForTesting(command));
 }
 
 TEST_F(ParserTest, Backslash) {
@@ -203,15 +203,15 @@
 "foo = bar\\baz\n"
 "foo2 = bar\\ baz\n"
 ));
-  EXPECT_EQ("bar\\baz", state.bindings_.LookupVariable("foo"));
-  EXPECT_EQ("bar\\ baz", state.bindings_.LookupVariable("foo2"));
+  EXPECT_EQ("bar\\baz", state.root_scope_.LookupVariable("foo"));
+  EXPECT_EQ("bar\\ baz", state.root_scope_.LookupVariable("foo2"));
 }
 
 TEST_F(ParserTest, Comment) {
   ASSERT_NO_FATAL_FAILURE(AssertParse(
 "# this is a comment\n"
 "foo = not # a comment\n"));
-  EXPECT_EQ("not # a comment", state.bindings_.LookupVariable("foo"));
+  EXPECT_EQ("not # a comment", state.root_scope_.LookupVariable("foo"));
 }
 
 TEST_F(ParserTest, Dollars) {
@@ -222,7 +222,7 @@
 "x = $$dollar\n"
 "build $x: foo y\n"
 ));
-  EXPECT_EQ("$dollar", state.bindings_.LookupVariable("x"));
+  EXPECT_EQ("$dollar", state.root_scope_.LookupVariable("x"));
 #ifdef _WIN32
   EXPECT_EQ("$dollarbar$baz$blah", state.edges_[0]->EvaluateCommand());
 #else
@@ -418,6 +418,38 @@
 "default subninja\n"));
 }
 
+TEST_F(ParserTest, NulCharErrors) {
+  {
+    State local_state;
+    ManifestParser parser(&local_state, NULL);
+    std::string err;
+    EXPECT_FALSE(parser.ParseTest("\0"_s, &err));
+    EXPECT_EQ("input:1: unexpected NUL byte\n", err);
+  }
+
+  {
+    State local_state;
+    ManifestParser parser(&local_state, NULL);
+    std::string err;
+    EXPECT_FALSE(parser.ParseTest("build foo\0"_s, &err));
+    EXPECT_EQ("input:1: unexpected NUL byte\n"
+              "build foo\n"
+              "         ^ near here"
+              , err);
+  }
+
+  {
+    State local_state;
+    ManifestParser parser(&local_state, NULL);
+    std::string err;
+    EXPECT_FALSE(parser.ParseTest("foo\0"_s, &err));
+    EXPECT_EQ("input:1: expected '=', got nul byte\n"
+              "foo\n"
+              "   ^ near here"
+              , err);
+  }
+}
+
 TEST_F(ParserTest, Errors) {
   {
     State local_state;
@@ -707,10 +739,15 @@
   }
 
   {
+    // The default statement's target must be listed earlier in a build
+    // statement.
     State local_state;
     ManifestParser parser(&local_state, NULL);
     string err;
-    EXPECT_FALSE(parser.ParseTest("default nonexistent\n",
+    EXPECT_FALSE(parser.ParseTest("default nonexistent\n"
+                                  "rule cat\n"
+                                  "  command = cat $in > $out\n"
+                                  "build nonexistent: cat existent\n",
                                   &err));
     EXPECT_EQ("input:1: unknown target 'nonexistent'\n"
               "default nonexistent\n"
@@ -792,7 +829,8 @@
     string err;
     EXPECT_FALSE(parser.ParseTest("pool foo\n"
                                   "  depth = 4\n"
-                                  "pool foo\n", &err));
+                                  "pool foo\n"
+                                  "  depth = 2\n", &err));
     EXPECT_EQ("input:3: duplicate pool 'foo'\n"
               "pool foo\n"
               "        ^ near here"
@@ -854,6 +892,16 @@
   EXPECT_EQ("", err);
 }
 
+TEST_F(ParserTest, MultipleImplicitOutputsWithDeps) {
+  State local_state;
+  ManifestParser parser(&local_state, NULL);
+  string err;
+  EXPECT_TRUE(parser.ParseTest("rule cc\n  command = foo\n  deps = gcc\n"
+                               "build a.o | a.gcno: cc c.cc\n",
+                               &err));
+  EXPECT_EQ("", err);
+}
+
 TEST_F(ParserTest, MultipleOutputsWithDeps) {
   State local_state;
   ManifestParser parser(&local_state, NULL);
@@ -932,7 +980,7 @@
 
   ASSERT_EQ(1u, fs_.files_read_.size());
   EXPECT_EQ("include.ninja", fs_.files_read_[0]);
-  EXPECT_EQ("inner", state.bindings_.LookupVariable("var"));
+  EXPECT_EQ("inner", state.root_scope_.LookupVariable("var"));
 }
 
 TEST_F(ParserTest, BrokenInclude) {
@@ -965,6 +1013,16 @@
   ASSERT_TRUE(edge->is_order_only(1));
 }
 
+TEST_F(ParserTest, Validations) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(
+"rule cat\n  command = cat $in > $out\n"
+"build foo: cat bar |@ baz\n"));
+
+  Edge* edge = state.LookupNode("foo")->in_edge();
+  ASSERT_EQ(edge->validations_.size(), 1);
+  EXPECT_EQ(edge->validations_[0]->path(), "baz");
+}
+
 TEST_F(ParserTest, ImplicitOutput) {
   ASSERT_NO_FATAL_FAILURE(AssertParse(
 "rule cat\n"
@@ -1085,3 +1143,67 @@
       "  description = YAY!\r\n",
       &err));
 }
+
+TEST_F(ParserTest, IncludeUsingAVariable) {
+  // Each include statement should use the $path binding from the previous
+  // manifest declarations.
+  fs_.Create("foo.ninja", "path = bar.ninja\n"
+                          "include $path\n");
+  fs_.Create("bar.ninja", "");
+  ASSERT_NO_FATAL_FAILURE(AssertParse(
+"path = foo.ninja\n"
+"include $path\n"
+"include $path\n"
+"path = nonexistent.ninja\n"));
+}
+
+TEST_F(ParserTest, UnscopedPool) {
+  // Pools aren't scoped.
+  fs_.Create("foo.ninja", "pool link\n"
+                          "  depth = 3\n");
+  ASSERT_NO_FATAL_FAILURE(AssertParse(
+"rule cat\n"
+"  command = cat $in > $out\n"
+"  pool = link\n"
+"subninja foo.ninja\n"
+"build a: cat b\n"));
+}
+
+TEST_F(ParserTest, PoolDeclaredAfterUse) {
+  // A pool must be declared before an edge that uses it.
+  ManifestParser parser(&state, nullptr);
+  std::string err;
+  EXPECT_FALSE(parser.ParseTest("rule cat\n"
+                                "  command = cat $in > $out\n"
+                                "  pool = link\n"
+                                "build a: cat b\n"
+                                "pool link\n"
+                                "  depth = 3\n", &err));
+  EXPECT_EQ("input:5: unknown pool name 'link'\n", err);
+}
+
+TEST_F(ParserTest, DefaultReferencesEdgeInput) {
+  // The default statement's target must be listed before the default statement.
+  // It's OK if the target's first reference is an input, though.
+  ASSERT_NO_FATAL_FAILURE(AssertParse(
+"rule cat\n  command = cat $in > $out\n"
+"build a1: cat b1\n"
+"build a2: cat b2\n"
+"default b1 b2\n"
+"build b1: cat c1\n"));
+}
+
+TEST_F(ParserTest, SelfVarExpansion) {
+  ASSERT_NO_FATAL_FAILURE(AssertParse(
+"var = xxx\n"
+"var1 = ${var}\n"
+"var = a${var}a\n"
+"var2 = ${var}\n"
+"var = b${var}b\n"
+"var3 = ${var}\n"
+"var = c${var}c\n"));
+  EXPECT_EQ("xxx", state.root_scope_.LookupVariable("var1"));
+  EXPECT_EQ("axxxa", state.root_scope_.LookupVariable("var2"));
+  EXPECT_EQ("baxxxab", state.root_scope_.LookupVariable("var3"));
+  EXPECT_EQ("cbaxxxabc", state.root_scope_.LookupVariable("var"));
+}
diff --git a/src/metrics.cc b/src/metrics.cc
index a7d3c7a..5e4cef9 100644
--- a/src/metrics.cc
+++ b/src/metrics.cc
@@ -19,13 +19,17 @@
 #include <string.h>
 
 #ifndef _WIN32
+#include <sys/resource.h>
 #include <sys/time.h>
+#include <sys/types.h>
+#include <unistd.h>
 #else
 #include <windows.h>
 #endif
 
 #include <algorithm>
 
+#include "status.h"
 #include "util.h"
 
 Metrics* g_metrics = NULL;
@@ -75,45 +79,40 @@
 }  // anonymous namespace
 
 
-ScopedMetric::ScopedMetric(Metric* metric) {
-  metric_ = metric;
-  if (!metric_)
-    return;
+void ScopedMetric::RecordStart() {
   start_ = HighResTimer();
 }
-ScopedMetric::~ScopedMetric() {
-  if (!metric_)
-    return;
-  metric_->count++;
-  int64_t dt = TimerToMicros(HighResTimer() - start_);
-  metric_->sum += dt;
+
+void ScopedMetric::RecordResult() {
+  int64_t duration = TimerToMicros(HighResTimer() - start_);
+  metric_->AddResult(1, duration);
 }
 
 Metric* Metrics::NewMetric(const string& name) {
-  Metric* metric = new Metric;
-  metric->name = name;
-  metric->count = 0;
-  metric->sum = 0;
-  metrics_.push_back(metric);
-  return metric;
+  std::lock_guard<std::mutex> lock(mutex_);
+  Metric* result = new Metric(name);
+  metrics_.push_back(result);
+  return result;
 }
 
-void Metrics::Report() {
+void Metrics::Report(Status *status) {
+  std::lock_guard<std::mutex> lock(mutex_);
+
   int width = 0;
   for (vector<Metric*>::iterator i = metrics_.begin();
        i != metrics_.end(); ++i) {
-    width = max((int)(*i)->name.size(), width);
+    width = max((int)(*i)->name().size(), width);
   }
 
-  printf("%-*s\t%-6s\t%-9s\t%s\n", width,
+  status->Debug("%-*s\t%-6s\t%-9s\t%s", width,
          "metric", "count", "avg (us)", "total (ms)");
   for (vector<Metric*>::iterator i = metrics_.begin();
        i != metrics_.end(); ++i) {
     Metric* metric = *i;
-    double total = metric->sum / (double)1000;
-    double avg = metric->sum / (double)metric->count;
-    printf("%-*s\t%-6d\t%-8.1f\t%.1f\n", width, metric->name.c_str(),
-           metric->count, avg, total);
+    double total = metric->time() / (double)1000;
+    double avg = metric->time() / (double)metric->count();
+    status->Debug("%-*s\t%-6d\t%-8.1f\t%.1f", width, metric->name().c_str(),
+           metric->count(), avg, total);
   }
 }
 
@@ -125,3 +124,39 @@
   return TimerToMicros(HighResTimer()) / 1000;
 }
 
+void DumpMemoryUsage(Status *status) {
+#if defined(__linux__)
+  std::vector<std::string> words;
+  struct rusage usage {};
+  if (getrusage(RUSAGE_SELF, &usage) == 0) {
+    words.push_back(std::to_string(usage.ru_majflt) + " maj faults");
+    words.push_back(std::to_string(usage.ru_minflt) + " min faults");
+    words.push_back(std::to_string(usage.ru_maxrss / 1024) + " MiB maxrss");
+  }
+  char status_path[256];
+  snprintf(status_path, sizeof(status_path), "/proc/%d/status",
+           static_cast<int>(getpid()));
+  if (FILE* status_fp = fopen(status_path, "r")) {
+    char* line = nullptr;
+    size_t n = 0;
+    while (getline(&line, &n, status_fp) > 0) {
+      unsigned long rss = 0;
+      if (sscanf(line, "VmRSS:\t%lu kB\n", &rss) == 1) {
+        words.push_back(std::to_string(rss / 1024) + " MiB rss");
+      }
+    }
+    free(line);
+    fclose(status_fp);
+  }
+  if (!words.empty()) {
+    string final;
+    for (size_t i = 0; i < words.size(); ++i) {
+      if (i > 0) {
+        final += ", ";
+      }
+      final += words[i];
+    }
+    status->Debug("%s", final.c_str());
+  }
+#endif
+}
diff --git a/src/metrics.h b/src/metrics.h
index b6da859..48f26d1 100644
--- a/src/metrics.h
+++ b/src/metrics.h
@@ -15,32 +15,83 @@
 #ifndef NINJA_METRICS_H_
 #define NINJA_METRICS_H_
 
+#include <atomic>
+#include <mutex>
 #include <string>
 #include <vector>
 using namespace std;
 
 #include "util.h"  // For int64_t.
 
+struct Status;
+
 /// The Metrics module is used for the debug mode that dumps timing stats of
 /// various actions.  To use, see METRIC_RECORD below.
 
 /// A single metrics we're tracking, like "depfile load time".
 struct Metric {
-  string name;
-  /// Number of times we've hit the code path.
-  int count;
-  /// Total time (in micros) we've spent on the code path.
-  int64_t sum;
-};
+  Metric(const std::string& name) : name_(name) {}
 
+  void AddResult(int count, int64_t time_us) {
+    thread_local size_t this_slot = GetThreadSlotIndex();
+    Slot& slot = slots_[this_slot];
+    slot.count += count;
+    slot.time += time_us;
+  }
 
-/// A scoped object for recording a metric across the body of a function.
-/// Used by the METRIC_RECORD macro.
-struct ScopedMetric {
-  explicit ScopedMetric(Metric* metric);
-  ~ScopedMetric();
+  const std::string& name() const { return name_; }
+
+  int count() const {
+    int result = 0;
+    for (const Slot& slot : slots_) {
+      result += slot.count;
+    }
+    return result;
+  }
+
+  int64_t time() const {
+    int64_t result = 0;
+    for (const Slot& slot : slots_) {
+      result += slot.time;
+    }
+    return result;
+  }
 
 private:
+  /// Try to give each thread a different slot to reduce thread contention.
+  struct NINJA_ALIGNAS_CACHE_LINE Slot {
+    /// Number of times we've hit the code path.
+    std::atomic<int> count {};
+    /// Total time (in micros) we've spent on the code path.
+    std::atomic<int64_t> time {};
+  };
+
+  std::string name_;
+  std::vector<Slot> slots_ { GetThreadSlotCount() };
+};
+
+/// A scoped object for recording a metric across the body of a function.
+/// Used by the METRIC_RECORD macro. Inline the metric_ null check to minimize
+/// the effect on the typical case where metrics are disabled.
+struct ScopedMetric {
+  explicit ScopedMetric(Metric* metric) : metric_(metric) {
+    if (metric_ == nullptr) {
+      return;
+    }
+    RecordStart();
+  }
+
+  ~ScopedMetric() {
+    if (metric_ == nullptr) {
+      return;
+    }
+    RecordResult();
+  }
+
+private:
+  void RecordStart();
+  void RecordResult();
+
   Metric* metric_;
   /// Timestamp when the measurement started.
   /// Value is platform-dependent.
@@ -52,9 +103,10 @@
   Metric* NewMetric(const string& name);
 
   /// Print a summary report to stdout.
-  void Report();
+  void Report(Status *status);
 
 private:
+  std::mutex mutex_;
   vector<Metric*> metrics_;
 };
 
@@ -89,4 +141,6 @@
 
 extern Metrics* g_metrics;
 
+void DumpMemoryUsage(Status *status);
+
 #endif // NINJA_METRICS_H_
diff --git a/src/ninja.cc b/src/ninja.cc
index b608426..ac5cb7e 100644
--- a/src/ninja.cc
+++ b/src/ninja.cc
@@ -30,6 +30,9 @@
 #include <unistd.h>
 #endif
 
+#include <deque>
+#include <unordered_map>
+
 #include "browse.h"
 #include "build.h"
 #include "build_log.h"
@@ -42,6 +45,8 @@
 #include "manifest_parser.h"
 #include "metrics.h"
 #include "state.h"
+#include "status.h"
+#include "thread_pool.h"
 #include "util.h"
 #include "version.h"
 
@@ -53,6 +58,11 @@
 void CreateWin32MiniDump(_EXCEPTION_POINTERS* pep);
 #endif
 
+// Ninja intentionally leaks memory. Turn off LeakSanitizer by default.
+extern "C" const char* __asan_default_options() {
+  return "detect_leaks=0";
+}
+
 namespace {
 
 struct Tool;
@@ -77,13 +87,17 @@
   /// Whether a depfile with multiple targets on separate lines should
   /// warn or print an error.
   bool depfile_distinct_target_lines_should_err;
+
+  /// Whether to remain persistent.
+  bool persistent;
 };
 
 /// The Ninja main() loads up a series of data structures; various tools need
 /// to poke into these, so store them as fields on an object.
 struct NinjaMain : public BuildLogUser {
   NinjaMain(const char* ninja_command, const BuildConfig& config) :
-      ninja_command_(ninja_command), config_(config) {}
+      ninja_command_(ninja_command), config_(config),
+      start_time_millis_(GetTimeMillis()) {}
 
   /// Command line used to run Ninja.
   const char* ninja_command_;
@@ -116,6 +130,8 @@
 
   // The various subcommands, run via "-t XXX".
   int ToolGraph(const Options* options, int argc, char* argv[]);
+  int ToolPath(const Options* options, int argc, char* argv[]);
+  int ToolInputs(const Options* options, int argc, char* argv[]);
   int ToolQuery(const Options* options, int argc, char* argv[]);
   int ToolDeps(const Options* options, int argc, char* argv[]);
   int ToolBrowse(const Options* options, int argc, char* argv[]);
@@ -142,14 +158,14 @@
   /// Rebuild the manifest, if necessary.
   /// Fills in \a err on error.
   /// @return true if the manifest was rebuilt.
-  bool RebuildManifest(const char* input_file, string* err);
+  bool RebuildManifest(const char* input_file, string* err, Status* status);
 
   /// Build the targets listed on the command line.
   /// @return an exit code.
-  int RunBuild(int argc, char** argv);
+  int RunBuild(int argc, char** argv, Status* status);
 
   /// Dump the output requested by '-d stats'.
-  void DumpMetrics();
+  void DumpMetrics(Status *status);
 
   virtual bool IsPathDead(StringPiece s) const {
     Node* n = state_.LookupNode(s);
@@ -165,11 +181,13 @@
     // Do keep entries around for files which still exist on disk, for
     // generators that want to use this information.
     string err;
-    TimeStamp mtime = disk_interface_.Stat(s.AsString(), &err);
+    TimeStamp mtime = disk_interface_.LStat(s.AsString(), nullptr, &err);
     if (mtime == -1)
       Error("%s", err.c_str());  // Log and ignore Stat() errors.
     return mtime == 0;
   }
+
+  int64_t start_time_millis_;
 };
 
 /// Subtools, accessible via "-t foo".
@@ -219,8 +237,14 @@
 "  -d MODE  enable debugging (use '-d list' to list modes)\n"
 "  -t TOOL  run a subtool (use '-t list' to list subtools)\n"
 "    terminates toplevel options; further flags are passed to the tool\n"
-"  -w FLAG  adjust warnings (use '-w list' to list warnings)\n",
-          kNinjaVersion, config.parallelism);
+"  -o FLAG  adjust options (use '-o list' to list options)\n"
+"  -w FLAG  adjust warnings (use '-w list' to list warnings)\n"
+#ifndef _WIN32
+"\n"
+"  --frontend COMMAND    execute COMMAND and pass serialized build output to it\n"
+"  --frontend_file FILE  write serialized build output to FILE\n"
+#endif
+      , kNinjaVersion, config.parallelism);
 }
 
 /// Choose a default value for the -j (parallelism) flag.
@@ -238,7 +262,8 @@
 
 /// Rebuild the build manifest, if necessary.
 /// Returns true if the manifest was rebuilt.
-bool NinjaMain::RebuildManifest(const char* input_file, string* err) {
+bool NinjaMain::RebuildManifest(const char* input_file, string* err,
+                                Status* status) {
   string path = input_file;
   uint64_t slash_bits;  // Unused because this path is only used for lookup.
   if (!CanonicalizePath(&path, &slash_bits, err))
@@ -247,8 +272,9 @@
   if (!node)
     return false;
 
-  Builder builder(&state_, config_, &build_log_, &deps_log_, &disk_interface_);
-  if (!builder.AddTarget(node, err))
+  Builder builder(&state_, config_, &build_log_, &deps_log_, &disk_interface_,
+                  status, start_time_millis_);
+  if (!builder.AddTargets({ node }, err))
     return false;
 
   if (builder.AlreadyUpToDate())
@@ -285,11 +311,11 @@
   Node* node = state_.LookupNode(path);
   if (node) {
     if (first_dependent) {
-      if (node->out_edges().empty()) {
+      if (!node->has_out_edge()) {
         *err = "'" + path + "' has no out edge";
         return NULL;
       }
-      Edge* edge = node->out_edges()[0];
+      Edge* edge = node->GetOutEdges()[0];
       if (edge->outputs_.empty()) {
         edge->Dump();
         Fatal("edge has no outputs");
@@ -347,6 +373,156 @@
   return 0;
 }
 
+int NinjaMain::ToolPath(const Options* options, int argc, char* argv[]) {
+  if (argc != 2) {
+    Error("expected two targets to find a dependency chain between");
+    return 1;
+  }
+  std::string err;
+  Node* out = CollectTarget(argv[0], &err);
+  if (!out) {
+    Error("%s", err.c_str());
+    return 1;
+  }
+  Node* in = CollectTarget(argv[1], &err);
+  if (!in) {
+    Error("%s", err.c_str());
+    return 1;
+  }
+
+  // BFS from "in", looking for "out". The map record a node's input that leads
+  // back to "in".
+  std::unordered_map<Node*, Node*> node_next;
+  std::deque<Node*> queue;
+  queue.push_back(in);
+  while (!queue.empty()) {
+    Node* node = queue[0];
+    queue.pop_front();
+    if (node == out) {
+      for (; node != nullptr; node = node_next[node]) {
+        printf("%s\n", node->path().c_str());
+      }
+      return 0;
+    }
+    for (Edge* edge : node->GetOutEdges()) {
+      for (Node* output : edge->outputs_) {
+        if (node_next.count(output) == 0) {
+          node_next[output] = node;
+          queue.push_back(output);
+        }
+      }
+    }
+    for (Edge* edge : node->GetValidationOutEdges()) {
+      for (Node* output : edge->outputs_) {
+        if (node_next.count(output) == 0) {
+          node_next[output] = node;
+          queue.push_back(output);
+        }
+      }
+    }
+  }
+  Error("%s does not depend on %s", out->path().c_str(), in->path().c_str());
+  return 1;
+}
+
+void ToolInputsProcessNodeDeps(Node* node, DepsLog* deps_log, bool leaf_only,
+                               bool include_deps);
+
+void ToolInputsProcessNode(Node* input, DepsLog* deps_log, bool leaf_only,
+                           bool include_deps) {
+  if (input->InputsChecked()) return;
+  input->MarkInputsChecked();
+
+  // Recursively process input edges, possibly printing this node here.
+  Edge* input_edge = input->in_edge();
+  if (input_edge == nullptr || !leaf_only) {
+    printf("%s\n", input->path().c_str());
+  }
+  if (input_edge) {
+    for (Node* input : input_edge->inputs_) {
+      ToolInputsProcessNode(input, deps_log, leaf_only, include_deps);
+    }
+    if (include_deps && input_edge->outputs_.size() > 0) {
+      // Check deps on the input edge's first output because deps log entries
+      // are only stored on the first output of an edge.
+      ToolInputsProcessNodeDeps(input_edge->outputs_[0], deps_log, leaf_only,
+                                include_deps);
+    }
+  }
+}
+
+void ToolInputsProcessNodeDeps(Node* node, DepsLog* deps_log, bool leaf_only,
+                               bool include_deps) {
+  // Print all of this node's deps from the deps log. This often includes files
+  // that are not known by the node's input edge.
+  if (deps_log->IsDepsEntryLiveFor(node)) {
+    if (DepsLog::Deps* deps = deps_log->GetDeps(node); deps != nullptr) {
+      for (int i = 0; i < deps->node_count; ++i) {
+        if (Node* dep = deps->nodes[i]; dep != nullptr) {
+          ToolInputsProcessNode(dep, deps_log, leaf_only, include_deps);
+        }
+      }
+    }
+  }
+}
+
+int NinjaMain::ToolInputs(const Options* options, int argc, char* argv[]) {
+  // The inputs tool uses getopt, and expects argv[0] to contain the name of
+  // the tool, i.e. "inputs".
+  ++argc;
+  --argv;
+
+  bool leaf_only = true;
+  bool include_deps = false;
+
+  optind = 1;
+  int opt;
+  while ((opt = getopt(argc, argv, "idh")) != -1) {
+    switch (opt) {
+      case 'i':
+        leaf_only = false;
+        break;
+      case 'd':
+        include_deps = true;
+        break;
+      case 'h':
+      default:
+        printf(
+            "usage: ninja -t inputs [options] target [target...]\n\n"
+            "options:\n"
+            "  -i    Include intermediate inputs.\n"
+            "  -d    Include deps from the deps log file (.ninja_deps).\n");
+        return 1;
+    }
+  }
+  argv += optind;
+  argc -= optind;
+
+  vector<Node*> nodes;
+  string err;
+  if (!CollectTargetsFromArgs(argc, argv, &nodes, &err)) {
+    Error("%s", err.c_str());
+    return 1;
+  }
+
+  for (Node* node : nodes) {
+    node->MarkInputsChecked();
+    // Call ToolInputsProcessNode on this node's inputs, and not on itself,
+    // so that this node is not included in the output.
+    if (Edge* edge = node->in_edge(); edge != nullptr) {
+      for (Node* input : edge->inputs_) {
+        ToolInputsProcessNode(input, &deps_log_, leaf_only, include_deps);
+      }
+      if (include_deps && edge->outputs_.size() > 0) {
+        ToolInputsProcessNodeDeps(edge->outputs_[0], &deps_log_, leaf_only,
+                                  include_deps);
+      }
+    }
+  }
+  return 0;
+}
+
+
 int NinjaMain::ToolQuery(const Options* options, int argc, char* argv[]) {
   if (argc == 0) {
     Error("expected a target to query");
@@ -372,15 +548,33 @@
           label = "|| ";
         printf("    %s%s\n", label, edge->inputs_[in]->path().c_str());
       }
+      if (!edge->validations_.empty()) {
+        printf("  validations:\n");
+        for (Node* validation : edge->validations_) {
+          printf("    %s\n", validation->path().c_str());
+        }
+      }
     }
     printf("  outputs:\n");
-    for (vector<Edge*>::const_iterator edge = node->out_edges().begin();
-         edge != node->out_edges().end(); ++edge) {
+    const std::vector<Edge*> out_edges = node->GetOutEdges();
+    for (vector<Edge*>::const_iterator edge = out_edges.begin();
+         edge != out_edges.end(); ++edge) {
       for (vector<Node*>::iterator out = (*edge)->outputs_.begin();
            out != (*edge)->outputs_.end(); ++out) {
         printf("    %s\n", (*out)->path().c_str());
       }
     }
+    const std::vector<Edge*> validation_edges = node->GetValidationOutEdges();
+    if (!validation_edges.empty()) {
+      printf("  validation for:\n");
+      for (vector<Edge*>::const_iterator edge = validation_edges.begin();
+           edge != validation_edges.end(); ++edge) {
+        for (vector<Node*>::iterator out = (*edge)->outputs_.begin();
+             out != (*edge)->outputs_.end(); ++out) {
+          printf("    %s\n", (*out)->path().c_str());
+        }
+      }
+    }
   }
   return 0;
 }
@@ -555,7 +749,7 @@
 }
 
 enum PrintCommandMode { PCM_Single, PCM_All };
-void PrintCommands(Edge* edge, set<Edge*>* seen, PrintCommandMode mode) {
+void PrintCommands(Edge* edge, EdgeSet* seen, PrintCommandMode mode) {
   if (!edge)
     return;
   if (!seen->insert(edge).second)
@@ -606,7 +800,7 @@
     return 1;
   }
 
-  set<Edge*> seen;
+  EdgeSet seen;
   for (vector<Node*>::iterator in = nodes.begin(); in != nodes.end(); ++in)
     PrintCommands((*in)->in_edge(), &seen, mode);
 
@@ -826,6 +1020,10 @@
       Tool::RUN_AFTER_LOGS, &NinjaMain::ToolDeps },
     { "graph", "output graphviz dot file for targets",
       Tool::RUN_AFTER_LOAD, &NinjaMain::ToolGraph },
+    { "inputs", "show all (recursive) inputs for a target",
+      Tool::RUN_AFTER_LOGS, &NinjaMain::ToolInputs },
+    { "path", "find dependency path between two targets",
+      Tool::RUN_AFTER_LOGS, &NinjaMain::ToolPath },
     { "query", "show inputs/outputs for a path",
       Tool::RUN_AFTER_LOGS, &NinjaMain::ToolQuery },
     { "targets",  "list targets by their rule or depth in the DAG",
@@ -878,6 +1076,7 @@
 #ifdef _WIN32
 "  nostatcache  don't batch stat() calls per directory and cache them\n"
 #endif
+"  nothreads    don't use threads to parallelize ninja\n"
 "multiple modes can be enabled via -d FOO -d BAR\n");
     return false;
   } else if (name == "stats") {
@@ -895,11 +1094,14 @@
   } else if (name == "nostatcache") {
     g_experimental_statcache = false;
     return true;
+  } else if (name == "nothreads") {
+    g_use_threads = false;
+    return true;
   } else {
     const char* suggestion =
         SpellcheckString(name.c_str(),
                          "stats", "explain", "keepdepfile", "keeprsp",
-                         "nostatcache", NULL);
+                         "nostatcache", "nothreads", NULL);
     if (suggestion) {
       Error("unknown debug setting '%s', did you mean '%s'?",
             name.c_str(), suggestion);
@@ -912,13 +1114,18 @@
 
 /// Set a warning flag.  Returns false if Ninja should exit instead  of
 /// continuing.
-bool WarningEnable(const string& name, Options* options) {
+bool WarningEnable(const string& name, Options* options, BuildConfig* config) {
   if (name == "list") {
     printf("warning flags:\n"
 "  dupbuild={err,warn}  multiple build lines for one target\n"
 "  phonycycle={err,warn}  phony build statement references itself\n"
 "  depfilemulti={err,warn}  depfile has multiple output paths on separate lines\n"
-    );
+"  missingdepfile={err,warn}  how to treat missing depfiles\n"
+"\n"
+" requires -o usesphonyoutputs=yes\n"
+"  outputdir={err,warn}  how to treat outputs that are directories\n"
+"  missingoutfile={err,warn}  how to treat missing output files\n"
+"  oldoutput={err,warn}  how to treat output files older than their inputs\n");
     return false;
   } else if (name == "dupbuild=err") {
     options->dupe_edges_should_err = true;
@@ -938,10 +1145,38 @@
   } else if (name == "depfilemulti=warn") {
     options->depfile_distinct_target_lines_should_err = false;
     return true;
+  } else if (name == "missingdepfile=err") {
+    config->missing_depfile_should_err = true;
+    return true;
+  } else if (name == "missingdepfile=warn") {
+    config->missing_depfile_should_err = false;
+    return true;
+  } else if (name == "outputdir=err") {
+    config->output_directory_should_err = true;
+    return true;
+  } else if (name == "outputdir=warn") {
+    config->output_directory_should_err = false;
+    return true;
+  } else if (name == "missingoutfile=err") {
+    config->missing_output_file_should_err = true;
+    return true;
+  } else if (name == "missingoutfile=warn") {
+    config->missing_output_file_should_err = false;
+    return true;
+  } else if (name == "oldoutput=err") {
+    config->old_output_should_err = true;
+    return true;
+  } else if (name == "oldoutput=warn") {
+    config->old_output_should_err = false;
+    return true;
   } else {
     const char* suggestion =
         SpellcheckString(name.c_str(), "dupbuild=err", "dupbuild=warn",
-                         "phonycycle=err", "phonycycle=warn", NULL);
+                         "phonycycle=err", "phonycycle=warn",
+                         "missingdepfile=err", "missingdepfile=warn",
+                         "outputdir=err", "outputdir=warn",
+                         "missingoutfile=err", "missingoutfile=warn",
+                         "oldoutput=err", "oldoutput=warn", NULL);
     if (suggestion) {
       Error("unknown warning flag '%s', did you mean '%s'?",
             name.c_str(), suggestion);
@@ -952,6 +1187,46 @@
   }
 }
 
+/// Set an option flag.  Returns false if Ninja should exit instead  of
+/// continuing.
+bool OptionEnable(const string& name, Options* options, BuildConfig* config) {
+  if (name == "list") {
+    printf("option flags:\n"
+"  usesphonyoutputs={yes,no}  whether the generate uses 'phony_output's so \n"
+"                             that these warnings work:\n"
+"                                outputdir\n"
+"                                missingoutfile\n"
+"                                oldoutput\n"
+"  preremoveoutputs={yes,no}  whether to remove outputs before running rule\n");
+    return false;
+  } else if (name == "usesphonyoutputs=yes") {
+    config->uses_phony_outputs = true;
+    return true;
+  } else if (name == "usesphonyoutputs=no") {
+    config->uses_phony_outputs = false;
+    return true;
+  } else if (name == "preremoveoutputs=yes") {
+    config->pre_remove_output_files = true;
+    return true;
+  } else if (name == "preremoveoutputs=no") {
+    config->pre_remove_output_files = false;
+    return true;
+  } else {
+    const char* suggestion =
+        SpellcheckString(name.c_str(),
+                         "usesphonyoutputs=yes", "usesphonyoutputs=no",
+                         "preremoveoutputs=yes", "preremoveoutputs=no",
+                         NULL);
+    if (suggestion) {
+      Error("unknown option flag '%s', did you mean '%s'?",
+            name.c_str(), suggestion);
+    } else {
+      Error("unknown option flag '%s'", name.c_str());
+    }
+    return false;
+  }
+}
+
 bool NinjaMain::OpenBuildLog(bool recompact_only) {
   string log_path = ".ninja_log";
   if (!build_dir_.empty())
@@ -1004,14 +1279,14 @@
   }
 
   if (recompact_only) {
-    bool success = deps_log_.Recompact(path, &err);
+    bool success = deps_log_.Recompact(path, disk_interface_, &err);
     if (!success)
       Error("failed recompaction: %s", err.c_str());
     return success;
   }
 
   if (!config_.dry_run) {
-    if (!deps_log_.OpenForWrite(path, &err)) {
+    if (!deps_log_.OpenForWrite(path, disk_interface_, &err)) {
       Error("opening deps log: %s", err.c_str());
       return false;
     }
@@ -1020,18 +1295,20 @@
   return true;
 }
 
-void NinjaMain::DumpMetrics() {
-  g_metrics->Report();
+void NinjaMain::DumpMetrics(Status *status) {
+  g_metrics->Report(status);
 
-  printf("\n");
+  status->Debug("");
+
   int count = (int)state_.paths_.size();
   int buckets = (int)state_.paths_.bucket_count();
-  printf("path->node hash load %.2f (%d entries / %d buckets)\n",
-         count / (double) buckets, count, buckets);
+  status->Debug("path->node hash load %.2f (%d entries / %d buckets), %zu edges",
+         count / (double) buckets, count, buckets, state_.edges_.size());
+  DumpMemoryUsage(status);
 }
 
 bool NinjaMain::EnsureBuildDirExists() {
-  build_dir_ = state_.bindings_.LookupVariable("builddir");
+  build_dir_ = state_.root_scope_.LookupVariable("builddir");
   if (!build_dir_.empty() && !config_.dry_run) {
     if (!disk_interface_.MakeDirs(build_dir_ + "/.") && errno != EEXIST) {
       Error("creating build directory %s: %s",
@@ -1042,39 +1319,33 @@
   return true;
 }
 
-int NinjaMain::RunBuild(int argc, char** argv) {
+int NinjaMain::RunBuild(int argc, char** argv, Status* status) {
   string err;
   vector<Node*> targets;
   if (!CollectTargetsFromArgs(argc, argv, &targets, &err)) {
-    Error("%s", err.c_str());
+    status->Error("%s", err.c_str());
     return 1;
   }
 
   disk_interface_.AllowStatCache(g_experimental_statcache);
 
-  Builder builder(&state_, config_, &build_log_, &deps_log_, &disk_interface_);
-  for (size_t i = 0; i < targets.size(); ++i) {
-    if (!builder.AddTarget(targets[i], &err)) {
-      if (!err.empty()) {
-        Error("%s", err.c_str());
-        return 1;
-      } else {
-        // Added a target that is already up-to-date; not really
-        // an error.
-      }
-    }
+  Builder builder(&state_, config_, &build_log_, &deps_log_, &disk_interface_,
+                  status, start_time_millis_);
+  if (!builder.AddTargets(targets, &err)) {
+    status->Error("%s", err.c_str());
+    return 1;
   }
 
   // Make sure restat rules do not see stale timestamps.
   disk_interface_.AllowStatCache(false);
 
   if (builder.AlreadyUpToDate()) {
-    printf("ninja: no work to do.\n");
+    status->Info("no work to do.");
     return 0;
   }
 
   if (!builder.Build(&err)) {
-    printf("ninja: build stopped: %s.\n", err.c_str());
+    status->Info("build stopped: %s.", err.c_str());
     if (err.find("interrupted by user") != string::npos) {
       return 2;
     }
@@ -1113,8 +1384,16 @@
               Options* options, BuildConfig* config) {
   config->parallelism = GuessParallelism();
 
-  enum { OPT_VERSION = 1 };
+  enum {
+    OPT_VERSION = 1,
+    OPT_FRONTEND = 2,
+    OPT_FRONTEND_FILE = 3,
+  };
   const option kLongOptions[] = {
+#ifndef _WIN32
+    { "frontend", required_argument, NULL, OPT_FRONTEND },
+    { "frontend_file", required_argument, NULL, OPT_FRONTEND_FILE },
+#endif
     { "help", no_argument, NULL, 'h' },
     { "version", no_argument, NULL, OPT_VERSION },
     { "verbose", no_argument, NULL, 'v' },
@@ -1123,7 +1402,7 @@
 
   int opt;
   while (!options->tool &&
-         (opt = getopt_long(*argc, *argv, "d:f:j:k:l:nt:vw:C:h", kLongOptions,
+         (opt = getopt_long(*argc, *argv, "d:f:j:k:l:mnt:vw:o:C:ph", kLongOptions,
                             NULL)) != -1) {
     switch (opt) {
       case 'd':
@@ -1176,15 +1455,28 @@
         config->verbosity = BuildConfig::VERBOSE;
         break;
       case 'w':
-        if (!WarningEnable(optarg, options))
+        if (!WarningEnable(optarg, options, config))
+          return 1;
+        break;
+      case 'o':
+        if (!OptionEnable(optarg, options, config))
           return 1;
         break;
       case 'C':
         options->working_dir = optarg;
         break;
+      case 'p':
+        options->persistent = true;
+        break;
       case OPT_VERSION:
         printf("%s\n", kNinjaVersion);
         return 0;
+      case OPT_FRONTEND:
+        config->frontend = optarg;
+        break;
+      case OPT_FRONTEND_FILE:
+        config->frontend_file = optarg;
+        break;
       case 'h':
       default:
         Usage(*config);
@@ -1194,9 +1486,30 @@
   *argv += optind;
   *argc -= optind;
 
+  if (config->frontend != NULL && config->frontend_file != NULL) {
+    Fatal("only one of --frontend or --frontend_file may be specified.");
+  }
+
+  if (config->pre_remove_output_files && !config->uses_phony_outputs) {
+    Fatal("preremoveoutputs=yes requires usesphonyoutputs=yes.");
+  }
+
   return -1;
 }
 
+static void WaitForInput(const BuildConfig& config) {
+  static char* buf = nullptr;
+  static size_t len = 0;
+
+  if (config.verbosity == BuildConfig::VERBOSE) {
+    fprintf(stderr, "ninja waiting for input...\n");
+  }
+  ssize_t rc = getline(&buf, &len, stdin);
+  if (rc < 0) {
+    exit(0);
+  }
+}
+
 NORETURN void real_main(int argc, char** argv) {
   // Use exit() instead of return in this function to avoid potentially
   // expensive cleanup when destructing NinjaMain.
@@ -1224,12 +1537,15 @@
     // Don't print this if a tool is being used, so that tool output
     // can be piped into a file without this string showing up.
     if (!options.tool)
-      printf("ninja: Entering directory `%s'\n", options.working_dir);
+      Info("Entering directory `%s'", options.working_dir);
     if (chdir(options.working_dir) < 0) {
-      Fatal("chdir to '%s' - %s", options.working_dir, strerror(errno));
+      Error("chdir to '%s' - %s", options.working_dir, strerror(errno));
+      exit(1);
     }
   }
 
+  SetThreadPoolThreadCount(g_use_threads ? GetProcessorCount() : 1);
+
   if (options.tool && options.tool->when == Tool::RUN_AFTER_FLAGS) {
     // None of the RUN_AFTER_FLAGS actually use a NinjaMain, but it's needed
     // by other tools.
@@ -1237,11 +1553,22 @@
     exit((ninja.*options.tool->func)(&options, argc, argv));
   }
 
+  Status* status = NULL;
+
   // Limit number of rebuilds, to prevent infinite loops.
   const int kCycleLimit = 100;
   for (int cycle = 1; cycle <= kCycleLimit; ++cycle) {
     NinjaMain ninja(ninja_command, config);
 
+    if (status == NULL) {
+#ifndef _WIN32
+      if (config.frontend != NULL || config.frontend_file != NULL)
+        status = new StatusSerializer(config);
+      else
+#endif
+        status = new StatusPrinter(config);
+    }
+
     ManifestParserOptions parser_opts;
     if (options.dupe_edges_should_err) {
       parser_opts.dupe_edge_action_ = kDupeEdgeActionError;
@@ -1252,7 +1579,7 @@
     ManifestParser parser(&ninja.state_, &ninja.disk_interface_, parser_opts);
     string err;
     if (!parser.Load(options.input_file, &err)) {
-      Error("%s", err.c_str());
+      status->Error("%s", err.c_str());
       exit(1);
     }
 
@@ -1269,7 +1596,7 @@
       exit((ninja.*options.tool->func)(&options, argc, argv));
 
     // Attempt to rebuild the manifest before building anything else
-    if (ninja.RebuildManifest(options.input_file, &err)) {
+    if (ninja.RebuildManifest(options.input_file, &err, status)) {
       // In dry_run mode the regeneration will succeed without changing the
       // manifest forever. Better to return immediately.
       if (config.dry_run)
@@ -1277,18 +1604,37 @@
       // Start the build over with the new manifest.
       continue;
     } else if (!err.empty()) {
-      Error("rebuilding '%s': %s", options.input_file, err.c_str());
+      status->Error("rebuilding '%s': %s", options.input_file, err.c_str());
       exit(1);
     }
 
-    int result = ninja.RunBuild(argc, argv);
+    int result = 0;
+    do {
+      Stopwatch stopwatch;
+
+      if (options.persistent) {
+        WaitForInput(config);
+        stopwatch.Restart();
+      }
+
+      result = ninja.RunBuild(argc, argv, status);
+      if (options.persistent) {
+        fprintf(stderr, "build %s in %0.3f seconds\n",
+                result == 0 ? "succeeded" : "failed", stopwatch.Elapsed());
+        ninja.state_.Reset();
+      }
+    } while (options.persistent);
+
     if (g_metrics)
-      ninja.DumpMetrics();
+      ninja.DumpMetrics(status);
+
+    delete status;
     exit(result);
   }
 
-  Error("manifest '%s' still dirty after %d tries\n",
+  status->Error("manifest '%s' still dirty after %d tries",
       options.input_file, kCycleLimit);
+  delete status;
   exit(1);
 }
 
diff --git a/src/ninja_test.cc b/src/ninja_test.cc
index d642c5c..066ed43 100644
--- a/src/ninja_test.cc
+++ b/src/ninja_test.cc
@@ -25,8 +25,9 @@
 #include <getopt.h>
 #endif
 
-#include "test.h"
 #include "line_printer.h"
+#include "test.h"
+#include "thread_pool.h"
 
 struct RegisteredTest {
   testing::Test* (*factory)();
@@ -104,6 +105,9 @@
         *test_filter = optarg;
         break;
       }  // else fall through.
+#if defined(__has_cpp_attribute) && __has_cpp_attribute(clang::fallthrough)
+      [[clang::fallthrough]];
+#endif
     default:
       Usage();
       return false;
@@ -129,6 +133,8 @@
 int main(int argc, char **argv) {
   int tests_started = 0;
 
+  SetThreadPoolThreadCount(16);
+
   const char* test_filter = "*";
   if (!ReadFlags(&argc, &argv, &test_filter))
     return 1;
diff --git a/src/parallel_map.h b/src/parallel_map.h
new file mode 100644
index 0000000..00799d8
--- /dev/null
+++ b/src/parallel_map.h
@@ -0,0 +1,109 @@
+// Copyright 2019 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef NINJA_PARALLEL_MAP_H_
+#define NINJA_PARALLEL_MAP_H_
+
+#include <functional>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+#include "thread_pool.h"
+#include "util.h"
+
+namespace detail {
+
+template <typename T>
+struct MapFnResults {
+  MapFnResults(size_t size) : size_(size), results_(new T[size] {}) {}
+
+  using ReturnType = std::vector<T>;
+
+  template <typename Vector, typename MapFn>
+  void MapItem(Vector& vec, const MapFn& map_fn, size_t idx) {
+    assert(idx < size_);
+    results_.get()[idx] = map_fn(vec[idx]);
+  }
+
+  ReturnType Finish() {
+    ReturnType result;
+    result.reserve(size_);
+    std::move(results_.get(), results_.get() + size_,
+              std::back_inserter(result));
+    return result;
+  }
+
+private:
+  size_t size_;
+
+  /// We'd like to store the intermediate results using std::vector<T>, but
+  /// the std::vector<bool> specialization doesn't allow concurrent modification
+  /// of different elements, so use a simple array instead.
+  std::unique_ptr<T[]> results_;
+};
+
+template <>
+struct MapFnResults<void> {
+  MapFnResults(size_t) {}
+
+  using ReturnType = void;
+
+  template <typename Vector, typename MapFn>
+  void MapItem(Vector& vec, const MapFn& map_fn, size_t idx) {
+    map_fn(vec[idx]);
+  }
+
+  ReturnType Finish() {}
+};
+
+} // namespace detail
+
+template <typename T>
+std::vector<std::pair<T, T>> SplitByThreads(T total) {
+  return SplitByCount(total, GetOptimalThreadPoolJobCount());
+}
+
+/// Run |map_fn| on each element of |vec| and return a vector containing the
+/// result, in the same order as the input vector. Returns void instead if the
+/// map function returns void.
+///
+/// The map function is invoked from multiple threads, so the functor is marked
+/// const. (e.g. A "mutable" lambda isn't allowed.)
+template <typename Vector, typename MapFn>
+auto ParallelMap(ThreadPool* thread_pool, Vector&& vec, const MapFn& map_fn) ->
+    typename detail::MapFnResults<
+      typename std::remove_reference<
+        decltype(map_fn(vec[0]))>::type>::ReturnType {
+  // Identify the return type of the map function, and if it isn't void, prepare
+  // a vector to hold the result of mapping each item in the sequence.
+  using MapReturn = typename std::remove_reference<decltype(map_fn(vec[0]))>::type;
+  detail::MapFnResults<MapReturn> results(vec.size());
+
+  // Split the sequence up into groups.
+  std::vector<std::pair<size_t, size_t>> ranges = SplitByThreads(vec.size());
+  std::vector<std::function<void()>> tasks;
+  for (auto& range : ranges) {
+    tasks.emplace_back([&results, &vec, &map_fn, range] {
+      for (size_t idx = range.first; idx < range.second; ++idx) {
+        results.MapItem(vec, map_fn, idx);
+      }
+    });
+  }
+  thread_pool->RunTasks(std::move(tasks));
+
+  return results.Finish();
+}
+
+#endif  // NINJA_PARALLEL_MAP_H_
diff --git a/src/proto.cc b/src/proto.cc
new file mode 100644
index 0000000..c29e621
--- /dev/null
+++ b/src/proto.cc
@@ -0,0 +1,154 @@
+// Copyright 2017 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#define __STDC_LIMIT_MACROS
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "proto.h"
+
+// This file and proto.h implement a minimal write-only protobuf runtime to be
+// used with headers generated by misc/generate_proto_header.py.
+
+// From https://github.com/google/protobuf/blob/3.0.x/src/google/protobuf/wire_format_lite.h
+enum WireType {
+  WIRETYPE_VARINT = 0,
+  WIRETYPE_FIXED64 = 1,
+  WIRETYPE_LENGTH_DELIMITED = 2,
+  WIRETYPE_START_GROUP = 3,
+  WIRETYPE_END_GROUP = 4,
+  WIRETYPE_FIXED32 = 5,
+};
+
+static uint8_t MakeTag(int number, WireType wire_type) {
+  const size_t FIELD_NUMBER_SHIFT = 3;
+  return (number << FIELD_NUMBER_SHIFT) | static_cast<uint8_t>(wire_type);
+}
+
+// Based on https://github.com/google/protobuf/blob/3.0.x/src/google/protobuf/io/coded_stream.h
+// Compile-time equivalent of VarintSize64().
+template <uint64_t Value>
+struct StaticVarintSize {
+  static const int value =
+      (Value < (1ULL << 7))
+          ? 1
+          : (Value < (1ULL << 14))
+              ? 2
+              : (Value < (1ULL << 21))
+                  ? 3
+                  : (Value < (1ULL << 28))
+                      ? 4
+                      : (Value < (1ULL << 35))
+                          ? 5
+                          : (Value < (1ULL << 42))
+                              ? 6
+                              : (Value < (1ULL << 49))
+                                  ? 7
+                                  : (Value < (1ULL << 56))
+                                      ? 8
+                                      : (Value < (1ULL << 63))
+                                          ? 9
+                                          : 10;
+};
+
+static void WriteVarint32WithType(std::ostream* output, int number,
+                                  uint32_t value, WireType type) {
+  uint8_t buf[StaticVarintSize<UINT32_MAX>::value + 1];
+  uint8_t *target = buf;
+
+  *target++ = MakeTag(number, type);
+
+  while (value >= 0x80) {
+    *target = static_cast<uint8_t>(value | 0x80);
+    value >>= 7;
+    ++target;
+  }
+  *target++ = static_cast<uint8_t>(value);
+
+  output->write(reinterpret_cast<char*>(buf), target - buf);
+}
+
+void WriteVarint32NoTag(std::ostream* output, uint32_t value) {
+  uint8_t buf[StaticVarintSize<UINT32_MAX>::value];
+  uint8_t *target = buf;
+
+  while (value >= 0x80) {
+    *target = static_cast<uint8_t>(value | 0x80);
+    value >>= 7;
+    ++target;
+  }
+  *target++ = static_cast<uint8_t>(value);
+
+  output->write(reinterpret_cast<char*>(buf), target - buf);
+}
+
+void WriteVarint32(std::ostream* output, int number, uint32_t value) {
+  WriteVarint32WithType(output, number, value, WIRETYPE_VARINT);
+}
+
+void WriteVarint32SignExtended(std::ostream* output, int number,
+                               int32_t value) {
+  if (value < 0) {
+    WriteVarint64(output, number, static_cast<uint64_t>(value));
+  } else {
+    WriteVarint32(output, number, static_cast<uint32_t>(value));
+  }
+}
+
+void WriteVarint64(std::ostream* output, int number, uint64_t value) {
+  uint8_t buf[StaticVarintSize<UINT64_MAX>::value + 1];
+  uint8_t *target = buf;
+
+  *target++ = MakeTag(number, WIRETYPE_VARINT);
+
+  while (value >= 0x80) {
+    *target = static_cast<uint8_t>(value | 0x80);
+    value >>= 7;
+    ++target;
+  }
+  *target++ = static_cast<uint8_t>(value);
+
+  output->write(reinterpret_cast<char*>(buf), target - buf);
+}
+
+void WriteFixed32(std::ostream* output, int number, uint32_t value) {
+  uint8_t buf[sizeof(value) + 1];
+  uint8_t *target = buf;
+
+  *target++ = MakeTag(number, WIRETYPE_FIXED32);
+  memcpy(target, &value, sizeof(value));
+
+  output->write(reinterpret_cast<char*>(buf), sizeof(buf));
+}
+
+void WriteFixed64(std::ostream* output, int number, uint64_t value) {
+  uint8_t buf[sizeof(value) + 1];
+  uint8_t *target = buf;
+
+  *target++ = MakeTag(number, WIRETYPE_FIXED64);
+  memcpy(target, &value, sizeof(value));
+
+  output->write(reinterpret_cast<char*>(buf), sizeof(buf));
+}
+
+void WriteString(std::ostream* output, int number, const std::string& value) {
+  WriteLengthDelimited(output, number, value.size());
+  output->write(value.data(), value.size());
+}
+
+void WriteLengthDelimited(std::ostream* output, int number, size_t size) {
+  WriteVarint32WithType(output, number, size, WIRETYPE_LENGTH_DELIMITED);
+}
diff --git a/src/proto.h b/src/proto.h
new file mode 100644
index 0000000..8552719
--- /dev/null
+++ b/src/proto.h
@@ -0,0 +1,110 @@
+// Copyright 2017 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef NINJA_PROTO_H_
+#define NINJA_PROTO_H_
+
+#include <iostream>
+
+// This file and proto.cc implement a minimal write-only protobuf runtime to be
+// used with headers generated by misc/generate_proto_header.py.
+
+// Based on https://github.com/google/protobuf/blob/3.0.x/src/google/protobuf/wire_format_lite.h
+static inline uint32_t ZigZagEncode32(int32_t n) {
+  // Note:  the right-shift must be arithmetic
+  return (static_cast<uint32_t>(n) << 1) ^ (n >> 31);
+}
+
+static inline uint64_t ZigZagEncode64(int64_t n) {
+  // Note:  the right-shift must be arithmetic
+  return (static_cast<uint64_t>(n) << 1) ^ (n >> 63);
+}
+
+// Based on https://github.com/google/protobuf/blob/3.0.x/src/google/protobuf/io/coded_stream.h
+static inline size_t VarintSize32(uint32_t value) {
+  if (value < (1 << 7)) {
+    return 1;
+  } else if (value < (1 << 14)) {
+    return 2;
+  } else if (value < (1 << 21)) {
+    return 3;
+  } else if (value < (1 << 28)) {
+    return 4;
+  } else {
+    return 5;
+  }
+}
+
+static inline size_t VarintSize32SignExtended(int32_t value) {
+  if (value < 0) {
+    return 10;     // TODO(kenton):  Make this a symbolic constant.
+  } else {
+    return VarintSize32(static_cast<uint32_t>(value));
+  }
+}
+
+static inline size_t VarintSize64(uint64_t value) {
+  if (value < (1ull << 35)) {
+    if (value < (1ull << 7)) {
+      return 1;
+    } else if (value < (1ull << 14)) {
+      return 2;
+    } else if (value < (1ull << 21)) {
+      return 3;
+    } else if (value < (1ull << 28)) {
+      return 4;
+    } else {
+      return 5;
+    }
+  } else {
+    if (value < (1ull << 42)) {
+      return 6;
+    } else if (value < (1ull << 49)) {
+      return 7;
+    } else if (value < (1ull << 56)) {
+      return 8;
+    } else if (value < (1ull << 63)) {
+      return 9;
+    } else {
+      return 10;
+    }
+  }
+}
+
+static inline size_t VarintSizeBool(bool /*value*/) {
+  return 1;
+}
+
+static inline size_t FixedSize32(uint32_t /*value*/) {
+  return 4;
+}
+
+static inline size_t FixedSize64(uint64_t /*value*/) {
+  return 8;
+}
+
+static inline size_t StringSize(const std::string& value) {
+  return VarintSize32(value.size()) + value.size();
+}
+
+void WriteVarint32(std::ostream* output, int number, uint32_t value);
+void WriteVarint32NoTag(std::ostream* output, uint32_t value);
+void WriteVarint32SignExtended(std::ostream* output, int number, int32_t value);
+void WriteVarint64(std::ostream* output, int number, uint64_t value);
+void WriteFixed32(std::ostream* output, int number, uint32_t value);
+void WriteFixed64(std::ostream* output, int number, uint64_t value);
+void WriteString(std::ostream* output, int number, const std::string& value);
+void WriteLengthDelimited(std::ostream* output, int number, size_t size);
+
+#endif
diff --git a/src/state.cc b/src/state.cc
index 9b3c7e1..f8e47aa 100644
--- a/src/state.cc
+++ b/src/state.cc
@@ -22,6 +22,9 @@
 #include "metrics.h"
 #include "util.h"
 
+Pool::Pool(const HashedStrView& name, int depth) : name_(name), depth_(depth) {
+  pos_.base = new BasePosition {{ &State::kBuiltinScope, 0 }}; // leaked
+}
 
 void Pool::EdgeScheduled(const Edge& edge) {
   if (depth_ != 0)
@@ -38,7 +41,7 @@
   delayed_.insert(edge);
 }
 
-void Pool::RetrieveReadyEdges(set<Edge*>* ready_queue) {
+void Pool::RetrieveReadyEdges(EdgeSet* ready_queue) {
   DelayedEdges::iterator it = delayed_.begin();
   while (it != delayed_.end()) {
     Edge* edge = *it;
@@ -61,63 +64,77 @@
   }
 }
 
-// static
-bool Pool::WeightedEdgeCmp(const Edge* a, const Edge* b) {
-  if (!a) return b;
-  if (!b) return false;
-  int weight_diff = a->weight() - b->weight();
-  return ((weight_diff < 0) || (weight_diff == 0 && a < b));
-}
-
+Scope State::kBuiltinScope({});
 Pool State::kDefaultPool("", 0);
 Pool State::kConsolePool("console", 1);
-const Rule State::kPhonyRule("phony");
+Rule State::kPhonyRule("phony");
 
 State::State() {
-  bindings_.AddRule(&kPhonyRule);
+  // Reserve scope position (root, 0) for built-in rules.
+  root_scope_.AllocDecls(1);
+
+  AddBuiltinRule(&kPhonyRule);
   AddPool(&kDefaultPool);
   AddPool(&kConsolePool);
 }
 
-void State::AddPool(Pool* pool) {
-  assert(LookupPool(pool->name()) == NULL);
-  pools_[pool->name()] = pool;
+// Add a built-in rule at the top of the root scope.
+void State::AddBuiltinRule(Rule* rule) {
+  root_scope_.AddRule(rule);
 }
 
-Pool* State::LookupPool(const string& pool_name) {
-  map<string, Pool*>::iterator i = pools_.find(pool_name);
-  if (i == pools_.end())
-    return NULL;
-  return i->second;
+bool State::AddPool(Pool* pool) {
+  return pools_.insert({ pool->name_hashed(), pool }).second;
 }
 
 Edge* State::AddEdge(const Rule* rule) {
   Edge* edge = new Edge();
+  edge->pos_.base = new BasePosition {{ &root_scope_, 0 }}; // leaked
   edge->rule_ = rule;
   edge->pool_ = &State::kDefaultPool;
-  edge->env_ = &bindings_;
+  edge->id_ = edges_.size();
   edges_.push_back(edge);
   return edge;
 }
 
-Node* State::GetNode(StringPiece path, uint64_t slash_bits) {
-  Node* node = LookupNode(path);
-  if (node)
-    return node;
-  node = new Node(path.AsString(), slash_bits);
-  paths_[node->path()] = node;
-  return node;
+Pool* State::LookupPool(const HashedStrView& pool_name) {
+  auto i = pools_.find(pool_name);
+  if (i == pools_.end())
+    return nullptr;
+  return i->second;
 }
 
-Node* State::LookupNode(StringPiece path) const {
-  METRIC_RECORD("lookup node");
-  Paths::const_iterator i = paths_.find(path);
-  if (i != paths_.end())
-    return i->second;
-  return NULL;
+Pool* State::LookupPoolAtPos(const HashedStrView& pool_name,
+                             DeclIndex dfs_location) {
+  Pool* result = LookupPool(pool_name);
+  if (result == nullptr) return nullptr;
+  return result->dfs_location() < dfs_location ? result : nullptr;
 }
 
-Node* State::SpellcheckNode(const string& path) {
+Node* State::GetNode(const HashedStrView& path, uint64_t slash_bits) {
+  if (Node** opt_node = paths_.Lookup(path))
+    return *opt_node;
+  // Create a new node and try to insert it.
+  std::unique_ptr<Node> node(new Node(path, slash_bits));
+  if (paths_.insert({node->path_hashed(), node.get()}).second)
+    return node.release();
+  // Another thread beat us to it. Use its node instead.
+  return *paths_.Lookup(path);
+}
+
+Node* State::LookupNode(const HashedStrView& path) const {
+  if (Node* const* opt_node = paths_.Lookup(path))
+    return *opt_node;
+  return nullptr;
+}
+
+Node* State::LookupNodeAtPos(const HashedStrView& path,
+                             DeclIndex dfs_location) const {
+  Node* result = LookupNode(path);
+  return result && result->dfs_location() < dfs_location ? result : nullptr;
+}
+
+Node* State::SpellcheckNode(StringPiece path) {
   const bool kAllowReplacements = true;
   const int kMaxValidEditDistance = 3;
 
@@ -125,7 +142,7 @@
   Node* result = NULL;
   for (Paths::iterator i = paths_.begin(); i != paths_.end(); ++i) {
     int distance = EditDistance(
-        i->first, path, kAllowReplacements, kMaxValidEditDistance);
+        i->first.str_view(), path, kAllowReplacements, kMaxValidEditDistance);
     if (distance < min_distance && i->second) {
       min_distance = distance;
       result = i->second;
@@ -149,16 +166,6 @@
   return true;
 }
 
-bool State::AddDefault(StringPiece path, string* err) {
-  Node* node = LookupNode(path);
-  if (!node) {
-    *err = "unknown target '" + path.AsString() + "'";
-    return false;
-  }
-  defaults_.push_back(node);
-  return true;
-}
-
 vector<Node*> State::RootNodes(string* err) const {
   vector<Node*> root_nodes;
   // Search for nodes with no output.
@@ -166,7 +173,7 @@
        e != edges_.end(); ++e) {
     for (vector<Node*>::const_iterator out = (*e)->outputs_.begin();
          out != (*e)->outputs_.end(); ++out) {
-      if ((*out)->out_edges().empty())
+      if (!(*out)->has_out_edge())
         root_nodes.push_back(*out);
     }
   }
@@ -181,12 +188,19 @@
   return defaults_.empty() ? RootNodes(err) : defaults_;
 }
 
+DeclIndex State::AllocDfsLocation(DeclIndex count) {
+  DeclIndex result = dfs_location_;
+  dfs_location_ += count;
+  return result;
+}
+
 void State::Reset() {
   for (Paths::iterator i = paths_.begin(); i != paths_.end(); ++i)
     i->second->ResetState();
   for (vector<Edge*>::iterator e = edges_.begin(); e != edges_.end(); ++e) {
     (*e)->outputs_ready_ = false;
     (*e)->mark_ = Edge::VisitNone;
+    (*e)->precomputed_dirtiness_ = false;
   }
 }
 
@@ -201,9 +215,7 @@
   }
   if (!pools_.empty()) {
     printf("resource_pools:\n");
-    for (map<string, Pool*>::const_iterator it = pools_.begin();
-         it != pools_.end(); ++it)
-    {
+    for (auto it = pools_.begin(); it != pools_.end(); ++it) {
       if (!it->second->name().empty()) {
         it->second->Dump();
       }
diff --git a/src/state.h b/src/state.h
index 6fe886c..db18d60 100644
--- a/src/state.h
+++ b/src/state.h
@@ -16,13 +16,16 @@
 #define NINJA_STATE_H_
 
 #include <map>
+#include <memory>
 #include <set>
 #include <string>
 #include <vector>
 using namespace std;
 
 #include "eval_env.h"
-#include "hash_map.h"
+#include "graph.h"
+#include "string_piece.h"
+#include "concurrent_hash_map.h"
 #include "util.h"
 
 struct Edge;
@@ -38,13 +41,17 @@
 /// the total scheduled weight diminishes enough (i.e. when a scheduled edge
 /// completes).
 struct Pool {
-  Pool(const string& name, int depth)
-    : name_(name), current_use_(0), depth_(depth), delayed_(&WeightedEdgeCmp) {}
+  Pool() {}
+