From 6bbc783f48498b808e19db4441299dc7d85a278b Mon Sep 17 00:00:00 2001
From: "Simon J. Gerraty" <sjg@FreeBSD.org>
Date: Sat, 5 Sep 2020 16:11:04 +0000
Subject: [PATCH] Import bmake-20200902

Lots of code refactoring, simplification and cleanup.
Lots of new unit-tests providing much higher code coverage.
All courtesy of rillig at netbsd.

Other significant changes:

o new read-only variable .SHELL which provides the path of the shell
  used to run scripts (as defined by  the .SHELL target).

o new debug option -dl: LINT mode, does the equivalent of := for all
  variable assignments so that file and line number are reported for
  variable parse errors.
---
 ChangeLog                                     |  240 +
 FILES                                         |  527 +-
 LICENSE                                       |    1 +
 Makefile                                      |   42 +-
 VERSION                                       |    2 +-
 arch.c                                        |  499 +-
 bmake.1                                       |  134 +-
 bmake.cat1                                    |  130 +-
 bsd.after-import.mk                           |   21 +-
 buf.c                                         |  216 +-
 buf.h                                         |   66 +-
 compat.c                                      |  136 +-
 cond.c                                        |  677 +--
 dir.c                                         | 1170 ++--
 dir.h                                         |   28 +-
 enum.c                                        |  100 +
 enum.h                                        |  193 +
 for.c                                         |  157 +-
 hash.c                                        |  277 +-
 hash.h                                        |   81 +-
 job.c                                         |  134 +-
 job.h                                         |   17 +-
 lst.c                                         |  641 +++
 lst.h                                         |  194 +-
 lst.lib/Makefile                              |    0
 lst.lib/lstAppend.c                           |  121 -
 lst.lib/lstAtEnd.c                            |   79 -
 lst.lib/lstAtFront.c                          |   76 -
 lst.lib/lstClose.c                            |   85 -
 lst.lib/lstConcat.c                           |  184 -
 lst.lib/lstDatum.c                            |   76 -
 lst.lib/lstDeQueue.c                          |   86 -
 lst.lib/lstDestroy.c                          |  101 -
 lst.lib/lstDupl.c                             |  107 -
 lst.lib/lstEnQueue.c                          |   77 -
 lst.lib/lstFind.c                             |   73 -
 lst.lib/lstFindFrom.c                         |   89 -
 lst.lib/lstFirst.c                            |   76 -
 lst.lib/lstForEach.c                          |   75 -
 lst.lib/lstForEachFrom.c                      |  124 -
 lst.lib/lstInit.c                             |   85 -
 lst.lib/lstInsert.c                           |  121 -
 lst.lib/lstInt.h                              |  105 -
 lst.lib/lstIsAtEnd.c                          |   86 -
 lst.lib/lstIsEmpty.c                          |   74 -
 lst.lib/lstLast.c                             |   76 -
 lst.lib/lstMember.c                           |   77 -
 lst.lib/lstNext.c                             |  119 -
 lst.lib/lstOpen.c                             |   86 -
 lst.lib/lstPrev.c                             |   78 -
 lst.lib/lstRemove.c                           |  134 -
 lst.lib/lstReplace.c                          |   77 -
 lst.lib/lstSucc.c                             |   78 -
 main.c                                        |  376 +-
 make-conf.h                                   |   10 +-
 make.1                                        |  134 +-
 make.c                                        |  464 +-
 make.h                                        |  411 +-
 make_malloc.c                                 |   50 +-
 make_malloc.h                                 |   19 +-
 meta.c                                        |  151 +-
 metachar.c                                    |   36 +-
 metachar.h                                    |   21 +-
 mk/ChangeLog                                  |  301 +-
 mk/FILES                                      |    1 +
 mk/README                                     |    8 +-
 mk/auto.dep.mk                                |   14 +-
 mk/auto.obj.mk                                |   12 +-
 mk/autoconf.mk                                |   10 +-
 mk/autodep.mk                                 |   16 +-
 mk/compiler.mk                                |    8 +-
 mk/cython.mk                                  |    8 +-
 mk/dirdeps-cache-update.mk                    |  179 +
 mk/dirdeps-options.mk                         |   39 +-
 mk/dirdeps-targets.mk                         |   47 +-
 mk/dirdeps.mk                                 |  132 +-
 mk/dpadd.mk                                   |   30 +-
 mk/files.mk                                   |    8 +-
 mk/gendirdeps.mk                              |   42 +-
 mk/host-target.mk                             |    4 +-
 mk/host.libnames.mk                           |    8 +-
 mk/inc.mk                                     |    8 +-
 mk/init.mk                                    |   14 +-
 mk/install-mk                                 |   14 +-
 mk/install-new.mk                             |    8 +-
 mk/java.mk                                    |   10 +-
 mk/lib.mk                                     |   14 +-
 mk/libnames.mk                                |    8 +-
 mk/libs.mk                                    |   20 +-
 mk/links.mk                                   |    8 +-
 mk/manifest.mk                                |   16 +-
 mk/meta.autodep.mk                            |   10 +-
 mk/meta.stage.mk                              |    8 +-
 mk/meta.subdir.mk                             |   10 +-
 mk/meta.sys.mk                                |   14 +-
 mk/meta2deps.py                               |   48 +-
 mk/meta2deps.sh                               |   30 +-
 mk/mk-files.txt                               |   51 +-
 mk/mkopt.sh                                   |    8 +-
 mk/obj.mk                                     |    8 +-
 mk/options.mk                                 |   12 +-
 mk/own.mk                                     |    8 +-
 mk/prlist.mk                                  |    8 +-
 mk/prog.mk                                    |    4 +-
 mk/progs.mk                                   |   17 +-
 mk/rst2htm.mk                                 |    8 +-
 mk/scripts.mk                                 |    8 +-
 mk/srctop.mk                                  |   10 +-
 mk/stage-install.sh                           |   61 +-
 mk/sys.clean-env.mk                           |   10 +-
 mk/sys.debug.mk                               |    8 +-
 mk/sys.dependfile.mk                          |    8 +-
 mk/sys.mk                                     |   11 +-
 mk/sys.vars.mk                                |   10 +-
 mk/sys/AIX.mk                                 |    8 +-
 mk/sys/Darwin.mk                              |    4 +-
 mk/sys/Generic.mk                             |    8 +-
 mk/sys/HP-UX.mk                               |   10 +-
 mk/sys/IRIX.mk                                |    4 +-
 mk/sys/Linux.mk                               |    6 +-
 mk/sys/NetBSD.mk                              |    4 +-
 mk/sys/OSF1.mk                                |    6 +-
 mk/sys/OpenBSD.mk                             |    6 +-
 mk/sys/SunOS.mk                               |    4 +-
 mk/sys/UnixWare.mk                            |    4 +-
 mk/target-flags.mk                            |   12 +-
 mk/warnings.mk                                |   20 +-
 mk/whats.mk                                   |   10 +-
 mk/yacc.mk                                    |    8 +-
 nonints.h                                     |   69 +-
 os.sh                                         |    9 +-
 parse.c                                       |  346 +-
 sprite.h                                      |  116 -
 str.c                                         |  251 +-
 strlist.c                                     |   20 +-
 strlist.h                                     |    8 +-
 suff.c                                        | 1233 ++---
 targ.c                                        |  448 +-
 trace.c                                       |   12 +-
 unit-tests/Makefile                           |  358 +-
 unit-tests/archive-suffix.exp                 |    2 +
 unit-tests/archive-suffix.mk                  |   23 +
 unit-tests/archive.exp                        |   13 +
 unit-tests/archive.mk                         |   45 +
 unit-tests/cmd-interrupt.exp                  |    9 +
 unit-tests/cmd-interrupt.mk                   |   50 +
 unit-tests/cmdline.exp                        |    5 +
 unit-tests/cmdline.mk                         |   37 +
 unit-tests/cond-cmp-numeric-eq.exp            |    1 +
 unit-tests/cond-cmp-numeric-eq.mk             |   53 +
 unit-tests/cond-cmp-numeric-ge.exp            |    1 +
 unit-tests/cond-cmp-numeric-ge.mk             |   75 +
 unit-tests/cond-cmp-numeric-gt.exp            |    1 +
 unit-tests/cond-cmp-numeric-gt.mk             |   73 +
 unit-tests/cond-cmp-numeric-le.exp            |    1 +
 unit-tests/cond-cmp-numeric-le.mk             |   75 +
 unit-tests/cond-cmp-numeric-lt.exp            |    1 +
 unit-tests/cond-cmp-numeric-lt.mk             |   73 +
 unit-tests/cond-cmp-numeric-ne.exp            |    1 +
 unit-tests/cond-cmp-numeric-ne.mk             |   49 +
 unit-tests/cond-cmp-numeric.exp               |    1 +
 unit-tests/cond-cmp-numeric.mk                |    8 +
 unit-tests/cond-cmp-string.exp                |    5 +
 unit-tests/cond-cmp-string.mk                 |   39 +
 unit-tests/cond-func-commands.exp             |    1 +
 unit-tests/cond-func-commands.mk              |   36 +
 unit-tests/cond-func-defined.exp              |    5 +
 unit-tests/cond-func-defined.mk               |   33 +
 unit-tests/cond-func-empty.exp                |    1 +
 unit-tests/cond-func-empty.mk                 |    8 +
 unit-tests/cond-func-exists.exp               |    1 +
 unit-tests/cond-func-exists.mk                |   42 +
 unit-tests/cond-func-make.exp                 |    1 +
 unit-tests/cond-func-make.mk                  |    8 +
 unit-tests/cond-func-target.exp               |    1 +
 unit-tests/cond-func-target.mk                |   38 +
 unit-tests/cond-func.exp                      |    9 +
 unit-tests/cond-func.mk                       |   63 +
 unit-tests/cond-late.exp                      |    1 +
 unit-tests/cond-late.mk                       |   10 +-
 unit-tests/cond-op-and.exp                    |    1 +
 unit-tests/cond-op-and.mk                     |   27 +
 unit-tests/cond-op-not.exp                    |    1 +
 unit-tests/cond-op-not.mk                     |   22 +
 unit-tests/cond-op-or.exp                     |    1 +
 unit-tests/cond-op-or.mk                      |   27 +
 unit-tests/cond-op-parentheses.exp            |    1 +
 unit-tests/cond-op-parentheses.mk             |    8 +
 unit-tests/cond-op.exp                        |    5 +
 unit-tests/cond-op.mk                         |   60 +
 unit-tests/cond-short.mk                      |   20 +-
 unit-tests/cond-token-number.exp              |    1 +
 unit-tests/cond-token-number.mk               |    8 +
 unit-tests/cond-token-plain.exp               |    1 +
 unit-tests/cond-token-plain.mk                |    9 +
 unit-tests/cond-token-string.exp              |    1 +
 unit-tests/cond-token-string.mk               |    8 +
 unit-tests/cond-token-var.exp                 |    7 +
 unit-tests/cond-token-var.mk                  |   34 +
 unit-tests/counter.exp                        |   88 +
 unit-tests/counter.mk                         |   31 +
 unit-tests/dep-colon.exp                      |    1 +
 unit-tests/dep-colon.mk                       |    8 +
 unit-tests/dep-double-colon.exp               |    5 +
 unit-tests/dep-double-colon.mk                |   11 +
 unit-tests/dep-exclam.exp                     |    1 +
 unit-tests/dep-exclam.mk                      |    8 +
 unit-tests/dep-none.exp                       |    4 +
 unit-tests/dep-none.mk                        |    3 +
 unit-tests/dep-var.exp                        |    2 +
 unit-tests/dep-var.mk                         |   33 +
 unit-tests/dep-wildcards.exp                  |    1 +
 unit-tests/dep-wildcards.mk                   |    8 +
 unit-tests/dep.exp                            |    1 +
 unit-tests/dep.mk                             |    8 +
 unit-tests/depsrc-exec.exp                    |    1 +
 unit-tests/depsrc-exec.mk                     |    8 +
 unit-tests/depsrc-ignore.exp                  |   11 +
 unit-tests/depsrc-ignore.mk                   |   67 +
 unit-tests/depsrc-made.exp                    |    1 +
 unit-tests/depsrc-made.mk                     |    8 +
 unit-tests/depsrc-make.exp                    |    1 +
 unit-tests/depsrc-make.mk                     |    8 +
 unit-tests/depsrc-meta.exp                    |    1 +
 unit-tests/depsrc-meta.mk                     |    8 +
 unit-tests/depsrc-nometa.exp                  |    1 +
 unit-tests/depsrc-nometa.mk                   |    8 +
 unit-tests/depsrc-nometa_cmp.exp              |    1 +
 unit-tests/depsrc-nometa_cmp.mk               |    8 +
 unit-tests/depsrc-nopath.exp                  |    1 +
 unit-tests/depsrc-nopath.mk                   |    8 +
 unit-tests/depsrc-notmain.exp                 |    1 +
 unit-tests/depsrc-notmain.mk                  |    8 +
 unit-tests/depsrc-optional.exp                |    1 +
 unit-tests/depsrc-optional.mk                 |    8 +
 unit-tests/depsrc-phony.exp                   |    1 +
 unit-tests/depsrc-phony.mk                    |    8 +
 unit-tests/depsrc-precious.exp                |    1 +
 unit-tests/depsrc-precious.mk                 |    8 +
 unit-tests/depsrc-recursive.exp               |    1 +
 unit-tests/depsrc-recursive.mk                |    8 +
 unit-tests/depsrc-silent.exp                  |    4 +
 unit-tests/depsrc-silent.mk                   |   12 +
 unit-tests/depsrc-use.exp                     |    6 +
 unit-tests/depsrc-use.mk                      |   24 +
 unit-tests/depsrc-usebefore-double-colon.exp  |    2 +
 unit-tests/depsrc-usebefore-double-colon.mk   |   30 +
 unit-tests/depsrc-usebefore.exp               |    6 +
 unit-tests/depsrc-usebefore.mk                |   24 +
 unit-tests/depsrc-wait.exp                    |    1 +
 unit-tests/depsrc-wait.mk                     |    8 +
 unit-tests/depsrc.exp                         |    1 +
 unit-tests/depsrc.mk                          |    9 +
 unit-tests/deptgt-begin.exp                   |    4 +
 unit-tests/deptgt-begin.mk                    |   13 +
 unit-tests/deptgt-default.exp                 |    1 +
 unit-tests/deptgt-default.mk                  |    8 +
 unit-tests/deptgt-delete_on_error.exp         |    1 +
 unit-tests/deptgt-delete_on_error.mk          |    8 +
 unit-tests/deptgt-end.exp                     |    4 +
 unit-tests/deptgt-end.mk                      |   13 +
 unit-tests/deptgt-error.exp                   |    1 +
 unit-tests/deptgt-error.mk                    |    8 +
 unit-tests/deptgt-ignore.exp                  |    1 +
 unit-tests/deptgt-ignore.mk                   |    8 +
 unit-tests/deptgt-interrupt.exp               |    1 +
 unit-tests/deptgt-interrupt.mk                |    8 +
 unit-tests/deptgt-main.exp                    |    1 +
 unit-tests/deptgt-main.mk                     |    8 +
 unit-tests/deptgt-makeflags.exp               |    1 +
 unit-tests/deptgt-makeflags.mk                |    8 +
 unit-tests/deptgt-no_parallel.exp             |    1 +
 unit-tests/deptgt-no_parallel.mk              |    8 +
 unit-tests/deptgt-nopath.exp                  |    1 +
 unit-tests/deptgt-nopath.mk                   |    8 +
 unit-tests/deptgt-notparallel.exp             |    1 +
 unit-tests/deptgt-notparallel.mk              |    8 +
 unit-tests/deptgt-objdir.exp                  |    1 +
 unit-tests/deptgt-objdir.mk                   |    8 +
 unit-tests/deptgt-order.exp                   |    1 +
 unit-tests/deptgt-order.mk                    |    8 +
 unit-tests/deptgt-path-suffix.exp             |    1 +
 unit-tests/deptgt-path-suffix.mk              |    8 +
 unit-tests/deptgt-path.exp                    |    1 +
 unit-tests/deptgt-path.mk                     |    8 +
 unit-tests/deptgt-phony.exp                   |    1 +
 unit-tests/deptgt-phony.mk                    |    8 +
 unit-tests/deptgt-precious.exp                |    1 +
 unit-tests/deptgt-precious.mk                 |    8 +
 unit-tests/deptgt-shell.exp                   |    1 +
 unit-tests/deptgt-shell.mk                    |    8 +
 unit-tests/deptgt-silent.exp                  |    1 +
 unit-tests/deptgt-silent.mk                   |    8 +
 unit-tests/deptgt-stale.exp                   |    1 +
 unit-tests/deptgt-stale.mk                    |    8 +
 unit-tests/deptgt-suffixes.exp                |    7 +
 unit-tests/deptgt-suffixes.mk                 |   18 +
 unit-tests/deptgt.exp                         |    1 +
 unit-tests/deptgt.mk                          |    9 +
 unit-tests/dir-expand-path.exp                |    4 +
 unit-tests/dir-expand-path.mk                 |   19 +
 unit-tests/dir.exp                            |   19 +
 unit-tests/dir.mk                             |   58 +
 unit-tests/directive-elif.exp                 |    1 +
 unit-tests/directive-elif.mk                  |    8 +
 unit-tests/directive-elifdef.exp              |    1 +
 unit-tests/directive-elifdef.mk               |    8 +
 unit-tests/directive-elifmake.exp             |    1 +
 unit-tests/directive-elifmake.mk              |    8 +
 unit-tests/directive-elifndef.exp             |    1 +
 unit-tests/directive-elifndef.mk              |    8 +
 unit-tests/directive-elifnmake.exp            |    1 +
 unit-tests/directive-elifnmake.mk             |    8 +
 unit-tests/directive-else.exp                 |    8 +
 unit-tests/directive-else.mk                  |   32 +
 unit-tests/directive-endif.exp                |    1 +
 unit-tests/directive-endif.mk                 |    8 +
 unit-tests/directive-error.exp                |    1 +
 unit-tests/directive-error.mk                 |    8 +
 unit-tests/directive-export-env.exp           |    1 +
 unit-tests/directive-export-env.mk            |    8 +
 unit-tests/directive-export-literal.exp       |    1 +
 unit-tests/directive-export-literal.mk        |    8 +
 unit-tests/directive-export.exp               |    1 +
 unit-tests/directive-export.mk                |    8 +
 unit-tests/directive-for-generating-endif.exp |    7 +
 unit-tests/directive-for-generating-endif.mk  |   25 +
 unit-tests/directive-for.exp                  |    1 +
 unit-tests/directive-for.mk                   |   97 +
 unit-tests/directive-if.exp                   |    1 +
 unit-tests/directive-if.mk                    |    8 +
 unit-tests/directive-ifdef.exp                |    1 +
 unit-tests/directive-ifdef.mk                 |    8 +
 unit-tests/directive-ifmake.exp               |   10 +
 unit-tests/directive-ifmake.mk                |   55 +
 unit-tests/directive-ifndef.exp               |    1 +
 unit-tests/directive-ifndef.mk                |    8 +
 unit-tests/directive-ifnmake.exp              |    1 +
 unit-tests/directive-ifnmake.mk               |    8 +
 unit-tests/directive-info.exp                 |    1 +
 unit-tests/directive-info.mk                  |    8 +
 unit-tests/directive-undef.exp                |    1 +
 unit-tests/directive-undef.mk                 |   17 +
 unit-tests/directive-unexport-env.exp         |    1 +
 unit-tests/directive-unexport-env.mk          |    8 +
 unit-tests/directive-unexport.exp             |    1 +
 unit-tests/directive-unexport.mk              |    8 +
 unit-tests/directive-warning.exp              |    1 +
 unit-tests/directive-warning.mk               |    8 +
 unit-tests/directive.exp                      |    1 +
 unit-tests/directive.mk                       |    8 +
 unit-tests/directives.exp                     |   42 +
 unit-tests/directives.mk                      |  163 +
 unit-tests/envfirst.exp                       |    1 +
 unit-tests/envfirst.mk                        |   42 +
 unit-tests/export-all.mk                      |    3 +-
 unit-tests/export-variants.exp                |    1 +
 unit-tests/export-variants.mk                 |   40 +
 unit-tests/export.exp                         |    2 +
 unit-tests/export.mk                          |   31 +-
 unit-tests/forloop.mk                         |    4 +-
 unit-tests/impsrc.exp                         |   16 +-
 unit-tests/impsrc.mk                          |   29 +-
 unit-tests/include-main.mk                    |    4 +-
 unit-tests/lint.exp                           |    4 +
 unit-tests/lint.mk                            |   17 +
 unit-tests/make-exported.exp                  |    3 +
 unit-tests/make-exported.mk                   |   16 +
 unit-tests/moderrs.exp                        |  129 +-
 unit-tests/moderrs.mk                         |  151 +-
 unit-tests/modmatch.exp                       |    3 -
 unit-tests/modmatch.mk                        |   17 +-
 unit-tests/modmisc.exp                        |   40 +-
 unit-tests/modmisc.mk                         |  105 +-
 unit-tests/modorder.exp                       |   12 -
 unit-tests/modorder.mk                        |   24 -
 unit-tests/modts.exp                          |   37 +-
 unit-tests/modts.mk                           |   37 +-
 unit-tests/opt-backwards.exp                  |    1 +
 unit-tests/opt-backwards.mk                   |    8 +
 unit-tests/opt-chdir.exp                      |    1 +
 unit-tests/opt-chdir.mk                       |    8 +
 unit-tests/opt-debug-g1.exp                   |   15 +
 unit-tests/opt-debug-g1.mk                    |   19 +
 unit-tests/opt-debug.exp                      |    1 +
 unit-tests/opt-debug.mk                       |    8 +
 unit-tests/opt-define.exp                     |    1 +
 unit-tests/opt-define.mk                      |    8 +
 unit-tests/opt-env.exp                        |    1 +
 unit-tests/opt-env.mk                         |    8 +
 unit-tests/opt-file.exp                       |    1 +
 unit-tests/opt-file.mk                        |    8 +
 unit-tests/opt-ignore.exp                     |   12 +
 unit-tests/opt-ignore.mk                      |   30 +
 unit-tests/opt-include-dir.exp                |    1 +
 unit-tests/opt-include-dir.mk                 |    8 +
 unit-tests/opt-jobs-internal.exp              |    1 +
 unit-tests/opt-jobs-internal.mk               |    8 +
 unit-tests/opt-jobs.exp                       |    1 +
 unit-tests/opt-jobs.mk                        |    8 +
 unit-tests/opt-keep-going.exp                 |    6 +
 unit-tests/opt-keep-going.mk                  |   24 +
 unit-tests/opt-m-include-dir.exp              |    2 +
 unit-tests/opt-m-include-dir.mk               |   61 +
 unit-tests/opt-no-action-at-all.exp           |    1 +
 unit-tests/opt-no-action-at-all.mk            |    8 +
 unit-tests/opt-no-action.exp                  |   13 +
 unit-tests/opt-no-action.mk                   |   33 +
 unit-tests/opt-query.exp                      |    2 +
 unit-tests/opt-query.mk                       |   24 +
 unit-tests/opt-raw.exp                        |    1 +
 unit-tests/opt-raw.mk                         |    8 +
 unit-tests/opt-silent.exp                     |    1 +
 unit-tests/opt-silent.mk                      |    8 +
 unit-tests/opt-touch.exp                      |    1 +
 unit-tests/opt-touch.mk                       |    8 +
 unit-tests/opt-tracefile.exp                  |    1 +
 unit-tests/opt-tracefile.mk                   |    8 +
 unit-tests/opt-var-expanded.exp               |    3 +
 unit-tests/opt-var-expanded.mk                |    6 +
 unit-tests/opt-var-literal.exp                |    3 +
 unit-tests/opt-var-literal.mk                 |    6 +
 unit-tests/opt-warnings-as-errors.exp         |    7 +
 unit-tests/opt-warnings-as-errors.mk          |   11 +
 unit-tests/opt-where-am-i.exp                 |    1 +
 unit-tests/opt-where-am-i.mk                  |    8 +
 unit-tests/opt-x-reduce-exported.exp          |    1 +
 unit-tests/opt-x-reduce-exported.mk           |    8 +
 unit-tests/opt.exp                            |    1 +
 unit-tests/opt.mk                             |    8 +
 unit-tests/phony-end.exp                      |    2 +-
 unit-tests/posix1.mk                          |    4 +-
 unit-tests/recursive.exp                      |    5 +
 unit-tests/recursive.mk                       |   37 +
 unit-tests/sh-dots.exp                        |   15 +
 unit-tests/sh-dots.mk                         |   37 +
 unit-tests/sh-jobs-error.exp                  |    1 +
 unit-tests/sh-jobs-error.mk                   |    9 +
 unit-tests/sh-jobs.exp                        |    1 +
 unit-tests/sh-jobs.mk                         |    9 +
 unit-tests/sh-leading-at.exp                  |    5 +
 unit-tests/sh-leading-at.mk                   |   10 +
 unit-tests/sh-leading-hyphen.exp              |    1 +
 unit-tests/sh-leading-hyphen.mk               |    9 +
 unit-tests/sh-leading-plus.exp                |    4 +
 unit-tests/sh-leading-plus.mk                 |    8 +
 unit-tests/sh-meta-chars.exp                  |    1 +
 unit-tests/sh-meta-chars.mk                   |   11 +
 unit-tests/sh-multi-line.exp                  |    1 +
 unit-tests/sh-multi-line.mk                   |    9 +
 unit-tests/sh-single-line.exp                 |    1 +
 unit-tests/sh-single-line.mk                  |   12 +
 unit-tests/sh.exp                             |    1 +
 unit-tests/sh.mk                              |    9 +
 unit-tests/sysv.exp                           |    1 -
 unit-tests/sysv.mk                            |   11 +-
 unit-tests/unexport-env.mk                    |    3 +-
 unit-tests/unexport.mk                        |   13 +-
 unit-tests/use-inference.exp                  |    4 +
 unit-tests/use-inference.mk                   |   35 +
 unit-tests/var-class-cmdline.exp              |    1 +
 unit-tests/var-class-cmdline.mk               |    8 +
 unit-tests/var-class-env.exp                  |    1 +
 unit-tests/var-class-env.mk                   |    8 +
 unit-tests/var-class-global.exp               |    1 +
 unit-tests/var-class-global.mk                |    8 +
 unit-tests/var-class-local-legacy.exp         |    1 +
 unit-tests/var-class-local-legacy.mk          |    8 +
 unit-tests/var-class-local.exp                |    1 +
 unit-tests/var-class-local.mk                 |    8 +
 unit-tests/var-class.exp                      |    1 +
 unit-tests/var-class.mk                       |    9 +
 unit-tests/var-op-append.exp                  |    1 +
 unit-tests/var-op-append.mk                   |    9 +
 unit-tests/var-op-assign.exp                  |    6 +
 unit-tests/var-op-assign.mk                   |   89 +
 unit-tests/var-op-default.exp                 |    1 +
 unit-tests/var-op-default.mk                  |    9 +
 unit-tests/var-op-expand.exp                  |    1 +
 unit-tests/var-op-expand.mk                   |    9 +
 unit-tests/var-op-shell.exp                   |    1 +
 unit-tests/var-op-shell.mk                    |    9 +
 unit-tests/var-op.exp                         |    1 +
 unit-tests/var-op.mk                          |    8 +
 unit-tests/vardebug.exp                       |   80 +
 unit-tests/vardebug.mk                        |   59 +
 unit-tests/varfind.exp                        |   15 +
 unit-tests/varfind.mk                         |   31 +
 unit-tests/varmisc.exp                        |   48 +
 unit-tests/varmisc.mk                         |  147 +-
 unit-tests/varmod-assign.exp                  |   26 +
 unit-tests/varmod-assign.mk                   |   81 +
 unit-tests/varmod-defined.exp                 |    1 +
 unit-tests/varmod-defined.mk                  |   28 +
 unit-tests/varmod-edge.exp                    |   35 +-
 unit-tests/varmod-edge.mk                     |   27 +-
 unit-tests/varmod-exclam-shell.exp            |    1 +
 unit-tests/varmod-exclam-shell.mk             |   28 +
 unit-tests/varmod-extension.exp               |   10 +
 unit-tests/varmod-extension.mk                |    9 +
 unit-tests/varmod-gmtime.exp                  |    9 +
 unit-tests/varmod-gmtime.mk                   |   35 +
 unit-tests/varmod-hash.exp                    |    9 +
 unit-tests/varmod-hash.mk                     |   10 +
 unit-tests/varmod-head.exp                    |   10 +
 unit-tests/varmod-head.mk                     |    9 +
 unit-tests/varmod-ifelse.exp                  |    1 +
 unit-tests/varmod-ifelse.mk                   |    9 +
 unit-tests/varmod-l-name-to-value.exp         |    1 +
 unit-tests/varmod-l-name-to-value.mk          |   31 +
 unit-tests/varmod-localtime.exp               |    4 +
 unit-tests/varmod-localtime.mk                |    9 +
 unit-tests/varmod-loop.exp                    |   16 +
 unit-tests/varmod-loop.mk                     |   63 +
 unit-tests/varmod-match-escape.exp            |    3 +
 unit-tests/varmod-match-escape.mk             |   20 +
 unit-tests/varmod-match.exp                   |    5 +
 unit-tests/varmod-match.mk                    |   22 +
 unit-tests/varmod-no-match.exp                |    1 +
 unit-tests/varmod-no-match.mk                 |    9 +
 unit-tests/varmod-order-reverse.exp           |    1 +
 unit-tests/varmod-order-reverse.mk            |   13 +
 unit-tests/varmod-order-shuffle.exp           |    1 +
 unit-tests/varmod-order-shuffle.mk            |   39 +
 unit-tests/varmod-order.exp                   |    7 +
 unit-tests/varmod-order.mk                    |   19 +
 unit-tests/varmod-path.exp                    |    4 +
 unit-tests/varmod-path.mk                     |   35 +
 unit-tests/varmod-quote-dollar.exp            |    1 +
 unit-tests/varmod-quote-dollar.mk             |   10 +
 unit-tests/varmod-quote.exp                   |    1 +
 unit-tests/varmod-quote.mk                    |    9 +
 unit-tests/varmod-range.exp                   |    8 +
 unit-tests/varmod-range.mk                    |   10 +
 unit-tests/varmod-remember.exp                |    3 +
 unit-tests/varmod-remember.mk                 |   12 +
 unit-tests/varmod-root.exp                    |   10 +
 unit-tests/varmod-root.mk                     |    9 +
 unit-tests/varmod-select-words.exp            |    1 +
 unit-tests/varmod-select-words.mk             |    9 +
 unit-tests/varmod-shell.exp                   |    1 +
 unit-tests/varmod-shell.mk                    |    9 +
 unit-tests/varmod-subst-regex.exp             |   23 +
 unit-tests/varmod-subst-regex.mk              |   87 +
 unit-tests/varmod-subst.exp                   |   51 +
 unit-tests/varmod-subst.mk                    |  153 +
 unit-tests/varmod-sysv.exp                    |    8 +
 unit-tests/varmod-sysv.mk                     |   61 +
 unit-tests/varmod-tail.exp                    |   10 +
 unit-tests/varmod-tail.mk                     |    9 +
 unit-tests/varmod-to-abs.exp                  |    1 +
 unit-tests/varmod-to-abs.mk                   |    9 +
 unit-tests/varmod-to-lower.exp                |    1 +
 unit-tests/varmod-to-lower.mk                 |   19 +
 unit-tests/varmod-to-many-words.exp           |    1 +
 unit-tests/varmod-to-many-words.mk            |    9 +
 unit-tests/varmod-to-one-word.exp             |    1 +
 unit-tests/varmod-to-one-word.mk              |    9 +
 unit-tests/varmod-to-separator.exp            |    9 +
 unit-tests/varmod-to-separator.mk             |  118 +
 unit-tests/varmod-to-upper.exp                |    2 +
 unit-tests/varmod-to-upper.mk                 |   21 +
 unit-tests/varmod-undefined.exp               |    1 +
 unit-tests/varmod-undefined.mk                |   55 +
 unit-tests/varmod-unique.exp                  |    1 +
 unit-tests/varmod-unique.mk                   |   39 +
 unit-tests/varmod.exp                         |    1 +
 unit-tests/varmod.mk                          |    8 +
 unit-tests/varname-dollar.exp                 |    5 +
 unit-tests/varname-dollar.mk                  |   29 +
 unit-tests/varname-dot-alltargets.exp         |    4 +
 unit-tests/varname-dot-alltargets.mk          |   25 +
 unit-tests/varname-dot-curdir.exp             |    1 +
 unit-tests/varname-dot-curdir.mk              |    8 +
 unit-tests/varname-dot-includedfromdir.exp    |    1 +
 unit-tests/varname-dot-includedfromdir.mk     |    8 +
 unit-tests/varname-dot-includedfromfile.exp   |    1 +
 unit-tests/varname-dot-includedfromfile.mk    |    8 +
 unit-tests/varname-dot-includes.exp           |    2 +
 unit-tests/varname-dot-includes.mk            |   20 +
 unit-tests/varname-dot-libs.exp               |    2 +
 unit-tests/varname-dot-libs.mk                |   20 +
 unit-tests/varname-dot-make-dependfile.exp    |    1 +
 unit-tests/varname-dot-make-dependfile.mk     |    8 +
 .../varname-dot-make-expand_variables.exp     |    1 +
 .../varname-dot-make-expand_variables.mk      |    8 +
 unit-tests/varname-dot-make-exported.exp      |    1 +
 unit-tests/varname-dot-make-exported.mk       |    8 +
 unit-tests/varname-dot-make-jobs-prefix.exp   |    1 +
 unit-tests/varname-dot-make-jobs-prefix.mk    |    8 +
 unit-tests/varname-dot-make-jobs.exp          |    1 +
 unit-tests/varname-dot-make-jobs.mk           |    8 +
 unit-tests/varname-dot-make-level.exp         |    1 +
 unit-tests/varname-dot-make-level.mk          |    8 +
 .../varname-dot-make-makefile_preference.exp  |    1 +
 .../varname-dot-make-makefile_preference.mk   |    8 +
 unit-tests/varname-dot-make-makefiles.exp     |    1 +
 unit-tests/varname-dot-make-makefiles.mk      |    8 +
 .../varname-dot-make-meta-bailiwick.exp       |    1 +
 unit-tests/varname-dot-make-meta-bailiwick.mk |    8 +
 unit-tests/varname-dot-make-meta-created.exp  |    1 +
 unit-tests/varname-dot-make-meta-created.mk   |    8 +
 unit-tests/varname-dot-make-meta-files.exp    |    1 +
 unit-tests/varname-dot-make-meta-files.mk     |    8 +
 .../varname-dot-make-meta-ignore_filter.exp   |    1 +
 .../varname-dot-make-meta-ignore_filter.mk    |    8 +
 .../varname-dot-make-meta-ignore_paths.exp    |    1 +
 .../varname-dot-make-meta-ignore_paths.mk     |    8 +
 .../varname-dot-make-meta-ignore_patterns.exp |    1 +
 .../varname-dot-make-meta-ignore_patterns.mk  |    8 +
 unit-tests/varname-dot-make-meta-prefix.exp   |    1 +
 unit-tests/varname-dot-make-meta-prefix.mk    |    8 +
 unit-tests/varname-dot-make-mode.exp          |    1 +
 unit-tests/varname-dot-make-mode.mk           |    8 +
 unit-tests/varname-dot-make-path_filemon.exp  |    1 +
 unit-tests/varname-dot-make-path_filemon.mk   |    8 +
 unit-tests/varname-dot-make-pid.exp           |    1 +
 unit-tests/varname-dot-make-pid.mk            |    8 +
 unit-tests/varname-dot-make-ppid.exp          |    1 +
 unit-tests/varname-dot-make-ppid.mk           |    8 +
 unit-tests/varname-dot-make-save_dollars.exp  |    1 +
 unit-tests/varname-dot-make-save_dollars.mk   |    8 +
 unit-tests/varname-dot-makeoverrides.exp      |    1 +
 unit-tests/varname-dot-makeoverrides.mk       |    8 +
 unit-tests/varname-dot-newline.exp            |    4 +
 unit-tests/varname-dot-newline.mk             |   23 +
 unit-tests/varname-dot-objdir.exp             |    1 +
 unit-tests/varname-dot-objdir.mk              |    8 +
 unit-tests/varname-dot-parsedir.exp           |    1 +
 unit-tests/varname-dot-parsedir.mk            |    8 +
 unit-tests/varname-dot-parsefile.exp          |    1 +
 unit-tests/varname-dot-parsefile.mk           |    8 +
 unit-tests/varname-dot-path.exp               |    1 +
 unit-tests/varname-dot-path.mk                |    8 +
 unit-tests/varname-dot-shell.exp              |   19 +
 unit-tests/varname-dot-shell.mk               |   25 +
 unit-tests/varname-dot-targets.exp            |    1 +
 unit-tests/varname-dot-targets.mk             |    8 +
 unit-tests/varname-empty.exp                  |   11 +
 unit-tests/varname-empty.mk                   |   26 +
 unit-tests/varname-make.exp                   |    1 +
 unit-tests/varname-make.mk                    |    8 +
 .../varname-make_print_var_on_error.exp       |    1 +
 unit-tests/varname-make_print_var_on_error.mk |    8 +
 unit-tests/varname-makeflags.exp              |    1 +
 unit-tests/varname-makeflags.mk               |    8 +
 unit-tests/varname-pwd.exp                    |    1 +
 unit-tests/varname-pwd.mk                     |    8 +
 unit-tests/varname-vpath.exp                  |    1 +
 unit-tests/varname-vpath.mk                   |    8 +
 unit-tests/varname.exp                        |    1 +
 unit-tests/varname.mk                         |    8 +
 unit-tests/varparse-dynamic.exp               |    5 +
 unit-tests/varparse-dynamic.mk                |   14 +
 util.c                                        |   74 +-
 var.c                                         | 4825 ++++++++---------
 656 files changed, 14943 insertions(+), 10949 deletions(-)
 create mode 100755 enum.c
 create mode 100755 enum.h
 create mode 100644 lst.c
 delete mode 100644 lst.lib/Makefile
 delete mode 100644 lst.lib/lstAppend.c
 delete mode 100644 lst.lib/lstAtEnd.c
 delete mode 100644 lst.lib/lstAtFront.c
 delete mode 100644 lst.lib/lstClose.c
 delete mode 100644 lst.lib/lstConcat.c
 delete mode 100644 lst.lib/lstDatum.c
 delete mode 100644 lst.lib/lstDeQueue.c
 delete mode 100644 lst.lib/lstDestroy.c
 delete mode 100644 lst.lib/lstDupl.c
 delete mode 100644 lst.lib/lstEnQueue.c
 delete mode 100644 lst.lib/lstFind.c
 delete mode 100644 lst.lib/lstFindFrom.c
 delete mode 100644 lst.lib/lstFirst.c
 delete mode 100644 lst.lib/lstForEach.c
 delete mode 100644 lst.lib/lstForEachFrom.c
 delete mode 100644 lst.lib/lstInit.c
 delete mode 100644 lst.lib/lstInsert.c
 delete mode 100644 lst.lib/lstInt.h
 delete mode 100644 lst.lib/lstIsAtEnd.c
 delete mode 100644 lst.lib/lstIsEmpty.c
 delete mode 100644 lst.lib/lstLast.c
 delete mode 100644 lst.lib/lstMember.c
 delete mode 100644 lst.lib/lstNext.c
 delete mode 100644 lst.lib/lstOpen.c
 delete mode 100644 lst.lib/lstPrev.c
 delete mode 100644 lst.lib/lstRemove.c
 delete mode 100644 lst.lib/lstReplace.c
 delete mode 100644 lst.lib/lstSucc.c
 create mode 100644 mk/dirdeps-cache-update.mk
 delete mode 100644 sprite.h
 create mode 100755 unit-tests/archive-suffix.exp
 create mode 100755 unit-tests/archive-suffix.mk
 create mode 100644 unit-tests/archive.exp
 create mode 100644 unit-tests/archive.mk
 create mode 100755 unit-tests/cmd-interrupt.exp
 create mode 100755 unit-tests/cmd-interrupt.mk
 create mode 100644 unit-tests/cmdline.exp
 create mode 100644 unit-tests/cmdline.mk
 create mode 100644 unit-tests/cond-cmp-numeric-eq.exp
 create mode 100755 unit-tests/cond-cmp-numeric-eq.mk
 create mode 100644 unit-tests/cond-cmp-numeric-ge.exp
 create mode 100755 unit-tests/cond-cmp-numeric-ge.mk
 create mode 100644 unit-tests/cond-cmp-numeric-gt.exp
 create mode 100755 unit-tests/cond-cmp-numeric-gt.mk
 create mode 100644 unit-tests/cond-cmp-numeric-le.exp
 create mode 100755 unit-tests/cond-cmp-numeric-le.mk
 create mode 100644 unit-tests/cond-cmp-numeric-lt.exp
 create mode 100755 unit-tests/cond-cmp-numeric-lt.mk
 create mode 100644 unit-tests/cond-cmp-numeric-ne.exp
 create mode 100755 unit-tests/cond-cmp-numeric-ne.mk
 create mode 100644 unit-tests/cond-cmp-numeric.exp
 create mode 100644 unit-tests/cond-cmp-numeric.mk
 create mode 100644 unit-tests/cond-cmp-string.exp
 create mode 100644 unit-tests/cond-cmp-string.mk
 create mode 100644 unit-tests/cond-func-commands.exp
 create mode 100644 unit-tests/cond-func-commands.mk
 create mode 100644 unit-tests/cond-func-defined.exp
 create mode 100644 unit-tests/cond-func-defined.mk
 create mode 100644 unit-tests/cond-func-empty.exp
 create mode 100644 unit-tests/cond-func-empty.mk
 create mode 100644 unit-tests/cond-func-exists.exp
 create mode 100644 unit-tests/cond-func-exists.mk
 create mode 100644 unit-tests/cond-func-make.exp
 create mode 100644 unit-tests/cond-func-make.mk
 create mode 100644 unit-tests/cond-func-target.exp
 create mode 100644 unit-tests/cond-func-target.mk
 create mode 100644 unit-tests/cond-func.exp
 create mode 100644 unit-tests/cond-func.mk
 create mode 100644 unit-tests/cond-op-and.exp
 create mode 100644 unit-tests/cond-op-and.mk
 create mode 100644 unit-tests/cond-op-not.exp
 create mode 100644 unit-tests/cond-op-not.mk
 create mode 100644 unit-tests/cond-op-or.exp
 create mode 100644 unit-tests/cond-op-or.mk
 create mode 100644 unit-tests/cond-op-parentheses.exp
 create mode 100644 unit-tests/cond-op-parentheses.mk
 create mode 100644 unit-tests/cond-op.exp
 create mode 100644 unit-tests/cond-op.mk
 create mode 100644 unit-tests/cond-token-number.exp
 create mode 100644 unit-tests/cond-token-number.mk
 create mode 100644 unit-tests/cond-token-plain.exp
 create mode 100644 unit-tests/cond-token-plain.mk
 create mode 100644 unit-tests/cond-token-string.exp
 create mode 100644 unit-tests/cond-token-string.mk
 create mode 100644 unit-tests/cond-token-var.exp
 create mode 100644 unit-tests/cond-token-var.mk
 create mode 100644 unit-tests/counter.exp
 create mode 100644 unit-tests/counter.mk
 create mode 100644 unit-tests/dep-colon.exp
 create mode 100644 unit-tests/dep-colon.mk
 create mode 100644 unit-tests/dep-double-colon.exp
 create mode 100644 unit-tests/dep-double-colon.mk
 create mode 100644 unit-tests/dep-exclam.exp
 create mode 100644 unit-tests/dep-exclam.mk
 create mode 100755 unit-tests/dep-none.exp
 create mode 100755 unit-tests/dep-none.mk
 create mode 100755 unit-tests/dep-var.exp
 create mode 100755 unit-tests/dep-var.mk
 create mode 100644 unit-tests/dep-wildcards.exp
 create mode 100644 unit-tests/dep-wildcards.mk
 create mode 100644 unit-tests/dep.exp
 create mode 100644 unit-tests/dep.mk
 create mode 100644 unit-tests/depsrc-exec.exp
 create mode 100644 unit-tests/depsrc-exec.mk
 create mode 100644 unit-tests/depsrc-ignore.exp
 create mode 100644 unit-tests/depsrc-ignore.mk
 create mode 100644 unit-tests/depsrc-made.exp
 create mode 100644 unit-tests/depsrc-made.mk
 create mode 100644 unit-tests/depsrc-make.exp
 create mode 100644 unit-tests/depsrc-make.mk
 create mode 100644 unit-tests/depsrc-meta.exp
 create mode 100644 unit-tests/depsrc-meta.mk
 create mode 100644 unit-tests/depsrc-nometa.exp
 create mode 100644 unit-tests/depsrc-nometa.mk
 create mode 100644 unit-tests/depsrc-nometa_cmp.exp
 create mode 100644 unit-tests/depsrc-nometa_cmp.mk
 create mode 100644 unit-tests/depsrc-nopath.exp
 create mode 100644 unit-tests/depsrc-nopath.mk
 create mode 100644 unit-tests/depsrc-notmain.exp
 create mode 100644 unit-tests/depsrc-notmain.mk
 create mode 100644 unit-tests/depsrc-optional.exp
 create mode 100644 unit-tests/depsrc-optional.mk
 create mode 100644 unit-tests/depsrc-phony.exp
 create mode 100644 unit-tests/depsrc-phony.mk
 create mode 100644 unit-tests/depsrc-precious.exp
 create mode 100644 unit-tests/depsrc-precious.mk
 create mode 100644 unit-tests/depsrc-recursive.exp
 create mode 100644 unit-tests/depsrc-recursive.mk
 create mode 100644 unit-tests/depsrc-silent.exp
 create mode 100644 unit-tests/depsrc-silent.mk
 create mode 100644 unit-tests/depsrc-use.exp
 create mode 100644 unit-tests/depsrc-use.mk
 create mode 100755 unit-tests/depsrc-usebefore-double-colon.exp
 create mode 100755 unit-tests/depsrc-usebefore-double-colon.mk
 create mode 100644 unit-tests/depsrc-usebefore.exp
 create mode 100644 unit-tests/depsrc-usebefore.mk
 create mode 100644 unit-tests/depsrc-wait.exp
 create mode 100644 unit-tests/depsrc-wait.mk
 create mode 100644 unit-tests/depsrc.exp
 create mode 100644 unit-tests/depsrc.mk
 create mode 100644 unit-tests/deptgt-begin.exp
 create mode 100644 unit-tests/deptgt-begin.mk
 create mode 100644 unit-tests/deptgt-default.exp
 create mode 100644 unit-tests/deptgt-default.mk
 create mode 100644 unit-tests/deptgt-delete_on_error.exp
 create mode 100644 unit-tests/deptgt-delete_on_error.mk
 create mode 100644 unit-tests/deptgt-end.exp
 create mode 100644 unit-tests/deptgt-end.mk
 create mode 100644 unit-tests/deptgt-error.exp
 create mode 100644 unit-tests/deptgt-error.mk
 create mode 100644 unit-tests/deptgt-ignore.exp
 create mode 100644 unit-tests/deptgt-ignore.mk
 create mode 100644 unit-tests/deptgt-interrupt.exp
 create mode 100644 unit-tests/deptgt-interrupt.mk
 create mode 100644 unit-tests/deptgt-main.exp
 create mode 100644 unit-tests/deptgt-main.mk
 create mode 100644 unit-tests/deptgt-makeflags.exp
 create mode 100644 unit-tests/deptgt-makeflags.mk
 create mode 100644 unit-tests/deptgt-no_parallel.exp
 create mode 100644 unit-tests/deptgt-no_parallel.mk
 create mode 100644 unit-tests/deptgt-nopath.exp
 create mode 100644 unit-tests/deptgt-nopath.mk
 create mode 100644 unit-tests/deptgt-notparallel.exp
 create mode 100644 unit-tests/deptgt-notparallel.mk
 create mode 100644 unit-tests/deptgt-objdir.exp
 create mode 100644 unit-tests/deptgt-objdir.mk
 create mode 100644 unit-tests/deptgt-order.exp
 create mode 100644 unit-tests/deptgt-order.mk
 create mode 100644 unit-tests/deptgt-path-suffix.exp
 create mode 100644 unit-tests/deptgt-path-suffix.mk
 create mode 100644 unit-tests/deptgt-path.exp
 create mode 100644 unit-tests/deptgt-path.mk
 create mode 100644 unit-tests/deptgt-phony.exp
 create mode 100644 unit-tests/deptgt-phony.mk
 create mode 100644 unit-tests/deptgt-precious.exp
 create mode 100644 unit-tests/deptgt-precious.mk
 create mode 100644 unit-tests/deptgt-shell.exp
 create mode 100644 unit-tests/deptgt-shell.mk
 create mode 100644 unit-tests/deptgt-silent.exp
 create mode 100644 unit-tests/deptgt-silent.mk
 create mode 100644 unit-tests/deptgt-stale.exp
 create mode 100644 unit-tests/deptgt-stale.mk
 create mode 100644 unit-tests/deptgt-suffixes.exp
 create mode 100644 unit-tests/deptgt-suffixes.mk
 create mode 100644 unit-tests/deptgt.exp
 create mode 100644 unit-tests/deptgt.mk
 create mode 100644 unit-tests/dir-expand-path.exp
 create mode 100755 unit-tests/dir-expand-path.mk
 create mode 100644 unit-tests/dir.exp
 create mode 100644 unit-tests/dir.mk
 create mode 100644 unit-tests/directive-elif.exp
 create mode 100644 unit-tests/directive-elif.mk
 create mode 100644 unit-tests/directive-elifdef.exp
 create mode 100644 unit-tests/directive-elifdef.mk
 create mode 100644 unit-tests/directive-elifmake.exp
 create mode 100644 unit-tests/directive-elifmake.mk
 create mode 100644 unit-tests/directive-elifndef.exp
 create mode 100644 unit-tests/directive-elifndef.mk
 create mode 100644 unit-tests/directive-elifnmake.exp
 create mode 100644 unit-tests/directive-elifnmake.mk
 create mode 100644 unit-tests/directive-else.exp
 create mode 100644 unit-tests/directive-else.mk
 create mode 100644 unit-tests/directive-endif.exp
 create mode 100644 unit-tests/directive-endif.mk
 create mode 100644 unit-tests/directive-error.exp
 create mode 100644 unit-tests/directive-error.mk
 create mode 100644 unit-tests/directive-export-env.exp
 create mode 100644 unit-tests/directive-export-env.mk
 create mode 100644 unit-tests/directive-export-literal.exp
 create mode 100644 unit-tests/directive-export-literal.mk
 create mode 100644 unit-tests/directive-export.exp
 create mode 100644 unit-tests/directive-export.mk
 create mode 100755 unit-tests/directive-for-generating-endif.exp
 create mode 100755 unit-tests/directive-for-generating-endif.mk
 create mode 100755 unit-tests/directive-for.exp
 create mode 100755 unit-tests/directive-for.mk
 create mode 100644 unit-tests/directive-if.exp
 create mode 100644 unit-tests/directive-if.mk
 create mode 100644 unit-tests/directive-ifdef.exp
 create mode 100644 unit-tests/directive-ifdef.mk
 create mode 100644 unit-tests/directive-ifmake.exp
 create mode 100644 unit-tests/directive-ifmake.mk
 create mode 100644 unit-tests/directive-ifndef.exp
 create mode 100644 unit-tests/directive-ifndef.mk
 create mode 100644 unit-tests/directive-ifnmake.exp
 create mode 100644 unit-tests/directive-ifnmake.mk
 create mode 100644 unit-tests/directive-info.exp
 create mode 100644 unit-tests/directive-info.mk
 create mode 100644 unit-tests/directive-undef.exp
 create mode 100644 unit-tests/directive-undef.mk
 create mode 100644 unit-tests/directive-unexport-env.exp
 create mode 100644 unit-tests/directive-unexport-env.mk
 create mode 100644 unit-tests/directive-unexport.exp
 create mode 100644 unit-tests/directive-unexport.mk
 create mode 100644 unit-tests/directive-warning.exp
 create mode 100644 unit-tests/directive-warning.mk
 create mode 100644 unit-tests/directive.exp
 create mode 100644 unit-tests/directive.mk
 create mode 100644 unit-tests/directives.exp
 create mode 100644 unit-tests/directives.mk
 create mode 100644 unit-tests/envfirst.exp
 create mode 100644 unit-tests/envfirst.mk
 create mode 100755 unit-tests/export-variants.exp
 create mode 100755 unit-tests/export-variants.mk
 create mode 100755 unit-tests/lint.exp
 create mode 100755 unit-tests/lint.mk
 create mode 100755 unit-tests/make-exported.exp
 create mode 100755 unit-tests/make-exported.mk
 delete mode 100644 unit-tests/modorder.exp
 delete mode 100644 unit-tests/modorder.mk
 create mode 100644 unit-tests/opt-backwards.exp
 create mode 100644 unit-tests/opt-backwards.mk
 create mode 100644 unit-tests/opt-chdir.exp
 create mode 100644 unit-tests/opt-chdir.mk
 create mode 100755 unit-tests/opt-debug-g1.exp
 create mode 100755 unit-tests/opt-debug-g1.mk
 create mode 100644 unit-tests/opt-debug.exp
 create mode 100644 unit-tests/opt-debug.mk
 create mode 100644 unit-tests/opt-define.exp
 create mode 100644 unit-tests/opt-define.mk
 create mode 100644 unit-tests/opt-env.exp
 create mode 100644 unit-tests/opt-env.mk
 create mode 100644 unit-tests/opt-file.exp
 create mode 100644 unit-tests/opt-file.mk
 create mode 100644 unit-tests/opt-ignore.exp
 create mode 100644 unit-tests/opt-ignore.mk
 create mode 100644 unit-tests/opt-include-dir.exp
 create mode 100644 unit-tests/opt-include-dir.mk
 create mode 100644 unit-tests/opt-jobs-internal.exp
 create mode 100644 unit-tests/opt-jobs-internal.mk
 create mode 100644 unit-tests/opt-jobs.exp
 create mode 100644 unit-tests/opt-jobs.mk
 create mode 100644 unit-tests/opt-keep-going.exp
 create mode 100644 unit-tests/opt-keep-going.mk
 create mode 100644 unit-tests/opt-m-include-dir.exp
 create mode 100644 unit-tests/opt-m-include-dir.mk
 create mode 100644 unit-tests/opt-no-action-at-all.exp
 create mode 100644 unit-tests/opt-no-action-at-all.mk
 create mode 100644 unit-tests/opt-no-action.exp
 create mode 100644 unit-tests/opt-no-action.mk
 create mode 100644 unit-tests/opt-query.exp
 create mode 100644 unit-tests/opt-query.mk
 create mode 100644 unit-tests/opt-raw.exp
 create mode 100644 unit-tests/opt-raw.mk
 create mode 100644 unit-tests/opt-silent.exp
 create mode 100644 unit-tests/opt-silent.mk
 create mode 100644 unit-tests/opt-touch.exp
 create mode 100644 unit-tests/opt-touch.mk
 create mode 100644 unit-tests/opt-tracefile.exp
 create mode 100644 unit-tests/opt-tracefile.mk
 create mode 100644 unit-tests/opt-var-expanded.exp
 create mode 100644 unit-tests/opt-var-expanded.mk
 create mode 100644 unit-tests/opt-var-literal.exp
 create mode 100644 unit-tests/opt-var-literal.mk
 create mode 100644 unit-tests/opt-warnings-as-errors.exp
 create mode 100644 unit-tests/opt-warnings-as-errors.mk
 create mode 100644 unit-tests/opt-where-am-i.exp
 create mode 100644 unit-tests/opt-where-am-i.mk
 create mode 100644 unit-tests/opt-x-reduce-exported.exp
 create mode 100644 unit-tests/opt-x-reduce-exported.mk
 create mode 100644 unit-tests/opt.exp
 create mode 100644 unit-tests/opt.mk
 create mode 100644 unit-tests/recursive.exp
 create mode 100644 unit-tests/recursive.mk
 create mode 100755 unit-tests/sh-dots.exp
 create mode 100755 unit-tests/sh-dots.mk
 create mode 100644 unit-tests/sh-jobs-error.exp
 create mode 100644 unit-tests/sh-jobs-error.mk
 create mode 100644 unit-tests/sh-jobs.exp
 create mode 100644 unit-tests/sh-jobs.mk
 create mode 100644 unit-tests/sh-leading-at.exp
 create mode 100644 unit-tests/sh-leading-at.mk
 create mode 100644 unit-tests/sh-leading-hyphen.exp
 create mode 100644 unit-tests/sh-leading-hyphen.mk
 create mode 100644 unit-tests/sh-leading-plus.exp
 create mode 100644 unit-tests/sh-leading-plus.mk
 create mode 100644 unit-tests/sh-meta-chars.exp
 create mode 100644 unit-tests/sh-meta-chars.mk
 create mode 100644 unit-tests/sh-multi-line.exp
 create mode 100644 unit-tests/sh-multi-line.mk
 create mode 100644 unit-tests/sh-single-line.exp
 create mode 100644 unit-tests/sh-single-line.mk
 create mode 100644 unit-tests/sh.exp
 create mode 100644 unit-tests/sh.mk
 create mode 100644 unit-tests/use-inference.exp
 create mode 100644 unit-tests/use-inference.mk
 create mode 100644 unit-tests/var-class-cmdline.exp
 create mode 100644 unit-tests/var-class-cmdline.mk
 create mode 100644 unit-tests/var-class-env.exp
 create mode 100644 unit-tests/var-class-env.mk
 create mode 100644 unit-tests/var-class-global.exp
 create mode 100644 unit-tests/var-class-global.mk
 create mode 100644 unit-tests/var-class-local-legacy.exp
 create mode 100644 unit-tests/var-class-local-legacy.mk
 create mode 100644 unit-tests/var-class-local.exp
 create mode 100644 unit-tests/var-class-local.mk
 create mode 100644 unit-tests/var-class.exp
 create mode 100644 unit-tests/var-class.mk
 create mode 100644 unit-tests/var-op-append.exp
 create mode 100644 unit-tests/var-op-append.mk
 create mode 100644 unit-tests/var-op-assign.exp
 create mode 100644 unit-tests/var-op-assign.mk
 create mode 100644 unit-tests/var-op-default.exp
 create mode 100644 unit-tests/var-op-default.mk
 create mode 100644 unit-tests/var-op-expand.exp
 create mode 100644 unit-tests/var-op-expand.mk
 create mode 100644 unit-tests/var-op-shell.exp
 create mode 100644 unit-tests/var-op-shell.mk
 create mode 100644 unit-tests/var-op.exp
 create mode 100644 unit-tests/var-op.mk
 create mode 100644 unit-tests/vardebug.exp
 create mode 100644 unit-tests/vardebug.mk
 create mode 100644 unit-tests/varfind.exp
 create mode 100644 unit-tests/varfind.mk
 create mode 100644 unit-tests/varmod-assign.exp
 create mode 100644 unit-tests/varmod-assign.mk
 create mode 100644 unit-tests/varmod-defined.exp
 create mode 100644 unit-tests/varmod-defined.mk
 create mode 100644 unit-tests/varmod-exclam-shell.exp
 create mode 100644 unit-tests/varmod-exclam-shell.mk
 create mode 100644 unit-tests/varmod-extension.exp
 create mode 100644 unit-tests/varmod-extension.mk
 create mode 100644 unit-tests/varmod-gmtime.exp
 create mode 100644 unit-tests/varmod-gmtime.mk
 create mode 100644 unit-tests/varmod-hash.exp
 create mode 100644 unit-tests/varmod-hash.mk
 create mode 100644 unit-tests/varmod-head.exp
 create mode 100644 unit-tests/varmod-head.mk
 create mode 100644 unit-tests/varmod-ifelse.exp
 create mode 100644 unit-tests/varmod-ifelse.mk
 create mode 100644 unit-tests/varmod-l-name-to-value.exp
 create mode 100644 unit-tests/varmod-l-name-to-value.mk
 create mode 100644 unit-tests/varmod-localtime.exp
 create mode 100644 unit-tests/varmod-localtime.mk
 create mode 100644 unit-tests/varmod-loop.exp
 create mode 100644 unit-tests/varmod-loop.mk
 create mode 100755 unit-tests/varmod-match-escape.exp
 create mode 100755 unit-tests/varmod-match-escape.mk
 create mode 100644 unit-tests/varmod-match.exp
 create mode 100644 unit-tests/varmod-match.mk
 create mode 100644 unit-tests/varmod-no-match.exp
 create mode 100644 unit-tests/varmod-no-match.mk
 create mode 100644 unit-tests/varmod-order-reverse.exp
 create mode 100644 unit-tests/varmod-order-reverse.mk
 create mode 100644 unit-tests/varmod-order-shuffle.exp
 create mode 100644 unit-tests/varmod-order-shuffle.mk
 create mode 100644 unit-tests/varmod-order.exp
 create mode 100644 unit-tests/varmod-order.mk
 create mode 100644 unit-tests/varmod-path.exp
 create mode 100644 unit-tests/varmod-path.mk
 create mode 100644 unit-tests/varmod-quote-dollar.exp
 create mode 100644 unit-tests/varmod-quote-dollar.mk
 create mode 100644 unit-tests/varmod-quote.exp
 create mode 100644 unit-tests/varmod-quote.mk
 create mode 100644 unit-tests/varmod-range.exp
 create mode 100644 unit-tests/varmod-range.mk
 create mode 100644 unit-tests/varmod-remember.exp
 create mode 100644 unit-tests/varmod-remember.mk
 create mode 100644 unit-tests/varmod-root.exp
 create mode 100644 unit-tests/varmod-root.mk
 create mode 100644 unit-tests/varmod-select-words.exp
 create mode 100644 unit-tests/varmod-select-words.mk
 create mode 100644 unit-tests/varmod-shell.exp
 create mode 100644 unit-tests/varmod-shell.mk
 create mode 100644 unit-tests/varmod-subst-regex.exp
 create mode 100644 unit-tests/varmod-subst-regex.mk
 create mode 100644 unit-tests/varmod-subst.exp
 create mode 100644 unit-tests/varmod-subst.mk
 create mode 100644 unit-tests/varmod-sysv.exp
 create mode 100644 unit-tests/varmod-sysv.mk
 create mode 100644 unit-tests/varmod-tail.exp
 create mode 100644 unit-tests/varmod-tail.mk
 create mode 100644 unit-tests/varmod-to-abs.exp
 create mode 100644 unit-tests/varmod-to-abs.mk
 create mode 100644 unit-tests/varmod-to-lower.exp
 create mode 100644 unit-tests/varmod-to-lower.mk
 create mode 100644 unit-tests/varmod-to-many-words.exp
 create mode 100644 unit-tests/varmod-to-many-words.mk
 create mode 100644 unit-tests/varmod-to-one-word.exp
 create mode 100644 unit-tests/varmod-to-one-word.mk
 create mode 100644 unit-tests/varmod-to-separator.exp
 create mode 100644 unit-tests/varmod-to-separator.mk
 create mode 100644 unit-tests/varmod-to-upper.exp
 create mode 100644 unit-tests/varmod-to-upper.mk
 create mode 100644 unit-tests/varmod-undefined.exp
 create mode 100644 unit-tests/varmod-undefined.mk
 create mode 100644 unit-tests/varmod-unique.exp
 create mode 100644 unit-tests/varmod-unique.mk
 create mode 100644 unit-tests/varmod.exp
 create mode 100644 unit-tests/varmod.mk
 create mode 100644 unit-tests/varname-dollar.exp
 create mode 100644 unit-tests/varname-dollar.mk
 create mode 100644 unit-tests/varname-dot-alltargets.exp
 create mode 100644 unit-tests/varname-dot-alltargets.mk
 create mode 100644 unit-tests/varname-dot-curdir.exp
 create mode 100644 unit-tests/varname-dot-curdir.mk
 create mode 100644 unit-tests/varname-dot-includedfromdir.exp
 create mode 100644 unit-tests/varname-dot-includedfromdir.mk
 create mode 100644 unit-tests/varname-dot-includedfromfile.exp
 create mode 100644 unit-tests/varname-dot-includedfromfile.mk
 create mode 100755 unit-tests/varname-dot-includes.exp
 create mode 100755 unit-tests/varname-dot-includes.mk
 create mode 100755 unit-tests/varname-dot-libs.exp
 create mode 100755 unit-tests/varname-dot-libs.mk
 create mode 100644 unit-tests/varname-dot-make-dependfile.exp
 create mode 100644 unit-tests/varname-dot-make-dependfile.mk
 create mode 100644 unit-tests/varname-dot-make-expand_variables.exp
 create mode 100644 unit-tests/varname-dot-make-expand_variables.mk
 create mode 100644 unit-tests/varname-dot-make-exported.exp
 create mode 100644 unit-tests/varname-dot-make-exported.mk
 create mode 100644 unit-tests/varname-dot-make-jobs-prefix.exp
 create mode 100644 unit-tests/varname-dot-make-jobs-prefix.mk
 create mode 100644 unit-tests/varname-dot-make-jobs.exp
 create mode 100644 unit-tests/varname-dot-make-jobs.mk
 create mode 100644 unit-tests/varname-dot-make-level.exp
 create mode 100644 unit-tests/varname-dot-make-level.mk
 create mode 100644 unit-tests/varname-dot-make-makefile_preference.exp
 create mode 100644 unit-tests/varname-dot-make-makefile_preference.mk
 create mode 100644 unit-tests/varname-dot-make-makefiles.exp
 create mode 100644 unit-tests/varname-dot-make-makefiles.mk
 create mode 100644 unit-tests/varname-dot-make-meta-bailiwick.exp
 create mode 100644 unit-tests/varname-dot-make-meta-bailiwick.mk
 create mode 100644 unit-tests/varname-dot-make-meta-created.exp
 create mode 100644 unit-tests/varname-dot-make-meta-created.mk
 create mode 100644 unit-tests/varname-dot-make-meta-files.exp
 create mode 100644 unit-tests/varname-dot-make-meta-files.mk
 create mode 100644 unit-tests/varname-dot-make-meta-ignore_filter.exp
 create mode 100644 unit-tests/varname-dot-make-meta-ignore_filter.mk
 create mode 100644 unit-tests/varname-dot-make-meta-ignore_paths.exp
 create mode 100644 unit-tests/varname-dot-make-meta-ignore_paths.mk
 create mode 100644 unit-tests/varname-dot-make-meta-ignore_patterns.exp
 create mode 100644 unit-tests/varname-dot-make-meta-ignore_patterns.mk
 create mode 100644 unit-tests/varname-dot-make-meta-prefix.exp
 create mode 100644 unit-tests/varname-dot-make-meta-prefix.mk
 create mode 100644 unit-tests/varname-dot-make-mode.exp
 create mode 100644 unit-tests/varname-dot-make-mode.mk
 create mode 100644 unit-tests/varname-dot-make-path_filemon.exp
 create mode 100644 unit-tests/varname-dot-make-path_filemon.mk
 create mode 100644 unit-tests/varname-dot-make-pid.exp
 create mode 100644 unit-tests/varname-dot-make-pid.mk
 create mode 100644 unit-tests/varname-dot-make-ppid.exp
 create mode 100644 unit-tests/varname-dot-make-ppid.mk
 create mode 100644 unit-tests/varname-dot-make-save_dollars.exp
 create mode 100644 unit-tests/varname-dot-make-save_dollars.mk
 create mode 100644 unit-tests/varname-dot-makeoverrides.exp
 create mode 100644 unit-tests/varname-dot-makeoverrides.mk
 create mode 100644 unit-tests/varname-dot-newline.exp
 create mode 100644 unit-tests/varname-dot-newline.mk
 create mode 100644 unit-tests/varname-dot-objdir.exp
 create mode 100644 unit-tests/varname-dot-objdir.mk
 create mode 100644 unit-tests/varname-dot-parsedir.exp
 create mode 100644 unit-tests/varname-dot-parsedir.mk
 create mode 100644 unit-tests/varname-dot-parsefile.exp
 create mode 100644 unit-tests/varname-dot-parsefile.mk
 create mode 100644 unit-tests/varname-dot-path.exp
 create mode 100644 unit-tests/varname-dot-path.mk
 create mode 100755 unit-tests/varname-dot-shell.exp
 create mode 100755 unit-tests/varname-dot-shell.mk
 create mode 100644 unit-tests/varname-dot-targets.exp
 create mode 100644 unit-tests/varname-dot-targets.mk
 create mode 100644 unit-tests/varname-empty.exp
 create mode 100755 unit-tests/varname-empty.mk
 create mode 100644 unit-tests/varname-make.exp
 create mode 100644 unit-tests/varname-make.mk
 create mode 100644 unit-tests/varname-make_print_var_on_error.exp
 create mode 100644 unit-tests/varname-make_print_var_on_error.mk
 create mode 100644 unit-tests/varname-makeflags.exp
 create mode 100644 unit-tests/varname-makeflags.mk
 create mode 100644 unit-tests/varname-pwd.exp
 create mode 100644 unit-tests/varname-pwd.mk
 create mode 100644 unit-tests/varname-vpath.exp
 create mode 100644 unit-tests/varname-vpath.mk
 create mode 100644 unit-tests/varname.exp
 create mode 100644 unit-tests/varname.mk
 create mode 100644 unit-tests/varparse-dynamic.exp
 create mode 100644 unit-tests/varparse-dynamic.mk

diff --git a/ChangeLog b/ChangeLog
index 20e2628b3bed..847c2e4c0f90 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,243 @@
+2020-09-02  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200902
+	Merge with NetBSD make, pick up
+	o use make_stat to ensure no confusion over valid fields
+	returned by cached_stat
+	o var.c: make VarQuote const-correct
+	o add unit tests for .for
+
+2020-09-01  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200901
+	Merge with NetBSD make, pick up
+	o rename Hash_Table fields
+	o make data types in Dir_HasWildcards more precise
+
+2020-08-31  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200831
+	Merge with NetBSD make, pick up
+	o suff.c: fix unbalanced Lst_Open/Lst_Close in SuffFindCmds
+	o lst.c: Lst_Open renable assert that list isn't open
+	o unit test for .TARGET dependent flags
+	o var.c: fix aliasing bug in VarUniq
+	o more unit tests for :u
+
+2020-08-30  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200830
+	Merge with NetBSD make, pick up
+	o allow for strict type checking for Boolean
+	o Var_Parse never returns NULL
+	o Var_Subst never returns NULL
+	o Lst_Find now takes boolean match function
+	o rename Lst_Memeber to Lst_FindDatum
+	o rename LstNode functions to match their type
+	o rename GNode.iParents to implicitParents
+	o fix assertion failure for .SUFFIXES in archives
+	o compat.c: clean up documentation for CompatInterrupt and Compat_Run
+	remove unreachable code from CompatRunCommand
+	o main.c: simplify getBoolean
+	o stc.c: replace brk_string with simpler Str_Words
+	o suff.c: add debug macros
+
+2020-08-28  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200828
+	Merge with NetBSD make, pick up
+	o lst.c: inline LstIsValid and LstNodeIsValid
+	o remove trailing S from Lst function names after migration complete
+	o more comment cleanup/clarification
+	o suff.c: clean up suffix handling
+	o more unit tests
+
+2020-08-26  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200826
+	Merge with NetBSD make, pick up
+	o enum.c: distinguish between bitsets containing flags and
+	ordinary enums
+	o var.c: fix error message for ::!= modifier with shell error
+	o fix bugs in -DCLEANUP mode
+
+2020-08-24  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200824
+	Merge with NetBSD make, pick up
+	o in debug mode, print GNode details in symbols
+
+2020-08-23  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200823
+	Merge with NetBSD make, pick up
+	o lst.c: more asserts,
+	make args to Lst_Find match others.
+	o var.c: pass flags to VarAdd
+	o arch.c: use Buffer
+	o str.c: brk_string return size_t for nwords
+	o more unit tests
+
+2020-08-22  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION):
+	Merge with NetBSD make, pick up
+	o var.c: support for read-only variables eg .SHELL
+	being the shell used to run scripts.
+	o lst.c: more simplification
+	o more documentation and style cleanup
+	o more unit tests
+	o ensure unit-test/Makefile is run by TEST_MAKE
+	o reduce duplication of header inclusion
+
+2020-08-21  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200821
+	Merge with NetBSD make, pick up
+	o lst.c: revert invalid assertion - but document it
+	o dir.c: split Dir_Init into two functions
+
+2020-08-20  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* lst.c: needs inttypes.h on Linux
+
+	* VERSION (_MAKE_VERSION): 20200820
+	Merge with NetBSD make, pick up
+	o make.1: clarify some passages
+	o var.c: more cleanup, clarify comments
+	o make_malloc.c: remove unreachable code
+	o cond.c: make CondGetString easier to debug
+	o simplify list usage
+	o unit-tests: more
+
+2020-08-16  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200816
+	Merge with NetBSD make, pick up
+	o refactor unit-tests to be more fine grained
+	  not all tests moved yet
+
+2020-08-14  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200814
+	Merge with NetBSD make, pick up
+	o more str_concat variants
+	o more enums for flags
+	o var.c: cleanup for higher warnings level
+
+2020-08-10  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200810
+	Merge with NetBSD make, pick up
+	o more unit tests
+	o general comment and style cleanup
+
+2020-08-08  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200808
+	Merge with NetBSD make, pick up
+	o enum.[ch]: streamline, enums for use in flags and debug output
+	o cond.c: cleanup
+	o var.c: reduce duplicate code for modifiers
+	debug logging for Var_Parse
+	more detailed debug output
+	o more unit tests
+
+2020-08-06  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* unit-tests/Makefile: -r for recursive and include Makefile.inc
+	so I can run tests in meta mode
+	supress extra noise if in meta mode
+
+	* VERSION (_MAKE_VERSION): 20200806
+	Merge with NetBSD make, pick up
+	o parse.c: remove VARE_WANTRES for LINT
+	we just want to check parsing (for now).
+
+2020-08-05  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200805
+	Merge with NetBSD make, pick up
+	o make.1: Rework the description of dependence operators
+
+2020-08-03  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200803
+	Merge with NetBSD make, pick up
+	o revert some C99 usage, for max portability
+	o unit-tests/lint
+
+2020-08-02  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200802
+	Merge with NetBSD make, pick up
+	o more unit tests
+
+2020-08-01  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* Remove NetBSD specific plumbing from unit-tests/Makefile
+
+	* VERSION (_MAKE_VERSION): 20200801
+	Merge with NetBSD make, pick up
+	o make Var_Value return const
+	o size_t for buf sizes
+	o optimize some buffer operations - avoid strlen
+
+2020-07-31  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200731
+	Merge with NetBSD make, pick up
+	o var.c: fix undefinded behavior for incomplete :t modifier
+	  fixes unit-test/moderrs on Ubuntu
+	o parse.c: When parsing variable assignments other than :=
+	  if DEBUG(LINT) test substition of value, so we get a file and
+	  line number in the resulting error.
+	o dir.c: fix parsing of nested braces in dependency lines
+	  add unit-tests
+
+2020-07-30  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200730
+	Merge with NetBSD make, pick up
+	o var.c: minor cleanup
+	o unit-tests: more tests to improve code coverage
+
+2020-07-28  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200728
+	Merge with NetBSD make, pick up
+	o var.c: more optimizations
+
+2020-07-26  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200726
+	Merge with NetBSD make, pick up
+	o collapse lsd.lib into lst.c - reduce code size and allow inlining
+	o lots of function comment updates
+	o var.c: more optimizations
+	o make return of Var_Parse const
+
+2020-07-20  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200720
+	Merge with NetBSD make, pick up
+	o DEBUG_HASH report stats at end and tone down the noise
+	o var.c: each flag type gets its own prefix.
+	move SysV string matching to var.c
+	make ampersand in ${VAR:from=to&} an ordinary character
+	cleanup and simplify implementation of modifiers
+	o make.1: move documentation for assignment modifiers
+
+2020-07-18  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* VERSION (_MAKE_VERSION): 20200718
+	Merge with NetBSD make, pick up
+	o DEBUG_HASH to see how well the hash tables are working
+
+2020-07-11  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* bsd.after-import.mk: make sure we update unit-tests/Makefile
+
 2020-07-10  Simon J Gerraty  <sjg@beast.crufty.net>
 
 	* configure.in: use AC_INCLUDES_DEFAULT rather than AC_HEADER_STDC
diff --git a/FILES b/FILES
index 0716bffa3c3b..33127290a76e 100644
--- a/FILES
+++ b/FILES
@@ -23,6 +23,8 @@ configure.in
 dir.c
 dir.h
 dirname.c
+enum.c
+enum.h
 filemon/filemon.h
 filemon/filemon_dev.c
 filemon/filemon_ktrace.c
@@ -34,36 +36,8 @@ hash.h
 install-sh
 job.c
 job.h
+lst.c
 lst.h
-lst.lib/Makefile
-lst.lib/lstAppend.c
-lst.lib/lstAtEnd.c
-lst.lib/lstAtFront.c
-lst.lib/lstClose.c
-lst.lib/lstConcat.c
-lst.lib/lstDatum.c
-lst.lib/lstDeQueue.c
-lst.lib/lstDestroy.c
-lst.lib/lstDupl.c
-lst.lib/lstEnQueue.c
-lst.lib/lstFind.c
-lst.lib/lstFindFrom.c
-lst.lib/lstFirst.c
-lst.lib/lstForEach.c
-lst.lib/lstForEachFrom.c
-lst.lib/lstInit.c
-lst.lib/lstInsert.c
-lst.lib/lstInt.h
-lst.lib/lstIsAtEnd.c
-lst.lib/lstIsEmpty.c
-lst.lib/lstLast.c
-lst.lib/lstMember.c
-lst.lib/lstNext.c
-lst.lib/lstOpen.c
-lst.lib/lstPrev.c
-lst.lib/lstRemove.c
-lst.lib/lstReplace.c
-lst.lib/lstSucc.c
 machine.sh
 main.c
 make-bootstrap.sh.in
@@ -88,7 +62,6 @@ ranlib.h
 realpath.c
 setenv.c
 sigcompat.c
-sprite.h
 str.c
 stresep.c
 strlcpy.c
@@ -100,22 +73,234 @@ trace.c
 trace.h
 unit-tests/Makefile
 unit-tests/Makefile.config.in
+unit-tests/archive.exp
+unit-tests/archive.mk
+unit-tests/archive-suffix.exp
+unit-tests/archive-suffix.mk
+unit-tests/cmd-interrupt.exp
+unit-tests/cmd-interrupt.mk
+unit-tests/cmdline.exp
+unit-tests/cmdline.mk
 unit-tests/comment.exp
 unit-tests/comment.mk
+unit-tests/cond-cmp-numeric-eq.exp
+unit-tests/cond-cmp-numeric-eq.mk
+unit-tests/cond-cmp-numeric-ge.exp
+unit-tests/cond-cmp-numeric-ge.mk
+unit-tests/cond-cmp-numeric-gt.exp
+unit-tests/cond-cmp-numeric-gt.mk
+unit-tests/cond-cmp-numeric-le.exp
+unit-tests/cond-cmp-numeric-le.mk
+unit-tests/cond-cmp-numeric-lt.exp
+unit-tests/cond-cmp-numeric-lt.mk
+unit-tests/cond-cmp-numeric-ne.exp
+unit-tests/cond-cmp-numeric-ne.mk
+unit-tests/cond-cmp-numeric.exp
+unit-tests/cond-cmp-numeric.mk
+unit-tests/cond-cmp-string.exp
+unit-tests/cond-cmp-string.mk
+unit-tests/cond-func.exp
+unit-tests/cond-func.mk
+unit-tests/cond-func-commands.exp
+unit-tests/cond-func-commands.mk
+unit-tests/cond-func-defined.exp
+unit-tests/cond-func-defined.mk
+unit-tests/cond-func-empty.exp
+unit-tests/cond-func-empty.mk
+unit-tests/cond-func-exists.exp
+unit-tests/cond-func-exists.mk
+unit-tests/cond-func-make.exp
+unit-tests/cond-func-make.mk
+unit-tests/cond-func-target.exp
+unit-tests/cond-func-target.mk
+unit-tests/cond-late.exp
+unit-tests/cond-late.mk
+unit-tests/cond-op-and.exp
+unit-tests/cond-op-and.mk
+unit-tests/cond-op-not.exp
+unit-tests/cond-op-not.mk
+unit-tests/cond-op-or.exp
+unit-tests/cond-op-or.mk
+unit-tests/cond-op-parentheses.exp
+unit-tests/cond-op-parentheses.mk
+unit-tests/cond-op.exp
+unit-tests/cond-op.mk
+unit-tests/cond-short.exp
+unit-tests/cond-short.mk
+unit-tests/cond-token-number.exp
+unit-tests/cond-token-number.mk
+unit-tests/cond-token-plain.exp
+unit-tests/cond-token-plain.mk
+unit-tests/cond-token-string.exp
+unit-tests/cond-token-string.mk
+unit-tests/cond-token-var.exp
+unit-tests/cond-token-var.mk
 unit-tests/cond1.exp
 unit-tests/cond1.mk
 unit-tests/cond2.exp
 unit-tests/cond2.mk
-unit-tests/cond-late.mk
-unit-tests/cond-late.exp
-unit-tests/cond-short.mk
-unit-tests/cond-short.exp
+unit-tests/counter.exp
+unit-tests/counter.mk
+unit-tests/dep-colon.exp
+unit-tests/dep-colon.mk
+unit-tests/dep-double-colon.exp
+unit-tests/dep-double-colon.mk
+unit-tests/dep-exclam.exp
+unit-tests/dep-exclam.mk
+unit-tests/dep-none.exp
+unit-tests/dep-none.mk
+unit-tests/dep-var.exp
+unit-tests/dep-var.mk
+unit-tests/dep-wildcards.exp
+unit-tests/dep-wildcards.mk
+unit-tests/dep.exp
+unit-tests/dep.mk
+unit-tests/depsrc-exec.exp
+unit-tests/depsrc-exec.mk
+unit-tests/depsrc-ignore.exp
+unit-tests/depsrc-ignore.mk
+unit-tests/depsrc-made.exp
+unit-tests/depsrc-made.mk
+unit-tests/depsrc-make.exp
+unit-tests/depsrc-make.mk
+unit-tests/depsrc-meta.exp
+unit-tests/depsrc-meta.mk
+unit-tests/depsrc-nometa.exp
+unit-tests/depsrc-nometa.mk
+unit-tests/depsrc-nometa_cmp.exp
+unit-tests/depsrc-nometa_cmp.mk
+unit-tests/depsrc-nopath.exp
+unit-tests/depsrc-nopath.mk
+unit-tests/depsrc-notmain.exp
+unit-tests/depsrc-notmain.mk
+unit-tests/depsrc-optional.exp
+unit-tests/depsrc-optional.mk
+unit-tests/depsrc-phony.exp
+unit-tests/depsrc-phony.mk
+unit-tests/depsrc-precious.exp
+unit-tests/depsrc-precious.mk
+unit-tests/depsrc-recursive.exp
+unit-tests/depsrc-recursive.mk
+unit-tests/depsrc-silent.exp
+unit-tests/depsrc-silent.mk
+unit-tests/depsrc-use.exp
+unit-tests/depsrc-use.mk
+unit-tests/depsrc-usebefore.exp
+unit-tests/depsrc-usebefore.mk
+unit-tests/depsrc-usebefore-double-colon.exp
+unit-tests/depsrc-usebefore-double-colon.mk
+unit-tests/depsrc-wait.exp
+unit-tests/depsrc-wait.mk
+unit-tests/depsrc.exp
+unit-tests/depsrc.mk
+unit-tests/deptgt-begin.exp
+unit-tests/deptgt-begin.mk
+unit-tests/deptgt-default.exp
+unit-tests/deptgt-default.mk
+unit-tests/deptgt-delete_on_error.exp
+unit-tests/deptgt-delete_on_error.mk
+unit-tests/deptgt-end.exp
+unit-tests/deptgt-end.mk
+unit-tests/deptgt-error.exp
+unit-tests/deptgt-error.mk
+unit-tests/deptgt-ignore.exp
+unit-tests/deptgt-ignore.mk
+unit-tests/deptgt-interrupt.exp
+unit-tests/deptgt-interrupt.mk
+unit-tests/deptgt-main.exp
+unit-tests/deptgt-main.mk
+unit-tests/deptgt-makeflags.exp
+unit-tests/deptgt-makeflags.mk
+unit-tests/deptgt-no_parallel.exp
+unit-tests/deptgt-no_parallel.mk
+unit-tests/deptgt-nopath.exp
+unit-tests/deptgt-nopath.mk
+unit-tests/deptgt-notparallel.exp
+unit-tests/deptgt-notparallel.mk
+unit-tests/deptgt-objdir.exp
+unit-tests/deptgt-objdir.mk
+unit-tests/deptgt-order.exp
+unit-tests/deptgt-order.mk
+unit-tests/deptgt-path-suffix.exp
+unit-tests/deptgt-path-suffix.mk
+unit-tests/deptgt-path.exp
+unit-tests/deptgt-path.mk
+unit-tests/deptgt-phony.exp
+unit-tests/deptgt-phony.mk
+unit-tests/deptgt-precious.exp
+unit-tests/deptgt-precious.mk
+unit-tests/deptgt-shell.exp
+unit-tests/deptgt-shell.mk
+unit-tests/deptgt-silent.exp
+unit-tests/deptgt-silent.mk
+unit-tests/deptgt-stale.exp
+unit-tests/deptgt-stale.mk
+unit-tests/deptgt-suffixes.exp
+unit-tests/deptgt-suffixes.mk
+unit-tests/deptgt.exp
+unit-tests/deptgt.mk
+unit-tests/dir.exp
+unit-tests/dir.mk
+unit-tests/dir-expand-path.exp
+unit-tests/dir-expand-path.mk
+unit-tests/directive-elif.exp
+unit-tests/directive-elif.mk
+unit-tests/directive-elifdef.exp
+unit-tests/directive-elifdef.mk
+unit-tests/directive-elifmake.exp
+unit-tests/directive-elifmake.mk
+unit-tests/directive-elifndef.exp
+unit-tests/directive-elifndef.mk
+unit-tests/directive-elifnmake.exp
+unit-tests/directive-elifnmake.mk
+unit-tests/directive-else.exp
+unit-tests/directive-else.mk
+unit-tests/directive-endif.exp
+unit-tests/directive-endif.mk
+unit-tests/directive-error.exp
+unit-tests/directive-error.mk
+unit-tests/directive-export-env.exp
+unit-tests/directive-export-env.mk
+unit-tests/directive-export-literal.exp
+unit-tests/directive-export-literal.mk
+unit-tests/directive-export.exp
+unit-tests/directive-export.mk
+unit-tests/directive-for.exp
+unit-tests/directive-for.mk
+unit-tests/directive-for-generating-endif.exp
+unit-tests/directive-for-generating-endif.mk
+unit-tests/directive-if.exp
+unit-tests/directive-if.mk
+unit-tests/directive-ifdef.exp
+unit-tests/directive-ifdef.mk
+unit-tests/directive-ifmake.exp
+unit-tests/directive-ifmake.mk
+unit-tests/directive-ifndef.exp
+unit-tests/directive-ifndef.mk
+unit-tests/directive-ifnmake.exp
+unit-tests/directive-ifnmake.mk
+unit-tests/directive-info.exp
+unit-tests/directive-info.mk
+unit-tests/directive-undef.exp
+unit-tests/directive-undef.mk
+unit-tests/directive-unexport-env.exp
+unit-tests/directive-unexport-env.mk
+unit-tests/directive-unexport.exp
+unit-tests/directive-unexport.mk
+unit-tests/directive-warning.exp
+unit-tests/directive-warning.mk
+unit-tests/directive.exp
+unit-tests/directive.mk
+unit-tests/directives.exp
+unit-tests/directives.mk
 unit-tests/dollar.exp
 unit-tests/dollar.mk
 unit-tests/doterror.exp
 unit-tests/doterror.mk
 unit-tests/dotwait.exp
 unit-tests/dotwait.mk
+unit-tests/envfirst.exp
+unit-tests/envfirst.mk
 unit-tests/error.exp
 unit-tests/error.mk
 unit-tests/escape.exp
@@ -124,6 +309,8 @@ unit-tests/export-all.exp
 unit-tests/export-all.mk
 unit-tests/export-env.exp
 unit-tests/export-env.mk
+unit-tests/export-variants.exp
+unit-tests/export-variants.mk
 unit-tests/export.exp
 unit-tests/export.mk
 unit-tests/forloop.exp
@@ -138,6 +325,10 @@ unit-tests/include-main.exp
 unit-tests/include-main.mk
 unit-tests/include-sub.mk
 unit-tests/include-subsub.mk
+unit-tests/lint.exp
+unit-tests/lint.mk
+unit-tests/make-exported.exp
+unit-tests/make-exported.mk
 unit-tests/misc.exp
 unit-tests/misc.mk
 unit-tests/moderrs.exp
@@ -146,12 +337,62 @@ unit-tests/modmatch.exp
 unit-tests/modmatch.mk
 unit-tests/modmisc.exp
 unit-tests/modmisc.mk
-unit-tests/modorder.exp
-unit-tests/modorder.mk
 unit-tests/modts.exp
 unit-tests/modts.mk
 unit-tests/modword.exp
 unit-tests/modword.mk
+unit-tests/opt-backwards.exp
+unit-tests/opt-backwards.mk
+unit-tests/opt-chdir.exp
+unit-tests/opt-chdir.mk
+unit-tests/opt-debug.exp
+unit-tests/opt-debug.mk
+unit-tests/opt-debug-g1.exp
+unit-tests/opt-debug-g1.mk
+unit-tests/opt-define.exp
+unit-tests/opt-define.mk
+unit-tests/opt-env.exp
+unit-tests/opt-env.mk
+unit-tests/opt-file.exp
+unit-tests/opt-file.mk
+unit-tests/opt-ignore.exp
+unit-tests/opt-ignore.mk
+unit-tests/opt-include-dir.exp
+unit-tests/opt-include-dir.mk
+unit-tests/opt-jobs-internal.exp
+unit-tests/opt-jobs-internal.mk
+unit-tests/opt-jobs.exp
+unit-tests/opt-jobs.mk
+unit-tests/opt-keep-going.exp
+unit-tests/opt-keep-going.mk
+unit-tests/opt-m-include-dir.exp
+unit-tests/opt-m-include-dir.mk
+unit-tests/opt-no-action-at-all.exp
+unit-tests/opt-no-action-at-all.mk
+unit-tests/opt-no-action.exp
+unit-tests/opt-no-action.mk
+unit-tests/opt-query.exp
+unit-tests/opt-query.mk
+unit-tests/opt-raw.exp
+unit-tests/opt-raw.mk
+unit-tests/opt-silent.exp
+unit-tests/opt-silent.mk
+unit-tests/opt-touch.exp
+unit-tests/opt-touch.mk
+unit-tests/opt-tracefile.exp
+unit-tests/opt-tracefile.mk
+unit-tests/opt-var-expanded.exp
+unit-tests/opt-var-expanded.mk
+unit-tests/opt-var-literal.exp
+unit-tests/opt-var-literal.mk
+unit-tests/opt-warnings-as-errors.exp
+unit-tests/opt-warnings-as-errors.mk
+unit-tests/opt-where-am-i.exp
+unit-tests/opt-where-am-i.mk
+unit-tests/opt-x-reduce-exported.exp
+unit-tests/opt-x-reduce-exported.mk
+unit-tests/opt.exp
+unit-tests/opt.mk
 unit-tests/order.exp
 unit-tests/order.mk
 unit-tests/phony-end.exp
@@ -162,6 +403,28 @@ unit-tests/posix1.exp
 unit-tests/posix1.mk
 unit-tests/qequals.exp
 unit-tests/qequals.mk
+unit-tests/recursive.exp
+unit-tests/recursive.mk
+unit-tests/sh-dots.exp
+unit-tests/sh-dots.mk
+unit-tests/sh-jobs-error.exp
+unit-tests/sh-jobs-error.mk
+unit-tests/sh-jobs.exp
+unit-tests/sh-jobs.mk
+unit-tests/sh-leading-at.exp
+unit-tests/sh-leading-at.mk
+unit-tests/sh-leading-hyphen.exp
+unit-tests/sh-leading-hyphen.mk
+unit-tests/sh-leading-plus.exp
+unit-tests/sh-leading-plus.mk
+unit-tests/sh-meta-chars.exp
+unit-tests/sh-meta-chars.mk
+unit-tests/sh-multi-line.exp
+unit-tests/sh-multi-line.mk
+unit-tests/sh-single-line.exp
+unit-tests/sh-single-line.mk
+unit-tests/sh.exp
+unit-tests/sh.mk
 unit-tests/suffixes.exp
 unit-tests/suffixes.mk
 unit-tests/sunshcmd.exp
@@ -174,12 +437,204 @@ unit-tests/unexport-env.exp
 unit-tests/unexport-env.mk
 unit-tests/unexport.exp
 unit-tests/unexport.mk
+unit-tests/use-inference.exp
+unit-tests/use-inference.mk
+unit-tests/var-class-cmdline.exp
+unit-tests/var-class-cmdline.mk
+unit-tests/var-class-env.exp
+unit-tests/var-class-env.mk
+unit-tests/var-class-global.exp
+unit-tests/var-class-global.mk
+unit-tests/var-class-local-legacy.exp
+unit-tests/var-class-local-legacy.mk
+unit-tests/var-class-local.exp
+unit-tests/var-class-local.mk
+unit-tests/var-class.exp
+unit-tests/var-class.mk
+unit-tests/var-op-append.exp
+unit-tests/var-op-append.mk
+unit-tests/var-op-assign.exp
+unit-tests/var-op-assign.mk
+unit-tests/var-op-default.exp
+unit-tests/var-op-default.mk
+unit-tests/var-op-expand.exp
+unit-tests/var-op-expand.mk
+unit-tests/var-op-shell.exp
+unit-tests/var-op-shell.mk
+unit-tests/var-op.exp
+unit-tests/var-op.mk
 unit-tests/varcmd.exp
 unit-tests/varcmd.mk
+unit-tests/vardebug.exp
+unit-tests/vardebug.mk
+unit-tests/varfind.exp
+unit-tests/varfind.mk
 unit-tests/varmisc.exp
 unit-tests/varmisc.mk
+unit-tests/varmod-assign.exp
+unit-tests/varmod-assign.mk
+unit-tests/varmod-defined.exp
+unit-tests/varmod-defined.mk
 unit-tests/varmod-edge.exp
 unit-tests/varmod-edge.mk
+unit-tests/varmod-exclam-shell.exp
+unit-tests/varmod-exclam-shell.mk
+unit-tests/varmod-extension.exp
+unit-tests/varmod-extension.mk
+unit-tests/varmod-gmtime.exp
+unit-tests/varmod-gmtime.mk
+unit-tests/varmod-hash.exp
+unit-tests/varmod-hash.mk
+unit-tests/varmod-head.exp
+unit-tests/varmod-head.mk
+unit-tests/varmod-ifelse.exp
+unit-tests/varmod-ifelse.mk
+unit-tests/varmod-l-name-to-value.exp
+unit-tests/varmod-l-name-to-value.mk
+unit-tests/varmod-localtime.exp
+unit-tests/varmod-localtime.mk
+unit-tests/varmod-loop.exp
+unit-tests/varmod-loop.mk
+unit-tests/varmod-match-escape.exp
+unit-tests/varmod-match-escape.mk
+unit-tests/varmod-match.exp
+unit-tests/varmod-match.mk
+unit-tests/varmod-no-match.exp
+unit-tests/varmod-no-match.mk
+unit-tests/varmod-order-reverse.exp
+unit-tests/varmod-order-reverse.mk
+unit-tests/varmod-order-shuffle.exp
+unit-tests/varmod-order-shuffle.mk
+unit-tests/varmod-order.exp
+unit-tests/varmod-order.mk
+unit-tests/varmod-path.exp
+unit-tests/varmod-path.mk
+unit-tests/varmod-quote-dollar.exp
+unit-tests/varmod-quote-dollar.mk
+unit-tests/varmod-quote.exp
+unit-tests/varmod-quote.mk
+unit-tests/varmod-range.exp
+unit-tests/varmod-range.mk
+unit-tests/varmod-remember.exp
+unit-tests/varmod-remember.mk
+unit-tests/varmod-root.exp
+unit-tests/varmod-root.mk
+unit-tests/varmod-select-words.exp
+unit-tests/varmod-select-words.mk
+unit-tests/varmod-shell.exp
+unit-tests/varmod-shell.mk
+unit-tests/varmod-subst-regex.exp
+unit-tests/varmod-subst-regex.mk
+unit-tests/varmod-subst.exp
+unit-tests/varmod-subst.mk
+unit-tests/varmod-sysv.exp
+unit-tests/varmod-sysv.mk
+unit-tests/varmod-tail.exp
+unit-tests/varmod-tail.mk
+unit-tests/varmod-to-abs.exp
+unit-tests/varmod-to-abs.mk
+unit-tests/varmod-to-lower.exp
+unit-tests/varmod-to-lower.mk
+unit-tests/varmod-to-many-words.exp
+unit-tests/varmod-to-many-words.mk
+unit-tests/varmod-to-one-word.exp
+unit-tests/varmod-to-one-word.mk
+unit-tests/varmod-to-separator.exp
+unit-tests/varmod-to-separator.mk
+unit-tests/varmod-to-upper.exp
+unit-tests/varmod-to-upper.mk
+unit-tests/varmod-undefined.exp
+unit-tests/varmod-undefined.mk
+unit-tests/varmod-unique.exp
+unit-tests/varmod-unique.mk
+unit-tests/varmod.exp
+unit-tests/varmod.mk
+unit-tests/varname-dollar.exp
+unit-tests/varname-dollar.mk
+unit-tests/varname-dot-alltargets.exp
+unit-tests/varname-dot-alltargets.mk
+unit-tests/varname-dot-curdir.exp
+unit-tests/varname-dot-curdir.mk
+unit-tests/varname-dot-includes.exp
+unit-tests/varname-dot-includes.mk
+unit-tests/varname-dot-includedfromdir.exp
+unit-tests/varname-dot-includedfromdir.mk
+unit-tests/varname-dot-includedfromfile.exp
+unit-tests/varname-dot-includedfromfile.mk
+unit-tests/varname-dot-libs.exp
+unit-tests/varname-dot-libs.mk
+unit-tests/varname-dot-make-dependfile.exp
+unit-tests/varname-dot-make-dependfile.mk
+unit-tests/varname-dot-make-expand_variables.exp
+unit-tests/varname-dot-make-expand_variables.mk
+unit-tests/varname-dot-make-exported.exp
+unit-tests/varname-dot-make-exported.mk
+unit-tests/varname-dot-make-jobs-prefix.exp
+unit-tests/varname-dot-make-jobs-prefix.mk
+unit-tests/varname-dot-make-jobs.exp
+unit-tests/varname-dot-make-jobs.mk
+unit-tests/varname-dot-make-level.exp
+unit-tests/varname-dot-make-level.mk
+unit-tests/varname-dot-make-makefile_preference.exp
+unit-tests/varname-dot-make-makefile_preference.mk
+unit-tests/varname-dot-make-makefiles.exp
+unit-tests/varname-dot-make-makefiles.mk
+unit-tests/varname-dot-make-meta-bailiwick.exp
+unit-tests/varname-dot-make-meta-bailiwick.mk
+unit-tests/varname-dot-make-meta-created.exp
+unit-tests/varname-dot-make-meta-created.mk
+unit-tests/varname-dot-make-meta-files.exp
+unit-tests/varname-dot-make-meta-files.mk
+unit-tests/varname-dot-make-meta-ignore_filter.exp
+unit-tests/varname-dot-make-meta-ignore_filter.mk
+unit-tests/varname-dot-make-meta-ignore_paths.exp
+unit-tests/varname-dot-make-meta-ignore_paths.mk
+unit-tests/varname-dot-make-meta-ignore_patterns.exp
+unit-tests/varname-dot-make-meta-ignore_patterns.mk
+unit-tests/varname-dot-make-meta-prefix.exp
+unit-tests/varname-dot-make-meta-prefix.mk
+unit-tests/varname-dot-make-mode.exp
+unit-tests/varname-dot-make-mode.mk
+unit-tests/varname-dot-make-path_filemon.exp
+unit-tests/varname-dot-make-path_filemon.mk
+unit-tests/varname-dot-make-pid.exp
+unit-tests/varname-dot-make-pid.mk
+unit-tests/varname-dot-make-ppid.exp
+unit-tests/varname-dot-make-ppid.mk
+unit-tests/varname-dot-make-save_dollars.exp
+unit-tests/varname-dot-make-save_dollars.mk
+unit-tests/varname-dot-makeoverrides.exp
+unit-tests/varname-dot-makeoverrides.mk
+unit-tests/varname-dot-newline.exp
+unit-tests/varname-dot-newline.mk
+unit-tests/varname-dot-objdir.exp
+unit-tests/varname-dot-objdir.mk
+unit-tests/varname-dot-parsedir.exp
+unit-tests/varname-dot-parsedir.mk
+unit-tests/varname-dot-parsefile.exp
+unit-tests/varname-dot-parsefile.mk
+unit-tests/varname-dot-path.exp
+unit-tests/varname-dot-path.mk
+unit-tests/varname-dot-shell.exp
+unit-tests/varname-dot-shell.mk
+unit-tests/varname-dot-targets.exp
+unit-tests/varname-dot-targets.mk
+unit-tests/varname-empty.exp
+unit-tests/varname-empty.mk
+unit-tests/varname-make.exp
+unit-tests/varname-make.mk
+unit-tests/varname-make_print_var_on_error.exp
+unit-tests/varname-make_print_var_on_error.mk
+unit-tests/varname-makeflags.exp
+unit-tests/varname-makeflags.mk
+unit-tests/varname-pwd.exp
+unit-tests/varname-pwd.mk
+unit-tests/varname-vpath.exp
+unit-tests/varname-vpath.mk
+unit-tests/varname.exp
+unit-tests/varname.mk
+unit-tests/varparse-dynamic.exp
+unit-tests/varparse-dynamic.mk
 unit-tests/varquote.exp
 unit-tests/varquote.mk
 unit-tests/varshell.exp
diff --git a/LICENSE b/LICENSE
index dbad0ec06a4d..283dd20cccd0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -3,6 +3,7 @@ original contributors or assignees.
 Including:
 
     Copyright (c) 1993-2020, Simon J Gerraty
+    Copyright (c) 2020, Roland Illig <rillig@NetBSD.org>
     Copyright (c) 2009-2016, Juniper Networks, Inc.
     Copyright (c) 2009, John Birrell.
     Copyright (c) 1997-2020 The NetBSD Foundation, Inc.
diff --git a/Makefile b/Makefile
index c329017b13ef..7277d9554de4 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-#	$Id: Makefile,v 1.107 2020/06/07 21:18:46 sjg Exp $
+#	$Id: Makefile,v 1.112 2020/08/28 16:26:17 sjg Exp $
 
 PROG=	bmake
 
@@ -8,9 +8,11 @@ SRCS= \
 	compat.c \
 	cond.c \
 	dir.c \
+	enum.c \
 	for.c \
 	hash.c \
 	job.c \
+	lst.c \
 	main.c \
 	make.c \
 	make_malloc.c \
@@ -25,36 +27,6 @@ SRCS= \
 	util.c \
 	var.c
 
-# from lst.lib/
-SRCS+= \
-	lstAppend.c \
-	lstAtEnd.c \
-	lstAtFront.c \
-	lstClose.c \
-	lstConcat.c \
-	lstDatum.c \
-	lstDeQueue.c \
-	lstDestroy.c \
-	lstDupl.c \
-	lstEnQueue.c \
-	lstFind.c \
-	lstFindFrom.c \
-	lstFirst.c \
-	lstForEach.c \
-	lstForEachFrom.c \
-	lstInit.c \
-	lstInsert.c \
-	lstIsAtEnd.c \
-	lstIsEmpty.c \
-	lstLast.c \
-	lstMember.c \
-	lstNext.c \
-	lstOpen.c \
-	lstPrev.c \
-	lstRemove.c \
-	lstReplace.c \
-	lstSucc.c
-
 .-include "VERSION"
 .-include "Makefile.inc"
 
@@ -97,7 +69,6 @@ COPTS.filemon_dev.c += -DHAVE_FILEMON_H -I${FILEMON_H:H}
 .endif				# USE_FILEMON
 
 .PATH:	${srcdir}
-.PATH:	${srcdir}/lst.lib
 
 .if make(obj) || make(clean)
 SUBDIR+= unit-tests
@@ -109,7 +80,7 @@ SUBDIR+= unit-tests
 # list of OS's which are derrived from BSD4.4
 BSD44_LIST= NetBSD FreeBSD OpenBSD DragonFly MirBSD Bitrig
 # we are...
-OS!= uname -s
+OS := ${.MAKE.OS:U${uname -s:L:sh}}
 # are we 4.4BSD ?
 isBSD44:=${BSD44_LIST:M${OS}}
 
@@ -237,5 +208,8 @@ install-mk:
 # end-delete2
 
 # A simple unit-test driver to help catch regressions
+TEST_MAKE ?= ${.OBJDIR}/${PROG:T}
 accept test:
-	cd ${.CURDIR}/unit-tests && MAKEFLAGS= ${.MAKE} -r -m / TEST_MAKE=${TEST_MAKE:U${.OBJDIR}/${PROG:T}} ${.TARGET}
+	cd ${.CURDIR}/unit-tests && \
+	MAKEFLAGS= ${TEST_MAKE} -r -m / ${.TARGET} ${TESTS:DTESTS=${TESTS:Q}}
+
diff --git a/VERSION b/VERSION
index 3c8b783c2861..b13b442cd4a4 100644
--- a/VERSION
+++ b/VERSION
@@ -1,2 +1,2 @@
 # keep this compatible with sh and make
-_MAKE_VERSION=20200710
+_MAKE_VERSION=20200902
diff --git a/arch.c b/arch.c
index 9db7ce2bd14b..fa54908d8817 100644
--- a/arch.c
+++ b/arch.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: arch.c,v 1.73 2020/07/03 08:02:55 rillig Exp $	*/
+/*	$NetBSD: arch.c,v 1.107 2020/08/30 11:15:05 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -69,14 +69,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: arch.c,v 1.73 2020/07/03 08:02:55 rillig Exp $";
+static char rcsid[] = "$NetBSD: arch.c,v 1.107 2020/08/30 11:15:05 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)arch.c	8.2 (Berkeley) 1/2/94";
 #else
-__RCSID("$NetBSD: arch.c,v 1.73 2020/07/03 08:02:55 rillig Exp $");
+__RCSID("$NetBSD: arch.c,v 1.107 2020/08/30 11:15:05 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -94,7 +94,7 @@ __RCSID("$NetBSD: arch.c,v 1.73 2020/07/03 08:02:55 rillig Exp $");
  * The interface to this module is:
  *	Arch_ParseArchive   	Given an archive specification, return a list
  *	    	  	    	of GNode's, one for each member in the spec.
- *	    	  	    	FAILURE is returned if the specification is
+ *	    	  	    	FALSE is returned if the specification is
  *	    	  	    	invalid for some reason.
  *
  *	Arch_Touch	    	Alter the modification time of the archive
@@ -184,12 +184,9 @@ typedef struct Arch {
     size_t	  fnamesize;  /* Size of the string table */
 } Arch;
 
-static int ArchFindArchive(const void *, const void *);
-#ifdef CLEANUP
-static void ArchFree(void *);
-#endif
-static struct ar_hdr *ArchStatMember(char *, char *, Boolean);
-static FILE *ArchFindMember(char *, char *, struct ar_hdr *, const char *);
+static struct ar_hdr *ArchStatMember(const char *, const char *, Boolean);
+static FILE *ArchFindMember(const char *, const char *,
+			    struct ar_hdr *, const char *);
 #if defined(__svr4__) || defined(__SVR4) || defined(__ELF__)
 #define SVR4ARCHIVES
 static int ArchSVR4Entry(Arch *, char *, size_t, FILE *);
@@ -225,19 +222,6 @@ static int ArchSVR4Entry(Arch *, char *, size_t, FILE *);
 #define AR_MAX_NAME_LEN	    (sizeof(arh.AR_NAME)-1)
 
 #ifdef CLEANUP
-/*-
- *-----------------------------------------------------------------------
- * ArchFree --
- *	Free memory used by an archive
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
 static void
 ArchFree(void *ap)
 {
@@ -272,23 +256,18 @@ ArchFree(void *ap)
  *	ctxt		Context in which to expand variables
  *
  * Results:
- *	SUCCESS if it was a valid specification. The linePtr is updated
+ *	TRUE if it was a valid specification. The linePtr is updated
  *	to point to the first non-space after the archive spec. The
  *	nodes for the members are placed on the given list.
- *
- * Side Effects:
- *	Some nodes may be created. The given list is extended.
- *
  *-----------------------------------------------------------------------
  */
-ReturnStatus
+Boolean
 Arch_ParseArchive(char **linePtr, Lst nodeLst, GNode *ctxt)
 {
     char	    *cp;	    /* Pointer into line */
     GNode	    *gn;     	    /* New node */
     char	    *libName;  	    /* Library-part of specification */
     char	    *memName;  	    /* Member-part of specification */
-    char	    *nameBuf;	    /* temporary place for node name */
     char	    saveChar;  	    /* Ending delimiter of member-name */
     Boolean 	    subLibName;	    /* TRUE if libName should have/had
 				     * variable substitution performed on it */
@@ -304,26 +283,25 @@ Arch_ParseArchive(char **linePtr, Lst nodeLst, GNode *ctxt)
 	     * so we can safely advance beyond it...
 	     */
 	    int 	length;
-	    void	*freeIt;
-	    char	*result;
+	    void	*result_freeIt;
+	    const char	*result;
+	    Boolean isError;
 
-	    result = Var_Parse(cp, ctxt, VARF_UNDEFERR|VARF_WANTRES,
-			       &length, &freeIt);
-	    free(freeIt);
+	    result = Var_Parse(cp, ctxt, VARE_UNDEFERR|VARE_WANTRES,
+			       &length, &result_freeIt);
+	    isError = result == var_Error;
+	    free(result_freeIt);
+	    if (isError)
+		return FALSE;
 
-	    if (result == var_Error) {
-		return FAILURE;
-	    } else {
-		subLibName = TRUE;
-	    }
-
-	    cp += length-1;
+	    subLibName = TRUE;
+	    cp += length - 1;
 	}
     }
 
     *cp++ = '\0';
     if (subLibName) {
-	libName = Var_Subst(NULL, libName, ctxt, VARF_UNDEFERR|VARF_WANTRES);
+	libName = Var_Subst(libName, ctxt, VARE_UNDEFERR|VARE_WANTRES);
     }
 
 
@@ -347,18 +325,18 @@ Arch_ParseArchive(char **linePtr, Lst nodeLst, GNode *ctxt)
 		 */
 		int 	length;
 		void	*freeIt;
-		char	*result;
+		const char *result;
+		Boolean isError;
 
-		result = Var_Parse(cp, ctxt, VARF_UNDEFERR|VARF_WANTRES,
+		result = Var_Parse(cp, ctxt, VARE_UNDEFERR|VARE_WANTRES,
 				   &length, &freeIt);
+		isError = result == var_Error;
 		free(freeIt);
 
-		if (result == var_Error) {
-		    return FAILURE;
-		} else {
-		    doSubst = TRUE;
-		}
+		if (isError)
+		    return FALSE;
 
+		doSubst = TRUE;
 		cp += length;
 	    } else {
 		cp++;
@@ -372,7 +350,7 @@ Arch_ParseArchive(char **linePtr, Lst nodeLst, GNode *ctxt)
 	 */
 	if (*cp == '\0') {
 	    printf("No closing parenthesis in archive specification\n");
-	    return FAILURE;
+	    return FALSE;
 	}
 
 	/*
@@ -401,20 +379,15 @@ Arch_ParseArchive(char **linePtr, Lst nodeLst, GNode *ctxt)
 	    char    *buf;
 	    char    *sacrifice;
 	    char    *oldMemName = memName;
-	    size_t   sz;
 
-	    memName = Var_Subst(NULL, memName, ctxt,
-				VARF_UNDEFERR|VARF_WANTRES);
+	    memName = Var_Subst(memName, ctxt, VARE_UNDEFERR | VARE_WANTRES);
 
 	    /*
 	     * Now form an archive spec and recurse to deal with nested
 	     * variables and multi-word variable values.... The results
 	     * are just placed at the end of the nodeLst we're returning.
 	     */
-	    sz = strlen(memName)+strlen(libName)+3;
-	    buf = sacrifice = bmake_malloc(sz);
-
-	    snprintf(buf, sz, "%s(%s)", libName, memName);
+	    buf = sacrifice = str_concat4(libName, "(", memName, ")");
 
 	    if (strchr(memName, '$') && strcmp(memName, oldMemName) == 0) {
 		/*
@@ -426,42 +399,43 @@ Arch_ParseArchive(char **linePtr, Lst nodeLst, GNode *ctxt)
 
 		if (gn == NULL) {
 		    free(buf);
-		    return FAILURE;
+		    return FALSE;
 		} else {
 		    gn->type |= OP_ARCHV;
-		    (void)Lst_AtEnd(nodeLst, gn);
+		    Lst_Append(nodeLst, gn);
 		}
-	    } else if (Arch_ParseArchive(&sacrifice, nodeLst, ctxt)!=SUCCESS) {
+	    } else if (!Arch_ParseArchive(&sacrifice, nodeLst, ctxt)) {
 		/*
-		 * Error in nested call -- free buffer and return FAILURE
+		 * Error in nested call -- free buffer and return FALSE
 		 * ourselves.
 		 */
 		free(buf);
-		return FAILURE;
+		return FALSE;
 	    }
 	    /*
 	     * Free buffer and continue with our work.
 	     */
 	    free(buf);
 	} else if (Dir_HasWildcards(memName)) {
-	    Lst	  members = Lst_Init(FALSE);
-	    char  *member;
-	    size_t sz = MAXPATHLEN, nsz;
-	    nameBuf = bmake_malloc(sz);
+	    Lst members = Lst_Init();
+	    Buffer nameBuf;
 
+	    Buf_Init(&nameBuf, 0);
 	    Dir_Expand(memName, dirSearchPath, members);
 	    while (!Lst_IsEmpty(members)) {
-		member = (char *)Lst_DeQueue(members);
-		nsz = strlen(libName) + strlen(member) + 3;
-		if (sz > nsz)
-		    nameBuf = bmake_realloc(nameBuf, sz = nsz * 2);
+		char *member = Lst_Dequeue(members);
 
-		snprintf(nameBuf, sz, "%s(%s)", libName, member);
+		Buf_Empty(&nameBuf);
+		Buf_AddStr(&nameBuf, libName);
+		Buf_AddStr(&nameBuf, "(");
+		Buf_AddStr(&nameBuf, member);
+		Buf_AddStr(&nameBuf, ")");
 		free(member);
-		gn = Targ_FindNode(nameBuf, TARG_CREATE);
+
+		gn = Targ_FindNode(Buf_GetAll(&nameBuf, NULL), TARG_CREATE);
 		if (gn == NULL) {
-		    free(nameBuf);
-		    return FAILURE;
+		    Buf_Destroy(&nameBuf, TRUE);
+		    return FALSE;
 		} else {
 		    /*
 		     * We've found the node, but have to make sure the rest of
@@ -471,19 +445,24 @@ Arch_ParseArchive(char **linePtr, Lst nodeLst, GNode *ctxt)
 		     * end of the provided list.
 		     */
 		    gn->type |= OP_ARCHV;
-		    (void)Lst_AtEnd(nodeLst, gn);
+		    Lst_Append(nodeLst, gn);
 		}
 	    }
-	    Lst_Destroy(members, NULL);
-	    free(nameBuf);
+	    Lst_Free(members);
+	    Buf_Destroy(&nameBuf, TRUE);
 	} else {
-	    size_t	sz = strlen(libName) + strlen(memName) + 3;
-	    nameBuf = bmake_malloc(sz);
-	    snprintf(nameBuf, sz, "%s(%s)", libName, memName);
-	    gn = Targ_FindNode(nameBuf, TARG_CREATE);
-	    free(nameBuf);
+	    Buffer nameBuf;
+
+	    Buf_Init(&nameBuf, 0);
+	    Buf_AddStr(&nameBuf, libName);
+	    Buf_AddStr(&nameBuf, "(");
+	    Buf_AddStr(&nameBuf, memName);
+	    Buf_AddStr(&nameBuf, ")");
+
+	    gn = Targ_FindNode(Buf_GetAll(&nameBuf, NULL), TARG_CREATE);
+	    Buf_Destroy(&nameBuf, TRUE);
 	    if (gn == NULL) {
-		return FAILURE;
+		return FALSE;
 	    } else {
 		/*
 		 * We've found the node, but have to make sure the rest of the
@@ -493,7 +472,7 @@ Arch_ParseArchive(char **linePtr, Lst nodeLst, GNode *ctxt)
 		 * provided list.
 		 */
 		gn->type |= OP_ARCHV;
-		(void)Lst_AtEnd(nodeLst, gn);
+		Lst_Append(nodeLst, gn);
 	    }
 	}
 	if (doSubst) {
@@ -520,31 +499,15 @@ Arch_ParseArchive(char **linePtr, Lst nodeLst, GNode *ctxt)
     } while (*cp != '\0' && isspace ((unsigned char)*cp));
 
     *linePtr = cp;
-    return SUCCESS;
+    return TRUE;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * ArchFindArchive --
- *	See if the given archive is the one we are looking for. Called
- *	From ArchStatMember and ArchFindMember via Lst_Find.
- *
- * Input:
- *	ar		Current list element
- *	archName	Name we want
- *
- * Results:
- *	0 if it is, non-zero if it isn't.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-static int
-ArchFindArchive(const void *ar, const void *archName)
+/* See if the given archive is the one we are looking for.
+ * Called via Lst_Find. */
+static Boolean
+ArchFindArchive(const void *ar, const void *desiredName)
 {
-    return strcmp(archName, ((const Arch *)ar)->name);
+    return strcmp(((const Arch *)ar)->name, desiredName) == 0;
 }
 
 /*-
@@ -565,24 +528,20 @@ ArchFindArchive(const void *ar, const void *archName)
  *	archive members. This is mostly because we have no assurances that
  *	The archive will remain constant after we read all the headers, so
  *	there's not much point in remembering the position...
- *
- * Side Effects:
- *
  *-----------------------------------------------------------------------
  */
 static struct ar_hdr *
-ArchStatMember(char *archive, char *member, Boolean hash)
+ArchStatMember(const char *archive, const char *member, Boolean hash)
 {
     FILE *	  arch;	      /* Stream to archive */
-    int		  size;       /* Size of archive member */
-    char	  *cp;	      /* Useful character pointer */
+    size_t	  size;       /* Size of archive member */
     char	  magic[SARMAG];
     LstNode	  ln;	      /* Lst member containing archive descriptor */
     Arch	  *ar;	      /* Archive descriptor */
     Hash_Entry	  *he;	      /* Entry containing member's description */
     struct ar_hdr arh;        /* archive-member header for reading archive */
     char	  memName[MAXPATHLEN+1];
-    	    	    	    /* Current member name while hashing. */
+			    /* Current member name while hashing. */
 
     /*
      * Because of space constraints and similar things, files are archived
@@ -590,14 +549,14 @@ ArchStatMember(char *archive, char *member, Boolean hash)
      * to point 'member' to the final component, if there is one, to make
      * the comparisons easier...
      */
-    cp = strrchr(member, '/');
-    if (cp != NULL) {
-	member = cp + 1;
+    const char *base = strrchr(member, '/');
+    if (base != NULL) {
+	member = base + 1;
     }
 
-    ln = Lst_Find(archives, archive, ArchFindArchive);
+    ln = Lst_Find(archives, ArchFindArchive, archive);
     if (ln != NULL) {
-	ar = (Arch *)Lst_Datum(ln);
+	ar = LstNode_Datum(ln);
 
 	he = Hash_FindEntry(&ar->members, member);
 
@@ -610,8 +569,7 @@ ArchStatMember(char *archive, char *member, Boolean hash)
 
 	    if (len > AR_MAX_NAME_LEN) {
 		len = AR_MAX_NAME_LEN;
-		strncpy(copy, member, AR_MAX_NAME_LEN);
-		copy[AR_MAX_NAME_LEN] = '\0';
+		snprintf(copy, sizeof copy, "%s", member);
 	    }
 	    if ((he = Hash_FindEntry(&ar->members, copy)) != NULL)
 		return (struct ar_hdr *)Hash_GetValue(he);
@@ -653,7 +611,7 @@ ArchStatMember(char *archive, char *member, Boolean hash)
      * can handle...
      */
     if ((fread(magic, SARMAG, 1, arch) != 1) ||
-    	(strncmp(magic, ARMAG, SARMAG) != 0)) {
+	(strncmp(magic, ARMAG, SARMAG) != 0)) {
 	    fclose(arch);
 	    return NULL;
     }
@@ -673,6 +631,8 @@ ArchStatMember(char *archive, char *member, Boolean hash)
 	     */
 	    goto badarch;
 	} else {
+	    char *nameend;
+
 	    /*
 	     * We need to advance the stream's pointer to the start of the
 	     * next header. Files are padded with newlines to an even-byte
@@ -680,13 +640,14 @@ ArchStatMember(char *archive, char *member, Boolean hash)
 	     * 'size' field of the header and round it up during the seek.
 	     */
 	    arh.AR_SIZE[sizeof(arh.AR_SIZE)-1] = '\0';
-	    size = (int)strtol(arh.AR_SIZE, NULL, 10);
+	    size = (size_t)strtol(arh.ar_size, NULL, 10);
 
 	    memcpy(memName, arh.AR_NAME, sizeof(arh.AR_NAME));
-	    for (cp = &memName[AR_MAX_NAME_LEN]; *cp == ' '; cp--) {
-		continue;
+	    nameend = memName + AR_MAX_NAME_LEN;
+	    while (*nameend == ' ') {
+		nameend--;
 	    }
-	    cp[1] = '\0';
+	    nameend[1] = '\0';
 
 #ifdef SVR4ARCHIVES
 	    /*
@@ -706,8 +667,8 @@ ArchStatMember(char *archive, char *member, Boolean hash)
 		}
 	    }
 	    else {
-		if (cp[0] == '/')
-		    cp[0] = '\0';
+		if (nameend[0] == '/')
+		    nameend[0] = '\0';
 	    }
 #endif
 
@@ -719,11 +680,11 @@ ArchStatMember(char *archive, char *member, Boolean hash)
 	    if (strncmp(memName, AR_EFMT1, sizeof(AR_EFMT1) - 1) == 0 &&
 		isdigit((unsigned char)memName[sizeof(AR_EFMT1) - 1])) {
 
-		unsigned int elen = atoi(&memName[sizeof(AR_EFMT1)-1]);
+		int elen = atoi(&memName[sizeof(AR_EFMT1)-1]);
 
-		if (elen > MAXPATHLEN)
+		if ((unsigned int)elen > MAXPATHLEN)
 			goto badarch;
-		if (fread(memName, elen, 1, arch) != 1)
+		if (fread(memName, (size_t)elen, 1, arch) != 1)
 			goto badarch;
 		memName[elen] = '\0';
 		if (fseek(arch, -elen, SEEK_CUR) != 0)
@@ -738,13 +699,13 @@ ArchStatMember(char *archive, char *member, Boolean hash)
 	    Hash_SetValue(he, bmake_malloc(sizeof(struct ar_hdr)));
 	    memcpy(Hash_GetValue(he), &arh, sizeof(struct ar_hdr));
 	}
-	if (fseek(arch, (size + 1) & ~1, SEEK_CUR) != 0)
+	if (fseek(arch, ((long)size + 1) & ~1, SEEK_CUR) != 0)
 	    goto badarch;
     }
 
     fclose(arch);
 
-    (void)Lst_AtEnd(archives, ar);
+    Lst_Append(archives, ar);
 
     /*
      * Now that the archive has been read and cached, we can look into
@@ -774,17 +735,14 @@ badarch:
  *	If it is "//", then load the table of filenames
  *	If it is "/<offset>", then try to substitute the long file name
  *	from offset of a table previously read.
+ *	If a table is read, the file pointer is moved to the next archive
+ *	member.
  *
  * Results:
  *	-1: Bad data in archive
  *	 0: A table was loaded from the file
  *	 1: Name was successfully substituted from table
  *	 2: Name was not successfully substituted from table
- *
- * Side Effects:
- *	If a table is read, the file pointer is moved to the next archive
- *	member
- *
  *-----------------------------------------------------------------------
  */
 static int
@@ -834,7 +792,7 @@ ArchSVR4Entry(Arch *ar, char *name, size_t size, FILE *arch)
 	    }
 	if (DEBUG(ARCH)) {
 	    fprintf(debug_file, "Found svr4 archive name table with %lu entries\n",
-	            (unsigned long)entry);
+		    (unsigned long)entry);
 	}
 	return 0;
     }
@@ -861,8 +819,7 @@ ArchSVR4Entry(Arch *ar, char *name, size_t size, FILE *arch)
 	fprintf(debug_file, "Replaced %s with %s\n", name, &ar->fnametab[entry]);
     }
 
-    (void)strncpy(name, &ar->fnametab[entry], MAXPATHLEN);
-    name[MAXPATHLEN] = '\0';
+    snprintf(name, MAXPATHLEN + 1, "%s", &ar->fnametab[entry]);
     return 1;
 }
 #endif
@@ -874,6 +831,7 @@ ArchSVR4Entry(Arch *ar, char *name, size_t size, FILE *arch)
  *	Locate a member of an archive, given the path of the archive and
  *	the path of the desired member. If the archive is to be modified,
  *	the mode should be "r+", if not, it should be "r".
+ *	The passed struct ar_hdr structure is filled in.
  *
  * Input:
  *	archive		Path to the archive
@@ -886,21 +844,17 @@ ArchSVR4Entry(Arch *ar, char *name, size_t size, FILE *arch)
  *	An FILE *, opened for reading and writing, positioned at the
  *	start of the member's struct ar_hdr, or NULL if the member was
  *	nonexistent. The current struct ar_hdr for member.
- *
- * Side Effects:
- *	The passed struct ar_hdr structure is filled in.
- *
  *-----------------------------------------------------------------------
  */
 static FILE *
-ArchFindMember(char *archive, char *member, struct ar_hdr *arhPtr,
+ArchFindMember(const char *archive, const char *member, struct ar_hdr *arhPtr,
     const char *mode)
 {
     FILE *	  arch;	      /* Stream to archive */
     int		  size;       /* Size of archive member */
-    char	  *cp;	      /* Useful character pointer */
     char	  magic[SARMAG];
     size_t	  len, tlen;
+    const char *  base;
 
     arch = fopen(archive, mode);
     if (arch == NULL) {
@@ -912,7 +866,7 @@ ArchFindMember(char *archive, char *member, struct ar_hdr *arhPtr,
      * can handle...
      */
     if ((fread(magic, SARMAG, 1, arch) != 1) ||
-    	(strncmp(magic, ARMAG, SARMAG) != 0)) {
+	(strncmp(magic, ARMAG, SARMAG) != 0)) {
 	    fclose(arch);
 	    return NULL;
     }
@@ -923,9 +877,9 @@ ArchFindMember(char *archive, char *member, struct ar_hdr *arhPtr,
      * to point 'member' to the final component, if there is one, to make
      * the comparisons easier...
      */
-    cp = strrchr(member, '/');
-    if (cp != NULL) {
-	member = cp + 1;
+    base = strrchr(member, '/');
+    if (base != NULL) {
+	member = base + 1;
     }
     len = tlen = strlen(member);
     if (len > sizeof(arhPtr->AR_NAME)) {
@@ -958,7 +912,7 @@ ArchFindMember(char *archive, char *member, struct ar_hdr *arhPtr,
 		 * the file at the actual member, rather than its header, but
 		 * not here...
 		 */
-		if (fseek(arch, -sizeof(struct ar_hdr), SEEK_CUR) != 0) {
+		if (fseek(arch, -(long)sizeof(struct ar_hdr), SEEK_CUR) != 0) {
 		    fclose(arch);
 		    return NULL;
 		}
@@ -974,14 +928,14 @@ ArchFindMember(char *archive, char *member, struct ar_hdr *arhPtr,
 					sizeof(AR_EFMT1) - 1) == 0 &&
 		isdigit((unsigned char)arhPtr->AR_NAME[sizeof(AR_EFMT1) - 1])) {
 
-		unsigned int elen = atoi(&arhPtr->AR_NAME[sizeof(AR_EFMT1)-1]);
+		int elen = atoi(&arhPtr->AR_NAME[sizeof(AR_EFMT1)-1]);
 		char ename[MAXPATHLEN + 1];
 
-		if (elen > MAXPATHLEN) {
+		if ((unsigned int)elen > MAXPATHLEN) {
 			fclose(arch);
 			return NULL;
 		}
-		if (fread(ename, elen, 1, arch) != 1) {
+		if (fread(ename, (size_t)elen, 1, arch) != 1) {
 			fclose(arch);
 			return NULL;
 		}
@@ -991,7 +945,7 @@ ArchFindMember(char *archive, char *member, struct ar_hdr *arhPtr,
 		}
 		if (strncmp(ename, member, len) == 0) {
 			/* Found as extended name */
-			if (fseek(arch, -sizeof(struct ar_hdr) - elen,
+			if (fseek(arch, -(long)sizeof(struct ar_hdr) - elen,
 				SEEK_CUR) != 0) {
 			    fclose(arch);
 			    return NULL;
@@ -1035,18 +989,15 @@ skip:
  *-----------------------------------------------------------------------
  * Arch_Touch --
  *	Touch a member of an archive.
+ *	The modification time of the entire archive is also changed.
+ *	For a library, this could necessitate the re-ranlib'ing of the
+ *	whole thing.
  *
  * Input:
  *	gn		Node of member to touch
  *
  * Results:
  *	The 'time' field of the member's header is updated.
- *
- * Side Effects:
- *	The modification time of the entire archive is also changed.
- *	For a library, this could necessitate the re-ranlib'ing of the
- *	whole thing.
- *
  *-----------------------------------------------------------------------
  */
 void
@@ -1060,8 +1011,8 @@ Arch_Touch(GNode *gn)
 			  Var_Value(MEMBER, gn, &p2),
 			  &arh, "r+");
 
-    free(p1);
-    free(p2);
+    bmake_free(p1);
+    bmake_free(p2);
 
     snprintf(arh.AR_DATE, sizeof(arh.AR_DATE), "%-12ld", (long) now);
 
@@ -1071,37 +1022,24 @@ Arch_Touch(GNode *gn)
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Arch_TouchLib --
- *	Given a node which represents a library, touch the thing, making
- *	sure that the table of contents also is touched.
+/* Given a node which represents a library, touch the thing, making sure that
+ * the table of contents also is touched.
+ *
+ * Both the modification time of the library and of the RANLIBMAG member are
+ * set to 'now'.
  *
  * Input:
  *	gn		The node of the library to touch
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	Both the modification time of the library and of the RANLIBMAG
- *	member are set to 'now'.
- *
- *-----------------------------------------------------------------------
  */
 void
-#if !defined(RANLIBMAG)
-Arch_TouchLib(GNode *gn MAKE_ATTR_UNUSED)
-#else
 Arch_TouchLib(GNode *gn)
-#endif
 {
 #ifdef RANLIBMAG
     FILE *	    arch;	/* Stream open to archive */
     struct ar_hdr   arh;      	/* Header describing table of contents */
     struct utimbuf  times;	/* Times for utime() call */
 
-    arch = ArchFindMember(gn->path, UNCONST(RANLIBMAG), &arh, "r+");
+    arch = ArchFindMember(gn->path, RANLIBMAG, &arh, "r+");
     snprintf(arh.AR_DATE, sizeof(arh.AR_DATE), "%-12ld", (long) now);
 
     if (arch != NULL) {
@@ -1111,25 +1049,16 @@ Arch_TouchLib(GNode *gn)
 	times.actime = times.modtime = now;
 	utime(gn->path, &times);
     }
+#else
+    (void)gn;
 #endif
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Arch_MTime --
- *	Return the modification time of a member of an archive.
+/* Return the modification time of a member of an archive. The mtime field
+ * of the given node is filled in with the value returned by the function.
  *
  * Input:
  *	gn		Node describing archive member
- *
- * Results:
- *	The modification time(seconds).
- *
- * Side Effects:
- *	The mtime field of the given node is filled in with the value
- *	returned by the function.
- *
- *-----------------------------------------------------------------------
  */
 time_t
 Arch_MTime(GNode *gn)
@@ -1142,8 +1071,8 @@ Arch_MTime(GNode *gn)
 			     Var_Value(MEMBER, gn, &p2),
 			     TRUE);
 
-    free(p1);
-    free(p2);
+    bmake_free(p1);
+    bmake_free(p2);
 
     if (arhPtr != NULL) {
 	modTime = (time_t)strtol(arhPtr->AR_DATE, NULL, 10);
@@ -1155,34 +1084,17 @@ Arch_MTime(GNode *gn)
     return modTime;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Arch_MemMTime --
- *	Given a non-existent archive member's node, get its modification
- *	time from its archived form, if it exists.
- *
- * Results:
- *	The modification time.
- *
- * Side Effects:
- *	The mtime field is filled in.
- *
- *-----------------------------------------------------------------------
- */
+/* Given a non-existent archive member's node, get its modification time from
+ * its archived form, if it exists. gn->mtime is filled in as well. */
 time_t
 Arch_MemMTime(GNode *gn)
 {
     LstNode 	  ln;
     GNode   	  *pgn;
-    char    	  *nameStart,
-		  *nameEnd;
 
-    if (Lst_Open(gn->parents) != SUCCESS) {
-	gn->mtime = 0;
-	return 0;
-    }
+    Lst_Open(gn->parents);
     while ((ln = Lst_Next(gn->parents)) != NULL) {
-	pgn = (GNode *)Lst_Datum(ln);
+	pgn = LstNode_Datum(ln);
 
 	if (pgn->type & OP_ARCHV) {
 	    /*
@@ -1192,12 +1104,13 @@ Arch_MemMTime(GNode *gn)
 	     * child. We keep searching its parents in case some other
 	     * parent requires this child to exist...
 	     */
-	    nameStart = strchr(pgn->name, '(') + 1;
-	    nameEnd = strchr(nameStart, ')');
+	    const char *nameStart = strchr(pgn->name, '(') + 1;
+	    const char *nameEnd = strchr(nameStart, ')');
+	    size_t nameLen = (size_t)(nameEnd - nameStart);
 
 	    if ((pgn->flags & REMAKE) &&
-		strncmp(nameStart, gn->name, nameEnd - nameStart) == 0) {
-				     gn->mtime = Arch_MTime(pgn);
+		strncmp(nameStart, gn->name, nameLen) == 0) {
+		gn->mtime = Arch_MTime(pgn);
 	    }
 	} else if (pgn->flags & REMAKE) {
 	    /*
@@ -1214,29 +1127,20 @@ Arch_MemMTime(GNode *gn)
     return gn->mtime;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Arch_FindLib --
- *	Search for a library along the given search path.
+/* Search for a library along the given search path.
+ *
+ * The node's 'path' field is set to the found path (including the
+ * actual file name, not -l...). If the system can handle the -L
+ * flag when linking (or we cannot find the library), we assume that
+ * the user has placed the .LIBS variable in the final linking
+ * command (or the linker will know where to find it) and set the
+ * TARGET variable for this node to be the node's name. Otherwise,
+ * we set the TARGET variable to be the full path of the library,
+ * as returned by Dir_FindFile.
  *
  * Input:
  *	gn		Node of library to find
  *	path		Search path
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	The node's 'path' field is set to the found path (including the
- *	actual file name, not -l...). If the system can handle the -L
- *	flag when linking (or we cannot find the library), we assume that
- *	the user has placed the .LIBRARIES variable in the final linking
- *	command (or the linker will know where to find it) and set the
- *	TARGET variable for this node to be the node's name. Otherwise,
- *	we set the TARGET variable to be the full path of the library,
- *	as returned by Dir_FindFile.
- *
- *-----------------------------------------------------------------------
  */
 void
 Arch_FindLib(GNode *gn, Lst path)
@@ -1258,44 +1162,38 @@ Arch_FindLib(GNode *gn, Lst path)
 #endif /* LIBRARIES */
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Arch_LibOODate --
- *	Decide if a node with the OP_LIB attribute is out-of-date. Called
- *	from Make_OODate to make its life easier.
+/* Decide if a node with the OP_LIB attribute is out-of-date. Called from
+ * Make_OODate to make its life easier.
+ * The library will be hashed if it hasn't been already.
  *
- *	There are several ways for a library to be out-of-date that are
- *	not available to ordinary files. In addition, there are ways
- *	that are open to regular files that are not available to
- *	libraries. A library that is only used as a source is never
- *	considered out-of-date by itself. This does not preclude the
- *	library's modification time from making its parent be out-of-date.
- *	A library will be considered out-of-date for any of these reasons,
- *	given that it is a target on a dependency line somewhere:
- *	    Its modification time is less than that of one of its
- *	    	  sources (gn->mtime < gn->cmgn->mtime).
- *	    Its modification time is greater than the time at which the
- *	    	  make began (i.e. it's been modified in the course
- *	    	  of the make, probably by archiving).
- *	    The modification time of one of its sources is greater than
- *		  the one of its RANLIBMAG member (i.e. its table of contents
- *	    	  is out-of-date). We don't compare of the archive time
- *		  vs. TOC time because they can be too close. In my
- *		  opinion we should not bother with the TOC at all since
- *		  this is used by 'ar' rules that affect the data contents
- *		  of the archive, not by ranlib rules, which affect the
- *		  TOC.
+ * There are several ways for a library to be out-of-date that are
+ * not available to ordinary files. In addition, there are ways
+ * that are open to regular files that are not available to
+ * libraries. A library that is only used as a source is never
+ * considered out-of-date by itself. This does not preclude the
+ * library's modification time from making its parent be out-of-date.
+ * A library will be considered out-of-date for any of these reasons,
+ * given that it is a target on a dependency line somewhere:
+ *
+ *	Its modification time is less than that of one of its sources
+ *	(gn->mtime < gn->cmgn->mtime).
+ *
+ *	Its modification time is greater than the time at which the make
+ *	began (i.e. it's been modified in the course of the make, probably
+ *	by archiving).
+ *
+ *	The modification time of one of its sources is greater than the one
+ *	of its RANLIBMAG member (i.e. its table of contents is out-of-date).
+ *	We don't compare of the archive time vs. TOC time because they can be
+ *	too close. In my opinion we should not bother with the TOC at all
+ *	since this is used by 'ar' rules that affect the data contents of the
+ *	archive, not by ranlib rules, which affect the TOC.
  *
  * Input:
  *	gn		The library's graph node
  *
  * Results:
  *	TRUE if the library is out-of-date. FALSE otherwise.
- *
- * Side Effects:
- *	The library will be hashed if it hasn't been already.
- *
- *-----------------------------------------------------------------------
  */
 Boolean
 Arch_LibOODate(GNode *gn)
@@ -1315,7 +1213,7 @@ Arch_LibOODate(GNode *gn)
 	struct ar_hdr  	*arhPtr;    /* Header for __.SYMDEF */
 	int 	  	modTimeTOC; /* The table-of-contents's mod time */
 
-	arhPtr = ArchStatMember(gn->path, UNCONST(RANLIBMAG), FALSE);
+	arhPtr = ArchStatMember(gn->path, RANLIBMAG, FALSE);
 
 	if (arhPtr != NULL) {
 	    modTimeTOC = (int)strtol(arhPtr->AR_DATE, NULL, 10);
@@ -1340,40 +1238,14 @@ Arch_LibOODate(GNode *gn)
     return oodate;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Arch_Init --
- *	Initialize things for this module.
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	The 'archives' list is initialized.
- *
- *-----------------------------------------------------------------------
- */
+/* Initialize things for this module. */
 void
 Arch_Init(void)
 {
-    archives = Lst_Init(FALSE);
+    archives = Lst_Init();
 }
 
-
-
-/*-
- *-----------------------------------------------------------------------
- * Arch_End --
- *	Cleanup things for this module.
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	The 'archives' list is freed
- *
- *-----------------------------------------------------------------------
- */
+/* Clean up things for this module. */
 void
 Arch_End(void)
 {
@@ -1382,35 +1254,22 @@ Arch_End(void)
 #endif
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Arch_IsLib --
- *	Check if the node is a library
- *
- * Results:
- *	True or False.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-int
+Boolean
 Arch_IsLib(GNode *gn)
 {
     static const char armag[] = "!<arch>\n";
-    char buf[sizeof(armag)-1];
+    char buf[sizeof armag - 1];
     int fd;
 
     if ((fd = open(gn->path, O_RDONLY)) == -1)
 	return FALSE;
 
-    if (read(fd, buf, sizeof(buf)) != sizeof(buf)) {
+    if (read(fd, buf, sizeof buf) != sizeof buf) {
 	(void)close(fd);
 	return FALSE;
     }
 
     (void)close(fd);
 
-    return memcmp(buf, armag, sizeof(buf)) == 0;
+    return memcmp(buf, armag, sizeof buf) == 0;
 }
diff --git a/bmake.1 b/bmake.1
index c468cfe053d7..2cc452f2d171 100644
--- a/bmake.1
+++ b/bmake.1
@@ -1,4 +1,4 @@
-.\"	$NetBSD: make.1,v 1.282 2020/06/06 20:28:42 wiz Exp $
+.\"	$NetBSD: make.1,v 1.289 2020/08/28 17:15:04 rillig Exp $
 .\"
 .\" Copyright (c) 1990, 1993
 .\"	The Regents of the University of California.  All rights reserved.
@@ -29,7 +29,7 @@
 .\"
 .\"	from: @(#)make.1	8.4 (Berkeley) 3/19/94
 .\"
-.Dd June 5, 2020
+.Dd August 28, 2020
 .Dt BMAKE 1
 .Os
 .Sh NAME
@@ -166,8 +166,15 @@ Print the input graph after making everything, or before exiting
 on error.
 .It Ar "g3"
 Print the input graph before exiting on error.
+.It Ar h
+Print debugging information about hash table operations.
 .It Ar j
 Print debugging information about running multiple shells.
+.It Ar L
+Turn on lint checks.
+This will throw errors for variable assignments that do not parse
+correctly, at the time of assignment so the file and line number
+are available.
 .It Ar l
 Print commands in Makefiles regardless of whether or not they are prefixed by
 .Ql @
@@ -311,7 +318,8 @@ as an argument).
 .It Fl n
 Display the commands that would have been executed, but do not
 actually execute them unless the target depends on the .MAKE special
-source (see below).
+source (see below) or the command is prefixed with
+.Ql Ic + .
 .It Fl N
 Display the commands which would have been executed, but do not
 actually execute any of them; useful for debugging top-level makefiles
@@ -412,37 +420,44 @@ or more sources.
 This creates a relationship where the targets
 .Dq depend
 on the sources
-and are usually created from them.
-The exact relationship between the target and the source is determined
-by the operator that separates them.
-The three operators are as follows:
+and are customarily created from them.
+A target is considered out-of-date if it does not exist, or if its
+modification time is less than that of any of its sources.
+An out-of-date target will be re-created, but not until all sources
+have been examined and themselves re-created as needed.
+Three operators may be used:
 .Bl -tag -width flag
 .It Ic \&:
-A target is considered out-of-date if its modification time is less than
-those of any of its sources.
-Sources for a target accumulate over dependency lines when this operator
-is used.
-The target is removed if
+Many dependency lines may name this target but only one may have
+attached shell commands.
+All sources named in all dependency lines are considered together,
+and if needed the attached shell commands are run to create or
+re-create the target.
+If
 .Nm
-is interrupted.
+is interrupted, the target is removed.
 .It Ic \&!
-Targets are always re-created, but not until all sources have been
-examined and re-created as necessary.
-Sources for a target accumulate over dependency lines when this operator
-is used.
-The target is removed if
-.Nm
-is interrupted.
+The same, but the target is always re-created whether or not it is out
+of date.
 .It Ic \&::
-If no sources are specified, the target is always re-created.
-Otherwise, a target is considered out-of-date if any of its sources has
-been modified more recently than the target.
-Sources for a target do not accumulate over dependency lines when this
-operator is used.
-The target will not be removed if
+Any dependency line may have attached shell commands, but each one
+is handled independently: its sources are considered and the attached
+shell commands are run if the target is out of date with respect to
+(only) those sources.
+Thus, different groups of the attached shell commands may be run
+depending on the circumstances.
+Furthermore, unlike
+.Ic \&:,
+for dependency lines with no sources, the attached shell
+commands are always run.
+Also unlike
+.Ic \&:,
+the target will not be removed if
 .Nm
 is interrupted.
 .El
+All dependency lines mentioning a particular target must use the same
+operator.
 .Pp
 Targets and sources may contain the shell wildcard values
 .Ql \&? ,
@@ -608,7 +623,7 @@ This shorter form is not recommended.
 .Pp
 If the variable name contains a dollar, then the name itself is expanded first.
 This allows almost arbitrary variable names, however names containing dollar,
-braces, parenthesis, or whitespace are really best avoided!
+braces, parentheses, or whitespace are really best avoided!
 .Pp
 If the result of expanding a variable contains a dollar sign
 .Pq Ql \&$
@@ -1126,6 +1141,9 @@ is set to the value of
 for all programs which
 .Nm
 executes.
+.It Ev .SHELL
+The pathname of the shell used to run target scripts.
+It is read-only.
 .It Ev .TARGETS
 The list of targets explicitly specified on the command line, if any.
 .It Ev VPATH
@@ -1171,7 +1189,7 @@ Replaces each word in the variable with its suffix.
 .It Cm \&:H
 Replaces each word in the variable with everything but the last component.
 .It Cm \&:M Ns Ar pattern
-Select only those words that match
+Selects only those words that match
 .Ar pattern .
 The standard shell wildcard characters
 .Pf ( Ql * ,
@@ -1195,11 +1213,11 @@ This is identical to
 but selects all words which do not match
 .Ar pattern .
 .It Cm \&:O
-Order every word in variable alphabetically.
+Orders every word in variable alphabetically.
 .It Cm \&:Or
-Order every word in variable in reverse alphabetical order.
+Orders every word in variable in reverse alphabetical order.
 .It Cm \&:Ox
-Randomize words in variable.
+Shuffles the words in variable.
 The results will be different each time you are referring to the
 modified variable; use the assignment with expansion
 .Pq Ql Cm \&:=
@@ -1249,7 +1267,7 @@ If a
 .Va utc
 value is not provided or is 0, the current time is used.
 .It Cm \&:hash
-Compute a 32-bit hash of the value and encode it as hex digits.
+Computes a 32-bit hash of the value and encode it as hex digits.
 .It Cm \&:localtime[=utc]
 The value is a format string for
 .Xr strftime 3 ,
@@ -1259,7 +1277,7 @@ If a
 .Va utc
 value is not provided or is 0, the current time is used.
 .It Cm \&:tA
-Attempt to convert variable to an absolute path using
+Attempts to convert variable to an absolute path using
 .Xr realpath 3 ,
 if that fails, the value is unchanged.
 .It Cm \&:tl
@@ -1271,7 +1289,7 @@ This modifier sets the separator to the character
 If
 .Ar c
 is omitted, then no separator is used.
-The common escapes (including octal numeric codes), work as expected.
+The common escapes (including octal numeric codes) work as expected.
 .It Cm \&:tu
 Converts variable to upper-case letters.
 .It Cm \&:tW
@@ -1287,21 +1305,21 @@ See also
 .Sm off
 .It Cm \&:S No \&/ Ar old_string No \&/ Ar new_string No \&/ Op Cm 1gW
 .Sm on
-Modify the first occurrence of
+Modifies the first occurrence of
 .Ar old_string
-in the variable's value, replacing it with
+in each word of the variable's value, replacing it with
 .Ar new_string .
 If a
 .Ql g
-is appended to the last slash of the pattern, all occurrences
+is appended to the last delimiter of the pattern, all occurrences
 in each word are replaced.
 If a
 .Ql 1
-is appended to the last slash of the pattern, only the first word
+is appended to the last delimiter of the pattern, only the first occurrence
 is affected.
 If a
 .Ql W
-is appended to the last slash of the pattern,
+is appended to the last delimiter of the pattern,
 then the value is treated as a single word
 (possibly containing embedded white space).
 If
@@ -1370,13 +1388,6 @@ as occur in the word or words it is found in; the
 .Ql W
 modifier causes the value to be treated as a single word
 (possibly containing embedded white space).
-Note that
-.Ql 1
-and
-.Ql g
-are orthogonal; the former specifies whether multiple words are
-potentially affected, the latter whether multiple substitutions can
-potentially occur within each affected word.
 .Pp
 As for the
 .Cm \&:S
@@ -1387,9 +1398,9 @@ and
 are subjected to variable expansion before being parsed as
 regular expressions.
 .It Cm \&:T
-Replaces each word in the variable with its last component.
+Replaces each word in the variable with its last path component.
 .It Cm \&:u
-Remove adjacent duplicate words (like
+Removes adjacent duplicate words (like
 .Xr uniq 1 ) .
 .Sm off
 .It Cm \&:\&? Ar true_string Cm \&: Ar false_string
@@ -1405,7 +1416,7 @@ usually contain variable expansions.
 A common error is trying to use expressions like
 .Dl ${NUMBERS:M42:?match:no}
 which actually tests defined(NUMBERS),
-to determine is any words match "42" you need to use something like:
+to determine if any words match "42" you need to use something like:
 .Dl ${"${NUMBERS:M42}" != \&"\&":?match:no} .
 .It Ar :old_string=new_string
 This is the
@@ -1449,7 +1460,7 @@ in either the
 or
 .Ar old_string ,
 only the first instance is treated specially (as the pattern character);
-all subsequent instances are treated as regular characters
+all subsequent instances are treated as regular characters.
 .Pp
 Variable expansion occurs in the normal fashion inside both
 .Ar old_string
@@ -1466,11 +1477,10 @@ This is the loop expansion mechanism from the OSF Development
 Environment (ODE) make.
 Unlike
 .Cm \&.for
-loops expansion occurs at the time of
-reference.
-Assign
+loops, expansion occurs at the time of reference.
+Assigns
 .Ar temp
-to each word in the variable and evaluate
+to each word in the variable and evaluates
 .Ar string .
 The ODE convention is that
 .Ar temp
@@ -1481,7 +1491,7 @@ For example.
 However a single character variable is often more readable:
 .Dl ${MAKE_PRINT_VAR_ON_ERROR:@v@$v='${$v}'${.newline}@}
 .It Cm \&:_[=var]
-Save the current variable value in
+Saves the current variable value in
 .Ql $_
 or the named
 .Va var
@@ -1502,7 +1512,7 @@ is used to save the result of the
 modifier which is later referenced using the index values from
 .Ql :range .
 .It Cm \&:U Ns Ar newval
-If the variable is undefined
+If the variable is undefined,
 .Ar newval
 is the value.
 If the variable is defined, the existing value is returned.
@@ -1512,7 +1522,7 @@ It is handy for setting per-target CFLAGS for instance:
 If a value is only required if the variable is undefined, use:
 .Dl ${VAR:D:Unewval}
 .It Cm \&:D Ns Ar newval
-If the variable is defined
+If the variable is defined,
 .Ar newval
 is the value.
 .It Cm \&:L
@@ -1641,7 +1651,7 @@ Returns the number of words in the value.
 .El \" :[range]
 .El
 .Sh INCLUDE STATEMENTS, CONDITIONALS AND FOR LOOPS
-Makefile inclusion, conditional structures and for loops  reminiscent
+Makefile inclusion, conditional structures and for loops reminiscent
 of the C programming language are provided in
 .Nm .
 All such structures are identified by a line beginning with a single
@@ -1687,7 +1697,7 @@ The possible conditionals are as follows:
 The message is printed along with the name of the makefile and line number,
 then
 .Nm
-will exit.
+will exit immediately.
 .It Ic .export Ar variable ...
 Export the specified global variable.
 If no variable list is provided, all globals are exported
@@ -1876,7 +1886,7 @@ operator is not an integral value, then
 string comparison is performed between the expanded
 variables.
 If no relational operator is given, it is assumed that the expanded
-variable is being compared against 0 or an empty string in the case
+variable is being compared against 0, or an empty string in the case
 of a string comparison.
 .Pp
 When
@@ -1917,7 +1927,7 @@ The syntax of a for loop is:
 .Pp
 .Bl -tag -compact -width Ds
 .It Ic \&.for Ar variable Oo Ar variable ... Oc Ic in Ar expression
-.It Aq make-rules
+.It Aq make-lines
 .It Ic \&.endfor
 .El
 .Pp
@@ -1929,7 +1939,7 @@ On each iteration of the loop, one word is taken and assigned to each
 in order, and these
 .Ic variables
 are substituted into the
-.Ic make-rules
+.Ic make-lines
 inside the body of the for loop.
 The number of words must come out even; that is, if there are three
 iteration variables, the number of words provided must be a multiple
diff --git a/bmake.cat1 b/bmake.cat1
index feb93698e34f..564e811737da 100644
--- a/bmake.cat1
+++ b/bmake.cat1
@@ -89,9 +89,15 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
 
              g3      Print the input graph before exiting on error.
 
+             h       Print debugging information about hash table operations.
+
              j       Print debugging information about running multiple
                      shells.
 
+             L       Turn on lint checks.  This will throw errors for variable
+                     assignments that do not parse correctly, at the time of
+                     assignment so the file and line number are available.
+
              l       Print commands in Makefiles regardless of whether or not
                      they are prefixed by `@' or other "quiet" flags.  Also
                      known as "loud" behavior.
@@ -188,7 +194,7 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
 
      -n      Display the commands that would have been executed, but do not
              actually execute them unless the target depends on the .MAKE spe-
-             cial source (see below).
+             cial source (see below) or the command is prefixed with `+'.
 
      -N      Display the commands which would have been executed, but do not
              actually execute any of them; useful for debugging top-level
@@ -260,25 +266,31 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
 FILE DEPENDENCY SPECIFICATIONS
      Dependency lines consist of one or more targets, an operator, and zero or
      more sources.  This creates a relationship where the targets ``depend''
-     on the sources and are usually created from them.  The exact relationship
-     between the target and the source is determined by the operator that sep-
-     arates them.  The three operators are as follows:
+     on the sources and are customarily created from them.  A target is con-
+     sidered out-of-date if it does not exist, or if its modification time is
+     less than that of any of its sources.  An out-of-date target will be re-
+     created, but not until all sources have been examined and themselves re-
+     created as needed.  Three operators may be used:
 
-     :     A target is considered out-of-date if its modification time is less
-           than those of any of its sources.  Sources for a target accumulate
-           over dependency lines when this operator is used.  The target is
-           removed if bmake is interrupted.
+     :     Many dependency lines may name this target but only one may have
+           attached shell commands.  All sources named in all dependency lines
+           are considered together, and if needed the attached shell commands
+           are run to create or re-create the target.  If bmake is inter-
+           rupted, the target is removed.
 
-     !     Targets are always re-created, but not until all sources have been
-           examined and re-created as necessary.  Sources for a target accumu-
-           late over dependency lines when this operator is used.  The target
-           is removed if bmake is interrupted.
+     !     The same, but the target is always re-created whether or not it is
+           out of date.
 
-     ::    If no sources are specified, the target is always re-created.  Oth-
-           erwise, a target is considered out-of-date if any of its sources
-           has been modified more recently than the target.  Sources for a
-           target do not accumulate over dependency lines when this operator
-           is used.  The target will not be removed if bmake is interrupted.
+     ::    Any dependency line may have attached shell commands, but each one
+           is handled independently: its sources are considered and the
+           attached shell commands are run if the target is out of date with
+           respect to (only) those sources.  Thus, different groups of the
+           attached shell commands may be run depending on the circumstances.
+           Furthermore, unlike :, for dependency lines with no sources, the
+           attached shell commands are always run.  Also unlike :, the target
+           will not be removed if bmake is interrupted.
+     All dependency lines mentioning a particular target must use the same
+     operator.
 
      Targets and sources may contain the shell wildcard values `?', `*', `[]',
      and `{}'.  The values `?', `*', and `[]' may only be used as part of the
@@ -374,7 +386,7 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
 
      If the variable name contains a dollar, then the name itself is expanded
      first.  This allows almost arbitrary variable names, however names con-
-     taining dollar, braces, parenthesis, or whitespace are really best
+     taining dollar, braces, parentheses, or whitespace are really best
      avoided!
 
      If the result of expanding a variable contains a dollar sign (`$') the
@@ -729,6 +741,9 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
                      contains a variable transform.  `PWD' is set to the value
                      of `.OBJDIR' for all programs which bmake executes.
 
+     .SHELL          The pathname of the shell used to run target scripts.  It
+                     is read-only.
+
      .TARGETS        The list of targets explicitly specified on the command
                      line, if any.
 
@@ -765,7 +780,7 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
           ponent.
 
      :Mpattern
-          Select only those words that match pattern.  The standard shell
+          Selects only those words that match pattern.  The standard shell
           wildcard characters (`*', `?', and `[]') may be used.  The wildcard
           characters may be escaped with a backslash (`\').  As a consequence
           of the way values are split into words, matched, and then joined, a
@@ -779,11 +794,11 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
           This is identical to `:M', but selects all words which do not match
           pattern.
 
-     :O   Order every word in variable alphabetically.
+     :O   Orders every word in variable alphabetically.
 
-     :Or  Order every word in variable in reverse alphabetical order.
+     :Or  Orders every word in variable in reverse alphabetical order.
 
-     :Ox  Randomize words in variable.  The results will be different each
+     :Ox  Shuffles the words in variable.  The results will be different each
           time you are referring to the modified variable; use the assignment
           with expansion (`:=') to prevent such behavior.  For example,
 
@@ -821,13 +836,13 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
           utc value is not provided or is 0, the current time is used.
 
      :hash
-          Compute a 32-bit hash of the value and encode it as hex digits.
+          Computes a 32-bit hash of the value and encode it as hex digits.
 
      :localtime[=utc]
           The value is a format string for strftime(3), using localtime(3).
           If a utc value is not provided or is 0, the current time is used.
 
-     :tA  Attempt to convert variable to an absolute path using realpath(3),
+     :tA  Attempts to convert variable to an absolute path using realpath(3),
           if that fails, the value is unchanged.
 
      :tl  Converts variable to lower-case letters.
@@ -836,7 +851,7 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
           Words in the variable are normally separated by a space on expan-
           sion.  This modifier sets the separator to the character c.  If c is
           omitted, then no separator is used.  The common escapes (including
-          octal numeric codes), work as expected.
+          octal numeric codes) work as expected.
 
      :tu  Converts variable to upper-case letters.
 
@@ -847,20 +862,20 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
           white space.  See also `:[@]'.
 
      :S/old_string/new_string/[1gW]
-          Modify the first occurrence of old_string in the variable's value,
-          replacing it with new_string.  If a `g' is appended to the last
-          slash of the pattern, all occurrences in each word are replaced.  If
-          a `1' is appended to the last slash of the pattern, only the first
-          word is affected.  If a `W' is appended to the last slash of the
-          pattern, then the value is treated as a single word (possibly con-
-          taining embedded white space).  If old_string begins with a caret
-          (`^'), old_string is anchored at the beginning of each word.  If
-          old_string ends with a dollar sign (`$'), it is anchored at the end
-          of each word.  Inside new_string, an ampersand (`&') is replaced by
-          old_string (without any `^' or `$').  Any character may be used as a
-          delimiter for the parts of the modifier string.  The anchoring,
-          ampersand and delimiter characters may be escaped with a backslash
-          (`\').
+          Modifies the first occurrence of old_string in each word of the
+          variable's value, replacing it with new_string.  If a `g' is
+          appended to the last delimiter of the pattern, all occurrences in
+          each word are replaced.  If a `1' is appended to the last delimiter
+          of the pattern, only the first occurrence is affected.  If a `W' is
+          appended to the last delimiter of the pattern, then the value is
+          treated as a single word (possibly containing embedded white space).
+          If old_string begins with a caret (`^'), old_string is anchored at
+          the beginning of each word.  If old_string ends with a dollar sign
+          (`$'), it is anchored at the end of each word.  Inside new_string,
+          an ampersand (`&') is replaced by old_string (without any `^' or
+          `$').  Any character may be used as a delimiter for the parts of the
+          modifier string.  The anchoring, ampersand and delimiter characters
+          may be escaped with a backslash (`\').
 
           Variable expansion occurs in the normal fashion inside both
           old_string and new_string with the single exception that a backslash
@@ -878,16 +893,13 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
           instances of the search pattern pattern as occur in the word or
           words it is found in; the `W' modifier causes the value to be
           treated as a single word (possibly containing embedded white space).
-          Note that `1' and `g' are orthogonal; the former specifies whether
-          multiple words are potentially affected, the latter whether multiple
-          substitutions can potentially occur within each affected word.
 
           As for the :S modifier, the pattern and replacement are subjected to
           variable expansion before being parsed as regular expressions.
 
-     :T   Replaces each word in the variable with its last component.
+     :T   Replaces each word in the variable with its last path component.
 
-     :u   Remove adjacent duplicate words (like uniq(1)).
+     :u   Removes adjacent duplicate words (like uniq(1)).
 
      :?true_string:false_string
           If the variable name (not its value), when parsed as a .if condi-
@@ -898,7 +910,7 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
           variable expansions.  A common error is trying to use expressions
           like
                 ${NUMBERS:M42:?match:no}
-          which actually tests defined(NUMBERS), to determine is any words
+          which actually tests defined(NUMBERS), to determine if any words
           match "42" you need to use something like:
                 ${"${NUMBERS:M42}" != "":?match:no}.
 
@@ -916,7 +928,7 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
           pattern matching character (%) in either the new_string or
           old_string, only the first instance is treated specially (as the
           pattern character); all subsequent instances are treated as regular
-          characters
+          characters.
 
           Variable expansion occurs in the normal fashion inside both
           old_string and new_string with the single exception that a backslash
@@ -925,17 +937,17 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
 
      :@temp@string@
           This is the loop expansion mechanism from the OSF Development Envi-
-          ronment (ODE) make.  Unlike .for loops expansion occurs at the time
-          of reference.  Assign temp to each word in the variable and evaluate
-          string.  The ODE convention is that temp should start and end with a
-          period.  For example.
+          ronment (ODE) make.  Unlike .for loops, expansion occurs at the time
+          of reference.  Assigns temp to each word in the variable and evalu-
+          ates string.  The ODE convention is that temp should start and end
+          with a period.  For example.
                 ${LINKS:@.LINK.@${LN} ${TARGET} ${.LINK.}@}
 
           However a single character variable is often more readable:
                 ${MAKE_PRINT_VAR_ON_ERROR:@v@$v='${$v}'${.newline}@}
 
      :_[=var]
-          Save the current variable value in `$_' or the named var for later
+          Saves the current variable value in `$_' or the named var for later
           reference.  Example usage:
 
                 M_cmpv.units = 1 1000 1000000
@@ -948,7 +960,7 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
           later referenced using the index values from `:range'.
 
      :Unewval
-          If the variable is undefined newval is the value.  If the variable
+          If the variable is undefined, newval is the value.  If the variable
           is defined, the existing value is returned.  This is another ODE
           make feature.  It is handy for setting per-target CFLAGS for
           instance:
@@ -957,7 +969,7 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
                 ${VAR:D:Unewval}
 
      :Dnewval
-          If the variable is defined newval is the value.
+          If the variable is defined, newval is the value.
 
      :L   The name of the variable is the value.
 
@@ -1033,7 +1045,7 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
           #      Returns the number of words in the value.
 
 INCLUDE STATEMENTS, CONDITIONALS AND FOR LOOPS
-     Makefile inclusion, conditional structures and for loops  reminiscent of
+     Makefile inclusion, conditional structures and for loops reminiscent of
      the C programming language are provided in bmake.  All such structures
      are identified by a line beginning with a single dot (`.') character.
      Files are included with either .include <file> or .include "file".  Vari-
@@ -1057,7 +1069,7 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
 
      .error message
              The message is printed along with the name of the makefile and
-             line number, then bmake will exit.
+             line number, then bmake will exit immediately.
 
      .export variable ...
              Export the specified global variable.  If no variable list is
@@ -1191,7 +1203,7 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
      variable expansion, either the left or right hand side of a `==' or `!='
      operator is not an integral value, then string comparison is performed
      between the expanded variables.  If no relational operator is given, it
-     is assumed that the expanded variable is being compared against 0 or an
+     is assumed that the expanded variable is being compared against 0, or an
      empty string in the case of a string comparison.
 
      When bmake is evaluating one of these conditional expressions, and it
@@ -1210,12 +1222,12 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
      The syntax of a for loop is:
 
      .for variable [variable ...] in expression
-     <make-rules>
+     <make-lines>
      .endfor
 
      After the for expression is evaluated, it is split into words.  On each
      iteration of the loop, one word is taken and assigned to each variable,
-     in order, and these variables are substituted into the make-rules inside
+     in order, and these variables are substituted into the make-lines inside
      the body of the for loop.  The number of words must come out even; that
      is, if there are three iteration variables, the number of words provided
      must be a multiple of three.
@@ -1556,4 +1568,4 @@ BMAKE(1)                FreeBSD General Commands Manual               BMAKE(1)
 
      There is no way of escaping a space character in a filename.
 
-FreeBSD 11.3                     June 5, 2020                     FreeBSD 11.3
+FreeBSD 11.3                    August 28, 2020                   FreeBSD 11.3
diff --git a/bsd.after-import.mk b/bsd.after-import.mk
index 23f30aa6c638..0d48f3c26648 100644
--- a/bsd.after-import.mk
+++ b/bsd.after-import.mk
@@ -1,4 +1,4 @@
-# $Id: bsd.after-import.mk,v 1.15 2018/12/30 17:14:24 sjg Exp $
+# $Id: bsd.after-import.mk,v 1.16 2020/07/12 03:39:01 sjg Exp $
 
 # This makefile is for use when integrating bmake into a BSD build
 # system.  Use this makefile after importing bmake.
@@ -9,7 +9,7 @@
 # The goal is to allow the benefits of autoconf without
 # the overhead of running configure.
 
-all: _makefile
+all: _makefile _utmakefile
 all: after-import
 
 # we rely on bmake
@@ -37,7 +37,9 @@ SRCTOP := ${srctop}
 .endif
 
 # This lets us match what boot-strap does
-.if !defined(HOST_OS)
+.if defined(.MAKE.OS)
+HOST_OS:= ${.MAKE.OS}
+.elif !defined(HOST_OS)
 HOST_OS!= uname
 .endif
 
@@ -107,5 +109,18 @@ _makefile:	bootstrap ${MAKEFILE}
 	@cmp -s ${.TARGET} ${.CURDIR}/Makefile || \
 	    mv ${.TARGET} ${.CURDIR}/Makefile
 
+_utmakefile: bootstrap ${MAKEFILE}
+	@echo Generating ${.CURDIR}/unit-tests/Makefile
+	@mkdir -p ${.CURDIR}/unit-tests
+	@(echo '# This is a generated file, do NOT edit!'; \
+	echo '# See ${_this:S,${SRCTOP}/,,}'; \
+	echo '#'; echo '# $$${HOST_OS}$$'; \
+	${MAKEFILE_SED} \
+	-e '/^UNIT_TESTS/s,=.*,= $${srcdir},' \
+	${BMAKE_SRC}/unit-tests/Makefile ) > ${.TARGET}
+	@cmp -s ${.TARGET} ${.CURDIR}/unit-tests/Makefile || \
+	    mv ${.TARGET} ${.CURDIR}/unit-tests/Makefile
+
+
 .include <bsd.obj.mk>
 
diff --git a/buf.c b/buf.c
index 360b8cedde13..f96d8fbf9792 100644
--- a/buf.c
+++ b/buf.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: buf.c,v 1.26 2020/07/03 08:02:55 rillig Exp $	*/
+/*	$NetBSD: buf.c,v 1.37 2020/08/23 08:21:50 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -70,173 +70,123 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: buf.c,v 1.26 2020/07/03 08:02:55 rillig Exp $";
+static char rcsid[] = "$NetBSD: buf.c,v 1.37 2020/08/23 08:21:50 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)buf.c	8.1 (Berkeley) 6/6/93";
 #else
-__RCSID("$NetBSD: buf.c,v 1.26 2020/07/03 08:02:55 rillig Exp $");
+__RCSID("$NetBSD: buf.c,v 1.37 2020/08/23 08:21:50 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
 
-/*-
- * buf.c --
- *	Functions for automatically-expanded buffers.
- */
+/* Functions for automatically-expanded null-terminated buffers. */
 
-#include    "make.h"
-#include    "buf.h"
+#include <limits.h>
+#include "make.h"
 
-#ifndef max
-#define max(a,b)  ((a) > (b) ? (a) : (b))
-#endif
-
-#define BUF_DEF_SIZE	256 	/* Default buffer size */
-
-/*-
- *-----------------------------------------------------------------------
- * Buf_Expand_1 --
- *	Extend buffer for single byte add.
- *
- *-----------------------------------------------------------------------
- */
+/* Extend the buffer for adding a single byte. */
 void
 Buf_Expand_1(Buffer *bp)
 {
-    bp->size += max(bp->size, 16);
+    bp->size += MAX(bp->size, 16);
     bp->buffer = bmake_realloc(bp->buffer, bp->size);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Buf_AddBytes --
- *	Add a number of bytes to the buffer.
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	Guess what?
- *
- *-----------------------------------------------------------------------
- */
+/* Add the given bytes to the buffer. */
 void
-Buf_AddBytes(Buffer *bp, int numBytes, const Byte *bytesPtr)
+Buf_AddBytes(Buffer *bp, const char *bytesPtr, size_t numBytes)
 {
-    int count = bp->count;
-    Byte *ptr;
+    size_t count = bp->count;
+    char *ptr;
 
     if (__predict_false(count + numBytes >= bp->size)) {
-	bp->size += max(bp->size, numBytes + 16);
+	bp->size += MAX(bp->size, numBytes + 16);
 	bp->buffer = bmake_realloc(bp->buffer, bp->size);
     }
 
     ptr = bp->buffer + count;
     bp->count = count + numBytes;
-    ptr[numBytes] = 0;
     memcpy(ptr, bytesPtr, numBytes);
+    ptr[numBytes] = '\0';
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Buf_GetAll --
- *	Get all the available data at once.
- *
- * Results:
- *	A pointer to the data and the number of bytes available.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-Byte *
-Buf_GetAll(Buffer *bp, int *numBytesPtr)
+/* Add the bytes between start and end to the buffer. */
+void
+Buf_AddBytesBetween(Buffer *bp, const char *start, const char *end)
 {
+    Buf_AddBytes(bp, start, (size_t)(end - start));
+}
 
+/* Add the given string to the buffer. */
+void
+Buf_AddStr(Buffer *bp, const char *str)
+{
+    Buf_AddBytes(bp, str, strlen(str));
+}
+
+/* Add the given number to the buffer. */
+void
+Buf_AddInt(Buffer *bp, int n)
+{
+    enum {
+	bits = sizeof(int) * CHAR_BIT,
+	max_octal_digits = (bits + 2) / 3,
+	max_decimal_digits = /* at most */ max_octal_digits,
+	max_sign_chars = 1,
+	buf_size = max_sign_chars + max_decimal_digits + 1
+    };
+    char buf[buf_size];
+
+    size_t len = (size_t)snprintf(buf, sizeof buf, "%d", n);
+    Buf_AddBytes(bp, buf, len);
+}
+
+/* Get the data (usually a string) from the buffer.
+ * The returned data is valid until the next modifying operation
+ * on the buffer.
+ *
+ * Returns the pointer to the data and optionally the length of the
+ * data in the buffer. */
+char *
+Buf_GetAll(Buffer *bp, size_t *numBytesPtr)
+{
     if (numBytesPtr != NULL)
 	*numBytesPtr = bp->count;
-
     return bp->buffer;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Buf_Empty --
- *	Throw away bytes in a buffer.
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	The bytes are discarded.
- *
- *-----------------------------------------------------------------------
- */
+/* Mark the buffer as empty, so it can be filled with data again. */
 void
 Buf_Empty(Buffer *bp)
 {
-
     bp->count = 0;
-    *bp->buffer = 0;
+    bp->buffer[0] = '\0';
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Buf_Init --
- *	Initialize a buffer. If no initial size is given, a reasonable
- *	default is used.
- *
- * Input:
- *	size		Initial size for the buffer
- *
- * Results:
- *	A buffer to be given to other functions in this library.
- *
- * Side Effects:
- *	The buffer is created, the space allocated and pointers
- *	initialized.
- *
- *-----------------------------------------------------------------------
- */
+/* Initialize a buffer.
+ * If the given initial size is 0, a reasonable default is used. */
 void
-Buf_Init(Buffer *bp, int size)
+Buf_Init(Buffer *bp, size_t size)
 {
     if (size <= 0) {
-	size = BUF_DEF_SIZE;
+	size = 256;
     }
     bp->size = size;
     bp->count = 0;
     bp->buffer = bmake_malloc(size);
-    *bp->buffer = 0;
+    bp->buffer[0] = '\0';
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Buf_Destroy --
- *	Nuke a buffer and all its resources.
- *
- * Input:
- *	buf		Buffer to destroy
- *	freeData	TRUE if the data should be destroyed
- *
- * Results:
- *	Data buffer, NULL if freed
- *
- * Side Effects:
- *	The buffer is freed.
- *
- *-----------------------------------------------------------------------
- */
-Byte *
+/* Reset the buffer.
+ * If freeData is TRUE, the data from the buffer is freed as well.
+ * Otherwise it is kept and returned. */
+char *
 Buf_Destroy(Buffer *buf, Boolean freeData)
 {
-    Byte *data;
-
-    data = buf->buffer;
+    char *data = buf->buffer;
     if (freeData) {
 	free(data);
 	data = NULL;
@@ -249,42 +199,24 @@ Buf_Destroy(Buffer *buf, Boolean freeData)
     return data;
 }
 
-
-/*-
- *-----------------------------------------------------------------------
- * Buf_DestroyCompact --
- *	Nuke a buffer and return its data.
- *
- * Input:
- *	buf		Buffer to destroy
- *
- * Results:
- *	Data buffer
- *
- * Side Effects:
- *	If the buffer size is much greater than its content,
- *	a new buffer will be allocated and the old one freed.
- *
- *-----------------------------------------------------------------------
- */
 #ifndef BUF_COMPACT_LIMIT
-# define BUF_COMPACT_LIMIT 128          /* worthwhile saving */
+# define BUF_COMPACT_LIMIT 128		/* worthwhile saving */
 #endif
 
-Byte *
+/* Reset the buffer and return its data.
+ *
+ * If the buffer size is much greater than its content,
+ * a new buffer will be allocated and the old one freed. */
+char *
 Buf_DestroyCompact(Buffer *buf)
 {
 #if BUF_COMPACT_LIMIT > 0
-    Byte *data;
-
     if (buf->size - buf->count >= BUF_COMPACT_LIMIT) {
 	/* We trust realloc to be smart */
-	data = bmake_realloc(buf->buffer, buf->count + 1);
-	if (data) {
-	    data[buf->count] = 0;
-	    Buf_Destroy(buf, FALSE);
-	    return data;
-	}
+	char *data = bmake_realloc(buf->buffer, buf->count + 1);
+	data[buf->count] = '\0';
+	Buf_Destroy(buf, FALSE);
+	return data;
     }
 #endif
     return Buf_Destroy(buf, FALSE);
diff --git a/buf.h b/buf.h
index 7bd2d2b7940b..1dc3cdbf58f3 100644
--- a/buf.h
+++ b/buf.h
@@ -1,4 +1,4 @@
-/*	$NetBSD: buf.h,v 1.19 2017/05/31 22:02:06 maya Exp $	*/
+/*	$NetBSD: buf.h,v 1.28 2020/09/01 17:38:26 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -72,48 +72,54 @@
  *	from: @(#)buf.h	8.1 (Berkeley) 6/6/93
  */
 
-/*-
- * buf.h --
- *	Header for users of the buf library.
- */
+/* Automatically growing null-terminated buffers of characters. */
 
 #ifndef MAKE_BUF_H
 #define MAKE_BUF_H
 
-typedef char Byte;
+#include <stddef.h>
 
+/* An automatically growing null-terminated buffer of characters. */
 typedef struct Buffer {
-    int	    size; 	/* Current size of the buffer */
-    int     count;	/* Number of bytes in buffer */
-    Byte    *buffer;	/* The buffer itself (zero terminated) */
+    size_t size;	/* Allocated size of the buffer, including the null */
+    size_t count;	/* Number of bytes in buffer, excluding the null */
+    char *buffer;	/* The buffer itself (always null-terminated) */
 } Buffer;
 
-/* If we aren't on netbsd, __predict_false() might not be defined. */
+/* If we aren't on NetBSD, __predict_false() might not be defined. */
 #ifndef __predict_false
 #define __predict_false(x) (x)
 #endif
 
-/* Buf_AddByte adds a single byte to a buffer. */
-#define	Buf_AddByte(bp, byte) do { \
-	int _count = ++(bp)->count; \
-	char *_ptr; \
-	if (__predict_false(_count >= (bp)->size)) \
-		Buf_Expand_1(bp); \
-	_ptr = (bp)->buffer + _count; \
-	_ptr[-1] = (byte); \
-	_ptr[0] = 0; \
-    } while (0)
-
-#define BUF_ERROR 256
-
-#define Buf_Size(bp) ((bp)->count)
-
 void Buf_Expand_1(Buffer *);
-void Buf_AddBytes(Buffer *, int, const Byte *);
-Byte *Buf_GetAll(Buffer *, int *);
+
+/* Buf_AddByte adds a single byte to a buffer. */
+static inline void MAKE_ATTR_UNUSED
+Buf_AddByte(Buffer *bp, char byte)
+{
+    size_t count = ++bp->count;
+    char *ptr;
+    if (__predict_false(count >= bp->size))
+	Buf_Expand_1(bp);
+    ptr = bp->buffer + count;
+    ptr[-1] = byte;
+    ptr[0] = 0;
+}
+
+static inline size_t MAKE_ATTR_UNUSED
+Buf_Size(const Buffer *bp)
+{
+    return bp->count;
+}
+
+void Buf_AddBytes(Buffer *, const char *, size_t);
+void Buf_AddBytesBetween(Buffer *, const char *, const char *);
+void Buf_AddStr(Buffer *, const char *);
+void Buf_AddInt(Buffer *, int);
+char *Buf_GetAll(Buffer *, size_t *);
 void Buf_Empty(Buffer *);
-void Buf_Init(Buffer *, int);
-Byte *Buf_Destroy(Buffer *, Boolean);
-Byte *Buf_DestroyCompact(Buffer *);
+void Buf_Init(Buffer *, size_t);
+char *Buf_Destroy(Buffer *, Boolean);
+char *Buf_DestroyCompact(Buffer *);
 
 #endif /* MAKE_BUF_H */
diff --git a/compat.c b/compat.c
index cd88884736f1..150696ad6713 100644
--- a/compat.c
+++ b/compat.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: compat.c,v 1.113 2020/07/03 08:13:23 rillig Exp $	*/
+/*	$NetBSD: compat.c,v 1.139 2020/08/30 20:08:47 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -70,14 +70,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: compat.c,v 1.113 2020/07/03 08:13:23 rillig Exp $";
+static char rcsid[] = "$NetBSD: compat.c,v 1.139 2020/08/30 20:08:47 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)compat.c	8.2 (Berkeley) 3/19/94";
 #else
-__RCSID("$NetBSD: compat.c,v 1.113 2020/07/03 08:13:23 rillig Exp $");
+__RCSID("$NetBSD: compat.c,v 1.139 2020/08/30 20:08:47 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -129,34 +129,24 @@ static void
 CompatDeleteTarget(GNode *gn)
 {
     if ((gn != NULL) && !Targ_Precious (gn)) {
-	char	  *p1;
-	char 	  *file = Var_Value(TARGET, gn, &p1);
+	char *p1;
+	const char *file = Var_Value(TARGET, gn, &p1);
 
 	if (!noExecute && eunlink(file) != -1) {
 	    Error("*** %s removed", file);
 	}
 
-	free(p1);
+	bmake_free(p1);
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * CompatInterrupt --
- *	Interrupt the creation of the current target and remove it if
- *	it ain't precious.
+/* Interrupt the creation of the current target and remove it if it ain't
+ * precious. Then exit.
  *
- * Results:
- *	None.
- *
- * Side Effects:
- *	The target is removed and the process exits. If .INTERRUPT exists,
- *	its commands are run first WITH INTERRUPTS IGNORED..
+ * If .INTERRUPT exists, its commands are run first WITH INTERRUPTS IGNORED.
  *
  * XXX: is .PRECIOUS supposed to inhibit .INTERRUPT? I doubt it, but I've
  * left the logic alone for now. - dholland 20160826
- *
- *-----------------------------------------------------------------------
  */
 static void
 CompatInterrupt(int signo)
@@ -190,7 +180,7 @@ CompatInterrupt(int signo)
 	kill(myPid, signo);
     }
 }
-
+
 /*-
  *-----------------------------------------------------------------------
  * CompatRunCommand --
@@ -215,7 +205,7 @@ CompatRunCommand(void *cmdp, void *gnp)
     char    	  *cmdStart;	/* Start of expanded command */
     char 	  *cp, *bp;
     Boolean 	  silent,   	/* Don't print command */
-	    	  doIt;		/* Execute even if -n */
+		  doIt;		/* Execute even if -n */
     volatile Boolean errCheck; 	/* Check errors */
     WAIT_T 	  reason;   	/* Reason for child's death */
     int	    	  status;   	/* Description of child's death */
@@ -224,21 +214,17 @@ CompatRunCommand(void *cmdp, void *gnp)
     LstNode 	  cmdNode;  	/* Node where current command is located */
     const char  ** volatile av;	/* Argument vector for thing to exec */
     char	** volatile mav;/* Copy of the argument vector for freeing */
-    int	    	  argc;	    	/* Number of arguments in av or 0 if not
-				 * dynamically allocated */
-    Boolean 	  local;    	/* TRUE if command should be executed
-				 * locally */
     Boolean 	  useShell;    	/* TRUE if command should be executed
 				 * using a shell */
     char	  * volatile cmd = (char *)cmdp;
     GNode	  *gn = (GNode *)gnp;
 
-    silent = gn->type & OP_SILENT;
+    silent = (gn->type & OP_SILENT) != 0;
     errCheck = !(gn->type & OP_IGNORE);
     doIt = FALSE;
 
-    cmdNode = Lst_Member(gn->commands, cmd);
-    cmdStart = Var_Subst(NULL, cmd, gn, VARF_WANTRES);
+    cmdNode = Lst_FindDatum(gn->commands, cmd);
+    cmdStart = Var_Subst(cmd, gn, VARE_WANTRES);
 
     /*
      * brk_string will return an argv with a NULL in av[0], thus causing
@@ -252,10 +238,11 @@ CompatRunCommand(void *cmdp, void *gnp)
 	return 0;
     }
     cmd = cmdStart;
-    Lst_Replace(cmdNode, cmdStart);
+    LstNode_Set(cmdNode, cmdStart);
 
     if ((gn->type & OP_SAVE_CMDS) && (gn != ENDNode)) {
-	(void)Lst_AtEnd(ENDNode->commands, cmdStart);
+        assert(ENDNode != NULL);
+	Lst_Append(ENDNode->commands, cmdStart);
 	return 0;
     }
     if (strcmp(cmdStart, "...") == 0) {
@@ -266,7 +253,7 @@ CompatRunCommand(void *cmdp, void *gnp)
     while ((*cmd == '@') || (*cmd == '-') || (*cmd == '+')) {
 	switch (*cmd) {
 	case '@':
-	    silent = DEBUG(LOUD) ? FALSE : TRUE;
+	    silent = !DEBUG(LOUD);
 	    break;
 	case '-':
 	    errCheck = FALSE;
@@ -330,7 +317,6 @@ CompatRunCommand(void *cmdp, void *gnp)
     if (DEBUG(JOB))
 	fprintf(debug_file, "Execute: '%s'\n", cmd);
 
-again:
     if (useShell) {
 	/*
 	 * We need to pass the command off to the shell, typically
@@ -352,9 +338,8 @@ again:
 	else
 		shargv[shargc++] = "-c";
 	shargv[shargc++] = cmd;
-	shargv[shargc++] = NULL;
+	shargv[shargc] = NULL;
 	av = shargv;
-	argc = 0;
 	bp = NULL;
 	mav = NULL;
     } else {
@@ -362,16 +347,12 @@ again:
 	 * No meta-characters, so no need to exec a shell. Break the command
 	 * into words to form an argument vector we can execute.
 	 */
-	mav = brk_string(cmd, &argc, TRUE, &bp);
-	if (mav == NULL) {
-		useShell = 1;
-		goto again;
-	}
+	Words words = Str_Words(cmd, FALSE);
+	mav = words.words;
+	bp = words.freeIt;
 	av = (void *)mav;
     }
 
-    local = TRUE;
-
 #ifdef USE_META
     if (useMeta) {
 	meta_compat_start();
@@ -392,10 +373,7 @@ again:
 	    meta_compat_child();
 	}
 #endif
-	if (local)
-	    (void)execvp(av[0], (char *const *)UNCONST(av));
-	else
-	    (void)execv(av[0], (char *const *)UNCONST(av));
+	(void)execvp(av[0], (char *const *)UNCONST(av));
 	execError("exec", av[0]);
 	_exit(1);
     }
@@ -403,7 +381,9 @@ again:
     free(mav);
     free(bp);
 
-    Lst_Replace(cmdNode, NULL);
+    /* XXX: Memory management looks suspicious here. */
+    /* XXX: Setting a list item to NULL is unexpected. */
+    LstNode_SetNull(cmdNode);
 
 #ifdef USE_META
     if (useMeta) {
@@ -436,18 +416,18 @@ again:
 #endif
 		if (status != 0) {
 		    if (DEBUG(ERROR)) {
-		        fprintf(debug_file, "\n*** Failed target:  %s\n*** Failed command: ",
+			fprintf(debug_file, "\n*** Failed target:  %s\n*** Failed command: ",
 			    gn->name);
-		        for (cp = cmd; *cp; ) {
-    			    if (isspace((unsigned char)*cp)) {
+			for (cp = cmd; *cp; ) {
+			    if (isspace((unsigned char)*cp)) {
 				fprintf(debug_file, " ");
-			        while (isspace((unsigned char)*cp))
+				while (isspace((unsigned char)*cp))
 				    cp++;
 			    } else {
 				fprintf(debug_file, "%c", *cp);
-			        cp++;
+				cp++;
 			    }
-		        }
+			}
 			fprintf(debug_file, "\n");
 		    }
 		    printf("*** Error code %d", status);
@@ -502,7 +482,7 @@ again:
 
     return status;
 }
-
+
 /*-
  *-----------------------------------------------------------------------
  * Compat_Make --
@@ -544,14 +524,14 @@ Compat_Make(void *gnp, void *pgnp)
 	Lst_ForEach(gn->children, Compat_Make, gn);
 	if ((gn->flags & REMAKE) == 0) {
 	    gn->made = ABORTED;
-	    pgn->flags &= ~REMAKE;
+	    pgn->flags &= ~(unsigned)REMAKE;
 	    goto cohorts;
 	}
 
-	if (Lst_Member(gn->iParents, pgn) != NULL) {
+	if (Lst_FindDatum(gn->implicitParents, pgn) != NULL) {
 	    char *p1;
 	    Var_Set(IMPSRC, Var_Value(TARGET, gn, &p1), pgn);
-	    free(p1);
+	    bmake_free(p1);
 	}
 
 	/*
@@ -614,7 +594,7 @@ Compat_Make(void *gnp, void *pgnp)
 		Lst_ForEach(gn->commands, CompatRunCommand, gn);
 		curTarg = NULL;
 	    } else {
-		Job_Touch(gn, gn->type & OP_SILENT);
+		Job_Touch(gn, (gn->type & OP_SILENT) != 0);
 	    }
 	} else {
 	    gn->made = ERROR;
@@ -640,7 +620,7 @@ Compat_Make(void *gnp, void *pgnp)
 		Make_TimeStamp(pgn, gn);
 	    }
 	} else if (keepgoing) {
-	    pgn->flags &= ~REMAKE;
+	    pgn->flags &= ~(unsigned)REMAKE;
 	} else {
 	    PrintOnError(gn, "\nStop.");
 	    exit(1);
@@ -650,18 +630,19 @@ Compat_Make(void *gnp, void *pgnp)
 	 * Already had an error when making this beastie. Tell the parent
 	 * to abort.
 	 */
-	pgn->flags &= ~REMAKE;
+	pgn->flags &= ~(unsigned)REMAKE;
     } else {
-	if (Lst_Member(gn->iParents, pgn) != NULL) {
+	if (Lst_FindDatum(gn->implicitParents, pgn) != NULL) {
 	    char *p1;
-	    Var_Set(IMPSRC, Var_Value(TARGET, gn, &p1), pgn);
-	    free(p1);
+	    const char *target = Var_Value(TARGET, gn, &p1);
+	    Var_Set(IMPSRC, target != NULL ? target : "", pgn);
+	    bmake_free(p1);
 	}
 	switch(gn->made) {
 	    case BEINGMADE:
 		Error("Graph cycles through %s", gn->name);
 		gn->made = ERROR;
-		pgn->flags &= ~REMAKE;
+		pgn->flags &= ~(unsigned)REMAKE;
 		break;
 	    case MADE:
 		if ((gn->type & OP_EXEC) == 0) {
@@ -683,22 +664,11 @@ cohorts:
     Lst_ForEach(gn->cohorts, Compat_Make, pgnp);
     return 0;
 }
-
-/*-
- *-----------------------------------------------------------------------
- * Compat_Run --
- *	Initialize this mode and start making.
+
+/* Initialize this module and start making.
  *
  * Input:
- *	targs		List of target nodes to re-create
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	Guess what?
- *
- *-----------------------------------------------------------------------
+ *	targs		The target nodes to re-create
  */
 void
 Compat_Run(Lst targs)
@@ -732,10 +702,10 @@ Compat_Run(Lst targs)
 	gn = Targ_FindNode(".BEGIN", TARG_NOCREATE);
 	if (gn != NULL) {
 	    Compat_Make(gn, gn);
-            if (gn->made == ERROR) {
-                PrintOnError(gn, "\nStop.");
-                exit(1);
-            }
+	    if (gn->made == ERROR) {
+		PrintOnError(gn, "\nStop.");
+		exit(1);
+	    }
 	}
     }
 
@@ -756,8 +726,8 @@ Compat_Run(Lst targs)
      *	    	  	    could not be made due to errors.
      */
     errors = 0;
-    while (!Lst_IsEmpty (targs)) {
-	gn = (GNode *)Lst_DeQueue(targs);
+    while (!Lst_IsEmpty(targs)) {
+	gn = Lst_Dequeue(targs);
 	Compat_Make(gn, gn);
 
 	if (gn->made == UPTODATE) {
diff --git a/cond.c b/cond.c
index 321d9a92e75b..7aa072d4eb22 100644
--- a/cond.c
+++ b/cond.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: cond.c,v 1.79 2020/07/09 22:34:08 sjg Exp $	*/
+/*	$NetBSD: cond.c,v 1.106 2020/08/29 13:38:48 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -70,14 +70,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: cond.c,v 1.79 2020/07/09 22:34:08 sjg Exp $";
+static char rcsid[] = "$NetBSD: cond.c,v 1.106 2020/08/29 13:38:48 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)cond.c	8.2 (Berkeley) 1/2/94";
 #else
-__RCSID("$NetBSD: cond.c,v 1.79 2020/07/09 22:34:08 sjg Exp $");
+__RCSID("$NetBSD: cond.c,v 1.106 2020/08/29 13:38:48 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -91,14 +91,10 @@ __RCSID("$NetBSD: cond.c,v 1.79 2020/07/09 22:34:08 sjg Exp $");
  *
  */
 
-#include    <assert.h>
-#include    <ctype.h>
-#include    <errno.h>    /* For strtoul() error checking */
+#include <errno.h>
 
-#include    "make.h"
-#include    "hash.h"
-#include    "dir.h"
-#include    "buf.h"
+#include "make.h"
+#include "dir.h"
 
 /*
  * The parsing of conditional expressions is based on this grammar:
@@ -121,8 +117,7 @@ __RCSID("$NetBSD: cond.c,v 1.79 2020/07/09 22:34:08 sjg Exp $");
  *	T -> ! T
  *	op -> == | != | > | < | >= | <=
  *
- * 'symbol' is some other symbol to which the default function (condDefProc)
- * is applied.
+ * 'symbol' is some other symbol to which the default function is applied.
  *
  * Tokens are scanned from the 'condExpr' string. The scanner (CondToken)
  * will return TOK_AND for '&' and '&&', TOK_OR for '|' and '||',
@@ -141,46 +136,16 @@ typedef enum {
     TOK_LPAREN, TOK_RPAREN, TOK_EOF, TOK_NONE, TOK_ERROR
 } Token;
 
-/*-
- * Structures to handle elegantly the different forms of #if's. The
- * last two fields are stored in condInvert and condDefProc, respectively.
- */
-static void CondPushBack(Token);
-static int CondGetArg(Boolean, char **, char **, const char *);
-static Boolean CondDoDefined(int, const char *);
-static int CondStrMatch(const void *, const void *);
-static Boolean CondDoMake(int, const char *);
-static Boolean CondDoExists(int, const char *);
-static Boolean CondDoTarget(int, const char *);
-static Boolean CondDoCommands(int, const char *);
-static Boolean CondCvtArg(char *, double *);
-static Token CondToken(Boolean);
-static Token CondT(Boolean);
-static Token CondF(Boolean);
 static Token CondE(Boolean);
-static int do_Cond_EvalExpression(Boolean *);
+static CondEvalResult do_Cond_EvalExpression(Boolean *);
 
-static const struct If {
-    const char	*form;	      /* Form of if */
-    int		formlen;      /* Length of form */
-    Boolean	doNot;	      /* TRUE if default function should be negated */
-    Boolean	(*defProc)(int, const char *); /* Default function to apply */
-} ifs[] = {
-    { "def",	  3,	  FALSE,  CondDoDefined },
-    { "ndef",	  4,	  TRUE,	  CondDoDefined },
-    { "make",	  4,	  FALSE,  CondDoMake },
-    { "nmake",	  5,	  TRUE,	  CondDoMake },
-    { "",	  0,	  FALSE,  CondDoDefined },
-    { NULL,	  0,	  FALSE,  NULL }
-};
-
-static const struct If *if_info;        /* Info for current statement */
-static char 	  *condExpr;	    	/* The expression to parse */
-static Token	  condPushBack=TOK_NONE;	/* Single push-back token used in
+static const struct If *if_info;	/* Info for current statement */
+static const char *condExpr;		/* The expression to parse */
+static Token condPushBack = TOK_NONE;	/* Single push-back token used in
 					 * parsing */
 
-static unsigned int	cond_depth = 0;  	/* current .if nesting level */
-static unsigned int	cond_min_depth = 0;  	/* depth at makefile open */
+static unsigned int cond_depth = 0;	/* current .if nesting level */
+static unsigned int cond_min_depth = 0;	/* depth at makefile open */
 
 /*
  * Indicate when we should be strict about lhs of comparisons.
@@ -194,58 +159,38 @@ static Boolean lhsStrict;
 static int
 istoken(const char *str, const char *tok, size_t len)
 {
-	return strncmp(str, tok, len) == 0 && !isalpha((unsigned char)str[len]);
+    return strncmp(str, tok, len) == 0 && !isalpha((unsigned char)str[len]);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * CondPushBack --
- *	Push back the most recent token read. We only need one level of
- *	this, so the thing is just stored in 'condPushback'.
- *
- * Input:
- *	t		Token to push back into the "stream"
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	condPushback is overwritten.
- *
- *-----------------------------------------------------------------------
- */
+/* Push back the most recent token read. We only need one level of
+ * this, so the thing is just stored in 'condPushback'. */
 static void
 CondPushBack(Token t)
 {
     condPushBack = t;
 }
-
+
 /*-
- *-----------------------------------------------------------------------
- * CondGetArg --
- *	Find the argument of a built-in function.
+ * Parse the argument of a built-in function.
  *
  * Results:
- *	The length of the argument and the address of the argument.
- *
- * Side Effects:
- *	The pointer is set to point to the closing parenthesis of the
- *	function call.
- *
- *-----------------------------------------------------------------------
+ *	The length of the argument.
+ *	*argPtr receives the argument as string.
+ *	*linePtr is updated to point behind the ')' of the function call.
  */
 static int
-CondGetArg(Boolean doEval, char **linePtr, char **argPtr, const char *func)
+CondGetArg(Boolean doEval, const char **linePtr, char **argPtr,
+	   const char *func)
 {
-    char	  *cp;
-    int	    	  argLen;
-    Buffer	  buf;
-    int           paren_depth;
-    char          ch;
+    const char *cp;
+    Buffer buf;
+    int paren_depth;
+    char ch;
+    size_t argLen;
 
     cp = *linePtr;
     if (func != NULL)
-	/* Skip opening '(' - verfied by caller */
+	/* Skip opening '(' - verified by caller */
 	cp++;
 
     if (*cp == '\0') {
@@ -283,23 +228,19 @@ CondGetArg(Boolean doEval, char **linePtr, char **argPtr, const char *func)
 	     * variable, so we don't do it too. Nor do we return an error,
 	     * though perhaps we should...
 	     */
-	    char  	*cp2;
-	    int		len;
-	    void	*freeIt;
-
-	    cp2 = Var_Parse(cp, VAR_CMD, VARF_UNDEFERR|
-			    (doEval ? VARF_WANTRES : 0),
-			    &len, &freeIt);
-	    Buf_AddBytes(&buf, strlen(cp2), cp2);
+	    int len;
+	    void *freeIt;
+	    VarEvalFlags eflags = VARE_UNDEFERR | (doEval ? VARE_WANTRES : 0);
+	    const char *cp2 = Var_Parse(cp, VAR_CMD, eflags, &len, &freeIt);
+	    Buf_AddStr(&buf, cp2);
 	    free(freeIt);
 	    cp += len;
 	    continue;
 	}
 	if (ch == '(')
 	    paren_depth++;
-	else
-	    if (ch == ')' && --paren_depth < 0)
-		break;
+	else if (ch == ')' && --paren_depth < 0)
+	    break;
 	Buf_AddByte(&buf, *cp);
 	cp++;
     }
@@ -313,105 +254,49 @@ CondGetArg(Boolean doEval, char **linePtr, char **argPtr, const char *func)
 
     if (func != NULL && *cp++ != ')') {
 	Parse_Error(PARSE_WARNING, "Missing closing parenthesis for %s()",
-		     func);
+		    func);
 	return 0;
     }
 
     *linePtr = cp;
     return argLen;
 }
-
-/*-
- *-----------------------------------------------------------------------
- * CondDoDefined --
- *	Handle the 'defined' function for conditionals.
- *
- * Results:
- *	TRUE if the given variable is defined.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
+
+/* Test whether the given variable is defined. */
 static Boolean
 CondDoDefined(int argLen MAKE_ATTR_UNUSED, const char *arg)
 {
-    char    *p1;
-    Boolean result;
-
-    if (Var_Value(arg, VAR_CMD, &p1) != NULL) {
-	result = TRUE;
-    } else {
-	result = FALSE;
-    }
-
-    free(p1);
+    char *freeIt;
+    Boolean result = Var_Value(arg, VAR_CMD, &freeIt) != NULL;
+    bmake_free(freeIt);
     return result;
 }
-
-/*-
- *-----------------------------------------------------------------------
- * CondStrMatch --
- *	Front-end for Str_Match so it returns 0 on match and non-zero
- *	on mismatch. Callback function for CondDoMake via Lst_Find
- *
- * Results:
- *	0 if string matches pattern
- *
- * Side Effects:
- *	None
- *
- *-----------------------------------------------------------------------
- */
-static int
-CondStrMatch(const void *string, const void *pattern)
+
+/* Wrapper around Str_Match, to be used by Lst_Find. */
+static Boolean
+CondFindStrMatch(const void *string, const void *pattern)
 {
-    return !Str_Match(string, pattern);
+    return Str_Match(string, pattern);
 }
-
-/*-
- *-----------------------------------------------------------------------
- * CondDoMake --
- *	Handle the 'make' function for conditionals.
- *
- * Results:
- *	TRUE if the given target is being made.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
+
+/* See if the given target is being made. */
 static Boolean
 CondDoMake(int argLen MAKE_ATTR_UNUSED, const char *arg)
 {
-    return Lst_Find(create, arg, CondStrMatch) != NULL;
+    return Lst_Find(create, CondFindStrMatch, arg) != NULL;
 }
-
-/*-
- *-----------------------------------------------------------------------
- * CondDoExists --
- *	See if the given file exists.
- *
- * Results:
- *	TRUE if the file exists and FALSE if it does not.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
+
+/* See if the given file exists. */
 static Boolean
 CondDoExists(int argLen MAKE_ATTR_UNUSED, const char *arg)
 {
     Boolean result;
-    char    *path;
+    char *path;
 
     path = Dir_FindFile(arg, dirSearchPath);
     if (DEBUG(COND)) {
 	fprintf(debug_file, "exists(%s) result is \"%s\"\n",
-	       arg, path ? path : "");
+		arg, path ? path : "");
     }
     if (path != NULL) {
 	result = TRUE;
@@ -421,68 +306,39 @@ CondDoExists(int argLen MAKE_ATTR_UNUSED, const char *arg)
     }
     return result;
 }
-
-/*-
- *-----------------------------------------------------------------------
- * CondDoTarget --
- *	See if the given node exists and is an actual target.
- *
- * Results:
- *	TRUE if the node exists as a target and FALSE if it does not.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
+
+/* See if the given node exists and is an actual target. */
 static Boolean
 CondDoTarget(int argLen MAKE_ATTR_UNUSED, const char *arg)
 {
-    GNode   *gn;
+    GNode *gn;
 
     gn = Targ_FindNode(arg, TARG_NOCREATE);
     return gn != NULL && !OP_NOP(gn->type);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * CondDoCommands --
- *	See if the given node exists and is an actual target with commands
- *	associated with it.
- *
- * Results:
- *	TRUE if the node exists as a target and has commands associated with
- *	it and FALSE if it does not.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
+/* See if the given node exists and is an actual target with commands
+ * associated with it. */
 static Boolean
 CondDoCommands(int argLen MAKE_ATTR_UNUSED, const char *arg)
 {
-    GNode   *gn;
+    GNode *gn;
 
     gn = Targ_FindNode(arg, TARG_NOCREATE);
     return gn != NULL && !OP_NOP(gn->type) && !Lst_IsEmpty(gn->commands);
 }
-
+
 /*-
- *-----------------------------------------------------------------------
- * CondCvtArg --
- *	Convert the given number into a double.
- *	We try a base 10 or 16 integer conversion first, if that fails
- *	then we try a floating point conversion instead.
+ * Convert the given number into a double.
+ * We try a base 10 or 16 integer conversion first, if that fails
+ * then we try a floating point conversion instead.
  *
  * Results:
  *	Sets 'value' to double value of string.
- *	Returns 'true' if the convertion suceeded
- *
- *-----------------------------------------------------------------------
+ *	Returns TRUE if the conversion succeeded.
  */
 static Boolean
-CondCvtArg(char *str, double *value)
+CondCvtArg(const char *str, double *value)
 {
     char *eptr, ech;
     unsigned long l_val;
@@ -510,33 +366,28 @@ CondCvtArg(char *str, double *value)
 }
 
 /*-
- *-----------------------------------------------------------------------
- * CondGetString --
- *	Get a string from a variable reference or an optionally quoted
- *	string.  This is called for the lhs and rhs of string compares.
+ * Get a string from a variable reference or an optionally quoted
+ * string.  This is called for the lhs and rhs of string compares.
  *
  * Results:
- *	Sets freeIt if needed,
- *	Sets quoted if string was quoted,
- *	Returns NULL on error,
- *	else returns string - absent any quotes.
+ *	Returns the string, absent any quotes, or NULL on error.
+ *	Sets quoted if the string was quoted.
+ *	Sets freeIt if needed.
  *
  * Side Effects:
- *	Moves condExpr to end of this token.
- *
- *
- *-----------------------------------------------------------------------
+ *	Moves condExpr past the end of this token.
  */
 /* coverity:[+alloc : arg-*2] */
-static char *
+static const char *
 CondGetString(Boolean doEval, Boolean *quoted, void **freeIt, Boolean strictLHS)
 {
     Buffer buf;
-    char *cp;
-    char *str;
-    int	len;
-    int qt;
-    char *start;
+    const char *cp;
+    const char *str;
+    int len;
+    Boolean qt;
+    const char *start;
+    VarEvalFlags eflags;
 
     Buf_Init(&buf, 0);
     str = NULL;
@@ -573,9 +424,9 @@ CondGetString(Boolean doEval, Boolean *quoted, void **freeIt, Boolean strictLHS)
 	    break;
 	case '$':
 	    /* if we are in quotes, then an undefined variable is ok */
-	    str = Var_Parse(condExpr, VAR_CMD,
-			    ((!qt && doEval) ? VARF_UNDEFERR : 0) |
-			    (doEval ? VARF_WANTRES : 0), &len, freeIt);
+	    eflags = ((!qt && doEval) ? VARE_UNDEFERR : 0) |
+		     (doEval ? VARE_WANTRES : 0);
+	    str = Var_Parse(condExpr, VAR_CMD, eflags, &len, freeIt);
 	    if (str == var_Error) {
 		if (*freeIt) {
 		    free(*freeIt);
@@ -596,7 +447,7 @@ CondGetString(Boolean doEval, Boolean *quoted, void **freeIt, Boolean strictLHS)
 	     */
 	    if ((condExpr == start + len) &&
 		(*condExpr == '\0' ||
-		 isspace((unsigned char) *condExpr) ||
+		 isspace((unsigned char)*condExpr) ||
 		 strchr("!=><)", *condExpr))) {
 		goto cleanup;
 	    }
@@ -610,12 +461,12 @@ CondGetString(Boolean doEval, Boolean *quoted, void **freeIt, Boolean strictLHS)
 		free(*freeIt);
 		*freeIt = NULL;
 	    }
-	    str = NULL;			/* not finished yet */
-	    condExpr--;			/* don't skip over next char */
+	    str = NULL;		/* not finished yet */
+	    condExpr--;		/* don't skip over next char */
 	    break;
 	default:
 	    if (strictLHS && !qt && *start != '$' &&
-		!isdigit((unsigned char) *start)) {
+		!isdigit((unsigned char)*start)) {
 		/* lhs must be quoted, a variable reference or number */
 		if (*freeIt) {
 		    free(*freeIt);
@@ -628,43 +479,51 @@ CondGetString(Boolean doEval, Boolean *quoted, void **freeIt, Boolean strictLHS)
 	    break;
 	}
     }
- got_str:
-    str = Buf_GetAll(&buf, NULL);
-    *freeIt = str;
- cleanup:
+got_str:
+    *freeIt = Buf_GetAll(&buf, NULL);
+    str = *freeIt;
+cleanup:
     Buf_Destroy(&buf, FALSE);
     return str;
 }
-
+
+/* The different forms of #if's. */
+static const struct If {
+    const char *form;		/* Form of if */
+    size_t formlen;		/* Length of form */
+    Boolean doNot;		/* TRUE if default function should be negated */
+    Boolean (*defProc)(int, const char *); /* Default function to apply */
+} ifs[] = {
+    { "def",   3, FALSE, CondDoDefined },
+    { "ndef",  4, TRUE,  CondDoDefined },
+    { "make",  4, FALSE, CondDoMake },
+    { "nmake", 5, TRUE,  CondDoMake },
+    { "",      0, FALSE, CondDoDefined },
+    { NULL,    0, FALSE, NULL }
+};
+
 /*-
- *-----------------------------------------------------------------------
- * CondToken --
- *	Return the next token from the input.
- *
- * Results:
- *	A Token for the next lexical token in the stream.
+ * Return the next token from the input.
  *
  * Side Effects:
  *	condPushback will be set back to TOK_NONE if it is used.
- *
- *-----------------------------------------------------------------------
  */
 static Token
 compare_expression(Boolean doEval)
 {
-    Token	t;
-    char	*lhs;
-    char	*rhs;
-    char	*op;
-    void	*lhsFree;
-    void	*rhsFree;
+    Token t;
+    const char *lhs;
+    const char *rhs;
+    const char *op;
+    void *lhsFree;
+    void *rhsFree;
     Boolean lhsQuoted;
     Boolean rhsQuoted;
-    double  	left, right;
+    double left, right;
 
     t = TOK_ERROR;
     rhs = NULL;
-    lhsFree = rhsFree = FALSE;
+    lhsFree = rhsFree = NULL;
     lhsQuoted = rhsQuoted = FALSE;
 
     /*
@@ -678,7 +537,7 @@ compare_expression(Boolean doEval)
     /*
      * Skip whitespace to get to the operator
      */
-    while (isspace((unsigned char) *condExpr))
+    while (isspace((unsigned char)*condExpr))
 	condExpr++;
 
     /*
@@ -688,39 +547,39 @@ compare_expression(Boolean doEval)
      */
     op = condExpr;
     switch (*condExpr) {
-	case '!':
-	case '=':
-	case '<':
-	case '>':
-	    if (condExpr[1] == '=') {
-		condExpr += 2;
-	    } else {
-		condExpr += 1;
-	    }
-	    break;
-	default:
-	    if (!doEval) {
-		t = TOK_FALSE;
-		goto done;
-	    }
-	    /* For .ifxxx "..." check for non-empty string. */
-	    if (lhsQuoted) {
-		t = lhs[0] != 0;
-		goto done;
-	    }
-	    /* For .ifxxx <number> compare against zero */
-	    if (CondCvtArg(lhs, &left)) {
-		t = left != 0.0;
-		goto done;
-	    }
-	    /* For .if ${...} check for non-empty string (defProc is ifdef). */
-	    if (if_info->form[0] == 0) {
-		t = lhs[0] != 0;
-		goto done;
-	    }
-	    /* Otherwise action default test ... */
-	    t = if_info->defProc(strlen(lhs), lhs) != if_info->doNot;
+    case '!':
+    case '=':
+    case '<':
+    case '>':
+	if (condExpr[1] == '=') {
+	    condExpr += 2;
+	} else {
+	    condExpr += 1;
+	}
+	break;
+    default:
+	if (!doEval) {
+	    t = TOK_FALSE;
 	    goto done;
+	}
+	/* For .ifxxx "..." check for non-empty string. */
+	if (lhsQuoted) {
+	    t = lhs[0] != 0;
+	    goto done;
+	}
+	/* For .ifxxx <number> compare against zero */
+	if (CondCvtArg(lhs, &left)) {
+	    t = left != 0.0;
+	    goto done;
+	}
+	/* For .if ${...} check for non-empty string (defProc is ifdef). */
+	if (if_info->form[0] == 0) {
+	    t = lhs[0] != 0;
+	    goto done;
+	}
+	/* Otherwise action default test ... */
+	t = if_info->defProc(strlen(lhs), lhs) != if_info->doNot;
+	goto done;
     }
 
     while (isspace((unsigned char)*condExpr))
@@ -742,16 +601,16 @@ compare_expression(Boolean doEval)
     }
 
     if (rhsQuoted || lhsQuoted) {
-do_string_compare:
+    do_string_compare:
 	if (((*op != '!') && (*op != '=')) || (op[1] != '=')) {
 	    Parse_Error(PARSE_WARNING,
-    "String comparison operator should be either == or !=");
+			"String comparison operator should be either == or !=");
 	    goto done;
 	}
 
 	if (DEBUG(COND)) {
 	    fprintf(debug_file, "lhs = \"%s\", rhs = \"%s\", op = %.2s\n",
-		   lhs, rhs, op);
+		    lhs, rhs, op);
 	}
 	/*
 	 * Null-terminate rhs and perform the comparison.
@@ -773,9 +632,9 @@ do_string_compare:
 
 	if (DEBUG(COND)) {
 	    fprintf(debug_file, "left = %f, right = %f, op = %.2s\n", left,
-		   right, op);
+		    right, op);
 	}
-	switch(op[0]) {
+	switch (op[0]) {
 	case '!':
 	    if (op[1] != '=') {
 		Parse_Error(PARSE_WARNING,
@@ -816,21 +675,23 @@ done:
 }
 
 static int
-get_mpt_arg(Boolean doEval, char **linePtr, char **argPtr, const char *func MAKE_ATTR_UNUSED)
+get_mpt_arg(Boolean doEval, const char **linePtr, char **argPtr,
+	    const char *func MAKE_ATTR_UNUSED)
 {
     /*
      * Use Var_Parse to parse the spec in parens and return
      * TOK_TRUE if the resulting string is empty.
      */
-    int	    length;
-    void    *freeIt;
-    char    *val;
-    char    *cp = *linePtr;
+    int length;
+    void *val_freeIt;
+    const char *val;
+    const char *cp = *linePtr;
 
     /* We do all the work here and return the result as the length */
     *argPtr = NULL;
 
-    val = Var_Parse(cp - 1, VAR_CMD, doEval ? VARF_WANTRES : 0, &length, &freeIt);
+    val = Var_Parse(cp - 1, VAR_CMD, doEval ? VARE_WANTRES : 0, &length,
+		    &val_freeIt);
     /*
      * Advance *linePtr to beyond the closing ). Note that
      * we subtract one because 'length' is calculated from 'cp - 1'.
@@ -838,12 +699,12 @@ get_mpt_arg(Boolean doEval, char **linePtr, char **argPtr, const char *func MAKE
     *linePtr = cp - 1 + length;
 
     if (val == var_Error) {
-	free(freeIt);
+	free(val_freeIt);
 	return -1;
     }
 
     /* A variable is empty when it just contains spaces... 4/15/92, christos */
-    while (isspace(*(unsigned char *)val))
+    while (isspace((unsigned char)val[0]))
 	val++;
 
     /*
@@ -851,7 +712,7 @@ get_mpt_arg(Boolean doEval, char **linePtr, char **argPtr, const char *func MAKE
      * true/false here.
      */
     length = *val ? 2 : 1;
-    free(freeIt);
+    free(val_freeIt);
     return length;
 }
 
@@ -865,32 +726,32 @@ static Token
 compare_function(Boolean doEval)
 {
     static const struct fn_def {
-	const char  *fn_name;
-	int         fn_name_len;
-        int         (*fn_getarg)(Boolean, char **, char **, const char *);
-	Boolean     (*fn_proc)(int, const char *);
+	const char *fn_name;
+	size_t fn_name_len;
+	int (*fn_getarg)(Boolean, const char **, char **, const char *);
+	Boolean (*fn_proc)(int, const char *);
     } fn_defs[] = {
-	{ "defined",   7, CondGetArg, CondDoDefined },
-	{ "make",      4, CondGetArg, CondDoMake },
-	{ "exists",    6, CondGetArg, CondDoExists },
-	{ "empty",     5, get_mpt_arg, CondDoEmpty },
-	{ "target",    6, CondGetArg, CondDoTarget },
-	{ "commands",  8, CondGetArg, CondDoCommands },
-	{ NULL,        0, NULL, NULL },
+	{ "defined",  7, CondGetArg,  CondDoDefined },
+	{ "make",     4, CondGetArg,  CondDoMake },
+	{ "exists",   6, CondGetArg,  CondDoExists },
+	{ "empty",    5, get_mpt_arg, CondDoEmpty },
+	{ "target",   6, CondGetArg,  CondDoTarget },
+	{ "commands", 8, CondGetArg,  CondDoCommands },
+	{ NULL,       0, NULL, NULL },
     };
     const struct fn_def *fn_def;
-    Token	t;
-    char	*arg = NULL;
-    int	arglen;
-    char *cp = condExpr;
-    char *cp1;
+    Token t;
+    char *arg = NULL;
+    int arglen;
+    const char *cp = condExpr;
+    const char *cp1;
 
     for (fn_def = fn_defs; fn_def->fn_name != NULL; fn_def++) {
 	if (!istoken(cp, fn_def->fn_name, fn_def->fn_name_len))
 	    continue;
 	cp += fn_def->fn_name_len;
 	/* There can only be whitespace before the '(' */
-	while (isspace(*(unsigned char *)cp))
+	while (isspace((unsigned char)*cp))
 	    cp++;
 	if (*cp != '(')
 	    break;
@@ -921,7 +782,7 @@ compare_function(Boolean doEval)
      * expression.
      */
     arglen = CondGetArg(doEval, &cp, &arg, NULL);
-    for (cp1 = cp; isspace(*(unsigned char *)cp1); cp1++)
+    for (cp1 = cp; isspace((unsigned char)*cp1); cp1++)
 	continue;
     if (*cp1 == '=' || *cp1 == '!')
 	return compare_expression(doEval);
@@ -1015,7 +876,7 @@ CondToken(Boolean doEval)
 static Token
 CondT(Boolean doEval)
 {
-    Token   t;
+    Token t;
 
     t = CondToken(doEval);
 
@@ -1045,7 +906,7 @@ CondT(Boolean doEval)
     }
     return t;
 }
-
+
 /*-
  *-----------------------------------------------------------------------
  * CondF --
@@ -1063,7 +924,7 @@ CondT(Boolean doEval)
 static Token
 CondF(Boolean doEval)
 {
-    Token   l, o;
+    Token l, o;
 
     l = CondT(doEval);
     if (l != TOK_ERROR) {
@@ -1091,7 +952,7 @@ CondF(Boolean doEval)
     }
     return l;
 }
-
+
 /*-
  *-----------------------------------------------------------------------
  * CondE --
@@ -1109,7 +970,7 @@ CondF(Boolean doEval)
 static Token
 CondE(Boolean doEval)
 {
-    Token   l, o;
+    Token l, o;
 
     l = CondF(doEval);
     if (l != TOK_ERROR) {
@@ -1139,64 +1000,7 @@ CondE(Boolean doEval)
     return l;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Cond_EvalExpression --
- *	Evaluate an expression in the passed line. The expression
- *	consists of &&, ||, !, make(target), defined(variable)
- *	and parenthetical groupings thereof.
- *
- * Results:
- *	COND_PARSE	if the condition was valid grammatically
- *	COND_INVALID  	if not a valid conditional.
- *
- *	(*value) is set to the boolean value of the condition
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-int
-Cond_EvalExpression(const struct If *info, char *line, Boolean *value, int eprint, Boolean strictLHS)
-{
-    static const struct If *dflt_info;
-    const struct If *sv_if_info = if_info;
-    char *sv_condExpr = condExpr;
-    Token sv_condPushBack = condPushBack;
-    int rval;
-
-    lhsStrict = strictLHS;
-
-    while (*line == ' ' || *line == '\t')
-	line++;
-
-    if (info == NULL && (info = dflt_info) == NULL) {
-	/* Scan for the entry for .if - it can't be first */
-	for (info = ifs; ; info++)
-	    if (info->form[0] == 0)
-		break;
-	dflt_info = info;
-    }
-    assert(info != NULL);
-
-    if_info = info;
-    condExpr = line;
-    condPushBack = TOK_NONE;
-
-    rval = do_Cond_EvalExpression(value);
-
-    if (rval == COND_INVALID && eprint)
-	Parse_Error(PARSE_FATAL, "Malformed conditional (%s)", line);
-
-    if_info = sv_if_info;
-    condExpr = sv_condExpr;
-    condPushBack = sv_condPushBack;
-
-    return rval;
-}
-
-static int
+static CondEvalResult
 do_Cond_EvalExpression(Boolean *value)
 {
 
@@ -1221,7 +1025,64 @@ do_Cond_EvalExpression(Boolean *value)
     return COND_INVALID;
 }
 
-
+/*-
+ *-----------------------------------------------------------------------
+ * Cond_EvalExpression --
+ *	Evaluate an expression in the passed line. The expression
+ *	consists of &&, ||, !, make(target), defined(variable)
+ *	and parenthetical groupings thereof.
+ *
+ * Results:
+ *	COND_PARSE	if the condition was valid grammatically
+ *	COND_INVALID  	if not a valid conditional.
+ *
+ *	(*value) is set to the boolean value of the condition
+ *
+ * Side Effects:
+ *	Any effects from evaluating the variables.
+ *-----------------------------------------------------------------------
+ */
+CondEvalResult
+Cond_EvalExpression(const struct If *info, char *line, Boolean *value,
+		    int eprint, Boolean strictLHS)
+{
+    static const struct If *dflt_info;
+    const struct If *sv_if_info = if_info;
+    const char *sv_condExpr = condExpr;
+    Token sv_condPushBack = condPushBack;
+    int rval;
+
+    lhsStrict = strictLHS;
+
+    while (*line == ' ' || *line == '\t')
+	line++;
+
+    if (info == NULL && (info = dflt_info) == NULL) {
+	/* Scan for the entry for .if - it can't be first */
+	for (info = ifs;; info++)
+	    if (info->form[0] == 0)
+		break;
+	dflt_info = info;
+    }
+    assert(info != NULL);
+
+    if_info = info;
+    condExpr = line;
+    condPushBack = TOK_NONE;
+
+    rval = do_Cond_EvalExpression(value);
+
+    if (rval == COND_INVALID && eprint)
+	Parse_Error(PARSE_FATAL, "Malformed conditional (%s)", line);
+
+    if_info = sv_if_info;
+    condExpr = sv_condExpr;
+    condPushBack = sv_condPushBack;
+
+    return rval;
+}
+
+
 /*-
  *-----------------------------------------------------------------------
  * Cond_Eval --
@@ -1241,35 +1102,31 @@ do_Cond_EvalExpression(Boolean *value)
  *	COND_SKIP	if should skip lines after the conditional
  *	COND_INVALID  	if not a valid conditional.
  *
- * Side Effects:
- *	None.
- *
  * Note that the states IF_ACTIVE and ELSE_ACTIVE are only different in order
- * to detect splurious .else lines (as are SKIP_TO_ELSE and SKIP_TO_ENDIF)
+ * to detect spurious .else lines (as are SKIP_TO_ELSE and SKIP_TO_ENDIF),
  * otherwise .else could be treated as '.elif 1'.
- *
  *-----------------------------------------------------------------------
  */
-int
+CondEvalResult
 Cond_Eval(char *line)
 {
-#define	    MAXIF      128	/* maximum depth of .if'ing */
-#define	    MAXIF_BUMP  32	/* how much to grow by */
+    enum { MAXIF = 128 };	/* maximum depth of .if'ing */
+    enum { MAXIF_BUMP = 32 };	/* how much to grow by */
     enum if_states {
 	IF_ACTIVE,		/* .if or .elif part active */
 	ELSE_ACTIVE,		/* .else part active */
 	SEARCH_FOR_ELIF,	/* searching for .elif/else to execute */
-	SKIP_TO_ELSE,           /* has been true, but not seen '.else' */
+	SKIP_TO_ELSE,		/* has been true, but not seen '.else' */
 	SKIP_TO_ENDIF		/* nothing else to execute */
     };
     static enum if_states *cond_state = NULL;
     static unsigned int max_if_depth = MAXIF;
 
     const struct If *ifp;
-    Boolean 	    isElif;
-    Boolean 	    value;
-    int	    	    level;  	/* Level at which to report errors. */
-    enum if_states  state;
+    Boolean isElif;
+    Boolean value;
+    int level;			/* Level at which to report errors. */
+    enum if_states state;
 
     level = PARSE_FATAL;
     if (!cond_state) {
@@ -1292,7 +1149,8 @@ Cond_Eval(char *line)
 	    }
 	    /* Return state for previous conditional */
 	    cond_depth--;
-	    return cond_state[cond_depth] <= ELSE_ACTIVE ? COND_PARSE : COND_SKIP;
+	    return cond_state[cond_depth] <= ELSE_ACTIVE
+		   ? COND_PARSE : COND_SKIP;
 	}
 
 	/* Quite likely this is 'else' or 'elif' */
@@ -1336,7 +1194,7 @@ Cond_Eval(char *line)
      * function is, etc. -- by looking in the table of valid "ifs"
      */
     line += 2;
-    for (ifp = ifs; ; ifp++) {
+    for (ifp = ifs;; ifp++) {
 	if (ifp->form == NULL)
 	    return COND_INVALID;
 	if (istoken(ifp->form, line, ifp->formlen)) {
@@ -1372,8 +1230,8 @@ Cond_Eval(char *line)
 	     * can need more than the default.
 	     */
 	    max_if_depth += MAXIF_BUMP;
-	    cond_state = bmake_realloc(cond_state, max_if_depth *
-		sizeof(*cond_state));
+	    cond_state = bmake_realloc(cond_state,
+				       max_if_depth * sizeof(*cond_state));
 	}
 	state = cond_state[cond_depth];
 	cond_depth++;
@@ -1400,21 +1258,6 @@ Cond_Eval(char *line)
     return COND_PARSE;
 }
 
-
-
-/*-
- *-----------------------------------------------------------------------
- * Cond_End --
- *	Make sure everything's clean at the end of a makefile.
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	Parse_Error will be called if open conditionals are around.
- *
- *-----------------------------------------------------------------------
- */
 void
 Cond_restore_depth(unsigned int saved_depth)
 {
diff --git a/dir.c b/dir.c
index 5c5e7e5c14f2..4a561deca6fc 100644
--- a/dir.c
+++ b/dir.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: dir.c,v 1.76 2020/07/03 08:13:23 rillig Exp $	*/
+/*	$NetBSD: dir.c,v 1.135 2020/09/02 04:32:13 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -70,14 +70,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: dir.c,v 1.76 2020/07/03 08:13:23 rillig Exp $";
+static char rcsid[] = "$NetBSD: dir.c,v 1.135 2020/09/02 04:32:13 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)dir.c	8.2 (Berkeley) 1/2/94";
 #else
-__RCSID("$NetBSD: dir.c,v 1.76 2020/07/03 08:13:23 rillig Exp $");
+__RCSID("$NetBSD: dir.c,v 1.135 2020/09/02 04:32:13 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -143,10 +143,20 @@ __RCSID("$NetBSD: dir.c,v 1.76 2020/07/03 08:13:23 rillig Exp $");
 #include <stdio.h>
 
 #include "make.h"
-#include "hash.h"
 #include "dir.h"
 #include "job.h"
 
+
+#define DIR_DEBUG0(fmt) \
+    if (!DEBUG(DIR)) (void) 0; else fprintf(debug_file, fmt)
+
+#define DIR_DEBUG1(fmt, arg1) \
+    if (!DEBUG(DIR)) (void) 0; else fprintf(debug_file, fmt, arg1)
+
+#define DIR_DEBUG2(fmt, arg1, arg2) \
+    if (!DEBUG(DIR)) (void) 0; else fprintf(debug_file, fmt, arg1, arg2)
+
+
 /*
  *	A search path consists of a Lst of Path structures. A Path structure
  *	has in it the name of the directory and a hash table of all the files
@@ -217,37 +227,35 @@ __RCSID("$NetBSD: dir.c,v 1.76 2020/07/03 08:13:23 rillig Exp $");
  *	in a cache for when Dir_MTime was actually called.
  */
 
-Lst          dirSearchPath;	/* main search path */
+Lst dirSearchPath;		/* main search path */
 
-static Lst   openDirectories;	/* the list of all open directories */
+static Lst openDirectories;	/* the list of all open directories */
 
 /*
  * Variables for gathering statistics on the efficiency of the hashing
  * mechanism.
  */
-static int    hits,	      /* Found in directory cache */
-	      misses,	      /* Sad, but not evil misses */
-	      nearmisses,     /* Found under search path */
-	      bigmisses;      /* Sought by itself */
+static int hits;		/* Found in directory cache */
+static int misses;		/* Sad, but not evil misses */
+static int nearmisses;		/* Found under search path */
+static int bigmisses;		/* Sought by itself */
 
-static Path    	  *dot;	    /* contents of current directory */
-static Path    	  *cur;	    /* contents of current directory, if not dot */
-static Path	  *dotLast; /* a fake path entry indicating we need to
-			     * look for . last */
-static Hash_Table mtimes;   /* Results of doing a last-resort stat in
-			     * Dir_FindFile -- if we have to go to the
-			     * system to find the file, we might as well
-			     * have its mtime on record. XXX: If this is done
-			     * way early, there's a chance other rules will
-			     * have already updated the file, in which case
-			     * we'll update it again. Generally, there won't
-			     * be two rules to update a single file, so this
-			     * should be ok, but... */
+static Path *dot;		/* contents of current directory */
+static Path *cur;		/* contents of current directory, if not dot */
+static Path *dotLast;		/* a fake path entry indicating we need to
+				 * look for . last */
 
-static Hash_Table lmtimes;  /* same as mtimes but for lstat */
+/* Results of doing a last-resort stat in Dir_FindFile -- if we have to go to
+ * the system to find the file, we might as well have its mtime on record.
+ *
+ * XXX: If this is done way early, there's a chance other rules will have
+ * already updated the file, in which case we'll update it again. Generally,
+ * there won't be two rules to update a single file, so this should be ok,
+ * but... */
+static Hash_Table mtimes;
+
+static Hash_Table lmtimes;	/* same as mtimes but for lstat */
 
-static int DirFindName(const void *, const void *);
-static int DirMatchFiles(const char *, Path *, Lst);
 static void DirExpandCurly(const char *, const char *, Lst, Lst);
 static void DirExpandInt(const char *, Lst, Lst);
 static int DirPrintWord(void *, void *);
@@ -259,23 +267,28 @@ static char *DirLookupAbs(Path *, const char *, const char *);
 
 
 /*
- * We use stat(2) a lot, cache the results
+ * We use stat(2) a lot, cache the results.
  * mtime and mode are all we care about.
  */
 struct cache_st {
-    time_t lmtime;			/* lstat */
-    time_t mtime;			/* stat */
-    mode_t  mode;
+    time_t lmtime;		/* lstat */
+    time_t mtime;		/* stat */
+    mode_t mode;
 };
 
 /* minimize changes below */
-#define CST_LSTAT 1
-#define CST_UPDATE 2
+typedef enum {
+    CST_LSTAT = 0x01,		/* call lstat(2) instead of stat(2) */
+    CST_UPDATE = 0x02		/* ignore existing cached entry */
+} CachedStatsFlags;
 
+/* Returns 0 and the result of stat(2) or lstat(2) in *mst, or -1 on error. */
 static int
-cached_stats(Hash_Table *htp, const char *pathname, struct stat *st, int flags)
+cached_stats(Hash_Table *htp, const char *pathname, struct make_stat *mst,
+	     CachedStatsFlags flags)
 {
     Hash_Entry *entry;
+    struct stat sys_st;
     struct cache_st *cst;
     int rc;
 
@@ -284,83 +297,74 @@ cached_stats(Hash_Table *htp, const char *pathname, struct stat *st, int flags)
 
     entry = Hash_FindEntry(htp, pathname);
 
-    if (entry && (flags & CST_UPDATE) == 0) {
-	cst = entry->clientPtr;
+    if (entry && !(flags & CST_UPDATE)) {
+	cst = Hash_GetValue(entry);
 
-	memset(st, 0, sizeof(*st));
-	st->st_mode = cst->mode;
-	st->st_mtime = (flags & CST_LSTAT) ? cst->lmtime : cst->mtime;
-	if (st->st_mtime) {
-	    if (DEBUG(DIR)) {
-		fprintf(debug_file, "Using cached time %s for %s\n",
-			Targ_FmtTime(st->st_mtime), pathname);
-	    }
+	mst->mst_mode = cst->mode;
+	mst->mst_mtime = (flags & CST_LSTAT) ? cst->lmtime : cst->mtime;
+	if (mst->mst_mtime) {
+	    DIR_DEBUG2("Using cached time %s for %s\n",
+		       Targ_FmtTime(mst->mst_mtime), pathname);
 	    return 0;
 	}
     }
 
-    rc = (flags & CST_LSTAT) ? lstat(pathname, st) : stat(pathname, st);
+    rc = (flags & CST_LSTAT)
+	 ? lstat(pathname, &sys_st)
+	 : stat(pathname, &sys_st);
     if (rc == -1)
 	return -1;
 
-    if (st->st_mtime == 0)
-	st->st_mtime = 1;      /* avoid confusion with missing file */
+    if (sys_st.st_mtime == 0)
+	sys_st.st_mtime = 1;	/* avoid confusion with missing file */
 
-    if (!entry)
+    mst->mst_mode = sys_st.st_mode;
+    mst->mst_mtime = sys_st.st_mtime;
+
+    if (entry == NULL)
 	entry = Hash_CreateEntry(htp, pathname, NULL);
-    if (!entry->clientPtr) {
-	entry->clientPtr = bmake_malloc(sizeof(*cst));
-	memset(entry->clientPtr, 0, sizeof(*cst));
+    if (Hash_GetValue(entry) == NULL) {
+	Hash_SetValue(entry, bmake_malloc(sizeof(*cst)));
+	memset(Hash_GetValue(entry), 0, sizeof(*cst));
     }
-    cst = entry->clientPtr;
-    if ((flags & CST_LSTAT)) {
-	cst->lmtime = st->st_mtime;
+    cst = Hash_GetValue(entry);
+    if (flags & CST_LSTAT) {
+	cst->lmtime = sys_st.st_mtime;
     } else {
-	cst->mtime = st->st_mtime;
-    }
-    cst->mode = st->st_mode;
-    if (DEBUG(DIR)) {
-	fprintf(debug_file, "   Caching %s for %s\n",
-	    Targ_FmtTime(st->st_mtime), pathname);
+	cst->mtime = sys_st.st_mtime;
     }
+    cst->mode = sys_st.st_mode;
+    DIR_DEBUG2("   Caching %s for %s\n",
+	       Targ_FmtTime(sys_st.st_mtime), pathname);
 
     return 0;
 }
 
 int
-cached_stat(const char *pathname, void *st)
+cached_stat(const char *pathname, struct make_stat *st)
 {
     return cached_stats(&mtimes, pathname, st, 0);
 }
 
 int
-cached_lstat(const char *pathname, void *st)
+cached_lstat(const char *pathname, struct make_stat *st)
 {
     return cached_stats(&lmtimes, pathname, st, CST_LSTAT);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Dir_Init --
- *	initialize things for this module
- *
- * Results:
- *	none
- *
- * Side Effects:
- *	some directories may be opened.
- *-----------------------------------------------------------------------
- */
+/* Initialize things for this module. */
 void
-Dir_Init(const char *cdname)
+Dir_Init(void)
+{
+    dirSearchPath = Lst_Init();
+    openDirectories = Lst_Init();
+    Hash_InitTable(&mtimes, 0);
+    Hash_InitTable(&lmtimes, 0);
+}
+
+void
+Dir_InitDir(const char *cdname)
 {
-    if (!cdname) {
-	dirSearchPath = Lst_Init(FALSE);
-	openDirectories = Lst_Init(FALSE);
-	Hash_InitTable(&mtimes, 0);
-	Hash_InitTable(&lmtimes, 0);
-	return;
-    }
     Dir_InitCur(cdname);
 
     dotLast = bmake_malloc(sizeof(Path));
@@ -371,7 +375,7 @@ Dir_Init(const char *cdname)
 }
 
 /*
- * Called by Dir_Init() and whenever .CURDIR is assigned to.
+ * Called by Dir_InitDir and whenever .CURDIR is assigned to.
  */
 void
 Dir_InitCur(const char *cdname)
@@ -397,18 +401,8 @@ Dir_InitCur(const char *cdname)
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Dir_InitDot --
- *	(re)initialize "dot" (current/object directory) path hash
- *
- * Results:
- *	none
- *
- * Side Effects:
- *	some directories may be opened.
- *-----------------------------------------------------------------------
- */
+/* (Re)initialize "dot" (current/object directory) path hash.
+ * Some directories may be opened. */
 void
 Dir_InitDot(void)
 {
@@ -416,8 +410,8 @@ Dir_InitDot(void)
 	LstNode ln;
 
 	/* Remove old entry from openDirectories, but do not destroy. */
-	ln = Lst_Member(openDirectories, dot);
-	(void)Lst_Remove(openDirectories, ln);
+	ln = Lst_FindDatum(openDirectories, dot);
+	Lst_Remove(openDirectories, ln);
     }
 
     dot = Dir_AddDir(NULL, ".");
@@ -432,21 +426,10 @@ Dir_InitDot(void)
      * to make sure it's not destroyed.
      */
     dot->refCount += 1;
-    Dir_SetPATH();			/* initialize */
+    Dir_SetPATH();		/* initialize */
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Dir_End --
- *	cleanup things for this module
- *
- * Results:
- *	none
- *
- * Side Effects:
- *	none
- *-----------------------------------------------------------------------
- */
+/* Clean up things for this module. */
 void
 Dir_End(void)
 {
@@ -460,9 +443,9 @@ Dir_End(void)
     Dir_Destroy(dotLast);
     Dir_Destroy(dot);
     Dir_ClearPath(dirSearchPath);
-    Lst_Destroy(dirSearchPath, NULL);
+    Lst_Free(dirSearchPath);
     Dir_ClearPath(openDirectories);
-    Lst_Destroy(openDirectories, NULL);
+    Lst_Free(openDirectories);
     Hash_DeleteTable(&mtimes);
 #endif
 }
@@ -475,122 +458,100 @@ Dir_End(void)
 void
 Dir_SetPATH(void)
 {
-    LstNode       ln;		/* a list element */
+    LstNode ln;			/* a list element */
     Path *p;
-    Boolean	  hasLastDot = FALSE;	/* true we should search dot last */
+    Boolean hasLastDot = FALSE;	/* true if we should search dot last */
 
     Var_Delete(".PATH", VAR_GLOBAL);
 
-    if (Lst_Open(dirSearchPath) == SUCCESS) {
-	if ((ln = Lst_First(dirSearchPath)) != NULL) {
-	    p = (Path *)Lst_Datum(ln);
-	    if (p == dotLast) {
-		hasLastDot = TRUE;
-		Var_Append(".PATH", dotLast->name, VAR_GLOBAL);
-	    }
+    Lst_Open(dirSearchPath);
+    if ((ln = Lst_First(dirSearchPath)) != NULL) {
+	p = LstNode_Datum(ln);
+	if (p == dotLast) {
+	    hasLastDot = TRUE;
+	    Var_Append(".PATH", dotLast->name, VAR_GLOBAL);
 	}
-
-	if (!hasLastDot) {
-	    if (dot)
-		Var_Append(".PATH", dot->name, VAR_GLOBAL);
-	    if (cur)
-		Var_Append(".PATH", cur->name, VAR_GLOBAL);
-	}
-
-	while ((ln = Lst_Next(dirSearchPath)) != NULL) {
-	    p = (Path *)Lst_Datum(ln);
-	    if (p == dotLast)
-		continue;
-	    if (p == dot && hasLastDot)
-		continue;
-	    Var_Append(".PATH", p->name, VAR_GLOBAL);
-	}
-
-	if (hasLastDot) {
-	    if (dot)
-		Var_Append(".PATH", dot->name, VAR_GLOBAL);
-	    if (cur)
-		Var_Append(".PATH", cur->name, VAR_GLOBAL);
-	}
-	Lst_Close(dirSearchPath);
     }
+
+    if (!hasLastDot) {
+	if (dot)
+	    Var_Append(".PATH", dot->name, VAR_GLOBAL);
+	if (cur)
+	    Var_Append(".PATH", cur->name, VAR_GLOBAL);
+    }
+
+    while ((ln = Lst_Next(dirSearchPath)) != NULL) {
+	p = LstNode_Datum(ln);
+	if (p == dotLast)
+	    continue;
+	if (p == dot && hasLastDot)
+	    continue;
+	Var_Append(".PATH", p->name, VAR_GLOBAL);
+    }
+
+    if (hasLastDot) {
+	if (dot)
+	    Var_Append(".PATH", dot->name, VAR_GLOBAL);
+	if (cur)
+	    Var_Append(".PATH", cur->name, VAR_GLOBAL);
+    }
+    Lst_Close(dirSearchPath);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * DirFindName --
- *	See if the Path structure describes the same directory as the
- *	given one by comparing their names. Called from Dir_AddDir via
- *	Lst_Find when searching the list of open directories.
- *
- * Input:
- *	p		Current name
- *	dname		Desired name
- *
- * Results:
- *	0 if it is the same. Non-zero otherwise
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
- */
-static int
-DirFindName(const void *p, const void *dname)
+/* See if the Path structure describes the same directory as the
+ * given one by comparing their names. Called from Dir_AddDir via
+ * Lst_Find when searching the list of open directories. */
+static Boolean
+DirFindName(const void *p, const void *desiredName)
 {
-    return strcmp(((const Path *)p)->name, dname);
+    return strcmp(((const Path *)p)->name, desiredName) == 0;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Dir_HasWildcards  --
- *	see if the given name has any wildcard characters in it
- *	be careful not to expand unmatching brackets or braces.
- *	XXX: This code is not 100% correct. ([^]] fails etc.)
- *	I really don't think that make(1) should be expanding
- *	patterns, because then you have to set a mechanism for
- *	escaping the expansion!
+/* See if the given name has any wildcard characters in it. Be careful not to
+ * expand unmatching brackets or braces.
+ *
+ * XXX: This code is not 100% correct ([^]] fails etc.). I really don't think
+ * that make(1) should be expanding patterns, because then you have to set a
+ * mechanism for escaping the expansion!
  *
  * Input:
  *	name		name to check
  *
  * Results:
  *	returns TRUE if the word should be expanded, FALSE otherwise
- *
- * Side Effects:
- *	none
- *-----------------------------------------------------------------------
  */
 Boolean
-Dir_HasWildcards(char *name)
+Dir_HasWildcards(const char *name)
 {
-    char *cp;
-    int wild = 0, brace = 0, bracket = 0;
+    const char *cp;
+    Boolean wild = FALSE;
+    int braces = 0, brackets = 0;
 
     for (cp = name; *cp; cp++) {
-	switch(*cp) {
+	switch (*cp) {
 	case '{':
-		brace++;
-		wild = 1;
-		break;
+	    braces++;
+	    wild = TRUE;
+	    break;
 	case '}':
-		brace--;
-		break;
+	    braces--;
+	    break;
 	case '[':
-		bracket++;
-		wild = 1;
-		break;
+	    brackets++;
+	    wild = TRUE;
+	    break;
 	case ']':
-		bracket--;
-		break;
+	    brackets--;
+	    break;
 	case '?':
 	case '*':
-		wild = 1;
-		break;
+	    wild = TRUE;
+	    break;
 	default:
-		break;
+	    break;
 	}
     }
-    return wild && bracket == 0 && brace == 0;
+    return wild && brackets == 0 && braces == 0;
 }
 
 /*-
@@ -607,20 +568,17 @@ Dir_HasWildcards(char *name)
  *	p		Directory to search
  *	expansion	Place to store the results
  *
- * Results:
- *	Always returns 0
- *
  * Side Effects:
  *	File names are added to the expansions lst. The directory will be
  *	fully hashed when this is done.
  *-----------------------------------------------------------------------
  */
-static int
+static void
 DirMatchFiles(const char *pattern, Path *p, Lst expansions)
 {
-    Hash_Search	  search;   	/* Index into the directory's table */
-    Hash_Entry	  *entry;   	/* Current entry in the table */
-    Boolean 	  isDot;    	/* TRUE if the directory being searched is . */
+    Hash_Search search;		/* Index into the directory's table */
+    Hash_Entry *entry;		/* Current entry in the table */
+    Boolean isDot;		/* TRUE if the directory being searched is . */
 
     isDot = (*p->name == '.' && p->name[1] == '\0');
 
@@ -638,13 +596,75 @@ DirMatchFiles(const char *pattern, Path *p, Lst expansions)
 	    ((entry->name[0] != '.') ||
 	     (pattern[0] == '.')))
 	{
-	    (void)Lst_AtEnd(expansions,
-			    (isDot ? bmake_strdup(entry->name) :
-			     str_concat(p->name, entry->name,
-					STR_ADDSLASH)));
+	    Lst_Append(expansions,
+			(isDot ? bmake_strdup(entry->name) :
+			 str_concat3(p->name, "/", entry->name)));
 	}
     }
-    return 0;
+}
+
+/* Find the next closing brace in the string, taking nested braces into
+ * account. */
+static const char *
+closing_brace(const char *p)
+{
+    int nest = 0;
+    while (*p != '\0') {
+	if (*p == '}' && nest == 0)
+	    break;
+	if (*p == '{')
+	    nest++;
+	if (*p == '}')
+	    nest--;
+	p++;
+    }
+    return p;
+}
+
+/* Find the next closing brace or comma in the string, taking nested braces
+ * into account. */
+static const char *
+separator_comma(const char *p)
+{
+    int nest = 0;
+    while (*p != '\0') {
+	if ((*p == '}' || *p == ',') && nest == 0)
+	    break;
+	if (*p == '{')
+	    nest++;
+	if (*p == '}')
+	    nest--;
+	p++;
+    }
+    return p;
+}
+
+static Boolean
+contains_wildcard(const char *p)
+{
+    for (; *p != '\0'; p++) {
+	switch (*p) {
+	case '*':
+	case '?':
+	case '{':
+	case '[':
+	    return TRUE;
+	}
+    }
+    return FALSE;
+}
+
+static char *
+concat3(const char *a, size_t a_len, const char *b, size_t b_len,
+	const char *c, size_t c_len)
+{
+    size_t s_len = a_len + b_len + c_len;
+    char *s = bmake_malloc(s_len + 1);
+    memcpy(s, a, a_len);
+    memcpy(s + a_len, b, b_len);
+    memcpy(s + a_len + b_len, c, c_len);
+    s[s_len] = '\0';
+    return s;
 }
 
 /*-
@@ -672,91 +692,41 @@ DirMatchFiles(const char *pattern, Path *p, Lst expansions)
 static void
 DirExpandCurly(const char *word, const char *brace, Lst path, Lst expansions)
 {
-    const char   *end;	    	/* Character after the closing brace */
-    const char   *cp;	    	/* Current position in brace clause */
-    const char   *start;   	/* Start of current piece of brace clause */
-    int	    	  bracelevel;	/* Number of braces we've seen. If we see a
-				 * right brace when this is 0, we've hit the
-				 * end of the clause. */
-    char    	 *file;    	/* Current expansion */
-    int	    	  otherLen; 	/* The length of the other pieces of the
-				 * expansion (chars before and after the
-				 * clause in 'word') */
-    char    	 *cp2;	    	/* Pointer for checking for wildcards in
-				 * expansion before calling Dir_Expand */
+    const char *prefix, *middle, *piece, *middle_end, *suffix;
+    size_t prefix_len, suffix_len;
 
-    start = brace+1;
+    /* Split the word into prefix '{' middle '}' suffix. */
 
-    /*
-     * Find the end of the brace clause first, being wary of nested brace
-     * clauses.
-     */
-    for (end = start, bracelevel = 0; *end != '\0'; end++) {
-	if (*end == '{') {
-	    bracelevel++;
-	} else if ((*end == '}') && (bracelevel-- == 0)) {
-	    break;
-	}
-    }
-    if (*end == '\0') {
-	Error("Unterminated {} clause \"%s\"", start);
+    middle = brace + 1;
+    middle_end = closing_brace(middle);
+    if (*middle_end == '\0') {
+	Error("Unterminated {} clause \"%s\"", middle);
 	return;
-    } else {
-	end++;
     }
-    otherLen = brace - word + strlen(end);
 
-    for (cp = start; cp < end; cp++) {
-	/*
-	 * Find the end of this piece of the clause.
-	 */
-	bracelevel = 0;
-	while (*cp != ',') {
-	    if (*cp == '{') {
-		bracelevel++;
-	    } else if ((*cp == '}') && (bracelevel-- <= 0)) {
-		break;
-	    }
-	    cp++;
-	}
-	/*
-	 * Allocate room for the combination and install the three pieces.
-	 */
-	file = bmake_malloc(otherLen + cp - start + 1);
-	if (brace != word) {
-	    strncpy(file, word, brace-word);
-	}
-	if (cp != start) {
-	    strncpy(&file[brace-word], start, cp-start);
-	}
-	strcpy(&file[(brace-word)+(cp-start)], end);
+    prefix = word;
+    prefix_len = (size_t)(brace - prefix);
+    suffix = middle_end + 1;
+    suffix_len = strlen(suffix);
 
-	/*
-	 * See if the result has any wildcards in it. If we find one, call
-	 * Dir_Expand right away, telling it to place the result on our list
-	 * of expansions.
-	 */
-	for (cp2 = file; *cp2 != '\0'; cp2++) {
-	    switch(*cp2) {
-	    case '*':
-	    case '?':
-	    case '{':
-	    case '[':
-		Dir_Expand(file, path, expansions);
-		goto next;
-	    }
-	}
-	if (*cp2 == '\0') {
-	    /*
-	     * Hit the end w/o finding any wildcards, so stick the expansion
-	     * on the end of the list.
-	     */
-	    (void)Lst_AtEnd(expansions, file);
-	} else {
-	next:
+    /* Split the middle into pieces, separated by commas. */
+
+    piece = middle;
+    while (piece < middle_end + 1) {
+	const char *piece_end = separator_comma(piece);
+	size_t piece_len = (size_t)(piece_end - piece);
+
+	char *file = concat3(prefix, prefix_len, piece, piece_len,
+			     suffix, suffix_len);
+
+	if (contains_wildcard(file)) {
+	    Dir_Expand(file, path, expansions);
 	    free(file);
+	} else {
+	    Lst_Append(expansions, file);
 	}
-	start = cp+1;
+
+	piece = piece_end + 1;	/* skip over the comma or closing brace */
     }
 }
 
@@ -784,32 +754,18 @@ DirExpandCurly(const char *word, const char *brace, Lst path, Lst expansions)
 static void
 DirExpandInt(const char *word, Lst path, Lst expansions)
 {
-    LstNode 	  ln;	    	/* Current node */
-    Path	  *p;	    	/* Directory in the node */
+    LstNode ln;			/* Current node */
 
-    if (Lst_Open(path) == SUCCESS) {
-	while ((ln = Lst_Next(path)) != NULL) {
-	    p = (Path *)Lst_Datum(ln);
-	    DirMatchFiles(word, p, expansions);
-	}
-	Lst_Close(path);
+    Lst_Open(path);
+    while ((ln = Lst_Next(path)) != NULL) {
+	Path *p = LstNode_Datum(ln);
+	DirMatchFiles(word, p, expansions);
     }
+    Lst_Close(path);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * DirPrintWord --
- *	Print a word in the list of expansions. Callback for Dir_Expand
- *	when DEBUG(DIR), via Lst_ForEach.
- *
- * Results:
- *	=== 0
- *
- * Side Effects:
- *	The passed word is printed, followed by a space.
- *
- *-----------------------------------------------------------------------
- */
+/* Print a word in the list of expansions.
+ * Callback for Dir_Expand when DEBUG(DIR), via Lst_ForEach. */
 static int
 DirPrintWord(void *word, void *dummy MAKE_ATTR_UNUSED)
 {
@@ -836,16 +792,18 @@ DirPrintWord(void *word, void *dummy MAKE_ATTR_UNUSED)
  *
  * Side Effects:
  *	Directories may be opened. Who knows?
+ *	Undefined behavior if the word is really in read-only memory.
  *-----------------------------------------------------------------------
  */
 void
 Dir_Expand(const char *word, Lst path, Lst expansions)
 {
-    const char    	  *cp;
+    const char *cp;
 
-    if (DEBUG(DIR)) {
-	fprintf(debug_file, "Expanding \"%s\"... ", word);
-    }
+    assert(path != NULL);
+    assert(expansions != NULL);
+
+    DIR_DEBUG1("Expanding \"%s\"... ", word);
 
     cp = strchr(word, '{');
     if (cp) {
@@ -872,13 +830,12 @@ Dir_Expand(const char *word, Lst path, Lst expansions)
 		/*
 		 * Back up to the start of the component
 		 */
-		char  *dirpath;
-
 		while (cp > word && *cp != '/') {
 		    cp--;
 		}
 		if (cp != word) {
 		    char sc;
+		    char *dirpath;
 		    /*
 		     * If the glob isn't in the first component, try and find
 		     * all the components up to the one with a wildcard.
@@ -898,10 +855,10 @@ Dir_Expand(const char *word, Lst path, Lst expansions)
 			char *dp = &dirpath[strlen(dirpath) - 1];
 			if (*dp == '/')
 			    *dp = '\0';
-			path = Lst_Init(FALSE);
+			path = Lst_Init();
 			(void)Dir_AddDir(path, dirpath);
-			DirExpandInt(cp+1, path, expansions);
-			Lst_Destroy(path, NULL);
+			DirExpandInt(cp + 1, path, expansions);
+			Lst_Free(path);
 		    }
 		} else {
 		    /*
@@ -948,21 +905,17 @@ Dir_Expand(const char *word, Lst path, Lst expansions)
  */
 static char *
 DirLookup(Path *p, const char *name MAKE_ATTR_UNUSED, const char *cp,
-          Boolean hasSlash MAKE_ATTR_UNUSED)
+	  Boolean hasSlash MAKE_ATTR_UNUSED)
 {
-    char *file;		/* the current filename to check */
+    char *file;			/* the current filename to check */
 
-    if (DEBUG(DIR)) {
-	fprintf(debug_file, "   %s ...\n", p->name);
-    }
+    DIR_DEBUG1("   %s ...\n", p->name);
 
     if (Hash_FindEntry(&p->files, cp) == NULL)
 	return NULL;
 
-    file = str_concat(p->name, cp, STR_ADDSLASH);
-    if (DEBUG(DIR)) {
-	fprintf(debug_file, "   returning %s\n", file);
-    }
+    file = str_concat3(p->name, "/", cp);
+    DIR_DEBUG1("   returning %s\n", file);
     p->hits += 1;
     hits += 1;
     return file;
@@ -986,11 +939,11 @@ DirLookup(Path *p, const char *name MAKE_ATTR_UNUSED, const char *cp,
 static char *
 DirLookupSubdir(Path *p, const char *name)
 {
-    struct stat	  stb;		/* Buffer for stat, if necessary */
-    char 	 *file;		/* the current filename to check */
+    struct make_stat mst;
+    char *file;			/* the current filename to check */
 
     if (p != dot) {
-	file = str_concat(p->name, name, STR_ADDSLASH);
+	file = str_concat3(p->name, "/", name);
     } else {
 	/*
 	 * Checking in dot -- DON'T put a leading ./ on the thing.
@@ -998,11 +951,9 @@ DirLookupSubdir(Path *p, const char *name)
 	file = bmake_strdup(name);
     }
 
-    if (DEBUG(DIR)) {
-	fprintf(debug_file, "checking %s ...\n", file);
-    }
+    DIR_DEBUG1("checking %s ...\n", file);
 
-    if (cached_stat(file, &stb) == 0) {
+    if (cached_stat(file, &mst) == 0) {
 	nearmisses += 1;
 	return file;
     }
@@ -1028,40 +979,34 @@ DirLookupSubdir(Path *p, const char *name)
 static char *
 DirLookupAbs(Path *p, const char *name, const char *cp)
 {
-	char *p1;		/* pointer into p->name */
-	const char *p2;		/* pointer into name */
+    char *p1;			/* pointer into p->name */
+    const char *p2;		/* pointer into name */
 
-	if (DEBUG(DIR)) {
-		fprintf(debug_file, "   %s ...\n", p->name);
-	}
+    DIR_DEBUG1("   %s ...\n", p->name);
 
-	/*
-	 * If the file has a leading path component and that component
-	 * exactly matches the entire name of the current search
-	 * directory, we can attempt another cache lookup. And if we don't
-	 * have a hit, we can safely assume the file does not exist at all.
-	 */
-	for (p1 = p->name, p2 = name; *p1 && *p1 == *p2; p1++, p2++) {
-		continue;
-	}
-	if (*p1 != '\0' || p2 != cp - 1) {
-		return NULL;
-	}
+    /*
+     * If the file has a leading path component and that component
+     * exactly matches the entire name of the current search
+     * directory, we can attempt another cache lookup. And if we don't
+     * have a hit, we can safely assume the file does not exist at all.
+     */
+    for (p1 = p->name, p2 = name; *p1 && *p1 == *p2; p1++, p2++) {
+	continue;
+    }
+    if (*p1 != '\0' || p2 != cp - 1) {
+	return NULL;
+    }
 
-	if (Hash_FindEntry(&p->files, cp) == NULL) {
-		if (DEBUG(DIR)) {
-			fprintf(debug_file, "   must be here but isn't -- returning\n");
-		}
-		/* Return empty string: terminates search */
-		return bmake_strdup("");
-	}
+    if (Hash_FindEntry(&p->files, cp) == NULL) {
+	DIR_DEBUG0("   must be here but isn't -- returning\n");
+	/* Return empty string: terminates search */
+	return bmake_strdup("");
+    }
 
-	p->hits += 1;
-	hits += 1;
-	if (DEBUG(DIR)) {
-		fprintf(debug_file, "   returning %s\n", name);
-	}
-	return bmake_strdup(name);
+    p->hits += 1;
+    hits += 1;
+    DIR_DEBUG1("   returning %s\n", name);
+    return bmake_strdup(name);
 }
 
 /*-
@@ -1081,25 +1026,20 @@ static char *
 DirFindDot(Boolean hasSlash MAKE_ATTR_UNUSED, const char *name, const char *cp)
 {
 
-	if (Hash_FindEntry(&dot->files, cp) != NULL) {
-	    if (DEBUG(DIR)) {
-		fprintf(debug_file, "   in '.'\n");
-	    }
-	    hits += 1;
-	    dot->hits += 1;
-	    return bmake_strdup(name);
-	}
-	if (cur &&
-	    Hash_FindEntry(&cur->files, cp) != NULL) {
-	    if (DEBUG(DIR)) {
-		fprintf(debug_file, "   in ${.CURDIR} = %s\n", cur->name);
-	    }
-	    hits += 1;
-	    cur->hits += 1;
-	    return str_concat(cur->name, cp, STR_ADDSLASH);
-	}
+    if (Hash_FindEntry(&dot->files, cp) != NULL) {
+	DIR_DEBUG0("   in '.'\n");
+	hits += 1;
+	dot->hits += 1;
+	return bmake_strdup(name);
+    }
+    if (cur && Hash_FindEntry(&cur->files, cp) != NULL) {
+	DIR_DEBUG1("   in ${.CURDIR} = %s\n", cur->name);
+	hits += 1;
+	cur->hits += 1;
+	return str_concat3(cur->name, "/", cp);
+    }
 
-	return NULL;
+    return NULL;
 }
 
 /*-
@@ -1127,14 +1067,14 @@ DirFindDot(Boolean hasSlash MAKE_ATTR_UNUSED, const char *name, const char *cp)
 char *
 Dir_FindFile(const char *name, Lst path)
 {
-    LstNode       ln;			/* a list element */
-    char	  *file;		/* the current filename to check */
-    Path	  *p;			/* current path member */
-    const char	  *cp;			/* Terminal name of file */
-    Boolean	  hasLastDot = FALSE;	/* true we should search dot last */
-    Boolean	  hasSlash;		/* true if 'name' contains a / */
-    struct stat	  stb;			/* Buffer for stat, if necessary */
-    const char   *trailing_dot = ".";
+    LstNode ln;			/* a list element */
+    char *file;			/* the current filename to check */
+    Path *p;			/* current path member */
+    const char *cp;		/* Terminal name of file */
+    Boolean hasLastDot = FALSE;	/* true we should search dot last */
+    Boolean hasSlash;		/* true if 'name' contains a / */
+    struct make_stat mst;	/* Buffer for stat, if necessary */
+    const char *trailing_dot = ".";
 
     /*
      * Find the final component of the name and note whether it has a
@@ -1149,29 +1089,23 @@ Dir_FindFile(const char *name, Lst path)
 	cp = name;
     }
 
-    if (DEBUG(DIR)) {
-	fprintf(debug_file, "Searching for %s ...", name);
-    }
+    DIR_DEBUG1("Searching for %s ...", name);
 
-    if (Lst_Open(path) == FAILURE) {
-	if (DEBUG(DIR)) {
-	    fprintf(debug_file, "couldn't open path, file not found\n");
-	}
+    if (path == NULL) {
+	DIR_DEBUG0("couldn't open path, file not found\n");
 	misses += 1;
 	return NULL;
     }
 
+    Lst_Open(path);
     if ((ln = Lst_First(path)) != NULL) {
-	p = (Path *)Lst_Datum(ln);
+	p = LstNode_Datum(ln);
 	if (p == dotLast) {
 	    hasLastDot = TRUE;
-            if (DEBUG(DIR))
-		fprintf(debug_file, "[dot last]...");
+	    DIR_DEBUG0("[dot last]...");
 	}
     }
-    if (DEBUG(DIR)) {
-	fprintf(debug_file, "\n");
-    }
+    DIR_DEBUG0("\n");
 
     /*
      * If there's no leading directory components or if the leading
@@ -1179,41 +1113,39 @@ Dir_FindFile(const char *name, Lst path)
      * of each of the directories on the search path.
      */
     if (!hasSlash || (cp - name == 2 && *name == '.')) {
-	    /*
-	     * We look through all the directories on the path seeking one which
-	     * contains the final component of the given name.  If such a beast
-	     * is found, we concatenate the directory name and the final
-	     * component and return the resulting string. If we don't find any
-	     * such thing, we go on to phase two...
-	     *
-	     * No matter what, we always look for the file in the current
-	     * directory before anywhere else (unless we found the magic
-	     * DOTLAST path, in which case we search it last) and we *do not*
-	     * add the ./ to it if it exists.
-	     * This is so there are no conflicts between what the user
-	     * specifies (fish.c) and what pmake finds (./fish.c).
-	     */
-	    if (!hasLastDot &&
-			(file = DirFindDot(hasSlash, name, cp)) != NULL) {
-		    Lst_Close(path);
-		    return file;
-	    }
+	/*
+	 * We look through all the directories on the path seeking one which
+	 * contains the final component of the given name.  If such a beast
+	 * is found, we concatenate the directory name and the final
+	 * component and return the resulting string. If we don't find any
+	 * such thing, we go on to phase two...
+	 *
+	 * No matter what, we always look for the file in the current
+	 * directory before anywhere else (unless we found the magic
+	 * DOTLAST path, in which case we search it last) and we *do not*
+	 * add the ./ to it if it exists.
+	 * This is so there are no conflicts between what the user
+	 * specifies (fish.c) and what pmake finds (./fish.c).
+	 */
+	if (!hasLastDot && (file = DirFindDot(hasSlash, name, cp)) != NULL) {
+	    Lst_Close(path);
+	    return file;
+	}
 
-	    while ((ln = Lst_Next(path)) != NULL) {
-		p = (Path *)Lst_Datum(ln);
-		if (p == dotLast)
-		    continue;
-		if ((file = DirLookup(p, name, cp, hasSlash)) != NULL) {
-		    Lst_Close(path);
-		    return file;
-		}
+	while ((ln = Lst_Next(path)) != NULL) {
+	    p = LstNode_Datum(ln);
+	    if (p == dotLast)
+		continue;
+	    if ((file = DirLookup(p, name, cp, hasSlash)) != NULL) {
+		Lst_Close(path);
+		return file;
 	    }
+	}
 
-	    if (hasLastDot &&
-			(file = DirFindDot(hasSlash, name, cp)) != NULL) {
-		    Lst_Close(path);
-		    return file;
-	    }
+	if (hasLastDot && (file = DirFindDot(hasSlash, name, cp)) != NULL) {
+	    Lst_Close(path);
+	    return file;
+	}
     }
     Lst_Close(path);
 
@@ -1232,9 +1164,7 @@ Dir_FindFile(const char *name, Lst path)
      * This phase is only performed if the file is *not* absolute.
      */
     if (!hasSlash) {
-	if (DEBUG(DIR)) {
-	    fprintf(debug_file, "   failed.\n");
-	}
+	DIR_DEBUG0("   failed.\n");
 	misses += 1;
 	return NULL;
     }
@@ -1245,30 +1175,28 @@ Dir_FindFile(const char *name, Lst path)
     }
 
     if (name[0] != '/') {
-	Boolean	checkedDot = FALSE;
+	Boolean checkedDot = FALSE;
 
-	if (DEBUG(DIR)) {
-	    fprintf(debug_file, "   Trying subdirectories...\n");
-	}
+	DIR_DEBUG0("   Trying subdirectories...\n");
 
 	if (!hasLastDot) {
-		if (dot) {
-			checkedDot = TRUE;
-			if ((file = DirLookupSubdir(dot, name)) != NULL)
-				return file;
-		}
-		if (cur && (file = DirLookupSubdir(cur, name)) != NULL)
-			return file;
+	    if (dot) {
+		checkedDot = TRUE;
+		if ((file = DirLookupSubdir(dot, name)) != NULL)
+		    return file;
+	    }
+	    if (cur && (file = DirLookupSubdir(cur, name)) != NULL)
+		return file;
 	}
 
-	(void)Lst_Open(path);
+	Lst_Open(path);
 	while ((ln = Lst_Next(path)) != NULL) {
-	    p = (Path *)Lst_Datum(ln);
+	    p = LstNode_Datum(ln);
 	    if (p == dotLast)
 		continue;
 	    if (p == dot) {
-		    if (checkedDot)
-			    continue;
+		if (checkedDot)
+		    continue;
 		checkedDot = TRUE;
 	    }
 	    if ((file = DirLookupSubdir(p, name)) != NULL) {
@@ -1279,13 +1207,13 @@ Dir_FindFile(const char *name, Lst path)
 	Lst_Close(path);
 
 	if (hasLastDot) {
-		if (dot && !checkedDot) {
-			checkedDot = TRUE;
-			if ((file = DirLookupSubdir(dot, name)) != NULL)
-				return file;
-		}
-		if (cur && (file = DirLookupSubdir(cur, name)) != NULL)
-			return file;
+	    if (dot && !checkedDot) {
+		checkedDot = TRUE;
+		if ((file = DirLookupSubdir(dot, name)) != NULL)
+		    return file;
+	    }
+	    if (cur && (file = DirLookupSubdir(cur, name)) != NULL)
+		return file;
 	}
 
 	if (checkedDot) {
@@ -1293,9 +1221,7 @@ Dir_FindFile(const char *name, Lst path)
 	     * Already checked by the given name, since . was in the path,
 	     * so no point in proceeding...
 	     */
-	    if (DEBUG(DIR)) {
-		fprintf(debug_file, "   Checked . already, returning NULL\n");
-	    }
+	    DIR_DEBUG0("   Checked . already, returning NULL\n");
 	    return NULL;
 	}
 
@@ -1310,12 +1236,10 @@ Dir_FindFile(const char *name, Lst path)
 	 * file does not exist at all.  This is signified by DirLookupAbs()
 	 * returning an empty string.
 	 */
-	if (DEBUG(DIR)) {
-	    fprintf(debug_file, "   Trying exact path matches...\n");
-	}
+	DIR_DEBUG0("   Trying exact path matches...\n");
 
-	if (!hasLastDot && cur && ((file = DirLookupAbs(cur, name, cp))
-		!= NULL)) {
+	if (!hasLastDot && cur &&
+	    ((file = DirLookupAbs(cur, name, cp)) != NULL)) {
 	    if (file[0] == '\0') {
 		free(file);
 		return NULL;
@@ -1323,9 +1247,9 @@ Dir_FindFile(const char *name, Lst path)
 	    return file;
 	}
 
-	(void)Lst_Open(path);
+	Lst_Open(path);
 	while ((ln = Lst_Next(path)) != NULL) {
-	    p = (Path *)Lst_Datum(ln);
+	    p = LstNode_Datum(ln);
 	    if (p == dotLast)
 		continue;
 	    if ((file = DirLookupAbs(p, name, cp)) != NULL) {
@@ -1339,8 +1263,8 @@ Dir_FindFile(const char *name, Lst path)
 	}
 	Lst_Close(path);
 
-	if (hasLastDot && cur && ((file = DirLookupAbs(cur, name, cp))
-		!= NULL)) {
+	if (hasLastDot && cur &&
+	    ((file = DirLookupAbs(cur, name, cp)) != NULL)) {
 	    if (file[0] == '\0') {
 		free(file);
 		return NULL;
@@ -1380,7 +1304,7 @@ Dir_FindFile(const char *name, Lst path)
     if (ln == NULL) {
 	return NULL;
     } else {
-	p = (Path *)Lst_Datum(ln);
+	p = LstNode_Datum(ln);
     }
 
     if (Hash_FindEntry(&p->files, cp) != NULL) {
@@ -1389,18 +1313,14 @@ Dir_FindFile(const char *name, Lst path)
 	return NULL;
     }
 #else /* !notdef */
-    if (DEBUG(DIR)) {
-	fprintf(debug_file, "   Looking for \"%s\" ...\n", name);
-    }
+    DIR_DEBUG1("   Looking for \"%s\" ...\n", name);
 
     bigmisses += 1;
-    if (cached_stat(name, &stb) == 0) {
+    if (cached_stat(name, &mst) == 0) {
 	return bmake_strdup(name);
     }
 
-    if (DEBUG(DIR)) {
-	fprintf(debug_file, "   failed. Returning NULL\n");
-    }
+    DIR_DEBUG0("   failed. Returning NULL\n");
     return NULL;
 #endif /* notdef */
 }
@@ -1416,7 +1336,7 @@ Dir_FindFile(const char *name, Lst path)
  *	here		starting directory
  *	search_path	the path we are looking for
  *	result		the result of a successful search is placed here
- *	rlen		the length of the result buffer
+ *	result_len	the length of the result buffer
  *			(typically MAXPATHLEN + 1)
  *
  * Results:
@@ -1426,62 +1346,57 @@ Dir_FindFile(const char *name, Lst path)
  * Side Effects:
  *-----------------------------------------------------------------------
  */
-int
-Dir_FindHereOrAbove(char *here, char *search_path, char *result, int rlen) {
+Boolean
+Dir_FindHereOrAbove(const char *here, const char *search_path,
+		    char *result, int result_len)
+{
+    struct make_stat mst;
+    char dirbase[MAXPATHLEN + 1], *dirbase_end;
+    char try[MAXPATHLEN + 1], *try_end;
 
-	struct stat st;
-	char dirbase[MAXPATHLEN + 1], *db_end;
-        char try[MAXPATHLEN + 1], *try_end;
+    /* copy out our starting point */
+    snprintf(dirbase, sizeof(dirbase), "%s", here);
+    dirbase_end = dirbase + strlen(dirbase);
 
-	/* copy out our starting point */
-	snprintf(dirbase, sizeof(dirbase), "%s", here);
-	db_end = dirbase + strlen(dirbase);
+    /* loop until we determine a result */
+    while (TRUE) {
 
-	/* loop until we determine a result */
-	while (1) {
+	/* try and stat(2) it ... */
+	snprintf(try, sizeof(try), "%s/%s", dirbase, search_path);
+	if (cached_stat(try, &mst) != -1) {
+	    /*
+	     * success!  if we found a file, chop off
+	     * the filename so we return a directory.
+	     */
+	    if ((mst.mst_mode & S_IFMT) != S_IFDIR) {
+		try_end = try + strlen(try);
+		while (try_end > try && *try_end != '/')
+		    try_end--;
+		if (try_end > try)
+		    *try_end = '\0';	/* chop! */
+	    }
 
-		/* try and stat(2) it ... */
-		snprintf(try, sizeof(try), "%s/%s", dirbase, search_path);
-		if (cached_stat(try, &st) != -1) {
-			/*
-			 * success!  if we found a file, chop off
-			 * the filename so we return a directory.
-			 */
-			if ((st.st_mode & S_IFMT) != S_IFDIR) {
-				try_end = try + strlen(try);
-				while (try_end > try && *try_end != '/')
-					try_end--;
-				if (try_end > try)
-					*try_end = 0;	/* chop! */
-			}
-
-			/*
-			 * done!
-			 */
-			snprintf(result, rlen, "%s", try);
-			return 1;
-		}
-
-		/*
-		 * nope, we didn't find it.  if we used up dirbase we've
-		 * reached the root and failed.
-		 */
-		if (db_end == dirbase)
-			break;		/* failed! */
-
-		/*
-		 * truncate dirbase from the end to move up a dir
-		 */
-		while (db_end > dirbase && *db_end != '/')
-			db_end--;
-		*db_end = 0;		/* chop! */
-
-	} /* while (1) */
+	    snprintf(result, result_len, "%s", try);
+	    return TRUE;
+	}
 
 	/*
-	 * we failed...
+	 * nope, we didn't find it.  if we used up dirbase we've
+	 * reached the root and failed.
 	 */
-	return 0;
+	if (dirbase_end == dirbase)
+	    break;		/* failed! */
+
+	/*
+	 * truncate dirbase from the end to move up a dir
+	 */
+	while (dirbase_end > dirbase && *dirbase_end != '/')
+	    dirbase_end--;
+	*dirbase_end = '\0';	/* chop! */
+
+    } /* while (TRUE) */
+
+    return FALSE;
 }
 
 /*-
@@ -1505,8 +1420,8 @@ Dir_FindHereOrAbove(char *here, char *search_path, char *result, int rlen) {
 int
 Dir_MTime(GNode *gn, Boolean recheck)
 {
-    char          *fullName;  /* the full pathname of name */
-    struct stat	  stb;	      /* buffer for finding the mod time */
+    char *fullName;		/* the full pathname of name */
+    struct make_stat mst;	/* buffer for finding the mod time */
 
     if (gn->type & OP_ARCHV) {
 	return Arch_MTime(gn);
@@ -1519,7 +1434,7 @@ Dir_MTime(GNode *gn, Boolean recheck)
 	else {
 	    fullName = Dir_FindFile(gn->name, Suff_FindPath(gn));
 	    if (fullName == NULL && gn->flags & FROM_DEPEND &&
-		!Lst_IsEmpty(gn->iParents)) {
+		!Lst_IsEmpty(gn->implicitParents)) {
 		char *cp;
 
 		cp = strrchr(gn->name, '/');
@@ -1539,15 +1454,15 @@ Dir_MTime(GNode *gn, Boolean recheck)
 			gn->path = bmake_strdup(fullName);
 			if (!Job_RunTarget(".STALE", gn->fname))
 			    fprintf(stdout,
-				"%s: %s, %d: ignoring stale %s for %s, "
-				"found %s\n", progname, gn->fname, gn->lineno,
-				makeDependfile, gn->name, fullName);
+				    "%s: %s, %d: ignoring stale %s for %s, "
+				    "found %s\n", progname, gn->fname,
+				    gn->lineno,
+				    makeDependfile, gn->name, fullName);
 		    }
 		}
 	    }
-	    if (DEBUG(DIR))
-		fprintf(debug_file, "Found '%s' as '%s'\n",
-			gn->name, fullName ? fullName : "(not found)" );
+	    DIR_DEBUG2("Found '%s' as '%s'\n",
+		       gn->name, fullName ? fullName : "(not found)");
 	}
     } else {
 	fullName = gn->path;
@@ -1557,13 +1472,13 @@ Dir_MTime(GNode *gn, Boolean recheck)
 	fullName = bmake_strdup(gn->name);
     }
 
-    if (cached_stats(&mtimes, fullName, &stb, recheck ? CST_UPDATE : 0) < 0) {
+    if (cached_stats(&mtimes, fullName, &mst, recheck ? CST_UPDATE : 0) < 0) {
 	if (gn->type & OP_MEMBER) {
 	    if (fullName != gn->path)
 		free(fullName);
 	    return Arch_MemMTime(gn);
 	} else {
-	    stb.st_mtime = 0;
+	    mst.mst_mtime = 0;
 	}
     }
 
@@ -1571,7 +1486,7 @@ Dir_MTime(GNode *gn, Boolean recheck)
 	gn->path = fullName;
     }
 
-    gn->mtime = stb.st_mtime;
+    gn->mtime = mst.mst_mtime;
     return gn->mtime;
 }
 
@@ -1585,6 +1500,8 @@ Dir_MTime(GNode *gn, Boolean recheck)
  * Input:
  *	path		the path to which the directory should be
  *			added
+ *			XXX: Why would this ever be NULL, and what does
+ *			that mean?
  *	name		the name of the directory to add
  *
  * Results:
@@ -1598,78 +1515,70 @@ Dir_MTime(GNode *gn, Boolean recheck)
 Path *
 Dir_AddDir(Lst path, const char *name)
 {
-    LstNode       ln = NULL; /* node in case Path structure is found */
-    Path	  *p = NULL;  /* pointer to new Path structure */
-    DIR     	  *d;	      /* for reading directory */
-    struct dirent *dp;	      /* entry in directory */
+    LstNode ln = NULL;		/* node in case Path structure is found */
+    Path *p = NULL;		/* pointer to new Path structure */
+    DIR *d;			/* for reading directory */
+    struct dirent *dp;		/* entry in directory */
 
-    if (strcmp(name, ".DOTLAST") == 0) {
-	ln = Lst_Find(path, name, DirFindName);
+    if (path != NULL && strcmp(name, ".DOTLAST") == 0) {
+	ln = Lst_Find(path, DirFindName, name);
 	if (ln != NULL)
-	    return (Path *)Lst_Datum(ln);
-	else {
-	    dotLast->refCount += 1;
-	    (void)Lst_AtFront(path, dotLast);
-	}
+	    return LstNode_Datum(ln);
+
+	dotLast->refCount++;
+	Lst_Prepend(path, dotLast);
     }
 
-    if (path)
-	ln = Lst_Find(openDirectories, name, DirFindName);
+    if (path != NULL)
+	ln = Lst_Find(openDirectories, DirFindName, name);
     if (ln != NULL) {
-	p = (Path *)Lst_Datum(ln);
-	if (path && Lst_Member(path, p) == NULL) {
+	p = LstNode_Datum(ln);
+	if (Lst_FindDatum(path, p) == NULL) {
 	    p->refCount += 1;
-	    (void)Lst_AtEnd(path, p);
-	}
-    } else {
-	if (DEBUG(DIR)) {
-	    fprintf(debug_file, "Caching %s ...", name);
-	}
-
-	if ((d = opendir(name)) != NULL) {
-	    p = bmake_malloc(sizeof(Path));
-	    p->name = bmake_strdup(name);
-	    p->hits = 0;
-	    p->refCount = 1;
-	    Hash_InitTable(&p->files, -1);
-
-	    while ((dp = readdir(d)) != NULL) {
-#if defined(sun) && defined(d_ino) /* d_ino is a sunos4 #define for d_fileno */
-		/*
-		 * The sun directory library doesn't check for a 0 inode
-		 * (0-inode slots just take up space), so we have to do
-		 * it ourselves.
-		 */
-		if (dp->d_fileno == 0) {
-		    continue;
-		}
-#endif /* sun && d_ino */
-		(void)Hash_CreateEntry(&p->files, dp->d_name, NULL);
-	    }
-	    (void)closedir(d);
-	    (void)Lst_AtEnd(openDirectories, p);
-	    if (path != NULL)
-		(void)Lst_AtEnd(path, p);
-	}
-	if (DEBUG(DIR)) {
-	    fprintf(debug_file, "done\n");
+	    Lst_Append(path, p);
 	}
+	return p;
     }
+
+    DIR_DEBUG1("Caching %s ...", name);
+
+    if ((d = opendir(name)) != NULL) {
+	p = bmake_malloc(sizeof(Path));
+	p->name = bmake_strdup(name);
+	p->hits = 0;
+	p->refCount = 1;
+	Hash_InitTable(&p->files, -1);
+
+	while ((dp = readdir(d)) != NULL) {
+#if defined(sun) && defined(d_ino) /* d_ino is a sunos4 #define for d_fileno */
+	    /*
+	     * The sun directory library doesn't check for a 0 inode
+	     * (0-inode slots just take up space), so we have to do
+	     * it ourselves.
+	     */
+	    if (dp->d_fileno == 0) {
+		continue;
+	    }
+#endif /* sun && d_ino */
+	    (void)Hash_CreateEntry(&p->files, dp->d_name, NULL);
+	}
+	(void)closedir(d);
+	Lst_Append(openDirectories, p);
+	if (path != NULL)
+	    Lst_Append(path, p);
+    }
+    DIR_DEBUG0("done\n");
     return p;
 }
 
 /*-
  *-----------------------------------------------------------------------
  * Dir_CopyDir --
- *	Callback function for duplicating a search path via Lst_Duplicate.
+ *	Callback function for duplicating a search path via Lst_Copy.
  *	Ups the reference count for the directory.
  *
  * Results:
  *	Returns the Path it was given.
- *
- * Side Effects:
- *	The refCount of the path is incremented.
- *
  *-----------------------------------------------------------------------
  */
 void *
@@ -1704,25 +1613,23 @@ Dir_CopyDir(void *p)
 char *
 Dir_MakeFlags(const char *flag, Lst path)
 {
-    char	  *str;	  /* the string which will be returned */
-    char	  *s1, *s2;/* the current directory preceded by 'flag' */
-    LstNode	  ln;	  /* the node of the current directory */
-    Path	  *p;	  /* the structure describing the current directory */
+    Buffer buf;
+    LstNode ln;			/* the node of the current directory */
 
-    str = bmake_strdup("");
+    Buf_Init(&buf, 0);
 
-    if (Lst_Open(path) == SUCCESS) {
+    if (path != NULL) {
+	Lst_Open(path);
 	while ((ln = Lst_Next(path)) != NULL) {
-	    p = (Path *)Lst_Datum(ln);
-	    s2 = str_concat(flag, p->name, 0);
-	    str = str_concat(s1 = str, s2, STR_ADDSPACE);
-	    free(s1);
-	    free(s2);
+	    Path *p = LstNode_Datum(ln);
+	    Buf_AddStr(&buf, " ");
+	    Buf_AddStr(&buf, flag);
+	    Buf_AddStr(&buf, p->name);
 	}
 	Lst_Close(path);
     }
 
-    return str;
+    return Buf_Destroy(&buf, FALSE);
 }
 
 /*-
@@ -1746,14 +1653,14 @@ Dir_MakeFlags(const char *flag, Lst path)
 void
 Dir_Destroy(void *pp)
 {
-    Path    	  *p = (Path *)pp;
+    Path *p = (Path *)pp;
     p->refCount -= 1;
 
     if (p->refCount == 0) {
-	LstNode	ln;
+	LstNode ln;
 
-	ln = Lst_Member(openDirectories, p);
-	(void)Lst_Remove(openDirectories, ln);
+	ln = Lst_FindDatum(openDirectories, p);
+	Lst_Remove(openDirectories, ln);
 
 	Hash_DeleteTable(&p->files);
 	free(p->name);
@@ -1781,9 +1688,8 @@ Dir_Destroy(void *pp)
 void
 Dir_ClearPath(Lst path)
 {
-    Path    *p;
     while (!Lst_IsEmpty(path)) {
-	p = (Path *)Lst_DeQueue(path);
+	Path *p = Lst_Dequeue(path);
 	Dir_Destroy(p);
     }
 }
@@ -1811,37 +1717,43 @@ void
 Dir_Concat(Lst path1, Lst path2)
 {
     LstNode ln;
-    Path    *p;
+    Path *p;
 
-    for (ln = Lst_First(path2); ln != NULL; ln = Lst_Succ(ln)) {
-	p = (Path *)Lst_Datum(ln);
-	if (Lst_Member(path1, p) == NULL) {
+    for (ln = Lst_First(path2); ln != NULL; ln = LstNode_Next(ln)) {
+	p = LstNode_Datum(ln);
+	if (Lst_FindDatum(path1, p) == NULL) {
 	    p->refCount += 1;
-	    (void)Lst_AtEnd(path1, p);
+	    Lst_Append(path1, p);
 	}
     }
 }
 
+static int
+percentage(int num, int den)
+{
+    return den != 0 ? num * 100 / den : 0;
+}
+
 /********** DEBUG INFO **********/
 void
 Dir_PrintDirectories(void)
 {
-    LstNode	ln;
-    Path	*p;
+    LstNode ln;
 
     fprintf(debug_file, "#*** Directory Cache:\n");
-    fprintf(debug_file, "# Stats: %d hits %d misses %d near misses %d losers (%d%%)\n",
-	      hits, misses, nearmisses, bigmisses,
-	      (hits+bigmisses+nearmisses ?
-	       hits * 100 / (hits + bigmisses + nearmisses) : 0));
+    fprintf(debug_file,
+	    "# Stats: %d hits %d misses %d near misses %d losers (%d%%)\n",
+	    hits, misses, nearmisses, bigmisses,
+	    percentage(hits, hits + bigmisses + nearmisses));
     fprintf(debug_file, "# %-20s referenced\thits\n", "directory");
-    if (Lst_Open(openDirectories) == SUCCESS) {
-	while ((ln = Lst_Next(openDirectories)) != NULL) {
-	    p = (Path *)Lst_Datum(ln);
-	    fprintf(debug_file, "# %-20s %10d\t%4d\n", p->name, p->refCount, p->hits);
-	}
-	Lst_Close(openDirectories);
+
+    Lst_Open(openDirectories);
+    while ((ln = Lst_Next(openDirectories)) != NULL) {
+	Path *p = LstNode_Datum(ln);
+	fprintf(debug_file, "# %-20s %10d\t%4d\n", p->name, p->refCount,
+		p->hits);
     }
+    Lst_Close(openDirectories);
 }
 
 static int
diff --git a/dir.h b/dir.h
index 52ab35e2fd25..10da0eb8ba78 100644
--- a/dir.h
+++ b/dir.h
@@ -1,4 +1,4 @@
-/*	$NetBSD: dir.h,v 1.18 2017/05/31 22:02:06 maya Exp $	*/
+/*	$NetBSD: dir.h,v 1.23 2020/09/02 04:08:54 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -72,29 +72,34 @@
  *	from: @(#)dir.h	8.1 (Berkeley) 6/6/93
  */
 
-/* dir.h --
- */
-
 #ifndef	MAKE_DIR_H
 #define	MAKE_DIR_H
 
-typedef struct Path {
+/* A cache of a directory, remembering all the files that exist in that
+ * directory. */
+typedef struct {
     char         *name;	    	/* Name of directory */
     int	    	  refCount; 	/* Number of paths with this directory */
     int		  hits;	    	/* the number of times a file in this
 				 * directory has been found */
-    Hash_Table    files;    	/* Hash table of files in directory */
+    Hash_Table    files;    	/* Hash set of files in directory */
 } Path;
 
-void Dir_Init(const char *);
+struct make_stat {
+    time_t mst_mtime;
+    mode_t mst_mode;
+};
+
+void Dir_Init(void);
+void Dir_InitDir(const char *);
 void Dir_InitCur(const char *);
 void Dir_InitDot(void);
 void Dir_End(void);
 void Dir_SetPATH(void);
-Boolean Dir_HasWildcards(char *);
+Boolean Dir_HasWildcards(const char *);
 void Dir_Expand(const char *, Lst, Lst);
 char *Dir_FindFile(const char *, Lst);
-int Dir_FindHereOrAbove(char *, char *, char *, int);
+Boolean Dir_FindHereOrAbove(const char *, const char *, char *, int);
 int Dir_MTime(GNode *, Boolean);
 Path *Dir_AddDir(Lst, const char *);
 char *Dir_MakeFlags(const char *, Lst);
@@ -103,6 +108,9 @@ void Dir_Concat(Lst, Lst);
 void Dir_PrintDirectories(void);
 void Dir_PrintPath(Lst);
 void Dir_Destroy(void *);
-void * Dir_CopyDir(void *);
+void *Dir_CopyDir(void *);
+
+int cached_lstat(const char *, struct make_stat *);
+int cached_stat(const char *, struct make_stat *);
 
 #endif /* MAKE_DIR_H */
diff --git a/enum.c b/enum.c
new file mode 100755
index 000000000000..9dec4f3a5f6a
--- /dev/null
+++ b/enum.c
@@ -0,0 +1,100 @@
+/*	$NetBSD: enum.c,v 1.6 2020/09/01 20:34:51 rillig Exp $	*/
+
+/*
+ Copyright (c) 2020 Roland Illig <rillig@NetBSD.org>
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
+ BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef MAKE_NATIVE
+static char rcsid[] = "$NetBSD: enum.c,v 1.6 2020/09/01 20:34:51 rillig Exp $";
+#else
+#include <sys/cdefs.h>
+#ifndef lint
+__RCSID("$NetBSD: enum.c,v 1.6 2020/09/01 20:34:51 rillig Exp $");
+#endif
+#endif
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "enum.h"
+
+/* Convert a bitset into a string representation, showing the names of the
+ * individual bits.
+ *
+ * Optionally, shortcuts for groups of bits can be added.  To have an effect,
+ * they need to be listed before their individual bits. */
+const char *
+Enum_FlagsToString(char *buf, size_t buf_size,
+		   int value, const EnumToStringSpec *spec)
+{
+	const char *buf_start = buf;
+	const char *sep = "";
+	size_t sep_len = 0;
+
+	for (; spec->es_value != 0; spec++) {
+		size_t name_len;
+
+		if ((value & spec->es_value) != spec->es_value)
+			    continue;
+		value &= ~spec->es_value;
+
+		assert(buf_size >= sep_len + 1);
+		memcpy(buf, sep, sep_len);
+		buf += sep_len;
+		buf_size -= sep_len;
+
+		name_len = strlen(spec->es_name);
+		assert(buf_size >= name_len + 1);
+		memcpy(buf, spec->es_name, name_len);
+		buf += name_len;
+		buf_size -= name_len;
+
+		sep = ENUM__SEP;
+		sep_len = sizeof ENUM__SEP - 1;
+	}
+
+	/* If this assertion fails, the listed enum values are incomplete. */
+	assert(value == 0);
+
+	if (buf == buf_start)
+		return "none";
+
+	assert(buf_size >= 1);
+	buf[0] = '\0';
+	return buf_start;
+}
+
+/* Convert a fixed-value enum into a string representation. */
+const char *
+Enum_ValueToString(int value, const EnumToStringSpec *spec)
+{
+	for (; spec->es_name[0] != '\0'; spec++) {
+	    if (value == spec->es_value)
+	        return spec->es_name;
+	}
+	abort(/* unknown enum value */);
+}
diff --git a/enum.h b/enum.h
new file mode 100755
index 000000000000..d6a94f11e7cd
--- /dev/null
+++ b/enum.h
@@ -0,0 +1,193 @@
+/*	$NetBSD: enum.h,v 1.9 2020/09/01 20:34:51 rillig Exp $	*/
+
+/*
+ Copyright (c) 2020 Roland Illig <rillig@NetBSD.org>
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
+ BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef MAKE_ENUM_H
+#define MAKE_ENUM_H
+
+/* Generate string representations for bitmasks and simple enums. */
+
+#include <stddef.h>
+
+typedef struct {
+	int es_value;
+	const char *es_name;
+} EnumToStringSpec;
+
+const char *Enum_FlagsToString(char *, size_t, int, const EnumToStringSpec *);
+const char *Enum_ValueToString(int, const EnumToStringSpec *);
+
+/* For Enum_FlagsToString, the separator between flags. */
+#define ENUM__SEP "|"
+
+/* Generate the string that joins all possible flags, to see how large the
+ * buffer must be. */
+#define ENUM__JOIN_STR_1(v1) \
+	#v1
+#define ENUM__JOIN_STR_2(v1, v2) \
+	ENUM__JOIN_STR_1(v1) ENUM__SEP \
+	ENUM__JOIN_STR_1(v2)
+#define ENUM__JOIN_STR_4(v1, v2, v3, v4) \
+	ENUM__JOIN_STR_2(v1, v2) ENUM__SEP \
+	ENUM__JOIN_STR_2(v3, v4)
+#define ENUM__JOIN_STR_8(v1, v2, v3, v4, v5, v6, v7, v8) \
+	ENUM__JOIN_STR_4(v1, v2, v3, v4) ENUM__SEP \
+	ENUM__JOIN_STR_4(v5, v6, v7, v8)
+#define ENUM__JOIN_STR_16(v01, v02, v03, v04, v05, v06, v07, v08, \
+			  v09, v10, v11, v12, v13, v14, v15, v16) \
+	ENUM__JOIN_STR_8(v01, v02, v03, v04, v05, v06, v07, v08) ENUM__SEP \
+	ENUM__JOIN_STR_8(v09, v10, v11, v12, v13, v14, v15, v16)
+
+#define ENUM__JOIN_2(part1, part2) \
+	part1 ENUM__SEP part2
+#define ENUM__JOIN_3(part1, part2, part3) \
+	part1 ENUM__SEP part2 ENUM__SEP part3
+#define ENUM__JOIN_4(part1, part2, part3, part4) \
+	part1 ENUM__SEP part2 ENUM__SEP part3 ENUM__SEP part4
+#define ENUM__JOIN_5(part1, part2, part3, part4, part5) \
+	part1 ENUM__SEP part2 ENUM__SEP part3 ENUM__SEP part4 ENUM__SEP part5
+
+/* List the pairs of enum value and corresponding name. */
+#define ENUM__SPEC_1(v1) \
+	{ v1, #v1 }
+#define ENUM__SPEC_2(v1, v2) \
+	ENUM__SPEC_1(v1), \
+	ENUM__SPEC_1(v2)
+#define ENUM__SPEC_4(v1, v2, v3, v4) \
+	ENUM__SPEC_2(v1, v2), \
+	ENUM__SPEC_2(v3, v4)
+#define ENUM__SPEC_8(v1, v2, v3, v4, v5, v6, v7, v8) \
+	ENUM__SPEC_4(v1, v2, v3, v4), \
+	ENUM__SPEC_4(v5, v6, v7, v8)
+#define ENUM__SPEC_16(v01, v02, v03, v04, v05, v06, v07, v08, \
+		      v09, v10, v11, v12, v13, v14, v15, v16) \
+	ENUM__SPEC_8(v01, v02, v03, v04, v05, v06, v07, v08), \
+	ENUM__SPEC_8(v09, v10, v11, v12, v13, v14, v15, v16)
+
+#define ENUM__SPECS_2(part1, part2) \
+	{ part1, part2, { 0, "" } }
+#define ENUM__SPECS_3(part1, part2, part3) \
+	{ part1, part2, part3, { 0, "" } }
+#define ENUM__SPECS_4(part1, part2, part3, part4) \
+	{ part1, part2, part3, part4, { 0, "" } }
+#define ENUM__SPECS_5(part1, part2, part3, part4, part5) \
+	{ part1, part2, part3, part4, part5, { 0, "" } }
+
+/* Declare the necessary data structures for calling Enum_ValueToString. */
+#define ENUM__VALUE_RTTI(typnam, specs) \
+	static const EnumToStringSpec typnam ## _ ## ToStringSpecs[] = specs
+
+/* Declare the necessary data structures for calling Enum_FlagsToString. */
+#define ENUM__FLAGS_RTTI(typnam, specs, joined) \
+	static const EnumToStringSpec typnam ## _ ## ToStringSpecs[] = specs; \
+	enum { typnam ## _ ## ToStringSize = sizeof joined }
+
+/* Declare the necessary data structures for calling Enum_FlagsToString
+ * for an enum with 3 flags. */
+#define ENUM_FLAGS_RTTI_3(typnam, v1, v2, v3) \
+	ENUM__FLAGS_RTTI(typnam, \
+	    ENUM__SPECS_2( \
+	    	ENUM__SPEC_2(v1, v2), \
+	    	ENUM__SPEC_1(v3)), \
+	    ENUM__JOIN_2( \
+	    	ENUM__JOIN_STR_2(v1, v2), \
+	    	ENUM__JOIN_STR_1(v3)))
+
+/* Declare the necessary data structures for calling Enum_FlagsToString
+ * for an enum with 8 flags. */
+#define ENUM_FLAGS_RTTI_8(typnam, v1, v2, v3, v4, v5, v6, v7, v8) \
+	ENUM__FLAGS_RTTI(typnam, \
+	    ENUM__SPECS_2( \
+		ENUM__SPEC_4(v1, v2, v3, v4), \
+		ENUM__SPEC_4(v5, v6, v7, v8)), \
+	    ENUM__JOIN_2( \
+		ENUM__JOIN_STR_4(v1, v2, v3, v4), \
+		ENUM__JOIN_STR_4(v5, v6, v7, v8)))
+
+/* Declare the necessary data structures for calling Enum_ValueToString
+ * for an enum with 8 constants. */
+#define ENUM_VALUE_RTTI_8(typnam, v1, v2, v3, v4, v5, v6, v7, v8) \
+	ENUM__VALUE_RTTI(typnam, \
+	    ENUM__SPECS_2( \
+		ENUM__SPEC_4(v1, v2, v3, v4), \
+		ENUM__SPEC_4(v5, v6, v7, v8)))
+
+/* Declare the necessary data structures for calling Enum_FlagsToString
+ * for an enum with 10 flags. */
+#define ENUM_FLAGS_RTTI_10(typnam, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) \
+	ENUM__FLAGS_RTTI(typnam, \
+	    ENUM__SPECS_2( \
+		ENUM__SPEC_8(v1, v2, v3, v4, v5, v6, v7, v8), \
+		ENUM__SPEC_2(v9, v10)), \
+	    ENUM__JOIN_2( \
+		ENUM__JOIN_STR_8(v1, v2, v3, v4, v5, v6, v7, v8), \
+		ENUM__JOIN_STR_2(v9, v10)))
+
+/* Declare the necessary data structures for calling Enum_FlagsToString
+ * for an enum with 31 flags. */
+#define ENUM_FLAGS_RTTI_31(typnam, \
+		     v01, v02, v03, v04, v05, v06, v07, v08, \
+		     v09, v10, v11, v12, v13, v14, v15, v16, \
+		     v17, v18, v19, v20, v21, v22, v23, v24, \
+		     v25, v26, v27, v28, v29, v30, v31) \
+    ENUM__FLAGS_RTTI(typnam, \
+        ENUM__SPECS_5( \
+            ENUM__SPEC_16(v01, v02, v03, v04, v05, v06, v07, v08, \
+			  v09, v10, v11, v12, v13, v14, v15, v16), \
+	    ENUM__SPEC_8(v17, v18, v19, v20, v21, v22, v23, v24), \
+	    ENUM__SPEC_4(v25, v26, v27, v28), \
+	    ENUM__SPEC_2(v29, v30), \
+	    ENUM__SPEC_1(v31)), \
+	ENUM__JOIN_5( \
+	    ENUM__JOIN_STR_16(v01, v02, v03, v04, v05, v06, v07, v08, \
+			      v09, v10, v11, v12, v13, v14, v15, v16), \
+	    ENUM__JOIN_STR_8(v17, v18, v19, v20, v21, v22, v23, v24), \
+	    ENUM__JOIN_STR_4(v25, v26, v27, v28), \
+	    ENUM__JOIN_STR_2(v29, v30), \
+	    ENUM__JOIN_STR_1(v31)))
+
+/* Declare the necessary data structures for calling Enum_FlagsToString
+ * for an enum with 32 flags. */
+#define ENUM_FLAGS_RTTI_32(typnam, \
+		     v01, v02, v03, v04, v05, v06, v07, v08, \
+		     v09, v10, v11, v12, v13, v14, v15, v16, \
+		     v17, v18, v19, v20, v21, v22, v23, v24, \
+		     v25, v26, v27, v28, v29, v30, v31, v32) \
+    ENUM__FLAGS_RTTI(typnam, \
+        ENUM__SPECS_2( \
+            ENUM__SPEC_16(v01, v02, v03, v04, v05, v06, v07, v08, \
+			  v09, v10, v11, v12, v13, v14, v15, v16), \
+	    ENUM__SPEC_16(v17, v18, v19, v20, v21, v22, v23, v24, \
+			  v25, v26, v27, v28, v29, v30, v31, v32)), \
+	ENUM__JOIN_2( \
+	    ENUM__JOIN_STR_16(v01, v02, v03, v04, v05, v06, v07, v08, \
+			      v09, v10, v11, v12, v13, v14, v15, v16), \
+	    ENUM__JOIN_STR_16(v17, v18, v19, v20, v21, v22, v23, v24, \
+	    		      v25, v26, v27, v28, v29, v30, v31, v32)))
+
+#endif
diff --git a/for.c b/for.c
index f20a0f58b06b..2e9963f2a7f6 100644
--- a/for.c
+++ b/for.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: for.c,v 1.54 2020/07/03 08:13:23 rillig Exp $	*/
+/*	$NetBSD: for.c,v 1.67 2020/08/30 19:56:02 rillig Exp $	*/
 
 /*
  * Copyright (c) 1992, The Regents of the University of California.
@@ -30,14 +30,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: for.c,v 1.54 2020/07/03 08:13:23 rillig Exp $";
+static char rcsid[] = "$NetBSD: for.c,v 1.67 2020/08/30 19:56:02 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)for.c	8.1 (Berkeley) 6/6/93";
 #else
-__RCSID("$NetBSD: for.c,v 1.54 2020/07/03 08:13:23 rillig Exp $");
+__RCSID("$NetBSD: for.c,v 1.67 2020/08/30 19:56:02 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -52,13 +52,7 @@ __RCSID("$NetBSD: for.c,v 1.54 2020/07/03 08:13:23 rillig Exp $");
  *
  */
 
-#include    <assert.h>
-#include    <ctype.h>
-
 #include    "make.h"
-#include    "hash.h"
-#include    "dir.h"
-#include    "buf.h"
 #include    "strlist.h"
 
 #define FOR_SUB_ESCAPE_CHAR  1
@@ -83,34 +77,22 @@ __RCSID("$NetBSD: for.c,v 1.54 2020/07/03 08:13:23 rillig Exp $");
  * For_Run.
  */
 
-static int  	  forLevel = 0;  	/* Nesting level	*/
+static int forLevel = 0;	/* Nesting level */
 
 /*
  * State of a for loop.
  */
-typedef struct _For {
-    Buffer	  buf;			/* Body of loop		*/
-    strlist_t     vars;			/* Iteration variables	*/
-    strlist_t     items;		/* Substitution items */
-    char          *parse_buf;
-    int           short_var;
-    int           sub_next;
+typedef struct {
+    Buffer buf;			/* Body of loop */
+    strlist_t vars;		/* Iteration variables */
+    strlist_t items;		/* Substitution items */
+    char *parse_buf;
+    int short_var;
+    int sub_next;
 } For;
 
-static For        *accumFor;            /* Loop being accumulated */
+static For *accumFor;		/* Loop being accumulated */
 
-
-
-static char *
-make_str(const char *ptr, int len)
-{
-	char *new_ptr;
-
-	new_ptr = bmake_malloc(len + 1);
-	memcpy(new_ptr, ptr, len);
-	new_ptr[len] = 0;
-	return new_ptr;
-}
 
 static void
 For_Free(For *arg)
@@ -148,14 +130,13 @@ For_Eval(char *line)
 {
     For *new_for;
     char *ptr = line, *sub;
-    int len;
+    size_t len;
     int escapes;
     unsigned char ch;
-    char **words, *word_buf;
-    int n, nwords;
+    Words words;
 
     /* Skip the '.' and any following whitespace */
-    for (ptr++; *ptr && isspace((unsigned char) *ptr); ptr++)
+    for (ptr++; *ptr && isspace((unsigned char)*ptr); ptr++)
 	continue;
 
     /*
@@ -163,8 +144,8 @@ For_Eval(char *line)
      * a for.
      */
     if (ptr[0] != 'f' || ptr[1] != 'o' || ptr[2] != 'r' ||
-	    !isspace((unsigned char) ptr[3])) {
-	if (ptr[0] == 'e' && strncmp(ptr+1, "ndfor", 5) == 0) {
+	!isspace((unsigned char)ptr[3])) {
+	if (ptr[0] == 'e' && strncmp(ptr + 1, "ndfor", 5) == 0) {
 	    Parse_Error(PARSE_FATAL, "for-less endfor");
 	    return -1;
 	}
@@ -181,7 +162,7 @@ For_Eval(char *line)
 
     /* Grab the variables. Terminate on "in". */
     for (;; ptr += len) {
-	while (*ptr && isspace((unsigned char) *ptr))
+	while (*ptr && isspace((unsigned char)*ptr))
 	    ptr++;
 	if (*ptr == '\0') {
 	    Parse_Error(PARSE_FATAL, "missing `in' in for");
@@ -196,7 +177,7 @@ For_Eval(char *line)
 	}
 	if (len == 1)
 	    new_for->short_var = 1;
-	strlist_add_str(&new_for->vars, make_str(ptr, len), len);
+	strlist_add_str(&new_for->vars, bmake_strldup(ptr, len), len);
     }
 
     if (strlist_num(&new_for->vars) == 0) {
@@ -205,7 +186,7 @@ For_Eval(char *line)
 	return -1;
     }
 
-    while (*ptr && isspace((unsigned char) *ptr))
+    while (*ptr && isspace((unsigned char)*ptr))
 	ptr++;
 
     /*
@@ -216,23 +197,25 @@ For_Eval(char *line)
      * We can't do the escapes here - because we don't know whether
      * we are substuting into ${...} or $(...).
      */
-    sub = Var_Subst(NULL, ptr, VAR_GLOBAL, VARF_WANTRES);
+    sub = Var_Subst(ptr, VAR_GLOBAL, VARE_WANTRES);
 
     /*
      * Split into words allowing for quoted strings.
      */
-    words = brk_string(sub, &nwords, FALSE, &word_buf);
+    words = Str_Words(sub, FALSE);
 
     free(sub);
 
-    if (words != NULL) {
-	for (n = 0; n < nwords; n++) {
-	    ptr = words[n];
+    {
+        size_t n;
+
+	for (n = 0; n < words.len; n++) {
+	    ptr = words.words[n];
 	    if (!*ptr)
 		continue;
 	    escapes = 0;
 	    while ((ch = *ptr++)) {
-		switch(ch) {
+		switch (ch) {
 		case ':':
 		case '$':
 		case '\\':
@@ -250,17 +233,17 @@ For_Eval(char *line)
 	     * We have to dup words[n] to maintain the semantics of
 	     * strlist.
 	     */
-	    strlist_add_str(&new_for->items, bmake_strdup(words[n]), escapes);
+	    strlist_add_str(&new_for->items, bmake_strdup(words.words[n]),
+			    escapes);
 	}
 
-	free(words);
-	free(word_buf);
+	Words_Free(words);
 
 	if ((len = strlist_num(&new_for->items)) > 0 &&
 	    len % (n = strlist_num(&new_for->vars))) {
 	    Parse_Error(PARSE_FATAL,
-			"Wrong number of words (%d) in .for substitution list"
-			" with %d vars", len, n);
+			"Wrong number of words (%zu) in .for substitution list"
+			" with %zu vars", len, n);
 	    /*
 	     * Return 'success' so that the body of the .for loop is
 	     * accumulated.
@@ -288,49 +271,35 @@ For_Accum(char *line)
 
     if (*ptr == '.') {
 
-	for (ptr++; *ptr && isspace((unsigned char) *ptr); ptr++)
+	for (ptr++; *ptr && isspace((unsigned char)*ptr); ptr++)
 	    continue;
 
 	if (strncmp(ptr, "endfor", 6) == 0 &&
-		(isspace((unsigned char) ptr[6]) || !ptr[6])) {
+	    (isspace((unsigned char)ptr[6]) || !ptr[6])) {
 	    if (DEBUG(FOR))
 		(void)fprintf(debug_file, "For: end for %d\n", forLevel);
 	    if (--forLevel <= 0)
 		return 0;
 	} else if (strncmp(ptr, "for", 3) == 0 &&
-		 isspace((unsigned char) ptr[3])) {
+		   isspace((unsigned char)ptr[3])) {
 	    forLevel++;
 	    if (DEBUG(FOR))
 		(void)fprintf(debug_file, "For: new loop %d\n", forLevel);
 	}
     }
 
-    Buf_AddBytes(&accumFor->buf, strlen(line), line);
+    Buf_AddStr(&accumFor->buf, line);
     Buf_AddByte(&accumFor->buf, '\n');
     return 1;
 }
 
-
-/*-
- *-----------------------------------------------------------------------
- * For_Run --
- *	Run the for loop, imitating the actions of an include file
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
 
-static int
+static size_t
 for_var_len(const char *var)
 {
     char ch, var_start, var_end;
     int depth;
-    int len;
+    size_t len;
 
     var_start = *var;
     if (var_start == 0)
@@ -360,24 +329,24 @@ for_var_len(const char *var)
 static void
 for_substitute(Buffer *cmds, strlist_t *items, unsigned int item_no, char ech)
 {
-    const char *item = strlist_str(items, item_no);
-    int len;
     char ch;
 
+    const char *item = strlist_str(items, item_no);
+
     /* If there were no escapes, or the only escape is the other variable
      * terminator, then just substitute the full string */
     if (!(strlist_info(items, item_no) &
-	    (ech == ')' ? ~FOR_SUB_ESCAPE_BRACE : ~FOR_SUB_ESCAPE_PAREN))) {
-	Buf_AddBytes(cmds, strlen(item), item);
+	  (ech == ')' ? ~FOR_SUB_ESCAPE_BRACE : ~FOR_SUB_ESCAPE_PAREN))) {
+	Buf_AddStr(cmds, item);
 	return;
     }
 
     /* Escape ':', '$', '\\' and 'ech' - removed by :U processing */
     while ((ch = *item++) != 0) {
 	if (ch == '$') {
-	    len = for_var_len(item);
+	    size_t len = for_var_len(item);
 	    if (len != 0) {
-		Buf_AddBytes(cmds, len + 1, item - 1);
+		Buf_AddBytes(cmds, item - 1, len + 1);
 		item += len;
 		continue;
 	    }
@@ -392,13 +361,14 @@ static char *
 For_Iterate(void *v_arg, size_t *ret_len)
 {
     For *arg = v_arg;
-    int i, len;
+    int i;
     char *var;
     char *cp;
     char *cmd_cp;
     char *body_end;
     char ch;
     Buffer cmds;
+    size_t cmd_len;
 
     if (arg->sub_next + strlist_num(&arg->vars) > strlist_num(&arg->items)) {
 	/* No more iterations */
@@ -421,9 +391,9 @@ For_Iterate(void *v_arg, size_t *ret_len)
      * to contrive a makefile where an unwanted substitution happens.
      */
 
-    cmd_cp = Buf_GetAll(&arg->buf, &len);
-    body_end = cmd_cp + len;
-    Buf_Init(&cmds, len + 256);
+    cmd_cp = Buf_GetAll(&arg->buf, &cmd_len);
+    body_end = cmd_cp + cmd_len;
+    Buf_Init(&cmds, cmd_len + 256);
     for (cp = cmd_cp; (cp = strchr(cp, '$')) != NULL;) {
 	char ech;
 	ch = *++cp;
@@ -431,15 +401,15 @@ For_Iterate(void *v_arg, size_t *ret_len)
 	    cp++;
 	    /* Check variable name against the .for loop variables */
 	    STRLIST_FOREACH(var, &arg->vars, i) {
-		len = strlist_info(&arg->vars, i);
-		if (memcmp(cp, var, len) != 0)
+		size_t vlen = strlist_info(&arg->vars, i);
+		if (memcmp(cp, var, vlen) != 0)
 		    continue;
-		if (cp[len] != ':' && cp[len] != ech && cp[len] != '\\')
+		if (cp[vlen] != ':' && cp[vlen] != ech && cp[vlen] != '\\')
 		    continue;
 		/* Found a variable match. Replace with :U<value> */
-		Buf_AddBytes(&cmds, cp - cmd_cp, cmd_cp);
-		Buf_AddBytes(&cmds, 2, ":U");
-		cp += len;
+		Buf_AddBytesBetween(&cmds, cmd_cp, cp);
+		Buf_AddStr(&cmds, ":U");
+		cp += vlen;
 		cmd_cp = cp;
 		for_substitute(&cmds, &arg->items, arg->sub_next + i, ech);
 		break;
@@ -457,15 +427,15 @@ For_Iterate(void *v_arg, size_t *ret_len)
 	    if (var[0] != ch || var[1] != 0)
 		continue;
 	    /* Found a variable match. Replace with ${:U<value>} */
-	    Buf_AddBytes(&cmds, cp - cmd_cp, cmd_cp);
-	    Buf_AddBytes(&cmds, 3, "{:U");
+	    Buf_AddBytesBetween(&cmds, cmd_cp, cp);
+	    Buf_AddStr(&cmds, "{:U");
 	    cmd_cp = ++cp;
 	    for_substitute(&cmds, &arg->items, arg->sub_next + i, /*{*/ '}');
-	    Buf_AddBytes(&cmds, 1, "}");
+	    Buf_AddByte(&cmds, '}');
 	    break;
 	}
     }
-    Buf_AddBytes(&cmds, body_end - cmd_cp, cmd_cp);
+    Buf_AddBytesBetween(&cmds, cmd_cp, body_end);
 
     cp = Buf_Destroy(&cmds, FALSE);
     if (DEBUG(FOR))
@@ -478,6 +448,7 @@ For_Iterate(void *v_arg, size_t *ret_len)
     return cp;
 }
 
+/* Run the for loop, imitating the actions of an include file. */
 void
 For_Run(int lineno)
 {
@@ -487,9 +458,9 @@ For_Run(int lineno)
     accumFor = NULL;
 
     if (strlist_num(&arg->items) == 0) {
-        /* Nothing to expand - possibly due to an earlier syntax error. */
-        For_Free(arg);
-        return;
+	/* Nothing to expand - possibly due to an earlier syntax error. */
+	For_Free(arg);
+	return;
     }
 
     Parse_SetInput(NULL, lineno, -1, For_Iterate, arg);
diff --git a/hash.c b/hash.c
index f2bbebc9ece7..290bed6b3f5a 100644
--- a/hash.c
+++ b/hash.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: hash.c,v 1.22 2020/07/03 17:03:09 rillig Exp $	*/
+/*	$NetBSD: hash.c,v 1.29 2020/09/01 21:11:31 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -70,14 +70,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: hash.c,v 1.22 2020/07/03 17:03:09 rillig Exp $";
+static char rcsid[] = "$NetBSD: hash.c,v 1.29 2020/09/01 21:11:31 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)hash.c	8.1 (Berkeley) 6/6/93";
 #else
-__RCSID("$NetBSD: hash.c,v 1.22 2020/07/03 17:03:09 rillig Exp $");
+__RCSID("$NetBSD: hash.c,v 1.29 2020/09/01 21:11:31 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -89,9 +89,7 @@ __RCSID("$NetBSD: hash.c,v 1.22 2020/07/03 17:03:09 rillig Exp $");
  * 	table.  Hash tables grow automatically as the amount of
  * 	information increases.
  */
-#include "sprite.h"
 #include "make.h"
-#include "hash.h"
 
 /*
  * Forward references to local procedures that are used before they're
@@ -107,29 +105,26 @@ static void RebuildTable(Hash_Table *);
 
 #define rebuildLimit 3
 
-/*
- *---------------------------------------------------------
- *
- * Hash_InitTable --
- *
- *	This routine just sets up the hash table.
+/* The hash function(s) */
+
+#ifndef HASH
+/* The default: this one matches Gosling's emacs */
+#define HASH(h, key, p) do { \
+	for (h = 0, p = key; *p;) \
+		h = (h << 5) - h + *p++; \
+	} while (0)
+
+#endif
+
+/* Sets up the hash table.
  *
  * Input:
- *	t		Structure to to hold table.
+ *	t		Structure to to hold the table.
  *	numBuckets	How many buckets to create for starters. This
  *			number is rounded up to a power of two.   If
  *			<= 0, a reasonable default is chosen. The
  *			table will grow in size later as needed.
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	Memory is allocated for the initial bucket area.
- *
- *---------------------------------------------------------
  */
-
 void
 Hash_InitTable(Hash_Table *t, int numBuckets)
 {
@@ -146,118 +141,85 @@ Hash_InitTable(Hash_Table *t, int numBuckets)
 			 continue;
 	}
 	t->numEntries = 0;
-	t->size = i;
-	t->mask = i - 1;
-	t->bucketPtr = hp = bmake_malloc(sizeof(*hp) * i);
+	t->maxchain = 0;
+	t->bucketsSize = i;
+	t->bucketsMask = i - 1;
+	t->buckets = hp = bmake_malloc(sizeof(*hp) * i);
 	while (--i >= 0)
 		*hp++ = NULL;
 }
 
-/*
- *---------------------------------------------------------
- *
- * Hash_DeleteTable --
- *
- *	This routine removes everything from a hash table
- *	and frees up the memory space it occupied (except for
- *	the space in the Hash_Table structure).
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	Lots of memory is freed up.
- *
- *---------------------------------------------------------
- */
-
+/* Removes everything from the hash table and frees up the memory space it
+ * occupied (except for the space in the Hash_Table structure). */
 void
 Hash_DeleteTable(Hash_Table *t)
 {
 	struct Hash_Entry **hp, *h, *nexth = NULL;
 	int i;
 
-	for (hp = t->bucketPtr, i = t->size; --i >= 0;) {
+	for (hp = t->buckets, i = t->bucketsSize; --i >= 0;) {
 		for (h = *hp++; h != NULL; h = nexth) {
 			nexth = h->next;
 			free(h);
 		}
 	}
-	free(t->bucketPtr);
+	free(t->buckets);
 
 	/*
 	 * Set up the hash table to cause memory faults on any future access
 	 * attempts until re-initialization.
 	 */
-	t->bucketPtr = NULL;
+	t->buckets = NULL;
 }
 
-/*
- *---------------------------------------------------------
- *
- * Hash_FindEntry --
- *
- * 	Searches a hash table for an entry corresponding to key.
+/* Searches the hash table for an entry corresponding to the key.
  *
  * Input:
  *	t		Hash table to search.
  *	key		A hash key.
  *
  * Results:
- *	The return value is a pointer to the entry for key,
- *	if key was present in the table.  If key was not
- *	present, NULL is returned.
- *
- * Side Effects:
- *	None.
- *
- *---------------------------------------------------------
+ *	Returns a pointer to the entry for key, or NULL if the table contains
+ *	no entry for the key.
  */
-
 Hash_Entry *
 Hash_FindEntry(Hash_Table *t, const char *key)
 {
 	Hash_Entry *e;
 	unsigned h;
 	const char *p;
+	int chainlen;
 
-	if (t == NULL || t->bucketPtr == NULL) {
+	if (t == NULL || t->buckets == NULL) {
 	    return NULL;
 	}
-	for (h = 0, p = key; *p;)
-		h = (h << 5) - h + *p++;
+	HASH(h, key, p);
 	p = key;
-	for (e = t->bucketPtr[h & t->mask]; e != NULL; e = e->next)
+	chainlen = 0;
+#ifdef DEBUG_HASH_LOOKUP
+	if (DEBUG(HASH))
+		fprintf(debug_file, "%s: %p h=%x key=%s\n", __func__,
+		    t, h, key);
+#endif
+	for (e = t->buckets[h & t->bucketsMask]; e != NULL; e = e->next) {
+		chainlen++;
 		if (e->namehash == h && strcmp(e->name, p) == 0)
-			return e;
-	return NULL;
+			break;
+	}
+	if (chainlen > t->maxchain)
+		t->maxchain = chainlen;
+	return e;
 }
 
-/*
- *---------------------------------------------------------
- *
- * Hash_CreateEntry --
- *
- *	Searches a hash table for an entry corresponding to
- *	key.  If no entry is found, then one is created.
+/* Searches the hash table for an entry corresponding to the key.
+ * If no entry is found, then one is created.
  *
  * Input:
  *	t		Hash table to search.
  *	key		A hash key.
- *	newPtr		Filled in with TRUE if new entry created,
+ *	newPtr		Filled with TRUE if new entry created,
  *			FALSE otherwise.
- *
- * Results:
- *	The return value is a pointer to the entry.  If *newPtr
- *	isn't NULL, then *newPtr is filled in with TRUE if a
- *	new entry was created, and FALSE if an entry already existed
- *	with the given key.
- *
- * Side Effects:
- *	Memory may be allocated, and the hash buckets may be modified.
- *---------------------------------------------------------
  */
-
 Hash_Entry *
 Hash_CreateEntry(Hash_Table *t, const char *key, Boolean *newPtr)
 {
@@ -265,33 +227,44 @@ Hash_CreateEntry(Hash_Table *t, const char *key, Boolean *newPtr)
 	unsigned h;
 	const char *p;
 	int keylen;
+	int chainlen;
 	struct Hash_Entry **hp;
 
 	/*
 	 * Hash the key.  As a side effect, save the length (strlen) of the
 	 * key in case we need to create the entry.
 	 */
-	for (h = 0, p = key; *p;)
-		h = (h << 5) - h + *p++;
+	HASH(h, key, p);
 	keylen = p - key;
 	p = key;
-	for (e = t->bucketPtr[h & t->mask]; e != NULL; e = e->next) {
+	chainlen = 0;
+#ifdef DEBUG_HASH_LOOKUP
+	if (DEBUG(HASH))
+		fprintf(debug_file, "%s: %p h=%x key=%s\n", __func__,
+		    t, h, key);
+#endif
+	for (e = t->buckets[h & t->bucketsMask]; e != NULL; e = e->next) {
+		chainlen++;
 		if (e->namehash == h && strcmp(e->name, p) == 0) {
 			if (newPtr != NULL)
 				*newPtr = FALSE;
-			return e;
+			break;
 		}
 	}
+	if (chainlen > t->maxchain)
+		t->maxchain = chainlen;
+	if (e)
+		return e;
 
 	/*
 	 * The desired entry isn't there.  Before allocating a new entry,
 	 * expand the table if necessary (and this changes the resulting
 	 * bucket chain).
 	 */
-	if (t->numEntries >= rebuildLimit * t->size)
+	if (t->numEntries >= rebuildLimit * t->bucketsSize)
 		RebuildTable(t);
 	e = bmake_malloc(sizeof(*e) + keylen);
-	hp = &t->bucketPtr[h & t->mask];
+	hp = &t->buckets[h & t->bucketsMask];
 	e->next = *hp;
 	*hp = e;
 	Hash_SetValue(e, NULL);
@@ -304,23 +277,7 @@ Hash_CreateEntry(Hash_Table *t, const char *key, Boolean *newPtr)
 	return e;
 }
 
-/*
- *---------------------------------------------------------
- *
- * Hash_DeleteEntry --
- *
- * 	Delete the given hash table entry and free memory associated with
- *	it.
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	Hash chain that entry lives in is modified and memory is freed.
- *
- *---------------------------------------------------------
- */
-
+/* Delete the given hash table entry and free memory associated with it. */
 void
 Hash_DeleteEntry(Hash_Table *t, Hash_Entry *e)
 {
@@ -328,7 +285,7 @@ Hash_DeleteEntry(Hash_Table *t, Hash_Entry *e)
 
 	if (e == NULL)
 		return;
-	for (hp = &t->bucketPtr[e->namehash & t->mask];
+	for (hp = &t->buckets[e->namehash & t->bucketsMask];
 	     (p = *hp) != NULL; hp = &p->next) {
 		if (p == e) {
 			*hp = p->next;
@@ -341,12 +298,7 @@ Hash_DeleteEntry(Hash_Table *t, Hash_Entry *e)
 	abort();
 }
 
-/*
- *---------------------------------------------------------
- *
- * Hash_EnumFirst --
- *	This procedure sets things up for a complete search
- *	of all entries recorded in the hash table.
+/* Sets things up for enumerating all entries in the hash table.
  *
  * Input:
  *	t		Table to be searched.
@@ -355,57 +307,34 @@ Hash_DeleteEntry(Hash_Table *t, Hash_Entry *e)
  * Results:
  *	The return value is the address of the first entry in
  *	the hash table, or NULL if the table is empty.
- *
- * Side Effects:
- *	The information in searchPtr is initialized so that successive
- *	calls to Hash_Next will return successive HashEntry's
- *	from the table.
- *
- *---------------------------------------------------------
  */
-
 Hash_Entry *
 Hash_EnumFirst(Hash_Table *t, Hash_Search *searchPtr)
 {
-	searchPtr->tablePtr = t;
-	searchPtr->nextIndex = 0;
-	searchPtr->hashEntryPtr = NULL;
+	searchPtr->table = t;
+	searchPtr->nextBucket = 0;
+	searchPtr->entry = NULL;
 	return Hash_EnumNext(searchPtr);
 }
 
-/*
- *---------------------------------------------------------
- *
- * Hash_EnumNext --
- *    This procedure returns successive entries in the hash table.
+/* Returns the next entry in the hash table, or NULL if the end of the table
+ * is reached.
  *
  * Input:
  *	searchPtr	Area used to keep state about search.
- *
- * Results:
- *    The return value is a pointer to the next HashEntry
- *    in the table, or NULL when the end of the table is
- *    reached.
- *
- * Side Effects:
- *    The information in searchPtr is modified to advance to the
- *    next entry.
- *
- *---------------------------------------------------------
  */
-
 Hash_Entry *
 Hash_EnumNext(Hash_Search *searchPtr)
 {
 	Hash_Entry *e;
-	Hash_Table *t = searchPtr->tablePtr;
+	Hash_Table *t = searchPtr->table;
 
 	/*
-	 * The hashEntryPtr field points to the most recently returned
-	 * entry, or is nil if we are starting up.  If not nil, we have
+	 * The entry field points to the most recently returned
+	 * entry, or is NULL if we are starting up.  If not NULL, we have
 	 * to start at the next one in the chain.
 	 */
-	e = searchPtr->hashEntryPtr;
+	e = searchPtr->entry;
 	if (e != NULL)
 		e = e->next;
 	/*
@@ -413,59 +342,49 @@ Hash_EnumNext(Hash_Search *searchPtr)
 	 * find the next nonempty chain.
 	 */
 	while (e == NULL) {
-		if (searchPtr->nextIndex >= t->size)
+		if (searchPtr->nextBucket >= t->bucketsSize)
 			return NULL;
-		e = t->bucketPtr[searchPtr->nextIndex++];
+		e = t->buckets[searchPtr->nextBucket++];
 	}
-	searchPtr->hashEntryPtr = e;
+	searchPtr->entry = e;
 	return e;
 }
 
-/*
- *---------------------------------------------------------
- *
- * RebuildTable --
- *	This local routine makes a new hash table that
- *	is larger than the old one.
- *
- * Results:
- * 	None.
- *
- * Side Effects:
- *	The entire hash table is moved, so any bucket numbers
- *	from the old table are invalid.
- *
- *---------------------------------------------------------
- */
-
+/* Makes a new hash table that is larger than the old one. The entire hash
+ * table is moved, so any bucket numbers from the old table become invalid. */
 static void
 RebuildTable(Hash_Table *t)
 {
 	Hash_Entry *e, *next = NULL, **hp, **xp;
 	int i, mask;
-        Hash_Entry **oldhp;
+	Hash_Entry **oldhp;
 	int oldsize;
 
-	oldhp = t->bucketPtr;
-	oldsize = i = t->size;
+	oldhp = t->buckets;
+	oldsize = i = t->bucketsSize;
 	i <<= 1;
-	t->size = i;
-	t->mask = mask = i - 1;
-	t->bucketPtr = hp = bmake_malloc(sizeof(*hp) * i);
+	t->bucketsSize = i;
+	t->bucketsMask = mask = i - 1;
+	t->buckets = hp = bmake_malloc(sizeof(*hp) * i);
 	while (--i >= 0)
 		*hp++ = NULL;
 	for (hp = oldhp, i = oldsize; --i >= 0;) {
 		for (e = *hp++; e != NULL; e = next) {
 			next = e->next;
-			xp = &t->bucketPtr[e->namehash & mask];
+			xp = &t->buckets[e->namehash & mask];
 			e->next = *xp;
 			*xp = e;
 		}
 	}
 	free(oldhp);
+	if (DEBUG(HASH))
+		fprintf(debug_file, "%s: %p size=%d entries=%d maxchain=%d\n",
+			__func__, t, t->bucketsSize, t->numEntries, t->maxchain);
+	t->maxchain = 0;
 }
 
-void Hash_ForEach(Hash_Table *t, void (*action)(void *, void *), void *data)
+void
+Hash_ForEach(Hash_Table *t, void (*action)(void *, void *), void *data)
 {
 	Hash_Search search;
 	Hash_Entry *e;
@@ -475,3 +394,11 @@ void Hash_ForEach(Hash_Table *t, void (*action)(void *, void *), void *data)
 	     e = Hash_EnumNext(&search))
 		action(Hash_GetValue(e), data);
 }
+
+void
+Hash_DebugStats(Hash_Table *t, const char *name)
+{
+    if (DEBUG(HASH))
+	fprintf(debug_file, "Hash_Table %s: size=%d numEntries=%d maxchain=%d\n",
+		name, t->bucketsSize, t->numEntries, t->maxchain);
+}
diff --git a/hash.h b/hash.h
index 2e4ea6a43560..d98a8144ed85 100644
--- a/hash.h
+++ b/hash.h
@@ -1,4 +1,4 @@
-/*	$NetBSD: hash.h,v 1.13 2020/07/03 17:03:09 rillig Exp $	*/
+/*	$NetBSD: hash.h,v 1.21 2020/09/01 21:11:31 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -72,71 +72,51 @@
  *	from: @(#)hash.h	8.1 (Berkeley) 6/6/93
  */
 
-/* hash.h --
- *
- * 	This file contains definitions used by the hash module,
- * 	which maintains hash tables.
- */
+/* Hash tables with strings as keys and arbitrary pointers as values. */
 
-#ifndef	_HASH_H
-#define	_HASH_H
-
-/*
- * The following defines one entry in the hash table.
- */
+#ifndef	MAKE_HASH_H
+#define	MAKE_HASH_H
 
+/* A single key-value entry in the hash table. */
 typedef struct Hash_Entry {
-    struct Hash_Entry *next;		/* Used to link together all the
-    					 * entries associated with the same
-					 * bucket. */
-    void	      *clientPtr;	/* Arbitrary pointer */
-    unsigned	      namehash;		/* hash value of key */
-    char	      name[1];		/* key string */
+    struct Hash_Entry *next;	/* Used to link together all the entries
+				 * associated with the same bucket. */
+    void	      *value;
+    unsigned	      namehash;	/* hash value of key */
+    char	      name[1];	/* key string, variable length */
 } Hash_Entry;
 
+/* The hash table containing the entries. */
 typedef struct Hash_Table {
-    struct Hash_Entry **bucketPtr;/* Pointers to Hash_Entry, one
-    				 * for each bucket in the table. */
-    int 	size;		/* Actual size of array. */
+    Hash_Entry **buckets;	/* Pointers to Hash_Entry, one
+				 * for each bucket in the table. */
+    int 	bucketsSize;
     int 	numEntries;	/* Number of entries in the table. */
-    int 	mask;		/* Used to select bits for hashing. */
+    int 	bucketsMask;	/* Used to select the bucket for a hash. */
+    int 	maxchain;	/* max length of chain detected */
 } Hash_Table;
 
 /*
  * The following structure is used by the searching routines
  * to record where we are in the search.
  */
-
 typedef struct Hash_Search {
-    Hash_Table  *tablePtr;	/* Table being searched. */
-    int 	nextIndex;	/* Next bucket to check (after current). */
-    Hash_Entry 	*hashEntryPtr;	/* Next entry to check in current bucket. */
+    Hash_Table  *table;		/* Table being searched. */
+    int 	nextBucket;	/* Next bucket to check (after current). */
+    Hash_Entry 	*entry;		/* Next entry to check in current bucket. */
 } Hash_Search;
 
-/*
- * Macros.
- */
+static inline void * MAKE_ATTR_UNUSED
+Hash_GetValue(Hash_Entry *h)
+{
+    return h->value;
+}
 
-/*
- * void * Hash_GetValue(h)
- *     Hash_Entry *h;
- */
-
-#define Hash_GetValue(h) ((h)->clientPtr)
-
-/*
- * Hash_SetValue(h, val);
- *     Hash_Entry *h;
- *     char *val;
- */
-
-#define Hash_SetValue(h, val) ((h)->clientPtr = (val))
-
-/*
- * Hash_Size(n) returns the number of words in an object of n bytes
- */
-
-#define	Hash_Size(n)	(((n) + sizeof (int) - 1) / sizeof (int))
+static inline void MAKE_ATTR_UNUSED
+Hash_SetValue(Hash_Entry *h, void *datum)
+{
+    h->value = datum;
+}
 
 void Hash_InitTable(Hash_Table *, int);
 void Hash_DeleteTable(Hash_Table *);
@@ -146,5 +126,6 @@ void Hash_DeleteEntry(Hash_Table *, Hash_Entry *);
 Hash_Entry *Hash_EnumFirst(Hash_Table *, Hash_Search *);
 Hash_Entry *Hash_EnumNext(Hash_Search *);
 void Hash_ForEach(Hash_Table *, void (*)(void *, void *), void *);
+void Hash_DebugStats(Hash_Table *, const char *);
 
-#endif /* _HASH_H */
+#endif /* MAKE_HASH_H */
diff --git a/job.c b/job.c
index b6a0ab334a6f..08fbc147f901 100644
--- a/job.c
+++ b/job.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: job.c,v 1.201 2020/07/03 08:13:23 rillig Exp $	*/
+/*	$NetBSD: job.c,v 1.227 2020/08/30 19:56:02 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -70,14 +70,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: job.c,v 1.201 2020/07/03 08:13:23 rillig Exp $";
+static char rcsid[] = "$NetBSD: job.c,v 1.227 2020/08/30 19:56:02 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)job.c	8.2 (Berkeley) 3/19/94";
 #else
-__RCSID("$NetBSD: job.c,v 1.201 2020/07/03 08:13:23 rillig Exp $");
+__RCSID("$NetBSD: job.c,v 1.227 2020/08/30 19:56:02 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -112,7 +112,7 @@ __RCSID("$NetBSD: job.c,v 1.201 2020/07/03 08:13:23 rillig Exp $");
  *
  *	Job_ParseShell	    	Given the line following a .SHELL target, parse
  *	    	  	    	the line as a shell specification. Returns
- *	    	  	    	FAILURE if the spec was incorrect.
+ *	    	  	    	FALSE if the spec was incorrect.
  *
  *	Job_Finish	    	Perform any final processing which needs doing.
  *	    	  	    	This includes the execution of any commands
@@ -142,7 +142,6 @@ __RCSID("$NetBSD: job.c,v 1.201 2020/07/03 08:13:23 rillig Exp $");
 #include <sys/time.h>
 #include "wait.h"
 
-#include <assert.h>
 #include <errno.h>
 #if !defined(USE_SELECT) && defined(HAVE_POLL_H)
 #include <poll.h>
@@ -155,15 +154,12 @@ __RCSID("$NetBSD: job.c,v 1.201 2020/07/03 08:13:23 rillig Exp $");
 #endif
 #endif
 #include <signal.h>
-#include <stdio.h>
-#include <string.h>
 #include <utime.h>
 #if defined(HAVE_SYS_SOCKET_H)
 # include <sys/socket.h>
 #endif
 
 #include "make.h"
-#include "hash.h"
 #include "dir.h"
 #include "job.h"
 #include "pathnames.h"
@@ -184,7 +180,6 @@ static int    	aborting = 0;	    /* why is the make aborting? */
  * this tracks the number of tokens currently "out" to build jobs.
  */
 int jobTokensRunning = 0;
-int not_parallel = 0;		    /* set if .NOT_PARALLEL */
 
 /*
  * XXX: Avoid SunOS bug... FILENO() is fp->_file, and file
@@ -311,9 +306,9 @@ static Shell *commandShell = &shells[DEFSHELL_INDEX]; /* this is the shell to
 						   * Job_ParseShell function */
 const char *shellPath = NULL,		  	  /* full pathname of
 						   * executable image */
-           *shellName = NULL;		      	  /* last component of shell */
+	   *shellName = NULL;		      	  /* last component of shell */
 char *shellErrFlag = NULL;
-static const char *shellArgv = NULL;		  /* Custom shell args */
+static char *shellArgv = NULL;	/* Custom shell args */
 
 
 STATIC Job	*job_table;	/* The structures that describe them */
@@ -370,11 +365,6 @@ static void JobSigLock(sigset_t *);
 static void JobSigUnlock(sigset_t *);
 static void JobSigReset(void);
 
-#if !defined(MALLOC_OPTIONS)
-# define MALLOC_OPTIONS "A"
-#endif
-const char *malloc_options= MALLOC_OPTIONS;
-
 static unsigned
 nfds_per_job(void)
 {
@@ -723,15 +713,14 @@ JobPrintCommand(void *cmdp, void *jobp)
     char	  *escCmd = NULL;    /* Command with quotes/backticks escaped */
     char     	  *cmd = (char *)cmdp;
     Job           *job = (Job *)jobp;
-    int           i, j;
 
     noSpecials = NoExecute(job->node);
 
     if (strcmp(cmd, "...") == 0) {
 	job->node->type |= OP_SAVE_CMDS;
 	if ((job->flags & JOB_IGNDOTS) == 0) {
-	    job->tailCmds = Lst_Succ(Lst_Member(job->node->commands,
-						cmd));
+	    LstNode dotsNode = Lst_FindDatum(job->node->commands, cmd);
+	    job->tailCmds = dotsNode != NULL ? LstNode_Next(dotsNode) : NULL;
 	    return 1;
 	}
 	return 0;
@@ -745,7 +734,7 @@ JobPrintCommand(void *cmdp, void *jobp)
 
     numCommands += 1;
 
-    cmdStart = cmd = Var_Subst(NULL, cmd, job->node, VARF_WANTRES);
+    cmdStart = cmd = Var_Subst(cmd, job->node, VARE_WANTRES);
 
     cmdTemplate = "%s\n";
 
@@ -785,15 +774,17 @@ JobPrintCommand(void *cmdp, void *jobp)
      */
 
     if (!commandShell->hasErrCtl) {
+	int i, j;
+
 	/* Worst that could happen is every char needs escaping. */
 	escCmd = bmake_malloc((strlen(cmd) * 2) + 1);
-	for (i = 0, j= 0; cmd[i] != '\0'; i++, j++) {
-		if (cmd[i] == '$' || cmd[i] == '`' || cmd[i] == '\\' ||
-			cmd[i] == '"')
-			escCmd[j++] = '\\';
-		escCmd[j] = cmd[i];
+	for (i = 0, j = 0; cmd[i] != '\0'; i++, j++) {
+	    if (cmd[i] == '$' || cmd[i] == '`' || cmd[i] == '\\' ||
+		cmd[i] == '"')
+		escCmd[j++] = '\\';
+	    escCmd[j] = cmd[i];
 	}
-	escCmd[j] = 0;
+	escCmd[j] = '\0';
     }
 
     if (shutUp) {
@@ -926,15 +917,15 @@ JobPrintCommand(void *cmdp, void *jobp)
  *	Always returns 0
  *
  * Side Effects:
- *	The command is tacked onto the end of postCommands's commands list.
+ *	The command is tacked onto the end of postCommands' commands list.
  *
  *-----------------------------------------------------------------------
  */
 static int
 JobSaveCommand(void *cmd, void *gn)
 {
-    cmd = Var_Subst(NULL, (char *)cmd, (GNode *)gn, VARF_WANTRES);
-    (void)Lst_AtEnd(postCommands->commands, cmd);
+    cmd = Var_Subst((char *)cmd, (GNode *)gn, VARE_WANTRES);
+    Lst_Append(postCommands->commands, cmd);
     return 0;
 }
 
@@ -1124,7 +1115,7 @@ JobFinish (Job *job, WAIT_T status)
 	if (job->tailCmds != NULL) {
 	    Lst_ForEachFrom(job->node->commands, job->tailCmds,
 			     JobSaveCommand,
-			    job->node);
+			     job->node);
 	}
 	job->node->made = MADE;
 	if (!(job->flags & JOB_SPECIAL))
@@ -1275,7 +1266,7 @@ Job_CheckCommands(GNode *gn, void (*abortProc)(const char *, ...))
 	     */
 	    Make_HandleUse(DEFAULT, gn);
 	    Var_Set(IMPSRC, Var_Value(TARGET, gn, &p1), gn);
-	    free(p1);
+	    bmake_free(p1);
 	} else if (Dir_MTime(gn, 0) == 0 && (gn->type & OP_SPECIAL) == 0) {
 	    /*
 	     * The node wasn't the target of an operator we have no .DEFAULT
@@ -1302,7 +1293,7 @@ Job_CheckCommands(GNode *gn, void (*abortProc)(const char *, ...))
 		(void)fprintf(stdout, "%s%s %s (continuing)\n", progname,
 		    msg, gn->name);
 		(void)fflush(stdout);
-  		return FALSE;
+		return FALSE;
 	    } else {
 		(*abortProc)("%s%s %s. Stop", progname, msg, gn->name);
 		return FALSE;
@@ -1345,7 +1336,7 @@ JobExec(Job *job, char **argv)
 	for (i = 0; argv[i] != NULL; i++) {
 	    (void)fprintf(debug_file, "%s ", argv[i]);
 	}
- 	(void)fprintf(debug_file, "\n");
+	(void)fprintf(debug_file, "\n");
     }
 
     /*
@@ -1719,7 +1710,7 @@ JobStart(GNode *gn, int flags)
 	 * up the graph.
 	 */
 	job->cmdFILE = stdout;
-    	Job_Touch(gn, job->flags&JOB_SILENT);
+	Job_Touch(gn, job->flags&JOB_SILENT);
 	noExec = TRUE;
     }
     /* Just in case it isn't already... */
@@ -1748,8 +1739,8 @@ JobStart(GNode *gn, int flags)
 	if (cmdsOK && aborting == 0) {
 	    if (job->tailCmds != NULL) {
 		Lst_ForEachFrom(job->node->commands, job->tailCmds,
-				JobSaveCommand,
-			       job->node);
+				 JobSaveCommand,
+				 job->node);
 	    }
 	    job->node->made = MADE;
 	    Make_Update(job->node);
@@ -1990,8 +1981,8 @@ JobRun(GNode *targ)
      * and .INTERRUPT job in the parallel job module. This has
      * the nice side effect that it avoids a lot of other problems.
      */
-    Lst lst = Lst_Init(FALSE);
-    Lst_AtEnd(lst, targ);
+    Lst lst = Lst_Init();
+    Lst_Append(lst, targ);
     (void)Make_Run(lst);
     Lst_Destroy(lst, NULL);
     JobStart(targ, JOB_SPECIAL);
@@ -2218,8 +2209,9 @@ Shell_Init(void)
 	    shellName++;
 	} else
 #endif
-	shellPath = str_concat(_PATH_DEFSHELLDIR, shellName, STR_ADDSLASH);
+	shellPath = str_concat3(_PATH_DEFSHELLDIR, "/", shellName);
     }
+    Var_Set_with_flags(".SHELL", shellPath, VAR_CMD, VAR_SET_READONLY);
     if (commandShell->exit == NULL) {
 	commandShell->exit = "";
     }
@@ -2253,38 +2245,23 @@ Shell_Init(void)
 const char *
 Shell_GetNewline(void)
 {
-
     return commandShell->newline;
 }
 
 void
 Job_SetPrefix(void)
 {
-
     if (targPrefix) {
 	free(targPrefix);
     } else if (!Var_Exists(MAKE_JOB_PREFIX, VAR_GLOBAL)) {
 	Var_Set(MAKE_JOB_PREFIX, "---", VAR_GLOBAL);
     }
 
-    targPrefix = Var_Subst(NULL, "${" MAKE_JOB_PREFIX "}",
-			   VAR_GLOBAL, VARF_WANTRES);
+    targPrefix = Var_Subst("${" MAKE_JOB_PREFIX "}",
+			   VAR_GLOBAL, VARE_WANTRES);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Job_Init --
- *	Initialize the process module
- *
- * Input:
- *
- * Results:
- *	none
- *
- * Side Effects:
- *	lists and counters are initialized
- *-----------------------------------------------------------------------
- */
+/* Initialize the process module. */
 void
 Job_Init(void)
 {
@@ -2390,19 +2367,7 @@ static void JobSigReset(void)
     (void)bmake_signal(SIGCHLD, SIG_DFL);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * JobMatchShell --
- *	Find a shell in 'shells' given its name.
- *
- * Results:
- *	A pointer to the Shell structure.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
+/* Find a shell in 'shells' given its name, or return NULL. */
 static Shell *
 JobMatchShell(const char *name)
 {
@@ -2425,7 +2390,7 @@ JobMatchShell(const char *name)
  *	line		The shell spec
  *
  * Results:
- *	FAILURE if the specification was incorrect.
+ *	FALSE if the specification was incorrect.
  *
  * Side Effects:
  *	commandShell points to a Shell structure (either predefined or
@@ -2462,12 +2427,13 @@ JobMatchShell(const char *name)
  *
  *-----------------------------------------------------------------------
  */
-ReturnStatus
+Boolean
 Job_ParseShell(char *line)
 {
+    Words	wordsList;
     char	**words;
     char	**argv;
-    int		argc;
+    size_t	argc;
     char	*path;
     Shell	newShell;
     Boolean	fullSpec = FALSE;
@@ -2477,17 +2443,20 @@ Job_ParseShell(char *line)
 	line++;
     }
 
-    free(UNCONST(shellArgv));
+    free(shellArgv);
 
     memset(&newShell, 0, sizeof(newShell));
 
     /*
      * Parse the specification by keyword
      */
-    words = brk_string(line, &argc, TRUE, &path);
+    wordsList = Str_Words(line, TRUE);
+    words = wordsList.words;
+    argc = wordsList.len;
+    path = wordsList.freeIt;
     if (words == NULL) {
 	Error("Unterminated quoted string [%s]", line);
-	return FAILURE;
+	return FALSE;
     }
     shellArgv = path;
 
@@ -2526,7 +2495,7 @@ Job_ParseShell(char *line)
 		    Parse_Error(PARSE_FATAL, "Unknown keyword \"%s\"",
 				*argv);
 		    free(words);
-		    return FAILURE;
+		    return FALSE;
 		}
 		fullSpec = TRUE;
 	    }
@@ -2542,13 +2511,13 @@ Job_ParseShell(char *line)
 	if (newShell.name == NULL) {
 	    Parse_Error(PARSE_FATAL, "Neither path nor name specified");
 	    free(words);
-	    return FAILURE;
+	    return FALSE;
 	} else {
 	    if ((sh = JobMatchShell(newShell.name)) == NULL) {
 		    Parse_Error(PARSE_WARNING, "%s: No matching shell",
 				newShell.name);
 		    free(words);
-		    return FAILURE;
+		    return FALSE;
 	    }
 	    commandShell = sh;
 	    shellName = newShell.name;
@@ -2584,7 +2553,7 @@ Job_ParseShell(char *line)
 		    Parse_Error(PARSE_WARNING, "%s: No matching shell",
 				shellName);
 		    free(words);
-		    return FAILURE;
+		    return FALSE;
 	    }
 	    commandShell = sh;
 	} else {
@@ -2613,7 +2582,7 @@ Job_ParseShell(char *line)
      * shell specification.
      */
     free(words);
-    return SUCCESS;
+    return TRUE;
 }
 
 /*-
@@ -2790,7 +2759,6 @@ Job_AbortAll(void)
 	continue;
 }
 
-
 /*-
  *-----------------------------------------------------------------------
  * JobRestartJobs --
@@ -3125,7 +3093,7 @@ emul_poll(struct pollfd *fd, int nfd, int timeout)
 	usecs = timeout * 1000;
 	tv.tv_sec = usecs / 1000000;
 	tv.tv_usec = usecs % 1000000;
-        tvp = &tv;
+	tvp = &tv;
     }
 
     nselect = select(maxfd + 1, &rfds, &wfds, 0, tvp);
diff --git a/job.h b/job.h
index 603c09e861f1..45fc1b7f8df4 100644
--- a/job.h
+++ b/job.h
@@ -1,4 +1,4 @@
-/*	$NetBSD: job.h,v 1.43 2020/07/03 08:13:23 rillig Exp $	*/
+/*	$NetBSD: job.h,v 1.47 2020/08/29 12:20:17 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -77,8 +77,8 @@
  * job.h --
  *	Definitions pertaining to the running of jobs in parallel mode.
  */
-#ifndef _JOB_H_
-#define _JOB_H_
+#ifndef MAKE_JOB_H
+#define MAKE_JOB_H
 
 #define TMPPAT	"makeXXXXXX"		/* relative to tmpdir */
 
@@ -110,7 +110,6 @@ emul_poll(struct pollfd *fd, int nfd, int timeout);
  */
 #define POLL_MSEC	5000
 
-
 /*-
  * Job Table definitions.
  *
@@ -164,7 +163,7 @@ typedef struct Job {
 				 * commands */
 #define JOB_TRACED	0x400	/* we've sent 'set -x' */
 
-    int	  	 jobPipe[2];	/* Pipe for readind output from job */
+    int	  	 jobPipe[2];	/* Pipe for reading output from job */
     struct pollfd *inPollfd;	/* pollfd associated with inPipe */
     char  	outBuf[JOB_BUFSIZE + 1];
 				/* Buffer for storing the output of the
@@ -179,7 +178,6 @@ typedef struct Job {
 #define inPipe jobPipe[0]
 #define outPipe jobPipe[1]
 
-
 /*-
  * Shell Specifications:
  * Each shell type has associated with it the following information:
@@ -252,23 +250,20 @@ void Shell_Init(void);
 const char *Shell_GetNewline(void);
 void Job_Touch(GNode *, Boolean);
 Boolean Job_CheckCommands(GNode *, void (*abortProc )(const char *, ...));
-#define CATCH_BLOCK	1
 void Job_CatchChildren(void);
 void Job_CatchOutput(void);
 void Job_Make(GNode *);
 void Job_Init(void);
-Boolean Job_Full(void);
 Boolean Job_Empty(void);
-ReturnStatus Job_ParseShell(char *);
+Boolean Job_ParseShell(char *);
 int Job_Finish(void);
 void Job_End(void);
 void Job_Wait(void);
 void Job_AbortAll(void);
-void JobFlagForMigration(int);
 void Job_TokenReturn(void);
 Boolean Job_TokenWithdraw(void);
 void Job_ServerStart(int, int, int);
 void Job_SetPrefix(void);
 Boolean Job_RunTarget(const char *, const char *);
 
-#endif /* _JOB_H_ */
+#endif /* MAKE_JOB_H */
diff --git a/lst.c b/lst.c
new file mode 100644
index 000000000000..26b2457cc013
--- /dev/null
+++ b/lst.c
@@ -0,0 +1,641 @@
+/* $NetBSD: lst.c,v 1.60 2020/08/31 05:56:02 rillig Exp $ */
+
+/*
+ * Copyright (c) 1988, 1989, 1990, 1993
+ *	The Regents of the University of California.  All rights reserved.
+ *
+ * This code is derived from software contributed to Berkeley by
+ * Adam de Boor.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#ifdef HAVE_INTTYPES_H
+#include <inttypes.h>
+#elif defined(HAVE_STDINT_H)
+#include <stdint.h>
+#endif
+
+#include "make.h"
+
+#ifndef MAKE_NATIVE
+static char rcsid[] = "$NetBSD: lst.c,v 1.60 2020/08/31 05:56:02 rillig Exp $";
+#else
+#include <sys/cdefs.h>
+#ifndef lint
+__RCSID("$NetBSD: lst.c,v 1.60 2020/08/31 05:56:02 rillig Exp $");
+#endif /* not lint */
+#endif
+
+struct ListNode {
+    struct ListNode *prev;	/* previous element in list */
+    struct ListNode *next;	/* next in list */
+    uint8_t useCount;		/* Count of functions using the node.
+				 * node may not be deleted until count
+				 * goes to 0 */
+    Boolean deleted;		/* List node should be removed when done */
+    union {
+	void *datum;		/* datum associated with this element */
+	const GNode *gnode;	/* alias, just for debugging */
+	const char *str;	/* alias, just for debugging */
+    };
+};
+
+typedef enum {
+    Head, Middle, Tail, Unknown
+} Where;
+
+struct List {
+    LstNode first;		/* first node in list */
+    LstNode last;		/* last node in list */
+
+    /* fields for sequential access */
+    Boolean isOpen;		/* true if list has been Lst_Open'ed */
+    Where lastAccess;		/* Where in the list the last access was */
+    LstNode curr;		/* current node, if open. NULL if
+				 * *just* opened */
+    LstNode prev;		/* Previous node, if open. Used by Lst_Remove */
+};
+
+/* Allocate and initialize a list node.
+ *
+ * The fields 'prev' and 'next' must be initialized by the caller.
+ */
+static LstNode
+LstNodeNew(void *datum)
+{
+    LstNode node = bmake_malloc(sizeof *node);
+    node->useCount = 0;
+    node->deleted = FALSE;
+    node->datum = datum;
+    return node;
+}
+
+static Boolean
+LstIsEmpty(Lst list)
+{
+    return list->first == NULL;
+}
+
+/* Create and initialize a new, empty list. */
+Lst
+Lst_Init(void)
+{
+    Lst list = bmake_malloc(sizeof *list);
+
+    list->first = NULL;
+    list->last = NULL;
+    list->isOpen = FALSE;
+    list->lastAccess = Unknown;
+
+    return list;
+}
+
+/* Duplicate an entire list, usually by copying the datum pointers.
+ * If copyProc is given, that function is used to create the new datum from the
+ * old datum, usually by creating a copy of it. */
+Lst
+Lst_Copy(Lst list, LstCopyProc copyProc)
+{
+    Lst newList;
+    LstNode node;
+
+    assert(list != NULL);
+
+    newList = Lst_Init();
+
+    for (node = list->first; node != NULL; node = node->next) {
+	void *datum = copyProc != NULL ? copyProc(node->datum) : node->datum;
+	Lst_Append(newList, datum);
+    }
+
+    return newList;
+}
+
+/* Free a list and all its nodes. The list data itself are not freed though. */
+void
+Lst_Free(Lst list)
+{
+    LstNode node;
+    LstNode next;
+
+    assert(list != NULL);
+
+    for (node = list->first; node != NULL; node = next) {
+	next = node->next;
+	free(node);
+    }
+
+    free(list);
+}
+
+/* Destroy a list and free all its resources. The freeProc is called with the
+ * datum from each node in turn before the node is freed. */
+void
+Lst_Destroy(Lst list, LstFreeProc freeProc)
+{
+    LstNode node;
+    LstNode next;
+
+    assert(list != NULL);
+    assert(freeProc != NULL);
+
+    for (node = list->first; node != NULL; node = next) {
+	next = node->next;
+	freeProc(node->datum);
+	free(node);
+    }
+
+    free(list);
+}
+
+/*
+ * Functions to modify a list
+ */
+
+/* Insert a new node with the given piece of data before the given node in the
+ * given list. */
+void
+Lst_InsertBefore(Lst list, LstNode node, void *datum)
+{
+    LstNode newNode;
+
+    assert(list != NULL);
+    assert(!LstIsEmpty(list));
+    assert(node != NULL);
+    assert(datum != NULL);
+
+    newNode = LstNodeNew(datum);
+    newNode->prev = node->prev;
+    newNode->next = node;
+
+    if (node->prev != NULL) {
+	node->prev->next = newNode;
+    }
+    node->prev = newNode;
+
+    if (node == list->first) {
+	list->first = newNode;
+    }
+}
+
+/* Add a piece of data at the start of the given list. */
+void
+Lst_Prepend(Lst list, void *datum)
+{
+    LstNode node;
+
+    assert(list != NULL);
+    assert(datum != NULL);
+
+    node = LstNodeNew(datum);
+    node->prev = NULL;
+    node->next = list->first;
+
+    if (list->first == NULL) {
+	list->first = node;
+	list->last = node;
+    } else {
+	list->first->prev = node;
+	list->first = node;
+    }
+}
+
+/* Add a piece of data at the end of the given list. */
+void
+Lst_Append(Lst list, void *datum)
+{
+    LstNode node;
+
+    assert(list != NULL);
+    assert(datum != NULL);
+
+    node = LstNodeNew(datum);
+    node->prev = list->last;
+    node->next = NULL;
+
+    if (list->last == NULL) {
+	list->first = node;
+	list->last = node;
+    } else {
+	list->last->next = node;
+	list->last = node;
+    }
+}
+
+/* Remove the given node from the given list.
+ * The datum stored in the node must be freed by the caller, if necessary. */
+void
+Lst_Remove(Lst list, LstNode node)
+{
+    assert(list != NULL);
+    assert(node != NULL);
+
+    /*
+     * unlink it from the list
+     */
+    if (node->next != NULL) {
+	node->next->prev = node->prev;
+    }
+    if (node->prev != NULL) {
+	node->prev->next = node->next;
+    }
+
+    /*
+     * if either the first or last of the list point to this node,
+     * adjust them accordingly
+     */
+    if (list->first == node) {
+	list->first = node->next;
+    }
+    if (list->last == node) {
+	list->last = node->prev;
+    }
+
+    /*
+     * Sequential access stuff. If the node we're removing is the current
+     * node in the list, reset the current node to the previous one. If the
+     * previous one was non-existent (prev == NULL), we set the
+     * end to be Unknown, since it is.
+     */
+    if (list->isOpen && list->curr == node) {
+	list->curr = list->prev;
+	if (list->curr == NULL) {
+	    list->lastAccess = Unknown;
+	}
+    }
+
+    /*
+     * note that the datum is unmolested. The caller must free it as
+     * necessary and as expected.
+     */
+    if (node->useCount == 0) {
+	free(node);
+    } else {
+	node->deleted = TRUE;
+    }
+}
+
+/* Replace the datum in the given node with the new datum. */
+void
+LstNode_Set(LstNode node, void *datum)
+{
+    assert(node != NULL);
+    assert(datum != NULL);
+
+    node->datum = datum;
+}
+
+/* Replace the datum in the given node to NULL. */
+void
+LstNode_SetNull(LstNode node)
+{
+    assert(node != NULL);
+
+    node->datum = NULL;
+}
+
+
+/*
+ * Node-specific functions
+ */
+
+/* Return the first node from the given list, or NULL if the list is empty. */
+LstNode
+Lst_First(Lst list)
+{
+    assert(list != NULL);
+
+    return list->first;
+}
+
+/* Return the last node from the given list, or NULL if the list is empty. */
+LstNode
+Lst_Last(Lst list)
+{
+    assert(list != NULL);
+
+    return list->last;
+}
+
+/* Return the successor to the given node on its list, or NULL. */
+LstNode
+LstNode_Next(LstNode node)
+{
+    assert(node != NULL);
+
+    return node->next;
+}
+
+/* Return the predecessor to the given node on its list, or NULL. */
+LstNode
+LstNode_Prev(LstNode node)
+{
+    assert(node != NULL);
+    return node->prev;
+}
+
+/* Return the datum stored in the given node. */
+void *
+LstNode_Datum(LstNode node)
+{
+    assert(node != NULL);
+    return node->datum;
+}
+
+
+/*
+ * Functions for entire lists
+ */
+
+/* Return TRUE if the given list is empty. */
+Boolean
+Lst_IsEmpty(Lst list)
+{
+    assert(list != NULL);
+
+    return LstIsEmpty(list);
+}
+
+/* Return the first node from the list for which the match function returns
+ * TRUE, or NULL if none of the nodes matched. */
+LstNode
+Lst_Find(Lst list, LstFindProc match, const void *matchArgs)
+{
+    return Lst_FindFrom(list, Lst_First(list), match, matchArgs);
+}
+
+/* Return the first node from the list, starting at the given node, for which
+ * the match function returns TRUE, or NULL if none of the nodes matches.
+ *
+ * The start node may be NULL, in which case nothing is found. This allows
+ * for passing Lst_First or LstNode_Next as the start node. */
+LstNode
+Lst_FindFrom(Lst list, LstNode node, LstFindProc match, const void *matchArgs)
+{
+    LstNode tln;
+
+    assert(list != NULL);
+    assert(match != NULL);
+
+    for (tln = node; tln != NULL; tln = tln->next) {
+	if (match(tln->datum, matchArgs))
+	    return tln;
+    }
+
+    return NULL;
+}
+
+/* Return the first node that contains the given datum, or NULL. */
+LstNode
+Lst_FindDatum(Lst list, const void *datum)
+{
+    LstNode node;
+
+    assert(list != NULL);
+    assert(datum != NULL);
+
+    for (node = list->first; node != NULL; node = node->next) {
+	if (node->datum == datum) {
+	    return node;
+	}
+    }
+
+    return NULL;
+}
+
+/* Apply the given function to each element of the given list. The function
+ * should return 0 if traversal should continue and non-zero if it should
+ * abort. */
+int
+Lst_ForEach(Lst list, LstActionProc proc, void *procData)
+{
+    if (LstIsEmpty(list))
+	return 0;		/* XXX: Document what this value means. */
+    return Lst_ForEachFrom(list, Lst_First(list), proc, procData);
+}
+
+/* Apply the given function to each element of the given list, starting from
+ * the given node. The function should return 0 if traversal should continue,
+ * and non-zero if it should abort. */
+int
+Lst_ForEachFrom(Lst list, LstNode node,
+		 LstActionProc proc, void *procData)
+{
+    LstNode tln = node;
+    LstNode next;
+    Boolean done;
+    int result;
+
+    assert(list != NULL);
+    assert(node != NULL);
+    assert(proc != NULL);
+
+    do {
+	/*
+	 * Take care of having the current element deleted out from under
+	 * us.
+	 */
+
+	next = tln->next;
+
+	/*
+	 * We're done with the traversal if
+	 *  - the next node to examine doesn't exist and
+	 *  - nothing's been added after the current node (check this
+	 *    after proc() has been called).
+	 */
+	done = next == NULL;
+
+	tln->useCount++;
+	result = (*proc)(tln->datum, procData);
+	tln->useCount--;
+
+	/*
+	 * Now check whether a node has been added.
+	 * Note: this doesn't work if this node was deleted before
+	 *       the new node was added.
+	 */
+	if (next != tln->next) {
+	    next = tln->next;
+	    done = 0;
+	}
+
+	if (tln->deleted) {
+	    free((char *)tln);
+	}
+	tln = next;
+    } while (!result && !LstIsEmpty(list) && !done);
+
+    return result;
+}
+
+/* Move all nodes from list2 to the end of list1.
+ * List2 is destroyed and freed. */
+void
+Lst_MoveAll(Lst list1, Lst list2)
+{
+    assert(list1 != NULL);
+    assert(list2 != NULL);
+
+    if (list2->first != NULL) {
+	list2->first->prev = list1->last;
+	if (list1->last != NULL) {
+	    list1->last->next = list2->first;
+	} else {
+	    list1->first = list2->first;
+	}
+	list1->last = list2->last;
+    }
+    free(list2);
+}
+
+/* Copy the element data from src to the start of dst. */
+void
+Lst_PrependAll(Lst dst, Lst src)
+{
+    LstNode node;
+    for (node = src->last; node != NULL; node = node->prev)
+	Lst_Prepend(dst, node->datum);
+}
+
+/* Copy the element data from src to the end of dst. */
+void
+Lst_AppendAll(Lst dst, Lst src)
+{
+    LstNode node;
+    for (node = src->first; node != NULL; node = node->next)
+	Lst_Append(dst, node->datum);
+}
+
+/*
+ * these functions are for dealing with a list as a table, of sorts.
+ * An idea of the "current element" is kept and used by all the functions
+ * between Lst_Open() and Lst_Close().
+ *
+ * The sequential functions access the list in a slightly different way.
+ * CurPtr points to their idea of the current node in the list and they
+ * access the list based on it.
+ */
+
+/* Open a list for sequential access. A list can still be searched, etc.,
+ * without confusing these functions. */
+void
+Lst_Open(Lst list)
+{
+    assert(list != NULL);
+    assert(!list->isOpen);
+
+    list->isOpen = TRUE;
+    list->lastAccess = LstIsEmpty(list) ? Head : Unknown;
+    list->curr = NULL;
+}
+
+/* Return the next node for the given list, or NULL if the end has been
+ * reached. */
+LstNode
+Lst_Next(Lst list)
+{
+    LstNode node;
+
+    assert(list != NULL);
+    assert(list->isOpen);
+
+    list->prev = list->curr;
+
+    if (list->curr == NULL) {
+	if (list->lastAccess == Unknown) {
+	    /*
+	     * If we're just starting out, lastAccess will be Unknown.
+	     * Then we want to start this thing off in the right
+	     * direction -- at the start with lastAccess being Middle.
+	     */
+	    list->curr = node = list->first;
+	    list->lastAccess = Middle;
+	} else {
+	    node = NULL;
+	    list->lastAccess = Tail;
+	}
+    } else {
+	node = list->curr->next;
+	list->curr = node;
+
+	if (node == list->first || node == NULL) {
+	    /*
+	     * If back at the front, then we've hit the end...
+	     */
+	    list->lastAccess = Tail;
+	} else {
+	    /*
+	     * Reset to Middle if gone past first.
+	     */
+	    list->lastAccess = Middle;
+	}
+    }
+
+    return node;
+}
+
+/* Close a list which was opened for sequential access. */
+void
+Lst_Close(Lst list)
+{
+    assert(list != NULL);
+    assert(list->isOpen);
+
+    list->isOpen = FALSE;
+    list->lastAccess = Unknown;
+}
+
+
+/*
+ * for using the list as a queue
+ */
+
+/* Add the datum to the tail of the given list. */
+void
+Lst_Enqueue(Lst list, void *datum)
+{
+    Lst_Append(list, datum);
+}
+
+/* Remove and return the datum at the head of the given list. */
+void *
+Lst_Dequeue(Lst list)
+{
+    void *datum;
+
+    assert(list != NULL);
+    assert(!LstIsEmpty(list));
+
+    datum = list->first->datum;
+    Lst_Remove(list, list->first);
+    assert(datum != NULL);
+    return datum;
+}
diff --git a/lst.h b/lst.h
index e207bc808bf9..d9aa9feb3b65 100644
--- a/lst.h
+++ b/lst.h
@@ -1,4 +1,4 @@
-/*	$NetBSD: lst.h,v 1.20 2014/09/07 20:55:34 joerg Exp $	*/
+/*	$NetBSD: lst.h,v 1.60 2020/09/02 23:33:13 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -73,117 +73,107 @@
  *	from: @(#)lst.h	8.1 (Berkeley) 6/6/93
  */
 
-/*-
- * lst.h --
- *	Header for using the list library
- */
-#ifndef _LST_H_
-#define _LST_H_
+/* Doubly-linked lists of arbitrary pointers. */
 
-#include	<sys/param.h>
-#include	<stdlib.h>
+#ifndef MAKE_LST_H
+#define MAKE_LST_H
 
-#include	"sprite.h"
-
-/*
- * basic typedef. This is what the Lst_ functions handle
- */
+#include <sys/param.h>
+#include <stdlib.h>
 
+/* A doubly-linked list of pointers. */
 typedef	struct List	*Lst;
+/* A single node in the doubly-linked list. */
 typedef	struct ListNode	*LstNode;
 
-typedef void		*DuplicateProc(void *);
-typedef void		FreeProc(void *);
+/* Copy a node, usually by allocating a copy of the given object.
+ * For reference-counted objects, the original object may need to be
+ * modified, therefore the parameter is not const. */
+typedef void *LstCopyProc(void *);
+/* Free the datum of a node, called before freeing the node itself. */
+typedef void LstFreeProc(void *);
+/* Return TRUE if the datum matches the args, for Lst_Find. */
+typedef Boolean LstFindProc(const void *datum, const void *args);
+/* An action for Lst_ForEach. */
+typedef int LstActionProc(void *datum, void *args);
 
-#define LST_CONCNEW	0   /* create new LstNode's when using Lst_Concat */
-#define LST_CONCLINK	1   /* relink LstNode's when using Lst_Concat */
+/* Create or destroy a list */
 
-/*
- * Creation/destruction functions
- */
-/* Create a new list */
-Lst		Lst_Init(Boolean);
-/* Duplicate an existing list */
-Lst		Lst_Duplicate(Lst, DuplicateProc *);
-/* Destroy an old one */
-void		Lst_Destroy(Lst, FreeProc *);
-/* True if list is empty */
-Boolean		Lst_IsEmpty(Lst);
+/* Create a new list. */
+Lst Lst_Init(void);
+/* Duplicate an existing list. */
+Lst Lst_Copy(Lst, LstCopyProc);
+/* Free the list, leaving the node data unmodified. */
+void Lst_Free(Lst);
+/* Free the list, freeing the node data using the given function. */
+void Lst_Destroy(Lst, LstFreeProc);
 
-/*
- * Functions to modify a list
- */
-/* Insert an element before another */
-ReturnStatus	Lst_InsertBefore(Lst, LstNode, void *);
-/* Insert an element after another */
-ReturnStatus	Lst_InsertAfter(Lst, LstNode, void *);
-/* Place an element at the front of a lst. */
-ReturnStatus	Lst_AtFront(Lst, void *);
-/* Place an element at the end of a lst. */
-ReturnStatus	Lst_AtEnd(Lst, void *);
-/* Remove an element */
-ReturnStatus	Lst_Remove(Lst, LstNode);
-/* Replace a node with a new value */
-ReturnStatus	Lst_Replace(LstNode, void *);
-/* Concatenate two lists */
-ReturnStatus	Lst_Concat(Lst, Lst, int);
+/* Get information about a list */
 
-/*
- * Node-specific functions
- */
-/* Return first element in list */
-LstNode		Lst_First(Lst);
-/* Return last element in list */
-LstNode		Lst_Last(Lst);
-/* Return successor to given element */
-LstNode		Lst_Succ(LstNode);
-/* Return predecessor to given element */
-LstNode		Lst_Prev(LstNode);
-/* Get datum from LstNode */
-void		*Lst_Datum(LstNode);
+Boolean Lst_IsEmpty(Lst);
+/* Return the first node of the list, or NULL. */
+LstNode Lst_First(Lst);
+/* Return the last node of the list, or NULL. */
+LstNode Lst_Last(Lst);
+/* Find the first node for which the function returns TRUE, or NULL. */
+LstNode Lst_Find(Lst, LstFindProc, const void *);
+/* Find the first node for which the function returns TRUE, or NULL.
+ * The search starts at the given node, towards the end of the list. */
+LstNode Lst_FindFrom(Lst, LstNode, LstFindProc, const void *);
+/* Find the first node that contains the given datum, or NULL. */
+LstNode Lst_FindDatum(Lst, const void *);
 
-/*
- * Functions for entire lists
- */
-/* Find an element in a list */
-LstNode		Lst_Find(Lst, const void *, int (*)(const void *, const void *));
-/* Find an element starting from somewhere */
-LstNode		Lst_FindFrom(Lst, LstNode, const void *,
-			     int (*cProc)(const void *, const void *));
-/*
- * See if the given datum is on the list. Returns the LstNode containing
- * the datum
- */
-LstNode		Lst_Member(Lst, void *);
-/* Apply a function to all elements of a lst */
-int		Lst_ForEach(Lst, int (*)(void *, void *), void *);
-/*
- * Apply a function to all elements of a lst starting from a certain point.
- * If the list is circular, the application will wrap around to the
- * beginning of the list again.
- */
-int		Lst_ForEachFrom(Lst, LstNode, int (*)(void *, void *),
-				void *);
-/*
- * these functions are for dealing with a list as a table, of sorts.
- * An idea of the "current element" is kept and used by all the functions
- * between Lst_Open() and Lst_Close().
- */
-/* Open the list */
-ReturnStatus	Lst_Open(Lst);
-/* Next element please */
-LstNode		Lst_Next(Lst);
-/* Done yet? */
-Boolean		Lst_IsAtEnd(Lst);
-/* Finish table access */
-void		Lst_Close(Lst);
+/* Modify a list */
 
-/*
- * for using the list as a queue
- */
-/* Place an element at tail of queue */
-ReturnStatus	Lst_EnQueue(Lst, void *);
-/* Remove an element from head of queue */
-void		*Lst_DeQueue(Lst);
+/* Insert a datum before the given node. */
+void Lst_InsertBefore(Lst, LstNode, void *);
+/* Place a datum at the front of the list. */
+void Lst_Prepend(Lst, void *);
+/* Place a datum at the end of the list. */
+void Lst_Append(Lst, void *);
+/* Remove the node from the list. */
+void Lst_Remove(Lst, LstNode);
+void Lst_PrependAll(Lst, Lst);
+void Lst_AppendAll(Lst, Lst);
+void Lst_MoveAll(Lst, Lst);
 
-#endif /* _LST_H_ */
+/* Node-specific functions */
+
+/* Return the successor of the node, or NULL. */
+LstNode LstNode_Next(LstNode);
+/* Return the predecessor of the node, or NULL. */
+LstNode LstNode_Prev(LstNode);
+/* Return the datum of the node. Usually not NULL. */
+void *LstNode_Datum(LstNode);
+/* Replace the value of the node. */
+void LstNode_Set(LstNode, void *);
+/* Set the value of the node to NULL. Having NULL in a list is unusual. */
+void LstNode_SetNull(LstNode);
+
+/* Iterating over a list, using a callback function */
+
+/* Apply a function to each datum of the list, until the callback function
+ * returns non-zero. */
+int Lst_ForEach(Lst, LstActionProc, void *);
+/* Apply a function to each datum of the list, starting at the node,
+ * until the callback function returns non-zero. */
+int Lst_ForEachFrom(Lst, LstNode, LstActionProc, void *);
+
+/* Iterating over a list while keeping track of the current node and possible
+ * concurrent modifications */
+
+/* Start iterating the list. */
+void Lst_Open(Lst);
+/* Return the next node, or NULL. */
+LstNode Lst_Next(Lst);
+/* Finish iterating the list. */
+void Lst_Close(Lst);
+
+/* Using the list as a queue */
+
+/* Add a datum at the tail of the queue. */
+void Lst_Enqueue(Lst, void *);
+/* Remove the head node of the queue and return its datum. */
+void *Lst_Dequeue(Lst);
+
+#endif /* MAKE_LST_H */
diff --git a/lst.lib/Makefile b/lst.lib/Makefile
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/lst.lib/lstAppend.c b/lst.lib/lstAppend.c
deleted file mode 100644
index 97e60d959d7d..000000000000
--- a/lst.lib/lstAppend.c
+++ /dev/null
@@ -1,121 +0,0 @@
-/*	$NetBSD: lstAppend.c,v 1.15 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstAppend.c,v 1.15 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstAppend.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstAppend.c,v 1.15 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstAppend.c --
- *	Add a new node with a new datum after an existing node
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_InsertAfter --
- *	Create a new node and add it to the given list after the given node.
- *
- * Input:
- *	l		affected list
- *	ln		node after which to append the datum
- *	d		said datum
- *
- * Results:
- *	SUCCESS if all went well.
- *
- * Side Effects:
- *	A new ListNode is created and linked in to the List. The lastPtr
- *	field of the List will be altered if ln is the last node in the
- *	list. lastPtr and firstPtr will alter if the list was empty and
- *	ln was NULL.
- *
- *-----------------------------------------------------------------------
- */
-ReturnStatus
-Lst_InsertAfter(Lst l, LstNode ln, void *d)
-{
-    List 	list;
-    ListNode	lNode;
-    ListNode	nLNode;
-
-    if (LstValid (l) && (ln == NULL && LstIsEmpty (l))) {
-	goto ok;
-    }
-
-    if (!LstValid (l) || LstIsEmpty (l)  || ! LstNodeValid (ln, l)) {
-	return FAILURE;
-    }
-    ok:
-
-    list = l;
-    lNode = ln;
-
-    PAlloc (nLNode, ListNode);
-    nLNode->datum = d;
-    nLNode->useCount = nLNode->flags = 0;
-
-    if (lNode == NULL) {
-	if (list->isCirc) {
-	    nLNode->nextPtr = nLNode->prevPtr = nLNode;
-	} else {
-	    nLNode->nextPtr = nLNode->prevPtr = NULL;
-	}
-	list->firstPtr = list->lastPtr = nLNode;
-    } else {
-	nLNode->prevPtr = lNode;
-	nLNode->nextPtr = lNode->nextPtr;
-
-	lNode->nextPtr = nLNode;
-	if (nLNode->nextPtr != NULL) {
-	    nLNode->nextPtr->prevPtr = nLNode;
-	}
-
-	if (lNode == list->lastPtr) {
-	    list->lastPtr = nLNode;
-	}
-    }
-
-    return SUCCESS;
-}
diff --git a/lst.lib/lstAtEnd.c b/lst.lib/lstAtEnd.c
deleted file mode 100644
index 4eadfdb0e7f3..000000000000
--- a/lst.lib/lstAtEnd.c
+++ /dev/null
@@ -1,79 +0,0 @@
-/*	$NetBSD: lstAtEnd.c,v 1.14 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstAtEnd.c,v 1.14 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstAtEnd.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstAtEnd.c,v 1.14 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstAtEnd.c --
- *	Add a node at the end of the list
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_AtEnd --
- *	Add a node to the end of the given list
- *
- * Input:
- *	l		List to which to add the datum
- *	d		Datum to add
- *
- * Results:
- *	SUCCESS if life is good.
- *
- * Side Effects:
- *	A new ListNode is created and added to the list.
- *
- *-----------------------------------------------------------------------
- */
-ReturnStatus
-Lst_AtEnd(Lst l, void *d)
-{
-    LstNode	end;
-
-    end = Lst_Last(l);
-    return Lst_InsertAfter(l, end, d);
-}
diff --git a/lst.lib/lstAtFront.c b/lst.lib/lstAtFront.c
deleted file mode 100644
index 724713c092ed..000000000000
--- a/lst.lib/lstAtFront.c
+++ /dev/null
@@ -1,76 +0,0 @@
-/*	$NetBSD: lstAtFront.c,v 1.14 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstAtFront.c,v 1.14 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstAtFront.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstAtFront.c,v 1.14 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstAtFront.c --
- *	Add a node at the front of the list
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_AtFront --
- *	Place a piece of data at the front of a list
- *
- * Results:
- *	SUCCESS or FAILURE
- *
- * Side Effects:
- *	A new ListNode is created and stuck at the front of the list.
- *	hence, firstPtr (and possible lastPtr) in the list are altered.
- *
- *-----------------------------------------------------------------------
- */
-ReturnStatus
-Lst_AtFront(Lst l, void *d)
-{
-    LstNode	front;
-
-    front = Lst_First(l);
-    return Lst_InsertBefore(l, front, d);
-}
diff --git a/lst.lib/lstClose.c b/lst.lib/lstClose.c
deleted file mode 100644
index a1a3e9da80de..000000000000
--- a/lst.lib/lstClose.c
+++ /dev/null
@@ -1,85 +0,0 @@
-/*	$NetBSD: lstClose.c,v 1.12 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstClose.c,v 1.12 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstClose.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstClose.c,v 1.12 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstClose.c --
- *	Close a list for sequential access.
- *	The sequential functions access the list in a slightly different way.
- *	CurPtr points to their idea of the current node in the list and they
- *	access the list based on it. Because the list is circular, Lst_Next
- *	and Lst_Prev will go around the list forever. Lst_IsAtEnd must be
- *	used to determine when to stop.
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Close --
- *	Close a list which was opened for sequential access.
- *
- * Input:
- *	l		The list to close
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	The list is closed.
- *
- *-----------------------------------------------------------------------
- */
-void
-Lst_Close(Lst l)
-{
-    List 	list = l;
-
-    if (LstValid(l) == TRUE) {
-	list->isOpen = FALSE;
-	list->atEnd = Unknown;
-    }
-}
diff --git a/lst.lib/lstConcat.c b/lst.lib/lstConcat.c
deleted file mode 100644
index 2f667c5b0119..000000000000
--- a/lst.lib/lstConcat.c
+++ /dev/null
@@ -1,184 +0,0 @@
-/*	$NetBSD: lstConcat.c,v 1.17 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstConcat.c,v 1.17 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstConcat.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstConcat.c,v 1.17 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * listConcat.c --
- *	Function to concatentate two lists.
- */
-
-#include    "lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Concat --
- *	Concatenate two lists. New elements are created to hold the data
- *	elements, if specified, but the elements themselves are not copied.
- *	If the elements should be duplicated to avoid confusion with another
- *	list, the Lst_Duplicate function should be called first.
- *	If LST_CONCLINK is specified, the second list is destroyed since
- *	its pointers have been corrupted and the list is no longer useable.
- *
- * Input:
- *	l1		The list to which l2 is to be appended
- *	l2		The list to append to l1
- *	flags		LST_CONCNEW if LstNode's should be duplicated
- *			LST_CONCLINK if should just be relinked
- *
- * Results:
- *	SUCCESS if all went well. FAILURE otherwise.
- *
- * Side Effects:
- *	New elements are created and appended the first list.
- *-----------------------------------------------------------------------
- */
-ReturnStatus
-Lst_Concat(Lst l1, Lst l2, int flags)
-{
-    ListNode  	ln;     /* original LstNode */
-    ListNode  	nln;    /* new LstNode */
-    ListNode  	last;   /* the last element in the list. Keeps
-				 * bookkeeping until the end */
-    List 	list1 = l1;
-    List 	list2 = l2;
-
-    if (!LstValid (l1) || !LstValid (l2)) {
-	return FAILURE;
-    }
-
-    if (flags == LST_CONCLINK) {
-	if (list2->firstPtr != NULL) {
-	    /*
-	     * We set the nextPtr of the
-	     * last element of list two to be NIL to make the loop easier and
-	     * so we don't need an extra case should the first list turn
-	     * out to be non-circular -- the final element will already point
-	     * to NIL space and the first element will be untouched if it
-	     * existed before and will also point to NIL space if it didn't.
-	     */
-	    list2->lastPtr->nextPtr = NULL;
-	    /*
-	     * So long as the second list isn't empty, we just link the
-	     * first element of the second list to the last element of the
-	     * first list. If the first list isn't empty, we then link the
-	     * last element of the list to the first element of the second list
-	     * The last element of the second list, if it exists, then becomes
-	     * the last element of the first list.
-	     */
-	    list2->firstPtr->prevPtr = list1->lastPtr;
-	    if (list1->lastPtr != NULL) {
- 		list1->lastPtr->nextPtr = list2->firstPtr;
-	    } else {
-		list1->firstPtr = list2->firstPtr;
-	    }
-	    list1->lastPtr = list2->lastPtr;
-	}
-	if (list1->isCirc && list1->firstPtr != NULL) {
-	    /*
-	     * If the first list is supposed to be circular and it is (now)
-	     * non-empty, we must make sure it's circular by linking the
-	     * first element to the last and vice versa
-	     */
-	    list1->firstPtr->prevPtr = list1->lastPtr;
-	    list1->lastPtr->nextPtr = list1->firstPtr;
-	}
-	free(l2);
-    } else if (list2->firstPtr != NULL) {
-	/*
-	 * We set the nextPtr of the last element of list 2 to be nil to make
-	 * the loop less difficult. The loop simply goes through the entire
-	 * second list creating new LstNodes and filling in the nextPtr, and
-	 * prevPtr to fit into l1 and its datum field from the
-	 * datum field of the corresponding element in l2. The 'last' node
-	 * follows the last of the new nodes along until the entire l2 has
-	 * been appended. Only then does the bookkeeping catch up with the
-	 * changes. During the first iteration of the loop, if 'last' is nil,
-	 * the first list must have been empty so the newly-created node is
-	 * made the first node of the list.
-	 */
-	list2->lastPtr->nextPtr = NULL;
-	for (last = list1->lastPtr, ln = list2->firstPtr;
-	     ln != NULL;
-	     ln = ln->nextPtr)
-	{
-	    PAlloc (nln, ListNode);
-	    nln->datum = ln->datum;
-	    if (last != NULL) {
-		last->nextPtr = nln;
-	    } else {
-		list1->firstPtr = nln;
-	    }
-	    nln->prevPtr = last;
-	    nln->flags = nln->useCount = 0;
-	    last = nln;
-	}
-
-	/*
-	 * Finish bookkeeping. The last new element becomes the last element
-	 * of list one.
-	 */
-	list1->lastPtr = last;
-
-	/*
-	 * The circularity of both list one and list two must be corrected
-	 * for -- list one because of the new nodes added to it; list two
-	 * because of the alteration of list2->lastPtr's nextPtr to ease the
-	 * above for loop.
-	 */
-	if (list1->isCirc) {
-	    list1->lastPtr->nextPtr = list1->firstPtr;
-	    list1->firstPtr->prevPtr = list1->lastPtr;
-	} else {
-	    last->nextPtr = NULL;
-	}
-
-	if (list2->isCirc) {
-	    list2->lastPtr->nextPtr = list2->firstPtr;
-	}
-    }
-
-    return SUCCESS;
-}
diff --git a/lst.lib/lstDatum.c b/lst.lib/lstDatum.c
deleted file mode 100644
index c8ccb558f3dc..000000000000
--- a/lst.lib/lstDatum.c
+++ /dev/null
@@ -1,76 +0,0 @@
-/*	$NetBSD: lstDatum.c,v 1.14 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstDatum.c,v 1.14 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstDatum.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstDatum.c,v 1.14 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstDatum.c --
- *	Return the datum associated with a list node.
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Datum --
- *	Return the datum stored in the given node.
- *
- * Results:
- *	The datum or NULL if the node is invalid.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-void *
-Lst_Datum(LstNode ln)
-{
-    if (ln != NULL) {
-	return ln->datum;
-    } else {
-	return NULL;
-    }
-}
diff --git a/lst.lib/lstDeQueue.c b/lst.lib/lstDeQueue.c
deleted file mode 100644
index 0f1452e4ac7e..000000000000
--- a/lst.lib/lstDeQueue.c
+++ /dev/null
@@ -1,86 +0,0 @@
-/*	$NetBSD: lstDeQueue.c,v 1.15 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstDeQueue.c,v 1.15 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstDeQueue.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstDeQueue.c,v 1.15 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstDeQueue.c --
- *	Remove the node and return its datum from the head of the list
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_DeQueue --
- *	Remove and return the datum at the head of the given list.
- *
- * Results:
- *	The datum in the node at the head or NULL if the list
- *	is empty.
- *
- * Side Effects:
- *	The head node is removed from the list.
- *
- *-----------------------------------------------------------------------
- */
-void *
-Lst_DeQueue(Lst l)
-{
-    void *rd;
-    ListNode	tln;
-
-    tln = Lst_First(l);
-    if (tln == NULL) {
-	return NULL;
-    }
-
-    rd = tln->datum;
-    if (Lst_Remove(l, tln) == FAILURE) {
-	return NULL;
-    } else {
-	return rd;
-    }
-}
diff --git a/lst.lib/lstDestroy.c b/lst.lib/lstDestroy.c
deleted file mode 100644
index 92c5b2b2050c..000000000000
--- a/lst.lib/lstDestroy.c
+++ /dev/null
@@ -1,101 +0,0 @@
-/*	$NetBSD: lstDestroy.c,v 1.16 2008/12/13 15:19:29 dsl Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstDestroy.c,v 1.16 2008/12/13 15:19:29 dsl Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstDestroy.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstDestroy.c,v 1.16 2008/12/13 15:19:29 dsl Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstDestroy.c --
- *	Nuke a list and all its resources
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Destroy --
- *	Destroy a list and free all its resources. If the freeProc is
- *	given, it is called with the datum from each node in turn before
- *	the node is freed.
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	The given list is freed in its entirety.
- *
- *-----------------------------------------------------------------------
- */
-void
-Lst_Destroy(Lst list, FreeProc *freeProc)
-{
-    ListNode	ln;
-    ListNode	tln = NULL;
-
-    if (list == NULL)
-	return;
-
-    /* To ease scanning */
-    if (list->lastPtr != NULL)
-	list->lastPtr->nextPtr = NULL;
-    else {
-	free(list);
-	return;
-    }
-
-    if (freeProc) {
-	for (ln = list->firstPtr; ln != NULL; ln = tln) {
-	     tln = ln->nextPtr;
-	     freeProc(ln->datum);
-	     free(ln);
-	}
-    } else {
-	for (ln = list->firstPtr; ln != NULL; ln = tln) {
-	     tln = ln->nextPtr;
-	     free(ln);
-	}
-    }
-
-    free(list);
-}
diff --git a/lst.lib/lstDupl.c b/lst.lib/lstDupl.c
deleted file mode 100644
index 6318ee4e462a..000000000000
--- a/lst.lib/lstDupl.c
+++ /dev/null
@@ -1,107 +0,0 @@
-/*	$NetBSD: lstDupl.c,v 1.17 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstDupl.c,v 1.17 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstDupl.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstDupl.c,v 1.17 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * listDupl.c --
- *	Duplicate a list. This includes duplicating the individual
- *	elements.
- */
-
-#include    "lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Duplicate --
- *	Duplicate an entire list. If a function to copy a void *is
- *	given, the individual client elements will be duplicated as well.
- *
- * Input:
- *	l		the list to duplicate
- *	copyProc	A function to duplicate each void *
- *
- * Results:
- *	The new Lst structure or NULL if failure.
- *
- * Side Effects:
- *	A new list is created.
- *-----------------------------------------------------------------------
- */
-Lst
-Lst_Duplicate(Lst l, DuplicateProc *copyProc)
-{
-    Lst 	nl;
-    ListNode  	ln;
-    List 	list = l;
-
-    if (!LstValid (l)) {
-	return NULL;
-    }
-
-    nl = Lst_Init(list->isCirc);
-    if (nl == NULL) {
-	return NULL;
-    }
-
-    ln = list->firstPtr;
-    while (ln != NULL) {
-	if (copyProc != NULL) {
-	    if (Lst_AtEnd(nl, copyProc(ln->datum)) == FAILURE) {
-		return NULL;
-	    }
-	} else if (Lst_AtEnd(nl, ln->datum) == FAILURE) {
-	    return NULL;
-	}
-
-	if (list->isCirc && ln == list->lastPtr) {
-	    ln = NULL;
-	} else {
-	    ln = ln->nextPtr;
-	}
-    }
-
-    return nl;
-}
diff --git a/lst.lib/lstEnQueue.c b/lst.lib/lstEnQueue.c
deleted file mode 100644
index c6941a8eb9d3..000000000000
--- a/lst.lib/lstEnQueue.c
+++ /dev/null
@@ -1,77 +0,0 @@
-/*	$NetBSD: lstEnQueue.c,v 1.14 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstEnQueue.c,v 1.14 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstEnQueue.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstEnQueue.c,v 1.14 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstEnQueue.c--
- *	Treat the list as a queue and place a datum at its end
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_EnQueue --
- *	Add the datum to the tail of the given list.
- *
- * Results:
- *	SUCCESS or FAILURE as returned by Lst_InsertAfter.
- *
- * Side Effects:
- *	the lastPtr field is altered all the time and the firstPtr field
- *	will be altered if the list used to be empty.
- *
- *-----------------------------------------------------------------------
- */
-ReturnStatus
-Lst_EnQueue(Lst l, void *d)
-{
-    if (LstValid (l) == FALSE) {
-	return FAILURE;
-    }
-
-    return Lst_InsertAfter(l, Lst_Last(l), d);
-}
diff --git a/lst.lib/lstFind.c b/lst.lib/lstFind.c
deleted file mode 100644
index a1d27d3ad686..000000000000
--- a/lst.lib/lstFind.c
+++ /dev/null
@@ -1,73 +0,0 @@
-/*	$NetBSD: lstFind.c,v 1.16 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstFind.c,v 1.16 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstFind.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstFind.c,v 1.16 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstFind.c --
- *	Find a node on a list.
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Find --
- *	Find a node on the given list using the given comparison function
- *	and the given datum.
- *
- * Results:
- *	The found node or NULL if none matches.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-LstNode
-Lst_Find(Lst l, const void *d, int (*cProc)(const void *, const void *))
-{
-    return Lst_FindFrom(l, Lst_First(l), d, cProc);
-}
diff --git a/lst.lib/lstFindFrom.c b/lst.lib/lstFindFrom.c
deleted file mode 100644
index 676c07392039..000000000000
--- a/lst.lib/lstFindFrom.c
+++ /dev/null
@@ -1,89 +0,0 @@
-/*	$NetBSD: lstFindFrom.c,v 1.16 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstFindFrom.c,v 1.16 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstFindFrom.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstFindFrom.c,v 1.16 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstFindFrom.c --
- *	Find a node on a list from a given starting point. Used by Lst_Find.
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_FindFrom --
- *	Search for a node starting and ending with the given one on the
- *	given list using the passed datum and comparison function to
- *	determine when it has been found.
- *
- * Results:
- *	The found node or NULL
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-LstNode
-Lst_FindFrom(Lst l, LstNode ln, const void *d,
-	     int (*cProc)(const void *, const void *))
-{
-    ListNode	tln;
-
-    if (!LstValid (l) || LstIsEmpty (l) || !LstNodeValid (ln, l)) {
-	return NULL;
-    }
-
-    tln = ln;
-
-    do {
-	if ((*cProc)(tln->datum, d) == 0)
-	    return tln;
-	tln = tln->nextPtr;
-    } while (tln != ln && tln != NULL);
-
-    return NULL;
-}
diff --git a/lst.lib/lstFirst.c b/lst.lib/lstFirst.c
deleted file mode 100644
index a79db57120dd..000000000000
--- a/lst.lib/lstFirst.c
+++ /dev/null
@@ -1,76 +0,0 @@
-/*	$NetBSD: lstFirst.c,v 1.13 2020/07/03 08:37:56 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstFirst.c,v 1.13 2020/07/03 08:37:56 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstFirst.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstFirst.c,v 1.13 2020/07/03 08:37:56 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstFirst.c --
- *	Return the first node of a list
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_First --
- *	Return the first node on the given list.
- *
- * Results:
- *	The first node or NULL if the list is empty.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-LstNode
-Lst_First(Lst l)
-{
-    if (!LstValid (l) || LstIsEmpty (l)) {
-	return NULL;
-    } else {
-	return l->firstPtr;
-    }
-}
diff --git a/lst.lib/lstForEach.c b/lst.lib/lstForEach.c
deleted file mode 100644
index dc2fdd8bfc57..000000000000
--- a/lst.lib/lstForEach.c
+++ /dev/null
@@ -1,75 +0,0 @@
-/*	$NetBSD: lstForEach.c,v 1.14 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstForEach.c,v 1.14 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstForEach.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstForEach.c,v 1.14 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstForeach.c --
- *	Perform a given function on all elements of a list.
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_ForEach --
- *	Apply the given function to each element of the given list. The
- *	function should return 0 if Lst_ForEach should continue and non-
- *	zero if it should abort.
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	Only those created by the passed-in function.
- *
- *-----------------------------------------------------------------------
- */
-/*VARARGS2*/
-int
-Lst_ForEach(Lst l, int (*proc)(void *, void *), void *d)
-{
-    return Lst_ForEachFrom(l, Lst_First(l), proc, d);
-}
diff --git a/lst.lib/lstForEachFrom.c b/lst.lib/lstForEachFrom.c
deleted file mode 100644
index a08ddf35935b..000000000000
--- a/lst.lib/lstForEachFrom.c
+++ /dev/null
@@ -1,124 +0,0 @@
-/*	$NetBSD: lstForEachFrom.c,v 1.18 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstForEachFrom.c,v 1.18 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstForEachFrom.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstForEachFrom.c,v 1.18 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * lstForEachFrom.c --
- *	Perform a given function on all elements of a list starting from
- *	a given point.
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_ForEachFrom --
- *	Apply the given function to each element of the given list. The
- *	function should return 0 if traversal should continue and non-
- *	zero if it should abort.
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	Only those created by the passed-in function.
- *
- *-----------------------------------------------------------------------
- */
-/*VARARGS2*/
-int
-Lst_ForEachFrom(Lst l, LstNode ln, int (*proc)(void *, void *),
-		void *d)
-{
-    ListNode	tln = ln;
-    List 	list = l;
-    ListNode	next;
-    Boolean 	    	done;
-    int     	    	result;
-
-    if (!LstValid (list) || LstIsEmpty (list)) {
-	return 0;
-    }
-
-    do {
-	/*
-	 * Take care of having the current element deleted out from under
-	 * us.
-	 */
-
-	next = tln->nextPtr;
-
-	/*
-	 * We're done with the traversal if
-	 *  - the next node to examine is the first in the queue or
-	 *    doesn't exist and
-	 *  - nothing's been added after the current node (check this
-	 *    after proc() has been called).
-	 */
-	done = (next == NULL || next == list->firstPtr);
-
-	(void) tln->useCount++;
-	result = (*proc) (tln->datum, d);
-	(void) tln->useCount--;
-
-	/*
-	 * Now check whether a node has been added.
-	 * Note: this doesn't work if this node was deleted before
-	 *       the new node was added.
-	 */
-	if (next != tln->nextPtr) {
-		next = tln->nextPtr;
-		done = 0;
-	}
-
-	if (tln->flags & LN_DELETED) {
-	    free((char *)tln);
-	}
-	tln = next;
-    } while (!result && !LstIsEmpty(list) && !done);
-
-    return result;
-}
diff --git a/lst.lib/lstInit.c b/lst.lib/lstInit.c
deleted file mode 100644
index 3255da7e59a1..000000000000
--- a/lst.lib/lstInit.c
+++ /dev/null
@@ -1,85 +0,0 @@
-/*	$NetBSD: lstInit.c,v 1.13 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstInit.c,v 1.13 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstInit.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstInit.c,v 1.13 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * init.c --
- *	Initialize a new linked list.
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Init --
- *	Create and initialize a new list.
- *
- * Input:
- *	circ		TRUE if the list should be made circular
- *
- * Results:
- *	The created list.
- *
- * Side Effects:
- *	A list is created, what else?
- *
- *-----------------------------------------------------------------------
- */
-Lst
-Lst_Init(Boolean circ)
-{
-    List	nList;
-
-    PAlloc (nList, List);
-
-    nList->firstPtr = NULL;
-    nList->lastPtr = NULL;
-    nList->isOpen = FALSE;
-    nList->isCirc = circ;
-    nList->atEnd = Unknown;
-
-    return nList;
-}
diff --git a/lst.lib/lstInsert.c b/lst.lib/lstInsert.c
deleted file mode 100644
index 845b8899e03b..000000000000
--- a/lst.lib/lstInsert.c
+++ /dev/null
@@ -1,121 +0,0 @@
-/*	$NetBSD: lstInsert.c,v 1.15 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstInsert.c,v 1.15 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstInsert.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstInsert.c,v 1.15 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstInsert.c --
- *	Insert a new datum before an old one
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_InsertBefore --
- *	Insert a new node with the given piece of data before the given
- *	node in the given list.
- *
- * Input:
- *	l		list to manipulate
- *	ln		node before which to insert d
- *	d		datum to be inserted
- *
- * Results:
- *	SUCCESS or FAILURE.
- *
- * Side Effects:
- *	the firstPtr field will be changed if ln is the first node in the
- *	list.
- *
- *-----------------------------------------------------------------------
- */
-ReturnStatus
-Lst_InsertBefore(Lst l, LstNode ln, void *d)
-{
-    ListNode	nLNode;	/* new lnode for d */
-    ListNode	lNode = ln;
-    List 	list = l;
-
-
-    /*
-     * check validity of arguments
-     */
-    if (LstValid (l) && (LstIsEmpty (l) && ln == NULL))
-	goto ok;
-
-    if (!LstValid (l) || LstIsEmpty (l) || !LstNodeValid (ln, l)) {
-	return FAILURE;
-    }
-
-    ok:
-    PAlloc (nLNode, ListNode);
-
-    nLNode->datum = d;
-    nLNode->useCount = nLNode->flags = 0;
-
-    if (ln == NULL) {
-	if (list->isCirc) {
-	    nLNode->prevPtr = nLNode->nextPtr = nLNode;
-	} else {
-	    nLNode->prevPtr = nLNode->nextPtr = NULL;
-	}
-	list->firstPtr = list->lastPtr = nLNode;
-    } else {
-	nLNode->prevPtr = lNode->prevPtr;
-	nLNode->nextPtr = lNode;
-
-	if (nLNode->prevPtr != NULL) {
-	    nLNode->prevPtr->nextPtr = nLNode;
-	}
-	lNode->prevPtr = nLNode;
-
-	if (lNode == list->firstPtr) {
-	    list->firstPtr = nLNode;
-	}
-    }
-
-    return SUCCESS;
-}
diff --git a/lst.lib/lstInt.h b/lst.lib/lstInt.h
deleted file mode 100644
index ac53dcb67d77..000000000000
--- a/lst.lib/lstInt.h
+++ /dev/null
@@ -1,105 +0,0 @@
-/*	$NetBSD: lstInt.h,v 1.22 2014/09/07 20:55:34 joerg Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- *
- *	from: @(#)lstInt.h	8.1 (Berkeley) 6/6/93
- */
-
-/*-
- * lstInt.h --
- *	Internals for the list library
- */
-#ifndef _LSTINT_H_
-#define _LSTINT_H_
-
-#include	  "../lst.h"
-#include	  "../make_malloc.h"
-
-typedef struct ListNode {
-	struct ListNode	*prevPtr;   /* previous element in list */
-	struct ListNode	*nextPtr;   /* next in list */
-	unsigned int	useCount:8, /* Count of functions using the node.
-				     * node may not be deleted until count
-				     * goes to 0 */
- 	    	    	flags:8;    /* Node status flags */
-	void		*datum;	    /* datum associated with this element */
-} *ListNode;
-/*
- * Flags required for synchronization
- */
-#define LN_DELETED  	0x0001      /* List node should be removed when done */
-
-typedef enum {
-    Head, Middle, Tail, Unknown
-} Where;
-
-typedef struct	List {
-	ListNode  	firstPtr; /* first node in list */
-	ListNode  	lastPtr;  /* last node in list */
-	Boolean	  	isCirc;	  /* true if the list should be considered
-				   * circular */
-/*
- * fields for sequential access
- */
-	Where	  	atEnd;	  /* Where in the list the last access was */
-	Boolean	  	isOpen;	  /* true if list has been Lst_Open'ed */
-	ListNode  	curPtr;	  /* current node, if open. NULL if
-				   * *just* opened */
-	ListNode  	prevPtr;  /* Previous node, if open. Used by
-				   * Lst_Remove */
-} *List;
-
-/*
- * PAlloc (var, ptype) --
- *	Allocate a pointer-typedef structure 'ptype' into the variable 'var'
- */
-#define	PAlloc(var,ptype)	var = (ptype) bmake_malloc(sizeof *(var))
-
-/*
- * LstValid (l) --
- *	Return TRUE if the list l is valid
- */
-#define LstValid(l)	((Lst)(l) != NULL)
-
-/*
- * LstNodeValid (ln, l) --
- *	Return TRUE if the LstNode ln is valid with respect to l
- */
-#define LstNodeValid(ln, l)	((ln) != NULL)
-
-/*
- * LstIsEmpty (l) --
- *	TRUE if the list l is empty.
- */
-#define LstIsEmpty(l)	(((List)(l))->firstPtr == NULL)
-
-#endif /* _LSTINT_H_ */
diff --git a/lst.lib/lstIsAtEnd.c b/lst.lib/lstIsAtEnd.c
deleted file mode 100644
index c5add4d9867f..000000000000
--- a/lst.lib/lstIsAtEnd.c
+++ /dev/null
@@ -1,86 +0,0 @@
-/*	$NetBSD: lstIsAtEnd.c,v 1.14 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstIsAtEnd.c,v 1.14 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstIsAtEnd.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstIsAtEnd.c,v 1.14 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstIsAtEnd.c --
- *	Tell if the current node is at the end of the list.
- *	The sequential functions access the list in a slightly different way.
- *	CurPtr points to their idea of the current node in the list and they
- *	access the list based on it. Because the list is circular, Lst_Next
- *	and Lst_Prev will go around the list forever. Lst_IsAtEnd must be
- *	used to determine when to stop.
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_IsAtEnd --
- *	Return true if have reached the end of the given list.
- *
- * Results:
- *	TRUE if at the end of the list (this includes the list not being
- *	open or being invalid) or FALSE if not. We return TRUE if the list
- *	is invalid or unopend so as to cause the caller to exit its loop
- *	asap, the assumption being that the loop is of the form
- *	    while (!Lst_IsAtEnd (l)) {
- *	    	  ...
- *	    }
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-Boolean
-Lst_IsAtEnd(Lst l)
-{
-    List list = l;
-
-    return !LstValid (l) || !list->isOpen ||
-	   list->atEnd == Head || list->atEnd == Tail;
-}
diff --git a/lst.lib/lstIsEmpty.c b/lst.lib/lstIsEmpty.c
deleted file mode 100644
index ccf4525a3506..000000000000
--- a/lst.lib/lstIsEmpty.c
+++ /dev/null
@@ -1,74 +0,0 @@
-/*	$NetBSD: lstIsEmpty.c,v 1.12 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstIsEmpty.c,v 1.12 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstIsEmpty.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstIsEmpty.c,v 1.12 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstIsEmpty.c --
- *	A single function to decide if a list is empty
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_IsEmpty --
- *	Return TRUE if the given list is empty.
- *
- * Results:
- *	TRUE if the list is empty, FALSE otherwise.
- *
- * Side Effects:
- *	None.
- *
- *	A list is considered empty if its firstPtr == NULL (or if
- *	the list itself is NULL).
- *-----------------------------------------------------------------------
- */
-Boolean
-Lst_IsEmpty(Lst l)
-{
-    return !LstValid(l) || LstIsEmpty(l);
-}
diff --git a/lst.lib/lstLast.c b/lst.lib/lstLast.c
deleted file mode 100644
index 1d65bf19473e..000000000000
--- a/lst.lib/lstLast.c
+++ /dev/null
@@ -1,76 +0,0 @@
-/*	$NetBSD: lstLast.c,v 1.13 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstLast.c,v 1.13 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstLast.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstLast.c,v 1.13 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstLast.c --
- *	Return the last element of a list
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Last --
- *	Return the last node on the list l.
- *
- * Results:
- *	The requested node or NULL if the list is empty.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-LstNode
-Lst_Last(Lst l)
-{
-    if (!LstValid(l) || LstIsEmpty (l)) {
-	return NULL;
-    } else {
-	return l->lastPtr;
-    }
-}
diff --git a/lst.lib/lstMember.c b/lst.lib/lstMember.c
deleted file mode 100644
index e9046aca1436..000000000000
--- a/lst.lib/lstMember.c
+++ /dev/null
@@ -1,77 +0,0 @@
-/*	$NetBSD: lstMember.c,v 1.14 2013/11/14 00:01:28 sjg Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstMember.c,v 1.14 2013/11/14 00:01:28 sjg Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstMember.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstMember.c,v 1.14 2013/11/14 00:01:28 sjg Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * lstMember.c --
- *	See if a given datum is on a given list.
- */
-
-#include    "lstInt.h"
-
-LstNode
-Lst_Member(Lst l, void *d)
-{
-    List    	  	list = l;
-    ListNode	lNode;
-
-    if (list == NULL) {
-	return NULL;
-    }
-    lNode = list->firstPtr;
-    if (lNode == NULL) {
-	return NULL;
-    }
-
-    do {
-	if (lNode->datum == d) {
-	    return lNode;
-	}
-	lNode = lNode->nextPtr;
-    } while (lNode != NULL && lNode != list->firstPtr);
-
-    return NULL;
-}
diff --git a/lst.lib/lstNext.c b/lst.lib/lstNext.c
deleted file mode 100644
index 9c180d2cfad1..000000000000
--- a/lst.lib/lstNext.c
+++ /dev/null
@@ -1,119 +0,0 @@
-/*	$NetBSD: lstNext.c,v 1.13 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstNext.c,v 1.13 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstNext.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstNext.c,v 1.13 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstNext.c --
- *	Return the next node for a list.
- *	The sequential functions access the list in a slightly different way.
- *	CurPtr points to their idea of the current node in the list and they
- *	access the list based on it. Because the list is circular, Lst_Next
- *	and Lst_Prev will go around the list forever. Lst_IsAtEnd must be
- *	used to determine when to stop.
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Next --
- *	Return the next node for the given list.
- *
- * Results:
- *	The next node or NULL if the list has yet to be opened. Also
- *	if the list is non-circular and the end has been reached, NULL
- *	is returned.
- *
- * Side Effects:
- *	the curPtr field is updated.
- *
- *-----------------------------------------------------------------------
- */
-LstNode
-Lst_Next(Lst l)
-{
-    ListNode	tln;
-    List 	list = l;
-
-    if ((LstValid (l) == FALSE) ||
-	(list->isOpen == FALSE)) {
-	    return NULL;
-    }
-
-    list->prevPtr = list->curPtr;
-
-    if (list->curPtr == NULL) {
-	if (list->atEnd == Unknown) {
-	    /*
-	     * If we're just starting out, atEnd will be Unknown.
-	     * Then we want to start this thing off in the right
-	     * direction -- at the start with atEnd being Middle.
-	     */
-	    list->curPtr = tln = list->firstPtr;
-	    list->atEnd = Middle;
-	} else {
-	    tln = NULL;
-	    list->atEnd = Tail;
-	}
-    } else {
-	tln = list->curPtr->nextPtr;
-	list->curPtr = tln;
-
-	if (tln == list->firstPtr || tln == NULL) {
-	    /*
-	     * If back at the front, then we've hit the end...
-	     */
-	    list->atEnd = Tail;
-	} else {
-	    /*
-	     * Reset to Middle if gone past first.
-	     */
-	    list->atEnd = Middle;
-	}
-    }
-
-    return tln;
-}
diff --git a/lst.lib/lstOpen.c b/lst.lib/lstOpen.c
deleted file mode 100644
index 919dd6d5000c..000000000000
--- a/lst.lib/lstOpen.c
+++ /dev/null
@@ -1,86 +0,0 @@
-/*	$NetBSD: lstOpen.c,v 1.13 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstOpen.c,v 1.13 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstOpen.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstOpen.c,v 1.13 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstOpen.c --
- *	Open a list for sequential access. The sequential functions access the
- *	list in a slightly different way. CurPtr points to their idea of the
- *	current node in the list and they access the list based on it.
- *	If the list is circular, Lst_Next and Lst_Prev will go around
- *	the list forever. Lst_IsAtEnd must be used to determine when to stop.
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Open --
- *	Open a list for sequential access. A list can still be searched,
- *	etc., without confusing these functions.
- *
- * Results:
- *	SUCCESS or FAILURE.
- *
- * Side Effects:
- *	isOpen is set TRUE and curPtr is set to NULL so the
- *	other sequential functions no it was just opened and can choose
- *	the first element accessed based on this.
- *
- *-----------------------------------------------------------------------
- */
-ReturnStatus
-Lst_Open(Lst l)
-{
-	if (LstValid (l) == FALSE) {
-		return FAILURE;
-	}
-	(l)->isOpen = TRUE;
-	(l)->atEnd = LstIsEmpty (l) ? Head : Unknown;
-	(l)->curPtr = NULL;
-
-	return SUCCESS;
-}
diff --git a/lst.lib/lstPrev.c b/lst.lib/lstPrev.c
deleted file mode 100644
index b6c548d9a523..000000000000
--- a/lst.lib/lstPrev.c
+++ /dev/null
@@ -1,78 +0,0 @@
-/*	$NetBSD: lstPrev.c,v 1.4 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstPrev.c,v 1.4 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstSucc.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstPrev.c,v 1.4 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstPrev.c --
- *	return the predecessor to a given node
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Prev --
- *	Return the predecessor to the given node on its list.
- *
- * Results:
- *	The predecessor of the node, if it exists (note that on a circular
- *	list, if the node is the only one in the list, it is its own
- *	predecessor).
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-LstNode
-Lst_Prev(LstNode ln)
-{
-    if (ln == NULL) {
-	return NULL;
-    } else {
-	return ln->prevPtr;
-    }
-}
diff --git a/lst.lib/lstRemove.c b/lst.lib/lstRemove.c
deleted file mode 100644
index 59245499bdc4..000000000000
--- a/lst.lib/lstRemove.c
+++ /dev/null
@@ -1,134 +0,0 @@
-/*	$NetBSD: lstRemove.c,v 1.17 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstRemove.c,v 1.17 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstRemove.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstRemove.c,v 1.17 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstRemove.c --
- *	Remove an element from a list
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Remove --
- *	Remove the given node from the given list.
- *
- * Results:
- *	SUCCESS or FAILURE.
- *
- * Side Effects:
- *	The list's firstPtr will be set to NULL if ln is the last
- *	node on the list. firsPtr and lastPtr will be altered if ln is
- *	either the first or last node, respectively, on the list.
- *
- *-----------------------------------------------------------------------
- */
-ReturnStatus
-Lst_Remove(Lst l, LstNode ln)
-{
-    List 	list = l;
-    ListNode	lNode = ln;
-
-    if (!LstValid (l) || !LstNodeValid (ln, l)) {
-	    return FAILURE;
-    }
-
-    /*
-     * unlink it from the list
-     */
-    if (lNode->nextPtr != NULL) {
-	lNode->nextPtr->prevPtr = lNode->prevPtr;
-    }
-    if (lNode->prevPtr != NULL) {
-	lNode->prevPtr->nextPtr = lNode->nextPtr;
-    }
-
-    /*
-     * if either the firstPtr or lastPtr of the list point to this node,
-     * adjust them accordingly
-     */
-    if (list->firstPtr == lNode) {
-	list->firstPtr = lNode->nextPtr;
-    }
-    if (list->lastPtr == lNode) {
-	list->lastPtr = lNode->prevPtr;
-    }
-
-    /*
-     * Sequential access stuff. If the node we're removing is the current
-     * node in the list, reset the current node to the previous one. If the
-     * previous one was non-existent (prevPtr == NULL), we set the
-     * end to be Unknown, since it is.
-     */
-    if (list->isOpen && (list->curPtr == lNode)) {
-	list->curPtr = list->prevPtr;
-	if (list->curPtr == NULL) {
-	    list->atEnd = Unknown;
-	}
-    }
-
-    /*
-     * the only way firstPtr can still point to ln is if ln is the last
-     * node on the list (the list is circular, so lNode->nextptr == lNode in
-     * this case). The list is, therefore, empty and is marked as such
-     */
-    if (list->firstPtr == lNode) {
-	list->firstPtr = NULL;
-    }
-
-    /*
-     * note that the datum is unmolested. The caller must free it as
-     * necessary and as expected.
-     */
-    if (lNode->useCount == 0) {
-	free(ln);
-    } else {
-	lNode->flags |= LN_DELETED;
-    }
-
-    return SUCCESS;
-}
diff --git a/lst.lib/lstReplace.c b/lst.lib/lstReplace.c
deleted file mode 100644
index f30cb00855e3..000000000000
--- a/lst.lib/lstReplace.c
+++ /dev/null
@@ -1,77 +0,0 @@
-/*	$NetBSD: lstReplace.c,v 1.14 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstReplace.c,v 1.14 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstReplace.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstReplace.c,v 1.14 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstReplace.c --
- *	Replace the datum in a node with a new datum
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Replace --
- *	Replace the datum in the given node with the new datum
- *
- * Results:
- *	SUCCESS or FAILURE.
- *
- * Side Effects:
- *	The datum field fo the node is altered.
- *
- *-----------------------------------------------------------------------
- */
-ReturnStatus
-Lst_Replace(LstNode ln, void *d)
-{
-    if (ln == NULL) {
-	return FAILURE;
-    } else {
-	(ln)->datum = d;
-	return SUCCESS;
-    }
-}
diff --git a/lst.lib/lstSucc.c b/lst.lib/lstSucc.c
deleted file mode 100644
index b3f73bb15fd2..000000000000
--- a/lst.lib/lstSucc.c
+++ /dev/null
@@ -1,78 +0,0 @@
-/*	$NetBSD: lstSucc.c,v 1.14 2020/07/03 08:37:57 rillig Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- */
-
-#ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: lstSucc.c,v 1.14 2020/07/03 08:37:57 rillig Exp $";
-#else
-#include <sys/cdefs.h>
-#ifndef lint
-#if 0
-static char sccsid[] = "@(#)lstSucc.c	8.1 (Berkeley) 6/6/93";
-#else
-__RCSID("$NetBSD: lstSucc.c,v 1.14 2020/07/03 08:37:57 rillig Exp $");
-#endif
-#endif /* not lint */
-#endif
-
-/*-
- * LstSucc.c --
- *	return the successor to a given node
- */
-
-#include	"lstInt.h"
-
-/*-
- *-----------------------------------------------------------------------
- * Lst_Succ --
- *	Return the successor to the given node on its list.
- *
- * Results:
- *	The successor of the node, if it exists (note that on a circular
- *	list, if the node is the only one in the list, it is its own
- *	successor).
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-LstNode
-Lst_Succ(LstNode ln)
-{
-    if (ln == NULL) {
-	return NULL;
-    } else {
-	return ln->nextPtr;
-    }
-}
diff --git a/main.c b/main.c
index 25141625e55c..f27320c57451 100644
--- a/main.c
+++ b/main.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: main.c,v 1.279 2020/07/03 08:13:23 rillig Exp $	*/
+/*	$NetBSD: main.c,v 1.331 2020/08/30 19:56:02 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -69,7 +69,7 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: main.c,v 1.279 2020/07/03 08:13:23 rillig Exp $";
+static char rcsid[] = "$NetBSD: main.c,v 1.331 2020/08/30 19:56:02 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
@@ -81,7 +81,7 @@ __COPYRIGHT("@(#) Copyright (c) 1988, 1989, 1990, 1993\
 #if 0
 static char sccsid[] = "@(#)main.c	8.3 (Berkeley) 3/19/94";
 #else
-__RCSID("$NetBSD: main.c,v 1.279 2020/07/03 08:13:23 rillig Exp $");
+__RCSID("$NetBSD: main.c,v 1.331 2020/08/30 19:56:02 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -124,13 +124,13 @@ __RCSID("$NetBSD: main.c,v 1.279 2020/07/03 08:13:23 rillig Exp $");
 #include <sys/utsname.h>
 #include "wait.h"
 
+#include <ctype.h>
 #include <errno.h>
 #include <signal.h>
 #include <stdarg.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <time.h>
-#include <ctype.h>
 
 #include "make.h"
 #include "hash.h"
@@ -162,7 +162,8 @@ static Lst		makefiles;	/* ordered list of makefiles to read */
 static int		printVars;	/* -[vV] argument */
 #define COMPAT_VARS 1
 #define EXPAND_VARS 2
-static Lst		variables;	/* list of variables to print */
+static Lst		variables;	/* list of variables to print
+					 * (for -v and -V) */
 int			maxJobs;	/* -j argument */
 static int		maxJobTokens;	/* -j argument */
 Boolean			compatMake;	/* -B argument */
@@ -180,14 +181,13 @@ Boolean			beSilent;	/* -s flag */
 Boolean			oldVars;	/* variable substitution style */
 Boolean			checkEnvFirst;	/* -e flag */
 Boolean			parseWarnFatal;	/* -W flag */
-Boolean			jobServer; 	/* -J flag */
 static int jp_0 = -1, jp_1 = -1;	/* ends of parent job pipe */
 Boolean			varNoExportEnv;	/* -X flag */
 Boolean			doing_depend;	/* Set while reading .depend */
 static Boolean		jobsRunning;	/* TRUE if the jobs might be running */
 static const char *	tracefile;
 static void		MainParseArgs(int, char **);
-static int		ReadMakefile(const void *, const void *);
+static int		ReadMakefile(const char *);
 static void		usage(void) MAKE_ATTR_DEAD;
 static void		purge_cached_realpaths(void);
 
@@ -257,7 +257,7 @@ parse_debug_options(const char *argvalue)
 	for (modules = argvalue; *modules; ++modules) {
 		switch (*modules) {
 		case 'A':
-			debug = ~0;
+			debug = ~(0|DEBUG_LINT);
 			break;
 		case 'a':
 			debug |= DEBUG_ARCH;
@@ -291,9 +291,15 @@ parse_debug_options(const char *argvalue)
 				++modules;
 			}
 			break;
+		case 'h':
+			debug |= DEBUG_HASH;
+			break;
 		case 'j':
 			debug |= DEBUG_JOB;
 			break;
+		case 'L':
+			debug |= DEBUG_LINT;
+			break;
 		case 'l':
 			debug |= DEBUG_LOUD;
 			break;
@@ -375,7 +381,7 @@ debug_setbuf:
 /*
  * does path contain any relative components
  */
-static int
+static Boolean
 is_relpath(const char *path)
 {
 	const char *cp;
@@ -383,10 +389,7 @@ is_relpath(const char *path)
 	if (path[0] != '/')
 		return TRUE;
 	cp = path;
-	do {
-		cp = strstr(cp, "/.");
-		if (!cp)
-			break;
+	while ((cp = strstr(cp, "/.")) != NULL) {
 		cp += 2;
 		if (cp[0] == '/' || cp[0] == '\0')
 			return TRUE;
@@ -394,7 +397,7 @@ is_relpath(const char *path)
 			if (cp[1] == '/' || cp[1] == '\0')
 				return TRUE;
 		}
-	} while (cp);
+	}
 	return FALSE;
 }
 
@@ -416,7 +419,7 @@ static void
 MainParseArgs(int argc, char **argv)
 {
 	char *p;
-	int c = '?';
+	char c = '?';
 	int arginc;
 	char *argvalue;
 	const char *getopt_def;
@@ -531,7 +534,6 @@ rearg:
 			} else {
 			    Var_Append(MAKEFLAGS, "-J", VAR_GLOBAL);
 			    Var_Append(MAKEFLAGS, argvalue, VAR_GLOBAL);
-			    jobServer = TRUE;
 			}
 			break;
 		case 'N':
@@ -553,7 +555,7 @@ rearg:
 		case 'v':
 			if (argvalue == NULL) goto noarg;
 			printVars = c == 'v' ? EXPAND_VARS : COMPAT_VARS;
-			(void)Lst_AtEnd(variables, argvalue);
+			Lst_Append(variables, argvalue);
 			Var_Append(MAKEFLAGS, "-V", VAR_GLOBAL);
 			Var_Append(MAKEFLAGS, argvalue, VAR_GLOBAL);
 			break;
@@ -581,7 +583,7 @@ rearg:
 			break;
 		case 'f':
 			if (argvalue == NULL) goto noarg;
-			(void)Lst_AtEnd(makefiles, argvalue);
+			Lst_Append(makefiles, argvalue);
 			break;
 		case 'i':
 			ignoreErrors = TRUE;
@@ -674,7 +676,7 @@ rearg:
 				Punt("illegal (null) argument.");
 			if (*argv[1] == '-' && !dashDash)
 				goto rearg;
-			(void)Lst_AtEnd(create, bmake_strdup(argv[1]));
+			Lst_Append(create, bmake_strdup(argv[1]));
 		}
 
 	return;
@@ -705,12 +707,10 @@ noarg:
 void
 Main_ParseArgLine(const char *line)
 {
-	char **argv;			/* Manufactured argument vector */
-	int argc;			/* Number of arguments in argv */
-	char *args;			/* Space used by the args */
-	char *buf, *p1;
-	char *argv0 = Var_Value(".MAKE", VAR_GLOBAL, &p1);
-	size_t len;
+	Words words;
+	char *p1;
+	const char *argv0 = Var_Value(".MAKE", VAR_GLOBAL, &p1);
+	char *buf;
 
 	if (line == NULL)
 		return;
@@ -733,21 +733,19 @@ Main_ParseArgLine(const char *line)
 			return;
 	}
 #endif
-	buf = bmake_malloc(len = strlen(line) + strlen(argv0) + 2);
-	(void)snprintf(buf, len, "%s %s", argv0, line);
+	buf = str_concat3(argv0, " ", line);
 	free(p1);
 
-	argv = brk_string(buf, &argc, TRUE, &args);
-	if (argv == NULL) {
+	words = Str_Words(buf, TRUE);
+	if (words.words == NULL) {
 		Error("Unterminated quoted string [%s]", buf);
 		free(buf);
 		return;
 	}
 	free(buf);
-	MainParseArgs(argc, argv);
+	MainParseArgs((int)words.len, words.words);
 
-	free(args);
-	free(argv);
+	Words_Free(words);
 }
 
 Boolean
@@ -775,7 +773,7 @@ Main_SetObjdir(const char *fmt, ...)
 			(void)fprintf(stderr, "make warning: %s: %s.\n",
 				      path, strerror(errno));
 		} else {
-			strncpy(objdir, path, MAXPATHLEN);
+			snprintf(objdir, sizeof objdir, "%s", path);
 			Var_Set(".OBJDIR", objdir, VAR_GLOBAL);
 			setenv("PWD", objdir, 1);
 			Dir_InitDot();
@@ -792,37 +790,44 @@ Main_SetObjdir(const char *fmt, ...)
 static Boolean
 Main_SetVarObjdir(const char *var, const char *suffix)
 {
-	char *p, *path, *xpath;
+	char *path_freeIt;
+	const char *path = Var_Value(var, VAR_CMD, &path_freeIt);
+	const char *xpath;
+	char *xpath_freeIt;
 
-	if ((path = Var_Value(var, VAR_CMD, &p)) == NULL ||
-	    *path == '\0')
+	if (path == NULL || path[0] == '\0') {
+		bmake_free(path_freeIt);
 		return FALSE;
+	}
 
 	/* expand variable substitutions */
+	xpath = path;
+	xpath_freeIt = NULL;
 	if (strchr(path, '$') != 0)
-		xpath = Var_Subst(NULL, path, VAR_GLOBAL, VARF_WANTRES);
-	else
-		xpath = path;
+		xpath = xpath_freeIt = Var_Subst(path, VAR_GLOBAL,
+						 VARE_WANTRES);
 
 	(void)Main_SetObjdir("%s%s", xpath, suffix);
 
-	if (xpath != path)
-		free(xpath);
-	free(p);
+	bmake_free(xpath_freeIt);
+	bmake_free(path_freeIt);
 	return TRUE;
 }
 
-/*-
- * ReadAllMakefiles --
- *	wrapper around ReadMakefile() to read all.
- *
- * Results:
- *	TRUE if ok, FALSE on error
- */
-static int
-ReadAllMakefiles(const void *p, const void *q)
+/* Read and parse the makefile.
+ * Return TRUE if reading the makefile succeeded, for Lst_Find. */
+static Boolean
+ReadMakefileSucceeded(const void *fname, const void *unused)
 {
-	return ReadMakefile(p, q) == 0;
+	return ReadMakefile(fname) == 0;
+}
+
+/* Read and parse the makefile.
+ * Return TRUE if reading the makefile failed, for Lst_Find. */
+static Boolean
+ReadMakefileFailed(const void *fname, const void *unused)
+{
+	return ReadMakefile(fname) != 0;
 }
 
 int
@@ -835,7 +840,7 @@ str2Lst_Append(Lst lp, char *str, const char *sep)
 	sep = " \t";
 
     for (n = 0, cp = strtok(str, sep); cp; cp = strtok(NULL, sep)) {
-	(void)Lst_AtEnd(lp, cp);
+	Lst_Append(lp, cp);
 	n++;
     }
     return n;
@@ -863,13 +868,13 @@ siginfo(int signo MAKE_ATTR_UNUSED)
 void
 MakeMode(const char *mode)
 {
-    char *mp = NULL;
+    char *mode_freeIt = NULL;
 
-    if (!mode)
-	mode = mp = Var_Subst(NULL, "${" MAKE_MODE ":tl}",
-			      VAR_GLOBAL, VARF_WANTRES);
+    if (mode == NULL)
+	mode = mode_freeIt = Var_Subst("${" MAKE_MODE ":tl}",
+				       VAR_GLOBAL, VARE_WANTRES);
 
-    if (mode && *mode) {
+    if (mode[0] != '\0') {
 	if (strstr(mode, "compat")) {
 	    compatMake = TRUE;
 	    forceJobs = FALSE;
@@ -880,7 +885,7 @@ MakeMode(const char *mode)
 #endif
     }
 
-    free(mp);
+    free(mode_freeIt);
 }
 
 static void
@@ -896,15 +901,13 @@ doPrintVars(void)
 	else
 		expandVars = getBoolean(".MAKE.EXPAND_VARIABLES", FALSE);
 
-	for (ln = Lst_First(variables); ln != NULL;
-	    ln = Lst_Succ(ln)) {
-		char *var = (char *)Lst_Datum(ln);
-		char *value;
+	for (ln = Lst_First(variables); ln != NULL; ln = LstNode_Next(ln)) {
+		char *var = LstNode_Datum(ln);
+		const char *value;
 		char *p1;
 
 		if (strchr(var, '$')) {
-			value = p1 = Var_Subst(NULL, var, VAR_GLOBAL,
-			    VARF_WANTRES);
+			value = p1 = Var_Subst(var, VAR_GLOBAL, VARE_WANTRES);
 		} else if (expandVars) {
 			char tmp[128];
 			int len = snprintf(tmp, sizeof(tmp), "${%s}", var);
@@ -912,13 +915,12 @@ doPrintVars(void)
 			if (len >= (int)sizeof(tmp))
 				Fatal("%s: variable name too big: %s",
 				    progname, var);
-			value = p1 = Var_Subst(NULL, tmp, VAR_GLOBAL,
-			    VARF_WANTRES);
+			value = p1 = Var_Subst(tmp, VAR_GLOBAL, VARE_WANTRES);
 		} else {
 			value = Var_Value(var, VAR_GLOBAL, &p1);
 		}
 		printf("%s\n", value ? value : "");
-		free(p1);
+		bmake_free(p1);
 	}
 }
 
@@ -962,7 +964,7 @@ runTargets(void)
 		Compat_Run(targs);
 		outOfDate = FALSE;
 	}
-	Lst_Destroy(targs, NULL);
+	Lst_Free(targs);
 	return outOfDate;
 }
 
@@ -1079,7 +1081,7 @@ main(int argc, char **argv)
 #else
 #ifndef MACHINE_ARCH
 #ifdef MAKE_MACHINE_ARCH
-            machine_arch = MAKE_MACHINE_ARCH;
+	    machine_arch = MAKE_MACHINE_ARCH;
 #else
 	    machine_arch = "unknown";
 #endif
@@ -1113,11 +1115,11 @@ main(int argc, char **argv)
 		VAR_GLOBAL);
 	Var_Set(MAKE_DEPENDFILE, ".depend", VAR_GLOBAL);
 
-	create = Lst_Init(FALSE);
-	makefiles = Lst_Init(FALSE);
+	create = Lst_Init();
+	makefiles = Lst_Init();
 	printVars = 0;
 	debugVflag = FALSE;
-	variables = Lst_Init(FALSE);
+	variables = Lst_Init();
 	beSilent = FALSE;		/* Print commands as executed */
 	ignoreErrors = FALSE;		/* Pay attention to non-zero returns */
 	noExecute = FALSE;		/* Execute all commands */
@@ -1199,7 +1201,7 @@ main(int argc, char **argv)
 #ifdef USE_META
 	meta_init();
 #endif
-	Dir_Init(NULL);		/* Dir_* safe to call from MainParseArgs */
+	Dir_Init();
 
 	/*
 	 * First snag any flags out of the MAKE environment variable.
@@ -1265,8 +1267,8 @@ main(int argc, char **argv)
 					(void)strncpy(curdir, pwd, MAXPATHLEN);
 			}
 		}
-		free(ptmp1);
-		free(ptmp2);
+		bmake_free(ptmp1);
+		bmake_free(ptmp2);
 	}
 #endif
 	Var_Set(".CURDIR", curdir, VAR_GLOBAL);
@@ -1280,7 +1282,7 @@ main(int argc, char **argv)
 	 * and * finally _PATH_OBJDIRPREFIX`pwd`, in that order.  If none
 	 * of these paths exist, just use .CURDIR.
 	 */
-	Dir_Init(curdir);
+	Dir_InitDir(curdir);
 	(void)Main_SetObjdir("%s", curdir);
 
 	if (!Main_SetVarObjdir("MAKEOBJDIRPREFIX", curdir) &&
@@ -1312,10 +1314,8 @@ main(int argc, char **argv)
 	if (!Lst_IsEmpty(create)) {
 		LstNode ln;
 
-		for (ln = Lst_First(create); ln != NULL;
-		    ln = Lst_Succ(ln)) {
-			char *name = (char *)Lst_Datum(ln);
-
+		for (ln = Lst_First(create); ln != NULL; ln = LstNode_Next(ln)) {
+			char *name = LstNode_Datum(ln);
 			Var_Append(".TARGETS", name, VAR_GLOBAL);
 		}
 	} else
@@ -1327,7 +1327,8 @@ main(int argc, char **argv)
 	 * add the directories from the DEFSYSPATH (more than one may be given
 	 * as dir1:...:dirn) to the system include path.
 	 */
-	if (syspath == NULL || *syspath == '\0')
+	/* XXX: mismatch: the -m option sets sysIncPath, not syspath */
+	if (syspath == NULL || syspath[0] == '\0')
 		syspath = defsyspath;
 	else
 		syspath = bmake_strdup(syspath);
@@ -1353,48 +1354,46 @@ main(int argc, char **argv)
 
 	/*
 	 * Read in the built-in rules first, followed by the specified
-	 * makefile, if it was (makefile != NULL), or the default
-	 * makefile and Makefile, in that order, if it wasn't.
+	 * makefiles, or the default makefile and Makefile, in that order,
+	 * if no makefiles were given on the command line.
 	 */
 	if (!noBuiltins) {
 		LstNode ln;
 
-		sysMkPath = Lst_Init(FALSE);
+		sysMkPath = Lst_Init();
 		Dir_Expand(_PATH_DEFSYSMK,
 			   Lst_IsEmpty(sysIncPath) ? defIncPath : sysIncPath,
 			   sysMkPath);
 		if (Lst_IsEmpty(sysMkPath))
 			Fatal("%s: no system rules (%s).", progname,
 			    _PATH_DEFSYSMK);
-		ln = Lst_Find(sysMkPath, NULL, ReadMakefile);
+		ln = Lst_Find(sysMkPath, ReadMakefileSucceeded, NULL);
 		if (ln == NULL)
 			Fatal("%s: cannot open %s.", progname,
-			    (char *)Lst_Datum(ln));
+			    (char *)LstNode_Datum(Lst_First(sysMkPath)));
 	}
 
 	if (!Lst_IsEmpty(makefiles)) {
 		LstNode ln;
 
-		ln = Lst_Find(makefiles, NULL, ReadAllMakefiles);
+		ln = Lst_Find(makefiles, ReadMakefileFailed, NULL);
 		if (ln != NULL)
 			Fatal("%s: cannot open %s.", progname,
-			    (char *)Lst_Datum(ln));
+			    (char *)LstNode_Datum(ln));
 	} else {
-	    p1 = Var_Subst(NULL, "${" MAKEFILE_PREFERENCE "}",
-		VAR_CMD, VARF_WANTRES);
-	    if (p1) {
+		p1 = Var_Subst("${" MAKEFILE_PREFERENCE "}",
+			       VAR_CMD, VARE_WANTRES);
 		(void)str2Lst_Append(makefiles, p1, NULL);
-		(void)Lst_Find(makefiles, NULL, ReadMakefile);
+		(void)Lst_Find(makefiles, ReadMakefileSucceeded, NULL);
 		free(p1);
-	    }
 	}
 
 	/* In particular suppress .depend for '-r -V .OBJDIR -f /dev/null' */
 	if (!noBuiltins || !printVars) {
-	    makeDependfile = Var_Subst(NULL, "${.MAKE.DEPENDFILE:T}",
-		VAR_CMD, VARF_WANTRES);
+	    makeDependfile = Var_Subst("${.MAKE.DEPENDFILE:T}",
+		VAR_CMD, VARE_WANTRES);
 	    doing_depend = TRUE;
-	    (void)ReadMakefile(makeDependfile, NULL);
+	    (void)ReadMakefile(makeDependfile);
 	    doing_depend = FALSE;
 	}
 
@@ -1404,14 +1403,14 @@ main(int argc, char **argv)
 	MakeMode(NULL);
 
 	Var_Append("MFLAGS", Var_Value(MAKEFLAGS, VAR_GLOBAL, &p1), VAR_GLOBAL);
-	free(p1);
+	bmake_free(p1);
 
 	if (!forceJobs && !compatMake &&
 	    Var_Exists(".MAKE.JOBS", VAR_GLOBAL)) {
 	    char *value;
 	    int n;
 
-	    value = Var_Subst(NULL, "${.MAKE.JOBS}", VAR_GLOBAL, VARF_WANTRES);
+	    value = Var_Subst("${.MAKE.JOBS}", VAR_GLOBAL, VARE_WANTRES);
 	    n = strtol(value, NULL, 0);
 	    if (n < 1) {
 		(void)fprintf(stderr, "%s: illegal value for .MAKE.JOBS -- must be positive integer!\n",
@@ -1439,8 +1438,9 @@ main(int argc, char **argv)
 	if (!compatMake)
 	    Job_ServerStart(maxJobTokens, jp_0, jp_1);
 	if (DEBUG(JOB))
-	    fprintf(debug_file, "job_pipe %d %d, maxjobs %d, tokens %d, compat %d\n",
-		jp_0, jp_1, maxJobs, maxJobTokens, compatMake);
+	    fprintf(debug_file,
+		    "job_pipe %d %d, maxjobs %d, tokens %d, compat %d\n",
+		    jp_0, jp_1, maxJobs, maxJobTokens, compatMake ? 1 : 0);
 
 	if (!printVars)
 	    Main_ExportMAKEFLAGS(TRUE);	/* initial export */
@@ -1461,7 +1461,7 @@ main(int argc, char **argv)
 		 */
 		static char VPATH[] = "${VPATH}";
 
-		vpath = Var_Subst(NULL, VPATH, VAR_CMD, VARF_WANTRES);
+		vpath = Var_Subst(VPATH, VAR_CMD, VARE_WANTRES);
 		path = vpath;
 		do {
 			/* skip to end of directory */
@@ -1502,9 +1502,9 @@ main(int argc, char **argv)
 	}
 
 #ifdef CLEANUP
-	Lst_Destroy(variables, NULL);
-	Lst_Destroy(makefiles, NULL);
-	Lst_Destroy(create, (FreeProc *)free);
+	Lst_Free(variables);
+	Lst_Free(makefiles);
+	Lst_Destroy(create, free);
 #endif
 
 	/* print the graph now it's been processed if the user requested it */
@@ -1522,7 +1522,7 @@ main(int argc, char **argv)
 	meta_finish();
 #endif
 	Suff_End();
-        Targ_End();
+	Targ_End();
 	Arch_End();
 	Var_End();
 	Parse_End();
@@ -1533,23 +1533,16 @@ main(int argc, char **argv)
 	return outOfDate ? 1 : 0;
 }
 
-/*-
- * ReadMakefile  --
- *	Open and parse the given makefile.
+/* Open and parse the given makefile, with all its side effects.
  *
  * Results:
  *	0 if ok. -1 if couldn't open file.
- *
- * Side Effects:
- *	lots
  */
 static int
-ReadMakefile(const void *p, const void *q MAKE_ATTR_UNUSED)
+ReadMakefile(const char *fname)
 {
-	const char *fname = p;		/* makefile to read */
 	int fd;
-	size_t len = MAXPATHLEN;
-	char *name, *path = bmake_malloc(len);
+	char *name, *path = NULL;
 
 	if (!strcmp(fname, "-")) {
 		Parse_File(NULL /*stdin*/, -1);
@@ -1557,22 +1550,16 @@ ReadMakefile(const void *p, const void *q MAKE_ATTR_UNUSED)
 	} else {
 		/* if we've chdir'd, rebuild the path name */
 		if (strcmp(curdir, objdir) && *fname != '/') {
-			size_t plen = strlen(curdir) + strlen(fname) + 2;
-			if (len < plen)
-				path = bmake_realloc(path, len = 2 * plen);
-
-			(void)snprintf(path, len, "%s/%s", curdir, fname);
+			path = str_concat3(curdir, "/", fname);
 			fd = open(path, O_RDONLY);
 			if (fd != -1) {
 				fname = path;
 				goto found;
 			}
+			free(path);
 
 			/* If curdir failed, try objdir (ala .depend) */
-			plen = strlen(objdir) + strlen(fname) + 2;
-			if (len < plen)
-				path = bmake_realloc(path, len = 2 * plen);
-			(void)snprintf(path, len, "%s/%s", objdir, fname);
+			path = str_concat3(objdir, "/", fname);
 			fd = open(path, O_RDONLY);
 			if (fd != -1) {
 				fname = path;
@@ -1613,31 +1600,32 @@ found:
 /*-
  * Cmd_Exec --
  *	Execute the command in cmd, and return the output of that command
- *	in a string.
+ *	in a string.  In the output, newlines are replaced with spaces.
  *
  * Results:
- *	A string containing the output of the command, or the empty string
- *	If errnum is not NULL, it contains the reason for the command failure
+ *	A string containing the output of the command, or the empty string.
+ *	*errfmt returns a format string describing the command failure,
+ *	if any, using a single %s conversion specification.
  *
  * Side Effects:
  *	The string must be freed by the caller.
  */
 char *
-Cmd_Exec(const char *cmd, const char **errnum)
+Cmd_Exec(const char *cmd, const char **errfmt)
 {
     const char	*args[4];   	/* Args for invoking the shell */
     int 	fds[2];	    	/* Pipe streams */
     int 	cpid;	    	/* Child PID */
     int 	pid;	    	/* PID from wait() */
-    char	*res;		/* result */
     WAIT_T	status;		/* command exit status */
     Buffer	buf;		/* buffer to store the result */
+    ssize_t	bytes_read;
+    char	*res;		/* result */
+    size_t	res_len;
     char	*cp;
-    int		cc;		/* bytes read, or -1 */
     int		savederr;	/* saved errno */
 
-
-    *errnum = NULL;
+    *errfmt = NULL;
 
     if (!shellName)
 	Shell_Init();
@@ -1653,7 +1641,7 @@ Cmd_Exec(const char *cmd, const char **errnum)
      * Open a pipe for fetching its output
      */
     if (pipe(fds) == -1) {
-	*errnum = "Couldn't create pipe for \"%s\"";
+	*errfmt = "Couldn't create pipe for \"%s\"";
 	goto bad;
     }
 
@@ -1682,7 +1670,7 @@ Cmd_Exec(const char *cmd, const char **errnum)
 	/*NOTREACHED*/
 
     case -1:
-	*errnum = "Couldn't exec \"%s\"";
+	*errfmt = "Couldn't exec \"%s\"";
 	goto bad;
 
     default:
@@ -1696,12 +1684,12 @@ Cmd_Exec(const char *cmd, const char **errnum)
 
 	do {
 	    char   result[BUFSIZ];
-	    cc = read(fds[0], result, sizeof(result));
-	    if (cc > 0)
-		Buf_AddBytes(&buf, cc, result);
+	    bytes_read = read(fds[0], result, sizeof(result));
+	    if (bytes_read > 0)
+		Buf_AddBytes(&buf, result, (size_t)bytes_read);
 	}
-	while (cc > 0 || (cc == -1 && errno == EINTR));
-	if (cc == -1)
+	while (bytes_read > 0 || (bytes_read == -1 && errno == EINTR));
+	if (bytes_read == -1)
 	    savederr = errno;
 
 	/*
@@ -1716,43 +1704,28 @@ Cmd_Exec(const char *cmd, const char **errnum)
 	    JobReapChild(pid, status, FALSE);
 	    continue;
 	}
-	cc = Buf_Size(&buf);
+	res_len = Buf_Size(&buf);
 	res = Buf_Destroy(&buf, FALSE);
 
 	if (savederr != 0)
-	    *errnum = "Couldn't read shell's output for \"%s\"";
+	    *errfmt = "Couldn't read shell's output for \"%s\"";
 
 	if (WIFSIGNALED(status))
-	    *errnum = "\"%s\" exited on a signal";
+	    *errfmt = "\"%s\" exited on a signal";
 	else if (WEXITSTATUS(status) != 0)
-	    *errnum = "\"%s\" returned non-zero status";
+	    *errfmt = "\"%s\" returned non-zero status";
 
-	/*
-	 * Null-terminate the result, convert newlines to spaces and
-	 * install it in the variable.
-	 */
-	res[cc] = '\0';
-	cp = &res[cc];
-
-	if (cc > 0 && *--cp == '\n') {
-	    /*
-	     * A final newline is just stripped
-	     */
-	    *cp-- = '\0';
-	}
-	while (cp >= res) {
-	    if (*cp == '\n') {
+	/* Convert newlines to spaces.  A final newline is just stripped */
+	if (res_len > 0 && res[res_len - 1] == '\n')
+	    res[res_len - 1] = '\0';
+	for (cp = res; *cp != '\0'; cp++)
+	    if (*cp == '\n')
 		*cp = ' ';
-	    }
-	    cp--;
-	}
 	break;
     }
     return res;
 bad:
-    res = bmake_malloc(1);
-    *res = '\0';
-    return res;
+    return bmake_strdup("");
 }
 
 /*-
@@ -1888,7 +1861,7 @@ DieHorribly(void)
  */
 void
 Finish(int errors)
-	           	/* number of errors encountered in Make_Make */
+			/* number of errors encountered in Make_Make */
 {
 	if (dieQuietly(NULL, -1))
 		exit(2);
@@ -1956,13 +1929,13 @@ usage(void)
 {
 	char *p;
 	if ((p = strchr(progname, '[')) != NULL)
-	    *p = '\0';
+		*p = '\0';
 
 	(void)fprintf(stderr,
-"usage: %s [-BeikNnqrstWwX] \n\
-            [-C directory] [-D variable] [-d flags] [-f makefile]\n\
-            [-I directory] [-J private] [-j max_jobs] [-m directory] [-T file]\n\
-            [-V variable] [-v variable] [variable=value] [target ...]\n",
+"usage: %s [-BeikNnqrstWwX] \n"
+"            [-C directory] [-D variable] [-d flags] [-f makefile]\n"
+"            [-I directory] [-J private] [-j max_jobs] [-m directory] [-T file]\n"
+"            [-V variable] [-v variable] [variable=value] [target ...]\n",
 	    progname);
 	exit(2);
 }
@@ -2010,7 +1983,8 @@ char *
 cached_realpath(const char *pathname, char *resolved)
 {
     GNode *cache;
-    char *rp, *cp;
+    const char *rp;
+    char *cp;
 
     if (!pathname || !pathname[0])
 	return NULL;
@@ -2024,7 +1998,7 @@ cached_realpath(const char *pathname, char *resolved)
 	Var_Set(pathname, rp, cache);
     } /* else should we negative-cache? */
 
-    free(cp);
+    bmake_free(cp);
     return rp ? resolved : NULL;
 }
 
@@ -2070,9 +2044,14 @@ void
 PrintOnError(GNode *gn, const char *s)
 {
     static GNode *en = NULL;
-    char tmp[64];
+    const char *expr;
     char *cp;
 
+    if (DEBUG(HASH)) {
+	Targ_Stats();
+	Var_Stats();
+    }
+
     /* we generally want to keep quiet if a sub-make died */
     if (dieQuietly(gn, -1))
 	return;
@@ -2092,14 +2071,10 @@ PrintOnError(GNode *gn, const char *s)
 	Var_Delete(".ERROR_CMD", VAR_GLOBAL);
 	Lst_ForEach(gn->commands, addErrorCMD, gn);
     }
-    strncpy(tmp, "${MAKE_PRINT_VAR_ON_ERROR:@v@$v='${$v}'\n@}",
-	    sizeof(tmp) - 1);
-    cp = Var_Subst(NULL, tmp, VAR_GLOBAL, VARF_WANTRES);
-    if (cp) {
-	if (*cp)
-	    printf("%s", cp);
-	free(cp);
-    }
+    expr = "${MAKE_PRINT_VAR_ON_ERROR:@v@$v='${$v}'\n@}";
+    cp = Var_Subst(expr, VAR_GLOBAL, VARE_WANTRES);
+    printf("%s", cp);
+    free(cp);
     fflush(stdout);
 
     /*
@@ -2115,18 +2090,17 @@ PrintOnError(GNode *gn, const char *s)
 void
 Main_ExportMAKEFLAGS(Boolean first)
 {
-    static int once = 1;
-    char tmp[64];
+    static Boolean once = TRUE;
+    const char *expr;
     char *s;
 
     if (once != first)
 	return;
-    once = 0;
+    once = FALSE;
 
-    strncpy(tmp, "${.MAKEFLAGS} ${.MAKEOVERRIDES:O:u:@v@$v=${$v:Q}@}",
-	    sizeof(tmp));
-    s = Var_Subst(NULL, tmp, VAR_CMD, VARF_WANTRES);
-    if (s && *s) {
+    expr = "${.MAKEFLAGS} ${.MAKEOVERRIDES:O:u:@v@$v=${$v:Q}@}";
+    s = Var_Subst(expr, VAR_CMD, VARE_WANTRES);
+    if (s[0] != '\0') {
 #ifdef POSIX
 	setenv("MAKEFLAGS", s, 1);
 #else
@@ -2147,8 +2121,8 @@ getTmpdir(void)
 	 * Honor $TMPDIR but only if it is valid.
 	 * Ensure it ends with /.
 	 */
-	tmpdir = Var_Subst(NULL, "${TMPDIR:tA:U" _PATH_TMP "}/", VAR_GLOBAL,
-			   VARF_WANTRES);
+	tmpdir = Var_Subst("${TMPDIR:tA:U" _PATH_TMP "}/", VAR_GLOBAL,
+			   VARE_WANTRES);
 	if (stat(tmpdir, &st) < 0 || !S_ISDIR(st.st_mode)) {
 	    free(tmpdir);
 	    tmpdir = bmake_strdup(_PATH_TMP);
@@ -2235,18 +2209,12 @@ s2Boolean(const char *s, Boolean bf)
  * is FALSE, otherwise TRUE.
  */
 Boolean
-getBoolean(const char *name, Boolean bf)
+getBoolean(const char *name, Boolean fallback)
 {
-    char tmp[64];
-    char *cp;
-
-    if (snprintf(tmp, sizeof(tmp), "${%s:U:tl}", name) < (int)(sizeof(tmp))) {
-	cp = Var_Subst(NULL, tmp, VAR_GLOBAL, VARF_WANTRES);
-
-	if (cp) {
-	    bf = s2Boolean(cp, bf);
-	    free(cp);
-	}
-    }
-    return bf;
+    char *expr = str_concat3("${", name, ":U:tl}");
+    char *value = Var_Subst(expr, VAR_GLOBAL, VARE_WANTRES);
+    Boolean res = s2Boolean(value, fallback);
+    free(value);
+    free(expr);
+    return res;
 }
diff --git a/make-conf.h b/make-conf.h
index a85b86d3efb5..5b13e295ae0c 100644
--- a/make-conf.h
+++ b/make-conf.h
@@ -1,4 +1,4 @@
-/*	$NetBSD: config.h,v 1.21 2012/03/31 00:12:24 christos Exp $	*/
+/*	$NetBSD: config.h,v 1.22 2020/09/01 17:40:34 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990 The Regents of the University of California.
@@ -100,7 +100,10 @@
  * LIBSUFF
  *	Is the suffix used to denote libraries and is used by the Suff module
  *	to find the search path on which to seek any -l<xx> targets.
- *
+ */
+#define	LIBSUFF	".a"
+
+/*
  * RECHECK
  *	If defined, Make_Update will check a target for its current
  *	modification time after it has been re-made, setting it to the
@@ -108,10 +111,9 @@
  *	Unfortunately, under NFS the modification time often doesn't
  *	get updated in time, so a target will appear to not have been
  *	re-made, causing later targets to appear up-to-date. On systems
- *	that don't have this problem, you should defined this. Under
+ *	that don't have this problem, you should define this. Under
  *	NFS you probably should not, unless you aren't exporting jobs.
  */
-#define	LIBSUFF	".a"
 #define	RECHECK
 
 /*
diff --git a/make.1 b/make.1
index ae7e7033c077..9d83abe31d6c 100644
--- a/make.1
+++ b/make.1
@@ -1,4 +1,4 @@
-.\"	$NetBSD: make.1,v 1.282 2020/06/06 20:28:42 wiz Exp $
+.\"	$NetBSD: make.1,v 1.289 2020/08/28 17:15:04 rillig Exp $
 .\"
 .\" Copyright (c) 1990, 1993
 .\"	The Regents of the University of California.  All rights reserved.
@@ -29,7 +29,7 @@
 .\"
 .\"	from: @(#)make.1	8.4 (Berkeley) 3/19/94
 .\"
-.Dd June 5, 2020
+.Dd August 28, 2020
 .Dt MAKE 1
 .Os
 .Sh NAME
@@ -166,8 +166,15 @@ Print the input graph after making everything, or before exiting
 on error.
 .It Ar "g3"
 Print the input graph before exiting on error.
+.It Ar h
+Print debugging information about hash table operations.
 .It Ar j
 Print debugging information about running multiple shells.
+.It Ar L
+Turn on lint checks.
+This will throw errors for variable assignments that do not parse
+correctly, at the time of assignment so the file and line number
+are available.
 .It Ar l
 Print commands in Makefiles regardless of whether or not they are prefixed by
 .Ql @
@@ -311,7 +318,8 @@ as an argument).
 .It Fl n
 Display the commands that would have been executed, but do not
 actually execute them unless the target depends on the .MAKE special
-source (see below).
+source (see below) or the command is prefixed with
+.Ql Ic + .
 .It Fl N
 Display the commands which would have been executed, but do not
 actually execute any of them; useful for debugging top-level makefiles
@@ -412,37 +420,44 @@ or more sources.
 This creates a relationship where the targets
 .Dq depend
 on the sources
-and are usually created from them.
-The exact relationship between the target and the source is determined
-by the operator that separates them.
-The three operators are as follows:
+and are customarily created from them.
+A target is considered out-of-date if it does not exist, or if its
+modification time is less than that of any of its sources.
+An out-of-date target will be re-created, but not until all sources
+have been examined and themselves re-created as needed.
+Three operators may be used:
 .Bl -tag -width flag
 .It Ic \&:
-A target is considered out-of-date if its modification time is less than
-those of any of its sources.
-Sources for a target accumulate over dependency lines when this operator
-is used.
-The target is removed if
+Many dependency lines may name this target but only one may have
+attached shell commands.
+All sources named in all dependency lines are considered together,
+and if needed the attached shell commands are run to create or
+re-create the target.
+If
 .Nm
-is interrupted.
+is interrupted, the target is removed.
 .It Ic \&!
-Targets are always re-created, but not until all sources have been
-examined and re-created as necessary.
-Sources for a target accumulate over dependency lines when this operator
-is used.
-The target is removed if
-.Nm
-is interrupted.
+The same, but the target is always re-created whether or not it is out
+of date.
 .It Ic \&::
-If no sources are specified, the target is always re-created.
-Otherwise, a target is considered out-of-date if any of its sources has
-been modified more recently than the target.
-Sources for a target do not accumulate over dependency lines when this
-operator is used.
-The target will not be removed if
+Any dependency line may have attached shell commands, but each one
+is handled independently: its sources are considered and the attached
+shell commands are run if the target is out of date with respect to
+(only) those sources.
+Thus, different groups of the attached shell commands may be run
+depending on the circumstances.
+Furthermore, unlike
+.Ic \&:,
+for dependency lines with no sources, the attached shell
+commands are always run.
+Also unlike
+.Ic \&:,
+the target will not be removed if
 .Nm
 is interrupted.
 .El
+All dependency lines mentioning a particular target must use the same
+operator.
 .Pp
 Targets and sources may contain the shell wildcard values
 .Ql \&? ,
@@ -608,7 +623,7 @@ This shorter form is not recommended.
 .Pp
 If the variable name contains a dollar, then the name itself is expanded first.
 This allows almost arbitrary variable names, however names containing dollar,
-braces, parenthesis, or whitespace are really best avoided!
+braces, parentheses, or whitespace are really best avoided!
 .Pp
 If the result of expanding a variable contains a dollar sign
 .Pq Ql \&$
@@ -1126,6 +1141,9 @@ is set to the value of
 for all programs which
 .Nm
 executes.
+.It Ev .SHELL
+The pathname of the shell used to run target scripts.
+It is read-only.
 .It Ev .TARGETS
 The list of targets explicitly specified on the command line, if any.
 .It Ev VPATH
@@ -1171,7 +1189,7 @@ Replaces each word in the variable with its suffix.
 .It Cm \&:H
 Replaces each word in the variable with everything but the last component.
 .It Cm \&:M Ns Ar pattern
-Select only those words that match
+Selects only those words that match
 .Ar pattern .
 The standard shell wildcard characters
 .Pf ( Ql * ,
@@ -1195,11 +1213,11 @@ This is identical to
 but selects all words which do not match
 .Ar pattern .
 .It Cm \&:O
-Order every word in variable alphabetically.
+Orders every word in variable alphabetically.
 .It Cm \&:Or
-Order every word in variable in reverse alphabetical order.
+Orders every word in variable in reverse alphabetical order.
 .It Cm \&:Ox
-Randomize words in variable.
+Shuffles the words in variable.
 The results will be different each time you are referring to the
 modified variable; use the assignment with expansion
 .Pq Ql Cm \&:=
@@ -1249,7 +1267,7 @@ If a
 .Va utc
 value is not provided or is 0, the current time is used.
 .It Cm \&:hash
-Compute a 32-bit hash of the value and encode it as hex digits.
+Computes a 32-bit hash of the value and encode it as hex digits.
 .It Cm \&:localtime[=utc]
 The value is a format string for
 .Xr strftime 3 ,
@@ -1259,7 +1277,7 @@ If a
 .Va utc
 value is not provided or is 0, the current time is used.
 .It Cm \&:tA
-Attempt to convert variable to an absolute path using
+Attempts to convert variable to an absolute path using
 .Xr realpath 3 ,
 if that fails, the value is unchanged.
 .It Cm \&:tl
@@ -1271,7 +1289,7 @@ This modifier sets the separator to the character
 If
 .Ar c
 is omitted, then no separator is used.
-The common escapes (including octal numeric codes), work as expected.
+The common escapes (including octal numeric codes) work as expected.
 .It Cm \&:tu
 Converts variable to upper-case letters.
 .It Cm \&:tW
@@ -1287,21 +1305,21 @@ See also
 .Sm off
 .It Cm \&:S No \&/ Ar old_string No \&/ Ar new_string No \&/ Op Cm 1gW
 .Sm on
-Modify the first occurrence of
+Modifies the first occurrence of
 .Ar old_string
-in the variable's value, replacing it with
+in each word of the variable's value, replacing it with
 .Ar new_string .
 If a
 .Ql g
-is appended to the last slash of the pattern, all occurrences
+is appended to the last delimiter of the pattern, all occurrences
 in each word are replaced.
 If a
 .Ql 1
-is appended to the last slash of the pattern, only the first word
+is appended to the last delimiter of the pattern, only the first occurrence
 is affected.
 If a
 .Ql W
-is appended to the last slash of the pattern,
+is appended to the last delimiter of the pattern,
 then the value is treated as a single word
 (possibly containing embedded white space).
 If
@@ -1370,13 +1388,6 @@ as occur in the word or words it is found in; the
 .Ql W
 modifier causes the value to be treated as a single word
 (possibly containing embedded white space).
-Note that
-.Ql 1
-and
-.Ql g
-are orthogonal; the former specifies whether multiple words are
-potentially affected, the latter whether multiple substitutions can
-potentially occur within each affected word.
 .Pp
 As for the
 .Cm \&:S
@@ -1387,9 +1398,9 @@ and
 are subjected to variable expansion before being parsed as
 regular expressions.
 .It Cm \&:T
-Replaces each word in the variable with its last component.
+Replaces each word in the variable with its last path component.
 .It Cm \&:u
-Remove adjacent duplicate words (like
+Removes adjacent duplicate words (like
 .Xr uniq 1 ) .
 .Sm off
 .It Cm \&:\&? Ar true_string Cm \&: Ar false_string
@@ -1405,7 +1416,7 @@ usually contain variable expansions.
 A common error is trying to use expressions like
 .Dl ${NUMBERS:M42:?match:no}
 which actually tests defined(NUMBERS),
-to determine is any words match "42" you need to use something like:
+to determine if any words match "42" you need to use something like:
 .Dl ${"${NUMBERS:M42}" != \&"\&":?match:no} .
 .It Ar :old_string=new_string
 This is the
@@ -1449,7 +1460,7 @@ in either the
 or
 .Ar old_string ,
 only the first instance is treated specially (as the pattern character);
-all subsequent instances are treated as regular characters
+all subsequent instances are treated as regular characters.
 .Pp
 Variable expansion occurs in the normal fashion inside both
 .Ar old_string
@@ -1466,11 +1477,10 @@ This is the loop expansion mechanism from the OSF Development
 Environment (ODE) make.
 Unlike
 .Cm \&.for
-loops expansion occurs at the time of
-reference.
-Assign
+loops, expansion occurs at the time of reference.
+Assigns
 .Ar temp
-to each word in the variable and evaluate
+to each word in the variable and evaluates
 .Ar string .
 The ODE convention is that
 .Ar temp
@@ -1481,7 +1491,7 @@ For example.
 However a single character variable is often more readable:
 .Dl ${MAKE_PRINT_VAR_ON_ERROR:@v@$v='${$v}'${.newline}@}
 .It Cm \&:_[=var]
-Save the current variable value in
+Saves the current variable value in
 .Ql $_
 or the named
 .Va var
@@ -1502,7 +1512,7 @@ is used to save the result of the
 modifier which is later referenced using the index values from
 .Ql :range .
 .It Cm \&:U Ns Ar newval
-If the variable is undefined
+If the variable is undefined,
 .Ar newval
 is the value.
 If the variable is defined, the existing value is returned.
@@ -1512,7 +1522,7 @@ It is handy for setting per-target CFLAGS for instance:
 If a value is only required if the variable is undefined, use:
 .Dl ${VAR:D:Unewval}
 .It Cm \&:D Ns Ar newval
-If the variable is defined
+If the variable is defined,
 .Ar newval
 is the value.
 .It Cm \&:L
@@ -1641,7 +1651,7 @@ Returns the number of words in the value.
 .El \" :[range]
 .El
 .Sh INCLUDE STATEMENTS, CONDITIONALS AND FOR LOOPS
-Makefile inclusion, conditional structures and for loops  reminiscent
+Makefile inclusion, conditional structures and for loops reminiscent
 of the C programming language are provided in
 .Nm .
 All such structures are identified by a line beginning with a single
@@ -1687,7 +1697,7 @@ The possible conditionals are as follows:
 The message is printed along with the name of the makefile and line number,
 then
 .Nm
-will exit.
+will exit immediately.
 .It Ic .export Ar variable ...
 Export the specified global variable.
 If no variable list is provided, all globals are exported
@@ -1876,7 +1886,7 @@ operator is not an integral value, then
 string comparison is performed between the expanded
 variables.
 If no relational operator is given, it is assumed that the expanded
-variable is being compared against 0 or an empty string in the case
+variable is being compared against 0, or an empty string in the case
 of a string comparison.
 .Pp
 When
@@ -1917,7 +1927,7 @@ The syntax of a for loop is:
 .Pp
 .Bl -tag -compact -width Ds
 .It Ic \&.for Ar variable Oo Ar variable ... Oc Ic in Ar expression
-.It Aq make-rules
+.It Aq make-lines
 .It Ic \&.endfor
 .El
 .Pp
@@ -1929,7 +1939,7 @@ On each iteration of the loop, one word is taken and assigned to each
 in order, and these
 .Ic variables
 are substituted into the
-.Ic make-rules
+.Ic make-lines
 inside the body of the for loop.
 The number of words must come out even; that is, if there are three
 iteration variables, the number of words provided must be a multiple
diff --git a/make.c b/make.c
index 9472a045c26a..da2b8adf6efd 100644
--- a/make.c
+++ b/make.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: make.c,v 1.99 2020/07/03 08:13:23 rillig Exp $	*/
+/*	$NetBSD: make.c,v 1.133 2020/08/30 14:11:42 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -69,14 +69,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: make.c,v 1.99 2020/07/03 08:13:23 rillig Exp $";
+static char rcsid[] = "$NetBSD: make.c,v 1.133 2020/08/30 14:11:42 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)make.c	8.1 (Berkeley) 6/6/93";
 #else
-__RCSID("$NetBSD: make.c,v 1.99 2020/07/03 08:13:23 rillig Exp $");
+__RCSID("$NetBSD: make.c,v 1.133 2020/08/30 14:11:42 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -116,7 +116,7 @@ __RCSID("$NetBSD: make.c,v 1.99 2020/07/03 08:13:23 rillig Exp $");
  */
 
 #include    "make.h"
-#include    "hash.h"
+#include    "enum.h"
 #include    "dir.h"
 #include    "job.h"
 
@@ -151,6 +151,43 @@ make_abort(GNode *gn, int line)
     abort();
 }
 
+ENUM_VALUE_RTTI_8(GNodeMade,
+		  UNMADE, DEFERRED, REQUESTED, BEINGMADE,
+		  MADE, UPTODATE, ERROR, ABORTED);
+
+ENUM_FLAGS_RTTI_31(GNodeType,
+		   OP_DEPENDS, OP_FORCE, OP_DOUBLEDEP,
+		   /* OP_OPMASK is omitted since it combines other flags */
+		   OP_OPTIONAL, OP_USE, OP_EXEC, OP_IGNORE,
+		   OP_PRECIOUS, OP_SILENT, OP_MAKE, OP_JOIN,
+		   OP_MADE, OP_SPECIAL, OP_USEBEFORE, OP_INVISIBLE,
+		   OP_NOTMAIN, OP_PHONY, OP_NOPATH, OP_WAIT,
+		   OP_NOMETA, OP_META, OP_NOMETA_CMP, OP_SUBMAKE,
+		   OP_TRANSFORM, OP_MEMBER, OP_LIB, OP_ARCHV,
+		   OP_HAS_COMMANDS, OP_SAVE_CMDS, OP_DEPS_FOUND, OP_MARK);
+
+ENUM_FLAGS_RTTI_10(GNodeFlags,
+		   REMAKE, CHILDMADE, FORCE, DONE_WAIT,
+		   DONE_ORDER, FROM_DEPEND, DONE_ALLSRC, CYCLE,
+		   DONECYCLE, INTERNAL);
+
+void
+GNode_FprintDetails(FILE *f, const char *prefix, const GNode *gn,
+		    const char *suffix)
+{
+    char type_buf[GNodeType_ToStringSize];
+    char flags_buf[GNodeFlags_ToStringSize];
+
+    fprintf(f, "%smade %s, type %s, flags %s%s",
+	    prefix,
+	    Enum_ValueToString(gn->made, GNodeMade_ToStringSpecs),
+	    Enum_FlagsToString(type_buf, sizeof type_buf,
+			       gn->type, GNodeType_ToStringSpecs),
+	    Enum_FlagsToString(flags_buf, sizeof flags_buf,
+			       gn->flags, GNodeFlags_ToStringSpecs),
+	    suffix);
+}
+
 /*-
  *-----------------------------------------------------------------------
  * Make_TimeStamp --
@@ -189,7 +226,7 @@ MakeTimeStamp(void *pgn, void *cgn)
 {
     return Make_TimeStamp((GNode *)pgn, (GNode *)cgn);
 }
-
+
 /*-
  *-----------------------------------------------------------------------
  * Make_OODate --
@@ -350,7 +387,7 @@ Make_OODate(GNode *gn)
 
     return oodate;
 }
-
+
 /*-
  *-----------------------------------------------------------------------
  * MakeAddChild  --
@@ -378,11 +415,11 @@ MakeAddChild(void *gnp, void *lp)
 	if (DEBUG(MAKE))
 	    fprintf(debug_file, "MakeAddChild: need to examine %s%s\n",
 		gn->name, gn->cohort_num);
-	(void)Lst_EnQueue(l, gn);
+	Lst_Enqueue(l, gn);
     }
     return 0;
 }
-
+
 /*-
  *-----------------------------------------------------------------------
  * MakeFindChild  --
@@ -412,31 +449,18 @@ MakeFindChild(void *gnp, void *pgnp)
 
     return 0;
 }
-
-/*-
- *-----------------------------------------------------------------------
- * Make_HandleUse --
- *	Function called by Make_Run and SuffApplyTransform on the downward
- *	pass to handle .USE and transformation nodes. It implements the
- *	.USE and transformation functionality by copying the node's commands,
- *	type flags and children to the parent node.
+
+/* Called by Make_Run and SuffApplyTransform on the downward pass to handle
+ * .USE and transformation nodes, by copying the child node's commands, type
+ * flags and children to the parent node.
  *
- *	A .USE node is much like an explicit transformation rule, except
- *	its commands are always added to the target node, even if the
- *	target already has commands.
+ * A .USE node is much like an explicit transformation rule, except its
+ * commands are always added to the target node, even if the target already
+ * has commands.
  *
  * Input:
  *	cgn		The .USE node
  *	pgn		The target of the .USE node
- *
- * Results:
- *	none
- *
- * Side Effects:
- *	Children and commands may be added to the parent and the parent's
- *	type may be changed.
- *
- *-----------------------------------------------------------------------
  */
 void
 Make_HandleUse(GNode *cgn, GNode *pgn)
@@ -452,52 +476,42 @@ Make_HandleUse(GNode *cgn, GNode *pgn)
 
     if ((cgn->type & (OP_USE|OP_USEBEFORE)) || Lst_IsEmpty(pgn->commands)) {
 	    if (cgn->type & OP_USEBEFORE) {
-		/*
-		 * .USEBEFORE --
-		 *	prepend the child's commands to the parent.
-		 */
-		Lst cmds = pgn->commands;
-		pgn->commands = Lst_Duplicate(cgn->commands, NULL);
-		(void)Lst_Concat(pgn->commands, cmds, LST_CONCNEW);
-		Lst_Destroy(cmds, NULL);
+		/* .USEBEFORE */
+		Lst_PrependAll(pgn->commands, cgn->commands);
 	    } else {
-		/*
-		 * .USE or target has no commands --
-		 *	append the child's commands to the parent.
-		 */
-		(void)Lst_Concat(pgn->commands, cgn->commands, LST_CONCNEW);
+		/* .USE, or target has no commands */
+		Lst_AppendAll(pgn->commands, cgn->commands);
 	    }
     }
 
-    if (Lst_Open(cgn->children) == SUCCESS) {
-	while ((ln = Lst_Next(cgn->children)) != NULL) {
-	    GNode *tgn, *gn = (GNode *)Lst_Datum(ln);
+    Lst_Open(cgn->children);
+    while ((ln = Lst_Next(cgn->children)) != NULL) {
+	GNode *gn = LstNode_Datum(ln);
 
-	    /*
-	     * Expand variables in the .USE node's name
-	     * and save the unexpanded form.
-	     * We don't need to do this for commands.
-	     * They get expanded properly when we execute.
-	     */
-	    if (gn->uname == NULL) {
-		gn->uname = gn->name;
-	    } else {
-		free(gn->name);
-	    }
-	    gn->name = Var_Subst(NULL, gn->uname, pgn, VARF_WANTRES);
-	    if (gn->name && gn->uname && strcmp(gn->name, gn->uname) != 0) {
-		/* See if we have a target for this node. */
-		tgn = Targ_FindNode(gn->name, TARG_NOCREATE);
-		if (tgn != NULL)
-		    gn = tgn;
-	    }
-
-	    (void)Lst_AtEnd(pgn->children, gn);
-	    (void)Lst_AtEnd(gn->parents, pgn);
-	    pgn->unmade += 1;
+	/*
+	 * Expand variables in the .USE node's name
+	 * and save the unexpanded form.
+	 * We don't need to do this for commands.
+	 * They get expanded properly when we execute.
+	 */
+	if (gn->uname == NULL) {
+	    gn->uname = gn->name;
+	} else {
+	    free(gn->name);
 	}
-	Lst_Close(cgn->children);
+	gn->name = Var_Subst(gn->uname, pgn, VARE_WANTRES);
+	if (gn->uname && strcmp(gn->name, gn->uname) != 0) {
+	    /* See if we have a target for this node. */
+	    GNode *tgn = Targ_FindNode(gn->name, TARG_NOCREATE);
+	    if (tgn != NULL)
+		gn = tgn;
+	}
+
+	Lst_Append(pgn->children, gn);
+	Lst_Append(gn->parents, pgn);
+	pgn->unmade += 1;
     }
+    Lst_Close(cgn->children);
 
     pgn->type |= cgn->type & ~(OP_OPMASK|OP_USE|OP_USEBEFORE|OP_TRANSFORM);
 }
@@ -547,7 +561,7 @@ MakeHandleUse(void *cgnp, void *pgnp)
      * children the parent has. This is used by Make_Run to decide
      * whether to queue the parent or examine its children...
      */
-    if ((ln = Lst_Member(pgn->children, cgn)) != NULL) {
+    if ((ln = Lst_FindDatum(pgn->children, cgn)) != NULL) {
 	Lst_Remove(pgn->children, ln);
 	pgn->unmade--;
     }
@@ -680,8 +694,8 @@ void
 Make_Update(GNode *cgn)
 {
     GNode 	*pgn;	/* the parent node */
-    char  	*cname;	/* the child's name */
-    LstNode	ln; 	/* Element in parents and iParents lists */
+    const char	*cname;	/* the child's name */
+    LstNode	ln; 	/* Element in parents and implicitParents lists */
     time_t	mtime = -1;
     char	*p1;
     Lst		parents;
@@ -691,7 +705,7 @@ Make_Update(GNode *cgn)
     checked++;
 
     cname = Var_Value(TARGET, cgn, &p1);
-    free(p1);
+    bmake_free(p1);
 
     if (DEBUG(MAKE))
 	fprintf(debug_file, "Make_Update: %s%s\n", cgn->name, cgn->cohort_num);
@@ -724,123 +738,123 @@ Make_Update(GNode *cgn)
     Lst_ForEach(centurion->order_succ, MakeBuildParent, Lst_First(toBeMade));
 
     /* Now mark all the parents as having one less unmade child */
-    if (Lst_Open(parents) == SUCCESS) {
-	while ((ln = Lst_Next(parents)) != NULL) {
-	    pgn = (GNode *)Lst_Datum(ln);
+    Lst_Open(parents);
+    while ((ln = Lst_Next(parents)) != NULL) {
+	pgn = LstNode_Datum(ln);
+	if (DEBUG(MAKE))
+	    fprintf(debug_file, "inspect parent %s%s: flags %x, "
+			"type %x, made %d, unmade %d ",
+		    pgn->name, pgn->cohort_num, pgn->flags,
+		    pgn->type, pgn->made, pgn->unmade-1);
+
+	if (!(pgn->flags & REMAKE)) {
+	    /* This parent isn't needed */
 	    if (DEBUG(MAKE))
-		fprintf(debug_file, "inspect parent %s%s: flags %x, "
-			    "type %x, made %d, unmade %d ",
-			pgn->name, pgn->cohort_num, pgn->flags,
-			pgn->type, pgn->made, pgn->unmade-1);
-
-	    if (!(pgn->flags & REMAKE)) {
-		/* This parent isn't needed */
-		if (DEBUG(MAKE))
-		    fprintf(debug_file, "- not needed\n");
-		continue;
-	    }
-	    if (mtime == 0 && !(cgn->type & OP_WAIT))
-		pgn->flags |= FORCE;
-
-	    /*
-	     * If the parent has the .MADE attribute, its timestamp got
-	     * updated to that of its newest child, and its unmake
-	     * child count got set to zero in Make_ExpandUse().
-	     * However other things might cause us to build one of its
-	     * children - and so we mustn't do any processing here when
-	     * the child build finishes.
-	     */
-	    if (pgn->type & OP_MADE) {
-		if (DEBUG(MAKE))
-		    fprintf(debug_file, "- .MADE\n");
-		continue;
-	    }
-
-	    if ( ! (cgn->type & (OP_EXEC|OP_USE|OP_USEBEFORE))) {
-		if (cgn->made == MADE)
-		    pgn->flags |= CHILDMADE;
-		(void)Make_TimeStamp(pgn, cgn);
-	    }
-
-	    /*
-	     * A parent must wait for the completion of all instances
-	     * of a `::' dependency.
-	     */
-	    if (centurion->unmade_cohorts != 0 || centurion->made < MADE) {
-		if (DEBUG(MAKE))
-		    fprintf(debug_file,
-			    "- centurion made %d, %d unmade cohorts\n",
-			    centurion->made, centurion->unmade_cohorts);
-		continue;
-	    }
-
-	    /* One more child of this parent is now made */
-	    pgn->unmade -= 1;
-	    if (pgn->unmade < 0) {
-		if (DEBUG(MAKE)) {
-		    fprintf(debug_file, "Graph cycles through %s%s\n",
-			pgn->name, pgn->cohort_num);
-		    Targ_PrintGraph(2);
-		}
-		Error("Graph cycles through %s%s", pgn->name, pgn->cohort_num);
-	    }
-
-	    /* We must always rescan the parents of .WAIT and .ORDER nodes. */
-	    if (pgn->unmade != 0 && !(centurion->type & OP_WAIT)
-		    && !(centurion->flags & DONE_ORDER)) {
-		if (DEBUG(MAKE))
-		    fprintf(debug_file, "- unmade children\n");
-		continue;
-	    }
-	    if (pgn->made != DEFERRED) {
-		/*
-		 * Either this parent is on a different branch of the tree,
-		 * or it on the RHS of a .WAIT directive
-		 * or it is already on the toBeMade list.
-		 */
-		if (DEBUG(MAKE))
-		    fprintf(debug_file, "- not deferred\n");
-		continue;
-	    }
-	    if (pgn->order_pred
-		    && Lst_ForEach(pgn->order_pred, MakeCheckOrder, 0)) {
-		/* A .ORDER rule stops us building this */
-		continue;
-	    }
-	    if (DEBUG(MAKE)) {
-		static int two = 2;
-		fprintf(debug_file, "- %s%s made, schedule %s%s (made %d)\n",
-			cgn->name, cgn->cohort_num,
-			pgn->name, pgn->cohort_num, pgn->made);
-		Targ_PrintNode(pgn, &two);
-	    }
-	    /* Ok, we can schedule the parent again */
-	    pgn->made = REQUESTED;
-	    (void)Lst_EnQueue(toBeMade, pgn);
+		fprintf(debug_file, "- not needed\n");
+	    continue;
 	}
-	Lst_Close(parents);
+	if (mtime == 0 && !(cgn->type & OP_WAIT))
+	    pgn->flags |= FORCE;
+
+	/*
+	 * If the parent has the .MADE attribute, its timestamp got
+	 * updated to that of its newest child, and its unmake
+	 * child count got set to zero in Make_ExpandUse().
+	 * However other things might cause us to build one of its
+	 * children - and so we mustn't do any processing here when
+	 * the child build finishes.
+	 */
+	if (pgn->type & OP_MADE) {
+	    if (DEBUG(MAKE))
+		fprintf(debug_file, "- .MADE\n");
+	    continue;
+	}
+
+	if ( ! (cgn->type & (OP_EXEC|OP_USE|OP_USEBEFORE))) {
+	    if (cgn->made == MADE)
+		pgn->flags |= CHILDMADE;
+	    (void)Make_TimeStamp(pgn, cgn);
+	}
+
+	/*
+	 * A parent must wait for the completion of all instances
+	 * of a `::' dependency.
+	 */
+	if (centurion->unmade_cohorts != 0 || centurion->made < MADE) {
+	    if (DEBUG(MAKE))
+		fprintf(debug_file,
+			"- centurion made %d, %d unmade cohorts\n",
+			centurion->made, centurion->unmade_cohorts);
+	    continue;
+	}
+
+	/* One more child of this parent is now made */
+	pgn->unmade -= 1;
+	if (pgn->unmade < 0) {
+	    if (DEBUG(MAKE)) {
+		fprintf(debug_file, "Graph cycles through %s%s\n",
+		    pgn->name, pgn->cohort_num);
+		Targ_PrintGraph(2);
+	    }
+	    Error("Graph cycles through %s%s", pgn->name, pgn->cohort_num);
+	}
+
+	/* We must always rescan the parents of .WAIT and .ORDER nodes. */
+	if (pgn->unmade != 0 && !(centurion->type & OP_WAIT)
+		&& !(centurion->flags & DONE_ORDER)) {
+	    if (DEBUG(MAKE))
+		fprintf(debug_file, "- unmade children\n");
+	    continue;
+	}
+	if (pgn->made != DEFERRED) {
+	    /*
+	     * Either this parent is on a different branch of the tree,
+	     * or it on the RHS of a .WAIT directive
+	     * or it is already on the toBeMade list.
+	     */
+	    if (DEBUG(MAKE))
+		fprintf(debug_file, "- not deferred\n");
+	    continue;
+	}
+	assert(pgn->order_pred != NULL);
+	if (Lst_ForEach(pgn->order_pred, MakeCheckOrder, 0)) {
+	    /* A .ORDER rule stops us building this */
+	    continue;
+	}
+	if (DEBUG(MAKE)) {
+	    static int two = 2;
+	    fprintf(debug_file, "- %s%s made, schedule %s%s (made %d)\n",
+		    cgn->name, cgn->cohort_num,
+		    pgn->name, pgn->cohort_num, pgn->made);
+	    Targ_PrintNode(pgn, &two);
+	}
+	/* Ok, we can schedule the parent again */
+	pgn->made = REQUESTED;
+	Lst_Enqueue(toBeMade, pgn);
     }
+    Lst_Close(parents);
 
     /*
      * Set the .PREFIX and .IMPSRC variables for all the implied parents
      * of this node.
      */
-    if (Lst_Open(cgn->iParents) == SUCCESS) {
-	char	*cpref = Var_Value(PREFIX, cgn, &p1);
+    Lst_Open(cgn->implicitParents);
+    {
+	const char *cpref = Var_Value(PREFIX, cgn, &p1);
 
-	while ((ln = Lst_Next(cgn->iParents)) != NULL) {
-	    pgn = (GNode *)Lst_Datum(ln);
+	while ((ln = Lst_Next(cgn->implicitParents)) != NULL) {
+	    pgn = LstNode_Datum(ln);
 	    if (pgn->flags & REMAKE) {
 		Var_Set(IMPSRC, cname, pgn);
 		if (cpref != NULL)
 		    Var_Set(PREFIX, cpref, pgn);
 	    }
 	}
-	free(p1);
-	Lst_Close(cgn->iParents);
+	bmake_free(p1);
+	Lst_Close(cgn->implicitParents);
     }
 }
-
+
 /*-
  *-----------------------------------------------------------------------
  * MakeAddAllSrc --
@@ -890,7 +904,7 @@ MakeAddAllSrc(void *cgnp, void *pgnp)
     cgn->type |= OP_MARK;
 
     if ((cgn->type & (OP_EXEC|OP_USE|OP_USEBEFORE|OP_INVISIBLE)) == 0) {
-	char *child, *allsrc;
+	const char *child, *allsrc;
 	char *p1 = NULL, *p2 = NULL;
 
 	if (cgn->type & OP_ARCHV)
@@ -904,7 +918,7 @@ MakeAddAllSrc(void *cgnp, void *pgnp)
 	}
 	if (allsrc != NULL)
 		Var_Append(ALLSRC, allsrc, pgn);
-	free(p2);
+	bmake_free(p2);
 	if (pgn->type & OP_JOIN) {
 	    if (cgn->made == MADE) {
 		Var_Append(OODATE, child, pgn);
@@ -930,11 +944,11 @@ MakeAddAllSrc(void *cgnp, void *pgnp)
 	     */
 	    Var_Append(OODATE, child, pgn);
 	}
-	free(p1);
+	bmake_free(p1);
     }
     return 0;
 }
-
+
 /*-
  *-----------------------------------------------------------------------
  * Make_DoAllVar --
@@ -976,11 +990,11 @@ Make_DoAllVar(GNode *gn)
     if (gn->type & OP_JOIN) {
 	char *p1;
 	Var_Set(TARGET, Var_Value(ALLSRC, gn, &p1), gn);
-	free(p1);
+	bmake_free(p1);
     }
     gn->flags |= DONE_ALLSRC;
 }
-
+
 /*-
  *-----------------------------------------------------------------------
  * MakeStartJobs --
@@ -1023,7 +1037,8 @@ MakeBuildChild(void *v_cn, void *toBeMade_next)
 	return 0;
 
     /* If this node is on the RHS of a .ORDER, check LHSs. */
-    if (cn->order_pred && Lst_ForEach(cn->order_pred, MakeCheckOrder, 0)) {
+    assert(cn->order_pred);
+    if (Lst_ForEach(cn->order_pred, MakeCheckOrder, 0)) {
 	/* Can't build this (or anything else in this child list) yet */
 	cn->made = DEFERRED;
 	return 0;			/* but keep looking */
@@ -1035,7 +1050,7 @@ MakeBuildChild(void *v_cn, void *toBeMade_next)
 
     cn->made = REQUESTED;
     if (toBeMade_next == NULL)
-	Lst_AtEnd(toBeMade, cn);
+	Lst_Append(toBeMade, cn);
     else
 	Lst_InsertBefore(toBeMade, toBeMade_next, cn);
 
@@ -1072,13 +1087,13 @@ MakeStartJobs(void)
     GNode	*gn;
     int		have_token = 0;
 
-    while (!Lst_IsEmpty (toBeMade)) {
+    while (!Lst_IsEmpty(toBeMade)) {
 	/* Get token now to avoid cycling job-list when we only have 1 token */
 	if (!have_token && !Job_TokenWithdraw())
 	    break;
 	have_token = 1;
 
-	gn = (GNode *)Lst_DeQueue(toBeMade);
+	gn = Lst_Dequeue(toBeMade);
 	if (DEBUG(MAKE))
 	    fprintf(debug_file, "Examining %s%s...\n",
 		    gn->name, gn->cohort_num);
@@ -1147,7 +1162,7 @@ MakeStartJobs(void)
 
     return FALSE;
 }
-
+
 /*-
  *-----------------------------------------------------------------------
  * MakePrintStatus --
@@ -1179,15 +1194,15 @@ MakePrintStatusOrder(void *ognp, void *gnp)
 	/* not waiting for this one */
 	return 0;
 
-    printf("    `%s%s' has .ORDER dependency against %s%s "
-		"(made %d, flags %x, type %x)\n",
-	    gn->name, gn->cohort_num,
-	    ogn->name, ogn->cohort_num, ogn->made, ogn->flags, ogn->type);
-    if (DEBUG(MAKE) && debug_file != stdout)
-	fprintf(debug_file, "    `%s%s' has .ORDER dependency against %s%s "
-		    "(made %d, flags %x, type %x)\n",
-		gn->name, gn->cohort_num,
-		ogn->name, ogn->cohort_num, ogn->made, ogn->flags, ogn->type);
+    printf("    `%s%s' has .ORDER dependency against %s%s ",
+	    gn->name, gn->cohort_num, ogn->name, ogn->cohort_num);
+    GNode_FprintDetails(stdout, "(", ogn, ")\n");
+
+    if (DEBUG(MAKE) && debug_file != stdout) {
+	fprintf(debug_file, "    `%s%s' has .ORDER dependency against %s%s ",
+		gn->name, gn->cohort_num, ogn->name, ogn->cohort_num);
+	GNode_FprintDetails(debug_file, "(", ogn, ")\n");
+    }
     return 0;
 }
 
@@ -1214,12 +1229,13 @@ MakePrintStatus(void *gnp, void *v_errors)
 	case REQUESTED:
 	case BEINGMADE:
 	    (*errors)++;
-	    printf("`%s%s' was not built (made %d, flags %x, type %x)!\n",
-		    gn->name, gn->cohort_num, gn->made, gn->flags, gn->type);
-	    if (DEBUG(MAKE) && debug_file != stdout)
-		fprintf(debug_file,
-			"`%s%s' was not built (made %d, flags %x, type %x)!\n",
-			gn->name, gn->cohort_num, gn->made, gn->flags, gn->type);
+	    printf("`%s%s' was not built", gn->name, gn->cohort_num);
+	    GNode_FprintDetails(stdout, " (", gn, ")!\n");
+	    if (DEBUG(MAKE) && debug_file != stdout) {
+		fprintf(debug_file, "`%s%s' was not built",
+			gn->name, gn->cohort_num);
+		GNode_FprintDetails(debug_file, " (", gn, ")!\n");
+	    }
 	    /* Most likely problem is actually caused by .ORDER */
 	    Lst_ForEach(gn->order_pred, MakePrintStatusOrder, gn);
 	    break;
@@ -1262,7 +1278,7 @@ MakePrintStatus(void *gnp, void *v_errors)
     Lst_ForEach(gn->children, MakePrintStatus, errors);
     return 0;
 }
-
+
 
 /*-
  *-----------------------------------------------------------------------
@@ -1281,7 +1297,7 @@ Make_ExpandUse(Lst targs)
     GNode  *gn;		/* a temporary pointer */
     Lst    examine; 	/* List of targets to examine */
 
-    examine = Lst_Duplicate(targs, NULL);
+    examine = Lst_Copy(targs, NULL);
 
     /*
      * Make an initial downward pass over the graph, marking nodes to be made
@@ -1291,8 +1307,8 @@ Make_ExpandUse(Lst targs)
      * be looked at in a minute, otherwise we add its children to our queue
      * and go on about our business.
      */
-    while (!Lst_IsEmpty (examine)) {
-	gn = (GNode *)Lst_DeQueue(examine);
+    while (!Lst_IsEmpty(examine)) {
+	gn = Lst_Dequeue(examine);
 
 	if (gn->flags & REMAKE)
 	    /* We've looked at this one already */
@@ -1302,13 +1318,8 @@ Make_ExpandUse(Lst targs)
 	    fprintf(debug_file, "Make_ExpandUse: examine %s%s\n",
 		    gn->name, gn->cohort_num);
 
-	if ((gn->type & OP_DOUBLEDEP) && !Lst_IsEmpty (gn->cohorts)) {
-	    /* Append all the 'cohorts' to the list of things to examine */
-	    Lst new;
-	    new = Lst_Duplicate(gn->cohorts, NULL);
-	    Lst_Concat(new, examine, LST_CONCLINK);
-	    examine = new;
-	}
+	if (gn->type & OP_DOUBLEDEP)
+	    Lst_PrependAll(examine, gn->cohorts);
 
 	/*
 	 * Apply any .USE rules before looking for implicit dependencies
@@ -1349,7 +1360,7 @@ Make_ExpandUse(Lst targs)
 	    Lst_ForEach(gn->children, MakeAddChild, examine);
     }
 
-    Lst_Destroy(examine, NULL);
+    Lst_Free(examine);
 }
 
 /*-
@@ -1369,8 +1380,8 @@ link_parent(void *cnp, void *pnp)
     GNode *cn = cnp;
     GNode *pn = pnp;
 
-    Lst_AtEnd(pn->children, cn);
-    Lst_AtEnd(cn->parents, pn);
+    Lst_Append(pn->children, cn);
+    Lst_Append(cn->parents, pn);
     pn->unmade++;
     return 0;
 }
@@ -1392,9 +1403,9 @@ add_wait_dep(void *v_cn, void *v_wn)
 	 fprintf(debug_file, ".WAIT: add dependency %s%s -> %s\n",
 		cn->name, cn->cohort_num, wn->name);
 
-    Lst_AtEnd(wn->children, cn);
+    Lst_Append(wn->children, cn);
     wn->unmade++;
-    Lst_AtEnd(cn->parents, wn);
+    Lst_Append(cn->parents, wn);
     return 0;
 }
 
@@ -1417,18 +1428,18 @@ Make_ProcessWait(Lst targs)
     pgn->flags = REMAKE;
     pgn->type = OP_PHONY | OP_DEPENDS;
     /* Get it displayed in the diag dumps */
-    Lst_AtFront(Targ_List(), pgn);
+    Lst_Prepend(Targ_List(), pgn);
 
     Lst_ForEach(targs, link_parent, pgn);
 
     /* Start building with the 'dummy' .MAIN' node */
     MakeBuildChild(pgn, NULL);
 
-    examine = Lst_Init(FALSE);
-    Lst_AtEnd(examine, pgn);
+    examine = Lst_Init();
+    Lst_Append(examine, pgn);
 
-    while (!Lst_IsEmpty (examine)) {
-	pgn = Lst_DeQueue(examine);
+    while (!Lst_IsEmpty(examine)) {
+	pgn = Lst_Dequeue(examine);
 
 	/* We only want to process each child-list once */
 	if (pgn->flags & DONE_WAIT)
@@ -1437,30 +1448,25 @@ Make_ProcessWait(Lst targs)
 	if (DEBUG(MAKE))
 	    fprintf(debug_file, "Make_ProcessWait: examine %s\n", pgn->name);
 
-	if ((pgn->type & OP_DOUBLEDEP) && !Lst_IsEmpty (pgn->cohorts)) {
-	    /* Append all the 'cohorts' to the list of things to examine */
-	    Lst new;
-	    new = Lst_Duplicate(pgn->cohorts, NULL);
-	    Lst_Concat(new, examine, LST_CONCLINK);
-	    examine = new;
-	}
+	if (pgn->type & OP_DOUBLEDEP)
+	    Lst_PrependAll(examine, pgn->cohorts);
 
 	owln = Lst_First(pgn->children);
 	Lst_Open(pgn->children);
 	for (; (ln = Lst_Next(pgn->children)) != NULL; ) {
-	    cgn = Lst_Datum(ln);
+	    cgn = LstNode_Datum(ln);
 	    if (cgn->type & OP_WAIT) {
 		/* Make the .WAIT node depend on the previous children */
 		Lst_ForEachFrom(pgn->children, owln, add_wait_dep, cgn);
 		owln = ln;
 	    } else {
-		Lst_AtEnd(examine, cgn);
+		Lst_Append(examine, cgn);
 	    }
 	}
 	Lst_Close(pgn->children);
     }
 
-    Lst_Destroy(examine, NULL);
+    Lst_Free(examine);
 }
 
 /*-
@@ -1493,7 +1499,7 @@ Make_Run(Lst targs)
     int	    	    errors; 	/* Number of errors the Job module reports */
 
     /* Start trying to make the current targets... */
-    toBeMade = Lst_Init(FALSE);
+    toBeMade = Lst_Init();
 
     Make_ExpandUse(targs);
     Make_ProcessWait(targs);
diff --git a/make.h b/make.h
index eafa761ea36a..520a6602518f 100644
--- a/make.h
+++ b/make.h
@@ -1,4 +1,4 @@
-/*	$NetBSD: make.h,v 1.109 2020/07/02 15:14:38 rillig Exp $	*/
+/*	$NetBSD: make.h,v 1.137 2020/09/02 23:42:58 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -77,8 +77,8 @@
  *	The global definitions for pmake
  */
 
-#ifndef _MAKE_H_
-#define _MAKE_H_
+#ifndef MAKE_MAKE_H
+#define MAKE_MAKE_H
 
 #ifdef HAVE_CONFIG_H
 # include "config.h"
@@ -86,7 +86,9 @@
 
 #include <sys/types.h>
 #include <sys/param.h>
+#include <sys/stat.h>
 
+#include <assert.h>
 #include <ctype.h>
 #include <fcntl.h>
 #include <stdio.h>
@@ -132,8 +134,34 @@
 #define MAKE_ATTR_PRINTFLIKE(fmtarg, firstvararg)	/* delete */
 #endif
 
-#include "sprite.h"
+/*
+ * A boolean type is defined as an integer, not an enum, for historic reasons.
+ * The only allowed values are the constants TRUE and FALSE (1 and 0).
+ */
+
+#ifdef USE_DOUBLE_BOOLEAN
+/* During development, to find type mismatches in function declarations. */
+typedef double Boolean;
+#elif defined(USE_UCHAR_BOOLEAN)
+/* During development, to find code that depends on the exact value of TRUE or
+ * that stores other values in Boolean variables. */
+typedef unsigned char Boolean;
+#define TRUE ((unsigned char)0xFF)
+#define FALSE ((unsigned char)0x00)
+#elif defined(USE_ENUM_BOOLEAN)
+typedef enum { FALSE, TRUE} Boolean;
+#else
+typedef int Boolean;
+#endif
+#ifndef TRUE
+#define TRUE	1
+#endif /* TRUE */
+#ifndef FALSE
+#define FALSE	0
+#endif /* FALSE */
+
 #include "lst.h"
+#include "enum.h"
 #include "hash.h"
 #include "make-conf.h"
 #include "buf.h"
@@ -150,163 +178,190 @@
 #define POSIX_SIGNALS
 #endif
 
-/*-
- * The structure for an individual graph node. Each node has several
- * pieces of data associated with it.
- *	1) the name of the target it describes
- *	2) the location of the target file in the file system.
- *	3) the type of operator used to define its sources (qv. parse.c)
- *	4) whether it is involved in this invocation of make
- *	5) whether the target has been remade
- *	6) whether any of its children has been remade
- *	7) the number of its children that are, as yet, unmade
- *	8) its modification time
- *	9) the modification time of its youngest child (qv. make.c)
- *	10) a list of nodes for which this is a source (parents)
- *	11) a list of nodes on which this depends (children)
- *	12) a list of nodes that depend on this, as gleaned from the
- *	    transformation rules (iParents)
- *	13) a list of ancestor nodes, which includes parents, iParents,
- *	    and recursive parents of parents
- *	14) a list of nodes of the same name created by the :: operator
- *	15) a list of nodes that must be made (if they're made) before
- *	    this node can be, but that do not enter into the datedness of
- *	    this node.
- *	16) a list of nodes that must be made (if they're made) before
- *	    this node or any child of this node can be, but that do not
- *	    enter into the datedness of this node.
- *	17) a list of nodes that must be made (if they're made) after
- *	    this node is, but that do not depend on this node, in the
- *	    normal sense.
- *	18) a Lst of ``local'' variables that are specific to this target
- *	   and this target only (qv. var.c [$@ $< $?, etc.])
- *	19) a Lst of strings that are commands to be given to a shell
- *	   to create this target.
- */
-typedef struct GNode {
-    char            *name;     	/* The target's name */
-    char            *uname;    	/* The unexpanded name of a .USE node */
-    char    	    *path;     	/* The full pathname of the file */
-    int             type;      	/* Its type (see the OP flags, below) */
+typedef enum  {
+    UNMADE,			/* Not examined yet */
+    DEFERRED,			/* Examined once (building child) */
+    REQUESTED,			/* on toBeMade list */
+    BEINGMADE,			/* Target is already being made.
+				 * Indicates a cycle in the graph. */
+    MADE,			/* Was out-of-date and has been made */
+    UPTODATE,			/* Was already up-to-date */
+    ERROR,			/* An error occurred while it was being
+				 * made (used only in compat mode) */
+    ABORTED			/* The target was aborted due to an error
+				 * making an inferior (compat). */
+} GNodeMade;
 
-    int             flags;
-#define REMAKE		0x1    	/* this target needs to be (re)made */
-#define	CHILDMADE	0x2	/* children of this target were made */
-#define FORCE		0x4	/* children don't exist, and we pretend made */
-#define DONE_WAIT	0x8	/* Set by Make_ProcessWait() */
-#define DONE_ORDER	0x10	/* Build requested by .ORDER processing */
-#define FROM_DEPEND	0x20	/* Node created from .depend */
-#define DONE_ALLSRC	0x40	/* We do it once only */
-#define CYCLE		0x1000  /* Used by MakePrintStatus */
-#define DONECYCLE	0x2000  /* Used by MakePrintStatus */
-#define INTERNAL	0x4000	/* Internal use only */
-    enum enum_made {
-	UNMADE, DEFERRED, REQUESTED, BEINGMADE,
-	MADE, UPTODATE, ERROR, ABORTED
-    }	    	    made;    	/* Set to reflect the state of processing
-				 * on this node:
-				 *  UNMADE - Not examined yet
-				 *  DEFERRED - Examined once (building child)
-				 *  REQUESTED - on toBeMade list
-				 *  BEINGMADE - Target is already being made.
-				 *  	Indicates a cycle in the graph.
-				 *  MADE - Was out-of-date and has been made
-				 *  UPTODATE - Was already up-to-date
-				 *  ERROR - An error occurred while it was being
-				 *  	made (used only in compat mode)
-				 *  ABORTED - The target was aborted due to
-				 *  	an error making an inferior (compat).
-				 */
-    int             unmade;    	/* The number of unmade children */
-
-    time_t          mtime;     	/* Its modification time */
-    struct GNode    *cmgn;    	/* The youngest child */
-
-    Lst     	    iParents;  	/* Links to parents for which this is an
-				 * implied source, if any */
-    Lst	    	    cohorts;  	/* Other nodes for the :: operator */
-    Lst             parents;   	/* Nodes that depend on this one */
-    Lst             children;  	/* Nodes on which this one depends */
-    Lst             order_pred;	/* .ORDER nodes we need made */
-    Lst             order_succ;	/* .ORDER nodes who need us */
-
-    char	    cohort_num[8]; /* #n for this cohort */
-    int		    unmade_cohorts;/* # of unmade instances on the
-				      cohorts list */
-    struct GNode    *centurion;	/* Pointer to the first instance of a ::
-				   node; only set when on a cohorts list */
-    unsigned int    checked;    /* Last time we tried to makle this node */
-
-    Hash_Table      context;	/* The local variables */
-    Lst             commands;  	/* Creation commands */
-
-    struct _Suff    *suffix;	/* Suffix for the node (determined by
-				 * Suff_FindDeps and opaque to everyone
-				 * but the Suff module) */
-    const char	    *fname;	/* filename where the GNode got defined */
-    int		     lineno;	/* line number where the GNode got defined */
-} GNode;
-
-/*
- * The OP_ constants are used when parsing a dependency line as a way of
+/* The OP_ constants are used when parsing a dependency line as a way of
  * communicating to other parts of the program the way in which a target
- * should be made. These constants are bitwise-OR'ed together and
- * placed in the 'type' field of each node. Any node that has
- * a 'type' field which satisfies the OP_NOP function was never never on
- * the lefthand side of an operator, though it may have been on the
- * righthand side...
- */
-#define OP_DEPENDS	0x00000001  /* Execution of commands depends on
-				     * kids (:) */
-#define OP_FORCE	0x00000002  /* Always execute commands (!) */
-#define OP_DOUBLEDEP	0x00000004  /* Execution of commands depends on kids
-				     * per line (::) */
-#define OP_OPMASK	(OP_DEPENDS|OP_FORCE|OP_DOUBLEDEP)
+ * should be made.
+ *
+ * These constants are bitwise-OR'ed together and placed in the 'type' field
+ * of each node. Any node that has a 'type' field which satisfies the OP_NOP
+ * function was never never on the left-hand side of an operator, though it
+ * may have been on the right-hand side... */
+typedef enum {
+    /* Execution of commands depends on children (:) */
+    OP_DEPENDS		= 1 << 0,
+    /* Always execute commands (!) */
+    OP_FORCE		= 1 << 1,
+    /* Execution of commands depends on children per line (::) */
+    OP_DOUBLEDEP	= 1 << 2,
 
-#define OP_OPTIONAL	0x00000008  /* Don't care if the target doesn't
-				     * exist and can't be created */
-#define OP_USE		0x00000010  /* Use associated commands for parents */
-#define OP_EXEC	  	0x00000020  /* Target is never out of date, but always
-				     * execute commands anyway. Its time
-				     * doesn't matter, so it has none...sort
-				     * of */
-#define OP_IGNORE	0x00000040  /* Ignore errors when creating the node */
-#define OP_PRECIOUS	0x00000080  /* Don't remove the target when
-				     * interrupted */
-#define OP_SILENT	0x00000100  /* Don't echo commands when executed */
-#define OP_MAKE		0x00000200  /* Target is a recursive make so its
-				     * commands should always be executed when
-				     * it is out of date, regardless of the
-				     * state of the -n or -t flags */
-#define OP_JOIN 	0x00000400  /* Target is out-of-date only if any of its
-				     * children was out-of-date */
-#define	OP_MADE		0x00000800  /* Assume the children of the node have
-				     * been already made */
-#define OP_SPECIAL	0x00001000  /* Special .BEGIN, .END, .INTERRUPT */
-#define	OP_USEBEFORE	0x00002000  /* Like .USE, only prepend commands */
-#define OP_INVISIBLE	0x00004000  /* The node is invisible to its parents.
-				     * I.e. it doesn't show up in the parents's
-				     * local variables. */
-#define OP_NOTMAIN	0x00008000  /* The node is exempt from normal 'main
-				     * target' processing in parse.c */
-#define OP_PHONY	0x00010000  /* Not a file target; run always */
-#define OP_NOPATH	0x00020000  /* Don't search for file in the path */
-#define OP_WAIT 	0x00040000  /* .WAIT phony node */
-#define OP_NOMETA	0x00080000  /* .NOMETA do not create a .meta file */
-#define OP_META		0x00100000  /* .META we _do_ want a .meta file */
-#define OP_NOMETA_CMP	0x00200000  /* Do not compare commands in .meta file */
-#define OP_SUBMAKE	0x00400000  /* Possibly a submake node */
-/* Attributes applied by PMake */
-#define OP_TRANSFORM	0x80000000  /* The node is a transformation rule */
-#define OP_MEMBER 	0x40000000  /* Target is a member of an archive */
-#define OP_LIB	  	0x20000000  /* Target is a library */
-#define OP_ARCHV  	0x10000000  /* Target is an archive construct */
-#define OP_HAS_COMMANDS	0x08000000  /* Target has all the commands it should.
-				     * Used when parsing to catch multiple
-				     * commands for a target */
-#define OP_SAVE_CMDS	0x04000000  /* Saving commands on .END (Compat) */
-#define OP_DEPS_FOUND	0x02000000  /* Already processed by Suff_FindDeps */
-#define	OP_MARK		0x01000000  /* Node found while expanding .ALLSRC */
+    OP_OPMASK		= OP_DEPENDS|OP_FORCE|OP_DOUBLEDEP,
+
+    /* Don't care if the target doesn't exist and can't be created */
+    OP_OPTIONAL		= 1 << 3,
+    /* Use associated commands for parents */
+    OP_USE		= 1 << 4,
+    /* Target is never out of date, but always execute commands anyway.
+     * Its time doesn't matter, so it has none...sort of */
+    OP_EXEC	  	= 1 << 5,
+    /* Ignore errors when creating the node */
+    OP_IGNORE		= 1 << 6,
+    /* Don't remove the target when interrupted */
+    OP_PRECIOUS		= 1 << 7,
+    /* Don't echo commands when executed */
+    OP_SILENT		= 1 << 8,
+    /* Target is a recursive make so its commands should always be executed
+     * when it is out of date, regardless of the state of the -n or -t flags */
+    OP_MAKE		= 1 << 9,
+    /* Target is out-of-date only if any of its children was out-of-date */
+    OP_JOIN		= 1 << 10,
+    /* Assume the children of the node have been already made */
+    OP_MADE		= 1 << 11,
+    /* Special .BEGIN, .END, .INTERRUPT */
+    OP_SPECIAL		= 1 << 12,
+    /* Like .USE, only prepend commands */
+    OP_USEBEFORE	= 1 << 13,
+    /* The node is invisible to its parents. I.e. it doesn't show up in the
+     * parents' local variables. */
+    OP_INVISIBLE	= 1 << 14,
+    /* The node is exempt from normal 'main target' processing in parse.c */
+    OP_NOTMAIN		= 1 << 15,
+    /* Not a file target; run always */
+    OP_PHONY		= 1 << 16,
+    /* Don't search for file in the path */
+    OP_NOPATH		= 1 << 17,
+    /* .WAIT phony node */
+    OP_WAIT		= 1 << 18,
+    /* .NOMETA do not create a .meta file */
+    OP_NOMETA		= 1 << 19,
+    /* .META we _do_ want a .meta file */
+    OP_META		= 1 << 20,
+    /* Do not compare commands in .meta file */
+    OP_NOMETA_CMP	= 1 << 21,
+    /* Possibly a submake node */
+    OP_SUBMAKE		= 1 << 22,
+
+    /* Attributes applied by PMake */
+
+    /* The node is a transformation rule */
+    OP_TRANSFORM	= 1 << 31,
+    /* Target is a member of an archive */
+    OP_MEMBER		= 1 << 30,
+    /* Target is a library */
+    OP_LIB		= 1 << 29,
+    /* Target is an archive construct */
+    OP_ARCHV		= 1 << 28,
+    /* Target has all the commands it should. Used when parsing to catch
+     * multiple commands for a target. */
+    OP_HAS_COMMANDS	= 1 << 27,
+    /* Saving commands on .END (Compat) */
+    OP_SAVE_CMDS	= 1 << 26,
+    /* Already processed by Suff_FindDeps */
+    OP_DEPS_FOUND	= 1 << 25,
+    /* Node found while expanding .ALLSRC */
+    OP_MARK		= 1 << 24
+} GNodeType;
+
+typedef enum {
+    REMAKE	= 0x0001,	/* this target needs to be (re)made */
+    CHILDMADE	= 0x0002,	/* children of this target were made */
+    FORCE	= 0x0004,	/* children don't exist, and we pretend made */
+    DONE_WAIT	= 0x0008,	/* Set by Make_ProcessWait() */
+    DONE_ORDER	= 0x0010,	/* Build requested by .ORDER processing */
+    FROM_DEPEND	= 0x0020,	/* Node created from .depend */
+    DONE_ALLSRC	= 0x0040,	/* We do it once only */
+    CYCLE	= 0x1000,	/* Used by MakePrintStatus */
+    DONECYCLE	= 0x2000,	/* Used by MakePrintStatus */
+    INTERNAL	= 0x4000	/* Internal use only */
+} GNodeFlags;
+
+/* A graph node represents a target that can possibly be made, including its
+ * relation to other targets and a lot of other details. */
+typedef struct GNode {
+    /* The target's name, such as "clean" or "make.c" */
+    char *name;
+    /* The unexpanded name of a .USE node */
+    char *uname;
+    /* The full pathname of the file belonging to the target.
+     * XXX: What about .PHONY targets? These don't have an associated path. */
+    char *path;
+
+    /* The type of operator used to define the sources (see the OP flags below).
+     * XXX: This looks like a wild mixture of type and flags. */
+    GNodeType type;
+    /* whether it is involved in this invocation of make */
+    GNodeFlags flags;
+
+    /* The state of processing on this node */
+    GNodeMade made;
+    int unmade;			/* The number of unmade children */
+
+    time_t mtime;		/* Its modification time */
+    struct GNode *cmgn;		/* The youngest child */
+
+    /* The GNodes for which this node is an implied source. May be empty.
+     * For example, when there is an inference rule for .c.o, the node for
+     * file.c has the node for file.o in this list. */
+    Lst implicitParents;
+
+    /* Other nodes of the same name for the :: operator. */
+    Lst cohorts;
+
+    /* The nodes that depend on this one, or in other words, the nodes for
+     * which this is a source. */
+    Lst parents;
+    /* The nodes on which this one depends. */
+    Lst children;
+
+    /* .ORDER nodes we need made. The nodes that must be made (if they're
+     * made) before this node can be made, but that do not enter into the
+     * datedness of this node. */
+    Lst order_pred;
+    /* .ORDER nodes who need us. The nodes that must be made (if they're made
+     * at all) after this node is made, but that do not depend on this node,
+     * in the normal sense. */
+    Lst order_succ;
+
+    /* #n for this cohort */
+    char cohort_num[8];
+    /* The number of unmade instances on the cohorts list */
+    int unmade_cohorts;
+    /* Pointer to the first instance of a '::' node; only set when on a
+     * cohorts list */
+    struct GNode *centurion;
+
+    /* Last time (sequence number) we tried to make this node */
+    unsigned int checked;
+
+    /* The "local" variables that are specific to this target and this target
+     * only, such as $@, $<, $?. */
+    Hash_Table context;
+
+    /* The commands to be given to a shell to create this target. */
+    Lst commands;
+
+    /* Suffix for the node (determined by Suff_FindDeps and opaque to everyone
+     * but the Suff module) */
+    struct Suff *suffix;
+
+    /* filename where the GNode got defined */
+    const char *fname;
+    /* line number where the GNode got defined */
+    int lineno;
+} GNode;
 
 #define NoExecute(gn) ((gn->type & OP_MAKE) ? noRecursiveExecute : noExecute)
 /*
@@ -329,18 +384,6 @@ typedef struct GNode {
 #define TARG_CREATE	0x01	  /* create node if not found */
 #define TARG_NOHASH	0x02	  /* don't look in/add to hash table */
 
-/*
- * These constants are all used by the Str_Concat function to decide how the
- * final string should look. If STR_ADDSPACE is given, a space will be
- * placed between the two strings. If STR_ADDSLASH is given, a '/' will
- * be used instead of a space. If neither is given, no intervening characters
- * will be placed between the two strings in the final output. If the
- * STR_DOFREE bit is set, the two input strings will be freed before
- * Str_Concat returns.
- */
-#define STR_ADDSPACE	0x01	/* add a space when Str_Concat'ing */
-#define STR_ADDSLASH	0x02	/* add a slash when Str_Concat'ing */
-
 /*
  * Error levels for parsing. PARSE_FATAL means the process cannot continue
  * once the makefile has been parsed. PARSE_WARNING means it can. Passed
@@ -353,9 +396,11 @@ typedef struct GNode {
 /*
  * Values returned by Cond_Eval.
  */
-#define COND_PARSE	0   	/* Parse the next lines */
-#define COND_SKIP 	1   	/* Skip the next lines */
-#define COND_INVALID	2   	/* Not a conditional statement */
+typedef enum {
+    COND_PARSE,			/* Parse the next lines */
+    COND_SKIP,			/* Skip the next lines */
+    COND_INVALID		/* Not a conditional statement */
+} CondEvalResult;
 
 /*
  * Definitions for the "local" variables. Used only for clarity.
@@ -403,7 +448,6 @@ extern Boolean	doing_depend;	/* TRUE if processing .depend */
 
 extern Boolean	checkEnvFirst;	/* TRUE if environment should be searched for
 				 * variables before the global context */
-extern Boolean	jobServer;	/* a jobServer already exists */
 
 extern Boolean	parseWarnFatal;	/* TRUE if makefile parsing warnings are
 				 * treated as errors */
@@ -420,7 +464,6 @@ extern GNode	*VAR_INTERNAL;	/* Variables defined internally by make
 extern GNode    *VAR_GLOBAL;   	/* Variables defined in a global context, e.g
 				 * in the Makefile itself */
 extern GNode    *VAR_CMD;    	/* Variables defined on the command line */
-extern GNode	*VAR_FOR;	/* Iteration variables */
 extern char    	var_Error[];   	/* Value returned by Var_Parse when an error
 				 * is encountered. It actually points to
 				 * an empty string, so naive callers needn't
@@ -439,6 +482,8 @@ extern char	*progname;	/* The program name */
 extern char	*makeDependfile; /* .depend */
 extern char	**savedEnv;	 /* if we replaced environ this will be non-NULL */
 
+extern int	makelevel;
+
 /*
  * We cannot vfork() in a child of vfork().
  * Most systems do not enforce this but some do.
@@ -464,7 +509,7 @@ extern pid_t	myPid;
  *	There is one bit per module.  It is up to the module what debug
  *	information to print.
  */
-extern FILE *debug_file;	/* Output written here - default stdout */
+extern FILE *debug_file;	/* Output is written here - default stderr */
 extern int debug;
 #define	DEBUG_ARCH	0x00001
 #define	DEBUG_COND	0x00002
@@ -481,12 +526,15 @@ extern int debug;
 #define DEBUG_ERROR	0x01000
 #define DEBUG_LOUD	0x02000
 #define DEBUG_META	0x04000
+#define DEBUG_HASH	0x08000
 
 #define DEBUG_GRAPH3	0x10000
 #define DEBUG_SCRIPT	0x20000
 #define DEBUG_PARSE	0x40000
 #define DEBUG_CWD	0x80000
 
+#define DEBUG_LINT	0x100000
+
 #define CONCAT(a,b)	a##b
 
 #define	DEBUG(module)	(debug & CONCAT(DEBUG_,module))
@@ -501,16 +549,13 @@ void Make_HandleUse(GNode *, GNode *);
 void Make_Update(GNode *);
 void Make_DoAllVar(GNode *);
 Boolean Make_Run(Lst);
-char * Check_Cwd_Cmd(const char *);
-void Check_Cwd(const char **);
 int dieQuietly(GNode *, int);
 void PrintOnError(GNode *, const char *);
 void Main_ExportMAKEFLAGS(Boolean);
 Boolean Main_SetObjdir(const char *, ...) MAKE_ATTR_PRINTFLIKE(1, 2);
 int mkTempFile(const char *, char **);
 int str2Lst_Append(Lst, char *, const char *);
-int cached_lstat(const char *, void *);
-int cached_stat(const char *, void *);
+void GNode_FprintDetails(FILE *, const char *, const GNode *, const char *);
 
 #ifdef __GNUC__
 #define UNCONST(ptr)	({ 		\
@@ -524,10 +569,10 @@ int cached_stat(const char *, void *);
 #endif
 
 #ifndef MIN
-#define MIN(a, b) ((a < b) ? a : b)
+#define MIN(a, b) (((a) < (b)) ? (a) : (b))
 #endif
 #ifndef MAX
-#define MAX(a, b) ((a > b) ? a : b)
+#define MAX(a, b) (((a) > (b)) ? (a) : (b))
 #endif
 
 /* At least GNU/Hurd systems lack hardcoded MAXPATHLEN/PATH_MAX */
@@ -547,4 +592,4 @@ int cached_stat(const char *, void *);
 #define KILLPG(pid, sig)	killpg((pid), (sig))
 #endif
 
-#endif /* _MAKE_H_ */
+#endif /* MAKE_MAKE_H */
diff --git a/make_malloc.c b/make_malloc.c
index 7e2f75ff85e0..ba9632b2b254 100644
--- a/make_malloc.c
+++ b/make_malloc.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: make_malloc.c,v 1.12 2020/07/03 08:02:55 rillig Exp $	*/
+/*	$NetBSD: make_malloc.c,v 1.18 2020/09/02 06:10:44 rillig Exp $	*/
 
 /*-
  * Copyright (c) 2009 The NetBSD Foundation, Inc.
@@ -28,7 +28,7 @@
 
 #ifdef MAKE_NATIVE
 #include <sys/cdefs.h>
-__RCSID("$NetBSD: make_malloc.c,v 1.12 2020/07/03 08:02:55 rillig Exp $");
+__RCSID("$NetBSD: make_malloc.c,v 1.18 2020/09/02 06:10:44 rillig Exp $");
 #endif
 
 #include <stdio.h>
@@ -41,10 +41,7 @@ __RCSID("$NetBSD: make_malloc.c,v 1.12 2020/07/03 08:02:55 rillig Exp $");
 #ifndef USE_EMALLOC
 static MAKE_ATTR_DEAD void enomem(void);
 
-/*
- * enomem --
- *	die when out of memory.
- */
+/* die when out of memory. */
 static MAKE_ATTR_DEAD void
 enomem(void)
 {
@@ -52,10 +49,7 @@ enomem(void)
 	exit(2);
 }
 
-/*
- * bmake_malloc --
- *	malloc, but die on error.
- */
+/* malloc, but die on error. */
 void *
 bmake_malloc(size_t len)
 {
@@ -66,10 +60,7 @@ bmake_malloc(size_t len)
 	return p;
 }
 
-/*
- * bmake_strdup --
- *	strdup, but die on error.
- */
+/* strdup, but die on error. */
 char *
 bmake_strdup(const char *str)
 {
@@ -82,33 +73,17 @@ bmake_strdup(const char *str)
 	return memcpy(p, str, len);
 }
 
-/*
- * bmake_strndup --
- *	strndup, but die on error.
- */
+/* Allocate a string starting from str with exactly len characters. */
 char *
-bmake_strndup(const char *str, size_t max_len)
+bmake_strldup(const char *str, size_t len)
 {
-	size_t len;
-	char *p;
-
-	if (str == NULL)
-		return NULL;
-
-	len = strlen(str);
-	if (len > max_len)
-		len = max_len;
-	p = bmake_malloc(len + 1);
+	char *p = bmake_malloc(len + 1);
 	memcpy(p, str, len);
 	p[len] = '\0';
-
 	return p;
 }
 
-/*
- * bmake_realloc --
- *	realloc, but die on error.
- */
+/* realloc, but die on error. */
 void *
 bmake_realloc(void *ptr, size_t size)
 {
@@ -117,3 +92,10 @@ bmake_realloc(void *ptr, size_t size)
 	return ptr;
 }
 #endif
+
+/* Allocate a string from start up to but excluding end. */
+char *
+bmake_strsedup(const char *start, const char *end)
+{
+	return bmake_strldup(start, (size_t)(end - start));
+}
diff --git a/make_malloc.h b/make_malloc.h
index 36d3eff3c027..ac804d79c711 100644
--- a/make_malloc.h
+++ b/make_malloc.h
@@ -1,4 +1,4 @@
-/*	$NetBSD: make_malloc.h,v 1.4 2009/01/24 14:43:29 dsl Exp $	*/
+/*	$NetBSD: make_malloc.h,v 1.10 2020/08/29 16:47:45 rillig Exp $	*/
 
 /*-
  * Copyright (c) 2009 The NetBSD Foundation, Inc.
@@ -30,12 +30,25 @@
 void *bmake_malloc(size_t);
 void *bmake_realloc(void *, size_t);
 char *bmake_strdup(const char *);
-char *bmake_strndup(const char *, size_t);
+char *bmake_strldup(const char *, size_t);
 #else
 #include <util.h>
 #define bmake_malloc(x)         emalloc(x)
 #define bmake_realloc(x,y)      erealloc(x,y)
 #define bmake_strdup(x)         estrdup(x)
-#define bmake_strndup(x,y)      estrndup(x,y)
+#define bmake_strldup(x,y)      estrndup(x,y)
 #endif
+char *bmake_strsedup(const char *, const char *);
 
+/* Thin wrapper around free(3) to avoid the extra function call in case
+ * p is NULL, which on x86_64 costs about 12 machine instructions.
+ * Other platforms are similarly affected.
+ *
+ * The case of a NULL pointer happens especially often after Var_Value,
+ * since only environment variables need to be freed, but not others. */
+static inline void MAKE_ATTR_UNUSED
+bmake_free(void *p)
+{
+    if (p != NULL)
+	free(p);
+}
diff --git a/meta.c b/meta.c
index 27f16a0da908..5d87febb0496 100644
--- a/meta.c
+++ b/meta.c
@@ -1,4 +1,4 @@
-/*      $NetBSD: meta.c,v 1.86 2020/07/11 00:39:53 sjg Exp $ */
+/*      $NetBSD: meta.c,v 1.113 2020/09/02 04:08:54 rillig Exp $ */
 
 /*
  * Implement 'meta' mode.
@@ -47,6 +47,7 @@ char * dirname(char *);
 #endif
 
 #include "make.h"
+#include "dir.h"
 #include "job.h"
 
 #ifdef USE_FILEMON
@@ -318,7 +319,7 @@ meta_name(char *mname, size_t mnamelen,
 static int
 is_submake(void *cmdp, void *gnp)
 {
-    static char *p_make = NULL;
+    static const char *p_make = NULL;
     static int p_len;
     char  *cmd = cmdp;
     GNode *gn = gnp;
@@ -333,7 +334,7 @@ is_submake(void *cmdp, void *gnp)
     }
     cp = strchr(cmd, '$');
     if ((cp)) {
-	mp = Var_Subst(NULL, cmd, gn, VARF_WANTRES);
+	mp = Var_Subst(cmd, gn, VARE_WANTRES);
 	cmd = mp;
     }
     cp2 = strstr(cmd, p_make);
@@ -372,13 +373,13 @@ printCMD(void *cmdp, void *mfpp)
 {
     meta_file_t *mfp = mfpp;
     char *cmd = cmdp;
-    char *cp = NULL;
+    char *cmd_freeIt = NULL;
 
     if (strchr(cmd, '$')) {
-	cmd = cp = Var_Subst(NULL, cmd, mfp->gn, VARF_WANTRES);
+	cmd = cmd_freeIt = Var_Subst(cmd, mfp->gn, VARE_WANTRES);
     }
     fprintf(mfp->fp, "CMD %s\n", cmd);
-    free(cp);
+    free(cmd_freeIt);
     return 0;
 }
 
@@ -403,7 +404,7 @@ static Boolean
 meta_needed(GNode *gn, const char *dname,
 	     char *objdir, int verbose)
 {
-    struct stat fs;
+    struct make_stat mst;
 
     if (verbose)
 	verbose = DEBUG(META);
@@ -437,7 +438,7 @@ meta_needed(GNode *gn, const char *dname,
     }
 
     /* The object directory may not exist. Check it.. */
-    if (cached_stat(dname, &fs) != 0) {
+    if (cached_stat(dname, &mst) != 0) {
 	if (verbose)
 	    fprintf(debug_file, "Skipping meta for %s: no .OBJDIR\n",
 		    gn->name);
@@ -488,7 +489,7 @@ meta_create(BuildMon *pbm, GNode *gn)
 	char *mp;
 
 	/* Describe the target we are building */
-	mp = Var_Subst(NULL, "${" MAKE_META_PREFIX "}", gn, VARF_WANTRES);
+	mp = Var_Subst("${" MAKE_META_PREFIX "}", gn, VARE_WANTRES);
 	if (*mp)
 	    fprintf(stdout, "%s\n", mp);
 	free(mp);
@@ -546,7 +547,7 @@ meta_create(BuildMon *pbm, GNode *gn)
     }
  out:
     for (i--; i >= 0; i--) {
-	free(p[i]);
+	bmake_free(p[i]);
     }
 
     return mf.fp;
@@ -629,24 +630,19 @@ meta_mode_init(const char *make_mode)
     /*
      * We consider ourselves master of all within ${.MAKE.META.BAILIWICK}
      */
-    metaBailiwick = Lst_Init(FALSE);
-    metaBailiwickStr = Var_Subst(NULL, "${.MAKE.META.BAILIWICK:O:u:tA}",
-	VAR_GLOBAL, VARF_WANTRES);
-    if (metaBailiwickStr) {
-	str2Lst_Append(metaBailiwick, metaBailiwickStr, NULL);
-    }
+    metaBailiwick = Lst_Init();
+    metaBailiwickStr = Var_Subst("${.MAKE.META.BAILIWICK:O:u:tA}",
+	VAR_GLOBAL, VARE_WANTRES);
+    str2Lst_Append(metaBailiwick, metaBailiwickStr, NULL);
     /*
      * We ignore any paths that start with ${.MAKE.META.IGNORE_PATHS}
      */
-    metaIgnorePaths = Lst_Init(FALSE);
+    metaIgnorePaths = Lst_Init();
     Var_Append(MAKE_META_IGNORE_PATHS,
 	       "/dev /etc /proc /tmp /var/run /var/tmp ${TMPDIR}", VAR_GLOBAL);
-    metaIgnorePathsStr = Var_Subst(NULL,
-		   "${" MAKE_META_IGNORE_PATHS ":O:u:tA}", VAR_GLOBAL,
-		   VARF_WANTRES);
-    if (metaIgnorePathsStr) {
-	str2Lst_Append(metaIgnorePaths, metaIgnorePathsStr, NULL);
-    }
+    metaIgnorePathsStr = Var_Subst("${" MAKE_META_IGNORE_PATHS ":O:u:tA}",
+				   VAR_GLOBAL, VARE_WANTRES);
+    str2Lst_Append(metaIgnorePaths, metaIgnorePathsStr, NULL);
 
     /*
      * We ignore any paths that match ${.MAKE.META.IGNORE_PATTERNS}
@@ -654,12 +650,12 @@ meta_mode_init(const char *make_mode)
     cp = NULL;
     if (Var_Value(MAKE_META_IGNORE_PATTERNS, VAR_GLOBAL, &cp)) {
 	metaIgnorePatterns = TRUE;
-	free(cp);
+	bmake_free(cp);
     }
     cp = NULL;
     if (Var_Value(MAKE_META_IGNORE_FILTER, VAR_GLOBAL, &cp)) {
 	metaIgnoreFilter = TRUE;
-	free(cp);
+	bmake_free(cp);
     }
 }
 
@@ -822,8 +818,8 @@ meta_job_output(Job *job, char *cp, const char *nl)
 	    if (!meta_prefix) {
 		char *cp2;
 
-		meta_prefix = Var_Subst(NULL, "${" MAKE_META_PREFIX "}",
-					VAR_GLOBAL, VARF_WANTRES);
+		meta_prefix = Var_Subst("${" MAKE_META_PREFIX "}",
+					VAR_GLOBAL, VARE_WANTRES);
 		if ((cp2 = strchr(meta_prefix, '$')))
 		    meta_prefix_len = cp2 - meta_prefix;
 		else
@@ -894,9 +890,11 @@ meta_job_finish(Job *job)
 void
 meta_finish(void)
 {
-    Lst_Destroy(metaBailiwick, NULL);
+    if (metaBailiwick != NULL)
+	Lst_Free(metaBailiwick);
     free(metaBailiwickStr);
-    Lst_Destroy(metaIgnorePaths, NULL);
+    if (metaIgnorePaths != NULL)
+	Lst_Free(metaIgnorePaths);
     free(metaIgnorePathsStr);
 }
 
@@ -958,39 +956,23 @@ prefix_match(void *p, void *q)
     return strncmp(path, prefix, n) == 0;
 }
 
-/*
- * looking for exact or prefix/ match to
- * Lst_Find wants 0 to stop search
- */
-static int
+/* See if the path equals prefix or starts with "prefix/". */
+static Boolean
 path_match(const void *p, const void *q)
 {
-    const char *prefix = q;
     const char *path = p;
+    const char *prefix = q;
     size_t n = strlen(prefix);
-    int rc;
 
-    if ((rc = strncmp(path, prefix, n)) == 0) {
-	switch (path[n]) {
-	case '\0':
-	case '/':
-	    break;
-	default:
-	    rc = 1;
-	    break;
-	}
-    }
-    return rc;
+    if (strncmp(path, prefix, n) != 0)
+        return FALSE;
+    return path[n] == '\0' || path[n] == '/';
 }
 
-/* Lst_Find wants 0 to stop search */
-static int
+static Boolean
 string_match(const void *p, const void *q)
 {
-    const char *p1 = p;
-    const char *p2 = q;
-
-    return strcmp(p1, p2);
+    return strcmp(p, q) == 0;
 }
 
 
@@ -1015,12 +997,12 @@ meta_ignore(GNode *gn, const char *p)
     }
 
     if (metaIgnorePatterns) {
+	const char *expr;
 	char *pm;
 
 	Var_Set(".p.", p, gn);
-	pm = Var_Subst(NULL,
-		       "${" MAKE_META_IGNORE_PATTERNS ":@m@${.p.:M$m}@}",
-		       gn, VARF_WANTRES);
+	expr = "${" MAKE_META_IGNORE_PATTERNS ":@m@${.p.:M$m}@}";
+	pm = Var_Subst(expr, gn, VARE_WANTRES);
 	if (*pm) {
 #ifdef DEBUG_META_MODE
 	    if (DEBUG(META))
@@ -1040,7 +1022,7 @@ meta_ignore(GNode *gn, const char *p)
 	snprintf(fname, sizeof(fname),
 		 "${%s:L:${%s:ts:}}",
 		 p, MAKE_META_IGNORE_FILTER);
-	fm = Var_Subst(NULL, fname, gn, VARF_WANTRES);
+	fm = Var_Subst(fname, gn, VARE_WANTRES);
 	if (*fm == '\0') {
 #ifdef DEBUG_META_MODE
 	    if (DEBUG(META))
@@ -1122,7 +1104,7 @@ meta_oodate(GNode *gn, Boolean oodate)
 	goto oodate_out;
     dname = fname3;
 
-    missingFiles = Lst_Init(FALSE);
+    missingFiles = Lst_Init();
 
     /*
      * We need to check if the target is out-of-date. This includes
@@ -1147,7 +1129,7 @@ meta_oodate(GNode *gn, Boolean oodate)
 	int pid;
 	int x;
 	LstNode ln;
-	struct stat fs;
+	struct make_stat mst;
 
 	if (!buf) {
 	    bufsz = 8 * BUFSIZ;
@@ -1240,7 +1222,7 @@ meta_oodate(GNode *gn, Boolean oodate)
 		    CHECK_VALID_META(p);
 		    pid = atoi(p);
 		    if (pid > 0 && pid != lastpid) {
-			char *ldir;
+			const char *ldir;
 			char *tp;
 
 			if (lastpid > 0) {
@@ -1254,12 +1236,12 @@ meta_oodate(GNode *gn, Boolean oodate)
 			ldir = Var_Value(ldir_vname, VAR_GLOBAL, &tp);
 			if (ldir) {
 			    strlcpy(latestdir, ldir, sizeof(latestdir));
-			    free(tp);
+			    bmake_free(tp);
 			}
 			ldir = Var_Value(lcwd_vname, VAR_GLOBAL, &tp);
 			if (ldir) {
 			    strlcpy(lcwd, ldir, sizeof(lcwd));
-			    free(tp);
+			    bmake_free(tp);
 			}
 		    }
 		    /* Skip past the pid. */
@@ -1337,15 +1319,16 @@ meta_oodate(GNode *gn, Boolean oodate)
 		case 'D':		/* unlink */
 		    if (*p == '/' && !Lst_IsEmpty(missingFiles)) {
 			/* remove any missingFiles entries that match p */
-			if ((ln = Lst_Find(missingFiles, p,
-					   path_match)) != NULL) {
+			ln = Lst_Find(missingFiles, path_match, p);
+			if (ln != NULL) {
 			    LstNode nln;
 			    char *tp;
 
 			    do {
-				nln = Lst_FindFrom(missingFiles, Lst_Succ(ln),
-						   p, path_match);
-				tp = Lst_Datum(ln);
+				nln = Lst_FindFrom(missingFiles,
+						   LstNode_Next(ln),
+						   path_match, p);
+				tp = LstNode_Datum(ln);
 				Lst_Remove(missingFiles, ln);
 				free(tp);
 			    } while ((ln = nln) != NULL);
@@ -1411,11 +1394,11 @@ meta_oodate(GNode *gn, Boolean oodate)
 		    if ((strstr("tmp", p)))
 			break;
 
-		    if ((link_src != NULL && cached_lstat(p, &fs) < 0) ||
-			(link_src == NULL && cached_stat(p, &fs) < 0)) {
+		    if ((link_src != NULL && cached_lstat(p, &mst) < 0) ||
+			(link_src == NULL && cached_stat(p, &mst) < 0)) {
 			if (!meta_ignore(gn, p)) {
-			    if (Lst_Find(missingFiles, p, string_match) == NULL)
-				Lst_AtEnd(missingFiles, bmake_strdup(p));
+			    if (Lst_Find(missingFiles, string_match, p) == NULL)
+				Lst_Append(missingFiles, bmake_strdup(p));
 			}
 		    }
 		    break;
@@ -1475,7 +1458,7 @@ meta_oodate(GNode *gn, Boolean oodate)
 			    if (DEBUG(META))
 				fprintf(debug_file, "%s: %d: looking for: %s\n", fname, lineno, *sdp);
 #endif
-			    if (cached_stat(*sdp, &fs) == 0) {
+			    if (cached_stat(*sdp, &mst) == 0) {
 				found = 1;
 				p = *sdp;
 			    }
@@ -1485,12 +1468,12 @@ meta_oodate(GNode *gn, Boolean oodate)
 			    if (DEBUG(META))
 				fprintf(debug_file, "%s: %d: found: %s\n", fname, lineno, p);
 #endif
-			    if (!S_ISDIR(fs.st_mode) &&
-				fs.st_mtime > gn->mtime) {
+			    if (!S_ISDIR(mst.mst_mode) &&
+				mst.mst_mtime > gn->mtime) {
 				if (DEBUG(META))
 				    fprintf(debug_file, "%s: %d: file '%s' is newer than the target...\n", fname, lineno, p);
 				oodate = TRUE;
-			    } else if (S_ISDIR(fs.st_mode)) {
+			    } else if (S_ISDIR(mst.mst_mode)) {
 				/* Update the latest directory. */
 				cached_realpath(p, latestdir);
 			    }
@@ -1500,8 +1483,8 @@ meta_oodate(GNode *gn, Boolean oodate)
 			     * A referenced file outside of CWD is missing.
 			     * We cannot catch every eventuality here...
 			     */
-			    if (Lst_Find(missingFiles, p, string_match) == NULL)
-				    Lst_AtEnd(missingFiles, bmake_strdup(p));
+			    if (Lst_Find(missingFiles, string_match, p) == NULL)
+				Lst_Append(missingFiles, bmake_strdup(p));
 			}
 		    }
 		    if (buf[0] == 'E') {
@@ -1524,7 +1507,7 @@ meta_oodate(GNode *gn, Boolean oodate)
 			fprintf(debug_file, "%s: %d: there were more build commands in the meta data file than there are now...\n", fname, lineno);
 		    oodate = TRUE;
 		} else {
-		    char *cmd = (char *)Lst_Datum(ln);
+		    char *cmd = LstNode_Datum(ln);
 		    Boolean hasOODATE = FALSE;
 
 		    if (strstr(cmd, "$?"))
@@ -1539,7 +1522,7 @@ meta_oodate(GNode *gn, Boolean oodate)
 			if (DEBUG(META))
 			    fprintf(debug_file, "%s: %d: cannot compare command using .OODATE\n", fname, lineno);
 		    }
-		    cmd = Var_Subst(NULL, cmd, gn, VARF_WANTRES|VARF_UNDEFERR);
+		    cmd = Var_Subst(cmd, gn, VARE_WANTRES|VARE_UNDEFERR);
 
 		    if ((cp = strchr(cmd, '\n'))) {
 			int n;
@@ -1576,7 +1559,7 @@ meta_oodate(GNode *gn, Boolean oodate)
 			    oodate = TRUE;
 		    }
 		    free(cmd);
-		    ln = Lst_Succ(ln);
+		    ln = LstNode_Next(ln);
 		}
 	    } else if (strcmp(buf, "CWD") == 0) {
 		/*
@@ -1601,7 +1584,7 @@ meta_oodate(GNode *gn, Boolean oodate)
 	if (!Lst_IsEmpty(missingFiles)) {
 	    if (DEBUG(META))
 		fprintf(debug_file, "%s: missing files: %s...\n",
-			fname, (char *)Lst_Datum(Lst_First(missingFiles)));
+			fname, (char *)LstNode_Datum(Lst_First(missingFiles)));
 	    oodate = TRUE;
 	}
 	if (!oodate && !have_filemon && filemonMissing) {
@@ -1628,7 +1611,7 @@ meta_oodate(GNode *gn, Boolean oodate)
 	}
     }
 
-    Lst_Destroy(missingFiles, (FreeProc *)free);
+    Lst_Destroy(missingFiles, free);
 
     if (oodate && needOODATE) {
 	/*
@@ -1638,12 +1621,12 @@ meta_oodate(GNode *gn, Boolean oodate)
 	 */
 	Var_Delete(OODATE, gn);
 	Var_Set(OODATE, Var_Value(ALLSRC, gn, &cp), gn);
-	free(cp);
+	bmake_free(cp);
     }
 
  oodate_out:
     for (i--; i >= 0; i--) {
-	free(pa[i]);
+	bmake_free(pa[i]);
     }
     return oodate;
 }
diff --git a/metachar.c b/metachar.c
index 49603383e660..971962272a4a 100644
--- a/metachar.c
+++ b/metachar.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: metachar.c,v 1.5 2015/06/19 08:03:35 mlelstv Exp $	*/
+/*	$NetBSD: metachar.c,v 1.6 2020/08/03 20:43:41 rillig Exp $	*/
 
 /*-
  * Copyright (c) 2015 The NetBSD Foundation, Inc.
@@ -38,7 +38,7 @@
 #endif
 
 #if defined(__RCSID) && !defined(lint)
-__RCSID("$NetBSD: metachar.c,v 1.5 2015/06/19 08:03:35 mlelstv Exp $");
+__RCSID("$NetBSD: metachar.c,v 1.6 2020/08/03 20:43:41 rillig Exp $");
 #endif
 
 #include "metachar.h"
@@ -52,37 +52,37 @@ __RCSID("$NetBSD: metachar.c,v 1.5 2015/06/19 08:03:35 mlelstv Exp $");
  */
 
 unsigned char _metachar[128] = {
-//    nul   soh   stx   etx   eot   enq   ack   bel
+/*    nul   soh   stx   etx   eot   enq   ack   bel */
 	1,    0,    0,    0,    0,    0,    0,    0,
-//     bs    ht    nl    vt    np    cr    so    si
+/*     bs    ht    nl    vt    np    cr    so    si */
 	0,    0,    1,    0,	0,    0,    0,    0,
-//    dle   dc1   dc2   dc3   dc4   nak   syn   etb
+/*    dle   dc1   dc2   dc3   dc4   nak   syn   etb */
 	0,    0,    0,    0,    0,    0,    0,    0,
-//    can    em   sub   esc    fs    gs    rs    us
+/*    can    em   sub   esc    fs    gs    rs    us */
 	0,    0,    0,    0,    0,    0,    0,    0,
-//     sp     !     "     #     $     %     &     '
+/*     sp     !     "     #     $     %     &     ' */
 	0,    1,    1,    1,    1,    0,    1,    1,
-//      (     )     *     +     ,     -     .     /
+/*      (     )     *     +     ,     -     .     / */
 	1,    1,    1,    0,    0,    0,    0,    0,
-//      0     1     2     3     4     5     6     7
+/*      0     1     2     3     4     5     6     7 */
 	0,    0,    0,    0,    0,    0,    0,    0,
-//      8     9     :     ;     <     =     >     ?
+/*      8     9     :     ;     <     =     >     ? */
 	0,    0,    0,    1,    1,    0,    1,    1,
-//      @     A     B     C     D     E     F     G
+/*      @     A     B     C     D     E     F     G */
 	0,    0,    0,    0,    0,    0,    0,    0,
-//      H     I     J     K     L     M     N     O
+/*      H     I     J     K     L     M     N     O */
 	0,    0,    0,    0,    0,    0,    0,    0,
-//      P     Q     R     S     T     U     V     W
+/*      P     Q     R     S     T     U     V     W */
 	0,    0,    0,    0,    0,    0,    0,    0,
-//      X     Y     Z     [     \     ]     ^     _
+/*      X     Y     Z     [     \     ]     ^     _ */
 	0,    0,    0,    1,    1,    1,    1,    0,
-//      `     a     b     c     d     e     f     g
+/*      `     a     b     c     d     e     f     g */
 	1,    0,    0,    0,    0,    0,    0,    0,
-//      h     i     j     k     l     m     n     o
+/*      h     i     j     k     l     m     n     o */
 	0,    0,    0,    0,    0,    0,    0,    0,
-//      p     q     r     s     t     u     v     w
+/*      p     q     r     s     t     u     v     w */
 	0,    0,    0,    0,    0,    0,    0,    0,
-//      x     y     z     {     |     }     ~   del
+/*      x     y     z     {     |     }     ~   del */
 	0,    0,    0,    1,    1,    1,    1,    0,
 };
 
diff --git a/metachar.h b/metachar.h
index db88d671067e..98408ab3fb58 100644
--- a/metachar.h
+++ b/metachar.h
@@ -1,4 +1,4 @@
-/*	$NetBSD: metachar.h,v 1.4 2015/06/21 20:26:02 christos Exp $	*/
+/*	$NetBSD: metachar.h,v 1.7 2020/08/25 17:37:09 rillig Exp $	*/
 
 /*-
  * Copyright (c) 2015 The NetBSD Foundation, Inc.
@@ -28,25 +28,16 @@
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  * POSSIBILITY OF SUCH DAMAGE.
  */
-#ifndef _METACHAR_H
-#define _METACHAR_H
+#ifndef MAKE_METACHAR_H
+#define MAKE_METACHAR_H
 
-#include <ctype.h>
+#include "make.h"
 
 extern unsigned char _metachar[];
 
 #define ismeta(c)	_metachar[(c) & 0x7f]
 
-static inline int
-hasmeta(const char *cmd)
-{
-	while (!ismeta(*cmd))
-		cmd++;
-
-	return *cmd != '\0';
-}
-
-static inline int
+static inline int MAKE_ATTR_UNUSED
 needshell(const char *cmd, int white)
 {
 	while (!ismeta(*cmd) && *cmd != ':' && *cmd != '=') {
@@ -58,4 +49,4 @@ needshell(const char *cmd, int white)
 	return *cmd != '\0';
 }
 
-#endif /* _METACHAR_H */
+#endif /* MAKE_METACHAR_H */
diff --git a/mk/ChangeLog b/mk/ChangeLog
index ca42bc2839c5..9ea9d42a7776 100644
--- a/mk/ChangeLog
+++ b/mk/ChangeLog
@@ -1,3 +1,106 @@
+2020-08-26  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* dirdeps.mk: ensure we cannot confuse a static cache for dynamic
+	(even more rare that use of static cache is playing clever tricks
+	with it)
+
+2020-08-16  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* dirdeps-cache-update.mk: allow
+	MK_STATIC_DIRDEPS_CACHE_UPDATE_IMMEDIATE to control when we
+	actually update STATIC_DIRDEPS_CACHE.
+
+	* stage-install.sh: create dest directory if needed
+	before running install(1)
+
+2020-08-10  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* dirdeps-targets.mk: include Makefile.dirdeps.options
+
+	* dirdeps.mk: use _TARGETS if defined for DIRDEPS_CACHE
+
+2020-08-09  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* dirdeps.mk: default BUILD_DIRDEPS_MAKEFILE to empty
+
+	* dirdeps-cache-update.mk: building parallel cache update
+	under the context of dirdeps-cached would be ideal, but
+	is problematic, so it runs as a sibling.
+	Use cache-built target to ensure we wait for it to complete if
+	necessary.
+
+2020-08-06  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* install-mk (MK_VERSION): 20200806
+
+	* dirdeps-options: allow TARGET_SPEC to affect option values.
+	Use DIRDEPS_OPTIONS_QUALIFIER_LIST before using bare MK_*
+
+	* dirdeps-targets.mk: check for MK_STATIC_DIRDEPS_CACHE defined
+	before looking for STATIC_DIRDEPS_CACHE
+
+2020-08-05  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* host-target.mk: Darwin use MACHINE for HOST_ARCH too
+
+	* dirdeps-options.mk: improve debug output
+
+2020-07-22  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* dirdeps.mk: set and export DYNAMIC_DIRDEPS_CACHE
+	for use by dirdeps-cache-update.mk
+
+	* dirdeps-targets.mk: set and export STATIC_DIRDEPS_CACHE
+	for use by dirdeps-cache-update.mk even if we don't use it.
+
+	* dirdeps-cache-update.mk: we only need worry about the background
+	update case, with the above, the update from DIRDEPS_CACHE is
+	simple.
+
+	* meta2deps.py: R 1234 . is not interesting
+
+2020-07-20  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* sys.mk: default MK_STATIC_DIRDEPS_CACHE from MK_DIRDEPS_CACHE
+
+	* dirdeps-options.mk: do not :tu DIRDEPS_OPTIONS
+	allows use of lower case for pseudo options.
+
+	* dirdeps-cache-update.mk: magic to deal with STATIC_DIRDEPS_CACHE
+
+2020-07-18  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* dirdeps-targets.mk: Look for Makefile.dirdeps.cache
+	which allows us to have a static cache for expensive targets.
+	Use -DWITHOUT_STATIC_DIRDEPS_CACHE -DWITH_DIRDEPS_CACHE
+	to regenerate the dirdeps.cache it is a copy of.
+
+2020-07-17  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* Get rid of BUILD_AT_LEVEL0, MK_DIRDEPS_BUILD makes more sense.
+
+2020-07-16  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* dirdeps.mk (DIRDEP_LOADAVG_REPORT): make it easy to record
+	load averages at intervals during build.
+
+2020-07-15  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* install-mk (MK_VERSION): 20200715
+
+	* dirdeps.mk: tweak Checking line to make matching Finished
+	lines for post-build analysis easier.
+
+	* meta.autodep.mk: use !defined(WITHOUT_META_STATS)
+
+	* progs.mk: avoid prog.mk outputting multiple Finished lines
+
+2020-07-11  Simon J Gerraty  <sjg@beast.crufty.net>
+
+	* dirdeps.mk: further optimize dirdeps.cache
+	generate a DIRDEPS.${.TARGET} list for other purposes
+	and improve the layout.
+
 2020-07-10  Simon J Gerraty  <sjg@beast.crufty.net>
 
 	* dirdeps.mk: optimize content of dirdeps.cache
@@ -124,7 +227,7 @@
 	* install-mk (MK_VERSION): 20180919
 
 	* dirdeps-options.mk: .undef cannot handle var that expands to
-	  more than one var. 
+	  more than one var.
 
 2018-07-08  Simon J Gerraty  <sjg@beast.crufty.net>
 
@@ -309,8 +412,8 @@
 2016-12-12  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20161212
-	
-	* meta2deps.py: set pid_cwd[pid] when we process 'C'hdir, 
+
+	* meta2deps.py: set pid_cwd[pid] when we process 'C'hdir,
 	rather than when we detect pid change.
 
 2016-12-07  Simon J. Gerraty  <sjg@bad.crufty.net>
@@ -322,7 +425,7 @@
 	  to use foo.tgz to reference the latest staged version - so we
 	  make foo.tgz a symlink to it.
 	  Using a target to do both operations ensures we stay in sync.
-	
+
 2016-11-26  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20161126
@@ -374,18 +477,18 @@
 
 2016-08-13  Simon J. Gerraty  <sjg@bad.crufty.net>
 
-	* meta.sys.mk (.MAKE.META.IGNORE_PATHS): 
+	* meta.sys.mk (.MAKE.META.IGNORE_PATHS):
 	  in meta mode we can ignore the mtime of makefiles
 
 2016-08-02  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20160802
-	
+
 	* lib.mk (libinstall): depends on beforinstall
 
 	* prog.mk (proginstall): depends on beforinstall
 	  patch from Lauri Tirkkonen
-	
+
 	* dirdeps.mk (bootstrap): When bootstrapping; creat
 	.MAKE.DEPENDFILE_DEFAULT and allow additional filtering via
 	.MAKE.DEPENDFILE_BOOTSTRAP_SED
@@ -408,7 +511,7 @@
 	* install-mk (MK_VERSION): 20160530
 	* meta.stage.mk: we assume ${CLEANFILES} gets .NOPATH
 	  make it so.
-	
+
 2016-05-12  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20160512
@@ -418,7 +521,7 @@
 	  skip INCLUDES_* for staged libs unless SRC_* defined.
 
 	* own.mk: add INCLUDEDIR
-	
+
 2016-04-18  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* dirdeps.mk: when doing -f dirdeps.mk if target suppies no
@@ -428,9 +531,9 @@
 2016-04-07  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* meta.autodep.mk: issue a warning if UPDATE_DEPENDFILE=NO due to
-	  NO_FILEMON_COOKIE  
+	  NO_FILEMON_COOKIE
 
-	* dirdeps.mk: move the logic that allows for 
+	* dirdeps.mk: move the logic that allows for
 	  make -f dirdeps.mk some/dir.${TARGET_SPEC}
 	  inside the check for !target(_DIRDEP_USE)
 
@@ -438,15 +541,15 @@
 
 	* Use <> when including local*.mk and others which may exist
 	  elsewhere so that user can better control what they get.
-	
-	* meta.autodep.mk (NO_FILEMON_COOKIE): 
+
+	* meta.autodep.mk (NO_FILEMON_COOKIE):
 	  create a cookie if we ever build dir with nofilemon
 	  so that UPDATE_DEPENDFILE will be forced to NO until cleaned.
 
 2016-04-01  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20160401
-	
+
 	* meta2deps.py: fix old print statement when debugging.
 
 	* gendirdeps.mk: META2DEPS_CMD append M2D_EXCLUDES with -X
@@ -455,12 +558,12 @@
 2016-03-22  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20160317 (St. Pats)
-	
+
 	* warnings.mk: g++ does not like -Wimplicit
-	
+
 	* sys.mk sys/*.mk lib.mk prog.mk: use CXX_SUFFIXES to handle the
 	  pelthora of common suffixes for C++
-	
+
 	* lib.mk: use .So for shared objects
 
 2016-03-15  Simon J. Gerraty  <sjg@bad.crufty.net>
@@ -469,9 +572,9 @@
 
 	* meta.stage.mk (LN_CP_SCRIPT): do not ln(1) if we have to chmod(1)
 	  normally only applies to scripts.
-	
+
 	* dirdeps.mk: NO_DIRDEPS_BELOW to supress DIRDEPS below RELDIR as
-	  well as outside it. 
+	  well as outside it.
 
 2016-03-10  Simon J. Gerraty  <sjg@bad.crufty.net>
 
@@ -509,17 +612,17 @@
 	  we cannot use the '$$' trick, but .export-literal does the job
 	  we need.
 	* auto.dep.mk: make use .dinclude if we can.
-	
+
 
 2016-02-05  Simon J. Gerraty  <sjg@bad.crufty.net>
 
-	* dirdeps.mk: 
+	* dirdeps.mk:
 	  Add _build_all_dirs such that local.dirdeps.mk can
 	  add fully qualified dirs to it.
-	  These will be built normally but the current 
+	  These will be built normally but the current
 	  DEP_RELDIR will not depend on then (to avoid cycles).
 	  This makes it easy to hook things like unit-tests into build.
-	
+
 
 2016-01-21  Simon J. Gerraty  <sjg@bad.crufty.net>
 
@@ -541,7 +644,7 @@
 	  to set MK_AUTO_OBJ and MK_DIRDEPS_BUILD
 	  include local.sys.env.mk early
 	  include local.sys.mk later
-	
+
 	* own.mk (OPTIONS_DEFAULT_NO): AUTO_OBJ etc moved to sys.mk
 
 2015-11-13  Simon J. Gerraty  <sjg@bad.crufty.net>
@@ -560,8 +663,8 @@
 2015-10-20  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20151020
-	
-	* dirdeps.mk: Add logic for 
+
+	* dirdeps.mk: Add logic for
 	  make -f dirdeps.mk some/dir.${TARGET_SPEC}
 
 2015-10-14  Simon J. Gerraty  <sjg@bad.crufty.net>
@@ -587,16 +690,16 @@
 2015-06-15  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20150615
-	
+
 	* auto.obj.mk: allow use of MAKEOBJDIRPREFIX too.
 	  Follow make's normal precedence rules.
-	
+
 	* gendirdeps.mk: allow customization of the header.
-	  eg. for FreeBSD: 
+	  eg. for FreeBSD:
 	  GENDIRDEPS_HEADER= echo '\# ${FreeBSD:L:@v@$$$v$$ @:M*F*}';
 
 	* meta.autodep.mk: ignore dirdeps.cache*
-	
+
 	* meta.stage.mk: when bootstrapping options it can be handy to
 	  throw warnings rather than errors for staging conflicts.
 
@@ -605,11 +708,11 @@
 2015-06-06  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20150606
-	
+
 	* dirdeps.mk: don't rely on manually maintained Makefile.depend
 	  to set DEP_RELDIR and reset DIRDEPS.
 	  By setting DEP_RELDIR ourselves we can skip :tA
-	
+
 	* gendirdeps.mk: skip setting DEP_RELDIR.
 
 2015-05-24  Simon J. Gerraty  <sjg@bad.crufty.net>
@@ -626,19 +729,19 @@
 
 	* meta.stage.mk: for STAGE_AS_* basename of file may not be unique
 	  so first use absolute path as key.
-	  Also skip staging at level 0. 
+	  Also skip staging at level 0.
 
 2015-04-30  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20150430
-	
+
 	* dirdeps.mk: fix _count_dirdeps for non-cache case.
 
 2015-04-16  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20150411
 	  bump version
-	
+
 	* own.mk: put AUTO_OBJ in OPTIONS_DEFAULT_NO rather than YES.
 	  it is here mainly for documentation purposes, since
 	  if using auto.obj.mk it is better done via sys.mk
@@ -646,21 +749,21 @@
 2015-04-01  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20150401
-	
+
 	* meta2deps.sh: support @list
-	
+
 	* meta2deps.py: updates from Juniper
-	  o add EXCLUDES 
+	  o add EXCLUDES
 	  o skip bogus input files.
 	  o treat 'M' and 'L' as both an 'R' and a 'W'
 
 2015-03-03  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20150303
-	
+
 	* dirdeps.mk: if MK_DIRDEPS_CACHE is yes, use dirdeps-cache
 	  which is built via sub-make so we have a .meta file to tell if
-	  it is out-of-date. 
+	  it is out-of-date.
 	  The dirdeps-cache contains the same dependency rules that we
 	  normaly construct on the fly.
 	  This adds a few seconds overhead when the cache is out of date,
@@ -669,15 +772,15 @@
 2014-11-18  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20141118
-	
+
 	* meta.stage.mk: add stale_staged
-	
+
 	* dirdeps.mk (_DIRDEP_USE_LEVEL): allow this to be tweaked
 	  only useful under very rare conditions such as
 	  FreeBSD's make universe.
 
 	* auto.obj.mk: Allow MK_AUTO_OBJ to set MKOBJDIRS=auto
-	
+
 2014-11-11  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20141111
@@ -695,24 +798,24 @@
 	  for GENDIRDEPS_FILTER to avoid surprises.
 
 2014-10-10  Simon J. Gerraty  <sjg@bad.crufty.net>
-	
+
 	* dirdeps.mk (NSkipHostDir): this needs SRCTOP prepended since by
 	  the time it is applied to __depdirs they have.
-	
+
 	* dirdeps.mk fix filtering of _machines since M_dep_qual_fixes
 	  expects patterns like *.${MACHINE}
-	
+
 	* cython.mk (pyprefix?): use pyprefix to find python bits
 	  since prefix might be something else (where we install our
 	  stuff)
-	
+
 2014-09-11  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20140911
-	
+
 	* dirdeps.mk: add bootstrap target to simplify adding support for
 	  new MACHINE.
-	
+
 2014-09-01  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* gendirdeps.mk: Add handling of GENDIRDEPS_FILTER_DIR_VARS and
@@ -722,7 +825,7 @@
 2014-08-28  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20140828
-	
+
 	* cython.mk: capture logic for building python extension modules
 	  with Cython.
 
@@ -735,8 +838,8 @@
 	* install-mk (MK_VERSION): 20140801
 
 	* dep.mk: use explicit MKDEP_MK rather than overload MKDEP to
-	identify the autodep.mk variant. 
-	
+	identify the autodep.mk variant.
+
 	* sys.dependfile.mk: delete .MAKE.DEPENDFILE if its
 	initial value does not match .MAKE.DEPENDFILE_PREFIX
 
@@ -774,7 +877,7 @@
 	  build).
 
 	* dirdeps.mk (__depdirs): ensure // don't sneak in
-	
+
 	* gendirdeps.mk (DIRDEPS): ensure // don't sneak in
 
 
@@ -794,7 +897,7 @@
 
 2014-02-09  Simon J. Gerraty  <sjg@bad.crufty.net>
 
-	* options.mk: cleanup and simplify semanitcs 
+	* options.mk: cleanup and simplify semanitcs
 	  NO_* dominates all, if both WITH_* and WITHOUT_*
 	  are defined then result is DOMINATE_* which defaults to "no".
 	  Ie. WITHOUT_ normally wins.
@@ -815,7 +918,7 @@
 	  as _build_dirs.
 	  Also fix the filtering of Makefile.depend files - for reporting
 	  what we are looking for (M_dep_qual_fixes can get confused by
-	  Makefile.depend) 
+	  Makefile.depend)
 	  Add some more debug info.
 
 2013-09-04  Simon J. Gerraty  <sjg@bad.crufty.net>
@@ -828,7 +931,7 @@
 
 	* install-mk (MK_VERSION): 20130801
 	* libs.mk: update to match progs.mk
-	
+
 2013-07-26  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20130726
@@ -837,13 +940,13 @@
 	    errors
 	    also allow @file to provide huge list of .meta files.
 	* meta2deps.py: add try_parse() to cleanup the above.
-	
+
 2013-07-16  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20130716
 	* own.mk: add GPROG as an option
 	* prog.mk: honor MK_GPROF==yes
-	
+
 2013-05-10  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): 20130505
@@ -878,7 +981,7 @@
 	* meta.stage.mk (LN_CP_SCRIPT): Add LnCp to do the ln||cp dance
 	  consistently.
 	* dirdeps.mk: better describe the dance in sys.mk for TARGET_SPEC.
-	
+
 2013-03-18  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* gendirdeps.mk: revert the dance around .MAKE.DEPENDFILE_DEFAULT
@@ -893,7 +996,7 @@
 	* gendirdeps.mk: ensure _objroot has trailing / if it needs it.
 	* meta2deps.py: if machine is "host", then also trim
 	  self.host_target from any OBJROOTS.
-	
+
 
 2013-03-11  Simon J. Gerraty  <sjg@bad.crufty.net>
 
@@ -909,7 +1012,7 @@
 
 2013-03-07  Simon J. Gerraty  <sjg@bad.crufty.net>
 
-	* sys.dependfile.mk (.MAKE.DEPENDFILE_DEFAULT): 
+	* sys.dependfile.mk (.MAKE.DEPENDFILE_DEFAULT):
 	  use a separate variable for the default .MAKE.DEPENDFILE value
 	  so that it can be controlled independently of
 	  .MAKE.DEPENDFILE_PREFERENCE
@@ -926,12 +1029,12 @@
 2013-02-10  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20130210
-	* import latest dirdeps.mk, gendirdeps.mk and meta2deps.py 
-	  from Juniper. 
+	* import latest dirdeps.mk, gendirdeps.mk and meta2deps.py
+	  from Juniper.
 	  o dirdeps.mk now fully supports TARGET_SPEC consisting of more
 	    than just MACHINE.
 	  o no longer use DEP_MACHINE from Makefile.depend* so remove it.
-	
+
 2013-01-23  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20130123
@@ -976,13 +1079,13 @@
 	* progs.mk: add MAN and CXXFLAGS to PROG_VARS
 	  also add PROGS_TARGETS and pass on PROG_CXX if it seems
 	  appropriate.
-	
+
 2012-11-04  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* meta.stage.mk: update CLEANFILES
 	  remove redundant cp of .dirdep from STAGE_AS_SCRIPT.
 	* progs.mk: Add LDADD to PROG_VARS
-	
+
 2012-10-12  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* meta.stage.mk (STAGE_DIR_FILTER): track dirs we stage to in
@@ -1004,11 +1107,11 @@
 
 	* install-mk (MK_VERSION): bump version to 20120711
 	* dep.mk: add explicit dependencies on SRCS after applying
-	  SRCS_DEP_FILTER 
+	  SRCS_DEP_FILTER
 	* meta.autodep.mk: add explicit dependencies on SRCS after
 	  applying SRCS_DEP_FILTER
 	* meta.autodep.mk: ensure GENDIRDEPS_FILTER is exported if needed.
-	
+
 2012-06-26  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20120626
@@ -1020,14 +1123,14 @@
 	* gendirdeps.mk: only produce unqualified deps if no
 	  .MAKE.DEPENDFILE_PREFERENCE ends in .${MACHINE}
 	* meta.subdir.mk: apply SUBDIRDEPS_FILTER
-	
+
 2012-04-20  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20120420
 	* add sys.dependfile.mk so we can experiment with
-	  .MAKE.DEPENDFILE_PREFERENCE 
+	  .MAKE.DEPENDFILE_PREFERENCE
 	* meta.autodep.mk: _DEPENDFILE is precious!
-	
+
 2012-03-15  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20120315
@@ -1068,37 +1171,37 @@
 	  o meta2deps.py add a clear 'ERROR:' token if an exception is raised.
 	  o gendirdeps.mk if ERROR: from meta2deps.py do not update
 	    anything.
-	
+
 2011-10-30  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-new.mk separate the cmp and copy logic to its own function.
-	
+
 2011-10-28  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20111028
 	* sys.mk: include auto.obj.mk if MKOBJDIRS is set to auto
 	* subdir.mk: ensure _SUBDIRUSE is provided
-	* meta.autodep.mk: remove dependency of gendirdeps.mk on auto.obj.mk 
+	* meta.autodep.mk: remove dependency of gendirdeps.mk on auto.obj.mk
 	* meta.subdir.mk: always allow for Makefile.depend
-	
+
 2011-10-10  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20111010
 	  o minor tweak to *dirdeps.mk from Juniper sjg@
-	
+
 2011-10-01  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20111001
 	  o add meta2deps.py from Juniper sjg@
 	  o tweak gendirdeps.mk to work with meta2deps.py when not
-	    cross-building 
-	* autoconf.mk: add autoconf-input as a hook for regenerating 
+	    cross-building
+	* autoconf.mk: add autoconf-input as a hook for regenerating
 	  AUTOCONF_INPUTS (configure).
 
 2011-08-24  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* meta.autodep.mk: if we do not have OBJS, .depend isn't a useful
-	  trigger for updating Makefile.depend* 
+	  trigger for updating Makefile.depend*
 
 2011-08-08  Simon J. Gerraty  <sjg@bad.crufty.net>
 
@@ -1155,7 +1258,7 @@
 2011-04-03  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* rst2htm.mk: convert rst to s5 (slides) or plain html depending
-	  on target name. 
+	  on target name.
 
 2011-03-30  Simon J. Gerraty  <sjg@bad.crufty.net>
 
@@ -1167,16 +1270,16 @@
 	  can be used to debug level 0 only and DEBUG_MAKE_FLAGS for the rest.
 	* sys.mk: re-define M_whence in terms of M_type.
 	  M_type is useful for checking if something is a builtin.
-	
+
 2011-03-16  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* meta.stage.mk: add stage_symlinks and leverage StageLinks for
-	  stage_libs 
+	  stage_libs
 
 2011-03-10  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* dirdeps.mk: correct value for _depdir_files depends on
-	  .MAKE.DEPENDFILE 
+	  .MAKE.DEPENDFILE
 	  Add our copyright - just to make it clear we have frobbed this
 	  quite a bit.
 	  DEP_MACHINE needs to be set to MACHINE each time, if using only
@@ -1216,7 +1319,7 @@
 2010-09-24  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20100919
-	include dirdeps.mk et al from Juniper Networks, 
+	include dirdeps.mk et al from Juniper Networks,
 	for meta mode - requires filemon(9).
 	* sys.mk, subdir.mk: Add hooks for meta mode.
 	we do this as meta.sys.mk, meta.autodep.mk and meta.subdir.mk
@@ -1282,7 +1385,7 @@
 	* install-mk (MK_VERSION): bump version to 20100420
 	* sys/NetBSD.mk: add MACHINE_CPU to keep netbsd makefiles happy
 	* autoconf.mk allow AUTO_AUTOCONF
-	
+
 2010-04-19  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* obj.mk: add objwarn to keep freebsd makefiles happy
@@ -1294,7 +1397,7 @@
 	* auto.dep.mk: add some explanation of how/what we do.
 	* autodep.mk: skip the .OPTIONAL frobbing of .depend
 	  bmake's FROM_DEPEND flag makes it redundant.
-	
+
 2010-04-13  Simon J. Gerraty  <sjg@bad.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20100404
@@ -1335,7 +1438,7 @@
 
 	* sys.mk,libnames.mk add .-include <local.*>
 	  this allows local customization without the need to edit the
-	  distributed files. 
+	  distributed files.
 
 2009-12-14  Simon J. Gerraty  <sjg@void.crufty.net>
 
@@ -1361,7 +1464,7 @@
 2009-11-17  Simon J. Gerraty  <sjg@void.crufty.net>
 
 	* install-mk (MK_VERSION): bump version
-	* host-target.mk: only export the expensive stuff 
+	* host-target.mk: only export the expensive stuff
 	* Generic.sys.mk (sys_mk): for SunOS we need to look for
 	  ${HOST_OS}.${HOST_OSMAJOR} too!
 
@@ -1411,7 +1514,7 @@
 
 	* install-mk (MK_VERSION): bump version
 	* general cleanup
-	* dpadd.mk introduce DPMAGIC_LIBS_* 
+	* dpadd.mk introduce DPMAGIC_LIBS_*
 
 2007-04-30  Simon J. Gerraty  <sjg@void.crufty.net>
 
@@ -1453,10 +1556,10 @@
 	* install-mk (MK_VERSION): bump version to 20061126
 
 	* warnings.mk: detect invalid WARNINGS_SET
-	
+
 	* warnings.mk: use ${.TARGET:T:R}.o when looking for target
-	specific warnings. 
-	
+	specific warnings.
+
 	* For .cc sources, turn off warnings that g++ vomits on.
 
 2006-11-08  Simon J. Gerraty  <sjg@void.crufty.net>
@@ -1483,11 +1586,11 @@
 2006-03-01  Simon J. Gerraty  <sjg@void.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20060301
-	* autodep.mk (.depend): 
+	* autodep.mk (.depend):
 	if MAKE_VERSION is newer than  20050530 we can make .END depend on
 	.depend and make .depend depend on __depsrcs that exist.
 	* dpadd.mk: add SRC_PATHADD
-	
+
 2005-11-04  Simon J. Gerraty  <sjg@void.crufty.net>
 
 	* install-mk (MK_VERSION): bump version to 20051104
@@ -1512,7 +1615,7 @@
 2004-02-15  Simon J. Gerraty  <sjg@void.crufty.net>
 
 	* own.mk: don't use NetBSD's _SRC_TOP_ it can
-	cause confusion.  Also don't take just 'mk' as a 
+	cause confusion.  Also don't take just 'mk' as a
 	srctop indicator.
 
 2004-02-14  Simon J. Gerraty  <sjg@void.crufty.net>
@@ -1538,7 +1641,7 @@
 	* set OS and ROOT_GROUP for those that we know the value.
 	for others (eg. Generic.sys.mk) wrap the != in an .ifndef so
 	we don't do it again for each sub-make.
-	
+
 2003-09-28  Simon J. Gerraty  <sjg@void.crufty.net>
 
 	* install-mk (MK_VERSION): 20030928
@@ -1554,8 +1657,8 @@
 
 2003-07-31  Simon J. Gerraty  <sjg@void.crufty.net>
 
-	* install-mk: add ability to use cp -f when updating 
-	destination .mk files.  Also now possible to play games with 
+	* install-mk: add ability to use cp -f when updating
+	destination .mk files.  Also now possible to play games with
 	FORCE_SYS_MK=ln etc on *BSD machines to link /usr/share/mk/sys.mk
 	into dest - not recommended unless you seriously want to.
 
@@ -1575,4 +1678,4 @@
 	* install-mk: Allow FORCE_SYS_MK to come from env
 
 
-	
+
diff --git a/mk/FILES b/mk/FILES
index 360fb613a15e..51b1acd716b2 100644
--- a/mk/FILES
+++ b/mk/FILES
@@ -61,6 +61,7 @@ warnings.mk
 whats.mk
 yacc.mk
 dirdeps.mk
+dirdeps-cache-update.mk
 dirdeps-options.mk
 dirdeps-targets.mk
 gendirdeps.mk
diff --git a/mk/README b/mk/README
index 3d79b6a2de7d..161426cfcd0c 100644
--- a/mk/README
+++ b/mk/README
@@ -1,10 +1,10 @@
-#	$Id: README,v 1.1 1997/03/11 07:27:15 sjg Exp $
+#	$Id: README,v 1.2 2020/08/19 17:51:53 sjg Exp $
 
 This directory contains some macro's derrived from the NetBSD bsd.*.mk
 macros.  They have the same names but without the bsd., separate macro
 files are needed to ensure we can make them do what we want for
 builing things outside of /usr/src.  Nearly all the comments below
-apply. 
+apply.
 
 #	$NetBSD: bsd.README,v 1.18 1997/01/13 00:54:23 mark Exp $
 #	@(#)bsd.README	5.1 (Berkeley) 5/11/90
@@ -347,9 +347,9 @@ If foo has multiple source files, add the line:
 =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
 
 The include file <bsd.subdir.mk> contains the default targets for building
-subdirectories.  It has the same eight targets as <bsd.prog.mk>: all, 
+subdirectories.  It has the same eight targets as <bsd.prog.mk>: all,
 clean, cleandir, depend, includes, install, lint, and tags.  For all of
-the directories listed in the variable SUBDIRS, the specified directory 
+the directories listed in the variable SUBDIRS, the specified directory
 will be visited and the target made.  There is also a default target which
 allows the command "make subdir" where subdir is any directory listed in
 the variable SUBDIRS.
diff --git a/mk/auto.dep.mk b/mk/auto.dep.mk
index 55b2971be6e0..d905649ab206 100644
--- a/mk/auto.dep.mk
+++ b/mk/auto.dep.mk
@@ -1,16 +1,16 @@
 #
 # RCSid:
-#	$Id: auto.dep.mk,v 1.5 2016/04/05 15:58:37 sjg Exp $
+#	$Id: auto.dep.mk,v 1.6 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2010, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -20,11 +20,11 @@
 
 # set MKDEP_MK=auto.dep.mk and dep.mk will include us
 
-# This version differs from autodep.mk, in that 
+# This version differs from autodep.mk, in that
 # we use ${.TARGET:T}.d rather than ${.TARGET:T:R}.d
 # this makes it simpler to get the args to -MF and -MT right
 # and ensure we can simply include all the .d files.
-# 
+#
 # However suffix rules do not work with something like .o.d so we
 # don't even try to handle 'make depend' gracefully.
 # dep.mk will handle that itself.
@@ -37,7 +37,7 @@ __${.PARSEFILE}__:
 
 # set this to -MMD to ignore /usr/include
 # actually it ignores <> so may not be a great idea
-CFLAGS_MD ?= -MD 
+CFLAGS_MD ?= -MD
 # -MF etc not available on all gcc versions.
 CFLAGS_MF ?= -MF ${.TARGET:T}.d -MT ${.TARGET:T}
 CFLAGS += ${CFLAGS_MD} ${CFLAGS_MF}
diff --git a/mk/auto.obj.mk b/mk/auto.obj.mk
index a12ea7a81318..0405b5e6441d 100644
--- a/mk/auto.obj.mk
+++ b/mk/auto.obj.mk
@@ -1,14 +1,14 @@
-# $Id: auto.obj.mk,v 1.15 2017/11/04 21:05:04 sjg Exp $
+# $Id: auto.obj.mk,v 1.16 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2004, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -16,7 +16,7 @@
 ECHO_TRACE ?= echo
 
 .ifndef Mkdirs
-# A race condition in some versions of mkdir, means that it can bail 
+# A race condition in some versions of mkdir, means that it can bail
 # if another process made a dir that mkdir expected to.
 # We repeat the mkdir -p a number of times to try and work around this.
 # We stop looping as soon as the dir exists.
@@ -53,7 +53,7 @@ __objdir:= ${__objdir}
 # We need to chdir, make the directory if needed
 .if !exists(${__objdir}/) && \
 	(${.TARGETS} == "" || ${.TARGETS:Nclean*:N*clean:Ndestroy*} != "")
-# This will actually make it... 
+# This will actually make it...
 __objdir_made != echo ${__objdir}/; umask ${OBJDIR_UMASK:U002}; \
         ${ECHO_TRACE} "[Creating objdir ${__objdir}...]" >&2; \
         ${Mkdirs}; Mkdirs ${__objdir}
diff --git a/mk/autoconf.mk b/mk/autoconf.mk
index 38f4ece48ad6..61e6978043a8 100644
--- a/mk/autoconf.mk
+++ b/mk/autoconf.mk
@@ -1,14 +1,14 @@
-# $Id: autoconf.mk,v 1.9 2017/08/13 20:03:13 sjg Exp $
+# $Id: autoconf.mk,v 1.10 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 1996-2009, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -49,7 +49,7 @@ CLEANFILES+= config.recheck config.gen config.status *.meta
 AUTOCONF ?= autoconf
 AUTOHEADER ?= autoheader
 
-# expand it to a full path 
+# expand it to a full path
 AUTOCONF := ${AUTOCONF:${M_whence}}
 
 .if exists(${AUTOCONF})
diff --git a/mk/autodep.mk b/mk/autodep.mk
index 7b5029f15728..a7bb942278c9 100644
--- a/mk/autodep.mk
+++ b/mk/autodep.mk
@@ -1,16 +1,16 @@
 #
 # RCSid:
-#	$Id: autodep.mk,v 1.37 2020/04/17 21:08:17 sjg Exp $
+#	$Id: autodep.mk,v 1.38 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 1999-2010, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 
@@ -51,7 +51,7 @@ ${s:T:R}.d:	$s
 .endfor
 
 __depsrcs:=${__depsrcs:T:R:S/$/.d/g}
-# we also need to handle makefiles where the .d's from __depsrcs 
+# we also need to handle makefiles where the .d's from __depsrcs
 # don't  match those from OBJS
 # we avoid using := here, since the modifier applied to OBJS
 # can cause trouble if there are any undefined vars in OBJS.
@@ -64,7 +64,7 @@ __dependsrcs= ${__dependsrcsx:O:u}
 
 # set this to -MMD to ignore /usr/include
 # actually it ignores <> so may not be a great idea
-CFLAGS_MD?=-MD 
+CFLAGS_MD?=-MD
 # -MF etc not available on all gcc versions.
 # we "fix" the .o later
 CFLAGS_MF?=-MF ${.TARGET:T:R}.d -MT ${.TARGET:T:R}.o
@@ -73,8 +73,8 @@ RM?= rm
 MAKE_SHELL?= sh
 
 # watch out for people who don't use CPPFLAGS
-CPPFLAGS_MD=${CFLAGS:M-[IUD]*} ${CPPFLAGS} 
-CXXFLAGS_MD=${CXXFLAGS:M-[IUD]*} ${CPPFLAGS} 
+CPPFLAGS_MD=${CFLAGS:M-[IUD]*} ${CPPFLAGS}
+CXXFLAGS_MD=${CXXFLAGS:M-[IUD]*} ${CPPFLAGS}
 
 # just in case these need to be different
 CC_MD?=${CC}
diff --git a/mk/compiler.mk b/mk/compiler.mk
index 40a57c081ab4..b20ecaa047e3 100644
--- a/mk/compiler.mk
+++ b/mk/compiler.mk
@@ -1,14 +1,14 @@
-# $Id: compiler.mk,v 1.6 2019/09/28 17:12:00 sjg Exp $
+# $Id: compiler.mk,v 1.7 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2019, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/cython.mk b/mk/cython.mk
index c1318b2174e9..d3c229c7269a 100644
--- a/mk/cython.mk
+++ b/mk/cython.mk
@@ -1,15 +1,15 @@
 # RCSid:
-#	$Id: cython.mk,v 1.7 2018/03/25 18:46:11 sjg Exp $
+#	$Id: cython.mk,v 1.8 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2014, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/dirdeps-cache-update.mk b/mk/dirdeps-cache-update.mk
new file mode 100644
index 000000000000..eb992e936eb8
--- /dev/null
+++ b/mk/dirdeps-cache-update.mk
@@ -0,0 +1,179 @@
+# $Id: dirdeps-cache-update.mk,v 1.21 2020/08/19 17:51:53 sjg Exp $
+#
+#	@(#) Copyright (c) 2020, Simon J. Gerraty
+#
+#	This file is provided in the hope that it will
+#	be of use.  There is absolutely NO WARRANTY.
+#	Permission to copy, redistribute or otherwise
+#	use this file is hereby granted provided that
+#	the above copyright notice and this notice are
+#	left intact.
+#
+#	Please send copies of changes and bug-fixes to:
+#	sjg@crufty.net
+#
+
+##
+#
+# This makefile deals with the updating of STATIC_DIRDEPS_CACHE.
+# Some targets are so huge that computing dirdeps takes a significant
+# amount of time.  For such targets a STATIC_DIRDEPS_CACHE can make
+# sense.
+#
+# If the target is represented by targets/pseudo/production
+# it's normal DIRDEPS would be in
+# targets/pseudo/production/Makefile.depend
+# and STATIC_DIRDEPS_CACHE would be
+# targets/pseudo/production/Makefile.dirdeps.cache
+# which is simply initialized by copying dirdeps.cache.production
+# from $OBJTOP
+#
+# When dirdeps-targets.mk is initializing DIRDEPS it will look for
+# Makefile.dirdeps.cache and unless told not to
+# (MK_STATIC_DIRDEPS_CACHE=no) will use it as DIRDEPS_CACHE.
+#
+# If MK_STATIC_DIRDEPS_CACHE_UPDATE is "yes", then this makefile
+# comes into play.
+#
+# We usually get included from local.dirdeps.mk
+# as well as Makefile.depend of RELDIR with a static Makefile.dirdeps.cache
+#
+# If we see that STATIC_DIRDEPS_CACHE is in use, we need to hook a
+# cache-update target into the build to regenerate dirdeps.cache
+# in parallel with the rest of the build.
+# If MK_STATIC_DIRDEPS_CACHE_UPDATE_IMMEDIATE is "yes" we update
+# STATIC_DIRDEPS_CACHE as soon as the update is ready,
+# otherwise it will be done at the end of the build.
+#
+# If STATIC_DIRDEPS_CACHE is not in use, but a DIRDEPS_CACHE is,
+# then we need do nothing except export STATIC_DIRDEPS_CACHE and
+# DYNAMIC_DIRDEPS_CACHE for use when we are include during the visit
+# to the ultimate target (targets/pseudo/production).
+#
+# Regardless of which happens, when included at .MAKE.LEVEL > 0
+# for a target other than cache-update we simply copy
+# DYNAMIC_DIRDEPS_CACHE to STATIC_DIRDEPS_CACHE with some optional
+# filtering.
+#
+# If we are included for the target cache-update we take care of
+# running dirdeps.mk again to generate the DYNAMIC_DIRDEPS_CACHE.
+#
+
+.if !target(_${.PARSEFILE}_)
+_${.PARSEFILE}_: .NOTMAIN
+
+STATIC_CACHE_SED += \
+	-e '/Autogenerated/s,-.*,- edit with care!,' \
+	-e '/cache-update/d'
+
+STATIC_DIRDEPS_CACHE_UPDATE_SCRIPT ?= \
+	{ echo Saving ${DYNAMIC_DIRDEPS_CACHE} as ${STATIC_DIRDEPS_CACHE}; \
+        sed ${STATIC_CACHE_SED} ${DYNAMIC_DIRDEPS_CACHE} > ${STATIC_DIRDEPS_CACHE}; }
+.endif
+
+.if ${MK_DIRDEPS_CACHE:Uno} == "yes"
+.if ${MK_STATIC_DIRDEPS_CACHE_UPDATE:Uno} == "yes"
+.if ${_debug_reldir:U0} || ${DEBUG_DIRDEPS:U:Mcache*} != ""
+_debug_cache = 1
+.else
+_debug_cache = 0
+.endif
+
+.if ${.MAKE.LEVEL} == 0 && !make(cache-update)
+
+.if ${_debug_cache}
+.info ${MK_STATIC_DIRDEPS_CACHE_UPDATE MK_STATIC_DIRDEPS_CACHE MK_DIRDEPS_CACHE DIRDEPS_CACHE STATIC_DIRDEPS_CACHE:L:@v@$v=${$v}@}
+.endif
+
+.if ${MK_STATIC_DIRDEPS_CACHE} == "yes" && defined(STATIC_DIRDEPS_CACHE) && exists(${STATIC_DIRDEPS_CACHE})
+.if !make(dirdeps)
+# We are using static cache and this is the only look we will get.
+# We want to generate an updated cache while we build
+# so need to hook cache-update to dirdeps now.
+# Note: we are running as a sibling to dirdeps-cached,
+# attempting to do this in that context is problematic.
+
+# One of these should exist - to actually kick off the cache generation
+.for d in ${STATIC_DIRDEPS_CACHE:H}/cache-update ${STATIC_DIRDEPS_CACHE:H:H}/cache-update ${STATIC_DIRDEPS_CACHE:H:H:H}/cache-update
+.if exists($d)
+cache_update_dirdep ?= $d.${TARGET_SPEC}
+.endif
+.endfor
+.if !target(${cache_update_dirdep})
+dirdeps: ${cache_update_dirdep}
+${cache_update_dirdep}: _DIRDEP_USE
+DYNAMIC_DIRDEPS_CACHE := ${OBJTOP}/dirdeps.cache.${STATIC_DIRDEPS_CACHE:H:T}-update
+.export DYNAMIC_DIRDEPS_CACHE STATIC_DIRDEPS_CACHE
+.endif
+.endif	# make(dirdeps)
+.endif	# MK_*
+
+.endif	# .MAKE.LEVEL 0
+
+.if ${.MAKE.LEVEL} > 0 && ${.CURDIR:T} == "cache-update"
+# we are the background update shim
+
+.if ${_debug_cache}
+.info level ${.MAKE.LEVEL}: ${MK_DIRDEPS_CACHE DYNAMIC_DIRDEPS_CACHE STATIC_DIRDEPS_CACHE:L:@v@$v=${$v}@}
+.endif
+
+all: cache-build
+cache-build: .META
+	@set -x; MAKELEVEL=0 \
+	${.MAKE} -C ${SRCTOP} -f ${RELDIR}/Makefile cache-update \
+	-DWITHOUT_STATIC_DIRDEPS_CACHE_UPDATE
+
+.endif	# cache-update
+
+.elif ${.MAKE.LEVEL} == 0 && make(cache-update) && !target(cache-update)
+# we were invoked above
+# we just leverage dirdeps.mk
+BUILD_DIRDEPS_TARGETS := ${STATIC_DIRDEPS_CACHE:H:T}
+DIRDEPS := ${STATIC_DIRDEPS_CACHE:H:S,^${SRCTOP}/,,}.${TARGET_SPEC}
+DIRDEPS_CACHE := ${DYNAMIC_DIRDEPS_CACHE}
+
+.if ${DEBUG_DIRDEPS:U:Mcache*} != ""
+.info level 0: ${MK_DIRDEPS_CACHE DIRDEPS_CACHE DIRDEPS:L:@v@$v=${$v}@}
+.endif
+
+# so cache-built below can check on us
+x!= echo; echo ${.MAKE.PID} > ${DIRDEPS_CACHE}.new.pid
+
+cache-update: ${DIRDEPS_CACHE}
+	@rm -f ${DIRDEPS_CACHE}.new.pid
+.if ${MK_STATIC_DIRDEPS_CACHE_UPDATE_IMMEDIATE:Uno} == "yes"
+	${STATIC_DIRDEPS_CACHE_UPDATE_SCRIPT}
+.endif
+
+all:
+
+.include <dirdeps.mk>
+
+.endif	# MK_STATIC_DIRDEPS_CACHE_UPDATE
+.endif	# MK_DIRDEPS_CACHE
+
+.if ${.MAKE.LEVEL} > 0 && ${MK_STATIC_DIRDEPS_CACHE_UPDATE:Uno} == "yes" && \
+	${STATIC_DIRDEPS_CACHE:Uno:H} == "${SRCTOP}/${RELDIR}"
+.if !defined(DYNAMIC_DIRDEPS_CACHE)
+all:
+.else
+# This is the easy bit, time to save the cache
+
+all: cache-update
+
+# ensure the cache update is completed
+cache-built:
+	@test -s ${DYNAMIC_DIRDEPS_CACHE}.new || exit 0; \
+	pid=`cat ${DYNAMIC_DIRDEPS_CACHE}.new.pid 2> /dev/null`; \
+	test $${pid:-0} -gt 1 || exit 0; \
+	echo "Waiting for $$pid to finish ${DYNAMIC_DIRDEPS_CACHE} ..."; \
+	while 'kill' -0 $$pid; do sleep 30; done > /dev/null 2>&1
+
+cache-update: cache-built
+.if ${MK_STATIC_DIRDEPS_CACHE_UPDATE_IMMEDIATE:Uno} == "no"
+	@test ! -s ${DYNAMIC_DIRDEPS_CACHE} || \
+	${STATIC_DIRDEPS_CACHE_UPDATE_SCRIPT}
+.endif
+
+.endif
+.endif
diff --git a/mk/dirdeps-options.mk b/mk/dirdeps-options.mk
index 4f74c02e1b8c..74f54a4cf665 100644
--- a/mk/dirdeps-options.mk
+++ b/mk/dirdeps-options.mk
@@ -1,6 +1,6 @@
-# $Id: dirdeps-options.mk,v 1.9 2018/09/20 00:07:19 sjg Exp $
+# $Id: dirdeps-options.mk,v 1.17 2020/08/07 01:57:38 sjg Exp $
 #
-#	@(#) Copyright (c) 2018, Simon J. Gerraty
+#	@(#) Copyright (c) 2018-2020, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
@@ -37,6 +37,11 @@
 # to whatever applies for that dir, or it can rely on globals
 # set in local.dirdeps-options.mk
 # Either way, we will .undef DIRDEPS.* when done.
+#
+# In some cases the value of MK_FOO might depend on TARGET_SPEC
+# so we qualify MK_FOO with .${TARGET_SPEC} and each component
+# TARGET_SPEC_VAR (in reverse order) before using MK_FOO.
+#
 
 # This should have been set by Makefile.depend.options
 # before including us
@@ -47,21 +52,43 @@ DIRDEPS_OPTIONS ?=
 
 .if ${.MAKE.LEVEL} == 0
 # :U below avoids potential errors when we :=
-.for o in ${DIRDEPS_OPTIONS:tu}
-DIRDEPS += ${DIRDEPS.$o.${MK_$o:U}:U}
+# some options can depend on TARGET_SPEC!
+DIRDEPS_OPTIONS_QUALIFIER_LIST ?= \
+	${DEP_TARGET_SPEC:U${TARGET_SPEC}} \
+	${TARGET_SPEC_VARSr:U${TARGET_SPEC_VARS}:@v@${DEP_$v:U${$v}}@}
+# note that we need to include $o in the variable _o$o
+# to ensure correct evaluation.
+.for o in ${DIRDEPS_OPTIONS}
+.undef _o$o _v$o
+.for x in ${DIRDEPS_OPTIONS_QUALIFIER_LIST}
+.if defined(MK_$o.$x)
+_o$o ?= MK_$o.$x
+_v$o ?= ${MK_$o.$x}
+.endif
+.endfor
+_v$o ?= ${MK_$o}
+.if ${_debug_reldir:U0}
+.info ${DEP_RELDIR:U${RELDIR}}.${DEP_TARGET_SPEC:U${TARGET_SPEC}}: o=$o ${_o$o:UMK_$o}=${_v$o:U} DIRDEPS += ${DIRDEPS.$o.${_v$o:U}:U}
+.endif
+DIRDEPS += ${DIRDEPS.$o.${_v$o:U}:U}
 .endfor
 DIRDEPS := ${DIRDEPS:O:u}
+.if ${_debug_reldir:U0}
+.info ${DEP_RELDIR:U${RELDIR}}: DIRDEPS=${DIRDEPS}
+.endif
 # avoid cross contamination
-.for o in ${DIRDEPS_OPTIONS:tu}
+.for o in ${DIRDEPS_OPTIONS}
 .undef DIRDEPS.$o.yes
 .undef DIRDEPS.$o.no
+.undef _o$o
+.undef _v$o
 .endfor
 .else
 # whether options are enabled or not,
 # we want to filter out the relevant DIRDEPS.*
 # we should only be included by meta.autodep.mk
 # if dependencies are to be updated
-.for o in ${DIRDEPS_OPTIONS:tu}
+.for o in ${DIRDEPS_OPTIONS}
 .for d in ${DIRDEPS.$o.yes} ${DIRDEPS.$o.no}
 .if exists(${SRCTOP}/$d)
 GENDIRDEPS_FILTER += N$d*
diff --git a/mk/dirdeps-targets.mk b/mk/dirdeps-targets.mk
index 50a1970d6211..73dcf3639d3b 100644
--- a/mk/dirdeps-targets.mk
+++ b/mk/dirdeps-targets.mk
@@ -1,15 +1,15 @@
 # RCSid:
-#       $Id: dirdeps-targets.mk,v 1.10 2020/06/06 22:41:02 sjg Exp $
+#       $Id: dirdeps-targets.mk,v 1.22 2020/08/15 18:00:11 sjg Exp $
 #
 #       @(#) Copyright (c) 2019-2020 Simon J. Gerraty
 #
 #       This file is provided in the hope that it will
 #       be of use.  There is absolutely NO WARRANTY.
 #       Permission to copy, redistribute or otherwise
-#       use this file is hereby granted provided that 
+#       use this file is hereby granted provided that
 #       the above copyright notice and this notice are
-#       left intact. 
-#      
+#       left intact.
+#
 #       Please send copies of changes and bug-fixes to:
 #       sjg@crufty.net
 #
@@ -25,7 +25,16 @@
 # We then search those dirs for any Makefile.depend*
 # Finally we select any that match conditions like REQUESTED_MACHINE
 # or TARGET_SPEC and initialize DIRDEPS accordingly.
-# 
+#
+# We will check each of the initial DIRDEPS for Makefile.dirdeps.options
+# and include any found.
+# This makes it feasible to tweak options like MK_DIRDEPS_CACHE
+# for a specific target.
+#
+# If MK_STATIC_DIRDEPS_CACHE is defined we will check if the
+# initial DIRDEPS has a static cache (Makefile.dirdeps.cache).
+# This only makes sense for seriously expensive targets.
+#
 
 .if ${.MAKE.LEVEL} == 0
 # pickup customizations
@@ -125,12 +134,38 @@ DIRDEPS := ${DIRDEPS:O:u}
 .endif
 # if we got DIRDEPS get to work
 .if !empty(DIRDEPS)
+DIRDEPS.dirs := ${DIRDEPS:S,^,${SRCTOP}/,:@d@${exists($d):?$d:${d:R}}@}
+# some targets what to tweak options we might want to process now
+.for m in ${DIRDEPS.dirs:S,$,/Makefile.dirdeps.options,}
+.-include <$m>
+.endfor
+.if defined(MK_STATIC_DIRDEPS_CACHE)
+# some targets are very expensive to compute dirdeps for
+# so we may have a static cache
+.for c in ${DIRDEPS.dirs:S,$,/Makefile.dirdeps.cache,}
+.if exists($c)
+STATIC_DIRDEPS_CACHE ?= $c
+.if ${MK_STATIC_DIRDEPS_CACHE} == "yes"
+DIRDEPS_CACHE ?= $c
+MK_DIRDEPS_CACHE = yes
+.endif
+.endif
+.endfor
+.if defined(STATIC_DIRDEPS_CACHE)
+.export STATIC_DIRDEPS_CACHE
+.endif
+.endif
+
+# allow a top-level makefile to do other stuff
+# before including dirdeps.mk
+.if ${MK_DIRDEPS_TARGETS_INCLUDE_DIRDEPS:Uyes} == "yes"
 .include <dirdeps.mk>
+.endif
 
 DIRDEPS_TARGETS_SKIP += all clean* destroy*
 
 .for t in ${.TARGETS:${DIRDEPS_TARGETS_SKIP:${M_ListToSkip}}}
 $t: dirdeps
-.endfor                                                                         
+.endfor
 .endif
 .endif
diff --git a/mk/dirdeps.mk b/mk/dirdeps.mk
index 337692479898..16673a04c07b 100644
--- a/mk/dirdeps.mk
+++ b/mk/dirdeps.mk
@@ -1,4 +1,4 @@
-# $Id: dirdeps.mk,v 1.106 2020/07/11 16:25:17 sjg Exp $
+# $Id: dirdeps.mk,v 1.125 2020/08/26 21:49:45 sjg Exp $
 
 # Copyright (c) 2010-2020, Simon J. Gerraty
 # Copyright (c) 2010-2018, Juniper Networks, Inc.
@@ -41,7 +41,7 @@
 #	or .<target_spec> suffix (see TARGET_SPEC_VARS below),
 #	for example to force building something for the pseudo
 #	machines "host" or "common" regardless of current ${MACHINE}.
-#	
+#
 #	All unqualified entries end up being qualified with .${TARGET_SPEC}
 #	and partially qualified (if TARGET_SPEC_VARS has multiple
 #	entries) are also expanded to a full .<target_spec>.
@@ -50,29 +50,22 @@
 #
 #	The fully qualified directory entries are used to construct a
 #	dependency graph that will drive the build later.
-#	
+#
 #	Also, for each fully qualified directory target, we will search
 #	using ${.MAKE.DEPENDFILE_PREFERENCE} to find additional
 #	dependencies.  We use Makefile.depend (default value for
 #	.MAKE.DEPENDFILE_PREFIX) to refer to these makefiles to
 #	distinguish them from others.
-#	
+#
 #	Before each Makefile.depend file is read, we set
 #	DEP_RELDIR to be the RELDIR (path relative to SRCTOP) for
 #	its directory, and DEP_MACHINE etc according to the .<target_spec>
 #	represented by the suffix of the corresponding target.
-#	
+#
 #	Since each Makefile.depend file includes dirdeps.mk, this
 #	processing is recursive and results in .MAKE.LEVEL 0 learning the
 #	dependencies of the tree wrt the initial directory (_DEP_RELDIR).
 #
-# BUILD_AT_LEVEL0
-#	Indicates whether .MAKE.LEVEL 0 builds anything:
-#	if "no" sub-makes are used to build everything,
-#	if "yes" sub-makes are only used to build for other machines.
-#	It is best to use "no", but this can require fixing some
-#	makefiles to not do anything at .MAKE.LEVEL 0.
-#
 # TARGET_SPEC_VARS
 #	The default value is just MACHINE, and for most environments
 #	this is sufficient.  The _DIRDEP_USE target actually sets
@@ -113,12 +106,12 @@
 #		# make sure we know what TARGET_SPEC is
 #		# as we may need it to find Makefile.depend*
 #		TARGET_SPEC = ${TARGET_SPEC_VARS:@v@${$v:U}@:ts,}
-#	
+#
 #	The following variables can influence the initial DIRDEPS
 #	computation with regard to the TARGET_SPECs that will be
 #	built.
 #	Most should also be considered by init.mk
-#	
+#
 #	ONLY_TARGET_SPEC_LIST
 #		Defines a list of TARGET_SPECs for which the current
 #		directory can be built.
@@ -137,6 +130,19 @@
 #		A list of MACHINEs the current directory should not be
 #		built for.
 #
+# _build_xtra_dirs
+#	local.dirdeps.mk can add targets to this variable.
+#	They will be hooked into the build, but independent of
+#	any other DIRDEP.
+#
+#	This allows for adding TESTS to the build, such that the build
+#	if any test fails, but without the risk of introducing
+#	circular dependencies.
+
+now_utc ?= ${%s:L:gmtime}
+.if !defined(start_utc)
+start_utc := ${now_utc}
+.endif
 
 .if !target(bootstrap) && (make(bootstrap) || \
 	make(bootstrap-this) || \
@@ -157,11 +163,6 @@ _DIRDEP_USE_LEVEL?= 0
 _CURDIR ?= ${.CURDIR}
 _OBJDIR ?= ${.OBJDIR}
 
-now_utc = ${%s:L:gmtime}
-.if !defined(start_utc)
-start_utc := ${now_utc}
-.endif
-
 .if ${MAKEFILE:T} == ${.PARSEFILE} && empty(DIRDEPS) && ${.TARGETS:Uall:M*/*} != ""
 # This little trick let's us do
 #
@@ -223,6 +224,7 @@ _tspec_m$i := ${TARGET_SPEC_VARS:[2..$i]:@w@[^,]+@:ts,}
 _tspec_a$i := ,${TARGET_SPEC_VARS:[$i..-1]:@v@$$$${DEP_$v}@:ts,}
 M_dep_qual_fixes += C;(\.${_tspec_m$i})$$;\1${_tspec_a$i};
 .endfor
+TARGET_SPEC_VARSr := ${TARGET_SPEC_VARS:[-1..1]}
 .else
 # A harmless? default.
 M_dep_qual_fixes = U
@@ -307,6 +309,7 @@ DEP_MACHINE := ${_DEP_TARGET_SPEC}
 
 # reset each time through
 _build_all_dirs =
+_build_xtra_dirs =
 
 # the first time we are included the _DIRDEP_USE target will not be defined
 # we can use this as a clue to do initialization and other one time things.
@@ -337,7 +340,7 @@ BUILD_DIRDEPS ?= yes
 
 .if ${MK_DIRDEPS_CACHE} == "yes"
 # this is where we will cache all our work
-DIRDEPS_CACHE ?= ${_OBJDIR:tA}/dirdeps.cache${.TARGETS:Nall:O:u:ts-:S,/,_,g:S,^,.,:N.}
+DIRDEPS_CACHE ?= ${_OBJDIR:tA}/dirdeps.cache${_TARGETS:U${.TARGETS}:Nall:O:u:ts-:S,/,_,g:S,^,.,:N.}
 .endif
 
 .if ${DEBUG_DIRDEPS:@x@${DEP_RELDIR:M$x}${${DEP_RELDIR}.${DEP_MACHINE}:L:M$x}@} != ""
@@ -393,6 +396,18 @@ DIRDEPS_FILTER += M${_DEP_RELDIR}
 DIRDEP_MAKE ?= ${.MAKE}
 DIRDEP_DIR ?= ${.TARGET:R}
 
+# if you want us to report load averages during build
+# DIRDEP_USE_PRELUDE += ${DIRDEP_LOADAVG_REPORT};
+
+DIRDEP_LOADAVG_CMD ?= ${UPTIME:Uuptime} | sed 's,.*\(load\),\1,'
+DIRDEP_LOADAVG_LAST = 0
+# yes the expression here is a bit complicated,
+# the trick is to only eval ${DIRDEP_LOADAVG_LAST::=${now_utc}}
+# when we want to report.
+DIRDEP_LOADAVG_REPORT = \
+	test -z "${"${expr ${now_utc} - ${DIRDEP_LOADAVG_INTEVAL:U60} - ${DIRDEP_LOADAVG_LAST}:L:sh:N-*}":?yes${DIRDEP_LOADAVG_LAST::=${now_utc}}:}" || \
+	echo "${TRACER}`${DIRDEP_LOADAVG_CMD}`"
+
 # we suppress SUBDIR when visiting the leaves
 # we assume sys.mk will set MACHINE_ARCH
 # you can add extras to DIRDEP_USE_ENV
@@ -400,7 +415,7 @@ DIRDEP_DIR ?= ${.TARGET:R}
 _DIRDEP_USE:	.USE .MAKE
 	@for m in ${.MAKE.MAKEFILE_PREFERENCE}; do \
 		test -s ${.TARGET:R}/$$m || continue; \
-		echo "${TRACER}Checking ${.TARGET:R} for ${.TARGET:E} ..."; \
+		echo "${TRACER}Checking ${.TARGET:S,${SRCTOP}/,,} for ${.TARGET:E} ..."; \
 		${DIRDEP_USE_PRELUDE} \
 		MACHINE_ARCH= NO_SUBDIR=1 ${DIRDEP_USE_ENV} \
 		TARGET_SPEC=${.TARGET:E} \
@@ -480,9 +495,13 @@ dirdeps-cached:	${DIRDEPS_CACHE} .MAKE
 		dirdeps MK_DIRDEPS_CACHE=no BUILD_DIRDEPS=no
 
 # these should generally do
-BUILD_DIRDEPS_MAKEFILE ?= ${MAKEFILE}
+BUILD_DIRDEPS_MAKEFILE ?=
 BUILD_DIRDEPS_TARGETS ?= ${.TARGETS}
 
+.if ${DIRDEPS_CACHE} != ${STATIC_DIRDEPS_CACHE:Uno} && ${DIRDEPS_CACHE:M${SRCTOP}/*} == ""
+# export this for dirdeps-cache-update.mk
+DYNAMIC_DIRDEPS_CACHE := ${DIRDEPS_CACHE}
+.export DYNAMIC_DIRDEPS_CACHE
 # we need the .meta file to ensure we update if
 # any of the Makefile.depend* changed.
 # We do not want to compare the command line though.
@@ -494,26 +513,28 @@ ${DIRDEPS_CACHE}:	.META .NOMETA_CMP
 	+@MAKELEVEL=${.MAKE.LEVEL} DIRDEPS_CACHE=${DIRDEPS_CACHE} \
 	DIRDEPS="${DIRDEPS}" \
 	TARGET_SPEC=${TARGET_SPEC} \
-	MAKEFLAGS= ${.MAKE} -C ${_CURDIR} -f ${BUILD_DIRDEPS_MAKEFILE} \
+	MAKEFLAGS= ${DIRDEP_CACHE_MAKE:U${.MAKE}} -C ${_CURDIR} \
+	${BUILD_DIRDEPS_MAKEFILE} \
 	${BUILD_DIRDEPS_TARGETS} BUILD_DIRDEPS_CACHE=yes \
 	.MAKE.DEPENDFILE=.none \
 	${.MAKEFLAGS:tW:S,-D ,-D,g:tw:M*WITH*} \
 	${.MAKEFLAGS:tW:S,-d ,-d,g:tw:M-d*} \
-	3>&1 1>&2 | sed 's,${SRCTOP},$${SRCTOP},g' >> ${.TARGET}.new && \
+	3>&1 1>&2 | sed 's,${SRCTOP},$${SRCTOP},g;s,_{,$${,g' >> ${.TARGET}.new && \
 	mv ${.TARGET}.new ${.TARGET}
 
+.endif
 .endif
 .elif !target(_count_dirdeps)
 # we want to capture the dirdeps count in the cache
 .END: _count_dirdeps
 _count_dirdeps: .NOMETA
-	@{ echo; echo '.info $${.newline}$${TRACER}Makefiles read: total=${.MAKE.MAKEFILES:[#]} depend=${.MAKE.MAKEFILES:M*depend*:[#]} dirdeps=${.ALLTARGETS:M${SRCTOP}*:O:u:[#]}'; } >&3
+	@{ echo; echo '.info $${.newline}$${TRACER}Makefiles read: total=${.MAKE.MAKEFILES:[#]} depend=${.MAKE.MAKEFILES:M*depend*:[#]} dirdeps=${.ALLTARGETS:M${SRCTOP}*:O:u:[#]} ${DIRDEP_INFO_XTRAS}'; } >&3
 
 .endif
 .elif !make(dirdeps) && !target(_count_dirdeps)
 beforedirdeps: _count_dirdeps
 _count_dirdeps: .NOMETA
-	@echo "${TRACER}Makefiles read: total=${.MAKE.MAKEFILES:[#]} depend=${.MAKE.MAKEFILES:M*depend*:[#]} dirdeps=${.ALLTARGETS:M${SRCTOP}*:O:u:[#]} seconds=`expr ${now_utc} - ${start_utc}`"
+	@echo "${TRACER}Makefiles read: total=${.MAKE.MAKEFILES:[#]} depend=${.MAKE.MAKEFILES:M*depend*:[#]} dirdeps=${.ALLTARGETS:M${SRCTOP}*:O:u:[#]} ${DIRDEP_INFO_XTRAS} seconds=`expr ${now_utc} - ${start_utc}`"
 
 .endif
 .endif
@@ -570,19 +591,7 @@ _build_dirs =
 
 .if ${DEP_RELDIR} == ${_DEP_RELDIR}
 # pickup other machines for this dir if necessary
-.if ${BUILD_AT_LEVEL0:Uyes} == "no"
 _build_dirs += ${_machines:@m@${_CURDIR}.$m@}
-.else
-_build_dirs += ${_machines:N${DEP_TARGET_SPEC}:@m@${_CURDIR}.$m@}
-.if ${DEP_TARGET_SPEC} == ${TARGET_SPEC}
-# pickup local dependencies now
-.if ${MAKE_VERSION} < 20160220
-.-include <.depend>
-.else
-.dinclude <.depend>
-.endif
-.endif
-.endif
 .endif
 
 .if ${_debug_reldir}
@@ -635,7 +644,7 @@ _build_dirs := ${_build_dirs:${M_dep_qual_fixes:ts:}:O:u}
 
 .endif				# empty DIRDEPS
 
-_build_all_dirs += ${_build_dirs}
+_build_all_dirs += ${_build_dirs} ${_build_xtra_dirs}
 _build_all_dirs := ${_build_all_dirs:O:u}
 
 # Normally if doing make -V something,
@@ -647,12 +656,7 @@ _build_all_dirs := ${_build_all_dirs:O:u}
 x!= echo; { echo; echo '\# ${DEP_RELDIR}.${DEP_TARGET_SPEC}'; } >&3
 # guard against _new_dirdeps being too big for a single command line
 _new_dirdeps := ${_build_all_dirs:@x@${target($x):?:$x}@}
-.if !empty(_new_dirdeps)
-.export _new_dirdeps
-x!= echo; { echo; echo "dirdeps: \\"; \
-	for x in $$_new_dirdeps; do echo "	$$x \\"; done; echo; \
-	for x in $$_new_dirdeps; do echo "$$x: _DIRDEP_USE"; done; } >&3
-.endif
+.export _build_xtra_dirs _new_dirdeps
 .if !empty(DEP_EXPORT_VARS)
 # Discouraged, but there are always exceptions.
 # Handle it here rather than explain how.
@@ -676,7 +680,8 @@ DEP_EXPORT_VARS=
 # this builds the dependency graph
 .for m in ${_machines}
 .if ${BUILD_DIRDEPS_CACHE} == "yes" && !empty(_build_dirs)
-x!= echo; { echo; echo "${_this_dir}.$m: \\"; } >&3
+x!= echo; { echo; echo 'DIRDEPS.${_this_dir}.$m = \'; } >&3
+_cache_deps =
 .endif
 # it would be nice to do :N${.TARGET}
 .if !empty(__qual_depdirs)
@@ -685,11 +690,7 @@ x!= echo; { echo; echo "${_this_dir}.$m: \\"; } >&3
 .info ${DEP_RELDIR}.$m: graph: ${_build_dirs:M*.$q}
 .endif
 .if ${BUILD_DIRDEPS_CACHE} == "yes"
-_cache_deps := ${_build_dirs:M*.$q}
-.if !empty(_cache_deps)
-.export _cache_deps
-x!= echo; for x in $$_cache_deps; do echo "	$$x \\"; done >&3
-.endif
+_cache_deps += ${_build_dirs:M*.$q}
 .else
 ${_this_dir}.$m: ${_build_dirs:M*.$q}
 .endif
@@ -699,11 +700,17 @@ ${_this_dir}.$m: ${_build_dirs:M*.$q}
 .info ${DEP_RELDIR}.$m: graph: ${_build_dirs:M*.$m:N${_this_dir}.$m}
 .endif
 .if ${BUILD_DIRDEPS_CACHE} == "yes"
-_cache_deps := ${_build_dirs:M*.$m:N${_this_dir}.$m}
+.if !empty(_build_dirs)
+_cache_deps += ${_build_dirs:M*.$m:N${_this_dir}.$m}
 .if !empty(_cache_deps)
 .export _cache_deps
 x!= echo; for x in $$_cache_deps; do echo "	$$x \\"; done >&3
 .endif
+x!= echo; { echo; echo '${_this_dir}.$m: $${DIRDEPS.${_this_dir}.$m}'; \
+	echo; echo 'dirdeps: ${_this_dir}.$m \'; \
+	for x in $$_build_xtra_dirs; do echo "	$$x \\"; done; \
+	echo; for x in $$_new_dirdeps; do echo "$$x: _DIRDEP_USE"; done; } >&3
+.endif
 .else
 ${_this_dir}.$m: ${_build_dirs:M*.$m:N${_this_dir}.$m}
 .endif
@@ -773,6 +780,27 @@ _DEP_RELDIR := ${RELDIR}
 # This is a final opportunity to add/hook global rules.
 .-include <local.dirdeps-build.mk>
 
+# skip _reldir_{finish,failed} if not included from Makefile.depend*
+# or not in meta mode
+.if !defined(WITHOUT_META_STATS) && ${.INCLUDEDFROMFILE:U:M${.MAKE.DEPENDFILE_PREFIX}*} != "" && ${.MAKE.MODE:Mmeta} != ""
+
+meta_stats= meta=${empty(.MAKE.META.FILES):?0:${.MAKE.META.FILES:[#]}} \
+	created=${empty(.MAKE.META.CREATED):?0:${.MAKE.META.CREATED:[#]}}
+
+.if !target(_reldir_finish)
+.END: _reldir_finish
+_reldir_finish: .NOMETA
+	@echo "${TRACER}Finished ${RELDIR}.${TARGET_SPEC} seconds=$$(( ${now_utc} - ${start_utc} )) ${meta_stats}"
+.endif
+
+.if !target(_reldir_failed)
+.ERROR: _reldir_failed
+_reldir_failed: .NOMETA
+	@echo "${TRACER}Failed ${RELDIR}.${TARGET_SPEC} seconds=$$(( ${now_utc} - ${start_utc} )) ${meta_stats}"
+.endif
+
+.endif
+
 # pickup local dependencies
 .if ${MAKE_VERSION} < 20160220
 .-include <.depend>
diff --git a/mk/dpadd.mk b/mk/dpadd.mk
index 02fa7f386921..45585ce76d9b 100644
--- a/mk/dpadd.mk
+++ b/mk/dpadd.mk
@@ -1,14 +1,14 @@
-# $Id: dpadd.mk,v 1.27 2019/05/17 13:58:53 sjg Exp $
+# $Id: dpadd.mk,v 1.28 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2004, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -75,7 +75,7 @@
 #	and -L${STAGE_OBJTOP}/usr/lib are sufficient, and we should
 #	have no need of anything else.
 #
-	
+
 .if !target(__${.PARSEFILE}__)
 __${.PARSEFILE}__:
 
@@ -98,7 +98,7 @@ RELOBJTOP?= ${OBJTOP}
 RELSRCTOP?= ${SRCTOP}
 
 # we get included just about everywhere so this is handy...
-# C*DEBUG_XTRA are for defining on cmd line etc 
+# C*DEBUG_XTRA are for defining on cmd line etc
 # so do not use in makefiles.
 .ifdef CFLAGS_DEBUG_XTRA
 CFLAGS_LAST += ${CFLAGS_DEBUG_XTRA}
@@ -180,14 +180,14 @@ SRC_LIBS+= ${_OBJDIR}/lib${LIB}.a
 .endif
 .endif
 
-# 
+#
 # This little bit of magic, assumes that SRC_libfoo will be
 # set if it cannot be correctly derrived from ${LIBFOO}
 # Note that SRC_libfoo and INCLUDES_libfoo should be named for the
 # actual library name not the variable name that might refer to it.
 # 99% of the time the two are the same, but the DPADD logic
 # only has the library name available, so stick to that.
-# 
+#
 
 SRC_LIBS?=
 # magic_libs includes those we want to link with
@@ -198,7 +198,7 @@ DPMAGIC_LIBS += ${__dpadd_magic_libs} \
 
 # we skip this for staged libs
 .for __lib in ${DPMAGIC_LIBS:O:u:N${STAGE_OBJTOP:Unot}*/lib/*}
-# 
+#
 # if SRC_libfoo is not set, then we assume that the srcdir corresponding
 # to where we found the library is correct.
 #
@@ -215,8 +215,8 @@ INCLUDES_${__lib:T:R}?= -I${exists(${SRC_${__lib:T:R}}/h):?${SRC_${__lib:T:R}}/h
 
 .endfor
 
-# even for staged libs we sometimes 
-# need to allow direct -I to avoid cicular dependencies 
+# even for staged libs we sometimes
+# need to allow direct -I to avoid cicular dependencies
 .for __lib in ${DPMAGIC_LIBS:O:u:T:R}
 .if !empty(SRC_${__lib}) && empty(INCLUDES_${__lib})
 # must be a staged lib
@@ -267,7 +267,7 @@ __dpadd_last_incs += ${__dpadd_magic_libs:O:u:@s@${SRC_LIBS_${s:T:R}:U}@:@x@${IN
 __dpadd_last_incs := \
 	${__dpadd_last_incs:N-I/usr/*} \
 	${__dpadd_incs:M-I/usr/*} \
-	${__dpadd_last_incs:M-I/usr/*} 
+	${__dpadd_last_incs:M-I/usr/*}
 __dpadd_incs := ${__dpadd_incs:N-I/usr/*}
 .endif
 
@@ -310,12 +310,12 @@ dpadd:	.NOTMAIN
 .endif
 
 .ifdef SRC_PATHADD
-# We don't want to assume that we need to .PATH every element of 
+# We don't want to assume that we need to .PATH every element of
 # SRC_LIBS, but the Makefile cannot do
 # .PATH: ${SRC_libfoo}
 # since the value of SRC_libfoo must be available at the time .PATH:
-# is read - and we only just worked it out.  
-# Further, they can't wait until after include of {lib,prog}.mk as 
+# is read - and we only just worked it out.
+# Further, they can't wait until after include of {lib,prog}.mk as
 # the .PATH is needed before then.
 # So we let the Makefile do
 # SRC_PATHADD+= ${SRC_libfoo}
diff --git a/mk/files.mk b/mk/files.mk
index fa16b118fb6c..513ab1fd819e 100644
--- a/mk/files.mk
+++ b/mk/files.mk
@@ -1,14 +1,14 @@
-# $Id: files.mk,v 1.6 2017/05/07 02:21:02 sjg Exp $
+# $Id: files.mk,v 1.7 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2017, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/gendirdeps.mk b/mk/gendirdeps.mk
index 82618f2225a3..b977f3c48d99 100644
--- a/mk/gendirdeps.mk
+++ b/mk/gendirdeps.mk
@@ -1,18 +1,18 @@
-# $Id: gendirdeps.mk,v 1.44 2020/06/23 04:21:51 sjg Exp $
+# $Id: gendirdeps.mk,v 1.46 2020/08/19 17:51:53 sjg Exp $
 
 # Copyright (c) 2011-2020, Simon J. Gerraty
 # Copyright (c) 2010-2018, Juniper Networks, Inc.
 # All rights reserved.
-# 
+#
 # Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions 
-# are met: 
+# modification, are permitted provided that the following conditions
+# are met:
 # 1. Redistributions of source code must retain the above copyright
-#    notice, this list of conditions and the following disclaimer. 
+#    notice, this list of conditions and the following disclaimer.
 # 2. Redistributions in binary form must reproduce the above copyright
 #    notice, this list of conditions and the following disclaimer in the
-#    documentation and/or other materials provided with the distribution.  
-# 
+#    documentation and/or other materials provided with the distribution.
+#
 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
@@ -23,7 +23,7 @@
 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 #
 # This makefile [re]generates ${.MAKE.DEPENDFILE}
@@ -104,7 +104,7 @@ GENDIRDEPS_FILTER += ${GENDIRDEPS_FILTER_DIR_VARS:@v@S,${$v},_{${v}},@}
 GENDIRDEPS_FILTER += ${GENDIRDEPS_FILTER_VARS:@v@S,/${$v}/,/_{${v}}/,@:NS,//,*:u}
 .endif
 
-# this (*should* be set in meta.sys.mk) 
+# this (*should* be set in meta.sys.mk)
 # is the script that extracts what we want.
 META2DEPS ?= ${.PARSEDIR}/meta2deps.sh
 META2DEPS := ${META2DEPS}
@@ -122,7 +122,7 @@ _py_d =
 .if ${META2DEPS:E} == "py"
 # we can afford to do this all the time.
 DPDEPS ?= no
-META2DEPS_CMD = ${_time} ${PYTHON} ${META2DEPS} ${_py_d} 
+META2DEPS_CMD = ${_time} ${PYTHON} ${META2DEPS} ${_py_d}
 .if ${DPDEPS:tl} != "no"
 META2DEPS_CMD += -D ${DPDEPS}
 .endif
@@ -155,8 +155,8 @@ M2D_OBJROOTS += ${STAGE_ROOT}
 # and tell it not to add machine qualifiers
 META2DEPS_ARGS += MACHINE=none
 .endif
-.if defined(SB_BACKING_SB) 
-META2DEPS_CMD += -S ${SB_BACKING_SB}/src 
+.if defined(SB_BACKING_SB)
+META2DEPS_CMD += -S ${SB_BACKING_SB}/src
 M2D_OBJROOTS += ${SB_BACKING_SB}/${SB_OBJPREFIX}
 .endif
 
@@ -177,7 +177,7 @@ _meta_files := ${META_FILES:N\*.meta:O:u}
 # assume a big list
 _meta_files_arg= @meta.list
 .if empty(_meta_files) && ${META_FILES:M\*.meta} != ""
-# XXX this should be considered a bad idea, 
+# XXX this should be considered a bad idea,
 # since we cannot ignore stale .meta
 x != cd ${_OBJDIR} && find . -name '*.meta' -print -o \( -type d ! -name . -prune \) | sed 's,^./,,' > meta.list; echo
 .elif ${_meta_files:[#]} > 500
@@ -234,10 +234,10 @@ dir_list += ${ddeps}
 
 # DIRDEPS represent things that had to have been built first
 # so they should all be undir OBJTOP.
-# Note that ${_OBJTOP}/bsd/include/machine will get reported 
+# Note that ${_OBJTOP}/bsd/include/machine will get reported
 # to us as $SRCTOP/bsd/sys/$MACHINE_ARCH/include meaning we
 # will want to visit bsd/include
-# so we add 
+# so we add
 # ${"${dir_list:M*bsd/sys/${MACHINE_ARCH}/include}":?bsd/include:}
 # to GENDIRDEPS_DIR_LIST_XTRAS
 _objtops = ${OBJTOP} ${_OBJTOP} ${_objtop}
@@ -272,11 +272,11 @@ DIRDEPS = \
 
 # We only consider things below $RELDIR/ if they have a makefile.
 # This is the same test that _DIRDEP_USE applies.
-# We have do a double test with dirdep_list as it _may_ contain 
+# We have do a double test with dirdep_list as it _may_ contain
 # qualified dirs - if we got anything from a stage dir.
 # qualdir_list we know are all qualified.
 # It would be nice do peform this check for all of DIRDEPS,
-# but we cannot assume that all of the tree is present, 
+# but we cannot assume that all of the tree is present,
 # in fact we can only assume that RELDIR is.
 DIRDEPS += \
 	${dirdep_list:M${RELDIR}/*:@d@${.MAKE.MAKEFILE_PREFERENCE:@m@${exists(${SRCTOP}/$d/$m):?$d:${exists(${SRCTOP}/${d:R}/$m):?$d:}}@}@} \
@@ -309,7 +309,7 @@ SRC_DIRDEPS = \
 SRC_DIRDEPS := ${SRC_DIRDEPS:${GENDIRDEPS_SRC_FILTER:UN/*:ts:}:C,//+,/,g:O:u}
 
 # if you want to capture SRC_DIRDEPS in .MAKE.DEPENDFILE put
-# SRC_DIRDEPS_FILE = ${_DEPENDFILE} 
+# SRC_DIRDEPS_FILE = ${_DEPENDFILE}
 # in local.gendirdeps.mk
 .if ${SRC_DIRDEPS_FILE:Uno:tl} != "no"
 ECHO_SRC_DIRDEPS = echo 'SRC_DIRDEPS = \'; echo '${SRC_DIRDEPS:@d@	$d \\${.newline}@}'; echo;
@@ -324,7 +324,7 @@ ${SRC_DIRDEPS_FILE}: ${META_FILES} ${_this} ${META2DEPS}
 .endif
 .endif
 .endif
-_include_src_dirdeps ?= 
+_include_src_dirdeps ?=
 
 all:	${_DEPENDFILE}
 
@@ -339,11 +339,7 @@ CAT_DEPEND ?= .depend
 .PHONY: ${_DEPENDFILE}
 .endif
 
-.if ${BUILD_AT_LEVEL0:Uno:tl} == "no"
 LOCAL_DEPENDS_GUARD ?= _{.MAKE.LEVEL} > 0
-.else
-LOCAL_DEPENDS_GUARD ?= _{DEP_RELDIR} == _{_DEP_RELDIR}
-.endif
 
 # 'cat .depend' should suffice, but if we are mixing build modes
 # .depend may contain things we don't want.
diff --git a/mk/host-target.mk b/mk/host-target.mk
index a83642c9698c..3e6094f16730 100644
--- a/mk/host-target.mk
+++ b/mk/host-target.mk
@@ -1,5 +1,5 @@
 # RCSid:
-#	$Id: host-target.mk,v 1.12 2020/07/08 23:35:29 sjg Exp $
+#	$Id: host-target.mk,v 1.13 2020/08/05 23:32:08 sjg Exp $
 
 # Host platform information; may be overridden
 .if !defined(_HOST_OSNAME)
@@ -16,7 +16,7 @@ _HOST_MACHINE != uname -m
 .endif
 .if !defined(_HOST_ARCH)
 # for NetBSD prefer $MACHINE (amd64 rather than x86_64)
-.if ${_HOST_OSNAME:NNetBSD} == ""
+.if ${_HOST_OSNAME:NDarwin:NNetBSD} == ""
 _HOST_ARCH := ${_HOST_MACHINE}
 .else
 _HOST_ARCH != uname -p 2> /dev/null || uname -m
diff --git a/mk/host.libnames.mk b/mk/host.libnames.mk
index c0a13d756d16..3afa73f9d2a3 100644
--- a/mk/host.libnames.mk
+++ b/mk/host.libnames.mk
@@ -1,14 +1,14 @@
-# $Id: host.libnames.mk,v 1.4 2010/01/11 23:01:31 sjg Exp $
+# $Id: host.libnames.mk,v 1.5 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2007-2009, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/inc.mk b/mk/inc.mk
index 74626d34646f..5fc14b32d88d 100644
--- a/mk/inc.mk
+++ b/mk/inc.mk
@@ -1,14 +1,14 @@
-# $Id: inc.mk,v 1.7 2017/05/06 17:29:45 sjg Exp $
+# $Id: inc.mk,v 1.8 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2008, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/init.mk b/mk/init.mk
index d4ae45372d9c..0dae997beb54 100644
--- a/mk/init.mk
+++ b/mk/init.mk
@@ -1,14 +1,14 @@
-# $Id: init.mk,v 1.17 2020/05/25 20:15:07 sjg Exp $
+# $Id: init.mk,v 1.21 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2002, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -65,7 +65,11 @@ CC_PIC?= -DPIC
 CXX_PIC?= ${CC_PIC}
 PROFFLAGS?= -DGPROF -DPROF
 
-.if ${.MAKE.LEVEL:U1} == 0 && ${BUILD_AT_LEVEL0:Uyes:tl} == "no"
+# targets  that are ok  at level 0
+LEVEL0_TARGETS += clean* destory*
+M_ListToSkip= O:u:S,^,N,:ts:
+
+.if ${.MAKE.LEVEL:U1} == 0 && ${MK_DIRDEPS_BUILD:Uno} == "yes" && ${.TARGETS:Uall:${LEVEL0_TARGETS:${M_ListToSkip}}} != ""
 # this tells lib.mk and prog.mk to not actually build anything
 _SKIP_BUILD = not building at level 0
 .endif
diff --git a/mk/install-mk b/mk/install-mk
index 486bcd25d05f..66185e42cc6e 100644
--- a/mk/install-mk
+++ b/mk/install-mk
@@ -33,7 +33,7 @@
 #
 #	All our *.mk files are copied to "dest" with appropriate
 #	ownership and permissions.
-#	
+#
 #	By default if a sys.mk can be found in a standard location
 #	(that bmake will find) then no sys.mk will be put in "dest".
 #
@@ -55,22 +55,22 @@
 #       Simon J. Gerraty <sjg@crufty.net>
 
 # RCSid:
-#	$Id: install-mk,v 1.174 2020/07/10 21:50:14 sjg Exp $
+#	$Id: install-mk,v 1.179 2020/08/26 21:49:45 sjg Exp $
 #
 #	@(#) Copyright (c) 1994 Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
 
-MK_VERSION=20200710
+MK_VERSION=20200826
 OWNER=
 GROUP=
 MODE=444
@@ -137,7 +137,7 @@ if [ -s $SYS_MK -a -d $dest ]; then
 	sys_mk_dir=`realpath $SYS_MK_DIR`
 	if [ $dest = $sys_mk_dir ]; then
 		case "$os" in
-		*BSD*)	SKIP_SYS_MK=: 
+		*BSD*)	SKIP_SYS_MK=:
 			SKIP_BSD_MK=:
 			;;
 		*)	# could be fake?
diff --git a/mk/install-new.mk b/mk/install-new.mk
index ddfff20e3b85..d312bdc26d46 100644
--- a/mk/install-new.mk
+++ b/mk/install-new.mk
@@ -1,14 +1,14 @@
-# $Id: install-new.mk,v 1.3 2012/03/24 18:25:49 sjg Exp $
+# $Id: install-new.mk,v 1.4 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2009, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/java.mk b/mk/java.mk
index ef4a5ea0ba10..e2149e7089a7 100644
--- a/mk/java.mk
+++ b/mk/java.mk
@@ -1,16 +1,16 @@
 #
 # RCSid:
-#	$Id: java.mk,v 1.14 2007/11/22 08:16:25 sjg Exp $
+#	$Id: java.mk,v 1.15 2020/08/19 17:51:53 sjg Exp $
 
 #	@(#) Copyright (c) 1998-2001, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -53,7 +53,7 @@ JAVAC_FLAGS+= ${JAVAC_DBG}
 
 .if defined(MAKE_VERSION) && !defined(NO_CLASSES_COOKIE)
 # java works best by compiling a bunch of classes at once.
-# this lot does that but needs a recent netbsd make or 
+# this lot does that but needs a recent netbsd make or
 # or its portable cousin bmake.
 .for __s in ${SRCS}
 __c:= ${__classdest}${__s:.java=.class}
diff --git a/mk/lib.mk b/mk/lib.mk
index 03d24e27cbe6..c3979414ec49 100644
--- a/mk/lib.mk
+++ b/mk/lib.mk
@@ -1,4 +1,4 @@
-# $Id: lib.mk,v 1.70 2020/05/02 02:10:20 sjg Exp $
+# $Id: lib.mk,v 1.71 2020/08/19 17:51:53 sjg Exp $
 
 .if !target(__${.PARSEFILE}__)
 __${.PARSEFILE}__:
@@ -67,7 +67,7 @@ META_NOECHO?= echo
 		# Alpha-specific shared library flags
 FPICFLAGS ?= -fPIC
 CPICFLAGS ?= -fPIC -DPIC
-CPPPICFLAGS?= -DPIC 
+CPPPICFLAGS?= -DPIC
 CAPICFLAGS?= ${CPPPICFLAGS} ${CPICFLAGS}
 APICFLAGS ?=
 .elif ${MACHINE_ARCH} == "mipsel" || ${MACHINE_ARCH} == "mipseb"
@@ -87,7 +87,7 @@ MKPICLIB= no
 
 .elif (${MACHINE_ARCH} == "sparc" || ${MACHINE_ARCH} == "sparc64") && \
        ${OBJECT_FMT} == "ELF"
-# If you use -fPIC you need to define BIGPIC to turn on 32-bit 
+# If you use -fPIC you need to define BIGPIC to turn on 32-bit
 # relocations in asm code
 FPICFLAGS ?= -fPIC
 CPICFLAGS ?= -fPIC -DPIC
@@ -102,7 +102,7 @@ SHLIB_SOVERSION=${SHLIB_FULLVERSION}
 SHLIB_SHFLAGS=
 FPICFLAGS ?= -fPIC
 CPICFLAGS?= -fPIC -DPIC
-CPPPICFLAGS?= -DPIC 
+CPPPICFLAGS?= -DPIC
 CAPICFLAGS?= ${CPPPICFLAGS} ${CPICFLAGS}
 APICFLAGS?= -k
 
@@ -246,7 +246,7 @@ DLLIB ?= -ldl
 .endif
 
 # some libs have lots of objects, and scanning all .o, .po and ${PICO} meta files
-# is a waste of time, this tells meta.autodep.mk to just pick one 
+# is a waste of time, this tells meta.autodep.mk to just pick one
 # (typically ${PICO})
 # yes, 42 is a random number.
 .if ${MK_DIRDEPS_BUILD} == "yes" && ${SRCS:Uno:[\#]} > 42
@@ -272,7 +272,7 @@ ${CXX_SUFFIXES:%=%.o}:
 	${COMPILE.cc} ${.IMPSRC}
 
 .S.o .s.o:
-	${COMPILE.S} ${CFLAGS:M-[ID]*} ${AINC} ${.IMPSRC} 
+	${COMPILE.S} ${CFLAGS:M-[ID]*} ${AINC} ${.IMPSRC}
 
 .if (${LD_X} == "")
 .c.po:
@@ -382,7 +382,7 @@ _LIBS+= ${libLDORDER_INC}
 .endif
 
 .if !defined(_SKIP_BUILD)
-realbuild: ${_LIBS} 
+realbuild: ${_LIBS}
 .endif
 
 all: _SUBDIRUSE
diff --git a/mk/libnames.mk b/mk/libnames.mk
index b0eabed5ea53..8140360714df 100644
--- a/mk/libnames.mk
+++ b/mk/libnames.mk
@@ -1,14 +1,14 @@
-# $Id: libnames.mk,v 1.8 2016/04/05 15:58:37 sjg Exp $
+# $Id: libnames.mk,v 1.9 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2007-2009, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/libs.mk b/mk/libs.mk
index 7f974bfa86b9..9f0079d6e511 100644
--- a/mk/libs.mk
+++ b/mk/libs.mk
@@ -1,14 +1,14 @@
-# $Id: libs.mk,v 1.3 2013/08/02 18:28:48 sjg Exp $
+# $Id: libs.mk,v 1.6 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2006, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -82,12 +82,18 @@ UPDATE_DEPENDFILE = NO
 LIBS_TARGETS+= cleandepend cleandir cleanobj depend install
 
 .for b in ${LIBS:R:T:S,^lib,,}
-lib$b.a: ${SRCS} ${DPADD} ${SRCS_lib$b} ${DPADD_lib$b} 
-	(cd ${.CURDIR} && ${.MAKE} -f ${MAKEFILE} LIB=$b)
+lib$b.a: ${SRCS} ${DPADD} ${SRCS_lib$b} ${DPADD_lib$b}
+	(cd ${.CURDIR} && ${.MAKE} -f ${MAKEFILE} LIB=$b -DWITHOUT_META_STATS)
 
 .for t in ${LIBS_TARGETS:O:u}
 $b.$t: .PHONY .MAKE
-	(cd ${.CURDIR} && ${.MAKE} -f ${MAKEFILE} LIB=$b ${@:E})
+	(cd ${.CURDIR} && ${.MAKE} -f ${MAKEFILE} LIB=$b ${@:E} -DWITHOUT_META_STATS)
 .endfor
 .endfor
+
+.if !defined(WITHOUT_META_STATS) && ${.MAKE.LEVEL} > 0
+.END: _reldir_finish
+.ERROR: _reldir_failed
+.endif
+
 .endif
diff --git a/mk/links.mk b/mk/links.mk
index aac3914fdd00..6bf0db080c23 100644
--- a/mk/links.mk
+++ b/mk/links.mk
@@ -1,14 +1,14 @@
-# $Id: links.mk,v 1.6 2014/09/29 17:14:40 sjg Exp $
+# $Id: links.mk,v 1.7 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2005, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/manifest.mk b/mk/manifest.mk
index 797038d19391..1e2f728f094e 100644
--- a/mk/manifest.mk
+++ b/mk/manifest.mk
@@ -1,14 +1,14 @@
-# $Id: manifest.mk,v 1.2 2014/10/31 18:06:17 sjg Exp $
+# $Id: manifest.mk,v 1.3 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2014, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -21,12 +21,12 @@
 # ${MANIFEST}.DIRS += bin sbin usr/bin ...
 # for each dir we have a ${MANIFEST}.SRCS.$dir
 # that provides the absolute path to the contents
-# ${MANIFEST}.SRCS.bin += ${OBJTOP}/bin/sh/sh 
+# ${MANIFEST}.SRCS.bin += ${OBJTOP}/bin/sh/sh
 # ${MANIFEST}.SYMLINKS is a list of src target pairs
 # for each file/dir there are a number of attributes
 # UID GID MODE FLAGS
 # which can be set per dir, per file or we use defaults
-# eg.  
+# eg.
 # MODE.sbin = 550
 # MODE.usr/sbin = 550
 # MODE.dirs = 555
@@ -37,7 +37,7 @@
 # means passwd gets 4555 other files in usr/bin get 555 and
 # files in usr/sbin get 500
 # STORE defaults to basename of src and target directory
-# but we can use 
+# but we can use
 # ${MANIFEST}.SRCS.sbin += ${OBJTOP}/bin/sh-static/sh-static
 # STORE.sbin/sh-static = sbin/sh
 #
@@ -47,7 +47,7 @@
 UID.dirs ?= 0
 GID.dirs ?= 0
 MODE.dirs ?= 775
-FLAGS.dirs ?= 
+FLAGS.dirs ?=
 
 UID.files ?= 0
 GID.files ?= 0
diff --git a/mk/meta.autodep.mk b/mk/meta.autodep.mk
index 9cb3f14ea0c6..5e18c35fa560 100644
--- a/mk/meta.autodep.mk
+++ b/mk/meta.autodep.mk
@@ -1,4 +1,4 @@
-# $Id: meta.autodep.mk,v 1.50 2018/06/08 01:25:31 sjg Exp $
+# $Id: meta.autodep.mk,v 1.52 2020/07/18 05:57:57 sjg Exp $
 
 #
 #	@(#) Copyright (c) 2010, Simon J. Gerraty
@@ -57,7 +57,7 @@ _OBJTOP ?= ${OBJTOP}
 _OBJROOT ?= ${OBJROOT:U${_OBJTOP}}
 _DEPENDFILE := ${_CURDIR}/${.MAKE.DEPENDFILE:T}
 
-.if ${.MAKE.LEVEL} > 0 || ${BUILD_AT_LEVEL0:Uyes:tl} == "yes"
+.if ${.MAKE.LEVEL} > 0
 # do not allow auto update if we ever built this dir without filemon
 NO_FILEMON_COOKIE = .nofilemon
 CLEANFILES += ${NO_FILEMON_COOKIE}
@@ -73,10 +73,8 @@ UPDATE_DEPENDFILE = NO
 .endif
 
 .if ${.MAKE.LEVEL} == 0
-.if ${BUILD_AT_LEVEL0:Uyes:tl} == "no"
 UPDATE_DEPENDFILE = NO
 .endif
-.endif
 .if !exists(${_DEPENDFILE})
 _bootstrap_dirdeps = yes
 .endif
@@ -283,9 +281,7 @@ ${_DEPENDFILE}: ${_depend} ${.PARSEDIR}/gendirdeps.mk  ${META2DEPS} $${.MAKE.MET
 .endif
 
 .if ${_bootstrap_dirdeps} == "yes"
-.if ${BUILD_AT_LEVEL0:Uno} == "no"
 DIRDEPS+= ${RELDIR}.${TARGET_SPEC:U${MACHINE}}
-.endif
 # make sure this is included at least once
 .include <dirdeps.mk>
 .else
@@ -312,7 +308,7 @@ _reldir_finish: .NOMETA
 _reldir_failed: .NOMETA
 	@echo "${TIME_STAMP} Failed ${RELDIR}.${TARGET_SPEC} seconds=$$(( ${now_utc} - ${start_utc} )) ${meta_stats}"
 
-.if defined(WITH_META_STATS) && ${.MAKE.LEVEL} > 0
+.if !defined(WITHOUT_META_STATS) && ${.MAKE.LEVEL} > 0
 .END: _reldir_finish
 .ERROR: _reldir_failed
 .endif
diff --git a/mk/meta.stage.mk b/mk/meta.stage.mk
index 3b4624fc4599..c78685760c11 100644
--- a/mk/meta.stage.mk
+++ b/mk/meta.stage.mk
@@ -1,14 +1,14 @@
-# $Id: meta.stage.mk,v 1.59 2020/04/25 18:18:27 sjg Exp $
+# $Id: meta.stage.mk,v 1.60 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2011-2017, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/meta.subdir.mk b/mk/meta.subdir.mk
index d27de1079f87..39cf875d6b77 100644
--- a/mk/meta.subdir.mk
+++ b/mk/meta.subdir.mk
@@ -1,4 +1,4 @@
-# $Id: meta.subdir.mk,v 1.11 2015/11/24 22:26:51 sjg Exp $
+# $Id: meta.subdir.mk,v 1.12 2020/08/19 17:51:53 sjg Exp $
 
 #
 #	@(#) Copyright (c) 2010, Simon J. Gerraty
@@ -6,10 +6,10 @@
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -32,7 +32,7 @@ DIRDEPS = ${SUBDIR:N.WAIT:O:u:@d@${RELDIR}/$d@}
 .include <meta.autodep.mk>
 .else
 # this is the cunning bit
-# actually it is probably a bit risky 
+# actually it is probably a bit risky
 # since we may pickup subdirs which are not relevant
 # the alternative is a walk through the tree though
 # which is difficult without a sub-make.
diff --git a/mk/meta.sys.mk b/mk/meta.sys.mk
index a561e04534f8..77b4893a8785 100644
--- a/mk/meta.sys.mk
+++ b/mk/meta.sys.mk
@@ -1,4 +1,4 @@
-# $Id: meta.sys.mk,v 1.36 2020/05/16 23:21:48 sjg Exp $
+# $Id: meta.sys.mk,v 1.38 2020/08/19 17:51:53 sjg Exp $
 
 #
 #	@(#) Copyright (c) 2010-2020, Simon J. Gerraty
@@ -6,10 +6,10 @@
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -155,12 +155,6 @@ dirdeps:
 # the first .MAIN: is what counts
 # by default dirdeps is all we want at level0
 .MAIN: dirdeps
-# tell dirdeps.mk what we want
-BUILD_AT_LEVEL0 = no
-.endif
-.if ${.TARGETS:Nall} == "" 
-# it works best if we do everything via sub-makes
-BUILD_AT_LEVEL0 ?= no
 .endif
 
 .endif
diff --git a/mk/meta2deps.py b/mk/meta2deps.py
index 253287a87d1b..9231003b70df 100755
--- a/mk/meta2deps.py
+++ b/mk/meta2deps.py
@@ -25,7 +25,7 @@ We only pay attention to a subset of the information in the
 
 'W'	files opened for write or read-write,
 	for filemon V3 and earlier.
-        
+
 'E'	files executed.
 
 'L'	files linked
@@ -37,20 +37,20 @@ We only pay attention to a subset of the information in the
 
 """
 RCSid:
-	$Id: meta2deps.py,v 1.30 2020/06/08 23:05:00 sjg Exp $
+	$Id: meta2deps.py,v 1.33 2020/08/19 17:51:53 sjg Exp $
 
-	Copyright (c) 2011-2019, Simon J. Gerraty
+	Copyright (c) 2011-2020, Simon J. Gerraty
 	Copyright (c) 2011-2017, Juniper Networks, Inc.
 	All rights reserved.
 
 	Redistribution and use in source and binary forms, with or without
-	modification, are permitted provided that the following conditions 
-	are met: 
+	modification, are permitted provided that the following conditions
+	are met:
 	1. Redistributions of source code must retain the above copyright
-	   notice, this list of conditions and the following disclaimer. 
+	   notice, this list of conditions and the following disclaimer.
 	2. Redistributions in binary form must reproduce the above copyright
 	   notice, this list of conditions and the following disclaimer in the
-	   documentation and/or other materials provided with the distribution.  
+	   documentation and/or other materials provided with the distribution.
 
 	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 	"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
@@ -62,8 +62,8 @@ RCSid:
 	DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 	(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-	OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
- 
+	OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
 """
 
 import os, re, sys
@@ -164,7 +164,7 @@ def sort_unique(list, cmp=None, key=None, reverse=False):
 
 def add_trims(x):
     return ['/' + x + '/',
-            '/' + x, 
+            '/' + x,
             x + '/',
             x]
 
@@ -181,7 +181,7 @@ class MetaFile:
     obj_deps = []
     src_deps = []
     file_deps = []
-    
+
     def __init__(self, name, conf={}):
         """if name is set we will parse it now.
         conf can have the follwing keys:
@@ -198,7 +198,7 @@ class MetaFile:
 
         TARGET_SPEC
                 Sometimes MACHINE isn't enough.
-                
+
         HOST_TARGET
                 when we build for the pseudo machine 'host'
                 the object tree uses HOST_TARGET rather than MACHINE.
@@ -222,7 +222,7 @@ class MetaFile:
         debug_out open file to send debug output to (sys.stderr)
 
         """
-        
+
         self.name = name
         self.debug = getv(conf, 'debug', 0)
         self.debug_out = getv(conf, 'debug_out', sys.stderr)
@@ -310,11 +310,11 @@ class MetaFile:
         self.obj_deps = []
         self.src_deps = []
         self.file_deps = []
-          
+
     def dirdeps(self, sep='\n'):
         """return DIRDEPS"""
         return sep.strip() + sep.join(self.obj_deps)
-    
+
     def src_dirdeps(self, sep='\n'):
         """return SRC_DIRDEPS"""
         return sep.strip() + sep.join(self.src_deps)
@@ -333,7 +333,7 @@ class MetaFile:
     def seenit(self, dir):
         """rememer that we have seen dir."""
         self.seen[dir] = 1
-          
+
     def add(self, list, data, clue=''):
         """add data to list if it isn't already there."""
         if data not in list:
@@ -392,10 +392,10 @@ class MetaFile:
             # give a useful clue
             print('{}:{}: '.format(self.name, self.line), end=' ', file=sys.stderr)
             raise
-        
+
     def parse(self, name=None, file=None):
         """A meta file looks like:
-        
+
         # Meta data file "path"
         CMD "command-line"
         CWD "cwd"
@@ -506,6 +506,8 @@ class MetaFile:
                 continue
             elif w[0] in 'ERWS':
                 path = w[2]
+                if path == '.':
+                    continue
                 self.parse_path(path, cwd, w[0], w)
 
         if not file:
@@ -601,13 +603,13 @@ class MetaFile:
                 self.seenit(w[2])
                 self.seenit(dir)
 
-                            
+
 def main(argv, klass=MetaFile, xopts='', xoptf=None):
     """Simple driver for class MetaFile.
 
     Usage:
         script [options] [key=value ...] "meta" ...
-        
+
     Options and key=value pairs contribute to the
     dictionary passed to MetaFile.
 
@@ -615,7 +617,7 @@ def main(argv, klass=MetaFile, xopts='', xoptf=None):
                 add "SRCTOP" to the "SRCTOPS" list.
 
     -C "CURDIR"
-    
+
     -O "OBJROOT"
                 add "OBJROOT" to the "OBJROOTS" list.
 
@@ -626,7 +628,7 @@ def main(argv, klass=MetaFile, xopts='', xoptf=None):
     -H "HOST_TARGET"
 
     -D "DPDEPS"
-    
+
     -d  bumps debug level
 
     """
@@ -666,7 +668,7 @@ def main(argv, klass=MetaFile, xopts='', xoptf=None):
 
     debug = 0
     output = True
-    
+
     opts, args = getopt.getopt(argv[1:], 'a:dS:C:O:R:m:D:H:qT:X:' + xopts)
     for o, a in opts:
         if o == '-a':
diff --git a/mk/meta2deps.sh b/mk/meta2deps.sh
index 59ccf7bd6216..e56e52a89675 100755
--- a/mk/meta2deps.sh
+++ b/mk/meta2deps.sh
@@ -5,11 +5,11 @@
 #
 # SYNOPSIS:
 #	meta2deps.sh SB="SB" "meta" ...
-#	
+#
 # DESCRIPTION:
 #	This script looks each "meta" file and extracts the
 #	information needed to deduce build and src dependencies.
-#	
+#
 #	To do this, we extract the 'CWD' record as well as all the
 #	syscall traces which describe 'R'ead, 'C'hdir and 'E'xec
 #	syscalls.
@@ -77,20 +77,20 @@
 
 
 # RCSid:
-#	$Id: meta2deps.sh,v 1.12 2016/12/13 20:44:16 sjg Exp $
+#	$Id: meta2deps.sh,v 1.13 2020/08/19 17:51:53 sjg Exp $
 
 # Copyright (c) 2010-2013, Juniper Networks, Inc.
 # All rights reserved.
-# 
+#
 # Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions 
-# are met: 
+# modification, are permitted provided that the following conditions
+# are met:
 # 1. Redistributions of source code must retain the above copyright
-#    notice, this list of conditions and the following disclaimer. 
+#    notice, this list of conditions and the following disclaimer.
 # 2. Redistributions in binary form must reproduce the above copyright
 #    notice, this list of conditions and the following disclaimer in the
-#    documentation and/or other materials provided with the distribution.  
-# 
+#    documentation and/or other materials provided with the distribution.
+#
 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
@@ -101,14 +101,14 @@
 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 meta2src() {
     cat /dev/null "$@" |
     sed -n '/^R .*\.[chyl]$/s,^..[0-9]* ,,p' |
     sort -u
 }
-    
+
 meta2dirs() {
     cat /dev/null "$@" |
     sed -n '/^R .*\/.*\.[a-z0-9][^\/]*$/s,^..[0-9]* \(.*\)/[^/]*$,\1,p' |
@@ -169,7 +169,7 @@ meta2deps() {
     case "$MACHINE" in
     host) _ht=$HOST_TARGET;;
     esac
-    
+
     for o in $OBJROOTS
     do
 	case "$MACHINE,/$o/" in
@@ -203,7 +203,7 @@ meta2deps() {
     obj_re=
     add_list '|' -s '/*' src_re $SRCTOPS
     add_list '|' -s '*' obj_re $OBJROOTS
-    
+
     [ -z "$RELDIR" ] && unset DPDEPS
     tf=/tmp/m2d$$-$USER
     rm -f $tf.*
@@ -274,10 +274,10 @@ meta2deps() {
 	    eval cwd_$pid=$cwd
 	    continue
 	    ;;
-	F,*) # $path is new pid  
+	F,*) # $path is new pid
 	    eval cwd_$path=$cwd ldir_$path=$ldir
 	    continue
-	    ;;	  
+	    ;;
 	*)  dir=${path%/*}
 	    case "$path" in
 	    $src_re|$obj_re) ;;
diff --git a/mk/mk-files.txt b/mk/mk-files.txt
index 4e69dfec0808..282f9b87a63c 100644
--- a/mk/mk-files.txt
+++ b/mk/mk-files.txt
@@ -14,17 +14,17 @@ Many years ago, when building large software projects, I used GNU make
 to simplify developing complex build trees.
 
 Since the early 90's my main development machines, run BSD
-(NetBSD_ to be precise), and the BSD source tree is good example of a
-large software project.   It quickly became clear that
-``/usr/share/mk/*.mk`` were a great model, but were quite tightly
-linked to building the BSD tree.
+(NetBSD_ to be precise, and more recently FreeBSD), and the BSD source
+tree is good example of a large software project.
+It quickly became clear that ``/usr/share/mk/*.mk`` were a great
+model, but at the time were quite tightly linked to building the BSD tree.
 
 Much as I liked using NetBSD, my customers were more likely to be
 using SunOS, HP-UX etc, so I started on bmake_ and a portable collection
 of mk-files (mk.tar.gz_).  NetBSD provided much of the original structure.
 
 Since then I've added a lot of features to NetBSD's make and hence to
-bmake which is kept closely in sync.  The mk-files however have 
+bmake which is kept closely in sync.  The mk-files however have
 diverged quite a bit, though ideas are still picked up from NetBSD
 and FreeBSD.
 
@@ -59,7 +59,7 @@ in such cases even the ``SRCS`` line is unnecessary as ``prog.mk``
 will default it to ``${PROG}.c``.
 
 It is the sensible use of defaults and the plethora of macro modifiers
-provided by bmake_ that allow simple makefiles such as the above
+provided by bmake_ that allow simple makefiles such as the above to
 *just work* on many different systems.
 
 
@@ -67,7 +67,7 @@ mk-files
 ========
 
 This section provides a brief description of some of the ``*.mk``
-files. 
+files.
 
 sys.mk
 ------
@@ -76,7 +76,7 @@ When bmake starts, it looks for ``sys.mk`` and reads it before doing
 anything else.  Thus, this is the place to setup the environment for
 everyone else.
 
-In this distribution, sys.mk avoids doing anything platform dependent.
+In this distribution, ``sys.mk`` avoids doing anything platform dependent.
 It is quite short, and includes a number of other files (which may or
 may not exists)
 
@@ -97,7 +97,7 @@ examples/sys.clean-env.mk
 			PATH HOME USER LOGNAME \
 			SRCTOP OBJTOP OBJROOT \
 			${_env_vars}
-		
+
 		_env_vars != env | egrep '^(${MAKE_SAVE_ENV_PREFIX:ts|})' | sed 's,=.*,,'; echo
 		_export_list =
 		.for v in ${MAKE_SAVE_ENV_VARS:O:u}
@@ -169,7 +169,7 @@ lib${LIB}.a
 	An archive lib of ``.o`` files, this is the default
 
 lib${LIB}_p.a
-	A profiled lib of ``.po`` files.  
+	A profiled lib of ``.po`` files.
 	Still an archive lib, but all the objects are built with
 	profiling in mind - hence the different extension.
 	It is skipped if ``MKPROFILE`` is "no".
@@ -194,7 +194,7 @@ lib${LIB}.${LD_so}
 
 There is a lot of platform specific tweaking in ``lib.mk``, largely the
 result of the original distributions trying to avoid interfering with
-the system's ``sys.mk``. 
+the system's ``sys.mk``.
 
 libnames.mk
 -----------
@@ -204,22 +204,19 @@ include ``*.libnames.mk`` of which:
 
 local.libnames.mk
 	does not exist unless you create it.  It is a handy way for you
-	to customize without touching the distributed files. 
+	to customize without touching the distributed files.
 	For example, on a test machine I needed to build openssl but
-	not install it, so put the following in ``local.libnames.mk``:: 
+	not install it, so put the following in ``local.libnames.mk``::
 
 		.if ${host_os} == "sunos"
 		LIBCRYPTO = ${OBJTOP}/openssl/lib/crypto/libcrypto${DLIBEXT}
 		LIBSSL = ${OBJTOP}/openssl/lib/ssl/libssl${DLIBEXT}
 		INCLUDES_libcrypto = -I${OBJ_libcrypto}
 		.endif
-		
+
 	The makefile created an openssl dir in ``${OBJ_libcrypto}`` to
 	gather all the headers. dpadd.mk_ did the rest.
 
-sjg.libnames.mk
-	not part of the mk-files distribution.
-
 host.libnames.mk
 	contains logic to find any libs named in ``HOST_LIBS`` in
 	``HOST_LIBDIRS``.
@@ -248,7 +245,7 @@ else in various ways::
 	# it also has the same effect as SRC_LIBS
 	DPADD += ${LIBSSLFD}
 
-	# indicate that not only must libsslfd be built, 
+	# indicate that not only must libsslfd be built,
 	# but that we need to link with it.
 	# this is almost exactly equivalent to
 	# DPADD += ${LIBSSLFD}
@@ -324,7 +321,7 @@ If ``SUBDIR_MUST_EXIST`` is set, missing directories cause an error,
 otherwise a warning is issued.  If you don't even want the warning,
 set ``MISSING_DIR=continue``.
 
-Traditionally, ``subdir.mk`` prints clue as it visits each subdir::
+Traditionally, ``subdir.mk`` prints clues as it visits each subdir::
 
 	===> ssl
 	===> ssl/lib
@@ -342,7 +339,7 @@ links.mk
 
 Provides rules for processing lists of ``LINKS`` and ``SYMLINKS``.
 Each is expected to be a list of ``link`` and ``target`` pairs
-(``link`` -> ``target``). 
+(``link`` -> ``target``).
 
 The logic is generally in a ``_*_SCRIPT`` which is referenced in a
 ``_*_USE`` (``.USE``) target.
@@ -353,7 +350,7 @@ For example::
 
 	SYMLINKS += ${.CURDIR}/${MACHINE_ARCH}/include machine
 	header_links: _BUILD_SYMLINKS_USE
-	
+
 	md.o: header_links
 
 would create a symlink called ``machine`` in ``${.OBJDIR}`` pointing to
@@ -394,10 +391,10 @@ post process the ``.d`` files into ``.depend``.
 auto.dep.mk
 -----------
 
-A much simpler implementation than autodep.mk_ it uses 
+A much simpler implementation than autodep.mk_ it uses
 ``-MF ${.TARGET:T}.d``
-to avoid possible conflicts during parallel builds.  
-This precludes the use of suffix rules to drive ``make depend``, so 
+to avoid possible conflicts during parallel builds.
+This precludes the use of suffix rules to drive ``make depend``, so
 dep.mk_ handles that if specifically requested.
 
 options.mk
@@ -452,7 +449,7 @@ on sparc, ``-Wno-unused`` would replace ``-Wunused``.
 
 You should never need to edit ``warnings.mk``, it will include
 ``warnings-sets.mk`` if it exists and you use that to make any local
-customizations. 
+customizations.
 
 rst2htm.mk
 ----------
@@ -470,7 +467,7 @@ Meta mode
 =========
 
 The 20110505 and later versions of ``mk-files`` include a number of
-makefiles contributed by Juniper Networks, Inc.  
+makefiles contributed by Juniper Networks, Inc.
 These allow the latest version of bmake_ to run in `meta mode`_
 see `dirdeps.mk`_
 
@@ -499,5 +496,5 @@ where you unpacked the tar file, you can::
 .. _mk.tar.gz: http://www.crufty.net/ftp/pub/sjg/mk.tar.gz
 
 :Author: sjg@crufty.net
-:Revision: $Id: mk-files.txt,v 1.18 2018/12/08 07:27:15 sjg Exp $
+:Revision: $Id: mk-files.txt,v 1.20 2020/08/19 17:51:53 sjg Exp $
 :Copyright: Crufty.NET
diff --git a/mk/mkopt.sh b/mk/mkopt.sh
index 929a5aa83a66..4a42c0ddf122 100755
--- a/mk/mkopt.sh
+++ b/mk/mkopt.sh
@@ -1,16 +1,16 @@
 #!/bin/sh
 
-# $Id: mkopt.sh,v 1.12 2020/06/23 04:16:35 sjg Exp $
+# $Id: mkopt.sh,v 1.13 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2014, 2020, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/obj.mk b/mk/obj.mk
index b936379d4f6b..487e25a55b6c 100644
--- a/mk/obj.mk
+++ b/mk/obj.mk
@@ -1,14 +1,14 @@
-# $Id: obj.mk,v 1.15 2012/11/11 22:37:02 sjg Exp $
+# $Id: obj.mk,v 1.16 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 1999-2010, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/options.mk b/mk/options.mk
index a03c6cde34dc..eb5253a6b7e8 100644
--- a/mk/options.mk
+++ b/mk/options.mk
@@ -1,14 +1,14 @@
-# $Id: options.mk,v 1.11 2020/05/02 21:23:52 sjg Exp $
+# $Id: options.mk,v 1.13 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2012, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -77,4 +77,6 @@ ${OPTION_PREFIX}${o:H} ?= no
 ${OPTION_PREFIX}${o:H} ?= ${${OPTION_PREFIX}${o:T}}
 .endif
 .endfor
-.undef OPTIONS_DEFAULT_VALUES OPTIONS_DEFAULT_NO OPTIONS_DEFAULT_YES
+.undef OPTIONS_DEFAULT_VALUES
+.undef OPTIONS_DEFAULT_NO
+.undef OPTIONS_DEFAULT_YES
diff --git a/mk/own.mk b/mk/own.mk
index 4c8425b0ee7c..b20b9e5e2c35 100644
--- a/mk/own.mk
+++ b/mk/own.mk
@@ -1,4 +1,4 @@
-# $Id: own.mk,v 1.40 2018/04/23 04:53:57 sjg Exp $
+# $Id: own.mk,v 1.41 2020/08/19 17:51:53 sjg Exp $
 
 .if !target(__${.PARSEFILE}__)
 __${.PARSEFILE}__:
@@ -61,7 +61,7 @@ YACC.y?=	${YACC} ${YFLAGS}
 
 # for suffix rules
 IMPFLAGS?=	${COPTS.${.IMPSRC:T}} ${CPUFLAGS.${.IMPSRC:T}} ${CPPFLAGS.${.IMPSRC:T}}
-.for s in .c .cc 
+.for s in .c .cc
 COMPILE.$s += ${IMPFLAGS}
 LINK.$s +=  ${IMPFLAGS}
 .endfor
@@ -211,8 +211,8 @@ CFLAGS+= ${CPPFLAGS}
 
 # allow for per target flags
 # apply the :T:R first, so the more specific :T can override if needed
-CPPFLAGS += ${CPPFLAGS_${.TARGET:T:R}} ${CPPFLAGS_${.TARGET:T}} 
-CFLAGS += ${CFLAGS_${.TARGET:T:R}} ${CFLAGS_${.TARGET:T}} 
+CPPFLAGS += ${CPPFLAGS_${.TARGET:T:R}} ${CPPFLAGS_${.TARGET:T}}
+CFLAGS += ${CFLAGS_${.TARGET:T:R}} ${CFLAGS_${.TARGET:T}}
 
 # Define SYS_INCLUDE to indicate whether you want symbolic links to the system
 # source (``symlinks''), or a separate copy (``copies''); (latter useful
diff --git a/mk/prlist.mk b/mk/prlist.mk
index 09d7dfdacc97..aca1fde25555 100644
--- a/mk/prlist.mk
+++ b/mk/prlist.mk
@@ -1,14 +1,14 @@
-# $Id: prlist.mk,v 1.3 2008/07/17 16:24:57 sjg Exp $
+# $Id: prlist.mk,v 1.4 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2006, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/prog.mk b/mk/prog.mk
index 4bc6260d208b..ea48837d5544 100644
--- a/mk/prog.mk
+++ b/mk/prog.mk
@@ -1,4 +1,4 @@
-#	$Id: prog.mk,v 1.35 2018/01/26 20:04:07 sjg Exp $
+#	$Id: prog.mk,v 1.36 2020/08/19 17:51:53 sjg Exp $
 
 .if !target(__${.PARSEFILE}__)
 __${.PARSEFILE}__:
@@ -99,7 +99,7 @@ _PROGLDOPTS+=	-Wl,-dynamic-linker=${_SHLINKER}
 _PROGLDOPTS+=	-Wl,-rpath-link,${DESTDIR}${SHLIBDIR}:${DESTDIR}/usr/lib \
 		-L${DESTDIR}${SHLIBDIR}
 .endif
-_PROGLDOPTS+=	-Wl,-rpath,${SHLIBDIR}:/usr/lib 
+_PROGLDOPTS+=	-Wl,-rpath,${SHLIBDIR}:/usr/lib
 
 .if defined(PROG_CXX)
 _CCLINK=	${CXX}
diff --git a/mk/progs.mk b/mk/progs.mk
index 7ccebbffb44c..16c381a50bf9 100644
--- a/mk/progs.mk
+++ b/mk/progs.mk
@@ -1,14 +1,14 @@
-# $Id: progs.mk,v 1.13 2013/08/02 18:28:48 sjg Exp $
+# $Id: progs.mk,v 1.16 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2006, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -87,11 +87,11 @@ x.$p= PROG_CXX=$p
 .endif
 
 $p ${p}_p: .PHONY .MAKE
-	(cd ${.CURDIR} && ${.MAKE} -f ${MAKEFILE} PROG=$p ${x.$p})
+	(cd ${.CURDIR} && ${.MAKE} -f ${MAKEFILE} PROG=$p ${x.$p} -DWITHOUT_META_STATS)
 
 .for t in ${PROGS_TARGETS:O:u}
 $p.$t: .PHONY .MAKE
-	(cd ${.CURDIR} && ${.MAKE} -f ${MAKEFILE} PROG=$p ${x.$p} ${@:E})
+	(cd ${.CURDIR} && ${.MAKE} -f ${MAKEFILE} PROG=$p ${x.$p} ${@:E} -DWITHOUT_META_STATS)
 .endfor
 .endfor
 
@@ -99,4 +99,9 @@ $p.$t: .PHONY .MAKE
 $t: ${PROGS:%=%.$t}
 .endfor
 
+.if !defined(WITHOUT_META_STATS) && ${.MAKE.LEVEL} > 0
+.END: _reldir_finish
+.ERROR: _reldir_failed
+.endif
+
 .endif
diff --git a/mk/rst2htm.mk b/mk/rst2htm.mk
index 296b73c68574..1db9792f4127 100644
--- a/mk/rst2htm.mk
+++ b/mk/rst2htm.mk
@@ -1,14 +1,14 @@
-# $Id: rst2htm.mk,v 1.10 2015/09/08 22:17:46 sjg Exp $
+# $Id: rst2htm.mk,v 1.11 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2009, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/scripts.mk b/mk/scripts.mk
index 9b6d82f49253..5ea2474e65a3 100644
--- a/mk/scripts.mk
+++ b/mk/scripts.mk
@@ -1,14 +1,14 @@
-# $Id: scripts.mk,v 1.3 2017/05/06 17:29:45 sjg Exp $
+# $Id: scripts.mk,v 1.4 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2006, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/srctop.mk b/mk/srctop.mk
index fab090c80d39..91594c7a98e4 100644
--- a/mk/srctop.mk
+++ b/mk/srctop.mk
@@ -1,14 +1,14 @@
-# $Id: srctop.mk,v 1.3 2012/11/11 23:20:18 sjg Exp $
+# $Id: srctop.mk,v 1.4 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2012, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -46,7 +46,7 @@ SRCTOP!= cd ${.CURDIR}; while :; do \
 		here=`pwd`; \
 		${_SRCTOP_TEST_} && { echo $$here; break; }; \
 		case $$here in /*/*/*) cd ..;; *) echo ""; break;; esac; \
-		done 
+		done
 .endif
 .if defined(SRCTOP) && exists(${SRCTOP}/.)
 .export SRCTOP
diff --git a/mk/stage-install.sh b/mk/stage-install.sh
index 64d044fa048c..674652d1d482 100755
--- a/mk/stage-install.sh
+++ b/mk/stage-install.sh
@@ -28,22 +28,26 @@
 #	"file".dirdep placed in "dest" or "dest".dirdep if it happed
 #	to be a file rather than a directory.
 #
+#	Before we run install(1), we check if "dest" needs to be a
+#	directory (more than one file in "args") and create it
+#	if necessary.
+#
 # SEE ALSO:
 #	meta.stage.mk
-#	
+#
 
 # RCSid:
-#	$Id: stage-install.sh,v 1.5 2013/04/19 16:32:24 sjg Exp $
+#	$Id: stage-install.sh,v 1.9 2020/08/28 01:04:13 sjg Exp $
 #
-#	@(#) Copyright (c) 2013, Simon J. Gerraty
+#	@(#) Copyright (c) 2013-2020, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -59,6 +63,45 @@ do
     esac
 done
 
+# get last entry from "$@" without side effects
+last_entry() {
+    while [ $# -gt 8 ]
+    do
+        shift 8
+    done
+    eval last=\$$#
+    echo $last
+}
+
+# mkdir $dest if needed (more than one file)
+mkdir_if_needed() {
+    (
+        lf=
+        while [ $# -gt 8 ]
+        do
+            shift 4
+        done
+        for f in "$@"
+        do
+            [ -f $f ] || continue
+            [ $f = $dest ] && continue
+            if [ -n "$lf" ]; then
+                # dest must be a directory
+                mkdir -p $dest
+                break
+            fi
+            lf=$f
+        done
+    )
+}
+
+args="$@"
+dest=`last_entry "$@"`
+case " $args " in
+*" -d "*) ;;
+*) [ -e $dest ] || mkdir_if_needed "$@";;
+esac
+
 # if .dirdep doesn't exist, just run install and be done
 _DIRDEP=${_DIRDEP:-$OBJDIR/.dirdep}
 [ -s $_DIRDEP ] && EXEC= || EXEC=exec
@@ -80,12 +123,6 @@ StageDirdep() {
   LnCp $_DIRDEP $t.dirdep || exit 1
 }
 
-args="$@"
-while [ $# -gt 8 ]
-do
-    shift 8
-done
-eval dest=\$$#
 if [ -f $dest ]; then
     # a file, there can be only one .dirdep needed
     StageDirdep $dest
diff --git a/mk/sys.clean-env.mk b/mk/sys.clean-env.mk
index 5ac74bba9342..88d32cb3c6e9 100644
--- a/mk/sys.clean-env.mk
+++ b/mk/sys.clean-env.mk
@@ -1,14 +1,14 @@
-# $Id: sys.clean-env.mk,v 1.22 2017/10/25 23:44:20 sjg Exp $
+# $Id: sys.clean-env.mk,v 1.23 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2009, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -55,7 +55,7 @@ MAKE_ENV_SAVE_PREFIX_LIST += \
 MAKE_ENV_SAVE_EXCLUDE_LIST ?= _
 
 # This is the actual list that we will save
-# HOME is probably something worth clobbering eg. 
+# HOME is probably something worth clobbering eg.
 # HOME=/var/empty
 MAKE_ENV_SAVE_VAR_LIST += \
 	HOME \
diff --git a/mk/sys.debug.mk b/mk/sys.debug.mk
index 7debfc6cf051..7fde27c24fdb 100644
--- a/mk/sys.debug.mk
+++ b/mk/sys.debug.mk
@@ -1,14 +1,14 @@
-# $Id: sys.debug.mk,v 1.1 2016/10/01 19:11:55 sjg Exp $
+# $Id: sys.debug.mk,v 1.2 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2009, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/sys.dependfile.mk b/mk/sys.dependfile.mk
index 5389c24fd1bb..7c1fd94d3eb8 100644
--- a/mk/sys.dependfile.mk
+++ b/mk/sys.dependfile.mk
@@ -1,14 +1,14 @@
-# $Id: sys.dependfile.mk,v 1.8 2016/03/11 01:34:13 sjg Exp $
+# $Id: sys.dependfile.mk,v 1.9 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2012, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/sys.mk b/mk/sys.mk
index 37e37390eb27..b21dfa8b4ebf 100644
--- a/mk/sys.mk
+++ b/mk/sys.mk
@@ -1,14 +1,14 @@
-# $Id: sys.mk,v 1.47 2020/04/17 21:08:17 sjg Exp $
+# $Id: sys.mk,v 1.51 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2003-2009, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -77,6 +77,7 @@ OPTIONS_DEFAULT_DEPENDENT += \
 	AUTO_OBJ/DIRDEPS_BUILD \
 	META_MODE/DIRDEPS_BUILD \
 	STAGING/DIRDEPS_BUILD \
+	STATIC_DIRDEPS_CACHE/DIRDEPS_CACHE \
 
 .-include <options.mk>
 
@@ -116,7 +117,7 @@ ROOT_GROUP != sed -n /:0:/s/:.*//p /etc/group
 
 unix ?= We run ${_HOST_OSNAME}.
 
-# We need a Bourne/POSIX shell 
+# We need a Bourne/POSIX shell
 MAKE_SHELL ?= sh
 SHELL ?= ${MAKE_SHELL}
 
diff --git a/mk/sys.vars.mk b/mk/sys.vars.mk
index 2f2e66700266..24e0ed26a15f 100644
--- a/mk/sys.vars.mk
+++ b/mk/sys.vars.mk
@@ -1,20 +1,20 @@
-# $Id: sys.vars.mk,v 1.4 2019/05/27 20:22:52 sjg Exp $
+# $Id: sys.vars.mk,v 1.5 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2003-2009, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
 
 # We use the following paradigm for preventing multiple inclusion.
-# It relies on the fact that conditionals and dependencies are resolved 
+# It relies on the fact that conditionals and dependencies are resolved
 # at the time they are read.
 #
 # _this ?= ${.PARSEFILE}
diff --git a/mk/sys/AIX.mk b/mk/sys/AIX.mk
index b848d99fb4e8..d591385be603 100644
--- a/mk/sys/AIX.mk
+++ b/mk/sys/AIX.mk
@@ -28,9 +28,9 @@ CC ?=		gcc
 DBG ?=		-O -g
 STATIC ?=		-static
 .else
-CC ?=             cc 
+CC ?=             cc
 DBG ?=         -g
-STATIC ?=         
+STATIC ?=
 .endif
 CFLAGS ?=		${DBG}
 COMPILE.c ?=	${CC} ${CFLAGS} ${CPPFLAGS} -c
@@ -138,7 +138,7 @@ ${CXX_SUFFIXES:%=%.a}:
 .s:
 	${LINK.s} -o ${.TARGET} ${.IMPSRC} ${LDLIBS}
 .s.o:
-	${COMPILE.s} -o ${.TARGET} ${.IMPSRC} 
+	${COMPILE.s} -o ${.TARGET} ${.IMPSRC}
 .s.a:
 	${COMPILE.s} ${.IMPSRC}
 	${AR} ${ARFLAGS} $@ $*.o
@@ -162,7 +162,7 @@ ${CXX_SUFFIXES:%=%.a}:
 	mv lex.yy.c ${.TARGET}
 .l.o:
 	${LEX.l} ${.IMPSRC}
-	${COMPILE.c} -o ${.TARGET} lex.yy.c 
+	${COMPILE.c} -o ${.TARGET} lex.yy.c
 	rm -f lex.yy.c
 
 # Yacc
diff --git a/mk/sys/Darwin.mk b/mk/sys/Darwin.mk
index ea04b416ff3a..06918a11a4ad 100644
--- a/mk/sys/Darwin.mk
+++ b/mk/sys/Darwin.mk
@@ -176,7 +176,7 @@ ${CXX_SUFFIXES:%=%.a}:
 .s:
 	${LINK.s} -o ${.TARGET} ${.IMPSRC} ${LDLIBS}
 .s.o:
-	${COMPILE.s} -o ${.TARGET} ${.IMPSRC} 
+	${COMPILE.s} -o ${.TARGET} ${.IMPSRC}
 .s.a:
 	${COMPILE.s} ${.IMPSRC}
 	${AR} ${ARFLAGS} $@ $*.o
@@ -200,7 +200,7 @@ ${CXX_SUFFIXES:%=%.a}:
 	mv lex.yy.c ${.TARGET}
 .l.o:
 	${LEX.l} ${.IMPSRC}
-	${COMPILE.c} -o ${.TARGET} lex.yy.c 
+	${COMPILE.c} -o ${.TARGET} lex.yy.c
 	rm -f lex.yy.c
 
 # Yacc
diff --git a/mk/sys/Generic.mk b/mk/sys/Generic.mk
index 9a3d3bffc183..51c72990f2ea 100644
--- a/mk/sys/Generic.mk
+++ b/mk/sys/Generic.mk
@@ -1,4 +1,4 @@
-#	$Id: Generic.mk,v 1.16 2020/06/29 14:34:42 sjg Exp $
+#	$Id: Generic.mk,v 1.17 2020/08/19 17:51:53 sjg Exp $
 #
 
 # some reasonable defaults
@@ -31,7 +31,7 @@ ARFLAGS ?=	rl
 
 AS ?=		as
 AFLAGS ?=
-.if ${MACHINE_ARCH} == "sparc64" 
+.if ${MACHINE_ARCH} == "sparc64"
 AFLAGS+= -Wa,-Av9a
 .endif
 COMPILE.s ?=	${CC} ${AFLAGS} -c
@@ -56,7 +56,7 @@ COMPILE.m ?=	${OBJC} ${OBJCFLAGS} ${CPPFLAGS} -c
 LINK.m ?=	${OBJC} ${OBJCFLAGS} ${CPPFLAGS} ${LDFLAGS}
 
 CPP ?=		cpp
-CPPFLAGS ?=	
+CPPFLAGS ?=
 
 FC ?=		f77
 FFLAGS ?=	-O
@@ -181,7 +181,7 @@ ${CXX_SUFFIXES:%=%.a}:
 	mv lex.yy.c ${.TARGET}
 .l.o:
 	${LEX.l} ${.IMPSRC}
-	${COMPILE.c} -o ${.TARGET} lex.yy.c 
+	${COMPILE.c} -o ${.TARGET} lex.yy.c
 	rm -f lex.yy.c
 
 # Yacc
diff --git a/mk/sys/HP-UX.mk b/mk/sys/HP-UX.mk
index 34dd2881d83d..f1c23148c186 100644
--- a/mk/sys/HP-UX.mk
+++ b/mk/sys/HP-UX.mk
@@ -1,4 +1,4 @@
-#	$Id: HP-UX.mk,v 1.14 2020/06/29 14:34:42 sjg Exp $
+#	$Id: HP-UX.mk,v 1.15 2020/08/19 17:51:53 sjg Exp $
 #	$NetBSD: sys.mk,v 1.19.2.1 1994/07/26 19:58:31 cgd Exp $
 #	@(#)sys.mk	5.11 (Berkeley) 3/13/91
 
@@ -8,7 +8,7 @@ unix ?=		We run ${OS}.
 
 # HP-UX's cc does not provide any clues as to wether this is 9.x or 10.x
 # nor does sys/param.h, so we'll use the existence of /hp-ux
-.if exists("/hp-ux") 
+.if exists("/hp-ux")
 OSMAJOR ?=9
 .endif
 OSMAJOR ?=10
@@ -60,7 +60,7 @@ PICFLAG ?= +z
 LD_x=
 DBG ?=-g -O
 .endif
-DBG ?=         
+DBG ?=
 STATIC ?=         -Wl,-a,archive
 .endif
 .if (${__HPUX_VERSION} == "10")
@@ -180,7 +180,7 @@ ${CXX_SUFFIXES:%=%.a}:
 .s:
 	${LINK.s} -o ${.TARGET} ${.IMPSRC} ${LDLIBS}
 .s.o:
-	${COMPILE.s} -o ${.TARGET} ${.IMPSRC} 
+	${COMPILE.s} -o ${.TARGET} ${.IMPSRC}
 .s.a:
 	${COMPILE.s} ${.IMPSRC}
 	${AR} ${ARFLAGS} $@ $*.o
@@ -204,7 +204,7 @@ ${CXX_SUFFIXES:%=%.a}:
 	mv lex.yy.c ${.TARGET}
 .l.o:
 	${LEX.l} ${.IMPSRC}
-	${COMPILE.c} -o ${.TARGET} lex.yy.c 
+	${COMPILE.c} -o ${.TARGET} lex.yy.c
 	rm -f lex.yy.c
 
 # Yacc
diff --git a/mk/sys/IRIX.mk b/mk/sys/IRIX.mk
index a194c1e78a11..00af15027f6e 100644
--- a/mk/sys/IRIX.mk
+++ b/mk/sys/IRIX.mk
@@ -44,7 +44,7 @@ COMPILE.m ?=	${OBJC} ${OBJCFLAGS} ${CPPFLAGS} -c
 LINK.m ?=	${OBJC} ${OBJCFLAGS} ${CPPFLAGS} ${LDFLAGS}
 
 CPP ?=		CC
-CPPFLAGS ?=	
+CPPFLAGS ?=
 
 FC ?=		f77
 FFLAGS ?=	-O
@@ -173,7 +173,7 @@ ${CXX_SUFFIXES:%=%.a}:
 	mv lex.yy.c ${.TARGET}
 .l.o:
 	${LEX.l} ${.IMPSRC}
-	${COMPILE.c} -o ${.TARGET} lex.yy.c 
+	${COMPILE.c} -o ${.TARGET} lex.yy.c
 	rm -f lex.yy.c
 
 # Yacc
diff --git a/mk/sys/Linux.mk b/mk/sys/Linux.mk
index bf80bb3addce..3cdc4dbe1a62 100644
--- a/mk/sys/Linux.mk
+++ b/mk/sys/Linux.mk
@@ -1,4 +1,4 @@
-#	$Id: Linux.mk,v 1.12 2020/06/29 14:34:42 sjg Exp $
+#	$Id: Linux.mk,v 1.13 2020/08/19 17:51:53 sjg Exp $
 #	$NetBSD: sys.mk,v 1.19.2.1 1994/07/26 19:58:31 cgd Exp $
 #	@(#)sys.mk	5.11 (Berkeley) 3/13/91
 
@@ -141,7 +141,7 @@ ${CXX_SUFFIXES:%=%.a}:
 .s:
 	${LINK.s} -o ${.TARGET} ${.IMPSRC} ${LDLIBS}
 .s.o:
-	${COMPILE.s} -o ${.TARGET} ${.IMPSRC} 
+	${COMPILE.s} -o ${.TARGET} ${.IMPSRC}
 .s.a:
 	${COMPILE.s} ${.IMPSRC}
 	${AR} ${ARFLAGS} $@ $*.o
@@ -165,7 +165,7 @@ ${CXX_SUFFIXES:%=%.a}:
 	mv lex.yy.c ${.TARGET}
 .l.o:
 	${LEX.l} ${.IMPSRC}
-	${COMPILE.c} -o ${.TARGET} lex.yy.c 
+	${COMPILE.c} -o ${.TARGET} lex.yy.c
 	rm -f lex.yy.c
 
 # Yacc
diff --git a/mk/sys/NetBSD.mk b/mk/sys/NetBSD.mk
index 2e72cc742e4f..6629a4445a2e 100644
--- a/mk/sys/NetBSD.mk
+++ b/mk/sys/NetBSD.mk
@@ -79,7 +79,7 @@ COMPILE.m ?=	${OBJC} ${OBJCFLAGS} ${CPPFLAGS} -c
 LINK.m ?=	${OBJC} ${OBJCFLAGS} ${CPPFLAGS} ${LDFLAGS}
 
 CPP ?=		cpp
-CPPFLAGS ?=	
+CPPFLAGS ?=
 
 FC ?=		f77
 FFLAGS ?=	-O
@@ -208,7 +208,7 @@ ${CXX_SUFFIXES:%=%.a}:
 	mv lex.yy.c ${.TARGET}
 .l.o:
 	${LEX.l} ${.IMPSRC}
-	${COMPILE.c} -o ${.TARGET} lex.yy.c 
+	${COMPILE.c} -o ${.TARGET} lex.yy.c
 	rm -f lex.yy.c
 
 # Yacc
diff --git a/mk/sys/OSF1.mk b/mk/sys/OSF1.mk
index 4c1a09f6f5f7..88e0ea28b930 100644
--- a/mk/sys/OSF1.mk
+++ b/mk/sys/OSF1.mk
@@ -1,4 +1,4 @@
-#	$Id: OSF1.mk,v 1.11 2020/06/29 14:34:42 sjg Exp $
+#	$Id: OSF1.mk,v 1.12 2020/08/19 17:51:53 sjg Exp $
 #	$NetBSD: sys.mk,v 1.19.2.1 1994/07/26 19:58:31 cgd Exp $
 #	@(#)sys.mk	5.11 (Berkeley) 3/13/91
 
@@ -31,7 +31,7 @@ LINK.s ?=		${CC} ${AFLAGS} ${LDFLAGS}
 COMPILE.S ?=	${CC} ${AFLAGS} ${CPPFLAGS} -c
 LINK.S ?=		${CC} ${AFLAGS} ${CPPFLAGS} ${LDFLAGS}
 .if exists(/opt/gnu/bin/gcc) || exists(/usr/local/bin/gcc)
-CC ?=		gcc 
+CC ?=		gcc
 .else
 CC ?=             cc -std
 .endif
@@ -176,7 +176,7 @@ ${CXX_SUFFIXES:%=%.a}:
 	mv lex.yy.c ${.TARGET}
 .l.o:
 	${LEX.l} ${.IMPSRC}
-	${COMPILE.c} -o ${.TARGET} lex.yy.c 
+	${COMPILE.c} -o ${.TARGET} lex.yy.c
 	rm -f lex.yy.c
 
 # Yacc
diff --git a/mk/sys/OpenBSD.mk b/mk/sys/OpenBSD.mk
index d73bb0d82c9a..7440a231e3bf 100644
--- a/mk/sys/OpenBSD.mk
+++ b/mk/sys/OpenBSD.mk
@@ -21,7 +21,7 @@ RANLIB ?=	ranlib
 
 AS ?=		as
 AFLAGS ?=
-.if ${MACHINE_ARCH} == "sparc64" 
+.if ${MACHINE_ARCH} == "sparc64"
 AFLAGS+= -Wa,-Av9a
 .endif
 COMPILE.s ?=	${CC} ${AFLAGS} -c
@@ -54,7 +54,7 @@ COMPILE.m ?=	${OBJC} ${OBJCFLAGS} ${CPPFLAGS} -c
 LINK.m ?=	${OBJC} ${OBJCFLAGS} ${CPPFLAGS} ${LDFLAGS}
 
 CPP ?=		cpp
-CPPFLAGS ?=	
+CPPFLAGS ?=
 
 FC ?=		f77
 FFLAGS ?=	-O
@@ -183,7 +183,7 @@ ${CXX_SUFFIXES:%=%.a}:
 	mv lex.yy.c ${.TARGET}
 .l.o:
 	${LEX.l} ${.IMPSRC}
-	${COMPILE.c} -o ${.TARGET} lex.yy.c 
+	${COMPILE.c} -o ${.TARGET} lex.yy.c
 	rm -f lex.yy.c
 
 # Yacc
diff --git a/mk/sys/SunOS.mk b/mk/sys/SunOS.mk
index e79cd3a7dbd4..4369c8d43b93 100644
--- a/mk/sys/SunOS.mk
+++ b/mk/sys/SunOS.mk
@@ -1,4 +1,4 @@
-#	$Id: SunOS.mk,v 1.11 2020/06/29 14:34:42 sjg Exp $
+#	$Id: SunOS.mk,v 1.12 2020/08/19 17:51:53 sjg Exp $
 
 .if ${.PARSEFILE} == "sys.mk"
 .include <host-target.mk>
@@ -197,7 +197,7 @@ ${CXX_SUFFIXES:%=%.a}:
 	mv lex.yy.c ${.TARGET}
 .l.o:
 	${LEX.l} ${.IMPSRC}
-	${COMPILE.c} -o ${.TARGET} lex.yy.c 
+	${COMPILE.c} -o ${.TARGET} lex.yy.c
 	rm -f lex.yy.c
 
 # Yacc
diff --git a/mk/sys/UnixWare.mk b/mk/sys/UnixWare.mk
index 888f0d90c0b4..272d3e65c2d8 100644
--- a/mk/sys/UnixWare.mk
+++ b/mk/sys/UnixWare.mk
@@ -1,4 +1,4 @@
-#	$Id: UnixWare.mk,v 1.6 2020/06/29 14:34:42 sjg Exp $
+#	$Id: UnixWare.mk,v 1.7 2020/08/19 17:51:53 sjg Exp $
 #	based on "Id: SunOS.5.sys.mk,v 1.6 2003/09/30 16:42:23 sjg Exp "
 #	$NetBSD: sys.mk,v 1.19.2.1 1994/07/26 19:58:31 cgd Exp $
 #	@(#)sys.mk	5.11 (Berkeley) 3/13/91
@@ -219,7 +219,7 @@ ${CXX_SUFFIXES:%=%.a}:
 	mv lex.yy.c ${.TARGET}
 .l.o:
 	${LEX.l} ${.IMPSRC}
-	${COMPILE.c} -o ${.TARGET} lex.yy.c 
+	${COMPILE.c} -o ${.TARGET} lex.yy.c
 	rm -f lex.yy.c
 
 # Yacc
diff --git a/mk/target-flags.mk b/mk/target-flags.mk
index 4525dbd08d7b..789f09b23115 100644
--- a/mk/target-flags.mk
+++ b/mk/target-flags.mk
@@ -27,27 +27,27 @@
 #	variables.  The original version of this macro file did
 #	elaborate things with CFLAGS.  The current, simpler
 #	implementation is ultimately more flexible.
-#	
+#
 #	It is important that target-flags.mk is included after other
 #	macro files and that target specific flags that may reference
 #	_$v are set after that.
-#	
+#
 #	Only works with a make(1) that does nested evaluation correctly.
 
 
 
 # RCSid:
-#	$Id: target-flags.mk,v 1.9 2014/04/05 22:56:54 sjg Exp $
+#	$Id: target-flags.mk,v 1.10 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 1998-2002, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/mk/warnings.mk b/mk/warnings.mk
index 7fb3ebd4a8f8..77635fbc8a29 100644
--- a/mk/warnings.mk
+++ b/mk/warnings.mk
@@ -1,15 +1,15 @@
 # RCSid:
-#	$Id: warnings.mk,v 1.14 2016/04/05 15:58:37 sjg Exp $
+#	$Id: warnings.mk,v 1.15 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2002, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
@@ -28,10 +28,10 @@ MIN_WARNINGS?= -Wall \
 	-Wformat \
 	-Wimplicit \
 	-Wunused \
-	-Wuninitialized 
+	-Wuninitialized
 
 LOW_WARNINGS?= ${MIN_WARNINGS} -W -Wstrict-prototypes -Wmissing-prototypes
- 
+
 MEDIUM_WARNINGS?= ${LOW_WARNINGS} -Werror
 
 HIGH_WARNINGS?= ${MEDIUM_WARNINGS} \
@@ -84,7 +84,7 @@ W_uninitialized=
 
 
 # .for loops have the [dis]advantage of being evaluated when read,
-# so adding to WARNINGS_SET[_${MACHINE_ARCH}] after this file is 
+# so adding to WARNINGS_SET[_${MACHINE_ARCH}] after this file is
 # read has no effect.
 # Replacing the above .for loops with the WARNINGS+= below solves that
 # but tiggers a double free bug in bmake-20040118 and earlier.
@@ -95,11 +95,11 @@ W_uninitialized=
 # In the second :@ "loop", the ::?= noise sets W_foo?=-Wfoo etc
 # which makes it easy to turn off override individual flags
 # (see W_uninitialized above).
-# 
+#
 # The last bit expands to ${W_foo_${.TARGET:T}:U${W_foo}}
 # which is the bit we ultimately want.  It allows W_* to be set on a
 # per target basis.
-# 
+#
 # NOTE: that we force the target extension to be .o
 #
 
@@ -110,7 +110,7 @@ M_warnings_list = @s@$${$$s_WARNINGS}@:O:u:@w@$${$${w:C/-(.)/\1_/}::?=$$w} $${$$
 _warnings = ${WARNINGS_SET_${MACHINE_ARCH}:U${WARNINGS_SET}:${M_warnings_list}}
 # now a list of all -Wno-* overrides not just those defined by WARNINGS_SET
 # since things like -Wall imply lots of others.
-# this should be a super-set of the -Wno-* in _warnings, but 
+# this should be a super-set of the -Wno-* in _warnings, but
 # just in case...
 _no_warnings = ${_warnings:M-Wno-*} ${ALL_WARNINGS_SETS:${M_warnings_list}:M-Wno-*}
 # -Wno-* must follow any others
diff --git a/mk/whats.mk b/mk/whats.mk
index 1dacb4c14365..e10964463d4a 100644
--- a/mk/whats.mk
+++ b/mk/whats.mk
@@ -1,20 +1,20 @@
-# $Id: whats.mk,v 1.9 2020/05/09 19:48:53 sjg Exp $
+# $Id: whats.mk,v 1.10 2020/08/19 17:51:53 sjg Exp $
 #
 #	@(#) Copyright (c) 2014-2020, Simon J. Gerraty
 #
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
 
 .if ${MK_WHATSTRING:Uno} == "yes"
-# it can be useful to embed a what(1) string in binaries 
+# it can be useful to embed a what(1) string in binaries
 # so that the build location can be seen from a core file.
 .if defined(KMOD)
 what_thing ?= ${KMOD}
diff --git a/mk/yacc.mk b/mk/yacc.mk
index 910b004431df..7f7e99578d70 100644
--- a/mk/yacc.mk
+++ b/mk/yacc.mk
@@ -1,4 +1,4 @@
-# $Id: yacc.mk,v 1.6 2011/06/10 22:45:46 sjg Exp $
+# $Id: yacc.mk,v 1.7 2020/08/19 17:51:53 sjg Exp $
 
 #
 #	@(#) Copyright (c) 1999-2011, Simon J. Gerraty
@@ -6,10 +6,10 @@
 #	This file is provided in the hope that it will
 #	be of use.  There is absolutely NO WARRANTY.
 #	Permission to copy, redistribute or otherwise
-#	use this file is hereby granted provided that 
+#	use this file is hereby granted provided that
 #	the above copyright notice and this notice are
-#	left intact. 
-#      
+#	left intact.
+#
 #	Please send copies of changes and bug-fixes to:
 #	sjg@crufty.net
 #
diff --git a/nonints.h b/nonints.h
index 3126650c9b59..6bcbd7690adb 100644
--- a/nonints.h
+++ b/nonints.h
@@ -1,4 +1,4 @@
-/*	$NetBSD: nonints.h,v 1.78 2020/07/03 07:40:13 rillig Exp $	*/
+/*	$NetBSD: nonints.h,v 1.102 2020/08/30 19:56:02 rillig Exp $	*/
 
 /*-
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -73,7 +73,7 @@
  */
 
 /* arch.c */
-ReturnStatus Arch_ParseArchive(char **, Lst, GNode *);
+Boolean Arch_ParseArchive(char **, Lst, GNode *);
 void Arch_Touch(GNode *);
 void Arch_TouchLib(GNode *);
 time_t Arch_MTime(GNode *);
@@ -82,7 +82,7 @@ void Arch_FindLib(GNode *, Lst);
 Boolean Arch_LibOODate(GNode *);
 void Arch_Init(void);
 void Arch_End(void);
-int Arch_IsLib(GNode *);
+Boolean Arch_IsLib(GNode *);
 
 /* compat.c */
 int CompatRunCommand(void *, void *);
@@ -91,8 +91,8 @@ int Compat_Make(void *, void *);
 
 /* cond.c */
 struct If;
-int Cond_EvalExpression(const struct If *, char *, Boolean *, int, Boolean);
-int Cond_Eval(char *);
+CondEvalResult Cond_EvalExpression(const struct If *, char *, Boolean *, int, Boolean);
+CondEvalResult Cond_Eval(char *);
 void Cond_restore_depth(unsigned int);
 unsigned int Cond_save_depth(void);
 
@@ -125,7 +125,6 @@ char *cached_realpath(const char *, char *);
 
 /* parse.c */
 void Parse_Error(int, const char *, ...) MAKE_ATTR_PRINTFLIKE(2, 3);
-Boolean Parse_AnyExport(void);
 Boolean Parse_IsVar(char *);
 void Parse_DoVar(char *, GNode *);
 void Parse_AddIncludeDir(char *);
@@ -136,12 +135,24 @@ void Parse_SetInput(const char *, int, int, char *(*)(void *, size_t *), void *)
 Lst Parse_MainName(void);
 
 /* str.c */
-char *str_concat(const char *, const char *, int);
-char **brk_string(const char *, int *, Boolean, char **);
+typedef struct {
+    char **words;
+    size_t len;
+    void *freeIt;
+} Words;
+
+Words Str_Words(const char *, Boolean);
+static inline void MAKE_ATTR_UNUSED
+Words_Free(Words w) {
+    free(w.words);
+    free(w.freeIt);
+}
+
+char *str_concat2(const char *, const char *);
+char *str_concat3(const char *, const char *, const char *);
+char *str_concat4(const char *, const char *, const char *, const char *);
 char *Str_FindSubstring(const char *, const char *);
 Boolean Str_Match(const char *, const char *);
-char *Str_SYSVMatch(const char *, const char *, size_t *, Boolean *);
-void Str_SYSVSubst(Buffer *, char *, char *, size_t, Boolean);
 
 #ifndef HAVE_STRLCPY
 /* strlcpy.c */
@@ -153,11 +164,11 @@ void Suff_ClearSuffixes(void);
 Boolean Suff_IsTransform(char *);
 GNode *Suff_AddTransform(char *);
 int Suff_EndTransform(void *, void *);
-void Suff_AddSuffix(char *, GNode **);
+void Suff_AddSuffix(const char *, GNode **);
 Lst Suff_GetPath(char *);
 void Suff_DoPaths(void);
 void Suff_AddInclude(char *);
-void Suff_AddLib(char *);
+void Suff_AddLib(const char *);
 void Suff_FindDeps(GNode *);
 Lst Suff_FindPath(GNode *);
 void Suff_SetNull(char *);
@@ -168,6 +179,7 @@ void Suff_PrintAll(void);
 /* targ.c */
 void Targ_Init(void);
 void Targ_End(void);
+void Targ_Stats(void);
 Lst Targ_List(void);
 GNode *Targ_NewGN(const char *);
 GNode *Targ_FindNode(const char *, int);
@@ -182,31 +194,40 @@ char *Targ_FmtTime(time_t);
 void Targ_PrintType(int);
 void Targ_PrintGraph(int);
 void Targ_Propagate(void);
-void Targ_Propagate_Wait(void);
 
 /* var.c */
 
 typedef enum {
-	VARF_UNDEFERR = 1,
-	VARF_WANTRES = 2,
-	VARF_ASSIGN = 4
-} Varf_Flags;
+    /* Treat undefined variables as errors. */
+    VARE_UNDEFERR	= 0x01,
+    /* Expand and evaluate variables during parsing. */
+    VARE_WANTRES	= 0x02,
+    VARE_ASSIGN		= 0x04
+} VarEvalFlags;
+
+typedef enum {
+    VAR_NO_EXPORT	= 0x01,	/* do not export */
+    /* Make the variable read-only. No further modification is possible,
+     * except for another call to Var_Set with the same flag. */
+    VAR_SET_READONLY	= 0x02
+} VarSet_Flags;
+
 
 void Var_Delete(const char *, GNode *);
 void Var_Set(const char *, const char *, GNode *);
+void Var_Set_with_flags(const char *, const char *, GNode *, VarSet_Flags);
 void Var_Append(const char *, const char *, GNode *);
 Boolean Var_Exists(const char *, GNode *);
-char *Var_Value(const char *, GNode *, char **);
-char *Var_Parse(const char *, GNode *, Varf_Flags, int *, void **);
-char *Var_Subst(const char *, const char *, GNode *, Varf_Flags);
-char *Var_GetTail(const char *);
-char *Var_GetHead(const char *);
+const char *Var_Value(const char *, GNode *, char **);
+const char *Var_Parse(const char *, GNode *, VarEvalFlags, int *, void **);
+char *Var_Subst(const char *, GNode *, VarEvalFlags);
 void Var_Init(void);
 void Var_End(void);
+void Var_Stats(void);
 void Var_Dump(GNode *);
 void Var_ExportVars(void);
-void Var_Export(char *, int);
-void Var_UnExport(char *);
+void Var_Export(const char *, Boolean);
+void Var_UnExport(const char *);
 
 /* util.c */
 void (*bmake_signal(int, void (*)(int)))(int);
diff --git a/os.sh b/os.sh
index e13f7ff58ece..7e6823b240c3 100755
--- a/os.sh
+++ b/os.sh
@@ -17,7 +17,7 @@
 #	Simon J. Gerraty <sjg@crufty.net>
 
 # RCSid:
-#	$Id: os.sh,v 1.55 2017/12/11 20:31:41 sjg Exp $
+#	$Id: os.sh,v 1.56 2020/08/05 23:25:22 sjg Exp $
 #
 #	@(#) Copyright (c) 1994 Simon J. Gerraty
 #
@@ -86,6 +86,9 @@ AIX)	# everyone loves to be different...
 	PS_AXC=-e
 	SHARE_ARCH=$OS/$OSMAJOR.X
 	;;
+Darwin) # a bit like BSD
+        HOST_ARCH=$MACHINE
+        ;;
 SunOS)
 	CHOWN=`Which chown /usr/etc:/usr/bin`
 	export CHOWN
@@ -208,12 +211,12 @@ esac
 
 TMP_DIRS=${TMP_DIRS:-"/tmp /var/tmp"}
 MACHINE_ARCH=${MACHINE_ARCH:-$MACHINE}
-case "$MACHINE_ARCH" in
+HOST_ARCH=${HOST_ARCH:-$MACHINE_ARCH}
+case "$HOST_ARCH" in
 x86*64|amd64) MACHINE32_ARCH=i386;;
 *64) MACHINE32_ARCH=`echo $MACHINE_ARCH | sed 's,64,32,'`;;
 *) MACHINE32_ARCH=$MACHINE_ARCH;;
 esac
-HOST_ARCH=${HOST_ARCH:-$MACHINE_ARCH}
 HOST_ARCH32=${HOST_ARCH32:-$MACHINE32_ARCH}
 # we mount server:/share/arch/$SHARE_ARCH as /usr/local
 SHARE_ARCH_DEFAULT=$OS/$OSMAJOR.X/$HOST_ARCH
diff --git a/parse.c b/parse.c
index 348595824507..ee065dd709dc 100644
--- a/parse.c
+++ b/parse.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: parse.c,v 1.236 2020/07/03 08:13:23 rillig Exp $	*/
+/*	$NetBSD: parse.c,v 1.275 2020/09/01 17:38:26 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -69,14 +69,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: parse.c,v 1.236 2020/07/03 08:13:23 rillig Exp $";
+static char rcsid[] = "$NetBSD: parse.c,v 1.275 2020/09/01 17:38:26 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)parse.c	8.3 (Berkeley) 3/19/94";
 #else
-__RCSID("$NetBSD: parse.c,v 1.236 2020/07/03 08:13:23 rillig Exp $");
+__RCSID("$NetBSD: parse.c,v 1.275 2020/09/01 17:38:26 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -125,17 +125,13 @@ __RCSID("$NetBSD: parse.c,v 1.236 2020/07/03 08:13:23 rillig Exp $");
 
 #include <sys/types.h>
 #include <sys/stat.h>
-#include <assert.h>
-#include <ctype.h>
 #include <errno.h>
 #include <stdarg.h>
 #include <stdio.h>
 
 #include "make.h"
-#include "hash.h"
 #include "dir.h"
 #include "job.h"
-#include "buf.h"
 #include "pathnames.h"
 
 #ifdef HAVE_STDINT_H
@@ -153,8 +149,7 @@ __RCSID("$NetBSD: parse.c,v 1.236 2020/07/03 08:13:23 rillig Exp $");
 #endif
 #endif
 
-////////////////////////////////////////////////////////////
-// types and constants
+/* types and constants */
 
 /*
  * Structure for a file being read ("included file")
@@ -231,8 +226,7 @@ typedef enum {
 #define RPAREN	')'
 
 
-////////////////////////////////////////////////////////////
-// result data
+/* result data */
 
 /*
  * The main target to create. This is the first target on the first
@@ -240,8 +234,7 @@ typedef enum {
  */
 static GNode *mainNode;
 
-////////////////////////////////////////////////////////////
-// eval state
+/* eval state */
 
 /* targets we're working on */
 static Lst targets;
@@ -265,8 +258,7 @@ static ParseSpecial specType;
  */
 static GNode	*predecessor;
 
-////////////////////////////////////////////////////////////
-// parser state
+/* parser state */
 
 /* true if currently in a dependency line or its commands */
 static Boolean inLine;
@@ -289,8 +281,7 @@ Lst parseIncPath;	/* dirs for "..." includes */
 Lst sysIncPath;		/* dirs for <...> includes */
 Lst defIncPath;		/* default for sysIncPath */
 
-////////////////////////////////////////////////////////////
-// parser tables
+/* parser tables */
 
 /*
  * The parseKeywords table is searched using binary search when deciding
@@ -350,8 +341,7 @@ static const struct {
 { ".WAIT",	  Wait, 	0 },
 };
 
-////////////////////////////////////////////////////////////
-// local functions
+/* local functions */
 
 static int ParseIsEscaped(const char *, const char *);
 static void ParseErrorInternal(const char *, size_t, int, const char *, ...)
@@ -379,8 +369,7 @@ static char *ParseReadLine(void);
 static void ParseFinishLine(void);
 static void ParseMark(GNode *);
 
-////////////////////////////////////////////////////////////
-// file loader
+/* file loader */
 
 struct loadedfile {
 	const char *path;		/* name, for error reports */
@@ -399,7 +388,7 @@ loadedfile_create(const char *path)
 	struct loadedfile *lf;
 
 	lf = bmake_malloc(sizeof(*lf));
-	lf->path = (path == NULL ? "(stdin)" : path);
+	lf->path = path == NULL ? "(stdin)" : path;
 	lf->buf = NULL;
 	lf->len = 0;
 	lf->maplen = 0;
@@ -442,17 +431,17 @@ loadedfile_nextbuf(void *x, size_t *len)
 /*
  * Try to get the size of a file.
  */
-static ReturnStatus
+static Boolean
 load_getsize(int fd, size_t *ret)
 {
 	struct stat st;
 
 	if (fstat(fd, &st) < 0) {
-		return FAILURE;
+		return FALSE;
 	}
 
 	if (!S_ISREG(st.st_mode)) {
-		return FAILURE;
+		return FALSE;
 	}
 
 	/*
@@ -465,11 +454,11 @@ load_getsize(int fd, size_t *ret)
 	 * While we're at it reject negative sizes too, just in case.
 	 */
 	if (st.st_size < 0 || st.st_size > 0x7fffffff) {
-		return FAILURE;
+		return FALSE;
 	}
 
 	*ret = (size_t) st.st_size;
-	return SUCCESS;
+	return TRUE;
 }
 
 /*
@@ -509,7 +498,7 @@ loadfile(const char *path, int fd)
 	}
 
 #ifdef HAVE_MMAP
-	if (load_getsize(fd, &lf->len) == SUCCESS) {
+	if (load_getsize(fd, &lf->len)) {
 		/* found a size, try mmap */
 #ifdef _SC_PAGESIZE
 		if (pagesize == 0)
@@ -600,8 +589,7 @@ done:
 	return lf;
 }
 
-////////////////////////////////////////////////////////////
-// old code
+/* old code */
 
 /*-
  *----------------------------------------------------------------------
@@ -714,8 +702,8 @@ ParseVErrorInternal(FILE *f, const char *cfname, size_t clineno, int type,
 					fname = cfname;
 			}
 			(void)fprintf(f, "%s/%s", dir, fname);
-			free(cp2);
-			free(cp);
+			bmake_free(cp2);
+			bmake_free(cp);
 		} else
 			(void)fprintf(f, "%s", cfname);
 
@@ -840,7 +828,7 @@ ParseMessage(char *line)
     while (isspace((unsigned char)*line))
 	line++;
 
-    line = Var_Subst(NULL, line, VAR_CMD, VARF_WANTRES);
+    line = Var_Subst(line, VAR_CMD, VARE_WANTRES);
     Parse_Error(mtype, "%s", line);
     free(line);
 
@@ -877,11 +865,11 @@ ParseLinkSrc(void *pgnp, void *cgnp)
     GNode          *pgn = (GNode *)pgnp;
     GNode          *cgn = (GNode *)cgnp;
 
-    if ((pgn->type & OP_DOUBLEDEP) && !Lst_IsEmpty (pgn->cohorts))
-	pgn = (GNode *)Lst_Datum(Lst_Last(pgn->cohorts));
-    (void)Lst_AtEnd(pgn->children, cgn);
+    if ((pgn->type & OP_DOUBLEDEP) && !Lst_IsEmpty(pgn->cohorts))
+	pgn = LstNode_Datum(Lst_Last(pgn->cohorts));
+    Lst_Append(pgn->children, cgn);
     if (specType == Not)
-	    (void)Lst_AtEnd(cgn->parents, pgn);
+	Lst_Append(cgn->parents, pgn);
     pgn->unmade += 1;
     if (DEBUG(PARSE)) {
 	fprintf(debug_file, "# %s: added child %s - %s\n", __func__,
@@ -929,7 +917,7 @@ ParseDoOp(void *gnp, void *opp)
 	return 1;
     }
 
-    if ((op == OP_DOUBLEDEP) && ((gn->type & OP_OPMASK) == OP_DOUBLEDEP)) {
+    if (op == OP_DOUBLEDEP && (gn->type & OP_OPMASK) == OP_DOUBLEDEP) {
 	/*
 	 * If the node was the object of a :: operator, we need to create a
 	 * new instance of it for the children and commands on this dependency
@@ -957,7 +945,7 @@ ParseDoOp(void *gnp, void *opp)
 	 * traversals will no longer see this node anyway. -mycroft)
 	 */
 	cohort->type = op | OP_INVISIBLE;
-	(void)Lst_AtEnd(gn->cohorts, cohort);
+	Lst_Append(gn->cohorts, cohort);
 	cohort->centurion = gn;
 	gn->unmade_cohorts += 1;
 	snprintf(cohort->cohort_num, sizeof cohort->cohort_num, "#%d",
@@ -1006,7 +994,8 @@ ParseDoSrc(int tOp, const char *src)
 	if (keywd != -1) {
 	    int op = parseKeywords[keywd].op;
 	    if (op != 0) {
-		Lst_ForEach(targets, ParseDoOp, &op);
+		if (targets != NULL)
+		    Lst_ForEach(targets, ParseDoOp, &op);
 		return;
 	    }
 	    if (parseKeywords[keywd].spec == Wait) {
@@ -1024,7 +1013,8 @@ ParseDoSrc(int tOp, const char *src)
 		if (doing_depend)
 		    ParseMark(gn);
 		gn->type = OP_WAIT | OP_PHONY | OP_DEPENDS | OP_NOTMAIN;
-		Lst_ForEach(targets, ParseLinkSrc, gn);
+		if (targets != NULL)
+		    Lst_ForEach(targets, ParseLinkSrc, gn);
 		return;
 	    }
 	}
@@ -1040,7 +1030,7 @@ ParseDoSrc(int tOp, const char *src)
 	 * invoked if the user didn't specify a target on the command
 	 * line. This is to allow #ifmake's to succeed, or something...
 	 */
-	(void)Lst_AtEnd(create, bmake_strdup(src));
+	Lst_Append(create, bmake_strdup(src));
 	/*
 	 * Add the name to the .TARGETS variable as well, so the user can
 	 * employ that, if desired.
@@ -1057,8 +1047,8 @@ ParseDoSrc(int tOp, const char *src)
 	if (doing_depend)
 	    ParseMark(gn);
 	if (predecessor != NULL) {
-	    (void)Lst_AtEnd(predecessor->order_succ, gn);
-	    (void)Lst_AtEnd(gn->order_pred, predecessor);
+	    Lst_Append(predecessor->order_succ, gn);
+	    Lst_Append(gn->order_pred, predecessor);
 	    if (DEBUG(PARSE)) {
 		fprintf(debug_file, "# %s: added Order dependency %s - %s\n",
 		    __func__, predecessor->name, gn->name);
@@ -1092,7 +1082,8 @@ ParseDoSrc(int tOp, const char *src)
 	if (tOp) {
 	    gn->type |= tOp;
 	} else {
-	    Lst_ForEach(targets, ParseLinkSrc, gn);
+	    if (targets != NULL)
+		Lst_ForEach(targets, ParseLinkSrc, gn);
 	}
 	break;
     }
@@ -1120,7 +1111,7 @@ static int
 ParseFindMain(void *gnp, void *dummy MAKE_ATTR_UNUSED)
 {
     GNode   	  *gn = (GNode *)gnp;
-    if ((gn->type & OP_NOTARGET) == 0) {
+    if (!(gn->type & OP_NOTARGET)) {
 	mainNode = gn;
 	Targ_SetMain(gn);
 	return 1;
@@ -1229,7 +1220,7 @@ ParseDoDependency(char *line)
     specType = Not;
     paths = NULL;
 
-    curTargs = Lst_Init(FALSE);
+    curTargs = Lst_Init();
 
     /*
      * First, grind through the targets.
@@ -1257,10 +1248,10 @@ ParseDoDependency(char *line)
 		int 	length;
 		void    *freeIt;
 
-		(void)Var_Parse(cp, VAR_CMD, VARF_UNDEFERR|VARF_WANTRES,
+		(void)Var_Parse(cp, VAR_CMD, VARE_UNDEFERR|VARE_WANTRES,
 				&length, &freeIt);
 		free(freeIt);
-		cp += length-1;
+		cp += length - 1;
 	    }
 	}
 
@@ -1275,11 +1266,11 @@ ParseDoDependency(char *line)
 	     * things like "archive(file1.o file2.o file3.o)" are permissible.
 	     * Arch_ParseArchive will set 'line' to be the first non-blank
 	     * after the archive-spec. It creates/finds nodes for the members
-	     * and places them on the given list, returning SUCCESS if all
-	     * went well and FAILURE if there was an error in the
+	     * and places them on the given list, returning TRUE if all
+	     * went well and FALSE if there was an error in the
 	     * specification. On error, line should remain untouched.
 	     */
-	    if (Arch_ParseArchive(&line, targets, VAR_CMD) != SUCCESS) {
+	    if (!Arch_ParseArchive(&line, targets, VAR_CMD)) {
 		Parse_Error(PARSE_FATAL,
 			     "Error in archive specification: \"%s\"", line);
 		goto out;
@@ -1304,9 +1295,18 @@ ParseDoDependency(char *line)
 		(strncmp(line, ">>>>>>", 6) == 0))
 		Parse_Error(PARSE_FATAL,
 		    "Makefile appears to contain unresolved cvs/rcs/??? merge conflicts");
-	    else
-		Parse_Error(PARSE_FATAL, lstart[0] == '.' ? "Unknown directive"
-				     : "Need an operator");
+	    else if (lstart[0] == '.') {
+		const char *dirstart = lstart + 1;
+		const char *dirend;
+		while (isspace((unsigned char)*dirstart))
+		    dirstart++;
+		dirend = dirstart;
+		while (isalnum((unsigned char)*dirend) || *dirend == '-')
+		    dirend++;
+		Parse_Error(PARSE_FATAL, "Unknown directive \"%.*s\"",
+			    (int)(dirend - dirstart), dirstart);
+	    } else
+		Parse_Error(PARSE_FATAL, "Need an operator");
 	    goto out;
 	}
 
@@ -1368,9 +1368,9 @@ ParseDoDependency(char *line)
 		switch (specType) {
 		case ExPath:
 		    if (paths == NULL) {
-			paths = Lst_Init(FALSE);
+			paths = Lst_Init();
 		    }
-		    (void)Lst_AtEnd(paths, dirSearchPath);
+		    Lst_Append(paths, dirSearchPath);
 		    break;
 		case Main:
 		    if (!Lst_IsEmpty(create)) {
@@ -1386,12 +1386,12 @@ ParseDoDependency(char *line)
 		    if (doing_depend)
 			ParseMark(gn);
 		    gn->type |= OP_NOTMAIN|OP_SPECIAL;
-		    (void)Lst_AtEnd(targets, gn);
+		    Lst_Append(targets, gn);
 		    break;
 		case Default:
 		    gn = Targ_NewGN(".DEFAULT");
 		    gn->type |= (OP_NOTMAIN|OP_TRANSFORM);
-		    (void)Lst_AtEnd(targets, gn);
+		    Lst_Append(targets, gn);
 		    DEFAULT = gn;
 		    break;
 		case DeleteOnError:
@@ -1426,9 +1426,9 @@ ParseDoDependency(char *line)
 		    goto out;
 		} else {
 		    if (paths == NULL) {
-			paths = Lst_Init(FALSE);
+			paths = Lst_Init();
 		    }
-		    (void)Lst_AtEnd(paths, path);
+		    Lst_Append(paths, path);
 		}
 	    }
 	}
@@ -1437,7 +1437,7 @@ ParseDoDependency(char *line)
 	 * Have word in line. Get or create its node and stick it at
 	 * the end of the targets list
 	 */
-	if ((specType == Not) && (*line != '\0')) {
+	if (specType == Not && *line != '\0') {
 	    if (Dir_HasWildcards(line)) {
 		/*
 		 * Targets are to be sought only in the current directory,
@@ -1445,7 +1445,7 @@ ParseDoDependency(char *line)
 		 * use Dir_Destroy in the destruction of the path as the
 		 * Dir module could have added a directory to the path...
 		 */
-		Lst	    emptyPath = Lst_Init(FALSE);
+		Lst	    emptyPath = Lst_Init();
 
 		Dir_Expand(line, emptyPath, curTargs);
 
@@ -1455,13 +1455,13 @@ ParseDoDependency(char *line)
 		 * No wildcards, but we want to avoid code duplication,
 		 * so create a list with the word on it.
 		 */
-		(void)Lst_AtEnd(curTargs, line);
+		Lst_Append(curTargs, line);
 	    }
 
 	    /* Apply the targets. */
 
 	    while(!Lst_IsEmpty(curTargs)) {
-		char	*targName = (char *)Lst_DeQueue(curTargs);
+		char *targName = Lst_Dequeue(curTargs);
 
 		if (!Suff_IsTransform (targName)) {
 		    gn = Targ_FindNode(targName, TARG_CREATE);
@@ -1471,7 +1471,7 @@ ParseDoDependency(char *line)
 		if (doing_depend)
 		    ParseMark(gn);
 
-		(void)Lst_AtEnd(targets, gn);
+		Lst_Append(targets, gn);
 	    }
 	} else if (specType == ExPath && *line != '.' && *line != '\0') {
 	    Parse_Error(PARSE_WARNING, "Extra target (%s) ignored", line);
@@ -1488,7 +1488,7 @@ ParseDoDependency(char *line)
 	    Boolean warning = FALSE;
 
 	    while (*cp && (ParseIsEscaped(lstart, cp) ||
-		((*cp != '!') && (*cp != ':')))) {
+		(*cp != '!' && *cp != ':'))) {
 		if (ParseIsEscaped(lstart, cp) ||
 		    (*cp != ' ' && *cp != '\t')) {
 		    warning = TRUE;
@@ -1505,15 +1505,15 @@ ParseDoDependency(char *line)
 	}
 	line = cp;
     } while (*line && (ParseIsEscaped(lstart, line) ||
-	((*line != '!') && (*line != ':'))));
+	(*line != '!' && *line != ':')));
 
     /*
      * Don't need the list of target names anymore...
      */
-    Lst_Destroy(curTargs, NULL);
+    Lst_Free(curTargs);
     curTargs = NULL;
 
-    if (!Lst_IsEmpty(targets)) {
+    if (targets != NULL && !Lst_IsEmpty(targets)) {
 	switch(specType) {
 	    default:
 		Parse_Error(PARSE_WARNING, "Special and mundane targets don't mix. Mundane ones ignored");
@@ -1563,7 +1563,8 @@ ParseDoDependency(char *line)
      * operator a target was defined with. It fails if the operator
      * used isn't consistent across all references.
      */
-    Lst_ForEach(targets, ParseDoOp, &op);
+    if (targets != NULL)
+	Lst_ForEach(targets, ParseDoOp, &op);
 
     /*
      * Onward to the sources.
@@ -1600,13 +1601,14 @@ ParseDoDependency(char *line)
 		beSilent = TRUE;
 		break;
 	    case ExPath:
-		Lst_ForEach(paths, ParseClearPath, NULL);
+		if (paths != NULL)
+		    Lst_ForEach(paths, ParseClearPath, NULL);
 		Dir_SetPATH();
 		break;
 #ifdef POSIX
-            case Posix:
-                Var_Set("%POSIX", "1003.2", VAR_GLOBAL);
-                break;
+	    case Posix:
+		Var_Set("%POSIX", "1003.2", VAR_GLOBAL);
+		break;
 #endif
 	    default:
 		break;
@@ -1620,22 +1622,22 @@ ParseDoDependency(char *line)
 	Main_ParseArgLine(line);
 	*line = '\0';
     } else if (specType == ExShell) {
-	if (Job_ParseShell(line) != SUCCESS) {
+	if (!Job_ParseShell(line)) {
 	    Parse_Error(PARSE_FATAL, "improper shell specification");
 	    goto out;
 	}
 	*line = '\0';
-    } else if ((specType == NotParallel) || (specType == SingleShell) ||
-	    (specType == DeleteOnError)) {
+    } else if (specType == NotParallel || specType == SingleShell ||
+	    specType == DeleteOnError) {
 	*line = '\0';
     }
 
     /*
      * NOW GO FOR THE SOURCES
      */
-    if ((specType == Suffixes) || (specType == ExPath) ||
-	(specType == Includes) || (specType == Libs) ||
-	(specType == Null) || (specType == ExObjdir))
+    if (specType == Suffixes || specType == ExPath ||
+	specType == Includes || specType == Libs ||
+	specType == Null || specType == ExObjdir)
     {
 	while (*line) {
 	    /*
@@ -1675,7 +1677,8 @@ ParseDoDependency(char *line)
 		    Suff_AddSuffix(line, &mainNode);
 		    break;
 		case ExPath:
-		    Lst_ForEach(paths, ParseAddDir, line);
+		    if (paths != NULL)
+			Lst_ForEach(paths, ParseAddDir, line);
 		    break;
 		case Includes:
 		    Suff_AddInclude(line);
@@ -1702,7 +1705,7 @@ ParseDoDependency(char *line)
 	    line = cp;
 	}
 	if (paths) {
-	    Lst_Destroy(paths, NULL);
+	    Lst_Free(paths);
 	    paths = NULL;
 	}
 	if (specType == ExPath)
@@ -1716,7 +1719,7 @@ ParseDoDependency(char *line)
 	     * and handle them accordingly.
 	     */
 	    for (; *cp && !isspace ((unsigned char)*cp); cp++) {
-		if ((*cp == LPAREN) && (cp > line) && (cp[-1] != '$')) {
+		if (*cp == LPAREN && cp > line && cp[-1] != '$') {
 		    /*
 		     * Only stop for a left parenthesis if it isn't at the
 		     * start of a word (that'll be for variable changes
@@ -1728,18 +1731,18 @@ ParseDoDependency(char *line)
 	    }
 
 	    if (*cp == LPAREN) {
-		sources = Lst_Init(FALSE);
-		if (Arch_ParseArchive(&line, sources, VAR_CMD) != SUCCESS) {
+		sources = Lst_Init();
+		if (!Arch_ParseArchive(&line, sources, VAR_CMD)) {
 		    Parse_Error(PARSE_FATAL,
 				 "Error in source archive spec \"%s\"", line);
 		    goto out;
 		}
 
-		while (!Lst_IsEmpty (sources)) {
-		    gn = (GNode *)Lst_DeQueue(sources);
+		while (!Lst_IsEmpty(sources)) {
+		    gn = Lst_Dequeue(sources);
 		    ParseDoSrc(tOp, gn->name);
 		}
-		Lst_Destroy(sources, NULL);
+		Lst_Free(sources);
 		cp = line;
 	    } else {
 		if (*cp) {
@@ -1756,7 +1759,7 @@ ParseDoDependency(char *line)
 	}
     }
 
-    if (mainNode == NULL) {
+    if (mainNode == NULL && targets != NULL) {
 	/*
 	 * If we have yet to decide on a main target to make, in the
 	 * absence of any user input, we want the first target on
@@ -1767,10 +1770,10 @@ ParseDoDependency(char *line)
     }
 
 out:
-    if (paths)
-	Lst_Destroy(paths, NULL);
-    if (curTargs)
-	    Lst_Destroy(curTargs, NULL);
+    if (paths != NULL)
+	Lst_Free(paths);
+    if (curTargs != NULL)
+	Lst_Free(curTargs);
 }
 
 /*-
@@ -1804,8 +1807,8 @@ Parse_IsVar(char *line)
     /*
      * Skip to variable name
      */
-    for (;(*line == ' ') || (*line == '\t'); line++)
-	continue;
+    while (*line == ' ' || *line == '\t')
+	line++;
 
     /* Scan for one of the assignment operators outside a variable expansion */
     while ((ch = *line++) != 0) {
@@ -1882,16 +1885,15 @@ Parse_DoVar(char *line, GNode *ctxt)
     /*
      * Skip to variable name
      */
-    while ((*line == ' ') || (*line == '\t')) {
+    while (*line == ' ' || *line == '\t')
 	line++;
-    }
 
     /*
      * Skip to operator character, nulling out whitespace as we go
      * XXX Rather than counting () and {} we should look for $ and
      * then expand the variable.
      */
-    for (depth = 0, cp = line + 1; depth > 0 || *cp != '='; cp++) {
+    for (depth = 0, cp = line; depth > 0 || *cp != '='; cp++) {
 	if (*cp == '(' || *cp == '{') {
 	    depth++;
 	    continue;
@@ -1953,8 +1955,17 @@ Parse_DoVar(char *line, GNode *ctxt)
 	    break;
     }
 
-    while (isspace ((unsigned char)*cp)) {
+    while (isspace((unsigned char)*cp))
 	cp++;
+
+    if (DEBUG(LINT)) {
+	if (type != VAR_SUBST && strchr(cp, '$') != NULL) {
+	    /* sanity check now */
+	    char *cp2;
+
+	    cp2 = Var_Subst(cp, ctxt, VARE_ASSIGN);
+	    free(cp2);
+	}
     }
 
     if (type == VAR_APPEND) {
@@ -1982,7 +1993,7 @@ Parse_DoVar(char *line, GNode *ctxt)
 	if (!Var_Exists(line, ctxt))
 	    Var_Set(line, "", ctxt);
 
-	cp = Var_Subst(NULL, cp, ctxt, VARF_WANTRES|VARF_ASSIGN);
+	cp = Var_Subst(cp, ctxt, VARE_WANTRES|VARE_ASSIGN);
 	oldVars = oldOldVars;
 	freeCp = TRUE;
 
@@ -1995,9 +2006,9 @@ Parse_DoVar(char *line, GNode *ctxt)
 	    /*
 	     * There's a dollar sign in the command, so perform variable
 	     * expansion on the whole thing. The resulting string will need
-	     * freeing when we're done, so set freeCmd to TRUE.
+	     * freeing when we're done.
 	     */
-	    cp = Var_Subst(NULL, cp, VAR_CMD, VARF_UNDEFERR|VARF_WANTRES);
+	    cp = Var_Subst(cp, VAR_CMD, VARE_UNDEFERR|VARE_WANTRES);
 	    freeCp = TRUE;
 	}
 
@@ -2026,7 +2037,7 @@ Parse_DoVar(char *line, GNode *ctxt)
     } else if (strcmp(line, MAKE_JOB_PREFIX) == 0) {
 	Job_SetPrefix();
     } else if (strcmp(line, MAKE_EXPORTED) == 0) {
-	Var_Export(cp, 0);
+	Var_Export(cp, FALSE);
     }
     if (freeCp)
 	free(cp);
@@ -2089,19 +2100,19 @@ ParseAddCmd(void *gnp, void *cmd)
     GNode *gn = (GNode *)gnp;
 
     /* Add to last (ie current) cohort for :: targets */
-    if ((gn->type & OP_DOUBLEDEP) && !Lst_IsEmpty (gn->cohorts))
-	gn = (GNode *)Lst_Datum(Lst_Last(gn->cohorts));
+    if ((gn->type & OP_DOUBLEDEP) && !Lst_IsEmpty(gn->cohorts))
+	gn = LstNode_Datum(Lst_Last(gn->cohorts));
 
     /* if target already supplied, ignore commands */
     if (!(gn->type & OP_HAS_COMMANDS)) {
-	(void)Lst_AtEnd(gn->commands, cmd);
+	Lst_Append(gn->commands, cmd);
 	if (ParseMaybeSubMake(cmd))
 	    gn->type |= OP_SUBMAKE;
 	ParseMark(gn);
     } else {
 #ifdef notyet
 	/* XXX: We cannot do this until we fix the tree */
-	(void)Lst_AtEnd(gn->commands, cmd);
+	Lst_Append(gn->commands, cmd);
 	Parse_Error(PARSE_WARNING,
 		     "overriding commands for target \"%s\"; "
 		     "previous commands defined at %s: %d ignored",
@@ -2225,7 +2236,7 @@ Parse_include_file(char *file, Boolean isSystem, Boolean depinc, int silent)
 		    break;
 		*prefEnd = '\0';
 	    }
-	    newName = str_concat(incdir, file + i, STR_ADDSLASH);
+	    newName = str_concat3(incdir, "/", file + i);
 	    fullname = Dir_FindFile(newName, parseIncPath);
 	    if (fullname == NULL)
 		fullname = Dir_FindFile(newName, dirSearchPath);
@@ -2235,7 +2246,7 @@ Parse_include_file(char *file, Boolean isSystem, Boolean depinc, int silent)
 
 	if (fullname == NULL) {
 	    /*
-    	     * Makefile wasn't found in same directory as included makefile.
+	     * Makefile wasn't found in same directory as included makefile.
 	     * Search for it first on the -I search path,
 	     * then on the .PATH search path, if not found in a -I directory.
 	     * If we have a suffix specific path we should use that.
@@ -2298,7 +2309,7 @@ ParseDoInclude(char *line)
 {
     char          endc;	    	/* the character which ends the file spec */
     char          *cp;		/* current position in file spec */
-    int		  silent = (*line != 'i') ? 1 : 0;
+    int		  silent = *line != 'i';
     char	  *file = &line[7 + silent];
 
     /* Skip to delimiter character so we know where to look */
@@ -2338,9 +2349,9 @@ ParseDoInclude(char *line)
      * Substitute for any variables in the file name before trying to
      * find the thing.
      */
-    file = Var_Subst(NULL, file, VAR_CMD, VARF_WANTRES);
+    file = Var_Subst(file, VAR_CMD, VARE_WANTRES);
 
-    Parse_include_file(file, endc == '>', (*line == 'd'), silent);
+    Parse_include_file(file, endc == '>', *line == 'd', silent);
     free(file);
 }
 
@@ -2363,20 +2374,20 @@ ParseDoInclude(char *line)
 static void
 ParseSetIncludedFile(void)
 {
-    char *pf, *fp = NULL;
-    char *pd, *dp = NULL;
+    const char *pf, *pd;
+    char *pf_freeIt, *pd_freeIt;
 
-    pf = Var_Value(".PARSEFILE", VAR_GLOBAL, &fp);
+    pf = Var_Value(".PARSEFILE", VAR_GLOBAL, &pf_freeIt);
     Var_Set(".INCLUDEDFROMFILE", pf, VAR_GLOBAL);
-    pd = Var_Value(".PARSEDIR", VAR_GLOBAL, &dp);
+    pd = Var_Value(".PARSEDIR", VAR_GLOBAL, &pd_freeIt);
     Var_Set(".INCLUDEDFROMDIR", pd, VAR_GLOBAL);
 
     if (DEBUG(PARSE))
 	fprintf(debug_file, "%s: ${.INCLUDEDFROMDIR} = `%s' "
 	    "${.INCLUDEDFROMFILE} = `%s'\n", __func__, pd, pf);
 
-    free(fp);
-    free(dp);
+    bmake_free(pf_freeIt);
+    bmake_free(pd_freeIt);
 }
 /*-
  *---------------------------------------------------------------------
@@ -2397,18 +2408,14 @@ ParseSetParseFile(const char *filename)
 {
     char *slash, *dirname;
     const char *pd, *pf;
-    int len;
 
     slash = strrchr(filename, '/');
     if (slash == NULL) {
 	Var_Set(".PARSEDIR", pd = curdir, VAR_GLOBAL);
 	Var_Set(".PARSEFILE", pf = filename, VAR_GLOBAL);
-	dirname= NULL;
+	dirname = NULL;
     } else {
-	len = slash - filename;
-	dirname = bmake_malloc(len + 1);
-	memcpy(dirname, filename, len);
-	dirname[len] = '\0';
+	dirname = bmake_strsedup(filename, slash);
 	Var_Set(".PARSEDIR", pd = dirname, VAR_GLOBAL);
 	Var_Set(".PARSEFILE", pf = slash + 1, VAR_GLOBAL);
     }
@@ -2427,14 +2434,12 @@ ParseSetParseFile(const char *filename)
 static void
 ParseTrackInput(const char *name)
 {
-    char *old;
-    char *ep;
     char *fp = NULL;
-    size_t name_len = strlen(name);
 
-    old = Var_Value(MAKE_MAKEFILES, VAR_GLOBAL, &fp);
+    const char *old = Var_Value(MAKE_MAKEFILES, VAR_GLOBAL, &fp);
     if (old) {
-	ep = old + strlen(old) - name_len;
+	size_t name_len = strlen(name);
+	const char *ep = old + strlen(old) - name_len;
 	/* does it contain name? */
 	for (; old != NULL; old = strchr(old, ' ')) {
 	    if (*old == ' ')
@@ -2448,9 +2453,7 @@ ParseTrackInput(const char *name)
     }
     Var_Append (MAKE_MAKEFILES, name, VAR_GLOBAL);
  cleanup:
-    if (fp) {
-	free(fp);
-    }
+    bmake_free(fp);
 }
 
 
@@ -2489,7 +2492,7 @@ Parse_SetInput(const char *name, int line, int fd,
 
     if (curFile != NULL)
 	/* Save exiting file info */
-	Lst_AtFront(includes, curFile);
+	Lst_Prepend(includes, curFile);
 
     /* Allocate and fill in new structure */
     curFile = bmake_malloc(sizeof *curFile);
@@ -2513,7 +2516,7 @@ Parse_SetInput(const char *name, int line, int fd,
     /* Get first block of input data */
     buf = curFile->nextbuf(curFile->nextbuf_arg, &len);
     if (buf == NULL) {
-        /* Was all a waste of time ... */
+	/* Was all a waste of time ... */
 	if (curFile->fname)
 	    free(curFile->fname);
 	free(curFile);
@@ -2546,13 +2549,13 @@ IsInclude(const char *line, Boolean sysv)
 	static const char inc[] = "include";
 	static const size_t inclen = sizeof(inc) - 1;
 
-	// 'd' is not valid for sysv
-	int o = strchr(&("ds-"[sysv]), *line) != NULL;
+	/* 'd' is not valid for sysv */
+	int o = strchr(sysv ? "s-" : "ds-", *line) != NULL;
 
 	if (strncmp(line + o, inc, inclen) != 0)
 		return FALSE;
 
-	// Space is not mandatory for BSD .include
+	/* Space is not mandatory for BSD .include */
 	return !sysv || isspace((unsigned char)line[inclen + o]);
 }
 
@@ -2614,7 +2617,7 @@ ParseTraditionalInclude(char *line)
 {
     char          *cp;		/* current position in file spec */
     int		   done = 0;
-    int		   silent = (line[0] != 'i') ? 1 : 0;
+    int		   silent = line[0] != 'i';
     char	  *file = &line[silent + 7];
     char	  *all_files;
 
@@ -2632,7 +2635,7 @@ ParseTraditionalInclude(char *line)
      * Substitute for any variables in the file name before trying to
      * find the thing.
      */
-    all_files = Var_Subst(NULL, file, VAR_CMD, VARF_WANTRES);
+    all_files = Var_Subst(file, VAR_CMD, VARE_WANTRES);
 
     if (*file == '\0') {
 	Parse_Error(PARSE_FATAL,
@@ -2701,7 +2704,7 @@ ParseGmakeExport(char *line)
     /*
      * Expand the value before putting it in the environment.
      */
-    value = Var_Subst(NULL, value, VAR_CMD, VARF_WANTRES);
+    value = Var_Subst(value, VAR_CMD, VARE_WANTRES);
     setenv(variable, value, 1);
     free(value);
 }
@@ -2755,9 +2758,8 @@ ParseEOF(void)
     free(curFile->P_str);
     free(curFile);
 
-    curFile = Lst_DeQueue(includes);
-
-    if (curFile == NULL) {
+    if (Lst_IsEmpty(includes)) {
+	curFile = NULL;
 	/* We've run out of input */
 	Var_Delete(".PARSEDIR", VAR_GLOBAL);
 	Var_Delete(".PARSEFILE", VAR_GLOBAL);
@@ -2766,6 +2768,7 @@ ParseEOF(void)
 	return DONE;
     }
 
+    curFile = Lst_Dequeue(includes);
     if (DEBUG(PARSE))
 	fprintf(debug_file, "ParseEOF: returning to file %s, line %d\n",
 	    curFile->fname, curFile->lineno);
@@ -3030,8 +3033,10 @@ static void
 ParseFinishLine(void)
 {
     if (inLine) {
-	Lst_ForEach(targets, Suff_EndTransform, NULL);
-	Lst_Destroy(targets, ParseHasCommands);
+        if (targets != NULL) {
+	    Lst_ForEach(targets, Suff_EndTransform, NULL);
+	    Lst_Destroy(targets, ParseHasCommands);
+	}
 	targets = NULL;
 	inLine = FALSE;
     }
@@ -3070,9 +3075,8 @@ Parse_File(const char *name, int fd)
     inLine = FALSE;
     fatals = 0;
 
-    if (name == NULL) {
-	    name = "(stdin)";
-    }
+    if (name == NULL)
+	name = "(stdin)";
 
     Parse_SetInput(name, 0, -1, loadedfile_nextbuf, lf);
     curFile->lf = lf;
@@ -3101,7 +3105,7 @@ Parse_File(const char *name, int fd)
 		    for (cp += 5; isspace((unsigned char) *cp); cp++)
 			continue;
 		    for (cp2 = cp; !isspace((unsigned char) *cp2) &&
-				   (*cp2 != '\0'); cp2++)
+				   *cp2 != '\0'; cp2++)
 			continue;
 		    *cp2 = '\0';
 		    Var_Delete(cp, VAR_GLOBAL);
@@ -3109,7 +3113,7 @@ Parse_File(const char *name, int fd)
 		} else if (strncmp(cp, "export", 6) == 0) {
 		    for (cp += 6; isspace((unsigned char) *cp); cp++)
 			continue;
-		    Var_Export(cp, 1);
+		    Var_Export(cp, TRUE);
 		    continue;
 		} else if (strncmp(cp, "unexport", 8) == 0) {
 		    Var_UnExport(cp);
@@ -3146,7 +3150,7 @@ Parse_File(const char *name, int fd)
 			cp = bmake_strdup(cp);
 			Lst_ForEach(targets, ParseAddCmd, cp);
 #ifdef CLEANUP
-			Lst_AtEnd(targCmds, cp);
+			Lst_Append(targCmds, cp);
 #endif
 		    }
 		}
@@ -3189,10 +3193,10 @@ Parse_File(const char *name, int fd)
 	     */
 	    cp = line;
 	    if (isspace((unsigned char) line[0])) {
-		while ((*cp != '\0') && isspace((unsigned char) *cp))
+		while (isspace((unsigned char) *cp))
 		    cp++;
 		while (*cp && (ParseIsEscaped(line, cp) ||
-			(*cp != ':') && (*cp != '!'))) {
+			*cp != ':' && *cp != '!')) {
 		    cp++;
 		}
 		if (*cp == '\0') {
@@ -3245,15 +3249,15 @@ Parse_File(const char *name, int fd)
 	     * variables expanded before being parsed. Tell the variable
 	     * module to complain if some variable is undefined...
 	     */
-	    line = Var_Subst(NULL, line, VAR_CMD, VARF_UNDEFERR|VARF_WANTRES);
+	    line = Var_Subst(line, VAR_CMD, VARE_UNDEFERR|VARE_WANTRES);
 
 	    /*
 	     * Need a non-circular list for the target nodes
 	     */
-	    if (targets)
-		Lst_Destroy(targets, NULL);
+	    if (targets != NULL)
+		Lst_Free(targets);
 
-	    targets = Lst_Init(FALSE);
+	    targets = Lst_Init();
 	    inLine = TRUE;
 
 	    ParseDoDependency(line);
@@ -3295,12 +3299,12 @@ void
 Parse_Init(void)
 {
     mainNode = NULL;
-    parseIncPath = Lst_Init(FALSE);
-    sysIncPath = Lst_Init(FALSE);
-    defIncPath = Lst_Init(FALSE);
-    includes = Lst_Init(FALSE);
+    parseIncPath = Lst_Init();
+    sysIncPath = Lst_Init();
+    defIncPath = Lst_Init();
+    includes = Lst_Init();
 #ifdef CLEANUP
-    targCmds = Lst_Init(FALSE);
+    targCmds = Lst_Init();
 #endif
 }
 
@@ -3308,13 +3312,13 @@ void
 Parse_End(void)
 {
 #ifdef CLEANUP
-    Lst_Destroy(targCmds, (FreeProc *)free);
+    Lst_Destroy(targCmds, free);
     if (targets)
-	Lst_Destroy(targets, NULL);
+	Lst_Free(targets);
     Lst_Destroy(defIncPath, Dir_Destroy);
     Lst_Destroy(sysIncPath, Dir_Destroy);
     Lst_Destroy(parseIncPath, Dir_Destroy);
-    Lst_Destroy(includes, NULL);	/* Should be empty now */
+    Lst_Free(includes);	/* Should be empty now */
 #endif
 }
 
@@ -3338,17 +3342,17 @@ Parse_MainName(void)
 {
     Lst           mainList;	/* result list */
 
-    mainList = Lst_Init(FALSE);
+    mainList = Lst_Init();
 
     if (mainNode == NULL) {
 	Punt("no target to make.");
-    	/*NOTREACHED*/
+	/*NOTREACHED*/
     } else if (mainNode->type & OP_DOUBLEDEP) {
-	(void)Lst_AtEnd(mainList, mainNode);
-	Lst_Concat(mainList, mainNode->cohorts, LST_CONCNEW);
+	Lst_Append(mainList, mainNode);
+	Lst_AppendAll(mainList, mainNode->cohorts);
     }
     else
-	(void)Lst_AtEnd(mainList, mainNode);
+	Lst_Append(mainList, mainNode);
     Var_Append(".TARGETS", mainNode->name, VAR_GLOBAL);
     return mainList;
 }
diff --git a/sprite.h b/sprite.h
deleted file mode 100644
index cdcffd99abed..000000000000
--- a/sprite.h
+++ /dev/null
@@ -1,116 +0,0 @@
-/*	$NetBSD: sprite.h,v 1.14 2017/05/31 22:02:06 maya Exp $	*/
-
-/*
- * Copyright (c) 1988, 1989, 1990, 1993
- *	The Regents of the University of California.  All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- *
- *	from: @(#)sprite.h	8.1 (Berkeley) 6/6/93
- */
-
-/*
- * Copyright (c) 1989 by Berkeley Softworks
- * All rights reserved.
- *
- * This code is derived from software contributed to Berkeley by
- * Adam de Boor.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. All advertising materials mentioning features or use of this software
- *    must display the following acknowledgement:
- *	This product includes software developed by the University of
- *	California, Berkeley and its contributors.
- * 4. Neither the name of the University nor the names of its contributors
- *    may be used to endorse or promote products derived from this software
- *    without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
- * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
- * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
- * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- * SUCH DAMAGE.
- *
- *	from: @(#)sprite.h	8.1 (Berkeley) 6/6/93
- */
-
-/*
- * sprite.h --
- *
- * Common constants and type declarations for Sprite.
- */
-
-#ifndef MAKE_SPRITE_H
-#define MAKE_SPRITE_H
-
-
-/*
- * A boolean type is defined as an integer, not an enum. This allows a
- * boolean argument to be an expression that isn't strictly 0 or 1 valued.
- */
-
-typedef int Boolean;
-#ifndef TRUE
-#define TRUE	1
-#endif /* TRUE */
-#ifndef FALSE
-#define FALSE	0
-#endif /* FALSE */
-
-/*
- * Functions that must return a status can return a ReturnStatus to
- * indicate success or type of failure.
- */
-
-typedef int  ReturnStatus;
-
-/*
- * The following statuses overlap with the first 2 generic statuses
- * defined in status.h:
- *
- * SUCCESS			There was no error.
- * FAILURE			There was a general error.
- */
-
-#define	SUCCESS			0x00000000
-#define	FAILURE			0x00000001
-
-#endif /* MAKE_SPRITE_H */
diff --git a/str.c b/str.c
index c3138b67b95b..59fdccb28867 100644
--- a/str.c
+++ b/str.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: str.c,v 1.51 2020/07/03 07:40:13 rillig Exp $	*/
+/*	$NetBSD: str.c,v 1.64 2020/08/30 19:56:02 rillig Exp $	*/
 
 /*-
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -69,85 +69,85 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: str.c,v 1.51 2020/07/03 07:40:13 rillig Exp $";
+static char rcsid[] = "$NetBSD: str.c,v 1.64 2020/08/30 19:56:02 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char     sccsid[] = "@(#)str.c	5.8 (Berkeley) 6/1/90";
 #else
-__RCSID("$NetBSD: str.c,v 1.51 2020/07/03 07:40:13 rillig Exp $");
+__RCSID("$NetBSD: str.c,v 1.64 2020/08/30 19:56:02 rillig Exp $");
 #endif
 #endif				/* not lint */
 #endif
 
 #include "make.h"
 
-/*-
- * str_concat --
- *	concatenate the two strings, inserting a space or slash between them,
- *	freeing them if requested.
- *
- * returns --
- *	the resulting string in allocated space.
- */
+/* Return the concatenation of s1 and s2, freshly allocated. */
 char *
-str_concat(const char *s1, const char *s2, int flags)
+str_concat2(const char *s1, const char *s2)
 {
-	int len1, len2;
-	char *result;
-
-	/* get the length of both strings */
-	len1 = strlen(s1);
-	len2 = strlen(s2);
-
-	/* allocate length plus separator plus EOS */
-	result = bmake_malloc((unsigned int)(len1 + len2 + 2));
-
-	/* copy first string into place */
+	size_t len1 = strlen(s1);
+	size_t len2 = strlen(s2);
+	char *result = bmake_malloc(len1 + len2 + 1);
 	memcpy(result, s1, len1);
-
-	/* add separator character */
-	if (flags & STR_ADDSPACE) {
-		result[len1] = ' ';
-		++len1;
-	} else if (flags & STR_ADDSLASH) {
-		result[len1] = '/';
-		++len1;
-	}
-
-	/* copy second string plus EOS into place */
 	memcpy(result + len1, s2, len2 + 1);
-
 	return result;
 }
 
-/*-
- * brk_string --
- *	Fracture a string into an array of words (as delineated by tabs or
- *	spaces) taking quotation marks into account.  Leading tabs/spaces
- *	are ignored.
- *
- * If expand is TRUE, quotes are removed and escape sequences
- *  such as \r, \t, etc... are expanded.
- *
- * returns --
- *	Pointer to the array of pointers to the words.
- *      Memory containing the actual words in *store_words_buf.
- *		Both of these must be free'd by the caller.
- *      Number of words in *store_words_len.
- */
-char **
-brk_string(const char *str, int *store_words_len, Boolean expand,
-	char **store_words_buf)
+/* Return the concatenation of s1, s2 and s3, freshly allocated. */
+char *
+str_concat3(const char *s1, const char *s2, const char *s3)
+{
+	size_t len1 = strlen(s1);
+	size_t len2 = strlen(s2);
+	size_t len3 = strlen(s3);
+	char *result = bmake_malloc(len1 + len2 + len3 + 1);
+	memcpy(result, s1, len1);
+	memcpy(result + len1, s2, len2);
+	memcpy(result + len1 + len2, s3, len3 + 1);
+	return result;
+}
+
+/* Return the concatenation of s1, s2, s3 and s4, freshly allocated. */
+char *
+str_concat4(const char *s1, const char *s2, const char *s3, const char *s4)
+{
+	size_t len1 = strlen(s1);
+	size_t len2 = strlen(s2);
+	size_t len3 = strlen(s3);
+	size_t len4 = strlen(s4);
+	char *result = bmake_malloc(len1 + len2 + len3 + len4 + 1);
+	memcpy(result, s1, len1);
+	memcpy(result + len1, s2, len2);
+	memcpy(result + len1 + len2, s3, len3);
+	memcpy(result + len1 + len2 + len3, s4, len4 + 1);
+	return result;
+}
+
+/* Fracture a string into an array of words (as delineated by tabs or spaces)
+ * taking quotation marks into account.  Leading tabs/spaces are ignored.
+ *
+ * If expand is TRUE, quotes are removed and escape sequences such as \r, \t,
+ * etc... are expanded. In this case, the return value is NULL on parse
+ * errors.
+ *
+ * Returns the fractured words, which must be freed later using Words_Free.
+ * If expand was TRUE and there was a parse error, words is NULL, and in that
+ * case, nothing needs to be freed.
+ */
+Words
+Str_Words(const char *str, Boolean expand)
 {
-	char inquote;
-	const char *str_p;
 	size_t str_len;
-    	char **words;
-	int words_len;
-	int words_cap = 50;
-	char *words_buf, *word_start, *word_end;
+	char *words_buf;
+	size_t words_cap;
+	char **words;
+	size_t words_len;
+	char inquote;
+	char *word_start;
+	char *word_end;
+	const char *str_p;
 
 	/* skip leading space chars. */
 	for (; *str == ' ' || *str == '\t'; ++str)
@@ -166,10 +166,11 @@ brk_string(const char *str, int *store_words_len, Boolean expand,
 	 */
 	words_len = 0;
 	inquote = '\0';
-	word_start = word_end = words_buf;
+	word_start = words_buf;
+	word_end = words_buf;
 	for (str_p = str;; ++str_p) {
 		char ch = *str_p;
-		switch(ch) {
+		switch (ch) {
 		case '"':
 		case '\'':
 			if (inquote) {
@@ -177,9 +178,8 @@ brk_string(const char *str, int *store_words_len, Boolean expand,
 					inquote = '\0';
 				else
 					break;
-			}
-			else {
-				inquote = (char) ch;
+			} else {
+				inquote = (char)ch;
 				/* Don't miss "" or '' */
 				if (word_start == NULL && str_p[1] == inquote) {
 					if (!expand) {
@@ -212,13 +212,14 @@ brk_string(const char *str, int *store_words_len, Boolean expand,
 			 * space and save off a pointer.
 			 */
 			if (word_start == NULL)
-			    goto done;
+				goto done;
 
 			*word_end++ = '\0';
 			if (words_len == words_cap) {
+				size_t new_size;
 				words_cap *= 2;		/* ramp up fast */
-				words = (char **)bmake_realloc(words,
-				    (words_cap + 1) * sizeof(char *));
+				new_size = (words_cap + 1) * sizeof(char *);
+				words = bmake_realloc(words, new_size);
 			}
 			words[words_len++] = word_start;
 			word_start = NULL;
@@ -226,8 +227,7 @@ brk_string(const char *str, int *store_words_len, Boolean expand,
 				if (expand && inquote) {
 					free(words);
 					free(words_buf);
-					*store_words_buf = NULL;
-					return NULL;
+					return (Words){ NULL, 0, NULL };
 				}
 				goto done;
 			}
@@ -273,10 +273,9 @@ brk_string(const char *str, int *store_words_len, Boolean expand,
 			word_start = word_end;
 		*word_end++ = ch;
 	}
-done:	words[words_len] = NULL;
-	*store_words_len = words_len;
-	*store_words_buf = words_buf;
-	return words;
+done:
+	words[words_len] = NULL;
+	return (Words){ words, words_len, words_buf };
 }
 
 /*
@@ -375,7 +374,7 @@ Str_Match(const char *str, const char *pat)
 		 */
 		if (*pat == '[') {
 			Boolean neg = pat[1] == '^';
-			pat += 1 + neg;
+			pat += neg ? 2 : 1;
 
 			for (;;) {
 				if (*pat == ']' || *pat == 0) {
@@ -423,107 +422,3 @@ Str_Match(const char *str, const char *pat)
 		str++;
 	}
 }
-
-/*-
- *-----------------------------------------------------------------------
- * Str_SYSVMatch --
- *	Check word against pattern for a match (% is wild),
- *
- * Input:
- *	word		Word to examine
- *	pattern		Pattern to examine against
- *	len		Number of characters to substitute
- *
- * Results:
- *	Returns the beginning position of a match or null. The number
- *	of characters matched is returned in len.
- *
- * Side Effects:
- *	None
- *
- *-----------------------------------------------------------------------
- */
-char *
-Str_SYSVMatch(const char *word, const char *pattern, size_t *len,
-    Boolean *hasPercent)
-{
-    const char *p = pattern;
-    const char *w = word;
-    const char *m;
-
-    *hasPercent = FALSE;
-    if (*p == '\0') {
-	/* Null pattern is the whole string */
-	*len = strlen(w);
-	return UNCONST(w);
-    }
-
-    if ((m = strchr(p, '%')) != NULL) {
-	*hasPercent = TRUE;
-	if (*w == '\0') {
-		/* empty word does not match pattern */
-		return NULL;
-	}
-	/* check that the prefix matches */
-	for (; p != m && *w && *w == *p; w++, p++)
-	     continue;
-
-	if (p != m)
-	    return NULL;	/* No match */
-
-	if (*++p == '\0') {
-	    /* No more pattern, return the rest of the string */
-	    *len = strlen(w);
-	    return UNCONST(w);
-	}
-    }
-
-    m = w;
-
-    /* Find a matching tail */
-    do
-	if (strcmp(p, w) == 0) {
-	    *len = w - m;
-	    return UNCONST(m);
-	}
-    while (*w++ != '\0');
-
-    return NULL;
-}
-
-
-/*-
- *-----------------------------------------------------------------------
- * Str_SYSVSubst --
- *	Substitute '%' on the pattern with len characters from src.
- *	If the pattern does not contain a '%' prepend len characters
- *	from src.
- *
- * Results:
- *	None
- *
- * Side Effects:
- *	Places result on buf
- *
- *-----------------------------------------------------------------------
- */
-void
-Str_SYSVSubst(Buffer *buf, char *pat, char *src, size_t len,
-    Boolean lhsHasPercent)
-{
-    char *m;
-
-    if ((m = strchr(pat, '%')) != NULL && lhsHasPercent) {
-	/* Copy the prefix */
-	Buf_AddBytes(buf, m - pat, pat);
-	/* skip the % */
-	pat = m + 1;
-    }
-    if (m != NULL || !lhsHasPercent) {
-	/* Copy the pattern */
-	Buf_AddBytes(buf, len, src);
-    }
-
-    /* append the rest */
-    Buf_AddBytes(buf, strlen(pat), pat);
-}
diff --git a/strlist.c b/strlist.c
index 3fb2f7dbb6c8..905e78fd8caa 100644
--- a/strlist.c
+++ b/strlist.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: strlist.c,v 1.4 2009/01/24 11:59:39 dsl Exp $	*/
+/*	$NetBSD: strlist.c,v 1.6 2020/08/25 17:37:09 rillig Exp $	*/
 
 /*-
  * Copyright (c) 2008 - 2009 The NetBSD Foundation, Inc.
@@ -33,18 +33,16 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: strlist.c,v 1.4 2009/01/24 11:59:39 dsl Exp $";
+static char rcsid[] = "$NetBSD: strlist.c,v 1.6 2020/08/25 17:37:09 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
-__RCSID("$NetBSD: strlist.c,v 1.4 2009/01/24 11:59:39 dsl Exp $");
+__RCSID("$NetBSD: strlist.c,v 1.6 2020/08/25 17:37:09 rillig Exp $");
 #endif /* not lint */
 #endif
 
-#include <stddef.h>
-#include <stdlib.h>
+#include "make.h"
 #include "strlist.h"
-#include "make_malloc.h"
 
 void
 strlist_init(strlist_t *sl)
@@ -76,18 +74,18 @@ strlist_add_str(strlist_t *sl, char *str, unsigned int info)
 	strlist_item_t *items;
 
 	if (str == NULL)
-	    return;
+		return;
 
 	n = sl->sl_num + 1;
 	sl->sl_num = n;
 	items = sl->sl_items;
 	if (n >= sl->sl_max) {
-	    items = bmake_realloc(items, (n + 7) * sizeof *sl->sl_items);
-	    sl->sl_items = items;
-	    sl->sl_max = n + 6;
+		items = bmake_realloc(items, (n + 7) * sizeof *sl->sl_items);
+		sl->sl_items = items;
+		sl->sl_max = n + 6;
 	}
 	items += n - 1;
 	items->si_str = str;
 	items->si_info = info;
-	items[1].si_str = NULL;         /* STRLIST_FOREACH() terminator */
+	items[1].si_str = NULL;	/* STRLIST_FOREACH() terminator */
 }
diff --git a/strlist.h b/strlist.h
index 2fc049e8643a..551317c090f3 100644
--- a/strlist.h
+++ b/strlist.h
@@ -1,4 +1,4 @@
-/*	$NetBSD: strlist.h,v 1.3 2009/01/16 21:15:34 dsl Exp $	*/
+/*	$NetBSD: strlist.h,v 1.4 2020/08/13 03:54:57 rillig Exp $	*/
 
 /*-
  * Copyright (c) 2008 - 2009 The NetBSD Foundation, Inc.
@@ -32,8 +32,8 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
-#ifndef _STRLIST_H
-#define _STRLIST_H
+#ifndef MAKE_STRLIST_H
+#define MAKE_STRLIST_H
 
 typedef struct {
     char          *si_str;
@@ -59,4 +59,4 @@ void strlist_add_str(strlist_t *, char *, unsigned int);
     if ((sl)->sl_items != NULL) \
 	for (index = 0; (v = strlist_str(sl, index)) != NULL; index++)
 
-#endif /* _STRLIST_H */
+#endif /* MAKE_STRLIST_H */
diff --git a/suff.c b/suff.c
index 5a40b73d788d..bcab3027e6dd 100644
--- a/suff.c
+++ b/suff.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: suff.c,v 1.88 2020/07/03 08:02:55 rillig Exp $	*/
+/*	$NetBSD: suff.c,v 1.142 2020/08/31 16:44:25 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -69,14 +69,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: suff.c,v 1.88 2020/07/03 08:02:55 rillig Exp $";
+static char rcsid[] = "$NetBSD: suff.c,v 1.142 2020/08/31 16:44:25 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)suff.c	8.4 (Berkeley) 3/21/94";
 #else
-__RCSID("$NetBSD: suff.c,v 1.88 2020/07/03 08:02:55 rillig Exp $");
+__RCSID("$NetBSD: suff.c,v 1.142 2020/08/31 16:44:25 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -135,12 +135,21 @@ __RCSID("$NetBSD: suff.c,v 1.88 2020/07/03 08:02:55 rillig Exp $");
  *				order to find the node.
  */
 
-#include    	  <assert.h>
-#include    	  <stdio.h>
 #include	  "make.h"
-#include	  "hash.h"
 #include	  "dir.h"
 
+#define SUFF_DEBUG0(fmt) \
+    if (!DEBUG(SUFF)) (void) 0; else fprintf(debug_file, fmt)
+
+#define SUFF_DEBUG1(fmt, arg1) \
+    if (!DEBUG(SUFF)) (void) 0; else fprintf(debug_file, fmt, arg1)
+
+#define SUFF_DEBUG2(fmt, arg1, arg2) \
+    if (!DEBUG(SUFF)) (void) 0; else fprintf(debug_file, fmt, arg1, arg2)
+
+#define SUFF_DEBUG3(fmt, arg1, arg2, arg3) \
+    if (!DEBUG(SUFF)) (void) 0; else fprintf(debug_file, fmt, arg1, arg2, arg3)
+
 static Lst       sufflist;	/* Lst of suffixes */
 #ifdef CLEANUP
 static Lst	 suffClean;	/* Lst of suffixes to be cleaned */
@@ -150,16 +159,23 @@ static Lst       transforms;	/* Lst of transformation rules */
 
 static int        sNum = 0;	/* Counter for assigning suffix numbers */
 
+typedef enum {
+    SUFF_INCLUDE	= 0x01,	/* One which is #include'd */
+    SUFF_LIBRARY	= 0x02,	/* One which contains a library */
+    SUFF_NULL		= 0x04	/* The empty suffix */
+    /* XXX: Why is SUFF_NULL needed? Wouldn't nameLen == 0 mean the same? */
+} SuffFlags;
+
+ENUM_FLAGS_RTTI_3(SuffFlags,
+		  SUFF_INCLUDE, SUFF_LIBRARY, SUFF_NULL);
+
 /*
  * Structure describing an individual suffix.
  */
-typedef struct _Suff {
-    char         *name;	    	/* The suffix itself */
-    int		 nameLen;	/* Length of the suffix */
-    short	 flags;      	/* Type of suffix */
-#define SUFF_INCLUDE	  0x01	    /* One which is #include'd */
-#define SUFF_LIBRARY	  0x02	    /* One which contains a library */
-#define SUFF_NULL 	  0x04	    /* The empty suffix */
+typedef struct Suff {
+    char         *name;	    	/* The suffix itself, such as ".c" */
+    int		 nameLen;	/* Length of the name, to avoid strlen calls */
+    SuffFlags	 flags;      	/* Type of suffix */
     Lst    	 searchPath;	/* The path along which files of this suffix
 				 * may be found */
     int          sNum;	      	/* The suffix number */
@@ -169,14 +185,6 @@ typedef struct _Suff {
     Lst		 ref;		/* List of lists this suffix is referenced */
 } Suff;
 
-/*
- * for SuffSuffIsSuffix
- */
-typedef struct {
-    char	*ename;		/* The end of the name */
-    int		 len;		/* Length of the name */
-} SuffixCmpData;
-
 /*
  * Structure used in the search for implied sources.
  */
@@ -213,12 +221,6 @@ static Suff 	    *emptySuff;	/* The empty suffix required for POSIX
 				 * single-suffix transformation rules */
 
 
-static const char *SuffStrIsPrefix(const char *, const char *);
-static char *SuffSuffIsSuffix(const Suff *, const SuffixCmpData *);
-static int SuffSuffIsSuffixP(const void *, const void *);
-static int SuffSuffHasNameP(const void *, const void *);
-static int SuffSuffIsPrefix(const void *, const void *);
-static int SuffGNHasNameP(const void *, const void *);
 static void SuffUnRef(void *, void *);
 static void SuffFree(void *);
 static void SuffInsert(Lst, Suff *);
@@ -227,10 +229,7 @@ static Boolean SuffParseTransform(char *, Suff **, Suff **);
 static int SuffRebuildGraph(void *, void *);
 static int SuffScanTargets(void *, void *);
 static int SuffAddSrc(void *, void *);
-static int SuffRemoveSrc(Lst);
 static void SuffAddLevel(Lst, Src *);
-static Src *SuffFindThem(Lst, Lst);
-static Src *SuffFindCmds(Src *, Lst);
 static void SuffExpandChildren(LstNode, GNode *);
 static void SuffExpandWildcards(LstNode, GNode *);
 static Boolean SuffApplyTransform(GNode *, GNode *, Suff *, Suff *);
@@ -269,35 +268,33 @@ SuffStrIsPrefix(const char *pref, const char *str)
     return *pref ? NULL : str;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffSuffIsSuffix  --
- *	See if suff is a suffix of str. sd->ename should point to THE END
- *	of the string to check. (THE END == the null byte)
+typedef struct {
+    char	*ename;		/* The end of the name */
+    int		 len;		/* Length of the name */
+} SuffSuffGetSuffixArgs;
+
+/* See if suff is a suffix of str. str->ename should point to THE END
+ * of the string to check. (THE END == the null byte)
  *
  * Input:
  *	s		possible suffix
- *	sd		string to examine
+ *	str		string to examine
  *
  * Results:
  *	NULL if it ain't, pointer to character in str before suffix if
  *	it is.
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
  */
 static char *
-SuffSuffIsSuffix(const Suff *s, const SuffixCmpData *sd)
+SuffSuffGetSuffix(const Suff *s, const SuffSuffGetSuffixArgs *str)
 {
     char  *p1;	    	/* Pointer into suffix name */
     char  *p2;	    	/* Pointer into string being examined */
 
-    if (sd->len < s->nameLen)
+    if (str->len < s->nameLen)
 	return NULL;		/* this string is shorter than the suffix */
 
     p1 = s->name + s->nameLen;
-    p2 = sd->ename;
+    p2 = str->ename;
 
     while (p1 >= s->name && *p1 == *p2) {
 	p1--;
@@ -307,126 +304,55 @@ SuffSuffIsSuffix(const Suff *s, const SuffixCmpData *sd)
     return p1 == s->name - 1 ? p2 : NULL;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffSuffIsSuffixP --
- *	Predicate form of SuffSuffIsSuffix. Passed as the callback function
- *	to Lst_Find.
- *
- * Results:
- *	0 if the suffix is the one desired, non-zero if not.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-static int
-SuffSuffIsSuffixP(const void *s, const void *sd)
+/* Predicate form of SuffSuffGetSuffix, for Lst_Find. */
+static Boolean
+SuffSuffIsSuffix(const void *s, const void *sd)
 {
-    return !SuffSuffIsSuffix(s, sd);
+    return SuffSuffGetSuffix(s, sd) != NULL;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffSuffHasNameP --
- *	Callback procedure for finding a suffix based on its name. Used by
- *	Suff_GetPath.
- *
- * Input:
- *	s		Suffix to check
- *	sd		Desired name
- *
- * Results:
- *	0 if the suffix is of the given name. non-zero otherwise.
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
- */
-static int
-SuffSuffHasNameP(const void *s, const void *sname)
+/* See if the suffix has the desired name. */
+static Boolean
+SuffSuffHasName(const void *s, const void *desiredName)
 {
-    return strcmp(sname, ((const Suff *)s)->name);
+    return strcmp(((const Suff *)s)->name, desiredName) == 0;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffSuffIsPrefix  --
- *	See if the suffix described by s is a prefix of the string. Care
- *	must be taken when using this to search for transformations and
- *	what-not, since there could well be two suffixes, one of which
- *	is a prefix of the other...
- *
- * Input:
- *	s		suffix to compare
- *	str		string to examine
- *
- * Results:
- *	0 if s is a prefix of str. non-zero otherwise
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
- */
-static int
+/* See if the suffix name is a prefix of the string. Care must be taken when
+ * using this to search for transformations and what-not, since there could
+ * well be two suffixes, one of which is a prefix of the other... */
+static Boolean
 SuffSuffIsPrefix(const void *s, const void *str)
 {
-    return SuffStrIsPrefix(((const Suff *)s)->name, str) == NULL;
+    return SuffStrIsPrefix(((const Suff *)s)->name, str) != NULL;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffGNHasNameP  --
- *	See if the graph node has the desired name
- *
- * Input:
- *	gn		current node we're looking at
- *	name		name we're looking for
- *
- * Results:
- *	0 if it does. non-zero if it doesn't
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
- */
-static int
-SuffGNHasNameP(const void *gn, const void *name)
+/* See if the graph node has the desired name. */
+static Boolean
+SuffGNHasName(const void *gn, const void *desiredName)
 {
-    return strcmp(name, ((const GNode *)gn)->name);
+    return strcmp(((const GNode *)gn)->name, desiredName) == 0;
 }
 
- 	    /*********** Maintenance Functions ************/
+	    /*********** Maintenance Functions ************/
 
 static void
 SuffUnRef(void *lp, void *sp)
 {
     Lst l = (Lst) lp;
 
-    LstNode ln = Lst_Member(l, sp);
+    LstNode ln = Lst_FindDatum(l, sp);
     if (ln != NULL) {
 	Lst_Remove(l, ln);
 	((Suff *)sp)->refCount--;
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffFree  --
- *	Free up all memory associated with the given suffix structure.
- *
- * Results:
- *	none
- *
- * Side Effects:
- *	the suffix entry is detroyed
- *-----------------------------------------------------------------------
- */
+/* Free up all memory associated with the given suffix structure. */
 static void
 SuffFree(void *sp)
 {
-    Suff           *s = (Suff *)sp;
+    Suff *s = (Suff *)sp;
 
     if (s == suffNull)
 	suffNull = NULL;
@@ -441,28 +367,16 @@ SuffFree(void *sp)
 	    s->refCount);
 #endif
 
-    Lst_Destroy(s->ref, NULL);
-    Lst_Destroy(s->children, NULL);
-    Lst_Destroy(s->parents, NULL);
+    Lst_Free(s->ref);
+    Lst_Free(s->children);
+    Lst_Free(s->parents);
     Lst_Destroy(s->searchPath, Dir_Destroy);
 
     free(s->name);
     free(s);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffRemove  --
- *	Remove the suffix into the list
- *
- * Results:
- *	None
- *
- * Side Effects:
- *	The reference count for the suffix is decremented and the
- *	suffix is possibly freed
- *-----------------------------------------------------------------------
- */
+/* Remove the suffix from the list, and free if it is otherwise unused. */
 static void
 SuffRemove(Lst l, Suff *s)
 {
@@ -472,23 +386,12 @@ SuffRemove(Lst l, Suff *s)
 	SuffFree(s);
     }
 }
-
-/*-
- *-----------------------------------------------------------------------
- * SuffInsert  --
- *	Insert the suffix into the list keeping the list ordered by suffix
- *	numbers.
+
+/* Insert the suffix into the list keeping the list ordered by suffix numbers.
  *
  * Input:
  *	l		the list where in s should be inserted
  *	s		the suffix to insert
- *
- * Results:
- *	None
- *
- * Side Effects:
- *	The reference count of the suffix is incremented
- *-----------------------------------------------------------------------
  */
 static void
 SuffInsert(Lst l, Suff *s)
@@ -496,100 +399,83 @@ SuffInsert(Lst l, Suff *s)
     LstNode 	  ln;		/* current element in l we're examining */
     Suff          *s2 = NULL;	/* the suffix descriptor in this element */
 
-    if (Lst_Open(l) == FAILURE) {
-	return;
-    }
+    Lst_Open(l);
     while ((ln = Lst_Next(l)) != NULL) {
-	s2 = (Suff *)Lst_Datum(ln);
+	s2 = LstNode_Datum(ln);
 	if (s2->sNum >= s->sNum) {
 	    break;
 	}
     }
-
     Lst_Close(l);
-    if (DEBUG(SUFF)) {
-	fprintf(debug_file, "inserting %s(%d)...", s->name, s->sNum);
-    }
+
+    SUFF_DEBUG2("inserting %s(%d)...", s->name, s->sNum);
+
     if (ln == NULL) {
-	if (DEBUG(SUFF)) {
-	    fprintf(debug_file, "at end of list\n");
-	}
-	(void)Lst_AtEnd(l, s);
+	SUFF_DEBUG0("at end of list\n");
+	Lst_Append(l, s);
 	s->refCount++;
-	(void)Lst_AtEnd(s->ref, l);
+	Lst_Append(s->ref, l);
     } else if (s2->sNum != s->sNum) {
-	if (DEBUG(SUFF)) {
-	    fprintf(debug_file, "before %s(%d)\n", s2->name, s2->sNum);
-	}
-	(void)Lst_InsertBefore(l, ln, s);
+	SUFF_DEBUG2("before %s(%d)\n", s2->name, s2->sNum);
+	Lst_InsertBefore(l, ln, s);
 	s->refCount++;
-	(void)Lst_AtEnd(s->ref, l);
-    } else if (DEBUG(SUFF)) {
-	fprintf(debug_file, "already there\n");
+	Lst_Append(s->ref, l);
+    } else {
+	SUFF_DEBUG0("already there\n");
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_ClearSuffixes --
- *	This is gross. Nuke the list of suffixes but keep all transformation
- *	rules around. The transformation graph is destroyed in this process,
- *	but we leave the list of rules so when a new graph is formed the rules
- *	will remain.
- *	This function is called from the parse module when a
- *	.SUFFIXES:\n line is encountered.
- *
- * Results:
- *	none
- *
- * Side Effects:
- *	the sufflist and its graph nodes are destroyed
- *-----------------------------------------------------------------------
- */
+static Suff *
+SuffNew(const char *name)
+{
+    Suff *s = bmake_malloc(sizeof(Suff));
+
+    s->name =   	bmake_strdup(name);
+    s->nameLen = 	strlen(s->name);
+    s->searchPath = Lst_Init();
+    s->children = 	Lst_Init();
+    s->parents = 	Lst_Init();
+    s->ref = 	Lst_Init();
+    s->sNum =   	sNum++;
+    s->flags =  	0;
+    s->refCount =	1;
+
+    return s;
+}
+
+/* This is gross. Nuke the list of suffixes but keep all transformation
+ * rules around. The transformation graph is destroyed in this process, but
+ * we leave the list of rules so when a new graph is formed the rules will
+ * remain. This function is called from the parse module when a .SUFFIXES:\n
+ * line is encountered. */
 void
 Suff_ClearSuffixes(void)
 {
 #ifdef CLEANUP
-    Lst_Concat(suffClean, sufflist, LST_CONCLINK);
+    Lst_MoveAll(suffClean, sufflist);
 #endif
-    sufflist = Lst_Init(FALSE);
+    sufflist = Lst_Init();
     sNum = 0;
     if (suffNull)
 	SuffFree(suffNull);
-    emptySuff = suffNull = bmake_malloc(sizeof(Suff));
+    emptySuff = suffNull = SuffNew("");
 
-    suffNull->name =   	    bmake_strdup("");
-    suffNull->nameLen =     0;
-    suffNull->searchPath =  Lst_Init(FALSE);
     Dir_Concat(suffNull->searchPath, dirSearchPath);
-    suffNull->children =    Lst_Init(FALSE);
-    suffNull->parents =	    Lst_Init(FALSE);
-    suffNull->ref =	    Lst_Init(FALSE);
-    suffNull->sNum =   	    sNum++;
     suffNull->flags =  	    SUFF_NULL;
-    suffNull->refCount =    1;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffParseTransform --
- *	Parse a transformation string to find its two component suffixes.
+/* Parse a transformation string to find its two component suffixes.
  *
  * Input:
  *	str		String being parsed
- *	srcPtr		Place to store source of trans.
- *	targPtr		Place to store target of trans.
+ *	out_src		Place to store source of trans.
+ *	out_targ	Place to store target of trans.
  *
  * Results:
- *	TRUE if the string is a valid transformation and FALSE otherwise.
- *
- * Side Effects:
- *	The passed pointers are overwritten.
- *
- *-----------------------------------------------------------------------
+ *	TRUE if the string is a valid transformation, FALSE otherwise.
  */
 static Boolean
-SuffParseTransform(char *str, Suff **srcPtr, Suff **targPtr)
+SuffParseTransform(char *str, Suff **out_src, Suff **out_targ)
 {
     LstNode		srcLn;	    /* element in suffix list of trans source*/
     Suff		*src;	    /* Source of transformation */
@@ -611,10 +497,10 @@ SuffParseTransform(char *str, Suff **srcPtr, Suff **targPtr)
      */
     for (;;) {
 	if (srcLn == NULL) {
-	    srcLn = Lst_Find(sufflist, str, SuffSuffIsPrefix);
+	    srcLn = Lst_Find(sufflist, SuffSuffIsPrefix, str);
 	} else {
-	    srcLn = Lst_FindFrom(sufflist, Lst_Succ(srcLn), str,
-				  SuffSuffIsPrefix);
+	    srcLn = Lst_FindFrom(sufflist, LstNode_Next(srcLn),
+				  SuffSuffIsPrefix, str);
 	}
 	if (srcLn == NULL) {
 	    /*
@@ -630,45 +516,30 @@ SuffParseTransform(char *str, Suff **srcPtr, Suff **targPtr)
 		 *
 		 * XXX: Use emptySuff over suffNull?
 		 */
-		*srcPtr = single;
-		*targPtr = suffNull;
+		*out_src = single;
+		*out_targ = suffNull;
 		return TRUE;
 	    }
 	    return FALSE;
 	}
-	src = (Suff *)Lst_Datum(srcLn);
+	src = LstNode_Datum(srcLn);
 	str2 = str + src->nameLen;
 	if (*str2 == '\0') {
 	    single = src;
 	    singleLn = srcLn;
 	} else {
-	    targLn = Lst_Find(sufflist, str2, SuffSuffHasNameP);
+	    targLn = Lst_Find(sufflist, SuffSuffHasName, str2);
 	    if (targLn != NULL) {
-		*srcPtr = src;
-		*targPtr = (Suff *)Lst_Datum(targLn);
+		*out_src = src;
+		*out_targ = LstNode_Datum(targLn);
 		return TRUE;
 	    }
 	}
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_IsTransform  --
- *	Return TRUE if the given string is a transformation rule
- *
- *
- * Input:
- *	str		string to check
- *
- * Results:
- *	TRUE if the string is a concatenation of two known suffixes.
- *	FALSE otherwise
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
- */
+/* Return TRUE if the given string is a transformation rule, that is, a
+ * concatenation of two known suffixes. */
 Boolean
 Suff_IsTransform(char *str)
 {
@@ -677,39 +548,35 @@ Suff_IsTransform(char *str)
     return SuffParseTransform(str, &src, &targ);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_AddTransform --
- *	Add the transformation rule described by the line to the
- *	list of rules and place the transformation itself in the graph
+/* Add the transformation rule described by the line to the list of rules
+ * and place the transformation itself in the graph.
  *
+ * The node is placed on the end of the transforms Lst and links are made
+ * between the two suffixes mentioned in the target name.
+
  * Input:
  *	line		name of transformation to add
  *
  * Results:
  *	The node created for the transformation in the transforms list
- *
- * Side Effects:
- *	The node is placed on the end of the transforms Lst and links are
- *	made between the two suffixes mentioned in the target name
- *-----------------------------------------------------------------------
  */
 GNode *
 Suff_AddTransform(char *line)
 {
     GNode         *gn;		/* GNode of transformation rule */
     Suff          *s,		/* source suffix */
-                  *t;		/* target suffix */
+		  *t;		/* target suffix */
     LstNode 	  ln;	    	/* Node for existing transformation */
+    Boolean ok;
 
-    ln = Lst_Find(transforms, line, SuffGNHasNameP);
+    ln = Lst_Find(transforms, SuffGNHasName, line);
     if (ln == NULL) {
 	/*
 	 * Make a new graph node for the transformation. It will be filled in
 	 * by the Parse module.
 	 */
 	gn = Targ_NewGN(line);
-	(void)Lst_AtEnd(transforms, gn);
+	Lst_Append(transforms, gn);
     } else {
 	/*
 	 * New specification for transformation rule. Just nuke the old list
@@ -717,58 +584,50 @@ Suff_AddTransform(char *line)
 	 * free the commands themselves, because a given command can be
 	 * attached to several different transformations.
 	 */
-	gn = (GNode *)Lst_Datum(ln);
-	Lst_Destroy(gn->commands, NULL);
-	Lst_Destroy(gn->children, NULL);
-	gn->commands = Lst_Init(FALSE);
-	gn->children = Lst_Init(FALSE);
+	gn = LstNode_Datum(ln);
+	Lst_Free(gn->commands);
+	Lst_Free(gn->children);
+	gn->commands = Lst_Init();
+	gn->children = Lst_Init();
     }
 
     gn->type = OP_TRANSFORM;
 
-    (void)SuffParseTransform(line, &s, &t);
+    ok = SuffParseTransform(line, &s, &t);
+    assert(ok);
+    (void)ok;
 
     /*
      * link the two together in the proper relationship and order
      */
-    if (DEBUG(SUFF)) {
-	fprintf(debug_file, "defining transformation from `%s' to `%s'\n",
+    SUFF_DEBUG2("defining transformation from `%s' to `%s'\n",
 		s->name, t->name);
-    }
     SuffInsert(t->children, s);
     SuffInsert(s->parents, t);
 
     return gn;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_EndTransform --
- *	Handle the finish of a transformation definition, removing the
- *	transformation from the graph if it has neither commands nor
- *	sources. This is a callback procedure for the Parse module via
- *	Lst_ForEach
+/* Handle the finish of a transformation definition, removing the
+ * transformation from the graph if it has neither commands nor sources.
+ * This is a callback procedure for the Parse module via Lst_ForEach.
+ *
+ * If the node has no commands or children, the children and parents lists
+ * of the affected suffixes are altered.
  *
  * Input:
  *	gnp		Node for transformation
- *	dummy		Node for transformation
  *
  * Results:
- *	=== 0
- *
- * Side Effects:
- *	If the node has no commands or children, the children and parents
- *	lists of the affected suffixes are altered.
- *
- *-----------------------------------------------------------------------
+ *	0, so that Lst_ForEach continues
  */
 int
 Suff_EndTransform(void *gnp, void *dummy MAKE_ATTR_UNUSED)
 {
     GNode *gn = (GNode *)gnp;
 
-    if ((gn->type & OP_DOUBLEDEP) && !Lst_IsEmpty (gn->cohorts))
-	gn = (GNode *)Lst_Datum(Lst_Last(gn->cohorts));
+    if ((gn->type & OP_DOUBLEDEP) && !Lst_IsEmpty(gn->cohorts))
+	gn = LstNode_Datum(Lst_Last(gn->cohorts));
     if ((gn->type & OP_TRANSFORM) && Lst_IsEmpty(gn->commands) &&
 	Lst_IsEmpty(gn->children))
     {
@@ -781,10 +640,8 @@ Suff_EndTransform(void *gnp, void *dummy MAKE_ATTR_UNUSED)
 	if (SuffParseTransform(gn->name, &s, &t)) {
 	    Lst	 p;
 
-	    if (DEBUG(SUFF)) {
-		fprintf(debug_file, "deleting transformation from `%s' to `%s'\n",
-		s->name, t->name);
-	    }
+	    SUFF_DEBUG2("deleting transformation from `%s' to `%s'\n",
+			s->name, t->name);
 
 	    /*
 	     * Store s->parents because s could be deleted in SuffRemove
@@ -806,35 +663,28 @@ Suff_EndTransform(void *gnp, void *dummy MAKE_ATTR_UNUSED)
 	     */
 	    SuffRemove(p, t);
 	}
-    } else if ((gn->type & OP_TRANSFORM) && DEBUG(SUFF)) {
-	fprintf(debug_file, "transformation %s complete\n", gn->name);
+    } else if (gn->type & OP_TRANSFORM) {
+        SUFF_DEBUG1("transformation %s complete\n", gn->name);
     }
 
     return 0;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffRebuildGraph --
- *	Called from Suff_AddSuffix via Lst_ForEach to search through the
- *	list of existing transformation rules and rebuild the transformation
- *	graph when it has been destroyed by Suff_ClearSuffixes. If the
- *	given rule is a transformation involving this suffix and another,
- *	existing suffix, the proper relationship is established between
- *	the two.
+/* Called from Suff_AddSuffix via Lst_ForEach to search through the list of
+ * existing transformation rules and rebuild the transformation graph when
+ * it has been destroyed by Suff_ClearSuffixes. If the given rule is a
+ * transformation involving this suffix and another, existing suffix, the
+ * proper relationship is established between the two.
+ *
+ * The appropriate links will be made between this suffix and others if
+ * transformation rules exist for it.
  *
  * Input:
  *	transformp	Transformation to test
  *	sp		Suffix to rebuild
  *
  * Results:
- *	Always 0.
- *
- * Side Effects:
- *	The appropriate links will be made between this suffix and
- *	others if transformation rules exist for it.
- *
- *-----------------------------------------------------------------------
+ *	0, so that Lst_ForEach continues
  */
 static int
 SuffRebuildGraph(void *transformp, void *sp)
@@ -844,20 +694,20 @@ SuffRebuildGraph(void *transformp, void *sp)
     char 	*cp;
     LstNode	ln;
     Suff  	*s2;
-    SuffixCmpData sd;
+    SuffSuffGetSuffixArgs sd;
 
     /*
      * First see if it is a transformation from this suffix.
      */
     cp = UNCONST(SuffStrIsPrefix(s->name, transform->name));
     if (cp != NULL) {
-	ln = Lst_Find(sufflist, cp, SuffSuffHasNameP);
+	ln = Lst_Find(sufflist, SuffSuffHasName, cp);
 	if (ln != NULL) {
 	    /*
 	     * Found target. Link in and return, since it can't be anything
 	     * else.
 	     */
-	    s2 = (Suff *)Lst_Datum(ln);
+	    s2 = LstNode_Datum(ln);
 	    SuffInsert(s2->children, s);
 	    SuffInsert(s->parents, s2);
 	    return 0;
@@ -869,13 +719,13 @@ SuffRebuildGraph(void *transformp, void *sp)
      */
     sd.len = strlen(transform->name);
     sd.ename = transform->name + sd.len;
-    cp = SuffSuffIsSuffix(s, &sd);
+    cp = SuffSuffGetSuffix(s, &sd);
     if (cp != NULL) {
 	/*
 	 * Null-terminate the source suffix in order to find it.
 	 */
 	cp[1] = '\0';
-	ln = Lst_Find(sufflist, transform->name, SuffSuffHasNameP);
+	ln = Lst_Find(sufflist, SuffSuffHasName, transform->name);
 	/*
 	 * Replace the start of the target suffix
 	 */
@@ -884,7 +734,7 @@ SuffRebuildGraph(void *transformp, void *sp)
 	    /*
 	     * Found it -- establish the proper relationship
 	     */
-	    s2 = (Suff *)Lst_Datum(ln);
+	    s2 = LstNode_Datum(ln);
 	    SuffInsert(s->children, s2);
 	    SuffInsert(s2->parents, s);
 	}
@@ -892,22 +742,16 @@ SuffRebuildGraph(void *transformp, void *sp)
     return 0;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffScanTargets --
- *	Called from Suff_AddSuffix via Lst_ForEach to search through the
- *	list of existing targets and find if any of the existing targets
- *	can be turned into a transformation rule.
+/* Called from Suff_AddSuffix via Lst_ForEach to search through the list of
+ * existing targets and find if any of the existing targets can be turned
+ * into a transformation rule.
+ *
+ * If such a target is found and the target is the current main target, the
+ * main target is set to NULL and the next target examined (if that exists)
+ * becomes the main target.
  *
  * Results:
  *	1 if a new main target has been selected, 0 otherwise.
- *
- * Side Effects:
- *	If such a target is found and the target is the current main
- *	target, the main target is set to NULL and the next target
- *	examined (if that exists) becomes the main target.
- *
- *-----------------------------------------------------------------------
  */
 static int
 SuffScanTargets(void *targetp, void *gsp)
@@ -923,7 +767,7 @@ SuffScanTargets(void *targetp, void *gsp)
 	return 1;
     }
 
-    if ((unsigned int)target->type == OP_TRANSFORM)
+    if (target->type == OP_TRANSFORM)
 	return 0;
 
     if ((ptr = strstr(target->name, gs->s->name)) == NULL ||
@@ -936,63 +780,43 @@ SuffScanTargets(void *targetp, void *gsp)
 	    *gs->gn = NULL;
 	    Targ_SetMain(NULL);
 	}
-	Lst_Destroy(target->children, NULL);
-	target->children = Lst_Init(FALSE);
+	Lst_Free(target->children);
+	target->children = Lst_Init();
 	target->type = OP_TRANSFORM;
 	/*
 	 * link the two together in the proper relationship and order
 	 */
-	if (DEBUG(SUFF)) {
-	    fprintf(debug_file, "defining transformation from `%s' to `%s'\n",
-		s->name, t->name);
-	}
+	SUFF_DEBUG2("defining transformation from `%s' to `%s'\n",
+		    s->name, t->name);
 	SuffInsert(t->children, s);
 	SuffInsert(s->parents, t);
     }
     return 0;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_AddSuffix --
- *	Add the suffix in string to the end of the list of known suffixes.
- *	Should we restructure the suffix graph? Make doesn't...
+/* Add the suffix to the end of the list of known suffixes.
+ * Should we restructure the suffix graph? Make doesn't...
+ *
+ * A GNode is created for the suffix and a Suff structure is created and
+ * added to the suffixes list unless the suffix was already known.
+ * The mainNode passed can be modified if a target mutated into a
+ * transform and that target happened to be the main target.
  *
  * Input:
- *	str		the name of the suffix to add
- *
- * Results:
- *	None
- *
- * Side Effects:
- *	A GNode is created for the suffix and a Suff structure is created and
- *	added to the suffixes list unless the suffix was already known.
- *	The mainNode passed can be modified if a target mutated into a
- *	transform and that target happened to be the main target.
- *-----------------------------------------------------------------------
+ *	name		the name of the suffix to add
  */
 void
-Suff_AddSuffix(char *str, GNode **gn)
+Suff_AddSuffix(const char *name, GNode **gn)
 {
     Suff          *s;	    /* new suffix descriptor */
     LstNode 	  ln;
     GNodeSuff	  gs;
 
-    ln = Lst_Find(sufflist, str, SuffSuffHasNameP);
+    ln = Lst_Find(sufflist, SuffSuffHasName, name);
     if (ln == NULL) {
-	s = bmake_malloc(sizeof(Suff));
+        s = SuffNew(name);
 
-	s->name =   	bmake_strdup(str);
-	s->nameLen = 	strlen(s->name);
-	s->searchPath = Lst_Init(FALSE);
-	s->children = 	Lst_Init(FALSE);
-	s->parents = 	Lst_Init(FALSE);
-	s->ref = 	Lst_Init(FALSE);
-	s->sNum =   	sNum++;
-	s->flags =  	0;
-	s->refCount =	1;
-
-	(void)Lst_AtEnd(sufflist, s);
+	Lst_Append(sufflist, s);
 	/*
 	 * We also look at our existing targets list to see if adding
 	 * this suffix will make one of our current targets mutate into
@@ -1011,51 +835,30 @@ Suff_AddSuffix(char *str, GNode **gn)
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_GetPath --
- *	Return the search path for the given suffix, if it's defined.
- *
- * Results:
- *	The searchPath for the desired suffix or NULL if the suffix isn't
- *	defined.
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
- */
+/* Return the search path for the given suffix, or NULL. */
 Lst
 Suff_GetPath(char *sname)
 {
     LstNode   	  ln;
     Suff    	  *s;
 
-    ln = Lst_Find(sufflist, sname, SuffSuffHasNameP);
+    ln = Lst_Find(sufflist, SuffSuffHasName, sname);
     if (ln == NULL) {
 	return NULL;
     } else {
-	s = (Suff *)Lst_Datum(ln);
+	s = LstNode_Datum(ln);
 	return s->searchPath;
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_DoPaths --
- *	Extend the search paths for all suffixes to include the default
- *	search path.
+/* Extend the search paths for all suffixes to include the default search
+ * path.
  *
- * Results:
- *	None.
- *
- * Side Effects:
- *	The searchPath field of all the suffixes is extended by the
- *	directories in dirSearchPath. If paths were specified for the
- *	".h" suffix, the directories are stuffed into a global variable
- *	called ".INCLUDES" with each directory preceded by a -I. The same
- *	is done for the ".a" suffix, except the variable is called
- *	".LIBS" and the flag is -L.
- *-----------------------------------------------------------------------
+ * The searchPath field of all the suffixes is extended by the directories
+ * in dirSearchPath. If paths were specified for the ".h" suffix, the
+ * directories are stuffed into a global variable called ".INCLUDES" with
+ * each directory preceded by a -I. The same is done for the ".a" suffix,
+ * except the variable is called ".LIBS" and the flag is -L.
  */
 void
 Suff_DoPaths(void)
@@ -1066,16 +869,14 @@ Suff_DoPaths(void)
     Lst	    	    	inIncludes; /* Cumulative .INCLUDES path */
     Lst	    	    	inLibs;	    /* Cumulative .LIBS path */
 
-    if (Lst_Open(sufflist) == FAILURE) {
-	return;
-    }
 
-    inIncludes = Lst_Init(FALSE);
-    inLibs = Lst_Init(FALSE);
+    inIncludes = Lst_Init();
+    inLibs = Lst_Init();
 
+    Lst_Open(sufflist);
     while ((ln = Lst_Next(sufflist)) != NULL) {
-	s = (Suff *)Lst_Datum(ln);
-	if (!Lst_IsEmpty (s->searchPath)) {
+	s = LstNode_Datum(ln);
+	if (!Lst_IsEmpty(s->searchPath)) {
 #ifdef INCLUDES
 	    if (s->flags & SUFF_INCLUDE) {
 		Dir_Concat(inIncludes, s->searchPath);
@@ -1089,9 +890,10 @@ Suff_DoPaths(void)
 	    Dir_Concat(s->searchPath, dirSearchPath);
 	} else {
 	    Lst_Destroy(s->searchPath, Dir_Destroy);
-	    s->searchPath = Lst_Duplicate(dirSearchPath, Dir_CopyDir);
+	    s->searchPath = Lst_Copy(dirSearchPath, Dir_CopyDir);
 	}
     }
+    Lst_Close(sufflist);
 
     Var_Set(".INCLUDES", ptr = Dir_MakeFlags("-I", inIncludes), VAR_GLOBAL);
     free(ptr);
@@ -1100,27 +902,15 @@ Suff_DoPaths(void)
 
     Lst_Destroy(inIncludes, Dir_Destroy);
     Lst_Destroy(inLibs, Dir_Destroy);
-
-    Lst_Close(sufflist);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_AddInclude --
- *	Add the given suffix as a type of file which gets included.
- *	Called from the parse module when a .INCLUDES line is parsed.
- *	The suffix must have already been defined.
+/* Add the given suffix as a type of file which gets included.
+ * Called from the parse module when a .INCLUDES line is parsed.
+ * The suffix must have already been defined.
+ * The SUFF_INCLUDE bit is set in the suffix's flags field.
  *
  * Input:
  *	sname		Name of the suffix to mark
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	The SUFF_INCLUDE bit is set in the suffix's flags field
- *
- *-----------------------------------------------------------------------
  */
 void
 Suff_AddInclude(char *sname)
@@ -1128,64 +918,46 @@ Suff_AddInclude(char *sname)
     LstNode	  ln;
     Suff	  *s;
 
-    ln = Lst_Find(sufflist, sname, SuffSuffHasNameP);
+    ln = Lst_Find(sufflist, SuffSuffHasName, sname);
     if (ln != NULL) {
-	s = (Suff *)Lst_Datum(ln);
+	s = LstNode_Datum(ln);
 	s->flags |= SUFF_INCLUDE;
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_AddLib --
- *	Add the given suffix as a type of file which is a library.
- *	Called from the parse module when parsing a .LIBS line. The
- *	suffix must have been defined via .SUFFIXES before this is
- *	called.
+/* Add the given suffix as a type of file which is a library.
+ * Called from the parse module when parsing a .LIBS line.
+ * The suffix must have been defined via .SUFFIXES before this is called.
+ * The SUFF_LIBRARY bit is set in the suffix's flags field.
  *
  * Input:
  *	sname		Name of the suffix to mark
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	The SUFF_LIBRARY bit is set in the suffix's flags field
- *
- *-----------------------------------------------------------------------
  */
 void
-Suff_AddLib(char *sname)
+Suff_AddLib(const char *sname)
 {
     LstNode	  ln;
     Suff	  *s;
 
-    ln = Lst_Find(sufflist, sname, SuffSuffHasNameP);
+    ln = Lst_Find(sufflist, SuffSuffHasName, sname);
     if (ln != NULL) {
-	s = (Suff *)Lst_Datum(ln);
+	s = LstNode_Datum(ln);
 	s->flags |= SUFF_LIBRARY;
     }
 }
 
- 	  /********** Implicit Source Search Functions *********/
+	  /********** Implicit Source Search Functions *********/
 
-/*-
- *-----------------------------------------------------------------------
- * SuffAddSrc  --
- *	Add a suffix as a Src structure to the given list with its parent
- *	being the given Src structure. If the suffix is the null suffix,
- *	the prefix is used unaltered as the file name in the Src structure.
+/* Add a suffix as a Src structure to the given list with its parent
+ * being the given Src structure. If the suffix is the null suffix,
+ * the prefix is used unaltered as the file name in the Src structure.
  *
  * Input:
  *	sp		suffix for which to create a Src structure
  *	lsp		list and parent for the new Src
  *
  * Results:
- *	always returns 0
- *
- * Side Effects:
- *	A Src structure is created and tacked onto the end of the list
- *-----------------------------------------------------------------------
+ *	0, so that Lst_ForEach continues
  */
 static int
 SuffAddSrc(void *sp, void *lsp)
@@ -1212,17 +984,17 @@ SuffAddSrc(void *sp, void *lsp)
 	s->refCount++;
 	s2->children =	0;
 	targ->children += 1;
-	(void)Lst_AtEnd(ls->l, s2);
+	Lst_Append(ls->l, s2);
 #ifdef DEBUG_SRC
-	s2->cp = Lst_Init(FALSE);
-	Lst_AtEnd(targ->cp, s2);
+	s2->cp = Lst_Init();
+	Lst_Append(targ->cp, s2);
 	fprintf(debug_file, "1 add %p %p to %p:", targ, s2, ls->l);
 	Lst_ForEach(ls->l, PrintAddr, NULL);
 	fprintf(debug_file, "\n");
 #endif
     }
     s2 = bmake_malloc(sizeof(Src));
-    s2->file = 	    str_concat(targ->pref, s->name, 0);
+    s2->file = 	    str_concat2(targ->pref, s->name);
     s2->pref =	    targ->pref;
     s2->parent =    targ;
     s2->node = 	    NULL;
@@ -1230,10 +1002,10 @@ SuffAddSrc(void *sp, void *lsp)
     s->refCount++;
     s2->children =  0;
     targ->children += 1;
-    (void)Lst_AtEnd(ls->l, s2);
+    Lst_Append(ls->l, s2);
 #ifdef DEBUG_SRC
-    s2->cp = Lst_Init(FALSE);
-    Lst_AtEnd(targ->cp, s2);
+    s2->cp = Lst_Init();
+    Lst_Append(targ->cp, s2);
     fprintf(debug_file, "2 add %p %p to %p:", targ, s2, ls->l);
     Lst_ForEach(ls->l, PrintAddr, NULL);
     fprintf(debug_file, "\n");
@@ -1242,21 +1014,11 @@ SuffAddSrc(void *sp, void *lsp)
     return 0;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffAddLevel  --
- *	Add all the children of targ as Src structures to the given list
+/* Add all the children of targ as Src structures to the given list.
  *
  * Input:
  *	l		list to which to add the new level
  *	targ		Src structure to use as the parent
- *
- * Results:
- *	None
- *
- * Side Effects:
- * 	Lots of structures are created and added to the list
- *-----------------------------------------------------------------------
  */
 static void
 SuffAddLevel(Lst l, Src *targ)
@@ -1269,44 +1031,31 @@ SuffAddLevel(Lst l, Src *targ)
     Lst_ForEach(targ->suff->children, SuffAddSrc, &ls);
 }
 
-/*-
- *----------------------------------------------------------------------
- * SuffRemoveSrc --
- *	Free all src structures in list that don't have a reference count
- *
- * Results:
- *	Ture if an src was removed
- *
- * Side Effects:
- *	The memory is free'd.
- *----------------------------------------------------------------------
- */
-static int
+/* Free the first Src in the list that doesn't have a reference count.
+ * Return whether a Src was removed. */
+static Boolean
 SuffRemoveSrc(Lst l)
 {
     LstNode ln;
     Src *s;
-    int t = 0;
 
-    if (Lst_Open(l) == FAILURE) {
-	return 0;
-    }
+    Lst_Open(l);
+
 #ifdef DEBUG_SRC
     fprintf(debug_file, "cleaning %lx: ", (unsigned long) l);
     Lst_ForEach(l, PrintAddr, NULL);
     fprintf(debug_file, "\n");
 #endif
 
-
     while ((ln = Lst_Next(l)) != NULL) {
-	s = (Src *)Lst_Datum(ln);
+	s = LstNode_Datum(ln);
 	if (s->children == 0) {
 	    free(s->file);
 	    if (!s->parent)
 		free(s->pref);
 	    else {
 #ifdef DEBUG_SRC
-		LstNode ln2 = Lst_Member(s->parent->cp, s);
+		LstNode ln2 = Lst_FindDatum(s->parent->cp, s);
 		if (ln2 != NULL)
 		    Lst_Remove(s->parent->cp, ln2);
 #endif
@@ -1314,11 +1063,10 @@ SuffRemoveSrc(Lst l)
 	    }
 #ifdef DEBUG_SRC
 	    fprintf(debug_file, "free: [l=%p] p=%p %d\n", l, s, s->children);
-	    Lst_Destroy(s->cp, NULL);
+	    Lst_Free(s->cp);
 #endif
 	    Lst_Remove(l, ln);
 	    free(s);
-	    t |= 1;
 	    Lst_Close(l);
 	    return TRUE;
 	}
@@ -1333,23 +1081,16 @@ SuffRemoveSrc(Lst l)
 
     Lst_Close(l);
 
-    return t;
+    return FALSE;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffFindThem --
- *	Find the first existing file/target in the list srcs
+/* Find the first existing file/target in the list srcs.
  *
  * Input:
  *	srcs		list of Src structures to search through
  *
  * Results:
- *	The lowest structure in the chain of transformations
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
+ *	The lowest structure in the chain of transformations, or NULL.
  */
 static Src *
 SuffFindThem(Lst srcs, Lst slst)
@@ -1360,12 +1101,10 @@ SuffFindThem(Lst srcs, Lst slst)
 
     rs = NULL;
 
-    while (!Lst_IsEmpty (srcs)) {
-	s = (Src *)Lst_DeQueue(srcs);
+    while (!Lst_IsEmpty(srcs)) {
+	s = Lst_Dequeue(srcs);
 
-	if (DEBUG(SUFF)) {
-	    fprintf(debug_file, "\ttrying %s...", s->file);
-	}
+	SUFF_DEBUG1("\ttrying %s...", s->file);
 
 	/*
 	 * A file is considered to exist if either a node exists in the
@@ -1388,51 +1127,41 @@ SuffFindThem(Lst srcs, Lst slst)
 	    break;
 	}
 
-	if (DEBUG(SUFF)) {
-	    fprintf(debug_file, "not there\n");
-	}
+	SUFF_DEBUG0("not there\n");
 
 	SuffAddLevel(srcs, s);
-	Lst_AtEnd(slst, s);
+	Lst_Append(slst, s);
     }
 
-    if (DEBUG(SUFF) && rs) {
-	fprintf(debug_file, "got it\n");
+    if (rs) {
+	SUFF_DEBUG0("got it\n");
     }
     return rs;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffFindCmds --
- *	See if any of the children of the target in the Src structure is
- *	one from which the target can be transformed. If there is one,
- *	a Src structure is put together for it and returned.
+/* See if any of the children of the target in the Src structure is one from
+ * which the target can be transformed. If there is one, a Src structure is
+ * put together for it and returned.
  *
  * Input:
- *	targ		Src structure to play with
+ *	targ		Src to play with
  *
  * Results:
- *	The Src structure of the "winning" child, or NULL if no such beast.
- *
- * Side Effects:
- *	A Src structure may be allocated.
- *
- *-----------------------------------------------------------------------
+ *	The Src of the "winning" child, or NULL.
  */
 static Src *
 SuffFindCmds(Src *targ, Lst slst)
 {
     LstNode 	  	ln; 	/* General-purpose list node */
     GNode		*t, 	/* Target GNode */
-	    	  	*s; 	/* Source GNode */
+			*s; 	/* Source GNode */
     int	    	  	prefLen;/* The length of the defined prefix */
     Suff    	  	*suff;	/* Suffix on matching beastie */
     Src	    	  	*ret;	/* Return value */
     char    	  	*cp;
 
     t = targ->node;
-    (void)Lst_Open(t->children);
+    Lst_Open(t->children);
     prefLen = strlen(targ->pref);
 
     for (;;) {
@@ -1441,7 +1170,7 @@ SuffFindCmds(Src *targ, Lst slst)
 	    Lst_Close(t->children);
 	    return NULL;
 	}
-	s = (GNode *)Lst_Datum(ln);
+	s = LstNode_Datum(ln);
 
 	if (s->type & OP_OPTIONAL && Lst_IsEmpty(t->commands)) {
 	    /*
@@ -1466,7 +1195,7 @@ SuffFindCmds(Src *targ, Lst slst)
 	 * The node matches the prefix ok, see if it has a known
 	 * suffix.
 	 */
-	ln = Lst_Find(sufflist, &cp[prefLen], SuffSuffHasNameP);
+	ln = Lst_Find(sufflist, SuffSuffHasName, &cp[prefLen]);
 	if (ln == NULL)
 	    continue;
 	/*
@@ -1475,9 +1204,11 @@ SuffFindCmds(Src *targ, Lst slst)
 	 *
 	 * XXX: Handle multi-stage transformations here, too.
 	 */
-	suff = (Suff *)Lst_Datum(ln);
+	suff = LstNode_Datum(ln);
 
-	if (Lst_Member(suff->parents, targ->suff) != NULL)
+	/* XXX: Can targ->suff be NULL here? */
+	if (targ->suff != NULL &&
+	    Lst_FindDatum(suff->parents, targ->suff) != NULL)
 	    break;
     }
 
@@ -1497,41 +1228,30 @@ SuffFindCmds(Src *targ, Lst slst)
     ret->children = 0;
     targ->children += 1;
 #ifdef DEBUG_SRC
-    ret->cp = Lst_Init(FALSE);
+    ret->cp = Lst_Init();
     fprintf(debug_file, "3 add %p %p\n", targ, ret);
-    Lst_AtEnd(targ->cp, ret);
+    Lst_Append(targ->cp, ret);
 #endif
-    Lst_AtEnd(slst, ret);
-    if (DEBUG(SUFF)) {
-	fprintf(debug_file, "\tusing existing source %s\n", s->name);
-    }
+    Lst_Append(slst, ret);
+    SUFF_DEBUG1("\tusing existing source %s\n", s->name);
+    Lst_Close(t->children);
     return ret;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffExpandChildren --
- *	Expand the names of any children of a given node that contain
- *	variable invocations or file wildcards into actual targets.
+/* Expand the names of any children of a given node that contain variable
+ * invocations or file wildcards into actual targets.
+ *
+ * The expanded node is removed from the parent's list of children, and the
+ * parent's unmade counter is decremented, but other nodes may be added.
  *
  * Input:
  *	cln		Child to examine
  *	pgn		Parent node being processed
- *
- * Results:
- *	=== 0 (continue)
- *
- * Side Effects:
- *	The expanded node is removed from the parent's list of children,
- *	and the parent's unmade counter is decremented, but other nodes
- * 	may be added.
- *
- *-----------------------------------------------------------------------
  */
 static void
 SuffExpandChildren(LstNode cln, GNode *pgn)
 {
-    GNode   	*cgn = (GNode *)Lst_Datum(cln);
+    GNode   	*cgn = LstNode_Datum(cln);
     GNode	*gn;	    /* New source 8) */
     char	*cp;	    /* Expanded value */
 
@@ -1554,13 +1274,11 @@ SuffExpandChildren(LstNode cln, GNode *pgn)
 	return;
     }
 
-    if (DEBUG(SUFF)) {
-	fprintf(debug_file, "Expanding \"%s\"...", cgn->name);
-    }
-    cp = Var_Subst(NULL, cgn->name, pgn, VARF_UNDEFERR|VARF_WANTRES);
+    SUFF_DEBUG1("Expanding \"%s\"...", cgn->name);
+    cp = Var_Subst(cgn->name, pgn, VARE_UNDEFERR|VARE_WANTRES);
 
-    if (cp != NULL) {
-	Lst	    members = Lst_Init(FALSE);
+    {
+	Lst	    members = Lst_Init();
 
 	if (cgn->type & OP_ARCHV) {
 	    /*
@@ -1592,7 +1310,7 @@ SuffExpandChildren(LstNode cln, GNode *pgn)
 		     */
 		    *cp++ = '\0';
 		    gn = Targ_FindNode(start, TARG_CREATE);
-		    (void)Lst_AtEnd(members, gn);
+		    Lst_Append(members, gn);
 		    while (*cp == ' ' || *cp == '\t') {
 			cp++;
 		    }
@@ -1606,12 +1324,12 @@ SuffExpandChildren(LstNode cln, GNode *pgn)
 		     * Start of a variable spec -- contact variable module
 		     * to find the end so we can skip over it.
 		     */
-		    char	*junk;
+		    const char	*junk;
 		    int 	len;
 		    void	*freeIt;
 
-		    junk = Var_Parse(cp, pgn, VARF_UNDEFERR|VARF_WANTRES,
-			&len, &freeIt);
+		    junk = Var_Parse(cp, pgn, VARE_UNDEFERR|VARE_WANTRES,
+				     &len, &freeIt);
 		    if (junk != var_Error) {
 			cp += len - 1;
 		    }
@@ -1630,7 +1348,7 @@ SuffExpandChildren(LstNode cln, GNode *pgn)
 		 * Stuff left over -- add it to the list too
 		 */
 		gn = Targ_FindNode(start, TARG_CREATE);
-		(void)Lst_AtEnd(members, gn);
+		Lst_Append(members, gn);
 	    }
 	    /*
 	     * Point cp back at the beginning again so the variable value
@@ -1643,28 +1361,25 @@ SuffExpandChildren(LstNode cln, GNode *pgn)
 	 * Add all elements of the members list to the parent node.
 	 */
 	while(!Lst_IsEmpty(members)) {
-	    gn = (GNode *)Lst_DeQueue(members);
+	    gn = Lst_Dequeue(members);
 
-	    if (DEBUG(SUFF)) {
-		fprintf(debug_file, "%s...", gn->name);
-	    }
+	    SUFF_DEBUG1("%s...", gn->name);
 	    /* Add gn to the parents child list before the original child */
-	    (void)Lst_InsertBefore(pgn->children, cln, gn);
-	    (void)Lst_AtEnd(gn->parents, pgn);
+	    Lst_InsertBefore(pgn->children, cln, gn);
+	    Lst_Append(gn->parents, pgn);
 	    pgn->unmade++;
 	    /* Expand wildcards on new node */
-	    SuffExpandWildcards(Lst_Prev(cln), pgn);
+	    SuffExpandWildcards(LstNode_Prev(cln), pgn);
 	}
-	Lst_Destroy(members, NULL);
+	Lst_Free(members);
 
 	/*
 	 * Free the result
 	 */
 	free(cp);
     }
-    if (DEBUG(SUFF)) {
-	fprintf(debug_file, "\n");
-    }
+
+    SUFF_DEBUG0("\n");
 
     /*
      * Now the source is expanded, remove it from the list of children to
@@ -1672,13 +1387,13 @@ SuffExpandChildren(LstNode cln, GNode *pgn)
      */
     pgn->unmade--;
     Lst_Remove(pgn->children, cln);
-    Lst_Remove(cgn->parents, Lst_Member(cgn->parents, pgn));
+    Lst_Remove(cgn->parents, Lst_FindDatum(cgn->parents, pgn));
 }
 
 static void
 SuffExpandWildcards(LstNode cln, GNode *pgn)
 {
-    GNode   	*cgn = (GNode *)Lst_Datum(cln);
+    GNode   	*cgn = LstNode_Datum(cln);
     GNode	*gn;	    /* New source 8) */
     char	*cp;	    /* Expanded value */
     Lst 	explist;    /* List of expansions */
@@ -1689,34 +1404,27 @@ SuffExpandWildcards(LstNode cln, GNode *pgn)
     /*
      * Expand the word along the chosen path
      */
-    explist = Lst_Init(FALSE);
+    explist = Lst_Init();
     Dir_Expand(cgn->name, Suff_FindPath(cgn), explist);
 
     while (!Lst_IsEmpty(explist)) {
 	/*
 	 * Fetch next expansion off the list and find its GNode
 	 */
-	cp = (char *)Lst_DeQueue(explist);
+	cp = Lst_Dequeue(explist);
 
-	if (DEBUG(SUFF)) {
-	    fprintf(debug_file, "%s...", cp);
-	}
+	SUFF_DEBUG1("%s...", cp);
 	gn = Targ_FindNode(cp, TARG_CREATE);
 
 	/* Add gn to the parents child list before the original child */
-	(void)Lst_InsertBefore(pgn->children, cln, gn);
-	(void)Lst_AtEnd(gn->parents, pgn);
+	Lst_InsertBefore(pgn->children, cln, gn);
+	Lst_Append(gn->parents, pgn);
 	pgn->unmade++;
     }
 
-    /*
-     * Nuke what's left of the list
-     */
-    Lst_Destroy(explist, NULL);
+    Lst_Free(explist);
 
-    if (DEBUG(SUFF)) {
-	fprintf(debug_file, "\n");
-    }
+    SUFF_DEBUG0("\n");
 
     /*
      * Now the source is expanded, remove it from the list of children to
@@ -1724,28 +1432,19 @@ SuffExpandWildcards(LstNode cln, GNode *pgn)
      */
     pgn->unmade--;
     Lst_Remove(pgn->children, cln);
-    Lst_Remove(cgn->parents, Lst_Member(cgn->parents, pgn));
+    Lst_Remove(cgn->parents, Lst_FindDatum(cgn->parents, pgn));
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_FindPath --
- *	Find a path along which to expand the node.
+/* Find a path along which to expand the node.
  *
- *	If the word has a known suffix, use that path.
- *	If it has no known suffix, use the default system search path.
+ * If the word has a known suffix, use that path.
+ * If it has no known suffix, use the default system search path.
  *
  * Input:
  *	gn		Node being examined
  *
  * Results:
  *	The appropriate path to search for the GNode.
- *
- * Side Effects:
- *	XXX: We could set the suffix here so that we don't have to scan
- *	again.
- *
- *-----------------------------------------------------------------------
  */
 Lst
 Suff_FindPath(GNode* gn)
@@ -1753,24 +1452,20 @@ Suff_FindPath(GNode* gn)
     Suff *suff = gn->suffix;
 
     if (suff == NULL) {
-	SuffixCmpData sd;   /* Search string data */
+	SuffSuffGetSuffixArgs sd;   /* Search string data */
 	LstNode ln;
 	sd.len = strlen(gn->name);
 	sd.ename = gn->name + sd.len;
-	ln = Lst_Find(sufflist, &sd, SuffSuffIsSuffixP);
+	ln = Lst_Find(sufflist, SuffSuffIsSuffix, &sd);
 
-	if (DEBUG(SUFF)) {
-	    fprintf(debug_file, "Wildcard expanding \"%s\"...", gn->name);
-	}
+	SUFF_DEBUG1("Wildcard expanding \"%s\"...", gn->name);
 	if (ln != NULL)
-	    suff = (Suff *)Lst_Datum(ln);
+	    suff = LstNode_Datum(ln);
 	/* XXX: Here we can save the suffix so we don't have to do this again */
     }
 
     if (suff != NULL) {
-	if (DEBUG(SUFF)) {
-	    fprintf(debug_file, "suffix is \"%s\"...", suff->name);
-	}
+	SUFF_DEBUG1("suffix is \"%s\"...", suff->name);
 	return suff->searchPath;
     } else {
 	/*
@@ -1780,11 +1475,8 @@ Suff_FindPath(GNode* gn)
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffApplyTransform --
- *	Apply a transformation rule, given the source and target nodes
- *	and suffixes.
+/* Apply a transformation rule, given the source and target nodes and
+ * suffixes.
  *
  * Input:
  *	tGn		Target node
@@ -1801,8 +1493,6 @@ Suff_FindPath(GNode* gn)
  *	All attributes but OP_DEPMASK and OP_TRANSFORM are applied
  *	to the target. The target also inherits all the sources for
  *	the transformation rule.
- *
- *-----------------------------------------------------------------------
  */
 static Boolean
 SuffApplyTransform(GNode *tGn, GNode *sGn, Suff *t, Suff *s)
@@ -1814,15 +1504,15 @@ SuffApplyTransform(GNode *tGn, GNode *sGn, Suff *t, Suff *s)
     /*
      * Form the proper links between the target and source.
      */
-    (void)Lst_AtEnd(tGn->children, sGn);
-    (void)Lst_AtEnd(sGn->parents, tGn);
+    Lst_Append(tGn->children, sGn);
+    Lst_Append(sGn->parents, tGn);
     tGn->unmade += 1;
 
     /*
      * Locate the transformation rule itself
      */
-    tname = str_concat(s->name, t->name, 0);
-    ln = Lst_Find(transforms, tname, SuffGNHasNameP);
+    tname = str_concat2(s->name, t->name);
+    ln = Lst_Find(transforms, SuffGNHasName, tname);
     free(tname);
 
     if (ln == NULL) {
@@ -1834,11 +1524,9 @@ SuffApplyTransform(GNode *tGn, GNode *sGn, Suff *t, Suff *s)
 	return FALSE;
     }
 
-    gn = (GNode *)Lst_Datum(ln);
+    gn = LstNode_Datum(ln);
 
-    if (DEBUG(SUFF)) {
-	fprintf(debug_file, "\tapplying %s -> %s to \"%s\"\n", s->name, t->name, tGn->name);
-    }
+    SUFF_DEBUG3("\tapplying %s -> %s to \"%s\"\n", s->name, t->name, tGn->name);
 
     /*
      * Record last child for expansion purposes
@@ -1853,8 +1541,8 @@ SuffApplyTransform(GNode *tGn, GNode *sGn, Suff *t, Suff *s)
     /*
      * Deal with wildcards and variables in any acquired sources
      */
-    for (ln = Lst_Succ(ln); ln != NULL; ln = nln) {
-	nln = Lst_Succ(ln);
+    for (ln = ln != NULL ? LstNode_Next(ln) : NULL; ln != NULL; ln = nln) {
+	nln = LstNode_Next(ln);
 	SuffExpandChildren(ln, tGn);
     }
 
@@ -1862,27 +1550,19 @@ SuffApplyTransform(GNode *tGn, GNode *sGn, Suff *t, Suff *s)
      * Keep track of another parent to which this beast is transformed so
      * the .IMPSRC variable can be set correctly for the parent.
      */
-    (void)Lst_AtEnd(sGn->iParents, tGn);
+    Lst_Append(sGn->implicitParents, tGn);
 
     return TRUE;
 }
 
 
-/*-
- *-----------------------------------------------------------------------
- * SuffFindArchiveDeps --
- *	Locate dependencies for an OP_ARCHV node.
+/* Locate dependencies for an OP_ARCHV node.
  *
  * Input:
  *	gn		Node for which to locate dependencies
  *
- * Results:
- *	None
- *
  * Side Effects:
  *	Same as Suff_FindDeps
- *
- *-----------------------------------------------------------------------
  */
 static void
 SuffFindArchiveDeps(GNode *gn, Lst slst)
@@ -1890,13 +1570,7 @@ SuffFindArchiveDeps(GNode *gn, Lst slst)
     char    	*eoarch;    /* End of archive portion */
     char    	*eoname;    /* End of member portion */
     GNode   	*mem;	    /* Node for member */
-    static const char	*copy[] = {
-	/* Variables to be copied from the member node */
-	TARGET,	    	    /* Must be first */
-	PREFIX,	    	    /* Must be second */
-    };
     LstNode 	ln, nln;    /* Next suffix node to check */
-    int	    	i;  	    /* Index into copy and vals */
     Suff    	*ms;	    /* Suffix descriptor for member */
     char    	*name;	    /* Start of member's name */
 
@@ -1932,18 +1606,19 @@ SuffFindArchiveDeps(GNode *gn, Lst slst)
     /*
      * Create the link between the two nodes right off
      */
-    (void)Lst_AtEnd(gn->children, mem);
-    (void)Lst_AtEnd(mem->parents, gn);
+    Lst_Append(gn->children, mem);
+    Lst_Append(mem->parents, gn);
     gn->unmade += 1;
 
     /*
      * Copy in the variables from the member node to this one.
      */
-    for (i = (sizeof(copy)/sizeof(copy[0]))-1; i >= 0; i--) {
-	char *p1;
-	Var_Set(copy[i], Var_Value(copy[i], mem, &p1), gn);
-	free(p1);
-
+    {
+	char *freeIt;
+	Var_Set(PREFIX, Var_Value(PREFIX, mem, &freeIt), gn);
+	bmake_free(freeIt);
+	Var_Set(TARGET, Var_Value(TARGET, mem, &freeIt), gn);
+	bmake_free(freeIt);
     }
 
     ms = mem->suffix;
@@ -1951,9 +1626,7 @@ SuffFindArchiveDeps(GNode *gn, Lst slst)
 	/*
 	 * Didn't know what it was -- use .NULL suffix if not in make mode
 	 */
-	if (DEBUG(SUFF)) {
-	    fprintf(debug_file, "using null suffix\n");
-	}
+	SUFF_DEBUG0("using null suffix\n");
 	ms = suffNull;
     }
 
@@ -1974,7 +1647,7 @@ SuffFindArchiveDeps(GNode *gn, Lst slst)
      * that still contain variables or wildcards in their names.
      */
     for (ln = Lst_First(gn->children); ln != NULL; ln = nln) {
-	nln = Lst_Succ(ln);
+	nln = LstNode_Next(ln);
 	SuffExpandChildren(ln, gn);
     }
 
@@ -1985,24 +1658,23 @@ SuffFindArchiveDeps(GNode *gn, Lst slst)
 	 * through the entire list, we just look at suffixes to which the
 	 * member's suffix may be transformed...
 	 */
-	SuffixCmpData	sd;		/* Search string data */
+	SuffSuffGetSuffixArgs sd;	/* Search string data */
 
 	/*
 	 * Use first matching suffix...
 	 */
 	sd.len = eoarch - gn->name;
 	sd.ename = eoarch;
-	ln = Lst_Find(ms->parents, &sd, SuffSuffIsSuffixP);
+	ln = Lst_Find(ms->parents, SuffSuffIsSuffix, &sd);
 
 	if (ln != NULL) {
 	    /*
 	     * Got one -- apply it
 	     */
-	    if (!SuffApplyTransform(gn, mem, (Suff *)Lst_Datum(ln), ms) &&
-		DEBUG(SUFF))
-	    {
-		fprintf(debug_file, "\tNo transformation from %s -> %s\n",
-		       ms->name, ((Suff *)Lst_Datum(ln))->name);
+	    Suff *suff = LstNode_Datum(ln);
+	    if (!SuffApplyTransform(gn, mem, suff, ms)) {
+		SUFF_DEBUG2("\tNo transformation from %s -> %s\n",
+			    ms->name, suff->name);
 	    }
 	}
     }
@@ -2030,21 +1702,13 @@ SuffFindArchiveDeps(GNode *gn, Lst slst)
     mem->type |= OP_MEMBER | OP_JOIN | OP_MADE;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * SuffFindNormalDeps --
- *	Locate implicit dependencies for regular targets.
+/* Locate implicit dependencies for regular targets.
  *
  * Input:
  *	gn		Node for which to find sources
  *
- * Results:
- *	None.
- *
  * Side Effects:
- *	Same as Suff_FindDeps...
- *
- *-----------------------------------------------------------------------
+ *	Same as Suff_FindDeps
  */
 static void
 SuffFindNormalDeps(GNode *gn, Lst slst)
@@ -2060,7 +1724,7 @@ SuffFindNormalDeps(GNode *gn, Lst slst)
     Src 	*src;	    /* General Src pointer */
     char    	*pref;	    /* Prefix to use */
     Src	    	*targ;	    /* General Src target pointer */
-    SuffixCmpData sd;	    /* Search string data */
+    SuffSuffGetSuffixArgs sd; /* Search string data */
 
 
     sd.len = strlen(gn->name);
@@ -2072,8 +1736,8 @@ SuffFindNormalDeps(GNode *gn, Lst slst)
      * Begin at the beginning...
      */
     ln = Lst_First(sufflist);
-    srcs = Lst_Init(FALSE);
-    targs = Lst_Init(FALSE);
+    srcs = Lst_Init();
+    targs = Lst_Init();
 
     /*
      * We're caught in a catch-22 here. On the one hand, we want to use any
@@ -2102,34 +1766,27 @@ SuffFindNormalDeps(GNode *gn, Lst slst)
 	    /*
 	     * Look for next possible suffix...
 	     */
-	    ln = Lst_FindFrom(sufflist, ln, &sd, SuffSuffIsSuffixP);
+	    ln = Lst_FindFrom(sufflist, ln, SuffSuffIsSuffix, &sd);
 
 	    if (ln != NULL) {
-		int	    prefLen;	    /* Length of the prefix */
+	        const char *eopref;
 
 		/*
 		 * Allocate a Src structure to which things can be transformed
 		 */
 		targ = bmake_malloc(sizeof(Src));
 		targ->file = bmake_strdup(gn->name);
-		targ->suff = (Suff *)Lst_Datum(ln);
+		targ->suff = LstNode_Datum(ln);
 		targ->suff->refCount++;
 		targ->node = gn;
 		targ->parent = NULL;
 		targ->children = 0;
 #ifdef DEBUG_SRC
-		targ->cp = Lst_Init(FALSE);
+		targ->cp = Lst_Init();
 #endif
 
-		/*
-		 * Allocate room for the prefix, whose end is found by
-		 * subtracting the length of the suffix from
-		 * the end of the name.
-		 */
-		prefLen = (eoname - targ->suff->nameLen) - sopref;
-		targ->pref = bmake_malloc(prefLen + 1);
-		memcpy(targ->pref, sopref, prefLen);
-		targ->pref[prefLen] = '\0';
+		eopref = eoname - targ->suff->nameLen;
+		targ->pref = bmake_strsedup(sopref, eopref);
 
 		/*
 		 * Add nodes from which the target can be made
@@ -2139,12 +1796,12 @@ SuffFindNormalDeps(GNode *gn, Lst slst)
 		/*
 		 * Record the target so we can nuke it
 		 */
-		(void)Lst_AtEnd(targs, targ);
+		Lst_Append(targs, targ);
 
 		/*
 		 * Search from this suffix's successor...
 		 */
-		ln = Lst_Succ(ln);
+		ln = LstNode_Next(ln);
 	    }
 	}
 
@@ -2152,9 +1809,8 @@ SuffFindNormalDeps(GNode *gn, Lst slst)
 	 * Handle target of unknown suffix...
 	 */
 	if (Lst_IsEmpty(targs) && suffNull != NULL) {
-	    if (DEBUG(SUFF)) {
-		fprintf(debug_file, "\tNo known suffix on %s. Using .NULL suffix\n", gn->name);
-	    }
+	    SUFF_DEBUG1("\tNo known suffix on %s. Using .NULL suffix\n",
+			gn->name);
 
 	    targ = bmake_malloc(sizeof(Src));
 	    targ->file = bmake_strdup(gn->name);
@@ -2165,7 +1821,7 @@ SuffFindNormalDeps(GNode *gn, Lst slst)
 	    targ->children = 0;
 	    targ->pref = bmake_strdup(sopref);
 #ifdef DEBUG_SRC
-	    targ->cp = Lst_Init(FALSE);
+	    targ->cp = Lst_Init();
 #endif
 
 	    /*
@@ -2177,14 +1833,12 @@ SuffFindNormalDeps(GNode *gn, Lst slst)
 	    if (Lst_IsEmpty(gn->commands))
 		SuffAddLevel(srcs, targ);
 	    else {
-		if (DEBUG(SUFF))
-		    fprintf(debug_file, "not ");
+		SUFF_DEBUG0("not ");
 	    }
 
-	    if (DEBUG(SUFF))
-		fprintf(debug_file, "adding suffix rules\n");
+	    SUFF_DEBUG0("adding suffix rules\n");
 
-	    (void)Lst_AtEnd(targs, targ);
+	    Lst_Append(targs, targ);
 	}
 
 	/*
@@ -2199,7 +1853,7 @@ SuffFindNormalDeps(GNode *gn, Lst slst)
 	     * for setting the local variables.
 	     */
 	    if (!Lst_IsEmpty(targs)) {
-		targ = (Src *)Lst_Datum(Lst_First(targs));
+		targ = LstNode_Datum(Lst_First(targs));
 	    } else {
 		targ = NULL;
 	    }
@@ -2223,14 +1877,12 @@ SuffFindNormalDeps(GNode *gn, Lst slst)
      * that still contain variables or wildcards in their names.
      */
     for (ln = Lst_First(gn->children); ln != NULL; ln = nln) {
-	nln = Lst_Succ(ln);
+	nln = LstNode_Next(ln);
 	SuffExpandChildren(ln, gn);
     }
 
     if (targ == NULL) {
-	if (DEBUG(SUFF)) {
-	    fprintf(debug_file, "\tNo valid suffix on %s\n", gn->name);
-	}
+	SUFF_DEBUG1("\tNo valid suffix on %s\n", gn->name);
 
 sfnd_abort:
 	/*
@@ -2314,8 +1966,8 @@ sfnd_abort:
 	     * up to, but not including, the parent node.
 	     */
 	    while (bottom && bottom->parent != NULL) {
-		if (Lst_Member(slst, bottom) == NULL) {
-		    Lst_AtEnd(slst, bottom);
+		if (Lst_FindDatum(slst, bottom) == NULL) {
+		    Lst_Append(slst, bottom);
 		}
 		bottom = bottom->parent;
 	    }
@@ -2390,45 +2042,31 @@ sfnd_abort:
      */
 sfnd_return:
     if (bottom)
-	if (Lst_Member(slst, bottom) == NULL)
-	    Lst_AtEnd(slst, bottom);
+	if (Lst_FindDatum(slst, bottom) == NULL)
+	    Lst_Append(slst, bottom);
 
     while (SuffRemoveSrc(srcs) || SuffRemoveSrc(targs))
 	continue;
 
-    Lst_Concat(slst, srcs, LST_CONCLINK);
-    Lst_Concat(slst, targs, LST_CONCLINK);
+    Lst_MoveAll(slst, srcs);
+    Lst_MoveAll(slst, targs);
 }
 
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_FindDeps  --
- *	Find implicit sources for the target described by the graph node
- *	gn
+/* Find implicit sources for the target described by the graph node.
  *
- * Results:
- *	Nothing.
+ * Nodes are added to the graph below the passed-in node. The nodes are
+ * marked to have their IMPSRC variable filled in. The PREFIX variable is set
+ * for the given node and all its implied children.
  *
- * Side Effects:
- *	Nodes are added to the graph below the passed-in node. The nodes
- *	are marked to have their IMPSRC variable filled in. The
- *	PREFIX variable is set for the given node and all its
- *	implied children.
- *
- * Notes:
- *	The path found by this target is the shortest path in the
- *	transformation graph, which may pass through non-existent targets,
- *	to an existing target. The search continues on all paths from the
- *	root suffix until a file is found. I.e. if there's a path
- *	.o -> .c -> .l -> .l,v from the root and the .l,v file exists but
- *	the .c and .l files don't, the search will branch out in
- *	all directions from .o and again from all the nodes on the
- *	next level until the .l,v node is encountered.
- *
- *-----------------------------------------------------------------------
+ * The path found by this target is the shortest path in the transformation
+ * graph, which may pass through non-existent targets, to an existing target.
+ * The search continues on all paths from the root suffix until a file is
+ * found. I.e. if there's a path .o -> .c -> .l -> .l,v from the root and the
+ * .l,v file exists but the .c and .l files don't, the search will branch out
+ * in all directions from .o and again from all the nodes on the next level
+ * until the .l,v node is encountered.
  */
-
 void
 Suff_FindDeps(GNode *gn)
 {
@@ -2438,32 +2076,20 @@ Suff_FindDeps(GNode *gn)
 	continue;
 }
 
-
-/*
- * Input:
- *	gn		node we're dealing with
- *
- */
 static void
 SuffFindDeps(GNode *gn, Lst slst)
 {
-    if (gn->type & OP_DEPS_FOUND) {
-	/*
-	 * If dependencies already found, no need to do it again...
-	 */
+    if (gn->type & OP_DEPS_FOUND)
 	return;
-    } else {
-	gn->type |= OP_DEPS_FOUND;
-    }
+    gn->type |= OP_DEPS_FOUND;
+
     /*
      * Make sure we have these set, may get revised below.
      */
     Var_Set(TARGET, gn->path ? gn->path : gn->name, gn);
     Var_Set(PREFIX, gn->name, gn);
 
-    if (DEBUG(SUFF)) {
-	fprintf(debug_file, "SuffFindDeps (%s)\n", gn->name);
-    }
+    SUFF_DEBUG1("SuffFindDeps (%s)\n", gn->name);
 
     if (gn->type & OP_ARCHV) {
 	SuffFindArchiveDeps(gn, slst);
@@ -2479,11 +2105,11 @@ SuffFindDeps(GNode *gn, Lst slst)
 	LstNode	ln;
 	Suff	*s;
 
-	ln = Lst_Find(sufflist, LIBSUFF, SuffSuffHasNameP);
+	ln = Lst_Find(sufflist, SuffSuffHasName, LIBSUFF);
 	if (gn->suffix)
 	    gn->suffix->refCount--;
 	if (ln != NULL) {
-	    gn->suffix = s = (Suff *)Lst_Datum(ln);
+	    gn->suffix = s = LstNode_Datum(ln);
 	    gn->suffix->refCount++;
 	    Arch_FindLib(gn, s->searchPath);
 	} else {
@@ -2501,25 +2127,13 @@ SuffFindDeps(GNode *gn, Lst slst)
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_SetNull --
- *	Define which suffix is the null suffix.
+/* Define which suffix is the null suffix.
+ *
+ * Need to handle the changing of the null suffix gracefully so the old
+ * transformation rules don't just go away.
  *
  * Input:
  *	name		Name of null suffix
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	'suffNull' is altered.
- *
- * Notes:
- *	Need to handle the changing of the null suffix gracefully so the
- *	old transformation rules don't just go away.
- *
- *-----------------------------------------------------------------------
  */
 void
 Suff_SetNull(char *name)
@@ -2527,9 +2141,9 @@ Suff_SetNull(char *name)
     Suff    *s;
     LstNode ln;
 
-    ln = Lst_Find(sufflist, name, SuffSuffHasNameP);
+    ln = Lst_Find(sufflist, SuffSuffHasName, name);
     if (ln != NULL) {
-	s = (Suff *)Lst_Datum(ln);
+	s = LstNode_Datum(ln);
 	if (suffNull != NULL) {
 	    suffNull->flags &= ~SUFF_NULL;
 	}
@@ -2544,26 +2158,16 @@ Suff_SetNull(char *name)
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Suff_Init --
- *	Initialize suffixes module
- *
- * Results:
- *	None
- *
- * Side Effects:
- *	Many
- *-----------------------------------------------------------------------
- */
+/* Initialize the suffixes module. */
 void
 Suff_Init(void)
 {
 #ifdef CLEANUP
-    suffClean = Lst_Init(FALSE);
+    suffClean = Lst_Init();
+    sufflist = Lst_Init();
 #endif
-    srclist = Lst_Init(FALSE);
-    transforms = Lst_Init(FALSE);
+    srclist = Lst_Init();
+    transforms = Lst_Init();
 
     /*
      * Create null suffix for single-suffix rules (POSIX). The thing doesn't
@@ -2574,19 +2178,7 @@ Suff_Init(void)
 }
 
 
-/*-
- *----------------------------------------------------------------------
- * Suff_End --
- *	Cleanup the this module
- *
- * Results:
- *	None
- *
- * Side Effects:
- *	The memory is free'd.
- *----------------------------------------------------------------------
- */
-
+/* Clean up the suffixes module. */
 void
 Suff_End(void)
 {
@@ -2595,8 +2187,8 @@ Suff_End(void)
     Lst_Destroy(suffClean, SuffFree);
     if (suffNull)
 	SuffFree(suffNull);
-    Lst_Destroy(srclist, NULL);
-    Lst_Destroy(transforms, NULL);
+    Lst_Free(srclist);
+    Lst_Free(transforms);
 #endif
 }
 
@@ -2614,30 +2206,15 @@ static int
 SuffPrintSuff(void *sp, void *dummy MAKE_ATTR_UNUSED)
 {
     Suff    *s = (Suff *)sp;
-    int	    flags;
-    int	    flag;
 
     fprintf(debug_file, "# `%s' [%d] ", s->name, s->refCount);
 
-    flags = s->flags;
-    if (flags) {
-	fputs(" (", debug_file);
-	while (flags) {
-	    flag = 1 << (ffs(flags) - 1);
-	    flags &= ~flag;
-	    switch (flag) {
-		case SUFF_NULL:
-		    fprintf(debug_file, "NULL");
-		    break;
-		case SUFF_INCLUDE:
-		    fprintf(debug_file, "INCLUDE");
-		    break;
-		case SUFF_LIBRARY:
-		    fprintf(debug_file, "LIBRARY");
-		    break;
-	    }
-	    fputc(flags ? '|' : ')', debug_file);
-	}
+    if (s->flags != 0) {
+	char flags_buf[SuffFlags_ToStringSize];
+
+	fprintf(debug_file, " (%s)",
+		Enum_FlagsToString(flags_buf, sizeof flags_buf,
+				   s->flags, SuffFlags_ToStringSpecs));
     }
     fputc('\n', debug_file);
     fprintf(debug_file, "#\tTo: ");
diff --git a/targ.c b/targ.c
index 15dc01f0e08d..c98abbed5705 100644
--- a/targ.c
+++ b/targ.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: targ.c,v 1.63 2020/07/03 08:02:55 rillig Exp $	*/
+/*	$NetBSD: targ.c,v 1.81 2020/09/01 20:54:00 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -69,14 +69,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: targ.c,v 1.63 2020/07/03 08:02:55 rillig Exp $";
+static char rcsid[] = "$NetBSD: targ.c,v 1.81 2020/09/01 20:54:00 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)targ.c	8.2 (Berkeley) 3/19/94";
 #else
-__RCSID("$NetBSD: targ.c,v 1.63 2020/07/03 08:02:55 rillig Exp $");
+__RCSID("$NetBSD: targ.c,v 1.81 2020/09/01 20:54:00 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -84,8 +84,7 @@ __RCSID("$NetBSD: targ.c,v 1.63 2020/07/03 08:02:55 rillig Exp $");
 /*-
  * targ.c --
  *	Functions for maintaining the Lst allTargets. Target nodes are
- * kept in two structures: a Lst, maintained by the list library, and a
- * hash table, maintained by the hash library.
+ *	kept in two structures: a Lst and a hash table.
  *
  * Interface:
  *	Targ_Init 	    	Initialization procedure.
@@ -133,7 +132,6 @@ __RCSID("$NetBSD: targ.c,v 1.63 2020/07/03 08:02:55 rillig Exp $");
 #include	  <time.h>
 
 #include	  "make.h"
-#include	  "hash.h"
 #include	  "dir.h"
 
 static Lst        allTargets;	/* the list of all targets found so far */
@@ -142,91 +140,49 @@ static Lst	  allGNs;	/* List of all the GNodes */
 #endif
 static Hash_Table targets;	/* a hash table of same */
 
-#define HTSIZE	191		/* initial size of hash table */
-
 static int TargPrintOnlySrc(void *, void *);
 static int TargPrintName(void *, void *);
 #ifdef CLEANUP
 static void TargFreeGN(void *);
 #endif
-static int TargPropagateCohort(void *, void *);
-static int TargPropagateNode(void *, void *);
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_Init --
- *	Initialize this module
- *
- * Results:
- *	None
- *
- * Side Effects:
- *	The allTargets list and the targets hash table are initialized
- *-----------------------------------------------------------------------
- */
 void
 Targ_Init(void)
 {
-    allTargets = Lst_Init(FALSE);
-    Hash_InitTable(&targets, HTSIZE);
+    allTargets = Lst_Init();
+    Hash_InitTable(&targets, 191);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_End --
- *	Finalize this module
- *
- * Results:
- *	None
- *
- * Side Effects:
- *	All lists and gnodes are cleared
- *-----------------------------------------------------------------------
- */
 void
 Targ_End(void)
 {
+    Targ_Stats();
 #ifdef CLEANUP
-    Lst_Destroy(allTargets, NULL);
-    if (allGNs)
+    Lst_Free(allTargets);
+    if (allGNs != NULL)
 	Lst_Destroy(allGNs, TargFreeGN);
     Hash_DeleteTable(&targets);
 #endif
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_List --
- *	Return the list of all targets
- *
- * Results:
- *	The list of all targets.
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
- */
+void
+Targ_Stats(void)
+{
+    Hash_DebugStats(&targets, "targets");
+}
+
+/* Return the list of all targets. */
 Lst
 Targ_List(void)
 {
     return allTargets;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_NewGN  --
- *	Create and initialize a new graph node
+/* Create and initialize a new graph node. The gnode is added to the list of
+ * all gnodes.
  *
  * Input:
- *	name		the name to stick in the new node
- *
- * Results:
- *	An initialized graph node with the name field filled with a copy
- *	of the passed name
- *
- * Side Effects:
- *	The gnode is added to the list of all gnodes.
- *-----------------------------------------------------------------------
+ *	name		the name of the node, such as "clean", "src.c"
  */
 GNode *
 Targ_NewGN(const char *name)
@@ -237,11 +193,7 @@ Targ_NewGN(const char *name)
     gn->name = bmake_strdup(name);
     gn->uname = NULL;
     gn->path = NULL;
-    if (name[0] == '-' && name[1] == 'l') {
-	gn->type = OP_LIB;
-    } else {
-	gn->type = 0;
-    }
+    gn->type = name[0] == '-' && name[1] == 'l' ? OP_LIB : 0;
     gn->unmade =    	0;
     gn->unmade_cohorts = 0;
     gn->cohort_num[0] = 0;
@@ -251,82 +203,65 @@ Targ_NewGN(const char *name)
     gn->checked =	0;
     gn->mtime =		0;
     gn->cmgn =		NULL;
-    gn->iParents =  	Lst_Init(FALSE);
-    gn->cohorts =   	Lst_Init(FALSE);
-    gn->parents =   	Lst_Init(FALSE);
-    gn->children =  	Lst_Init(FALSE);
-    gn->order_pred =  	Lst_Init(FALSE);
-    gn->order_succ =  	Lst_Init(FALSE);
+    gn->implicitParents = Lst_Init();
+    gn->cohorts =   	Lst_Init();
+    gn->parents =   	Lst_Init();
+    gn->children =  	Lst_Init();
+    gn->order_pred =  	Lst_Init();
+    gn->order_succ =  	Lst_Init();
     Hash_InitTable(&gn->context, 0);
-    gn->commands =  	Lst_Init(FALSE);
+    gn->commands =  	Lst_Init();
     gn->suffix =	NULL;
-    gn->lineno =	0;
     gn->fname = 	NULL;
+    gn->lineno =	0;
 
 #ifdef CLEANUP
     if (allGNs == NULL)
-	allGNs = Lst_Init(FALSE);
-    Lst_AtEnd(allGNs, gn);
+	allGNs = Lst_Init();
+    Lst_Append(allGNs, gn);
 #endif
 
     return gn;
 }
 
 #ifdef CLEANUP
-/*-
- *-----------------------------------------------------------------------
- * TargFreeGN  --
- *	Destroy a GNode
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	None.
- *-----------------------------------------------------------------------
- */
 static void
 TargFreeGN(void *gnp)
 {
     GNode *gn = (GNode *)gnp;
 
-
     free(gn->name);
     free(gn->uname);
     free(gn->path);
+
+    Lst_Free(gn->implicitParents);
+    Lst_Free(gn->cohorts);
+    Lst_Free(gn->parents);
+    Lst_Free(gn->children);
+    Lst_Free(gn->order_succ);
+    Lst_Free(gn->order_pred);
+    Hash_DeleteTable(&gn->context);
+    Lst_Free(gn->commands);
+
+    /* XXX: does gn->suffix need to be freed? It is reference-counted. */
     /* gn->fname points to name allocated when file was opened, don't free */
 
-    Lst_Destroy(gn->iParents, NULL);
-    Lst_Destroy(gn->cohorts, NULL);
-    Lst_Destroy(gn->parents, NULL);
-    Lst_Destroy(gn->children, NULL);
-    Lst_Destroy(gn->order_succ, NULL);
-    Lst_Destroy(gn->order_pred, NULL);
-    Hash_DeleteTable(&gn->context);
-    Lst_Destroy(gn->commands, NULL);
     free(gn);
 }
 #endif
 
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_FindNode  --
- *	Find a node in the list using the given name for matching
+/* Find a node in the list using the given name for matching.
+ * If the node is created, it is added to the .ALLTARGETS list.
  *
  * Input:
  *	name		the name to find
- *	flags		flags governing events when target not
- *			found
+ *	flags		flags governing events when target not found
  *
  * Results:
- *	The node in the list if it was. If it wasn't, return NULL of
+ *	The node in the list if it was. If it wasn't, return NULL if
  *	flags was TARG_NOCREATE or the newly created and initialized node
  *	if it was TARG_CREATE
- *
- * Side Effects:
- *	Sometimes a node is created and added to the list
- *-----------------------------------------------------------------------
  */
 GNode *
 Targ_FindNode(const char *name, int flags)
@@ -353,16 +288,16 @@ Targ_FindNode(const char *name, int flags)
     if (!(flags & TARG_NOHASH))
 	Hash_SetValue(he, gn);
     Var_Append(".ALLTARGETS", name, VAR_GLOBAL);
-    (void)Lst_AtEnd(allTargets, gn);
+    Lst_Append(allTargets, gn);
     if (doing_depend)
 	gn->flags |= FROM_DEPEND;
     return gn;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_FindList --
- *	Make a complete list of GNodes from the given list of names
+/* Make a complete list of GNodes from the given list of names.
+ * If flags is TARG_CREATE, nodes will be created for all names in
+ * names which do not yet have graph nodes. If flags is TARG_NOCREATE,
+ * an error message will be printed for each name which can't be found.
  *
  * Input:
  *	name		list of names to find
@@ -371,12 +306,6 @@ Targ_FindNode(const char *name, int flags)
  * Results:
  *	A complete list of graph nodes corresponding to all instances of all
  *	the names in names.
- *
- * Side Effects:
- *	If flags is TARG_CREATE, nodes will be created for all names in
- *	names which do not yet have graph nodes. If flags is TARG_NOCREATE,
- *	an error message will be printed for each name which can't be found.
- * -----------------------------------------------------------------------
  */
 Lst
 Targ_FindList(Lst names, int flags)
@@ -386,21 +315,19 @@ Targ_FindList(Lst names, int flags)
     GNode	   *gn;		/* node in tLn */
     char    	   *name;
 
-    nodes = Lst_Init(FALSE);
+    nodes = Lst_Init();
 
-    if (Lst_Open(names) == FAILURE) {
-	return nodes;
-    }
+    Lst_Open(names);
     while ((ln = Lst_Next(names)) != NULL) {
-	name = (char *)Lst_Datum(ln);
+	name = LstNode_Datum(ln);
 	gn = Targ_FindNode(name, flags);
 	if (gn != NULL) {
 	    /*
-	     * Note: Lst_AtEnd must come before the Lst_Concat so the nodes
+	     * Note: Lst_Append must come before the Lst_Concat so the nodes
 	     * are added to the list in the order in which they were
 	     * encountered in the makefile.
 	     */
-	    (void)Lst_AtEnd(nodes, gn);
+	    Lst_Append(nodes, gn);
 	} else if (flags == TARG_NOCREATE) {
 	    Error("\"%s\" -- target unknown.", name);
 	}
@@ -409,100 +336,33 @@ Targ_FindList(Lst names, int flags)
     return nodes;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_Ignore  --
- *	Return true if should ignore errors when creating gn
- *
- * Input:
- *	gn		node to check for
- *
- * Results:
- *	TRUE if should ignore errors
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
- */
+/* Return true if should ignore errors when creating gn. */
 Boolean
 Targ_Ignore(GNode *gn)
 {
-    if (ignoreErrors || gn->type & OP_IGNORE) {
-	return TRUE;
-    } else {
-	return FALSE;
-    }
+    return ignoreErrors || gn->type & OP_IGNORE;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_Silent  --
- *	Return true if be silent when creating gn
- *
- * Input:
- *	gn		node to check for
- *
- * Results:
- *	TRUE if should be silent
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
- */
+/* Return true if be silent when creating gn. */
 Boolean
 Targ_Silent(GNode *gn)
 {
-    if (beSilent || gn->type & OP_SILENT) {
-	return TRUE;
-    } else {
-	return FALSE;
-    }
+    return beSilent || gn->type & OP_SILENT;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_Precious --
- *	See if the given target is precious
- *
- * Input:
- *	gn		the node to check
- *
- * Results:
- *	TRUE if it is precious. FALSE otherwise
- *
- * Side Effects:
- *	None
- *-----------------------------------------------------------------------
- */
+/* See if the given target is precious. */
 Boolean
 Targ_Precious(GNode *gn)
 {
-    if (allPrecious || (gn->type & (OP_PRECIOUS|OP_DOUBLEDEP))) {
-	return TRUE;
-    } else {
-	return FALSE;
-    }
+    return allPrecious || gn->type & (OP_PRECIOUS | OP_DOUBLEDEP);
 }
 
 /******************* DEBUG INFO PRINTING ****************/
 
 static GNode	  *mainTarg;	/* the main target, as set by Targ_SetMain */
-/*-
- *-----------------------------------------------------------------------
- * Targ_SetMain --
- *	Set our idea of the main target we'll be creating. Used for
- *	debugging output.
- *
- * Input:
- *	gn		The main target we'll create
- *
- * Results:
- *	None.
- *
- * Side Effects:
- *	"mainTarg" is set to the main target's node.
- *-----------------------------------------------------------------------
- */
+
+/* Set our idea of the main target we'll be creating. Used for debugging
+ * output. */
 void
 Targ_SetMain(GNode *gn)
 {
@@ -527,20 +387,8 @@ Targ_PrintCmd(void *cmd, void *dummy MAKE_ATTR_UNUSED)
     return 0;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_FmtTime --
- *	Format a modification time in some reasonable way and return it.
- *
- * Results:
- *	The time reformatted.
- *
- * Side Effects:
- *	The time is placed in a static area, so it is overwritten
- *	with each call.
- *
- *-----------------------------------------------------------------------
- */
+/* Format a modification time in some reasonable way and return it.
+ * The time is placed in a static area, so it is overwritten with each call. */
 char *
 Targ_FmtTime(time_t tm)
 {
@@ -552,18 +400,7 @@ Targ_FmtTime(time_t tm)
     return buf;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_PrintType --
- *	Print out a type field giving only those attributes the user can
- *	set.
- *
- * Results:
- *
- * Side Effects:
- *
- *-----------------------------------------------------------------------
- */
+/* Print out a type field giving only those attributes the user can set. */
 void
 Targ_PrintType(int type)
 {
@@ -600,7 +437,7 @@ Targ_PrintType(int type)
 }
 
 static const char *
-made_name(enum enum_made made)
+made_name(GNodeMade made)
 {
     switch (made) {
     case UNMADE:     return "unmade";
@@ -615,20 +452,15 @@ made_name(enum enum_made made)
     }
 }
 
-/*-
- *-----------------------------------------------------------------------
- * TargPrintNode --
- *	print the contents of a node
- *-----------------------------------------------------------------------
- */
+/* Print the contents of a node. */
 int
 Targ_PrintNode(void *gnp, void *passp)
 {
     GNode         *gn = (GNode *)gnp;
     int	    	  pass = passp ? *(int *)passp : 0;
 
-    fprintf(debug_file, "# %s%s, flags %x, type %x, made %d\n",
-	    gn->name, gn->cohort_num, gn->flags, gn->type, gn->made);
+    fprintf(debug_file, "# %s%s", gn->name, gn->cohort_num);
+    GNode_FprintDetails(debug_file, ", ", gn, "\n");
     if (gn->flags == 0)
 	return 0;
 
@@ -655,26 +487,26 @@ Targ_PrintNode(void *gnp, void *passp)
 		    fprintf(debug_file, "# unmade\n");
 		}
 	    }
-	    if (!Lst_IsEmpty (gn->iParents)) {
+	    if (!Lst_IsEmpty(gn->implicitParents)) {
 		fprintf(debug_file, "# implicit parents: ");
-		Lst_ForEach(gn->iParents, TargPrintName, NULL);
+		Lst_ForEach(gn->implicitParents, TargPrintName, NULL);
 		fprintf(debug_file, "\n");
 	    }
 	} else {
 	    if (gn->unmade)
 		fprintf(debug_file, "# %d unmade children\n", gn->unmade);
 	}
-	if (!Lst_IsEmpty (gn->parents)) {
+	if (!Lst_IsEmpty(gn->parents)) {
 	    fprintf(debug_file, "# parents: ");
 	    Lst_ForEach(gn->parents, TargPrintName, NULL);
 	    fprintf(debug_file, "\n");
 	}
-	if (!Lst_IsEmpty (gn->order_pred)) {
+	if (!Lst_IsEmpty(gn->order_pred)) {
 	    fprintf(debug_file, "# order_pred: ");
 	    Lst_ForEach(gn->order_pred, TargPrintName, NULL);
 	    fprintf(debug_file, "\n");
 	}
-	if (!Lst_IsEmpty (gn->order_succ)) {
+	if (!Lst_IsEmpty(gn->order_succ)) {
 	    fprintf(debug_file, "# order_succ: ");
 	    Lst_ForEach(gn->order_succ, TargPrintName, NULL);
 	    fprintf(debug_file, "\n");
@@ -701,19 +533,8 @@ Targ_PrintNode(void *gnp, void *passp)
     return 0;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * TargPrintOnlySrc --
- *	Print only those targets that are just a source.
- *
- * Results:
- *	0.
- *
- * Side Effects:
- *	The name of each file is printed preceded by #\t
- *
- *-----------------------------------------------------------------------
- */
+/* Print only those targets that are just a source.
+ * The name of each file is printed, preceded by #\t. */
 static int
 TargPrintOnlySrc(void *gnp, void *dummy MAKE_ATTR_UNUSED)
 {
@@ -729,21 +550,10 @@ TargPrintOnlySrc(void *gnp, void *dummy MAKE_ATTR_UNUSED)
     return 0;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Targ_PrintGraph --
- *	print the entire graph. heh heh
- *
- * Input:
- *	pass		Which pass this is. 1 => no processing
- *			2 => processing done
- *
- * Results:
- *	none
- *
- * Side Effects:
- *	lots o' output
- *-----------------------------------------------------------------------
+/* Input:
+ *	pass		1 => before processing
+ *			2 => after processing
+ *			3 => after processing, an error occurred
  */
 void
 Targ_PrintGraph(int pass)
@@ -763,84 +573,26 @@ Targ_PrintGraph(int pass)
     Suff_PrintAll();
 }
 
-/*-
- *-----------------------------------------------------------------------
- * TargPropagateNode --
- *	Propagate information from a single node to related nodes if
- *	appropriate.
+/* Propagate some type information to cohort nodes (those from the ::
+ * dependency operator).
  *
- * Input:
- *	gnp		The node that we are processing.
- *
- * Results:
- *	Always returns 0, for the benefit of Lst_ForEach().
- *
- * Side Effects:
- *	Information is propagated from this node to cohort or child
- *	nodes.
- *
- *	If the node was defined with "::", then TargPropagateCohort()
- *	will be called for each cohort node.
- *
- *	If the node has recursive predecessors, then
- *	TargPropagateRecpred() will be called for each recursive
- *	predecessor.
- *-----------------------------------------------------------------------
- */
-static int
-TargPropagateNode(void *gnp, void *junk MAKE_ATTR_UNUSED)
-{
-    GNode	  *gn = (GNode *)gnp;
-
-    if (gn->type & OP_DOUBLEDEP)
-	Lst_ForEach(gn->cohorts, TargPropagateCohort, gnp);
-    return 0;
-}
-
-/*-
- *-----------------------------------------------------------------------
- * TargPropagateCohort --
- *	Propagate some bits in the type mask from a node to
- *	a related cohort node.
- *
- * Input:
- *	cnp		The node that we are processing.
- *	gnp		Another node that has cnp as a cohort.
- *
- * Results:
- *	Always returns 0, for the benefit of Lst_ForEach().
- *
- * Side Effects:
- *	cnp's type bitmask is modified to incorporate some of the
- *	bits from gnp's type bitmask.  (XXX need a better explanation.)
- *-----------------------------------------------------------------------
- */
-static int
-TargPropagateCohort(void *cgnp, void *pgnp)
-{
-    GNode	  *cgn = (GNode *)cgnp;
-    GNode	  *pgn = (GNode *)pgnp;
-
-    cgn->type |= pgn->type & ~OP_OPMASK;
-    return 0;
-}
-
-/*-
- *-----------------------------------------------------------------------
- * Targ_Propagate --
- *	Propagate information between related nodes.  Should be called
- *	after the makefiles are parsed but before any action is taken.
- *
- * Results:
- *	none
- *
- * Side Effects:
- *	Information is propagated between related nodes throughout the
- *	graph.
- *-----------------------------------------------------------------------
- */
+ * Should be called after the makefiles are parsed but before any action is
+ * taken. */
 void
 Targ_Propagate(void)
 {
-    Lst_ForEach(allTargets, TargPropagateNode, NULL);
+    LstNode pn, cn;
+
+    for (pn = Lst_First(allTargets); pn != NULL; pn = LstNode_Next(pn)) {
+	GNode *pgn = LstNode_Datum(pn);
+
+	if (!(pgn->type & OP_DOUBLEDEP))
+	    continue;
+
+	for (cn = Lst_First(pgn->cohorts); cn != NULL; cn = LstNode_Next(cn)) {
+	    GNode *cgn = LstNode_Datum(cn);
+
+	    cgn->type |= pgn->type & ~OP_OPMASK;
+	}
+    }
 }
diff --git a/trace.c b/trace.c
index 3ef210f02a8c..0611318dbf07 100644
--- a/trace.c
+++ b/trace.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: trace.c,v 1.12 2020/07/03 08:13:23 rillig Exp $	*/
+/*	$NetBSD: trace.c,v 1.15 2020/08/03 20:26:09 rillig Exp $	*/
 
 /*-
  * Copyright (c) 2000 The NetBSD Foundation, Inc.
@@ -31,11 +31,11 @@
 
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: trace.c,v 1.12 2020/07/03 08:13:23 rillig Exp $";
+static char rcsid[] = "$NetBSD: trace.c,v 1.15 2020/08/03 20:26:09 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
-__RCSID("$NetBSD: trace.c,v 1.12 2020/07/03 08:13:23 rillig Exp $");
+__RCSID("$NetBSD: trace.c,v 1.15 2020/08/03 20:26:09 rillig Exp $");
 #endif /* not lint */
 #endif
 
@@ -63,7 +63,7 @@ __RCSID("$NetBSD: trace.c,v 1.12 2020/07/03 08:13:23 rillig Exp $");
 
 static FILE *trfile;
 static pid_t trpid;
-char *trwd;
+const char *trwd;
 
 static const char *evname[] = {
 	"BEG",
@@ -77,10 +77,10 @@ static const char *evname[] = {
 void
 Trace_Init(const char *pathname)
 {
-	char *p1;
 	if (pathname != NULL) {
+		char *dontFreeIt;
 		trpid = getpid();
-		trwd = Var_Value(".CURDIR", VAR_GLOBAL, &p1);
+		trwd = Var_Value(".CURDIR", VAR_GLOBAL, &dontFreeIt);
 
 		trfile = fopen(pathname, "a");
 	}
diff --git a/unit-tests/Makefile b/unit-tests/Makefile
index c9cc13d769db..1566b177087a 100644
--- a/unit-tests/Makefile
+++ b/unit-tests/Makefile
@@ -1,6 +1,6 @@
-# $Id: Makefile,v 1.60 2020/07/10 00:48:32 sjg Exp $
+# $Id: Makefile,v 1.92 2020/09/02 18:39:29 sjg Exp $
 #
-# $NetBSD: Makefile,v 1.63 2020/07/09 22:40:14 sjg Exp $
+# $NetBSD: Makefile,v 1.130 2020/09/02 05:33:57 rillig Exp $
 #
 # Unit tests for make(1)
 #
@@ -25,68 +25,364 @@
 # named makefile (*.mk), with its own set of expected results (*.exp),
 # and it should be added to the TESTS list.
 #
-# Any added files must also be added to src/distrib/sets/lists/tests/mi.
-# Makefiles that are not added to TESTS must be ignored in
-# src/tests/usr.bin/make/t_make.sh (example: include-sub).
+# A few *.mk files are helper files for other tests (such as include-sub.mk)
+# and are thus not added to TESTS.  Such files must be ignored in
+# src/tests/usr.bin/make/t_make.sh.
 #
 
 # Each test is in a sub-makefile.
 # Keep the list sorted.
+# Any test that is commented out must be ignored in
+# src/tests/usr.bin/make/t_make.sh as well.
+TESTS+=		# archive	# broken on FreeBSD, enabled in t_make.sh
+TESTS+=		archive-suffix
+TESTS+=		cmd-interrupt
+TESTS+=		cmdline
 TESTS+=		comment
+TESTS+=		cond-cmp-numeric
+TESTS+=		cond-cmp-numeric-eq
+TESTS+=		cond-cmp-numeric-ge
+TESTS+=		cond-cmp-numeric-gt
+TESTS+=		cond-cmp-numeric-le
+TESTS+=		cond-cmp-numeric-lt
+TESTS+=		cond-cmp-numeric-ne
+TESTS+=		cond-cmp-string
+TESTS+=		cond-func
+TESTS+=		cond-func-commands
+TESTS+=		cond-func-defined
+TESTS+=		cond-func-empty
+TESTS+=		cond-func-exists
+TESTS+=		cond-func-make
+TESTS+=		cond-func-target
 TESTS+=		cond-late
+TESTS+=		cond-op
+TESTS+=		cond-op-and
+TESTS+=		cond-op-not
+TESTS+=		cond-op-or
+TESTS+=		cond-op-parentheses
 TESTS+=		cond-short
+TESTS+=		cond-token-number
+TESTS+=		cond-token-plain
+TESTS+=		cond-token-string
+TESTS+=		cond-token-var
 TESTS+=		cond1
 TESTS+=		cond2
+TESTS+=		counter
+TESTS+=		dep
+TESTS+=		dep-colon
+TESTS+=		dep-double-colon
+TESTS+=		dep-exclam
+TESTS+=		dep-none
+TESTS+=		dep-var
+TESTS+=		dep-wildcards
+TESTS+=		depsrc
+TESTS+=		depsrc-exec
+TESTS+=		depsrc-ignore
+TESTS+=		depsrc-made
+TESTS+=		depsrc-make
+TESTS+=		depsrc-meta
+TESTS+=		depsrc-nometa
+TESTS+=		depsrc-nometa_cmp
+TESTS+=		depsrc-nopath
+TESTS+=		depsrc-notmain
+TESTS+=		depsrc-optional
+TESTS+=		depsrc-phony
+TESTS+=		depsrc-precious
+TESTS+=		depsrc-recursive
+TESTS+=		depsrc-silent
+TESTS+=		depsrc-use
+TESTS+=		depsrc-usebefore
+TESTS+=		depsrc-usebefore-double-colon
+TESTS+=		depsrc-wait
+TESTS+=		deptgt
+TESTS+=		deptgt-begin
+TESTS+=		deptgt-default
+TESTS+=		deptgt-delete_on_error
+TESTS+=		deptgt-end
+TESTS+=		deptgt-error
+TESTS+=		deptgt-ignore
+TESTS+=		deptgt-interrupt
+TESTS+=		deptgt-main
+TESTS+=		deptgt-makeflags
+TESTS+=		deptgt-no_parallel
+TESTS+=		deptgt-nopath
+TESTS+=		deptgt-notparallel
+TESTS+=		deptgt-objdir
+TESTS+=		deptgt-order
+TESTS+=		deptgt-path
+TESTS+=		deptgt-path-suffix
+TESTS+=		deptgt-phony
+TESTS+=		deptgt-precious
+TESTS+=		deptgt-shell
+TESTS+=		deptgt-silent
+TESTS+=		deptgt-stale
+TESTS+=		deptgt-suffixes
+TESTS+=		dir
+TESTS+=		dir-expand-path
+TESTS+=		directive
+TESTS+=		directive-elif
+TESTS+=		directive-elifdef
+TESTS+=		directive-elifmake
+TESTS+=		directive-elifndef
+TESTS+=		directive-elifnmake
+TESTS+=		directive-else
+TESTS+=		directive-endif
+TESTS+=		directive-error
+TESTS+=		directive-export
+TESTS+=		directive-export-env
+TESTS+=		directive-export-literal
+TESTS+=		directive-for
+TESTS+=		directive-for-generating-endif
+TESTS+=		directive-if
+TESTS+=		directive-ifdef
+TESTS+=		directive-ifmake
+TESTS+=		directive-ifndef
+TESTS+=		directive-ifnmake
+TESTS+=		directive-info
+TESTS+=		directive-undef
+TESTS+=		directive-unexport
+TESTS+=		directive-unexport-env
+TESTS+=		directive-warning
+TESTS+=		directives
 TESTS+=		dollar
 TESTS+=		doterror
 TESTS+=		dotwait
+TESTS+=		envfirst
 TESTS+=		error
 TESTS+=		# escape	# broken by reverting POSIX changes
 TESTS+=		export
 TESTS+=		export-all
 TESTS+=		export-env
+TESTS+=		export-variants
 TESTS+=		forloop
 TESTS+=		forsubst
 TESTS+=		hash
-TESTS+=		# impsrc	# broken by reverting POSIX changes
+TESTS+=		impsrc
 TESTS+=		include-main
+TESTS+=		lint
+TESTS+=		make-exported
 TESTS+=		misc
 TESTS+=		moderrs
 TESTS+=		modmatch
 TESTS+=		modmisc
-TESTS+=		modorder
 TESTS+=		modts
 TESTS+=		modword
+TESTS+=		opt
+TESTS+=		opt-backwards
+TESTS+=		opt-chdir
+TESTS+=		opt-debug
+TESTS+=		opt-debug-g1
+TESTS+=		opt-define
+TESTS+=		opt-env
+TESTS+=		opt-file
+TESTS+=		opt-ignore
+TESTS+=		opt-include-dir
+TESTS+=		opt-jobs
+TESTS+=		opt-jobs-internal
+TESTS+=		opt-keep-going
+TESTS+=		opt-m-include-dir
+TESTS+=		opt-no-action
+TESTS+=		opt-no-action-at-all
+TESTS+=		opt-query
+TESTS+=		opt-raw
+TESTS+=		opt-silent
+TESTS+=		opt-touch
+TESTS+=		opt-tracefile
+TESTS+=		opt-var-expanded
+TESTS+=		opt-var-literal
+TESTS+=		opt-warnings-as-errors
+TESTS+=		opt-where-am-i
+TESTS+=		opt-x-reduce-exported
 TESTS+=		order
-TESTS+=		# phony-end	# broken by reverting POSIX changes
+TESTS+=		phony-end
 TESTS+=		posix
 TESTS+=		# posix1	# broken by reverting POSIX changes
 TESTS+=		qequals
-TESTS+=		# suffixes	# broken by reverting POSIX changes
+TESTS+=		recursive
+TESTS+=		sh
+TESTS+=		sh-dots
+TESTS+=		sh-jobs
+TESTS+=		sh-jobs-error
+TESTS+=		sh-leading-at
+TESTS+=		sh-leading-hyphen
+TESTS+=		sh-leading-plus
+TESTS+=		sh-meta-chars
+TESTS+=		sh-multi-line
+TESTS+=		sh-single-line
+TESTS+=		# suffixes	# runs into an endless loop (try -dA)
 TESTS+=		sunshcmd
 TESTS+=		sysv
 TESTS+=		ternary
 TESTS+=		unexport
 TESTS+=		unexport-env
+TESTS+=		use-inference
+TESTS+=		var-class
+TESTS+=		var-class-cmdline
+TESTS+=		var-class-env
+TESTS+=		var-class-global
+TESTS+=		var-class-local
+TESTS+=		var-class-local-legacy
+TESTS+=		var-op
+TESTS+=		var-op-append
+TESTS+=		var-op-assign
+TESTS+=		var-op-default
+TESTS+=		var-op-expand
+TESTS+=		var-op-shell
 TESTS+=		varcmd
+TESTS+=		vardebug
+TESTS+=		varfind
 TESTS+=		varmisc
+TESTS+=		varmod
+TESTS+=		varmod-assign
+TESTS+=		varmod-defined
 TESTS+=		varmod-edge
+TESTS+=		varmod-exclam-shell
+TESTS+=		varmod-extension
+TESTS+=		varmod-gmtime
+TESTS+=		varmod-hash
+TESTS+=		varmod-head
+TESTS+=		varmod-ifelse
+TESTS+=		varmod-l-name-to-value
+TESTS+=		varmod-localtime
+TESTS+=		varmod-loop
+TESTS+=		varmod-match
+TESTS+=		varmod-match-escape
+TESTS+=		varmod-no-match
+TESTS+=		varmod-order
+TESTS+=		varmod-order-reverse
+TESTS+=		varmod-order-shuffle
+TESTS+=		varmod-path
+TESTS+=		varmod-quote
+TESTS+=		varmod-quote-dollar
+TESTS+=		varmod-range
+TESTS+=		varmod-remember
+TESTS+=		varmod-root
+TESTS+=		varmod-select-words
+TESTS+=		varmod-shell
+TESTS+=		varmod-subst
+TESTS+=		varmod-subst-regex
+TESTS+=		varmod-sysv
+TESTS+=		varmod-tail
+TESTS+=		varmod-to-abs
+TESTS+=		varmod-to-lower
+TESTS+=		varmod-to-many-words
+TESTS+=		varmod-to-one-word
+TESTS+=		varmod-to-separator
+TESTS+=		varmod-to-upper
+TESTS+=		varmod-undefined
+TESTS+=		varmod-unique
+TESTS+=		varname
+TESTS+=		varname-dollar
+TESTS+=		varname-dot-alltargets
+TESTS+=		varname-dot-curdir
+TESTS+=		varname-dot-includes
+TESTS+=		varname-dot-includedfromdir
+TESTS+=		varname-dot-includedfromfile
+TESTS+=		varname-dot-libs
+TESTS+=		varname-dot-make-dependfile
+TESTS+=		varname-dot-make-expand_variables
+TESTS+=		varname-dot-make-exported
+TESTS+=		varname-dot-make-jobs
+TESTS+=		varname-dot-make-jobs-prefix
+TESTS+=		varname-dot-make-level
+TESTS+=		varname-dot-make-makefile_preference
+TESTS+=		varname-dot-make-makefiles
+TESTS+=		varname-dot-make-meta-bailiwick
+TESTS+=		varname-dot-make-meta-created
+TESTS+=		varname-dot-make-meta-files
+TESTS+=		varname-dot-make-meta-ignore_filter
+TESTS+=		varname-dot-make-meta-ignore_paths
+TESTS+=		varname-dot-make-meta-ignore_patterns
+TESTS+=		varname-dot-make-meta-prefix
+TESTS+=		varname-dot-make-mode
+TESTS+=		varname-dot-make-path_filemon
+TESTS+=		varname-dot-make-pid
+TESTS+=		varname-dot-make-ppid
+TESTS+=		varname-dot-make-save_dollars
+TESTS+=		varname-dot-makeoverrides
+TESTS+=		varname-dot-newline
+TESTS+=		varname-dot-objdir
+TESTS+=		varname-dot-parsedir
+TESTS+=		varname-dot-parsefile
+TESTS+=		varname-dot-path
+TESTS+=		varname-dot-shell
+TESTS+=		varname-dot-targets
+TESTS+=		varname-empty
+TESTS+=		varname-make
+TESTS+=		varname-make_print_var_on_error
+TESTS+=		varname-makeflags
+TESTS+=		varname-pwd
+TESTS+=		varname-vpath
+TESTS+=		varparse-dynamic
 TESTS+=		varquote
 TESTS+=		varshell
 
-# Override make flags for certain tests; default is -k.
+# Additional environment variables for some of the tests.
+# The base environment is -i PATH="$PATH".
+ENV.envfirst=		FROM_ENV=value-from-env
+ENV.varmisc=		FROM_ENV=env
+ENV.varmisc+=		FROM_ENV_BEFORE=env
+ENV.varmisc+=		FROM_ENV_AFTER=env
+
+# Override make flags for some of the tests; default is -k.
+# If possible, write ".MAKEFLAGS: -dv" in the test .mk file instead of
+# settings FLAGS.test=-dv here, since that is closer to the test code.
+FLAGS.archive=		-dA
+FLAGS.counter=		-dv
+FLAGS.directive-ifmake=	first second
 FLAGS.doterror=		# none
+FLAGS.envfirst=		-e
+FLAGS.export=		# none
+FLAGS.lint=		-dL -k
+FLAGS.opt-debug-g1=	-dg1
+FLAGS.opt-ignore=	-i
+FLAGS.opt-keep-going=	-k
+FLAGS.opt-no-action=	-n
+FLAGS.opt-query=	-q
+FLAGS.opt-var-expanded=	-v VAR -v VALUE
+FLAGS.opt-var-literal=	-V VAR -V VALUE
+FLAGS.opt-warnings-as-errors= -W
 FLAGS.order=		-j1
+FLAGS.recursive=	-dL
+FLAGS.sh-leading-plus=	-n
+FLAGS.vardebug=		-k -dv FROM_CMDLINE=
+FLAGS.varmod-match-escape= -dv
+FLAGS.varname-dot-shell= -dpv
+FLAGS.varname-empty=	-dv '$${:U}=cmdline-u' '=cmline-plain'
 
 # Some tests need extra post-processing.
-SED_CMDS.modmisc+=	-e 's,\(substitution error:\).*,\1 (details omitted),'
-SED_CMDS.varshell+=	-e 's,^[a-z]*sh: ,,'
+SED_CMDS.opt-debug-g1=	-e 's,${.CURDIR},CURDIR,'
+SED_CMDS.opt-debug-g1+=	-e '/Global Variables:/,/Suffixes:/d'
+SED_CMDS.sh-dots=	-e 's,^.*\.\.\.:.*,<normalized: ...: not found>,'
+SED_CMDS.varmod-subst-regex+= \
+			-e 's,\(Regex compilation error:\).*,\1 (details omitted),'
+SED_CMDS.varmod-edge+=	-e 's, line [0-9]*:, line omitted:,'
+SED_CMDS.varshell+=	-e 's,^${.SHELL:T}: ,,'
 SED_CMDS.varshell+=	-e '/command/s,No such.*,not found,'
+SED_CMDS.varname-dot-shell=	-e 's, = /.*, = (details omitted),'
+SED_CMDS.varname-dot-shell+=	-e 's,"/[^"]*","(details omitted)",'
+SED_CMDS.varname-dot-shell+=	-e 's,\[/[^]]*\],[(details omitted)],'
+
+# Some tests need an additional round of postprocessing.
+POSTPROC.counter=	${TOOL_SED} -n -e '/:RELEVANT = yes/,/:RELEVANT = no/p'
+POSTPROC.deptgt-suffixes= \
+			${TOOL_SED} -n -e '/^\#\*\*\* Suffixes/,/^\#\*/p'
+POSTPROC.vardebug=	${TOOL_SED} -n -e '/:RELEVANT = yes/,/:RELEVANT = no/p'
+POSTPROC.varmod-match-escape= ${TOOL_SED} -n -e '/^Pattern/p'
+POSTPROC.varname-dot-shell= \
+			awk '/\.SHELL/ || /^ParseReadLine/'
+POSTPROC.varname-empty=	${TOOL_SED} -n -e '/^Var_Set/p' -e '/^out:/p'
+
+# Some tests reuse other tests, which makes them unnecessarily fragile.
+export-all.rawout: export.mk
+unexport.rawout: export.mk
+unexport-env.rawout: export.mk
 
 # End of the configuration section.
 
 .MAIN: all
 
+.-include "Makefile.inc"
 .-include "Makefile.config"
 
 UNIT_TESTS:=	${.PARSEDIR}
@@ -96,16 +392,14 @@ OUTFILES=	${TESTS:=.out}
 
 all: ${OUTFILES}
 
-CLEANFILES+=		*.rawout *.out *.status *.tmp *.core *.tmp
+CLEANFILES=		*.rawout *.out *.status *.tmp *.core *.tmp
 CLEANFILES+=		obj*.[och] lib*.a	# posix1.mk
 CLEANFILES+=		issue* .[ab]*		# suffixes.mk
-CLEANRECURSIVE+=	dir dummy		# posix1.mk
+CLEANDIRS=		dir dummy		# posix1.mk
 
 clean:
 	rm -f ${CLEANFILES}
-.if !empty(CLEANRECURSIVE)
-	rm -rf ${CLEANRECURSIVE}
-.endif
+	rm -rf ${CLEANDIRS}
 
 TEST_MAKE?=	${.MAKE}
 TOOL_SED?=	sed
@@ -120,13 +414,24 @@ LANG=		C
 .export LANG LC_ALL
 .endif
 
+.if ${.MAKE.MODE:Unormal:Mmeta} != ""
+# we don't need the noise
+_MKMSG_TEST= :
+.endif
+
 # the tests are actually done with sub-makes.
 .SUFFIXES: .mk .rawout .out
 .mk.rawout:
-	@echo ${TEST_MAKE} ${FLAGS.${.TARGET:R}:U-k} -f ${.IMPSRC}
-	-@cd ${.OBJDIR} && \
-	{ ${TEST_MAKE} ${FLAGS.${.TARGET:R}:U-k} -f ${.IMPSRC} \
-	  2>&1 ; echo $$? >${.TARGET:R}.status ; } > ${.TARGET}.tmp
+	@${_MKMSG_TEST:Uecho '#      test '} ${.PREFIX}
+	@set -eu; \
+	cd ${.OBJDIR}; \
+	env -i PATH="$$PATH" ${ENV.${.TARGET:R}} \
+	  ${TEST_MAKE} \
+	    -r -C ${.CURDIR} -f ${.IMPSRC} \
+	    ${FLAGS.${.TARGET:R}:U-k} \
+	    > ${.TARGET}.tmp 2>&1 \
+	&& status=$$? || status=$$?; \
+	echo $$status > ${.TARGET:R}.status
 	@mv ${.TARGET}.tmp ${.TARGET}
 
 # Post-process the test output so that the results can be compared.
@@ -141,11 +446,12 @@ _SED_CMDS+=	-e 's,${.CURDIR:S,.,\\.,g}/,,g'
 _SED_CMDS+=	-e 's,${UNIT_TESTS:S,.,\\.,g}/,,g'
 
 .rawout.out:
-	@echo postprocess ${.TARGET}
 	@${TOOL_SED} ${_SED_CMDS} ${SED_CMDS.${.TARGET:R}} \
-	  < ${.IMPSRC} > ${.TARGET}.tmp
-	@echo "exit status `cat ${.TARGET:R}.status`" >> ${.TARGET}.tmp
-	@mv ${.TARGET}.tmp ${.TARGET}
+	  < ${.IMPSRC} > ${.TARGET}.tmp1
+	@${POSTPROC.${.TARGET:R}:Ucat} < ${.TARGET}.tmp1 > ${.TARGET}.tmp2
+	@rm ${.TARGET}.tmp1
+	@echo "exit status `cat ${.TARGET:R}.status`" >> ${.TARGET}.tmp2
+	@mv ${.TARGET}.tmp2 ${.TARGET}
 
 # Compare all output files
 test:	${OUTFILES} .PHONY
@@ -168,7 +474,7 @@ accept:
 	done
 
 .if exists(${TEST_MAKE})
-${TESTS:=.rawout}: ${TEST_MAKE}
+${TESTS:=.rawout}: ${TEST_MAKE} ${.PARSEDIR}/Makefile
 .endif
 
 .-include <obj.mk>
diff --git a/unit-tests/archive-suffix.exp b/unit-tests/archive-suffix.exp
new file mode 100755
index 000000000000..05f2ac6624c4
--- /dev/null
+++ b/unit-tests/archive-suffix.exp
@@ -0,0 +1,2 @@
+`all' is up to date.
+exit status 0
diff --git a/unit-tests/archive-suffix.mk b/unit-tests/archive-suffix.mk
new file mode 100755
index 000000000000..9f7fa219c667
--- /dev/null
+++ b/unit-tests/archive-suffix.mk
@@ -0,0 +1,23 @@
+# $NetBSD: archive-suffix.mk,v 1.1 2020/08/29 14:47:26 rillig Exp $
+#
+# Between 2020-08-23 and 2020-08-30, the below code produced an assertion
+# failure in Var_Set_with_flags, triggered by Compat_Make, when setting the
+# .IMPSRC of an archive node to its .TARGET.
+#
+# The code assumed that the .TARGET variable of every node would be set, but
+# but that is not guaranteed.
+#
+# Between 2016-03-15 and 2016-03-16 the behavior of the below code changed.
+# Until 2016-03-15, it remade the target, starting with 2016-03-16 it says
+# "`all' is up to date".
+
+.SUFFIXES:
+.SUFFIXES: .c .o
+
+all:	lib.a(obj1.o)
+
+.c.o:
+	: making $@
+
+obj1.c:
+	: $@
diff --git a/unit-tests/archive.exp b/unit-tests/archive.exp
new file mode 100644
index 000000000000..a42b4f39e173
--- /dev/null
+++ b/unit-tests/archive.exp
@@ -0,0 +1,13 @@
+rm -f libprog.a
+ar cru libprog.a archive.mk modmisc.mk varmisc.mk
+ranlib libprog.a
+ar t libprog.a
+archive.mk
+modmisc.mk
+varmisc.mk
+list-archive-wildcard: archive.mk
+list-archive-wildcard: ternary.mk
+depend-on-existing-member
+`depend-on-nonexistent-member' is up to date.
+rm -f libprog.a
+exit status 0
diff --git a/unit-tests/archive.mk b/unit-tests/archive.mk
new file mode 100644
index 000000000000..c3b7e919eab2
--- /dev/null
+++ b/unit-tests/archive.mk
@@ -0,0 +1,45 @@
+# $NetBSD: archive.mk,v 1.5 2020/08/23 17:51:24 rillig Exp $
+#
+# Very basic demonstration of handling archives, based on the description
+# in PSD.doc/tutorial.ms.
+
+ARCHIVE=	libprog.${EXT.a}
+FILES=		archive.${EXT.mk} modmisc.${EXT.mk} varmisc.mk
+
+EXT.a=		a
+EXT.mk=		mk
+
+MAKE_CMD=	${.MAKE} -f ${MAKEFILE}
+RUN?=		@set -eu;
+
+all:
+	${RUN} ${MAKE_CMD} remove-archive
+	${RUN} ${MAKE_CMD} create-archive
+	${RUN} ${MAKE_CMD} list-archive
+	${RUN} ${MAKE_CMD} list-archive-wildcard
+	${RUN} ${MAKE_CMD} depend-on-existing-member
+	${RUN} ${MAKE_CMD} depend-on-nonexistent-member
+	${RUN} ${MAKE_CMD} remove-archive
+
+create-archive: ${ARCHIVE}
+${ARCHIVE}: ${ARCHIVE}(${FILES})
+	ar cru ${.TARGET} ${.OODATE}
+	ranlib ${.TARGET}
+
+list-archive: ${ARCHIVE}
+	ar t ${.ALLSRC}
+
+# XXX: I had expected that this dependency would select all *.mk files from
+# the archive.  Instead, the globbing is done in the current directory.
+# To prevent an overly long file list, the pattern is restricted to [at]*.mk.
+list-archive-wildcard: ${ARCHIVE}([at]*.mk)
+	${RUN} printf '%s\n' ${.ALLSRC:O:@member@${.TARGET:Q}': '${member:Q}@}
+
+depend-on-existing-member: ${ARCHIVE}(archive.mk)
+	${RUN} echo $@
+
+depend-on-nonexistent-member: ${ARCHIVE}(nonexistent.mk)
+	${RUN} echo $@
+
+remove-archive:
+	rm -f ${ARCHIVE}
diff --git a/unit-tests/cmd-interrupt.exp b/unit-tests/cmd-interrupt.exp
new file mode 100755
index 000000000000..91f4439e7bea
--- /dev/null
+++ b/unit-tests/cmd-interrupt.exp
@@ -0,0 +1,9 @@
+> cmd-interrupt-ordinary
+make: *** cmd-interrupt-ordinary removed
+interrupt-ordinary: ok
+> cmd-interrupt-phony
+make: *** cmd-interrupt-phony removed
+interrupt-phony: ok
+> cmd-interrupt-precious
+interrupt-precious: ok
+exit status 0
diff --git a/unit-tests/cmd-interrupt.mk b/unit-tests/cmd-interrupt.mk
new file mode 100755
index 000000000000..033f3307bd2e
--- /dev/null
+++ b/unit-tests/cmd-interrupt.mk
@@ -0,0 +1,50 @@
+# $NetBSD: cmd-interrupt.mk,v 1.2 2020/08/28 18:16:22 rillig Exp $
+#
+# Tests for interrupting a command.
+#
+# If a command is interrupted (usually by the user, here by itself), the
+# target is removed.  This is to avoid having an unfinished target that
+# would be newer than all of its sources and would therefore not be
+# tried again in the next run.
+#
+# This happens for ordinary targets as well as for .PHONY targets, even
+# though the .PHONY targets usually do not correspond to a file.
+#
+# To protect the target from being removed, the target has to be marked with
+# the special source .PRECIOUS.  These targets need to ensure for themselves
+# that interrupting them does not leave an inconsistent state behind.
+#
+# See also:
+#	CompatDeleteTarget
+
+all: clean-before interrupt-ordinary interrupt-phony interrupt-precious clean-after
+
+clean-before clean-after: .PHONY
+	@rm -f cmd-interrupt-ordinary cmd-interrupt-phony cmd-interrupt-precious
+
+interrupt-ordinary: .PHONY
+	@${.MAKE} ${MAKEFLAGS} -f ${MAKEFILE} cmd-interrupt-ordinary || true
+	# The ././ is necessary to work around the file cache.
+	@echo ${.TARGET}: ${exists(././cmd-interrupt-ordinary) :? error : ok }
+
+interrupt-phony: .PHONY
+	@${.MAKE} ${MAKEFLAGS} -f ${MAKEFILE} cmd-interrupt-phony || true
+	# The ././ is necessary to work around the file cache.
+	@echo ${.TARGET}: ${exists(././cmd-interrupt-phony) :? error : ok }
+
+interrupt-precious: .PRECIOUS
+	@${.MAKE} ${MAKEFLAGS} -f ${MAKEFILE} cmd-interrupt-precious || true
+	# The ././ is necessary to work around the file cache.
+	@echo ${.TARGET}: ${exists(././cmd-interrupt-precious) :? ok : error }
+
+cmd-interrupt-ordinary:
+	> ${.TARGET}
+	@kill -INT ${.MAKE.PID}
+
+cmd-interrupt-phony: .PHONY
+	> ${.TARGET}
+	@kill -INT ${.MAKE.PID}
+
+cmd-interrupt-precious: .PRECIOUS
+	> ${.TARGET}
+	@kill -INT ${.MAKE.PID}
diff --git a/unit-tests/cmdline.exp b/unit-tests/cmdline.exp
new file mode 100644
index 000000000000..8e981ba5248c
--- /dev/null
+++ b/unit-tests/cmdline.exp
@@ -0,0 +1,5 @@
+makeobjdir-direct:
+show-objdir: /tmp/6a8899d2-d227-4b55-9b6b-f3c8eeb83fd5
+makeobjdir-indirect:
+show-objdir: /tmp/a7b41170-53f8-4cc2-bc5c-e4c3dd93ec45/
+exit status 0
diff --git a/unit-tests/cmdline.mk b/unit-tests/cmdline.mk
new file mode 100644
index 000000000000..c12c31220cb5
--- /dev/null
+++ b/unit-tests/cmdline.mk
@@ -0,0 +1,37 @@
+# $NetBSD: cmdline.mk,v 1.1 2020/07/28 22:44:44 rillig Exp $
+#
+# Tests for command line parsing and related special variables.
+
+RUN?=		@set -eu;
+TMPBASE?=	/tmp
+SUB1=		a7b41170-53f8-4cc2-bc5c-e4c3dd93ec45	# just a random UUID
+SUB2=		6a8899d2-d227-4b55-9b6b-f3c8eeb83fd5	# just a random UUID
+MAKE_CMD=	env TMPBASE=${TMPBASE}/${SUB1} ${.MAKE} -f ${MAKEFILE} -r
+DIR2=		${TMPBASE}/${SUB2}
+DIR12=		${TMPBASE}/${SUB1}/${SUB2}
+
+all: prepare-dirs
+all: makeobjdir-direct makeobjdir-indirect
+
+prepare-dirs:
+	${RUN} rm -rf ${DIR2} ${DIR12}
+	${RUN} mkdir -p ${DIR2} ${DIR12}
+
+# The .OBJDIR can be set via the MAKEOBJDIR command line variable.
+# It must be a command line variable; an environment variable would not work.
+makeobjdir-direct:
+	@echo $@:
+	${RUN} ${MAKE_CMD} MAKEOBJDIR=${DIR2} show-objdir
+
+# The .OBJDIR can be set via the MAKEOBJDIR command line variable,
+# and that variable could even contain the usual modifiers.
+# Since the .OBJDIR=MAKEOBJDIR assignment happens very early,
+# the SUB2 variable in the modifier is not defined yet and is therefore empty.
+# The SUB1 in the resulting path comes from the environment variable TMPBASE,
+# see MAKE_CMD.
+makeobjdir-indirect:
+	@echo $@:
+	${RUN} ${MAKE_CMD} MAKEOBJDIR='$${TMPBASE}/$${SUB2}' show-objdir
+
+show-objdir:
+	@echo $@: ${.OBJDIR:Q}
diff --git a/unit-tests/cond-cmp-numeric-eq.exp b/unit-tests/cond-cmp-numeric-eq.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-eq.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-cmp-numeric-eq.mk b/unit-tests/cond-cmp-numeric-eq.mk
new file mode 100755
index 000000000000..02b95ae593cb
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-eq.mk
@@ -0,0 +1,53 @@
+# $NetBSD: cond-cmp-numeric-eq.mk,v 1.1 2020/08/23 13:50:17 rillig Exp $
+#
+# Tests for numeric comparisons with the == operator in .if conditions.
+
+# This comparison yields the same result, whether numeric or character-based.
+.if 1 == 1
+.else
+.error
+.endif
+
+# This comparison yields the same result, whether numeric or character-based.
+.if 1 == 2
+.error
+.endif
+
+.if 2 == 1
+.error
+.endif
+
+# Scientific notation is supported, as per strtod.
+.if 2e7 == 2000e4
+.else
+.error
+.endif
+
+.if 2000e4 == 2e7
+.else
+.error
+.endif
+
+# Trailing zeroes after the decimal point are irrelevant for the numeric
+# value.
+.if 3.30000 == 3.3
+.else
+.error
+.endif
+
+.if 3.3 == 3.30000
+.else
+.error
+.endif
+
+# As of 2020-08-23, numeric comparison is implemented as parsing both sides
+# as double, and then performing a normal comparison.  The range of double is
+# typically 16 or 17 significant digits, therefore these two numbers seem to
+# be equal.
+.if 1.000000000000000001 == 1.000000000000000002
+.else
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-cmp-numeric-ge.exp b/unit-tests/cond-cmp-numeric-ge.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-ge.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-cmp-numeric-ge.mk b/unit-tests/cond-cmp-numeric-ge.mk
new file mode 100755
index 000000000000..510b80e14942
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-ge.mk
@@ -0,0 +1,75 @@
+# $NetBSD: cond-cmp-numeric-ge.mk,v 1.1 2020/08/23 13:50:17 rillig Exp $
+#
+# Tests for numeric comparisons with the >= operator in .if conditions.
+
+# When both sides are equal, the >= operator always yields true.
+.if 1 >= 1
+.else
+.error
+.endif
+
+# This comparison yields the same result, whether numeric or character-based.
+.if 1 >= 2
+.error
+.endif
+
+.if 2 >= 1
+.else
+.error
+.endif
+
+# If this comparison were character-based instead of numerical, the
+# 5 would be >= 14 since its first digit is greater.
+.if 5 >= 14
+.error
+.endif
+
+.if 14 >= 5
+.else
+.error
+.endif
+
+# Scientific notation is supported, as per strtod.
+.if 2e7 >= 1e8
+.error
+.endif
+
+.if 1e8 >= 2e7
+.else
+.error
+.endif
+
+# Floating pointer numbers can be compared as well.
+# This might be tempting to use for version numbers, but there are a few pitfalls.
+.if 3.141 >= 111.222
+.error
+.endif
+
+.if 111.222 >= 3.141
+.else
+.error
+.endif
+
+# When parsed as a version number, 3.30 is greater than 3.7.
+# Since make parses numbers as plain numbers, that leads to wrong results.
+# Numeric comparisons are not suited for comparing version number.
+.if 3.30 >= 3.7
+.error
+.endif
+
+.if 3.7 >= 3.30
+.else
+.error
+.endif
+
+# As of 2020-08-23, numeric comparison is implemented as parsing both sides
+# as double, and then performing a normal comparison.  The range of double is
+# typically 16 or 17 significant digits, therefore these two numbers seem to
+# be equal.
+.if 1.000000000000000001 >= 1.000000000000000002
+.else
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-cmp-numeric-gt.exp b/unit-tests/cond-cmp-numeric-gt.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-gt.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-cmp-numeric-gt.mk b/unit-tests/cond-cmp-numeric-gt.mk
new file mode 100755
index 000000000000..24ac1eb8531b
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-gt.mk
@@ -0,0 +1,73 @@
+# $NetBSD: cond-cmp-numeric-gt.mk,v 1.1 2020/08/23 13:50:17 rillig Exp $
+#
+# Tests for numeric comparisons with the > operator in .if conditions.
+
+# When both sides are equal, the > operator always yields false.
+.if 1 > 1
+.error
+.endif
+
+# This comparison yields the same result, whether numeric or character-based.
+.if 1 > 2
+.error
+.endif
+
+.if 2 > 1
+.else
+.error
+.endif
+
+# If this comparison were character-based instead of numerical, the
+# 5 would be > 14 since its first digit is greater.
+.if 5 > 14
+.error
+.endif
+
+.if 14 > 5
+.else
+.error
+.endif
+
+# Scientific notation is supported, as per strtod.
+.if 2e7 > 1e8
+.error
+.endif
+
+.if 1e8 > 2e7
+.else
+.error
+.endif
+
+# Floating pointer numbers can be compared as well.
+# This might be tempting to use for version numbers, but there are a few pitfalls.
+.if 3.141 > 111.222
+.error
+.endif
+
+.if 111.222 > 3.141
+.else
+.error
+.endif
+
+# When parsed as a version number, 3.30 is greater than 3.7.
+# Since make parses numbers as plain numbers, that leads to wrong results.
+# Numeric comparisons are not suited for comparing version number.
+.if 3.30 > 3.7
+.error
+.endif
+
+.if 3.7 > 3.30
+.else
+.error
+.endif
+
+# As of 2020-08-23, numeric comparison is implemented as parsing both sides
+# as double, and then performing a normal comparison.  The range of double is
+# typically 16 or 17 significant digits, therefore these two numbers seem to
+# be equal.
+.if 1.000000000000000001 > 1.000000000000000002
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-cmp-numeric-le.exp b/unit-tests/cond-cmp-numeric-le.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-le.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-cmp-numeric-le.mk b/unit-tests/cond-cmp-numeric-le.mk
new file mode 100755
index 000000000000..2e4f5e9e694b
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-le.mk
@@ -0,0 +1,75 @@
+# $NetBSD: cond-cmp-numeric-le.mk,v 1.1 2020/08/23 13:50:17 rillig Exp $
+#
+# Tests for numeric comparisons with the <= operator in .if conditions.
+
+# When both sides are equal, the <= operator always yields true.
+.if 1 <= 1
+.else
+.error
+.endif
+
+# This comparison yields the same result, whether numeric or character-based.
+.if 1 <= 2
+.else
+.error
+.endif
+
+.if 2 <= 1
+.error
+.endif
+
+# If this comparison were character-based instead of numerical, the
+# 5 would be >= 14 since its first digit is greater.
+.if 5 <= 14
+.else
+.error
+.endif
+
+.if 14 <= 5
+.error
+.endif
+
+# Scientific notation is supported, as per strtod.
+.if 2e7 <= 1e8
+.else
+.error
+.endif
+
+.if 1e8 <= 2e7
+.error
+.endif
+
+# Floating pointer numbers can be compared as well.
+# This might be tempting to use for version numbers, but there are a few pitfalls.
+.if 3.141 <= 111.222
+.else
+.error
+.endif
+
+.if 111.222 <= 3.141
+.error
+.endif
+
+# When parsed as a version number, 3.30 is greater than 3.7.
+# Since make parses numbers as plain numbers, that leads to wrong results.
+# Numeric comparisons are not suited for comparing version number.
+.if 3.30 <= 3.7
+.else
+.error
+.endif
+
+.if 3.7 <= 3.30
+.error
+.endif
+
+# As of 2020-08-23, numeric comparison is implemented as parsing both sides
+# as double, and then performing a normal comparison.  The range of double is
+# typically 16 or 17 significant digits, therefore these two numbers seem to
+# be equal.
+.if 1.000000000000000001 <= 1.000000000000000002
+.else
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-cmp-numeric-lt.exp b/unit-tests/cond-cmp-numeric-lt.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-lt.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-cmp-numeric-lt.mk b/unit-tests/cond-cmp-numeric-lt.mk
new file mode 100755
index 000000000000..a5fcceddff4b
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-lt.mk
@@ -0,0 +1,73 @@
+# $NetBSD: cond-cmp-numeric-lt.mk,v 1.1 2020/08/23 13:50:17 rillig Exp $
+#
+# Tests for numeric comparisons with the < operator in .if conditions.
+
+# When both sides are equal, the < operator always yields false.
+.if 1 < 1
+.error
+.endif
+
+# This comparison yields the same result, whether numeric or character-based.
+.if 1 < 2
+.else
+.error
+.endif
+
+.if 2 < 1
+.error
+.endif
+
+# If this comparison were character-based instead of numerical, the
+# 5 would be > 14 since its first digit is greater.
+.if 5 < 14
+.else
+.error
+.endif
+
+.if 14 < 5
+.error
+.endif
+
+# Scientific notation is supported, as per strtod.
+.if 2e7 < 1e8
+.else
+.error
+.endif
+
+.if 1e8 < 2e7
+.error
+.endif
+
+# Floating pointer numbers can be compared as well.
+# This might be tempting to use for version numbers, but there are a few pitfalls.
+.if 3.141 < 111.222
+.else
+.error
+.endif
+
+.if 111.222 < 3.141
+.error
+.endif
+
+# When parsed as a version number, 3.30 is greater than 3.7.
+# Since make parses numbers as plain numbers, that leads to wrong results.
+# Numeric comparisons are not suited for comparing version number.
+.if 3.30 < 3.7
+.else
+.error
+.endif
+
+.if 3.7 < 3.30
+.error
+.endif
+
+# As of 2020-08-23, numeric comparison is implemented as parsing both sides
+# as double, and then performing a normal comparison.  The range of double is
+# typically 16 or 17 significant digits, therefore these two numbers seem to
+# be equal.
+.if 1.000000000000000001 < 1.000000000000000002
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-cmp-numeric-ne.exp b/unit-tests/cond-cmp-numeric-ne.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-ne.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-cmp-numeric-ne.mk b/unit-tests/cond-cmp-numeric-ne.mk
new file mode 100755
index 000000000000..6f858584d139
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric-ne.mk
@@ -0,0 +1,49 @@
+# $NetBSD: cond-cmp-numeric-ne.mk,v 1.1 2020/08/23 13:50:17 rillig Exp $
+#
+# Tests for numeric comparisons with the != operator in .if conditions.
+
+# When both sides are equal, the != operator always yields false.
+.if 1 != 1
+.error
+.endif
+
+# This comparison yields the same result, whether numeric or character-based.
+.if 1 != 2
+.else
+.error
+.endif
+
+.if 2 != 1
+.else
+.error
+.endif
+
+# Scientific notation is supported, as per strtod.
+.if 2e7 != 2000e4
+.error
+.endif
+
+.if 2000e4 != 2e7
+.error
+.endif
+
+# Trailing zeroes after the decimal point are irrelevant for the numeric
+# value.
+.if 3.30000 != 3.3
+.error
+.endif
+
+.if 3.3 != 3.30000
+.error
+.endif
+
+# As of 2020-08-23, numeric comparison is implemented as parsing both sides
+# as double, and then performing a normal comparison.  The range of double is
+# typically 16 or 17 significant digits, therefore these two numbers seem to
+# be equal.
+.if 1.000000000000000001 != 1.000000000000000002
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-cmp-numeric.exp b/unit-tests/cond-cmp-numeric.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-cmp-numeric.mk b/unit-tests/cond-cmp-numeric.mk
new file mode 100644
index 000000000000..409636c3c3ca
--- /dev/null
+++ b/unit-tests/cond-cmp-numeric.mk
@@ -0,0 +1,8 @@
+# $NetBSD: cond-cmp-numeric.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for numeric comparisons in .if conditions.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/cond-cmp-string.exp b/unit-tests/cond-cmp-string.exp
new file mode 100644
index 000000000000..03dcb0416898
--- /dev/null
+++ b/unit-tests/cond-cmp-string.exp
@@ -0,0 +1,5 @@
+make: "cond-cmp-string.mk" line 18: Malformed conditional (str != str)
+make: "cond-cmp-string.mk" line 37: Malformed conditional ("string" != "str""ing")
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/cond-cmp-string.mk b/unit-tests/cond-cmp-string.mk
new file mode 100644
index 000000000000..67d86b61e88d
--- /dev/null
+++ b/unit-tests/cond-cmp-string.mk
@@ -0,0 +1,39 @@
+# $NetBSD: cond-cmp-string.mk,v 1.3 2020/08/20 18:43:19 rillig Exp $
+#
+# Tests for string comparisons in .if conditions.
+
+# This is a simple comparison of string literals.
+# Nothing surprising here.
+.if "str" != "str"
+.error
+.endif
+
+# The right-hand side of the comparison may be written without quotes.
+.if "str" != str
+.error
+.endif
+
+# The left-hand side of the comparison must be enclosed in quotes.
+# This one is not enclosed in quotes and thus generates an error message.
+.if str != str
+.error
+.endif
+
+# The left-hand side of the comparison requires a defined variable.
+# The variable named "" is not defined, but applying the :U modifier to it
+# makes it "kind of defined" (see VAR_KEEP).  Therefore it is ok here.
+.if ${:Ustr} != "str"
+.error
+.endif
+
+# Any character in a string literal may be escaped using a backslash.
+# This means that "\n" does not mean a newline but a simple "n".
+.if "string" != "\s\t\r\i\n\g"
+.error
+.endif
+
+# It is not possible to concatenate two string literals to form a single
+# string.
+.if "string" != "str""ing"
+.error
+.endif
diff --git a/unit-tests/cond-func-commands.exp b/unit-tests/cond-func-commands.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-func-commands.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-func-commands.mk b/unit-tests/cond-func-commands.mk
new file mode 100644
index 000000000000..4a098169048c
--- /dev/null
+++ b/unit-tests/cond-func-commands.mk
@@ -0,0 +1,36 @@
+# $NetBSD: cond-func-commands.mk,v 1.3 2020/08/23 14:07:20 rillig Exp $
+#
+# Tests for the commands() function in .if conditions.
+
+.MAIN: all
+
+# The target "target" does not exist yet, therefore it cannot have commands.
+.if commands(target)
+.error
+.endif
+
+target:
+
+# Now the target exists, but it still has no commands.
+.if commands(target)
+.error
+.endif
+
+target:
+	# not a command
+
+# Even after the comment, the target still has no commands.
+.if commands(target)
+.error
+.endif
+
+target:
+	@:;
+
+# Finally the target has commands.
+.if !commands(target)
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-func-defined.exp b/unit-tests/cond-func-defined.exp
new file mode 100644
index 000000000000..70c6342a02c3
--- /dev/null
+++ b/unit-tests/cond-func-defined.exp
@@ -0,0 +1,5 @@
+make: "cond-func-defined.mk" line 23: warning: Missing closing parenthesis for defined()
+make: "cond-func-defined.mk" line 23: Malformed conditional (!defined(A B))
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/cond-func-defined.mk b/unit-tests/cond-func-defined.mk
new file mode 100644
index 000000000000..dce1399183aa
--- /dev/null
+++ b/unit-tests/cond-func-defined.mk
@@ -0,0 +1,33 @@
+# $NetBSD: cond-func-defined.mk,v 1.3 2020/08/20 17:23:43 rillig Exp $
+#
+# Tests for the defined() function in .if conditions.
+
+DEF=		defined
+${:UA B}=	variable name with spaces
+
+.if !defined(DEF)
+.error
+.endif
+
+# Horizontal whitespace after the opening parenthesis is ignored.
+.if !defined( 	DEF)
+.error
+.endif
+
+# Horizontal whitespace before the closing parenthesis is ignored.
+.if !defined(DEF 	)
+.error
+.endif
+
+# The argument of a function must not directly contain whitespace.
+.if !defined(A B)
+.error
+.endif
+
+# If necessary, the whitespace can be generated by a variable expression.
+.if !defined(${:UA B})
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-func-empty.exp b/unit-tests/cond-func-empty.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-func-empty.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-func-empty.mk b/unit-tests/cond-func-empty.mk
new file mode 100644
index 000000000000..737403f94525
--- /dev/null
+++ b/unit-tests/cond-func-empty.mk
@@ -0,0 +1,8 @@
+# $NetBSD: cond-func-empty.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the empty() function in .if conditions.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/cond-func-exists.exp b/unit-tests/cond-func-exists.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-func-exists.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-func-exists.mk b/unit-tests/cond-func-exists.mk
new file mode 100644
index 000000000000..6386f21fdc7b
--- /dev/null
+++ b/unit-tests/cond-func-exists.mk
@@ -0,0 +1,42 @@
+# $NetBSD: cond-func-exists.mk,v 1.4 2020/08/28 12:59:36 rillig Exp $
+#
+# Tests for the exists() function in .if conditions.
+
+.if !exists(.)
+.error
+.endif
+
+# The argument to the function must not be enclosed in quotes.
+# Neither double quotes nor single quotes are allowed.
+.if exists(".")
+.error
+.endif
+
+.if exists('.')
+.error
+.endif
+
+# The only way to escape characters that would otherwise influence the parser
+# is to enclose them in a variable expression.  For function arguments,
+# neither the backslash nor the dollar sign act as escape character.
+.if exists(\.)
+.error
+.endif
+
+.if !exists(${:U.})
+.error
+.endif
+
+# The argument to the function can have several variable expressions.
+# See cond-func.mk for the characters that cannot be used directly.
+.if !exists(${.PARSEDIR}/${.PARSEFILE})
+.error
+.endif
+
+# Whitespace is trimmed on both sides of the function argument.
+.if !exists(	.	)
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-func-make.exp b/unit-tests/cond-func-make.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-func-make.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-func-make.mk b/unit-tests/cond-func-make.mk
new file mode 100644
index 000000000000..baa0d37da726
--- /dev/null
+++ b/unit-tests/cond-func-make.mk
@@ -0,0 +1,8 @@
+# $NetBSD: cond-func-make.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the make() function in .if conditions.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/cond-func-target.exp b/unit-tests/cond-func-target.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-func-target.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-func-target.mk b/unit-tests/cond-func-target.mk
new file mode 100644
index 000000000000..c36bcfe0a5f8
--- /dev/null
+++ b/unit-tests/cond-func-target.mk
@@ -0,0 +1,38 @@
+# $NetBSD: cond-func-target.mk,v 1.3 2020/08/23 14:07:20 rillig Exp $
+#
+# Tests for the target() function in .if conditions.
+
+.MAIN: all
+
+# The target "target" does not exist yet.
+.if target(target)
+.error
+.endif
+
+target:
+
+# The target exists, even though it does not have any commands.
+.if !target(target)
+.error
+.endif
+
+target:
+	# not a command
+
+# Adding a comment to an existing target does not change whether the target
+# is defined or not.
+.if !target(target)
+.error
+.endif
+
+target:
+	@:;
+
+# Adding a command to an existing target does not change whether the target
+# is defined or not.
+.if !target(target)
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-func.exp b/unit-tests/cond-func.exp
new file mode 100644
index 000000000000..0069ed75726c
--- /dev/null
+++ b/unit-tests/cond-func.exp
@@ -0,0 +1,9 @@
+make: "cond-func.mk" line 29: warning: Missing closing parenthesis for defined()
+make: "cond-func.mk" line 29: Malformed conditional (!defined(A B))
+make: "cond-func.mk" line 44: warning: Missing closing parenthesis for defined()
+make: "cond-func.mk" line 44: Malformed conditional (!defined(A&B))
+make: "cond-func.mk" line 47: warning: Missing closing parenthesis for defined()
+make: "cond-func.mk" line 47: Malformed conditional (!defined(A|B))
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/cond-func.mk b/unit-tests/cond-func.mk
new file mode 100644
index 000000000000..304735241e7f
--- /dev/null
+++ b/unit-tests/cond-func.mk
@@ -0,0 +1,63 @@
+# $NetBSD: cond-func.mk,v 1.1 2020/08/20 17:45:47 rillig Exp $
+#
+# Tests for those parts of the functions in .if conditions that are common
+# among several functions.
+#
+# The below test uses the function defined(...) since it has no side-effects,
+# the other functions (except empty(...)) would work equally well.
+
+DEF=			defined
+${:UA B}=		variable name with spaces
+${:UVAR(value)}=	variable name with parentheses
+${:UVAR{value}}=	variable name with braces
+
+.if !defined(DEF)
+.error
+.endif
+
+# Horizontal whitespace after the opening parenthesis is ignored.
+.if !defined( 	DEF)
+.error
+.endif
+
+# Horizontal whitespace before the closing parenthesis is ignored.
+.if !defined(DEF 	)
+.error
+.endif
+
+# The argument of a function must not directly contain whitespace.
+.if !defined(A B)
+.error
+.endif
+
+# If necessary, the whitespace can be generated by a variable expression.
+.if !defined(${:UA B})
+.error
+.endif
+
+# Characters that could be mistaken for operators must not appear directly
+# in a function argument.  As with whitespace, these can be generated
+# indirectly.
+#
+# It's not entirely clear why these characters are forbidden.
+# The most plausible reason seems to be typo detection.
+.if !defined(A&B)
+.error
+.endif
+.if !defined(A|B)
+.error
+.endif
+
+# Even parentheses may appear in variable names.
+# They must be balanced though.
+.if !defined(VAR(value))
+.error
+.endif
+
+# Braces do not have any special meaning when parsing arguments.
+.if !defined(VAR{value})
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-late.exp b/unit-tests/cond-late.exp
index 3717b088c646..46c4aa2f4230 100644
--- a/unit-tests/cond-late.exp
+++ b/unit-tests/cond-late.exp
@@ -1,3 +1,4 @@
+make: Bad conditional expression ` != "no"' in  != "no"?:
 yes
 no
 exit status 0
diff --git a/unit-tests/cond-late.mk b/unit-tests/cond-late.mk
index 2eb02b42b920..397f5febd480 100644
--- a/unit-tests/cond-late.mk
+++ b/unit-tests/cond-late.mk
@@ -1,4 +1,4 @@
-# $NetBSD: cond-late.mk,v 1.1 2020/04/29 23:15:21 rillig Exp $
+# $NetBSD: cond-late.mk,v 1.2 2020/07/25 20:37:46 rillig Exp $
 #
 # Using the :? modifier, variable expressions can contain conditional
 # expressions that are evaluated late.  Any variables appearing in these
@@ -15,9 +15,15 @@
 # and then expand the variables, the output would change from the
 # current "yes no" to "yes yes", since both variables are non-empty.
 
+all: cond-literal
+
 COND.true=	"yes" == "yes"
 COND.false=	"yes" != "yes"
 
-all:
+cond-literal:
 	@echo ${ ${COND.true} :?yes:no}
 	@echo ${ ${COND.false} :?yes:no}
+
+VAR+=	${${UNDEF} != "no":?:}
+.if empty(VAR:Mpattern)
+.endif
diff --git a/unit-tests/cond-op-and.exp b/unit-tests/cond-op-and.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-op-and.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-op-and.mk b/unit-tests/cond-op-and.mk
new file mode 100644
index 000000000000..a204a227c4f0
--- /dev/null
+++ b/unit-tests/cond-op-and.mk
@@ -0,0 +1,27 @@
+# $NetBSD: cond-op-and.mk,v 1.3 2020/08/28 14:48:37 rillig Exp $
+#
+# Tests for the && operator in .if conditions.
+
+.if 0 && 0
+.error
+.endif
+
+.if 1 && 0
+.error
+.endif
+
+.if 0 && 1
+.error
+.endif
+
+.if !(1 && 1)
+.error
+.endif
+
+# The right-hand side is not evaluated since the left-hand side is already
+# false.
+.if 0 && ${UNDEF}
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-op-not.exp b/unit-tests/cond-op-not.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-op-not.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-op-not.mk b/unit-tests/cond-op-not.mk
new file mode 100644
index 000000000000..ad0a0939eecf
--- /dev/null
+++ b/unit-tests/cond-op-not.mk
@@ -0,0 +1,22 @@
+# $NetBSD: cond-op-not.mk,v 1.3 2020/08/28 14:48:37 rillig Exp $
+#
+# Tests for the ! operator in .if conditions.
+
+# The exclamation mark negates its operand.
+.if !1
+.error
+.endif
+
+# Exclamation marks can be chained.
+# This doesn't happen in practice though.
+.if !!!1
+.error
+.endif
+
+# The ! binds more tightly than the &&.
+.if !!0 && 1
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-op-or.exp b/unit-tests/cond-op-or.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-op-or.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-op-or.mk b/unit-tests/cond-op-or.mk
new file mode 100644
index 000000000000..696b9dd23062
--- /dev/null
+++ b/unit-tests/cond-op-or.mk
@@ -0,0 +1,27 @@
+# $NetBSD: cond-op-or.mk,v 1.3 2020/08/28 14:48:37 rillig Exp $
+#
+# Tests for the || operator in .if conditions.
+
+.if 0 || 0
+.error
+.endif
+
+.if !(1 || 0)
+.error
+.endif
+
+.if !(0 || 1)
+.error
+.endif
+
+.if !(1 || 1)
+.error
+.endif
+
+# The right-hand side is not evaluated since the left-hand side is already
+# true.
+.if 1 || ${UNDEF}
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/cond-op-parentheses.exp b/unit-tests/cond-op-parentheses.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-op-parentheses.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-op-parentheses.mk b/unit-tests/cond-op-parentheses.mk
new file mode 100644
index 000000000000..6c48d83dd2be
--- /dev/null
+++ b/unit-tests/cond-op-parentheses.mk
@@ -0,0 +1,8 @@
+# $NetBSD: cond-op-parentheses.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for parentheses in .if conditions.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/cond-op.exp b/unit-tests/cond-op.exp
new file mode 100644
index 000000000000..4caec4df659b
--- /dev/null
+++ b/unit-tests/cond-op.exp
@@ -0,0 +1,5 @@
+make: "cond-op.mk" line 45: Malformed conditional ("!word" == !word)
+make: "cond-op.mk" line 57: Parsing continues until here.
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/cond-op.mk b/unit-tests/cond-op.mk
new file mode 100644
index 000000000000..a22e4181e8fe
--- /dev/null
+++ b/unit-tests/cond-op.mk
@@ -0,0 +1,60 @@
+# $NetBSD: cond-op.mk,v 1.4 2020/08/28 14:07:51 rillig Exp $
+#
+# Tests for operators like &&, ||, ! in .if conditions.
+#
+# See also:
+#	cond-op-and.mk
+#	cond-op-not.mk
+#	cond-op-or.mk
+#	cond-op-parentheses.mk
+
+# In make, && binds more tightly than ||, like in C.
+# If make had the same precedence for both && and ||, the result would be
+# different.
+# If || were to bind more tightly than &&, the result would be different
+# as well.
+.if !(1 || 1 && 0)
+.error
+.endif
+
+# If make were to interpret the && and || operators like the shell, the
+# implicit binding would be this:
+.if (1 || 1) && 0
+.error
+.endif
+
+# The precedence of the ! operator is different from C though. It has a
+# lower precedence than the comparison operators.
+.if !"word" == "word"
+.error
+.endif
+
+# This is how the above condition is actually interpreted.
+.if !("word" == "word")
+.error
+.endif
+
+# TODO: Demonstrate that the precedence of the ! and == operators actually
+# makes a difference.  There is a simple example for sure, I just cannot
+# wrap my head around it.
+
+# This condition is malformed because the '!' on the right-hand side must not
+# appear unquoted.  If any, it must be enclosed in quotes.
+# In any case, it is not interpreted as a negation of an unquoted string.
+# See CondGetString.
+.if "!word" == !word
+.error
+.endif
+
+# Surprisingly, the ampersand and pipe are allowed in bare strings.
+# That's another opportunity for writing confusing code.
+# See CondGetString, which only has '!' in the list of stop characters.
+.if "a&&b||c" != a&&b||c
+.error
+.endif
+
+# Just in case that parsing should ever stop on the first error.
+.info Parsing continues until here.
+
+all:
+	@:;
diff --git a/unit-tests/cond-short.mk b/unit-tests/cond-short.mk
index 41d76768a404..ae441d80f7d9 100644
--- a/unit-tests/cond-short.mk
+++ b/unit-tests/cond-short.mk
@@ -1,4 +1,4 @@
-# $NetBSD: cond-short.mk,v 1.7 2020/07/09 22:34:09 sjg Exp $
+# $NetBSD: cond-short.mk,v 1.9 2020/08/19 22:47:09 rillig Exp $
 #
 # Demonstrates that in conditions, the right-hand side of an && or ||
 # is only evaluated if it can actually influence the result.
@@ -66,6 +66,19 @@ VAR=	# empty again, for the following tests
 .if 0 && !empty(:U${:!echo "unexpected exclam modifier" 1>&2 !})
 .endif
 
+# Irrelevant assignment modifiers are skipped as well.
+.if 0 && ${1 2 3:L:@i@${FIRST::?=$i}@}
+.endif
+.if 0 && ${1 2 3:L:@i@${LAST::=$i}@}
+.endif
+.if 0 && ${1 2 3:L:@i@${APPENDED::+=$i}@}
+.endif
+.if 0 && ${echo.1 echo.2 echo.3:L:@i@${RAN::!=${i:C,.*,&; & 1>\&2,:S,., ,g}}@}
+.endif
+.if defined(FIRST) || defined(LAST) || defined(APPENDED) || defined(RAN)
+.warning first=${FIRST} last=${LAST} appended=${APPENDED} ran=${RAN}
+.endif
+
 # The || operator.
 
 .if 1 || ${echo "unexpected or" 1>&2 :L:sh}
@@ -110,6 +123,7 @@ x=Ok
 x=Fail
 .endif
 x!= echo 'defined(V42) && ${V42} > 0: $x' >&2; echo
+
 # this one throws both String comparison operator and
 # Malformed conditional with cond.c 1.78
 # indirect iV2 would expand to "" and treated as 0
@@ -119,6 +133,7 @@ x=Fail
 x=Ok
 .endif
 x!= echo 'defined(V66) && ( "${iV2}" < ${V42} ): $x' >&2; echo
+
 # next two thow String comparison operator with cond.c 1.78
 # indirect iV1 would expand to 42
 .if 1 || ${iV1} < ${V42}
@@ -127,12 +142,14 @@ x=Ok
 x=Fail
 .endif
 x!= echo '1 || ${iV1} < ${V42}: $x' >&2; echo
+
 .if 1 || ${iV2:U2} < ${V42}
 x=Ok
 .else
 x=Fail
 .endif
 x!= echo '1 || ${iV2:U2} < ${V42}: $x' >&2; echo
+
 # the same expressions are fine when the lhs is expanded
 # ${iV1} expands to 42
 .if 0 || ${iV1} <= ${V42}
@@ -141,6 +158,7 @@ x=Ok
 x=Fail
 .endif
 x!= echo '0 || ${iV1} <= ${V42}: $x' >&2; echo
+
 # ${iV2:U2} expands to 2
 .if 0 || ${iV2:U2} < ${V42}
 x=Ok
diff --git a/unit-tests/cond-token-number.exp b/unit-tests/cond-token-number.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-token-number.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-token-number.mk b/unit-tests/cond-token-number.mk
new file mode 100644
index 000000000000..30e7e84e81bb
--- /dev/null
+++ b/unit-tests/cond-token-number.mk
@@ -0,0 +1,8 @@
+# $NetBSD: cond-token-number.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for number tokens in .if conditions.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/cond-token-plain.exp b/unit-tests/cond-token-plain.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-token-plain.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-token-plain.mk b/unit-tests/cond-token-plain.mk
new file mode 100644
index 000000000000..fc13245382f1
--- /dev/null
+++ b/unit-tests/cond-token-plain.mk
@@ -0,0 +1,9 @@
+# $NetBSD: cond-token-plain.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for plain tokens (that is, string literals without quotes)
+# in .if conditions.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/cond-token-string.exp b/unit-tests/cond-token-string.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/cond-token-string.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/cond-token-string.mk b/unit-tests/cond-token-string.mk
new file mode 100644
index 000000000000..1a8019754824
--- /dev/null
+++ b/unit-tests/cond-token-string.mk
@@ -0,0 +1,8 @@
+# $NetBSD: cond-token-string.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for quoted and unquoted string literals in .if conditions.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/cond-token-var.exp b/unit-tests/cond-token-var.exp
new file mode 100644
index 000000000000..eb71a43c55f3
--- /dev/null
+++ b/unit-tests/cond-token-var.exp
@@ -0,0 +1,7 @@
+make: "cond-token-var.mk" line 9: ok
+make: "cond-token-var.mk" line 15: Malformed conditional (${UNDEF} == ${DEF})
+make: "cond-token-var.mk" line 20: Malformed conditional (${DEF} == ${UNDEF})
+make: "cond-token-var.mk" line 29: Malformed conditional (${UNDEF})
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/cond-token-var.mk b/unit-tests/cond-token-var.mk
new file mode 100644
index 000000000000..5d5bbf2c7fc4
--- /dev/null
+++ b/unit-tests/cond-token-var.mk
@@ -0,0 +1,34 @@
+# $NetBSD: cond-token-var.mk,v 1.3 2020/08/20 19:43:42 rillig Exp $
+#
+# Tests for variables in .if conditions.
+
+DEF=	defined
+
+# A defined variable may appear on either side of the comparison.
+.if ${DEF} == ${DEF}
+.info ok
+.else
+.error
+.endif
+
+# A variable that appears on the left-hand side must be defined.
+.if ${UNDEF} == ${DEF}
+.error
+.endif
+
+# A variable that appears on the right-hand side must be defined.
+.if ${DEF} == ${UNDEF}
+.error
+.endif
+
+# A defined variable may appear as an expression of its own.
+.if ${DEF}
+.endif
+
+# An undefined variable generates a warning.
+.if ${UNDEF}
+.endif
+
+# The :U modifier turns an undefined variable into an ordinary expression.
+.if ${UNDEF:U}
+.endif
diff --git a/unit-tests/counter.exp b/unit-tests/counter.exp
new file mode 100644
index 000000000000..c00b3e57056d
--- /dev/null
+++ b/unit-tests/counter.exp
@@ -0,0 +1,88 @@
+Global:RELEVANT = yes (load-time part)
+Global:COUNTER = 
+Global:NEXT = ${COUNTER::=${COUNTER} a}${COUNTER:[#]}
+Global:A = 
+Var_Parse: ${NEXT} with VARE_WANTRES|VARE_ASSIGN
+Var_Parse: ${COUNTER::=${COUNTER} a}${COUNTER:[#]} with VARE_WANTRES|VARE_ASSIGN
+Applying ${COUNTER::...} to "" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Var_Parse: ${COUNTER} a}${COUNTER:[#]} with VARE_WANTRES
+Modifier part: " a"
+Global:COUNTER =  a
+Result of ${COUNTER::=${COUNTER} a} is "" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Var_Parse: ${COUNTER} a}${COUNTER:[#]} with VARE_WANTRES|VARE_ASSIGN
+Var_Parse: ${COUNTER:[#]} with VARE_WANTRES|VARE_ASSIGN
+Applying ${COUNTER:[...} to " a" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Modifier part: "#"
+Result of ${COUNTER:[#]} is "1" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Global:A = ${COUNTER::= a a}1
+Global:B = 
+Var_Parse: ${NEXT} with VARE_WANTRES|VARE_ASSIGN
+Var_Parse: ${COUNTER::=${COUNTER} a}${COUNTER:[#]} with VARE_WANTRES|VARE_ASSIGN
+Applying ${COUNTER::...} to " a" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Var_Parse: ${COUNTER} a}${COUNTER:[#]} with VARE_WANTRES
+Modifier part: " a a"
+Global:COUNTER =  a a
+Result of ${COUNTER::=${COUNTER} a} is "" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Var_Parse: ${COUNTER} a}${COUNTER:[#]} with VARE_WANTRES|VARE_ASSIGN
+Var_Parse: ${COUNTER:[#]} with VARE_WANTRES|VARE_ASSIGN
+Applying ${COUNTER:[...} to " a a" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Modifier part: "#"
+Result of ${COUNTER:[#]} is "2" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Global:B = ${COUNTER::= a a a}2
+Global:C = 
+Var_Parse: ${NEXT} with VARE_WANTRES|VARE_ASSIGN
+Var_Parse: ${COUNTER::=${COUNTER} a}${COUNTER:[#]} with VARE_WANTRES|VARE_ASSIGN
+Applying ${COUNTER::...} to " a a" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Var_Parse: ${COUNTER} a}${COUNTER:[#]} with VARE_WANTRES
+Modifier part: " a a a"
+Global:COUNTER =  a a a
+Result of ${COUNTER::=${COUNTER} a} is "" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Var_Parse: ${COUNTER} a}${COUNTER:[#]} with VARE_WANTRES|VARE_ASSIGN
+Var_Parse: ${COUNTER:[#]} with VARE_WANTRES|VARE_ASSIGN
+Applying ${COUNTER:[...} to " a a a" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Modifier part: "#"
+Result of ${COUNTER:[#]} is "3" (eflags = VARE_WANTRES|VARE_ASSIGN, vflags = none)
+Global:C = ${COUNTER::= a a a a}3
+Global:RELEVANT = no
+Global:RELEVANT = yes (run-time part)
+Result of ${RELEVANT::=yes (run-time part)} is "" (eflags = VARE_WANTRES, vflags = none)
+Var_Parse: ${A:Q} B=${B:Q} C=${C:Q} COUNTER=${COUNTER:[#]:Q} with VARE_WANTRES
+Var_Parse: ${COUNTER::= a a}1 with VARE_WANTRES
+Applying ${COUNTER::...} to " a a a" (eflags = VARE_WANTRES, vflags = none)
+Modifier part: " a a"
+Global:COUNTER =  a a
+Result of ${COUNTER::= a a} is "" (eflags = VARE_WANTRES, vflags = none)
+Applying ${A:Q} to "1" (eflags = VARE_WANTRES, vflags = none)
+QuoteMeta: [1]
+Result of ${A:Q} is "1" (eflags = VARE_WANTRES, vflags = none)
+Var_Parse: ${B:Q} C=${C:Q} COUNTER=${COUNTER:[#]:Q} with VARE_WANTRES
+Var_Parse: ${COUNTER::= a a a}2 with VARE_WANTRES
+Applying ${COUNTER::...} to " a a" (eflags = VARE_WANTRES, vflags = none)
+Modifier part: " a a a"
+Global:COUNTER =  a a a
+Result of ${COUNTER::= a a a} is "" (eflags = VARE_WANTRES, vflags = none)
+Applying ${B:Q} to "2" (eflags = VARE_WANTRES, vflags = none)
+QuoteMeta: [2]
+Result of ${B:Q} is "2" (eflags = VARE_WANTRES, vflags = none)
+Var_Parse: ${C:Q} COUNTER=${COUNTER:[#]:Q} with VARE_WANTRES
+Var_Parse: ${COUNTER::= a a a a}3 with VARE_WANTRES
+Applying ${COUNTER::...} to " a a a" (eflags = VARE_WANTRES, vflags = none)
+Modifier part: " a a a a"
+Global:COUNTER =  a a a a
+Result of ${COUNTER::= a a a a} is "" (eflags = VARE_WANTRES, vflags = none)
+Applying ${C:Q} to "3" (eflags = VARE_WANTRES, vflags = none)
+QuoteMeta: [3]
+Result of ${C:Q} is "3" (eflags = VARE_WANTRES, vflags = none)
+Var_Parse: ${COUNTER:[#]:Q} with VARE_WANTRES
+Applying ${COUNTER:[...} to " a a a a" (eflags = VARE_WANTRES, vflags = none)
+Modifier part: "#"
+Result of ${COUNTER:[#]} is "4" (eflags = VARE_WANTRES, vflags = none)
+Applying ${COUNTER:Q} to "4" (eflags = VARE_WANTRES, vflags = none)
+QuoteMeta: [4]
+Result of ${COUNTER:Q} is "4" (eflags = VARE_WANTRES, vflags = none)
+A=1 B=2 C=3 COUNTER=4
+Var_Parse: ${RELEVANT::=no} with VARE_WANTRES
+Applying ${RELEVANT::...} to "yes (run-time part)" (eflags = VARE_WANTRES, vflags = none)
+Modifier part: "no"
+Global:RELEVANT = no
+exit status 0
diff --git a/unit-tests/counter.mk b/unit-tests/counter.mk
new file mode 100644
index 000000000000..23e7dcc0e8d2
--- /dev/null
+++ b/unit-tests/counter.mk
@@ -0,0 +1,31 @@
+# $NetBSD: counter.mk,v 1.1 2020/08/02 14:53:02 rillig Exp $
+#
+# Demonstrates that it is not easily possible to let make count
+# the number of times a variable is actually accessed.
+#
+# As of 2020-08-02, the counter ends up at having 4 words, even
+# though the NEXT variable is only accessed 3 times.  This is
+# surprising.
+#
+# A hint to this surprising behavior is that the variables don't
+# get fully expanded.  For example, A does not simply contain the
+# value "1" but an additional unexpanded ${COUNTER:...} before it.
+
+RELEVANT=	yes (load-time part)	# just to filter the output
+
+COUNTER=	# zero
+
+NEXT=		${COUNTER::=${COUNTER} a}${COUNTER:[#]}
+
+# This variable is first set to empty and then expanded.
+# See parse.c, function Parse_DoVar, keyword "!Var_Exists".
+A:=		${NEXT}
+B:=		${NEXT}
+C:=		${NEXT}
+
+RELEVANT=	no
+
+all:
+	@: ${RELEVANT::=yes (run-time part)}
+	@echo A=${A:Q} B=${B:Q} C=${C:Q} COUNTER=${COUNTER:[#]:Q}
+	@: ${RELEVANT::=no}
diff --git a/unit-tests/dep-colon.exp b/unit-tests/dep-colon.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/dep-colon.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/dep-colon.mk b/unit-tests/dep-colon.mk
new file mode 100644
index 000000000000..7355c59ae557
--- /dev/null
+++ b/unit-tests/dep-colon.mk
@@ -0,0 +1,8 @@
+# $NetBSD: dep-colon.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the : operator in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/dep-double-colon.exp b/unit-tests/dep-double-colon.exp
new file mode 100644
index 000000000000..5528abbab32d
--- /dev/null
+++ b/unit-tests/dep-double-colon.exp
@@ -0,0 +1,5 @@
+command 1a
+command 1b
+command 2a
+command 2b
+exit status 0
diff --git a/unit-tests/dep-double-colon.mk b/unit-tests/dep-double-colon.mk
new file mode 100644
index 000000000000..de4cd9bc9e33
--- /dev/null
+++ b/unit-tests/dep-double-colon.mk
@@ -0,0 +1,11 @@
+# $NetBSD: dep-double-colon.mk,v 1.3 2020/08/22 12:42:32 rillig Exp $
+#
+# Tests for the :: operator in dependency declarations.
+
+all::
+	@echo 'command 1a'
+	@echo 'command 1b'
+
+all::
+	@echo 'command 2a'
+	@echo 'command 2b'
diff --git a/unit-tests/dep-exclam.exp b/unit-tests/dep-exclam.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/dep-exclam.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/dep-exclam.mk b/unit-tests/dep-exclam.mk
new file mode 100644
index 000000000000..2779a66ea6e3
--- /dev/null
+++ b/unit-tests/dep-exclam.mk
@@ -0,0 +1,8 @@
+# $NetBSD: dep-exclam.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the ! operator in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/dep-none.exp b/unit-tests/dep-none.exp
new file mode 100755
index 000000000000..0d299fcca8d3
--- /dev/null
+++ b/unit-tests/dep-none.exp
@@ -0,0 +1,4 @@
+make: no target to make.
+
+make: stopped in unit-tests
+exit status 2
diff --git a/unit-tests/dep-none.mk b/unit-tests/dep-none.mk
new file mode 100755
index 000000000000..74b50aa29051
--- /dev/null
+++ b/unit-tests/dep-none.mk
@@ -0,0 +1,3 @@
+# $NetBSD: dep-none.mk,v 1.1 2020/08/22 12:51:11 rillig Exp $
+#
+# Test for a Makefile that declares no target at all.
diff --git a/unit-tests/dep-var.exp b/unit-tests/dep-var.exp
new file mode 100755
index 000000000000..819a939ed70a
--- /dev/null
+++ b/unit-tests/dep-var.exp
@@ -0,0 +1,2 @@
+def2
+exit status 0
diff --git a/unit-tests/dep-var.mk b/unit-tests/dep-var.mk
new file mode 100755
index 000000000000..b0ce4b2c53ae
--- /dev/null
+++ b/unit-tests/dep-var.mk
@@ -0,0 +1,33 @@
+# $NetBSD: dep-var.mk,v 1.1 2020/08/22 16:51:26 rillig Exp $
+#
+# Tests for variable references in dependency declarations.
+#
+# Uh oh, this feels so strange that probably nobody uses it. But it seems to
+# be the only way to reach the lower half of SuffExpandChildren.
+
+# XXX: The -dv log says:
+#	Var_Parse: ${UNDEF1} with VARE_UNDEFERR|VARE_WANTRES
+# but no error message is generated for this line.
+# The variable expression ${UNDEF1} simply expands to an empty string.
+all: ${UNDEF1}
+
+# Using a double dollar in order to circumvent immediate variable expansion
+# feels like unintended behavior.  At least the manual page says nothing at
+# all about defined or undefined variables in dependency lines.
+#
+# At the point where the expression ${DEF2} is expanded, the variable DEF2
+# is defined, so everything's fine.
+all: $${DEF2}
+
+# This variable is not defined at all.
+# XXX: The -dv log says:
+#	Var_Parse: ${UNDEF3} with VARE_UNDEFERR|VARE_WANTRES
+# but no error message is generated for this line, just like for UNDEF1.
+# The variable expression ${UNDEF3} simply expands to an empty string.
+all: $${UNDEF3}
+
+UNDEF1=	undef1
+DEF2=	def2
+
+undef1 def2:
+	@echo ${.TARGET}
diff --git a/unit-tests/dep-wildcards.exp b/unit-tests/dep-wildcards.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/dep-wildcards.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/dep-wildcards.mk b/unit-tests/dep-wildcards.mk
new file mode 100644
index 000000000000..c557efab3ebc
--- /dev/null
+++ b/unit-tests/dep-wildcards.mk
@@ -0,0 +1,8 @@
+# $NetBSD: dep-wildcards.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for wildcards such as *.c in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/dep.exp b/unit-tests/dep.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/dep.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/dep.mk b/unit-tests/dep.mk
new file mode 100644
index 000000000000..b2463dfc6458
--- /dev/null
+++ b/unit-tests/dep.mk
@@ -0,0 +1,8 @@
+# $NetBSD: dep.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for dependency declarations, such as "target: sources".
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-exec.exp b/unit-tests/depsrc-exec.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-exec.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-exec.mk b/unit-tests/depsrc-exec.mk
new file mode 100644
index 000000000000..a71284d33b3c
--- /dev/null
+++ b/unit-tests/depsrc-exec.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-exec.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .EXEC in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-ignore.exp b/unit-tests/depsrc-ignore.exp
new file mode 100644
index 000000000000..162f10ddc17b
--- /dev/null
+++ b/unit-tests/depsrc-ignore.exp
@@ -0,0 +1,11 @@
+ignore-errors begin
+false ignore-errors
+ignore-errors end
+all begin
+*** Error code 1 (ignored)
+false all
+*** Error code 1 (continuing)
+
+Stop.
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/depsrc-ignore.mk b/unit-tests/depsrc-ignore.mk
new file mode 100644
index 000000000000..1be3eabe8806
--- /dev/null
+++ b/unit-tests/depsrc-ignore.mk
@@ -0,0 +1,67 @@
+# $NetBSD: depsrc-ignore.mk,v 1.4 2020/08/29 16:13:27 rillig Exp $
+#
+# Tests for the special source .IGNORE in dependency declarations,
+# which ignores any command failures for that target.
+#
+# Even though ignore-errors fails, the all target is still made.
+# Since the all target is not marked with .IGNORE, it stops at the
+# first failing command.
+#
+# XXX: The ordering of the messages in the output is confusing.
+# The "ignored" comes much too late to be related to the "false
+# ignore-errors".  This is due to stdout being buffered.
+#
+# The "continuing" message comes from the -k option.  If there had been
+# other targets independent of "all", these would be built as well.
+#
+# Enabling the debugging option -de changes the order in which the messages
+# appear.  Now the "ignored" message is issued in the correct position.
+# The explanation for the output reordering is that the output is buffered.
+# As the manual page says, in debugging mode stdout is line buffered.
+# In these tests the output is redirected to a file, therefore stdout is
+# fully buffered.
+#
+# This is what actually happens, as of 2020-08-29.  To verify it, set the
+# following breakpoints in CompatRunCommand:
+#
+# * the "!silent" line, to see all commands.
+# * the "fflush" line, to see stdout being flushed.
+# * the "status = WEXITSTATUS" line
+# * the "(continuing)" line
+# * the "(ignored)" line
+#
+# The breakpoints are visited in the following order:
+#
+# "ignore-errors begin"
+#	Goes directly to STDOUT_FILENO since it is run in a child process.
+# "false ignore-errors"
+#	Goes to the stdout buffer (CompatRunCommand, keyword "!silent") and
+#	the immediate call to fflush(stdout) copies it to STDOUT_FILENO.
+# "*** Error code 1 (ignored)"
+#	Goes to the stdout buffer but is not flushed (CompatRunCommand, near
+#	the end).
+# "ignore-errors end"
+#	Goes directly to STDOUT_FILENO.
+# "all begin"
+#	Goes directly to STDOUT_FILENO.
+# "false all"
+#	Goes to the stdout buffer, where the "*** Error code 1 (ignored)" is
+#	still waiting to be flushed.  These two lines are flushed now.
+# "*** Error code 1 (continuing)"
+#	Goes to the stdout buffer.
+# "Stop."
+#	Goes to the stdout buffer.
+# exit(1)
+#	Flushes the stdout buffer to STDOUT_FILENO.
+
+all: ignore-errors
+
+ignore-errors: .IGNORE
+	@echo $@ begin
+	false $@
+	@echo $@ end
+
+all:
+	@echo $@ begin
+	false $@
+	@echo $@ end
diff --git a/unit-tests/depsrc-made.exp b/unit-tests/depsrc-made.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-made.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-made.mk b/unit-tests/depsrc-made.mk
new file mode 100644
index 000000000000..9adeb2487496
--- /dev/null
+++ b/unit-tests/depsrc-made.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-made.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .MADE in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-make.exp b/unit-tests/depsrc-make.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-make.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-make.mk b/unit-tests/depsrc-make.mk
new file mode 100644
index 000000000000..9631f3e913d0
--- /dev/null
+++ b/unit-tests/depsrc-make.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-make.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .MAKE in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-meta.exp b/unit-tests/depsrc-meta.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-meta.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-meta.mk b/unit-tests/depsrc-meta.mk
new file mode 100644
index 000000000000..6adef4baa5de
--- /dev/null
+++ b/unit-tests/depsrc-meta.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-meta.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .META in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-nometa.exp b/unit-tests/depsrc-nometa.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-nometa.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-nometa.mk b/unit-tests/depsrc-nometa.mk
new file mode 100644
index 000000000000..1942383856ab
--- /dev/null
+++ b/unit-tests/depsrc-nometa.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-nometa.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .NOMETA in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-nometa_cmp.exp b/unit-tests/depsrc-nometa_cmp.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-nometa_cmp.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-nometa_cmp.mk b/unit-tests/depsrc-nometa_cmp.mk
new file mode 100644
index 000000000000..a0032d2c812a
--- /dev/null
+++ b/unit-tests/depsrc-nometa_cmp.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-nometa_cmp.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .NOMETA_CMP in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-nopath.exp b/unit-tests/depsrc-nopath.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-nopath.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-nopath.mk b/unit-tests/depsrc-nopath.mk
new file mode 100644
index 000000000000..052c6f10db66
--- /dev/null
+++ b/unit-tests/depsrc-nopath.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-nopath.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .NOPATH in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-notmain.exp b/unit-tests/depsrc-notmain.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-notmain.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-notmain.mk b/unit-tests/depsrc-notmain.mk
new file mode 100644
index 000000000000..6e4a68c3b70d
--- /dev/null
+++ b/unit-tests/depsrc-notmain.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-notmain.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .NOTMAIN in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-optional.exp b/unit-tests/depsrc-optional.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-optional.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-optional.mk b/unit-tests/depsrc-optional.mk
new file mode 100644
index 000000000000..074dd0b4ef8f
--- /dev/null
+++ b/unit-tests/depsrc-optional.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-optional.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .OPTIONAL in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-phony.exp b/unit-tests/depsrc-phony.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-phony.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-phony.mk b/unit-tests/depsrc-phony.mk
new file mode 100644
index 000000000000..73186e81a5f9
--- /dev/null
+++ b/unit-tests/depsrc-phony.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-phony.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .PHONY in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-precious.exp b/unit-tests/depsrc-precious.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-precious.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-precious.mk b/unit-tests/depsrc-precious.mk
new file mode 100644
index 000000000000..699b83d767b1
--- /dev/null
+++ b/unit-tests/depsrc-precious.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-precious.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .PRECIOUS in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-recursive.exp b/unit-tests/depsrc-recursive.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-recursive.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-recursive.mk b/unit-tests/depsrc-recursive.mk
new file mode 100644
index 000000000000..5c629338bc8d
--- /dev/null
+++ b/unit-tests/depsrc-recursive.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-recursive.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .RECURSIVE in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc-silent.exp b/unit-tests/depsrc-silent.exp
new file mode 100644
index 000000000000..1ea493702c3b
--- /dev/null
+++ b/unit-tests/depsrc-silent.exp
@@ -0,0 +1,4 @@
+one
+two
+three
+exit status 0
diff --git a/unit-tests/depsrc-silent.mk b/unit-tests/depsrc-silent.mk
new file mode 100644
index 000000000000..98da7b387906
--- /dev/null
+++ b/unit-tests/depsrc-silent.mk
@@ -0,0 +1,12 @@
+# $NetBSD: depsrc-silent.mk,v 1.3 2020/08/29 17:34:21 rillig Exp $
+#
+# Tests for the special source .SILENT in dependency declarations,
+# which hides the commands, no matter whether they are prefixed with
+# '@' or not.
+
+# Without the .SILENT, the commands 'echo one' and 'echo two' would be
+# written to stdout.
+all: .SILENT
+	echo one
+	echo two
+	@echo three
diff --git a/unit-tests/depsrc-use.exp b/unit-tests/depsrc-use.exp
new file mode 100644
index 000000000000..c9810bda462d
--- /dev/null
+++ b/unit-tests/depsrc-use.exp
@@ -0,0 +1,6 @@
+first 1
+first 2
+second 1
+second 2
+directly
+exit status 0
diff --git a/unit-tests/depsrc-use.mk b/unit-tests/depsrc-use.mk
new file mode 100644
index 000000000000..17836cd39e23
--- /dev/null
+++ b/unit-tests/depsrc-use.mk
@@ -0,0 +1,24 @@
+# $NetBSD: depsrc-use.mk,v 1.4 2020/08/22 12:30:57 rillig Exp $
+#
+# Tests for the special source .USE in dependency declarations,
+# which allows to append common commands to other targets.
+
+all: action directly
+
+first: .USE
+	@echo first 1		# Using ${.TARGET} here would expand to "action"
+	@echo first 2
+
+second: .USE
+	@echo second 1
+	@echo second 2
+
+# It's possible but uncommon to have a .USE target with no commands.
+# This may happen as the result of expanding a .for loop.
+empty: .USE
+
+# It's possible but uncommon to directly make a .USEBEFORE target.
+directly: .USE
+	@echo directly
+
+action: first second empty
diff --git a/unit-tests/depsrc-usebefore-double-colon.exp b/unit-tests/depsrc-usebefore-double-colon.exp
new file mode 100755
index 000000000000..8bbdef5ad326
--- /dev/null
+++ b/unit-tests/depsrc-usebefore-double-colon.exp
@@ -0,0 +1,2 @@
+double-colon early 1
+exit status 0
diff --git a/unit-tests/depsrc-usebefore-double-colon.mk b/unit-tests/depsrc-usebefore-double-colon.mk
new file mode 100755
index 000000000000..448214112d32
--- /dev/null
+++ b/unit-tests/depsrc-usebefore-double-colon.mk
@@ -0,0 +1,30 @@
+# $NetBSD: depsrc-usebefore-double-colon.mk,v 1.1 2020/08/22 08:29:13 rillig Exp $
+#
+# Tests for the special source .USEBEFORE in dependency declarations,
+# combined with the double-colon dependency operator.
+
+all: action
+
+# The dependency operator :: allows commands to be added later to the same
+# target.
+double-colon:: .USEBEFORE
+	@echo double-colon early 1
+
+# This command is ignored, which kind of makes sense since this dependency
+# declaration has no .USEBEFORE source.
+double-colon::
+	@echo double-colon early 2
+
+# XXX: This command is ignored even though it has a .USEBEFORE source.
+# This is unexpected.
+double-colon:: .USEBEFORE
+	@echo double-colon early 3
+
+# At this point, the commands from the .USEBEFORE targets are copied to
+# the "action" target.
+action: double-colon
+
+# This command is not added to the "action" target since it comes too late.
+# The commands had been copied in the previous line already.
+double-colon::
+	@echo double-colon late
diff --git a/unit-tests/depsrc-usebefore.exp b/unit-tests/depsrc-usebefore.exp
new file mode 100644
index 000000000000..c9810bda462d
--- /dev/null
+++ b/unit-tests/depsrc-usebefore.exp
@@ -0,0 +1,6 @@
+first 1
+first 2
+second 1
+second 2
+directly
+exit status 0
diff --git a/unit-tests/depsrc-usebefore.mk b/unit-tests/depsrc-usebefore.mk
new file mode 100644
index 000000000000..c6be2bae0a9c
--- /dev/null
+++ b/unit-tests/depsrc-usebefore.mk
@@ -0,0 +1,24 @@
+# $NetBSD: depsrc-usebefore.mk,v 1.5 2020/08/22 11:53:18 rillig Exp $
+#
+# Tests for the special source .USEBEFORE in dependency declarations,
+# which allows to prepend common commands to other targets.
+
+all: action directly
+
+first: .USEBEFORE
+	@echo first 1		# Using ${.TARGET} here would expand to "action"
+	@echo first 2		# Using ${.TARGET} here would expand to "action"
+
+second: .USEBEFORE
+	@echo second 1
+	@echo second 2
+
+# It is possible but uncommon to have a .USEBEFORE target with no commands.
+# This may happen as the result of expanding a .for loop.
+empty: .USEBEFORE
+
+# It is possible but uncommon to directly make a .USEBEFORE target.
+directly: .USEBEFORE
+	@echo directly
+
+action: second first empty
diff --git a/unit-tests/depsrc-wait.exp b/unit-tests/depsrc-wait.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc-wait.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc-wait.mk b/unit-tests/depsrc-wait.mk
new file mode 100644
index 000000000000..bdf3be4608bb
--- /dev/null
+++ b/unit-tests/depsrc-wait.mk
@@ -0,0 +1,8 @@
+# $NetBSD: depsrc-wait.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special source .WAIT in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/depsrc.exp b/unit-tests/depsrc.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/depsrc.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/depsrc.mk b/unit-tests/depsrc.mk
new file mode 100644
index 000000000000..d461e1111d0f
--- /dev/null
+++ b/unit-tests/depsrc.mk
@@ -0,0 +1,9 @@
+# $NetBSD: depsrc.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for special sources (those starting with a dot, followed by
+# uppercase letters) in dependency declarations, such as .PHONY.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-begin.exp b/unit-tests/deptgt-begin.exp
new file mode 100644
index 000000000000..527020f6818d
--- /dev/null
+++ b/unit-tests/deptgt-begin.exp
@@ -0,0 +1,4 @@
+: parse time
+: .BEGIN
+: all
+exit status 0
diff --git a/unit-tests/deptgt-begin.mk b/unit-tests/deptgt-begin.mk
new file mode 100644
index 000000000000..c6ca2f4aa3c7
--- /dev/null
+++ b/unit-tests/deptgt-begin.mk
@@ -0,0 +1,13 @@
+# $NetBSD: deptgt-begin.mk,v 1.3 2020/08/29 17:34:21 rillig Exp $
+#
+# Tests for the special target .BEGIN in dependency declarations,
+# which is a container for commands that are run before any other
+# commands from the shell lines.
+
+.BEGIN:
+	: $@
+
+all:
+	: $@
+
+_!=	echo : parse time 1>&2
diff --git a/unit-tests/deptgt-default.exp b/unit-tests/deptgt-default.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-default.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-default.mk b/unit-tests/deptgt-default.mk
new file mode 100644
index 000000000000..814eaf72aed3
--- /dev/null
+++ b/unit-tests/deptgt-default.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-default.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .DEFAULT in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-delete_on_error.exp b/unit-tests/deptgt-delete_on_error.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-delete_on_error.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-delete_on_error.mk b/unit-tests/deptgt-delete_on_error.mk
new file mode 100644
index 000000000000..476bb8a54c52
--- /dev/null
+++ b/unit-tests/deptgt-delete_on_error.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-delete_on_error.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .DELETE_ON_ERROR in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-end.exp b/unit-tests/deptgt-end.exp
new file mode 100644
index 000000000000..3effe2e3182f
--- /dev/null
+++ b/unit-tests/deptgt-end.exp
@@ -0,0 +1,4 @@
+: .BEGIN
+: all
+: .END
+exit status 0
diff --git a/unit-tests/deptgt-end.mk b/unit-tests/deptgt-end.mk
new file mode 100644
index 000000000000..b3d59a610a2f
--- /dev/null
+++ b/unit-tests/deptgt-end.mk
@@ -0,0 +1,13 @@
+# $NetBSD: deptgt-end.mk,v 1.3 2020/08/29 17:34:21 rillig Exp $
+#
+# Tests for the special target .END in dependency declarations,
+# which is run after making the desired targets.
+
+.BEGIN:
+	: $@
+
+.END:
+	: $@
+
+all:
+	: $@
diff --git a/unit-tests/deptgt-error.exp b/unit-tests/deptgt-error.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-error.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-error.mk b/unit-tests/deptgt-error.mk
new file mode 100644
index 000000000000..07bc1b5d3408
--- /dev/null
+++ b/unit-tests/deptgt-error.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-error.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .ERROR in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-ignore.exp b/unit-tests/deptgt-ignore.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-ignore.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-ignore.mk b/unit-tests/deptgt-ignore.mk
new file mode 100644
index 000000000000..6ace0841f28b
--- /dev/null
+++ b/unit-tests/deptgt-ignore.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-ignore.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .IGNORE in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-interrupt.exp b/unit-tests/deptgt-interrupt.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-interrupt.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-interrupt.mk b/unit-tests/deptgt-interrupt.mk
new file mode 100644
index 000000000000..d555f864563e
--- /dev/null
+++ b/unit-tests/deptgt-interrupt.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-interrupt.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .INTERRUPT in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-main.exp b/unit-tests/deptgt-main.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-main.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-main.mk b/unit-tests/deptgt-main.mk
new file mode 100644
index 000000000000..cf1b1b4fd340
--- /dev/null
+++ b/unit-tests/deptgt-main.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-main.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .MAIN in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-makeflags.exp b/unit-tests/deptgt-makeflags.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-makeflags.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-makeflags.mk b/unit-tests/deptgt-makeflags.mk
new file mode 100644
index 000000000000..958751b64cfa
--- /dev/null
+++ b/unit-tests/deptgt-makeflags.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-makeflags.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .MAKEFLAGS in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-no_parallel.exp b/unit-tests/deptgt-no_parallel.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-no_parallel.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-no_parallel.mk b/unit-tests/deptgt-no_parallel.mk
new file mode 100644
index 000000000000..247f6a9ac8a5
--- /dev/null
+++ b/unit-tests/deptgt-no_parallel.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-no_parallel.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .NO_PARALLEL in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-nopath.exp b/unit-tests/deptgt-nopath.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-nopath.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-nopath.mk b/unit-tests/deptgt-nopath.mk
new file mode 100644
index 000000000000..a8cde2657dde
--- /dev/null
+++ b/unit-tests/deptgt-nopath.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-nopath.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .NOPATH in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-notparallel.exp b/unit-tests/deptgt-notparallel.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-notparallel.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-notparallel.mk b/unit-tests/deptgt-notparallel.mk
new file mode 100644
index 000000000000..db08aa9d3558
--- /dev/null
+++ b/unit-tests/deptgt-notparallel.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-notparallel.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .NOTPARALLEL in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-objdir.exp b/unit-tests/deptgt-objdir.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-objdir.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-objdir.mk b/unit-tests/deptgt-objdir.mk
new file mode 100644
index 000000000000..f5e9a15d8cb3
--- /dev/null
+++ b/unit-tests/deptgt-objdir.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-objdir.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .OBJDIR in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-order.exp b/unit-tests/deptgt-order.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-order.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-order.mk b/unit-tests/deptgt-order.mk
new file mode 100644
index 000000000000..003552f57a49
--- /dev/null
+++ b/unit-tests/deptgt-order.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-order.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .ORDER in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-path-suffix.exp b/unit-tests/deptgt-path-suffix.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-path-suffix.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-path-suffix.mk b/unit-tests/deptgt-path-suffix.mk
new file mode 100644
index 000000000000..3a7e697bc748
--- /dev/null
+++ b/unit-tests/deptgt-path-suffix.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-path-suffix.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .PATH.suffix in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-path.exp b/unit-tests/deptgt-path.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-path.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-path.mk b/unit-tests/deptgt-path.mk
new file mode 100644
index 000000000000..f74d1a46d1b1
--- /dev/null
+++ b/unit-tests/deptgt-path.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-path.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .PATH in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-phony.exp b/unit-tests/deptgt-phony.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-phony.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-phony.mk b/unit-tests/deptgt-phony.mk
new file mode 100644
index 000000000000..7f9909fa89a2
--- /dev/null
+++ b/unit-tests/deptgt-phony.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-phony.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .PHONY in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-precious.exp b/unit-tests/deptgt-precious.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-precious.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-precious.mk b/unit-tests/deptgt-precious.mk
new file mode 100644
index 000000000000..07b2bf719a4f
--- /dev/null
+++ b/unit-tests/deptgt-precious.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-precious.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .PRECIOUS in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-shell.exp b/unit-tests/deptgt-shell.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-shell.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-shell.mk b/unit-tests/deptgt-shell.mk
new file mode 100644
index 000000000000..71b535d89115
--- /dev/null
+++ b/unit-tests/deptgt-shell.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-shell.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .SHELL in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-silent.exp b/unit-tests/deptgt-silent.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-silent.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-silent.mk b/unit-tests/deptgt-silent.mk
new file mode 100644
index 000000000000..9ae64567fb97
--- /dev/null
+++ b/unit-tests/deptgt-silent.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-silent.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .SILENT in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-stale.exp b/unit-tests/deptgt-stale.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt-stale.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt-stale.mk b/unit-tests/deptgt-stale.mk
new file mode 100644
index 000000000000..cfde5a2bfd49
--- /dev/null
+++ b/unit-tests/deptgt-stale.mk
@@ -0,0 +1,8 @@
+# $NetBSD: deptgt-stale.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special target .STALE in dependency declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/deptgt-suffixes.exp b/unit-tests/deptgt-suffixes.exp
new file mode 100644
index 000000000000..b24f47becdaf
--- /dev/null
+++ b/unit-tests/deptgt-suffixes.exp
@@ -0,0 +1,7 @@
+#*** Suffixes:
+# `.custom-null' [1]  (SUFF_NULL)
+#	To: 
+#	From: 
+#	Search Path: . .. 
+#*** Transformations:
+exit status 0
diff --git a/unit-tests/deptgt-suffixes.mk b/unit-tests/deptgt-suffixes.mk
new file mode 100644
index 000000000000..791ff5eb5f03
--- /dev/null
+++ b/unit-tests/deptgt-suffixes.mk
@@ -0,0 +1,18 @@
+# $NetBSD: deptgt-suffixes.mk,v 1.3 2020/08/28 04:05:35 rillig Exp $
+#
+# Tests for the special target .SUFFIXES in dependency declarations.
+#
+# See also:
+#	varname-dot-includes.mk
+#	varname-dot-libs.mk
+
+.MAKEFLAGS: -dg1
+
+.SUFFIXES: .custom-null
+
+# TODO: What is the effect of this? How is it useful?
+.NULL: .custom-null
+.PATH.custom-null: . ..
+
+all:
+	@:;
diff --git a/unit-tests/deptgt.exp b/unit-tests/deptgt.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/deptgt.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/deptgt.mk b/unit-tests/deptgt.mk
new file mode 100644
index 000000000000..add21dc0baee
--- /dev/null
+++ b/unit-tests/deptgt.mk
@@ -0,0 +1,9 @@
+# $NetBSD: deptgt.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for special targets like .BEGIN or .SUFFIXES in dependency
+# declarations.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/dir-expand-path.exp b/unit-tests/dir-expand-path.exp
new file mode 100644
index 000000000000..74de255298bb
--- /dev/null
+++ b/unit-tests/dir-expand-path.exp
@@ -0,0 +1,4 @@
+dir-expand-path.dir.1/file1.src
+dir-expand-path.dir.1/file2.src
+dir-expand-path.dir.2/file3.src
+exit status 0
diff --git a/unit-tests/dir-expand-path.mk b/unit-tests/dir-expand-path.mk
new file mode 100755
index 000000000000..1ce349cce385
--- /dev/null
+++ b/unit-tests/dir-expand-path.mk
@@ -0,0 +1,19 @@
+# $NetBSD: dir-expand-path.mk,v 1.1 2020/08/22 21:55:54 rillig Exp $
+#
+# Tests for filename expansion in the search path.
+
+_!=	rm -rf dir-expand-path.dir.*
+_!=	mkdir dir-expand-path.dir.1
+_!=	mkdir dir-expand-path.dir.2
+_!=	touch dir-expand-path.dir.1/file1.src
+_!=	touch dir-expand-path.dir.1/file2.src
+_!=	touch dir-expand-path.dir.2/file3.src
+
+.PATH: dir-expand-path.dir.1
+.PATH: dir-expand-path.dir.2
+
+all: *.src
+	@printf '%s\n' ${.ALLSRC:O}
+
+.END:
+	@rm -rf dir-expand-path.dir.*
diff --git a/unit-tests/dir.exp b/unit-tests/dir.exp
new file mode 100644
index 000000000000..874a081e97ab
--- /dev/null
+++ b/unit-tests/dir.exp
@@ -0,0 +1,19 @@
+1
+2
+3
+4
+5
+13
+14
+15
+pre-patch
+pre-configure
+patch
+configure
+fetch
+fetch-post
+extract
+extract-post
+dup-1
+single-word
+exit status 0
diff --git a/unit-tests/dir.mk b/unit-tests/dir.mk
new file mode 100644
index 000000000000..f5926375312e
--- /dev/null
+++ b/unit-tests/dir.mk
@@ -0,0 +1,58 @@
+# $NetBSD: dir.mk,v 1.4 2020/07/31 20:16:21 rillig Exp $
+#
+# Tests for dir.c.
+
+# Dependency lines may use braces for expansion.
+all: {one,two,three}
+
+one:
+	@echo 1
+two:
+	@echo 2
+three:
+	@echo 3
+
+# The braces may start in the middle of a word.
+all: f{our,ive}
+
+four:
+	@echo 4
+five:
+	@echo 5
+six:
+	@echo 6
+
+# But nested braces don't work.
+all: {{thi,fou}r,fif}teen
+
+thirteen:
+	@echo 13
+fourteen:
+	@echo 14
+fifteen:
+	@echo 15
+
+# There may be multiple brace groups side by side.
+all: {pre-,}{patch,configure}
+
+pre-patch patch pre-configure configure:
+	@echo $@
+
+# Empty pieces are allowed in the braces.
+all: {fetch,extract}{,-post}
+
+fetch fetch-post extract extract-post:
+	@echo $@
+
+# The expansions may have duplicates.
+# These are merged together because of the dependency line.
+all: dup-{1,1,1,1,1,1,1}
+
+dup-1:
+	@echo $@
+
+# Other than in Bash, the braces are also expanded if there is no comma.
+all: {{{{{{{{{{single-word}}}}}}}}}}
+
+single-word:
+	@echo $@
diff --git a/unit-tests/directive-elif.exp b/unit-tests/directive-elif.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-elif.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-elif.mk b/unit-tests/directive-elif.mk
new file mode 100644
index 000000000000..fc8bb7e7d197
--- /dev/null
+++ b/unit-tests/directive-elif.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-elif.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .elif directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-elifdef.exp b/unit-tests/directive-elifdef.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-elifdef.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-elifdef.mk b/unit-tests/directive-elifdef.mk
new file mode 100644
index 000000000000..f960c1513e8e
--- /dev/null
+++ b/unit-tests/directive-elifdef.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-elifdef.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .elifdef directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-elifmake.exp b/unit-tests/directive-elifmake.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-elifmake.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-elifmake.mk b/unit-tests/directive-elifmake.mk
new file mode 100644
index 000000000000..c78e3aa91332
--- /dev/null
+++ b/unit-tests/directive-elifmake.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-elifmake.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .elifmake directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-elifndef.exp b/unit-tests/directive-elifndef.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-elifndef.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-elifndef.mk b/unit-tests/directive-elifndef.mk
new file mode 100644
index 000000000000..19bb66c11b01
--- /dev/null
+++ b/unit-tests/directive-elifndef.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-elifndef.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .elifndef directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-elifnmake.exp b/unit-tests/directive-elifnmake.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-elifnmake.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-elifnmake.mk b/unit-tests/directive-elifnmake.mk
new file mode 100644
index 000000000000..11c4b973c1c8
--- /dev/null
+++ b/unit-tests/directive-elifnmake.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-elifnmake.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .elifnmake directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-else.exp b/unit-tests/directive-else.exp
new file mode 100644
index 000000000000..387577099b81
--- /dev/null
+++ b/unit-tests/directive-else.exp
@@ -0,0 +1,8 @@
+make: "directive-else.mk" line 10: ok
+make: "directive-else.mk" line 14: ok
+make: "directive-else.mk" line 20: if-less else
+make: "directive-else.mk" line 26: ok
+make: "directive-else.mk" line 27: warning: extra else
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/directive-else.mk b/unit-tests/directive-else.mk
new file mode 100644
index 000000000000..3afa5648af67
--- /dev/null
+++ b/unit-tests/directive-else.mk
@@ -0,0 +1,32 @@
+# $NetBSD: directive-else.mk,v 1.3 2020/08/29 18:50:25 rillig Exp $
+#
+# Tests for the .else directive.
+
+# The .else directive does not take any arguments.
+# As of 2020-08-29, make doesn't warn about this.
+.if 0
+.warning must not be reached
+.else 123
+.info ok
+.endif
+
+.if 1
+.info ok
+.else 123
+.warning must not be reached
+.endif
+
+# An .else without a corresponding .if is an error.
+.else
+
+# Accidental extra .else directives are detected too.
+.if 0
+.warning must not be reached
+.else
+.info ok
+.else
+.info After an extra .else, everything is skipped.
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/directive-endif.exp b/unit-tests/directive-endif.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-endif.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-endif.mk b/unit-tests/directive-endif.mk
new file mode 100644
index 000000000000..12b608ffbeee
--- /dev/null
+++ b/unit-tests/directive-endif.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-endif.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .endif directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-error.exp b/unit-tests/directive-error.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-error.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-error.mk b/unit-tests/directive-error.mk
new file mode 100644
index 000000000000..3980016221ab
--- /dev/null
+++ b/unit-tests/directive-error.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-error.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .error directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-export-env.exp b/unit-tests/directive-export-env.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-export-env.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-export-env.mk b/unit-tests/directive-export-env.mk
new file mode 100644
index 000000000000..49d1edb9f6fe
--- /dev/null
+++ b/unit-tests/directive-export-env.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-export-env.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .export-env directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-export-literal.exp b/unit-tests/directive-export-literal.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-export-literal.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-export-literal.mk b/unit-tests/directive-export-literal.mk
new file mode 100644
index 000000000000..ed3957b288d6
--- /dev/null
+++ b/unit-tests/directive-export-literal.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-export-literal.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .export-literal directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-export.exp b/unit-tests/directive-export.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-export.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-export.mk b/unit-tests/directive-export.mk
new file mode 100644
index 000000000000..c7f9181b4b5a
--- /dev/null
+++ b/unit-tests/directive-export.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-export.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .export directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-for-generating-endif.exp b/unit-tests/directive-for-generating-endif.exp
new file mode 100755
index 000000000000..9e1301abf96f
--- /dev/null
+++ b/unit-tests/directive-for-generating-endif.exp
@@ -0,0 +1,7 @@
+make: "directive-for-generating-endif.mk" line 21: if-less endif
+make: "directive-for-generating-endif.mk" line 21: if-less endif
+make: "directive-for-generating-endif.mk" line 21: if-less endif
+make: "directive-for-generating-endif.mk" line 0: 3 open conditionals
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/directive-for-generating-endif.mk b/unit-tests/directive-for-generating-endif.mk
new file mode 100755
index 000000000000..b4d709551003
--- /dev/null
+++ b/unit-tests/directive-for-generating-endif.mk
@@ -0,0 +1,25 @@
+# $NetBSD: directive-for-generating-endif.mk,v 1.1 2020/08/29 18:50:25 rillig Exp $
+#
+# Test whether a .for loop can be used to generate multiple .endif
+# directives to close nested .if directives.  Depending on the exact
+# implementation, this might have been possible.
+#
+# If it were possible, the 3 .if directives would perfectly match the
+# 3 .endif directives generated by the .for loop.
+#
+# After the "included file" from the .for loop, the 3 .if directives
+# are still open.
+#
+# See For_Run and ParseReadLine.  Each .for loop is treated like a separately
+# included file, and in each included file the .if/.endif directives must be
+# balanced.
+
+.if 1
+.  if 2
+.    if 3
+.for i in 3 2 1
+.endif
+.endfor
+
+all:
+	@:;
diff --git a/unit-tests/directive-for.exp b/unit-tests/directive-for.exp
new file mode 100755
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-for.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-for.mk b/unit-tests/directive-for.mk
new file mode 100755
index 000000000000..5b3f299e5711
--- /dev/null
+++ b/unit-tests/directive-for.mk
@@ -0,0 +1,97 @@
+# $NetBSD: directive-for.mk,v 1.2 2020/09/02 22:58:59 rillig Exp $
+#
+# Tests for the .for directive.
+
+# Using the .for loop, lists of values can be produced.
+# In simple cases, the :@var@${var}@ variable modifier can be used to
+# reach the same effects.
+#
+.undef NUMBERS
+.for num in 1 2 3
+NUMBERS+=	${num}
+.endfor
+.if ${NUMBERS} != "1 2 3"
+.  error
+.endif
+
+# The .for loop also works for multiple iteration variables.
+.for name value in VARNAME value NAME2 value2
+${name}=	${value}
+.endfor
+.if ${VARNAME} != "value" || ${NAME2} != "value2"
+.  error
+.endif
+
+# The .for loop splits the items at whitespace, taking quotes into account,
+# just like the :M or :S variable modifiers.
+#
+# Until 2012-06-03, it had split the items exactly at whitespace, without
+# taking the quotes into account.
+#
+.undef WORDS
+.for var in one t\ w\ o "three three" 'four four' `five six`
+WORDS+=	counted
+.endfor
+.if ${WORDS:[#]} != 6
+.  error
+.endif
+
+# In the body of the .for loop, the iteration variables can be accessed
+# like normal variables, even though they are not really variables.
+#
+# Instead, the expression ${var} is transformed into ${:U1}, ${:U2} and so
+# on, before the loop body is evaluated.
+#
+# A notable effect of this implementation technique is that the .for
+# iteration variables and the normal global variables live in separate
+# namespaces and do not influence each other.
+#
+var=	value before
+var2=	value before
+.for var var2 in 1 2 3 4
+.endfor
+.if ${var} != "value before"
+.  warning After the .for loop, var must still have its original value.
+.endif
+.if ${var2} != "value before"
+.  warning After the .for loop, var2 must still have its original value.
+.endif
+
+# Everything from the paragraph above also applies if the loop body is
+# empty, even if there is no actual iteration since the loop items are
+# also empty.
+#
+var=	value before
+var2=	value before
+.for var var2 in ${:U}
+.endfor
+.if ${var} != "value before"
+.  warning After the .for loop, var must still have its original value.
+.endif
+.if ${var2} != "value before"
+.  warning After the .for loop, var2 must still have its original value.
+.endif
+
+# Until 2008-12-21, the values of the iteration variables were simply
+# inserted as plain text and then parsed as usual, which made it possible
+# to achieve all kinds of strange effects.
+#
+# Before that date, the .for loop expanded to:
+#	EXPANSION+= value
+# Since that date, the .for loop expands to:
+#	EXPANSION${:U+}= value
+#
+EXPANSION=	before
+EXPANSION+ =	before
+.for plus in +
+EXPANSION${plus}=	value
+.endfor
+.if ${EXPANSION} != "before"
+.  error This must be a make from before 2009.
+.endif
+.if ${EXPANSION+} != "value"
+.  error This must be a make from before 2009.
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/directive-if.exp b/unit-tests/directive-if.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-if.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-if.mk b/unit-tests/directive-if.mk
new file mode 100644
index 000000000000..72d7d0e2d920
--- /dev/null
+++ b/unit-tests/directive-if.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-if.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .if directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-ifdef.exp b/unit-tests/directive-ifdef.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-ifdef.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-ifdef.mk b/unit-tests/directive-ifdef.mk
new file mode 100644
index 000000000000..64f073fb5ae1
--- /dev/null
+++ b/unit-tests/directive-ifdef.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-ifdef.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .ifdef directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-ifmake.exp b/unit-tests/directive-ifmake.exp
new file mode 100644
index 000000000000..5fefeb252b48
--- /dev/null
+++ b/unit-tests/directive-ifmake.exp
@@ -0,0 +1,10 @@
+make: "directive-ifmake.mk" line 8: ok: positive condition works
+make: "directive-ifmake.mk" line 19: ok: negation works
+make: "directive-ifmake.mk" line 25: ok: double negation works
+make: "directive-ifmake.mk" line 32: ok: both mentioned
+make: "directive-ifmake.mk" line 39: ok: only those mentioned
+make: "directive-ifmake.mk" line 49: Targets can even be added at parse time.
+: first
+: second
+: late-target
+exit status 0
diff --git a/unit-tests/directive-ifmake.mk b/unit-tests/directive-ifmake.mk
new file mode 100644
index 000000000000..20329bc5ce25
--- /dev/null
+++ b/unit-tests/directive-ifmake.mk
@@ -0,0 +1,55 @@
+# $NetBSD: directive-ifmake.mk,v 1.4 2020/08/30 14:25:45 rillig Exp $
+#
+# Tests for the .ifmake directive, which provides a shortcut for asking
+# whether a certain target is requested to be made from the command line.
+
+# This is the most basic form.
+.ifmake first
+.info ok: positive condition works
+.else
+.warning positive condition fails
+.endif
+
+# The not operator works as expected.
+# An alternative interpretation were that this condition is asking whether
+# the target "!first" was requested.  To distinguish this, see the next test.
+.ifmake !first
+.warning unexpected
+.else
+.info ok: negation works
+.endif
+
+# See if the exclamation mark really means "not", or if it is just part of
+# the target name.
+.ifmake !!first
+.info ok: double negation works
+.else
+.warning double negation fails
+.endif
+
+# Multiple targets can be combined using the && and || operators.
+.ifmake first && second
+.info ok: both mentioned
+.else
+.warning && does not work as expected
+.endif
+
+# Negation also works in complex conditions.
+.ifmake first && !unmentioned
+.info ok: only those mentioned
+.else
+.warning && with ! does not work as expected
+.endif
+
+# Using the .MAKEFLAGS special dependency target, arbitrary command
+# line options can be added at parse time.  This means that it is
+# possible to extend the targets to be made.
+.MAKEFLAGS: late-target
+.ifmake late-target
+.info Targets can even be added at parse time.
+.else
+.info No, targets cannot be added at parse time anymore.
+.endif
+
+first second unmentioned late-target:
+	: $@
diff --git a/unit-tests/directive-ifndef.exp b/unit-tests/directive-ifndef.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-ifndef.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-ifndef.mk b/unit-tests/directive-ifndef.mk
new file mode 100644
index 000000000000..0981f817fcfd
--- /dev/null
+++ b/unit-tests/directive-ifndef.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-ifndef.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .ifndef directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-ifnmake.exp b/unit-tests/directive-ifnmake.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-ifnmake.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-ifnmake.mk b/unit-tests/directive-ifnmake.mk
new file mode 100644
index 000000000000..2a20249f6c76
--- /dev/null
+++ b/unit-tests/directive-ifnmake.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-ifnmake.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .ifnmake directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-info.exp b/unit-tests/directive-info.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-info.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-info.mk b/unit-tests/directive-info.mk
new file mode 100644
index 000000000000..3eb972ad7a0e
--- /dev/null
+++ b/unit-tests/directive-info.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-info.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .info directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-undef.exp b/unit-tests/directive-undef.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-undef.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-undef.mk b/unit-tests/directive-undef.mk
new file mode 100644
index 000000000000..ff91eeddb41f
--- /dev/null
+++ b/unit-tests/directive-undef.mk
@@ -0,0 +1,17 @@
+# $NetBSD: directive-undef.mk,v 1.3 2020/08/23 19:30:13 rillig Exp $
+#
+# Tests for the .undef directive.
+
+# As of 2020-07-28, .undef only undefines the first variable.
+# All further variable names are silently ignored.
+# See parse.c, string literal "undef".
+1=		1
+2=		2
+3=		3
+.undef 1 2 3
+.if ${1:U_}${2:U_}${3:U_} != _23
+.warning $1$2$3
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/directive-unexport-env.exp b/unit-tests/directive-unexport-env.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-unexport-env.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-unexport-env.mk b/unit-tests/directive-unexport-env.mk
new file mode 100644
index 000000000000..34d867d706ef
--- /dev/null
+++ b/unit-tests/directive-unexport-env.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-unexport-env.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .unexport-env directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-unexport.exp b/unit-tests/directive-unexport.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-unexport.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-unexport.mk b/unit-tests/directive-unexport.mk
new file mode 100644
index 000000000000..679387aac083
--- /dev/null
+++ b/unit-tests/directive-unexport.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-unexport.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .unexport directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive-warning.exp b/unit-tests/directive-warning.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive-warning.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive-warning.mk b/unit-tests/directive-warning.mk
new file mode 100644
index 000000000000..e1e636e3ec9f
--- /dev/null
+++ b/unit-tests/directive-warning.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive-warning.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the .warning directive.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directive.exp b/unit-tests/directive.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/directive.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/directive.mk b/unit-tests/directive.mk
new file mode 100644
index 000000000000..8d01a49a34cf
--- /dev/null
+++ b/unit-tests/directive.mk
@@ -0,0 +1,8 @@
+# $NetBSD: directive.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the preprocessing directives, such as .if or .info.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/directives.exp b/unit-tests/directives.exp
new file mode 100644
index 000000000000..15dd9aa9e962
--- /dev/null
+++ b/unit-tests/directives.exp
@@ -0,0 +1,42 @@
+make: "directives.mk" line 10: begin .export tests
+make: "directives.mk" line 11: Unknown directive "expor"
+make: "directives.mk" line 20: begin .export-env tests
+make: "directives.mk" line 30: begin .export-literal tests
+make: "directives.mk" line 40: begin .info tests
+make: "directives.mk" line 41: Unknown directive "inf"
+make: "directives.mk" line 42: Unknown directive "info"
+make: "directives.mk" line 43: message
+make: "directives.mk" line 44: indented message
+make: "directives.mk" line 45: Unknown directive "information"
+make: "directives.mk" line 46: message
+make: "directives.mk" line 50: begin .undef tests
+make: "directives.mk" line 51: Unknown directive "unde"
+make: "directives.mk" line 60: begin .unexport tests
+make: "directives.mk" line 61: Unknown directive "unexpor"
+make: "directives.mk" line 70: begin .unexport-env tests
+make: "directives.mk" line 80: begin .warning tests
+make: "directives.mk" line 81: Unknown directive "warn"
+make: "directives.mk" line 82: Unknown directive "warnin"
+make: "directives.mk" line 83: Unknown directive "warning"
+make: "directives.mk" line 84: warning: message
+make: "directives.mk" line 85: Unknown directive "warnings"
+make: "directives.mk" line 86: warning: messages
+make: "directives.mk" line 90: begin .elif misspellings tests, part 1
+make: "directives.mk" line 100: begin .elif misspellings tests, part 2
+make: "directives.mk" line 110: begin .elif misspellings tests, part 3
+make: "directives.mk" line 120: which branch is taken on misspelling after false?
+make: "directives.mk" line 127: else taken
+make: "directives.mk" line 130: which branch is taken on misspelling after true?
+make: "directives.mk" line 132: Unknown directive "elsif"
+make: "directives.mk" line 133: 1 taken
+make: "directives.mk" line 134: Unknown directive "elsif"
+make: "directives.mk" line 135: 2 taken
+make: "directives.mk" line 140: Unknown directive "indented"
+make: "directives.mk" line 141: Unknown directive "indented"
+make: "directives.mk" line 142: Unknown directive "indented"
+make: "directives.mk" line 143: Unknown directive "info"
+make: "directives.mk" line 150: Could not find nonexistent.mk
+make: "directives.mk" line 160: end of the tests
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/directives.mk b/unit-tests/directives.mk
new file mode 100644
index 000000000000..50e8e1315dda
--- /dev/null
+++ b/unit-tests/directives.mk
@@ -0,0 +1,163 @@
+# $NetBSD: directives.mk,v 1.5 2020/07/28 20:57:59 rillig Exp $
+#
+# Tests for parsing directives, in the same order as in the manual page.
+#
+# Each test group has 10 lines, to keep the line numbers in directives.exp
+# stable.
+#
+# no tests for .error since it exits immediately, see ParseMessage.
+
+.info begin .export tests
+.expor				# misspelled
+.export				# oops: missing argument
+.export VARNAME
+.exporting works		# oops: misspelled
+
+
+
+
+
+.info begin .export-env tests
+.export-en			# oops: misspelled
+.export-env
+.export-environment		# oops: misspelled
+
+
+
+
+
+
+.info begin .export-literal tests
+.export-litera			# oops: misspelled
+.export-literal			# oops: missing argument
+.export-literal VARNAME
+.export-literally		# oops: misspelled
+
+
+
+
+
+.info begin .info tests
+.inf				# misspelled
+.info				# oops: message should be "missing parameter"
+.info message
+.info		indented message
+.information
+.information message		# oops: misspelled
+.info.man:			# not a message, but a suffix rule
+
+
+.info begin .undef tests
+.unde				# misspelled
+.undef				# oops: missing argument
+.undefined			# oops: misspelled
+.undef VARNAME
+
+
+
+
+
+.info begin .unexport tests
+.unexpor			# misspelled
+.unexport			# oops: missing argument
+.unexport VARNAME		# ok
+.unexporting works		# oops: misspelled
+
+
+
+
+
+.info begin .unexport-env tests
+.unexport-en			# misspelled
+.unexport-env			# ok
+.unexport-environment		# oops: misspelled
+
+
+
+
+
+
+.info begin .warning tests
+.warn				# misspelled
+.warnin				# misspelled
+.warning			# oops: should be "missing argument"
+.warning message		# ok
+.warnings			# misspelled
+.warnings messages		# oops
+
+
+
+.info begin .elif misspellings tests, part 1
+.if 1
+.elif 1				# ok
+.elsif 1			# oops: misspelled
+.elseif 1			# oops: misspelled
+.endif
+
+
+
+
+.info begin .elif misspellings tests, part 2
+.if 0
+.elif 0				# ok
+.elsif 0			# oops: misspelled
+.elseif 0			# oops: misspelled
+.endif
+
+
+
+
+.info begin .elif misspellings tests, part 3
+.if 0
+.elsif 0			# oops: misspelled
+.endif
+.if 0
+.elseif 0			# oops: misspelled
+.endif
+
+
+
+.info which branch is taken on misspelling after false?
+.if 0
+.elsif 1
+.info 1 taken
+.elsif 2
+.info 2 taken
+.else
+.info else taken
+.endif
+
+.info which branch is taken on misspelling after true?
+.if 1
+.elsif 1
+.info 1 taken
+.elsif 2
+.info 2 taken
+.else
+.info else taken
+.endif
+
+.indented none
+.  indented 2 spaces
+.	indented tab
+.${:Uinfo} directives cannot be indirect
+
+
+
+
+
+
+.include "nonexistent.mk"
+.include "/dev/null"		# size 0
+# including a directory technically succeeds, but shouldn't.
+#.include "."			# directory
+
+
+
+
+
+
+.info end of the tests
+
+all:
+	@:
diff --git a/unit-tests/envfirst.exp b/unit-tests/envfirst.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/envfirst.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/envfirst.mk b/unit-tests/envfirst.mk
new file mode 100644
index 000000000000..f4744bd61285
--- /dev/null
+++ b/unit-tests/envfirst.mk
@@ -0,0 +1,42 @@
+# $NetBSD: envfirst.mk,v 1.2 2020/07/27 18:57:42 rillig Exp $
+#
+# The -e option makes environment variables stronger than global variables.
+
+.if ${FROM_ENV} != value-from-env
+.error ${FROM_ENV}
+.endif
+
+# Try to override the variable; this does not have any effect.
+FROM_ENV=	value-from-mk
+.if ${FROM_ENV} != value-from-env
+.error ${FROM_ENV}
+.endif
+
+# Try to append to the variable; this also doesn't have any effect.
+FROM_ENV+=	appended
+.if ${FROM_ENV} != value-from-env
+.error ${FROM_ENV}
+.endif
+
+# The default assignment also cannot change the variable.
+FROM_ENV?=	default
+.if ${FROM_ENV} != value-from-env
+.error ${FROM_ENV}
+.endif
+
+# Neither can the assignment modifiers.
+.if ${FROM_ENV::=from-condition}
+.endif
+.if ${FROM_ENV} != value-from-env
+.error ${FROM_ENV}
+.endif
+
+# Even .undef doesn't work since it only affects the global context,
+# which is independent from the environment variables.
+.undef FROM_ENV
+.if ${FROM_ENV} != value-from-env
+.error ${FROM_ENV}
+.endif
+
+all:
+	@: nothing
diff --git a/unit-tests/export-all.mk b/unit-tests/export-all.mk
index 200412f3b90a..c1910cac96b9 100644
--- a/unit-tests/export-all.mk
+++ b/unit-tests/export-all.mk
@@ -1,4 +1,4 @@
-# $Id: export-all.mk,v 1.1.1.2 2015/04/10 20:43:38 sjg Exp $
+# $Id: export-all.mk,v 1.1.1.3 2020/07/28 16:57:18 sjg Exp $
 
 UT_OK=good
 UT_F=fine
@@ -17,6 +17,7 @@ UT_OKDIR = ${${here}/../${here:T}:L:${M_tA}:T}
 
 .export
 
+FILTER_CMD=	grep ^UT_
 .include "export.mk"
 
 UT_TEST=export-all
diff --git a/unit-tests/export-variants.exp b/unit-tests/export-variants.exp
new file mode 100755
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/export-variants.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/export-variants.mk b/unit-tests/export-variants.mk
new file mode 100755
index 000000000000..1e254f1816cf
--- /dev/null
+++ b/unit-tests/export-variants.mk
@@ -0,0 +1,40 @@
+# $NetBSD: export-variants.mk,v 1.2 2020/08/08 13:00:07 rillig Exp $
+#
+# Test whether exported variables apply to each variant of running
+# external commands:
+#
+# The != assignments.
+# The :!cmd! modifier.
+# The :sh modifier.
+
+SHVAR!=	env | grep ^UT_ || true
+.if ${SHVAR} != ""
+.warning At this point, no variable should be exported.
+.endif
+
+.if ${:!env | grep ^UT_ || true!} != ""
+.warning At this point, no variable should be exported.
+.endif
+
+.if ${env | grep ^UT_ || true:L:sh} != ""
+.warning At this point, no variable should be exported.
+.endif
+
+UT_VAR=		value
+.export UT_VAR
+
+SHVAR!=	env | grep ^UT_ || true
+.if ${SHVAR} != "UT_VAR=value"
+.warning At this point, no variable should be exported.
+.endif
+
+.if ${:!env | grep ^UT_ || true!} != "UT_VAR=value"
+.warning At this point, some variables should be exported.
+.endif
+
+.if ${env | grep ^UT_ || true:L:sh} != "UT_VAR=value"
+.warning At this point, some variables should be exported.
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/export.exp b/unit-tests/export.exp
index 143771ce90ae..c0e9b98c5a44 100644
--- a/unit-tests/export.exp
+++ b/unit-tests/export.exp
@@ -1,3 +1,5 @@
+&=ampersand
+MAKELEVEL=1
 UT_DOLLAR=This is $UT_FU
 UT_FOO=foobar is fubar
 UT_FU=fubar
diff --git a/unit-tests/export.mk b/unit-tests/export.mk
index 01f69f918161..b98d175af314 100644
--- a/unit-tests/export.mk
+++ b/unit-tests/export.mk
@@ -1,22 +1,43 @@
-# $Id: export.mk,v 1.1.1.1 2014/08/30 18:57:18 sjg Exp $
+# $Id: export.mk,v 1.1.1.4 2020/08/08 22:34:25 sjg Exp $
 
 UT_TEST=export
 UT_FOO=foo${BAR}
 UT_FU=fubar
 UT_ZOO=hoopie
 UT_NO=all
-# belive it or not, we expect this one to come out with $UT_FU unexpanded.
+# believe it or not, we expect this one to come out with $UT_FU unexpanded.
 UT_DOLLAR= This is $$UT_FU
 
 .export UT_FU UT_FOO
 .export UT_DOLLAR
-# this one will be ignored
+
+.if !defined(.MAKE.PID)
+.error .MAKE.PID must be defined
+.endif
+@=	at
+%=	percent
+*=	asterisk
+${:U!}=	exclamation		# A direct != would try to run "exclamation"
+				# as a shell command and assign its output
+				# to the empty variable.
+&=	ampersand
+
+# This is ignored because it is internal.
 .export .MAKE.PID
+# These are ignored because they are local to the target.
+.export @
+.export %
+.export *
+.export !
+.export &
+# This is ignored because it is undefined.
+.export UNDEFINED
 
 BAR=bar is ${UT_FU}
 
 .MAKE.EXPORTED+= UT_ZOO UT_TEST
 
-all:
-	@env | grep '^UT_' | sort
+FILTER_CMD?=	egrep -v '^(MAKEFLAGS|PATH|PWD|SHLVL|_)='
 
+all:
+	@env | ${FILTER_CMD} | sort
diff --git a/unit-tests/forloop.mk b/unit-tests/forloop.mk
index dd3f07ddb19a..d61a53816df7 100644
--- a/unit-tests/forloop.mk
+++ b/unit-tests/forloop.mk
@@ -1,4 +1,4 @@
-# $Id: forloop.mk,v 1.1.1.2 2020/05/05 00:54:40 sjg Exp $
+# $Id: forloop.mk,v 1.1.1.3 2020/09/02 18:35:14 sjg Exp $
 
 all: for-loop
 
@@ -36,7 +36,7 @@ X!= echo 'a=$a b=$b' >&2; echo
 
 # Since at least 1993, iteration stops at the first newline.
 # Back then, the .newline variable didn't exist, therefore it was unlikely
-# that a newline ever occured.
+# that a newline ever occurred.
 .for var in a${.newline}b${.newline}c
 X!= echo 'newline-item=('${var:Q}')' 1>&2; echo
 .endfor
diff --git a/unit-tests/impsrc.exp b/unit-tests/impsrc.exp
index 23e8347d205d..3eb050e080c5 100644
--- a/unit-tests/impsrc.exp
+++ b/unit-tests/impsrc.exp
@@ -1,13 +1,13 @@
-expected: source4
-actual:   source4
+expected: 
+actual:   
 expected: target1.x
 actual:   target1.x
 expected: target1.y
 actual:   target1.y
-expected: source1
-actual:   source1
-expected: source2
-actual:   source2
-expected: source1
-actual:   source1
+expected: 
+actual:   
+expected: 
+actual:   
+expected: 
+actual:   
 exit status 0
diff --git a/unit-tests/impsrc.mk b/unit-tests/impsrc.mk
index 95ae0c34f3b5..6fb31adac938 100644
--- a/unit-tests/impsrc.mk
+++ b/unit-tests/impsrc.mk
@@ -1,4 +1,4 @@
-# $NetBSD: impsrc.mk,v 1.2 2014/08/30 22:21:07 sjg Exp $
+# $NetBSD: impsrc.mk,v 1.3 2020/08/07 13:43:50 rillig Exp $
 
 # Does ${.IMPSRC} work properly?
 # It should be set, in order of precedence, to ${.TARGET} of:
@@ -6,6 +6,8 @@
 #  2) the first prerequisite from the dependency line of an explicit rule, or
 #  3) the first prerequisite of an explicit rule.
 #
+# Items 2 and 3 work in GNU make.
+# Items 2 and 3 are not required by POSIX 2018.
 
 all: target1.z target2 target3 target4
 
@@ -19,25 +21,40 @@ all: target1.z target2 target3 target4
 	@echo 'expected: target1.y'
 	@echo 'actual:   $<'
 
+# (3) Making target1.z out of target1.y is done because of an inference rule.
+# Therefore $< is available here.
+
+# (2) This is an additional dependency on the inference rule .x.y.
+# The dependency target1.x comes from the inference rule,
+# therefore it is available as $<.
 target1.y: source3
 
+# (1) This is an explicit dependency, not an inference rule.
+# Therefore POSIX does not specify that $< be available here.
 target1.x: source4
-	@echo 'expected: source4'
+	@echo 'expected: '		# either 'source4' or ''
 	@echo 'actual:   $<'
 
+# (4) This is an explicit dependency, independent of any inference rule.
+# Therefore $< is not available here.
 target2: source1 source2
-	@echo 'expected: source1'
+	@echo 'expected: '
 	@echo 'actual:   $<'
 
+# (5) These are two explicit dependency rules.
+# The first doesn't have any dependencies, only the second has.
+# If any, the value of $< would be 'source2'.
 target3: source1
 target3: source2 source3
-	@echo 'expected: source2'
+	@echo 'expected: '
 	@echo 'actual:   $<'
 
+# (6) The explicit rule does not have associated commands.
+# The value of $< might come from that rule,
+# but it's equally fine to leave $< undefined.
 target4: source1
 target4:
-	@echo 'expected: source1'
+	@echo 'expected: '
 	@echo 'actual:   $<'
 
 source1 source2 source3 source4:
-
diff --git a/unit-tests/include-main.mk b/unit-tests/include-main.mk
index 452b6102a5ce..a53dd886b800 100644
--- a/unit-tests/include-main.mk
+++ b/unit-tests/include-main.mk
@@ -1,11 +1,11 @@
-# $NetBSD: include-main.mk,v 1.1 2020/05/17 12:36:26 rillig Exp $
+# $NetBSD: include-main.mk,v 1.2 2020/07/27 20:55:59 rillig Exp $
 #
 # Demonstrates that the .INCLUDEDFROMFILE magic variable does not behave
 # as described in the manual page.
 #
 # The manual page says that it is the "filename of the file this Makefile
 # was included from", while in reality it is the "filename in which the
-# latest .include happened".
+# latest .include happened". See parse.c, function ParseSetIncludeFile.
 #
 
 .if !defined(.INCLUDEDFROMFILE)
diff --git a/unit-tests/lint.exp b/unit-tests/lint.exp
new file mode 100755
index 000000000000..9cd0eeb1ddf4
--- /dev/null
+++ b/unit-tests/lint.exp
@@ -0,0 +1,4 @@
+make: In the :@ modifier of "VAR", the variable name "${:Ubar:S,b,v,}" must not contain a dollar.
+y@:Q}
+xvaluey
+exit status 0
diff --git a/unit-tests/lint.mk b/unit-tests/lint.mk
new file mode 100755
index 000000000000..56cd6be27860
--- /dev/null
+++ b/unit-tests/lint.mk
@@ -0,0 +1,17 @@
+# $NetBSD: lint.mk,v 1.2 2020/08/08 13:00:07 rillig Exp $
+#
+# Demonstrates stricter checks that are only enabled in the lint mode,
+# using the -dL option.
+
+# Ouch: as of 2020-08-03, make exits successfully even though the error
+# message has been issued as PARSE_FATAL.
+
+# Ouch: as of 2020-08-03, the variable is malformed and parsing stops
+# for a moment, but is continued after the wrongly-guessed end of the
+# variable, which echoes "y@:Q}".
+
+all: mod-loop-varname
+
+mod-loop-varname:
+	@echo ${VAR:Uvalue:@${:Ubar:S,b,v,}@x${var}y@:Q}
+	@echo ${VAR:Uvalue:@!@x$!y@:Q}	# surprisingly allowed
diff --git a/unit-tests/make-exported.exp b/unit-tests/make-exported.exp
new file mode 100755
index 000000000000..7408f16c4e6c
--- /dev/null
+++ b/unit-tests/make-exported.exp
@@ -0,0 +1,3 @@
+-literal=make-exported-value
+UT_VAR=
+exit status 0
diff --git a/unit-tests/make-exported.mk b/unit-tests/make-exported.mk
new file mode 100755
index 000000000000..36f4b356ae53
--- /dev/null
+++ b/unit-tests/make-exported.mk
@@ -0,0 +1,16 @@
+# $NetBSD: make-exported.mk,v 1.1 2020/08/09 12:59:16 rillig Exp $
+#
+# As of 2020-08-09, the code in Var_Export is shared between the .export
+# directive and the .MAKE.EXPORTED variable.  This leads to non-obvious
+# behavior for certain variable assignments.
+
+-env=		make-exported-value
+-literal=	make-exported-value
+UT_VAR=		${UNEXPANDED}
+
+# The following behavior is probably not intended.
+.MAKE.EXPORTED=		-env		# like .export-env
+.MAKE.EXPORTED=		-literal UT_VAR	# like .export-literal PATH
+
+all:
+	@env | sort | grep -E '^UT_|make-exported-value' || true
diff --git a/unit-tests/moderrs.exp b/unit-tests/moderrs.exp
index cb51aa09d909..f017cae4633a 100644
--- a/unit-tests/moderrs.exp
+++ b/unit-tests/moderrs.exp
@@ -10,7 +10,132 @@ VAR:S,V,v,=Thevariable
 Expect: Unclosed variable specification for VAR
 make: Unclosed variable specification after complex modifier (expecting '}') for VAR
 VAR:S,V,v,=Thevariable
-Expect: Unclosed substitution for VAR (, missing)
-make: Unclosed substitution for VAR (, missing)
+Expect: Unfinished modifier for VAR (',' missing)
+make: Unfinished modifier for VAR (',' missing)
 VAR:S,V,v=
+Expect: 2 errors about missing @ delimiter
+make: Unfinished modifier for UNDEF ('@' missing)
+
+make: Unfinished modifier for UNDEF ('@' missing)
+
+1 2 3
+modloop-close:
+make: Unclosed variable specification (expecting '}') for "UNDEF" (value "1}... 2}... 3}...") modifier @
+1}... 2}... 3}...
+1}... 2}... 3}...
+Expect: 2 errors about missing ] delimiter
+make: Unfinished modifier for UNDEF (']' missing)
+
+make: Unfinished modifier for UNDEF (']' missing)
+
+13=
+12345=ok
+Expect: 2 errors about missing ! delimiter
+make: Unfinished modifier for VARNAME ('!' missing)
+
+make: Unfinished modifier for ! ('!' missing)
+
+mod-subst-delimiter:
+make: Missing delimiter for :S modifier
+
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unclosed variable specification (expecting '}') for "VAR" (value "TheVariable") modifier S
+TheVariable
+TheVariable
+make: Missing delimiter for :S modifier
+1:
+make: Unfinished modifier for VAR (',' missing)
+2:
+make: Unfinished modifier for VAR (',' missing)
+3:
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unclosed variable specification (expecting '}') for "VAR" (value "TheVariable") modifier S
+TheVariable
+TheVariable
+mod-regex-delimiter:
+make: Missing delimiter for :C modifier
+
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unclosed variable specification (expecting '}') for "VAR" (value "TheVariable") modifier C
+TheVariable
+TheVariable
+make: Missing delimiter for :C modifier
+1:
+make: Unfinished modifier for VAR (',' missing)
+2:
+make: Unfinished modifier for VAR (',' missing)
+3:
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unfinished modifier for VAR (',' missing)
+
+make: Unclosed variable specification (expecting '}') for "VAR" (value "TheVariable") modifier C
+TheVariable
+TheVariable
+mod-regex-undefined-subexpression:
+one one 2 3 5 8 one3 2one 34
+make: No match for subexpression \2
+make: No match for subexpression \2
+make: No match for subexpression \1
+make: No match for subexpression \2
+make: No match for subexpression \1
+()+() ()+() ()+() 3 5 8 (3)+() ()+(1) 34
+mod-ts-parse:
+112358132134
+15152535558513521534
+make: Bad modifier `:ts\65oct' for FIB
+65oct}
+make: Bad modifier `:tsxy' for FIB
+xy}
+mod-t-parse:
+make: Bad modifier `:t' for FIB
+
+make: Bad modifier `:txy' for FIB
+y}
+make: Bad modifier `:t' for FIB
+
+make: Bad modifier `:t' for FIB
+M*}
+mod-ifelse-parse:
+make: Unfinished modifier for FIB (':' missing)
+
+make: Unfinished modifier for FIB (':' missing)
+
+make: Unfinished modifier for FIB ('}' missing)
+
+make: Unfinished modifier for FIB ('}' missing)
+
+then
+mod-remember-parse:
+1 1 2 3 5 8 13 21 34
+make: Unknown modifier '_'
+
+mod-sysv-parse:
+make: Unknown modifier '3'
+make: Unclosed variable specification (expecting '}') for "FIB" (value "") modifier 3
+
+make: Unknown modifier '3'
+make: Unclosed variable specification (expecting '}') for "FIB" (value "") modifier 3
+
+make: Unknown modifier '3'
+make: Unclosed variable specification (expecting '}') for "FIB" (value "") modifier 3
+
+1 1 2 x3 5 8 1x3 21 34
 exit status 0
diff --git a/unit-tests/moderrs.mk b/unit-tests/moderrs.mk
index d0eb17ff6df6..931158c8ecb9 100644
--- a/unit-tests/moderrs.mk
+++ b/unit-tests/moderrs.mk
@@ -1,4 +1,4 @@
-# $Id: moderrs.mk,v 1.1.1.1 2014/08/30 18:57:18 sjg Exp $
+# $Id: moderrs.mk,v 1.1.1.8 2020/08/26 16:40:43 sjg Exp $
 #
 # various modifier error tests
 
@@ -8,7 +8,20 @@ MOD_UNKN=Z
 MOD_TERM=S,V,v
 MOD_S:= ${MOD_TERM},
 
-all:	modunkn modunknV varterm vartermV modtermV
+FIB=	1 1 2 3 5 8 13 21 34
+
+all:	modunkn modunknV varterm vartermV modtermV modloop
+all:	modloop-close
+all:	modwords
+all:	modexclam
+all:	mod-subst-delimiter
+all:	mod-regex-delimiter
+all:	mod-regex-undefined-subexpression
+all:	mod-ts-parse
+all:	mod-t-parse
+all:	mod-ifelse-parse
+all:	mod-remember-parse
+all:	mod-sysv-parse
 
 modunkn:
 	@echo "Expect: Unknown modifier 'Z'"
@@ -27,5 +40,137 @@ vartermV:
 	@echo VAR:${MOD_TERM},=${VAR:${MOD_S}
 
 modtermV:
-	@echo "Expect: Unclosed substitution for VAR (, missing)"
+	@echo "Expect: Unfinished modifier for VAR (',' missing)"
 	-@echo "VAR:${MOD_TERM}=${VAR:${MOD_TERM}}"
+
+modloop:
+	@echo "Expect: 2 errors about missing @ delimiter"
+	@echo ${UNDEF:U1 2 3:@var}
+	@echo ${UNDEF:U1 2 3:@var@...}
+	@echo ${UNDEF:U1 2 3:@var@${var}@}
+
+# The closing brace after the ${var} is part of the replacement string.
+# In ParseModifierPart, braces and parentheses don't have to be balanced.
+# This is contrary to the :M, :N modifiers, where both parentheses and
+# braces must be balanced.
+# This is also contrary to the SysV modifier, where only the actually
+# used delimiter (either braces or parentheses) must be balanced.
+modloop-close:
+	@echo $@:
+	@echo ${UNDEF:U1 2 3:@var@${var}}...@
+	@echo ${UNDEF:U1 2 3:@var@${var}}...@}
+
+modwords:
+	@echo "Expect: 2 errors about missing ] delimiter"
+	@echo ${UNDEF:U1 2 3:[}
+	@echo ${UNDEF:U1 2 3:[#}
+
+	# out of bounds => empty
+	@echo 13=${UNDEF:U1 2 3:[13]}
+
+	# Word index out of bounds.
+	#
+	# On LP64I32, strtol returns LONG_MAX,
+	# which is then truncated to int (undefined behavior),
+	# typically resulting in -1.
+	# This -1 is interpreted as "the last word".
+	#
+	# On ILP32, strtol returns LONG_MAX,
+	# which is a large number.
+	# This results in a range from LONG_MAX - 1 to 3,
+	# which is empty.
+	@echo 12345=${UNDEF:U1 2 3:[123451234512345123451234512345]:S,^$,ok,:S,^3$,ok,}
+
+modexclam:
+	@echo "Expect: 2 errors about missing ! delimiter"
+	@echo ${VARNAME:!echo}
+	# When the final exclamation mark is missing, there is no
+	# fallback to the SysV substitution modifier.
+	# If there were a fallback, the output would be "exclam",
+	# and the above would have produced an "Unknown modifier '!'".
+	@echo ${!:L:!=exclam}
+
+mod-subst-delimiter:
+	@echo $@:
+	@echo ${VAR:S
+	@echo ${VAR:S,
+	@echo ${VAR:S,from
+	@echo ${VAR:S,from,
+	@echo ${VAR:S,from,to
+	@echo ${VAR:S,from,to,
+	@echo ${VAR:S,from,to,}
+	@echo 1: ${VAR:S
+	@echo 2: ${VAR:S,
+	@echo 3: ${VAR:S,from
+	@echo ${VAR:S,from,
+	@echo ${VAR:S,from,to
+	@echo ${VAR:S,from,to,
+	@echo ${VAR:S,from,to,}
+
+mod-regex-delimiter:
+	@echo $@:
+	@echo ${VAR:C
+	@echo ${VAR:C,
+	@echo ${VAR:C,from
+	@echo ${VAR:C,from,
+	@echo ${VAR:C,from,to
+	@echo ${VAR:C,from,to,
+	@echo ${VAR:C,from,to,}
+	@echo 1: ${VAR:C
+	@echo 2: ${VAR:C,
+	@echo 3: ${VAR:C,from
+	@echo ${VAR:C,from,
+	@echo ${VAR:C,from,to
+	@echo ${VAR:C,from,to,
+	@echo ${VAR:C,from,to,}
+
+# In regular expressions with alternatives, not all capturing groups are
+# always set; some may be missing.  Warn about these.
+#
+# Since there is no way to turn off this warning, the combination of
+# alternative matches and capturing groups is not widely used.
+#
+# A newly added modifier 'U' such as in :C,(a.)|(b.),\1\2,U might be added
+# for treating undefined capturing groups as empty, but that would create a
+# syntactical ambiguity since the :S and :C modifiers are open-ended (see
+# mod-subst-chain).  Luckily the modifier :U does not make sense after :C,
+# therefore this case does not happen in practice.
+# The sub-modifier for the :C modifier would have to be chosen wisely.
+mod-regex-undefined-subexpression:
+	@echo $@:
+	@echo ${FIB:C,1(.*),one\1,}		# all ok
+	@echo ${FIB:C,1(.*)|2(.*),(\1)+(\2),:Q}	# no match for subexpression
+
+mod-ts-parse:
+	@echo $@:
+	@echo ${FIB:ts}
+	@echo ${FIB:ts\65}	# octal 065 == U+0035 == '5'
+	@echo ${FIB:ts\65oct}	# bad modifier
+	@echo ${FIB:tsxy}	# modifier too long
+
+mod-t-parse:
+	@echo $@:
+	@echo ${FIB:t
+	@echo ${FIB:txy}
+	@echo ${FIB:t}
+	@echo ${FIB:t:M*}
+
+mod-ifelse-parse:
+	@echo $@:
+	@echo ${FIB:?
+	@echo ${FIB:?then
+	@echo ${FIB:?then:
+	@echo ${FIB:?then:else
+	@echo ${FIB:?then:else}
+
+mod-remember-parse:
+	@echo $@:
+	@echo ${FIB:_}		# ok
+	@echo ${FIB:__}		# modifier name too long
+
+mod-sysv-parse:
+	@echo $@:
+	@echo ${FIB:3
+	@echo ${FIB:3=
+	@echo ${FIB:3=x3
+	@echo ${FIB:3=x3}	# ok
diff --git a/unit-tests/modmatch.exp b/unit-tests/modmatch.exp
index a7bf8b748f5b..fcaf6c02ed69 100644
--- a/unit-tests/modmatch.exp
+++ b/unit-tests/modmatch.exp
@@ -14,7 +14,4 @@ LIB=e X_LIBS:M${LIB${LIB:tu}} is "/tmp/libe.a"
 LIB=e X_LIBS:M*/lib${LIB}.a is "/tmp/libe.a"
 LIB=e X_LIBS:M*/lib${LIB}.a:tu is "/TMP/LIBE.A"
 Mscanner=OK
-Upper=One Two Three Four
-Lower=five six seven
-nose=One Three five
 exit status 0
diff --git a/unit-tests/modmatch.mk b/unit-tests/modmatch.mk
index c631bbd3440f..f15b3d699a8e 100644
--- a/unit-tests/modmatch.mk
+++ b/unit-tests/modmatch.mk
@@ -1,3 +1,6 @@
+# $NetBSD: modmatch.mk,v 1.8 2020/08/16 20:03:53 rillig Exp $
+#
+# Tests for the :M and :S modifiers.
 
 X=a b c d e
 
@@ -15,7 +18,7 @@ res = no
 res = OK
 .endif
 
-all:	show-libs check-cclass slow
+all:	show-libs
 
 show-libs:
 	@for x in $X; do ${.MAKE} -f ${MAKEFILE} show LIB=$$x; done
@@ -25,15 +28,3 @@ show:
 	@echo 'LIB=${LIB} X_LIBS:M$${LIB$${LIB:tu}} is "${X_LIBS:M${LIB${LIB:tu}}}"'
 	@echo 'LIB=${LIB} X_LIBS:M*/lib$${LIB}.a is "${X_LIBS:M*/lib${LIB}.a}"'
 	@echo 'LIB=${LIB} X_LIBS:M*/lib$${LIB}.a:tu is "${X_LIBS:M*/lib${LIB}.a:tu}"'
-
-LIST= One Two Three Four five six seven
-
-check-cclass:
-	@echo Upper=${LIST:M[A-Z]*}
-	@echo Lower=${LIST:M[^A-Z]*}
-	@echo nose=${LIST:M[^s]*[ex]}
-
-# Before 2020-06-13, this expression took quite a long time in Str_Match,
-# calling itself 601080390 times for 16 asterisks.
-slow: .PHONY
-	@:;: ${:U****************:M****************b:Q}
diff --git a/unit-tests/modmisc.exp b/unit-tests/modmisc.exp
index 619ae3c7fc11..94f131052fdc 100644
--- a/unit-tests/modmisc.exp
+++ b/unit-tests/modmisc.exp
@@ -1,3 +1,4 @@
+make: Unknown modifier '$'
 path=':/bin:/tmp::/:.:/no/such/dir:.'
 path='/bin:/tmp:/:/no/such/dir'
 path='/bin:/tmp:/:/no/such/dir'
@@ -7,45 +8,14 @@ path_/usr/xbin=/opt/xbin/
 paths=/bin /tmp / /no/such/dir /opt/xbin
 PATHS=/BIN /TMP / /NO/SUCH/DIR /OPT/XBIN
 The answer is 42
-dirname of 'a/b/c def a.b.c a.b/c a a.a .gitignore a a.a' is 'a/b . . a.b . . . . .'
-basename of 'a/b/c def a.b.c a.b/c a a.a .gitignore a a.a' is 'c def a.b.c c a a.a .gitignore a a.a'
-suffix of 'a/b/c def a.b.c a.b/c a a.a .gitignore a a.a' is 'c b/c a gitignore a'
-root of 'a/b/c def a.b.c a.b/c a a.a .gitignore a a.a' is 'a/b/c def a.b a a a  a a'
 S:
 C:
 @:
 S:empty
 C:empty
 @:
-:a b b c:
-:a b b c:
-: b c:
-:a c:
-:x__ 3 x__ 3:
-:a b b c:
-:a b b c:
-: b c:
-make: RE substitution error: (details omitted)
-make: Unclosed substitution for  (, missing)
-:C,word,____,:Q}:
-:a c:
-:x__ 3 x__ 3:
-:+one+ +two+ +three+:
-mod-at-resolve:w1d2d3w w2i3w w1i2d3 2i${RES3}w w1d2d3 2i${RES3} 1i${RES2}w:
-mod-subst-dollar:$1:
-mod-subst-dollar:$2:
-mod-subst-dollar:$3:
-mod-subst-dollar:$4:
-mod-subst-dollar:$5:
-mod-subst-dollar:$6:
-mod-subst-dollar:$7:
-mod-subst-dollar:$8:
-mod-subst-dollar:U8:
-mod-subst-dollar:$$$$:
-mod-loop-dollar:1:
-mod-loop-dollar:${word}:
-mod-loop-dollar:$3$:
-mod-loop-dollar:$${word}$:
-mod-loop-dollar:$$5$$:
-mod-loop-dollar:$$${word}$$:
+mod-quote: new
+
+line
+mod-break-many-words: 500
 exit status 0
diff --git a/unit-tests/modmisc.mk b/unit-tests/modmisc.mk
index d0c334342934..f2977da8a0ba 100644
--- a/unit-tests/modmisc.mk
+++ b/unit-tests/modmisc.mk
@@ -1,4 +1,4 @@
-# $Id: modmisc.mk,v 1.1.1.3 2020/07/09 22:35:19 sjg Exp $
+# $Id: modmisc.mk,v 1.1.1.15 2020/08/23 15:52:08 sjg Exp $
 #
 # miscellaneous modifier tests
 
@@ -15,13 +15,15 @@ MOD_HOMES=S,/home/,/homes/,
 MOD_OPT=@d@$${exists($$d):?$$d:$${d:S,/usr,/opt,}}@
 MOD_SEP=S,:, ,g
 
-all:	modvar modvarloop modsysv mod-HTE emptyvar undefvar
-all:	mod-S mod-C mod-at-varname mod-at-resolve
-all:	mod-subst-dollar mod-loop-dollar
+all:	modvar modvarloop modsysv emptyvar undefvar
+all:	mod-quote
+all:	mod-break-many-words
 
+# See also sysv.mk.
 modsysv:
 	@echo "The answer is ${libfoo.a:L:libfoo.a=42}"
 
+# Demonstrates modifiers that are given indirectly from a variable.
 modvar:
 	@echo "path='${path}'"
 	@echo "path='${path:${MOD_NODOT}}'"
@@ -39,13 +41,6 @@ modvarloop:
 	@echo "paths=${paths}"
 	@echo "PATHS=${paths:tu}"
 
-PATHNAMES=	a/b/c def a.b.c a.b/c a a.a .gitignore a a.a
-mod-HTE:
-	@echo "dirname of '"${PATHNAMES:Q}"' is '"${PATHNAMES:H:Q}"'"
-	@echo "basename of '"${PATHNAMES:Q}"' is '"${PATHNAMES:T:Q}"'"
-	@echo "suffix of '"${PATHNAMES:Q}"' is '"${PATHNAMES:E:Q}"'"
-	@echo "root of '"${PATHNAMES:Q}"' is '"${PATHNAMES:R:Q}"'"
-
 # When a modifier is applied to the "" variable, the result is discarded.
 emptyvar:
 	@echo S:${:S,^$,empty,}
@@ -61,67 +56,37 @@ undefvar:
 	@echo C:${:U:C,^$,empty,}
 	@echo @:${:U:@var@empty@}
 
-mod-S:
-	@echo :${:Ua b b c:S,a b,,:Q}:
-	@echo :${:Ua b b c:S,a b,,1:Q}:
-	@echo :${:Ua b b c:S,a b,,W:Q}:
-	@echo :${:Ua b b c:S,b,,g:Q}:
-	@echo :${:U1 2 3 1 2 3:S,1 2,___,Wg:S,_,x,:Q}:
 
-mod-C:
-	@echo :${:Ua b b c:C,a b,,:Q}:
-	@echo :${:Ua b b c:C,a b,,1:Q}:
-	@echo :${:Ua b b c:C,a b,,W:Q}:
-	@echo :${:Uword1 word2:C,****,____,g:C,word,____,:Q}:
-	@echo :${:Ua b b c:C,b,,g:Q}:
-	@echo :${:U1 2 3 1 2 3:C,1 2,___,Wg:C,_,x,:Q}:
+mod-quote:
+	@echo $@: new${.newline:Q}${.newline:Q}line
 
-# In the :@ modifier, the name of the loop variable can even be generated
-# dynamically.  There's no practical use-case for this, and hopefully nobody
-# will ever depend on this, but technically it's possible.
-mod-at-varname:
-	@echo :${:Uone two three:@${:Ubar:S,b,v,}@+${var}+@:Q}:
+# Cover the bmake_realloc in brk_string.
+mod-break-many-words:
+	@echo $@: ${UNDEF:U:range=500:[#]}
 
-# The :@ modifier resolves the variables a little more often than expected.
-# In particular, it resolves _all_ variables from the context, and not only
-# the loop variable (in this case v).
+# To apply a modifier indirectly via another variable, the whole
+# modifier must be put into a single variable.
+.if ${value:L:${:US}${:U,value,replacement,}} != "S,value,replacement,}"
+.warning unexpected
+.endif
+
+# Adding another level of indirection (the 2 nested :U expressions) helps.
+.if ${value:L:${:U${:US}${:U,value,replacement,}}} != "replacement"
+.warning unexpected
+.endif
+
+# Multiple indirect modifiers can be applied one after another as long as
+# they are separated with colons.
+.if ${value:L:${:US,a,A,}:${:US,e,E,}} != "vAluE"
+.warning unexpected
+.endif
+
+# An indirect variable that evaluates to the empty string is allowed though.
+# This makes it possible to define conditional modifiers, like this:
 #
-# The d means direct reference, the i means indirect reference.
-RESOLVE=	${RES1} $${RES1}
-RES1=		1d${RES2} 1i$${RES2}
-RES2=		2d${RES3} 2i$${RES3}
-RES3=		3
+# M.little-endian=	S,1234,4321,
+# M.big-endian=		# none
+.if ${value:L:${:Dempty}S,a,A,} != "vAlue"
+.warning unexpected
+.endif
 
-mod-at-resolve:
-	@echo $@:${RESOLVE:@v@w${v}w@:Q}:
-
-# No matter how many dollar characters there are, they all get merged
-# into a single dollar by the :S modifier.
-mod-subst-dollar:
-	@echo $@:${:U1:S,^,$,:Q}:
-	@echo $@:${:U2:S,^,$$,:Q}:
-	@echo $@:${:U3:S,^,$$$,:Q}:
-	@echo $@:${:U4:S,^,$$$$,:Q}:
-	@echo $@:${:U5:S,^,$$$$$,:Q}:
-	@echo $@:${:U6:S,^,$$$$$$,:Q}:
-	@echo $@:${:U7:S,^,$$$$$$$,:Q}:
-	@echo $@:${:U8:S,^,$$$$$$$$,:Q}:
-# This generates no dollar at all:
-	@echo $@:${:UU8:S,^,${:U$$$$$$$$},:Q}:
-# Here is an alternative way to generate dollar characters.
-# It's unexpectedly complicated though.
-	@echo $@:${:U:range=5:ts\x24:C,[0-9],,g:Q}:
-
-# Demonstrate that it is possible to generate dollar characters using the
-# :@ modifier.
-#
-# These are edge cases that could have resulted in a parse error as well
-# since the $@ at the end could have been interpreted as a variable, which
-# would mean a missing closing @ delimiter.
-mod-loop-dollar:
-	@echo $@:${:U1:@word@${word}$@:Q}:
-	@echo $@:${:U2:@word@$${word}$$@:Q}:
-	@echo $@:${:U3:@word@$$${word}$$$@:Q}:
-	@echo $@:${:U4:@word@$$$${word}$$$$@:Q}:
-	@echo $@:${:U5:@word@$$$$${word}$$$$$@:Q}:
-	@echo $@:${:U6:@word@$$$$$${word}$$$$$$@:Q}:
diff --git a/unit-tests/modorder.exp b/unit-tests/modorder.exp
deleted file mode 100644
index 8e0aad2e2027..000000000000
--- a/unit-tests/modorder.exp
+++ /dev/null
@@ -1,12 +0,0 @@
-LIST      = one two three four five six seven eight nine ten
-LIST:O    = eight five four nine one seven six ten three two
-LIST:Or    = two three ten six seven one nine four five eight
-LIST:Ox   = Ok
-LIST:O:Ox = Ok
-LISTX     = Ok
-LISTSX    = Ok
-make: Bad modifier `:OX' for LIST
-BADMOD 1  = }
-make: Bad modifier `:OxXX' for LIST
-BADMOD 2  = XX}
-exit status 0
diff --git a/unit-tests/modorder.mk b/unit-tests/modorder.mk
deleted file mode 100644
index 89e64b43c57c..000000000000
--- a/unit-tests/modorder.mk
+++ /dev/null
@@ -1,24 +0,0 @@
-# $NetBSD: modorder.mk,v 1.3 2020/06/09 01:48:17 sjg Exp $
-
-LIST=		one two three four five six seven eight nine ten
-LISTX=		${LIST:Ox}
-LISTSX:=	${LIST:Ox}
-TEST_RESULT= && echo Ok || echo Failed
-
-# unit-tests have to produce the same results on each run
-# so we cannot actually include :Ox output.
-all:
-	@echo "LIST      = ${LIST}"
-	@echo "LIST:O    = ${LIST:O}"
-	@echo "LIST:Or    = ${LIST:Or}"
-	# Note that 1 in every 10! trials two independently generated
-	# randomized orderings will be the same.  The test framework doesn't
-	# support checking probabilistic output, so we accept that each of the
-	# 3 :Ox tests will incorrectly fail with probability 2.756E-7, which
-	# lets the whole test fail once in 1.209.600 runs, on average.
-	@echo "LIST:Ox   = `test '${LIST:Ox}' != '${LIST:Ox}' ${TEST_RESULT}`"
-	@echo "LIST:O:Ox = `test '${LIST:O:Ox}' != '${LIST:O:Ox}' ${TEST_RESULT}`"
-	@echo "LISTX     = `test '${LISTX}' != '${LISTX}' ${TEST_RESULT}`"
-	@echo "LISTSX    = `test '${LISTSX}' = '${LISTSX}' ${TEST_RESULT}`"
-	@echo "BADMOD 1  = ${LIST:OX}"
-	@echo "BADMOD 2  = ${LIST:OxXX}"
diff --git a/unit-tests/modts.exp b/unit-tests/modts.exp
index 338964963a86..5db79fc96586 100644
--- a/unit-tests/modts.exp
+++ b/unit-tests/modts.exp
@@ -1,34 +1,3 @@
-LIST="one two three four five six"
-LIST:ts,="one,two,three,four,five,six"
-LIST:ts/:tu="ONE/TWO/THREE/FOUR/FIVE/SIX"
-LIST:ts::tu="ONE:TWO:THREE:FOUR:FIVE:SIX"
-LIST:ts:tu="ONETWOTHREEFOURFIVESIX"
-LIST:tu:ts/="ONE/TWO/THREE/FOUR/FIVE/SIX"
-LIST:ts:="one:two:three:four:five:six"
-LIST:ts="onetwothreefourfivesix"
-LIST:ts:S/two/2/="one2threefourfivesix"
-LIST:S/two/2/:ts="one2threefourfivesix"
-LIST:ts/:S/two/2/="one/2/three/four/five/six"
-Pretend the '/' in '/n' etc. below are back-slashes.
-LIST:ts/n="one
-two
-three
-four
-five
-six"
-LIST:ts/t="one	two	three	four	five	six"
-LIST:ts/012:tu="ONE
-TWO
-THREE
-FOUR
-FIVE
-SIX"
-LIST:ts/xa:tu="ONE
-TWO
-THREE
-FOUR
-FIVE
-SIX"
 make: Bad modifier `:tx' for LIST
 LIST:tx="}"
 make: Bad modifier `:ts\X' for LIST
@@ -36,4 +5,10 @@ LIST:ts/x:tu="\X:tu}"
 FU_mod-ts="a/b/cool"
 FU_mod-ts:ts:T="cool" == cool?
 B.${AAA:ts}="Baaa" == Baaa?
+:ts :S => aaxBbxaaxbbxaaxbb
+:ts :S space    => axa a axc
+:ts :S space :M => axaxaxaxc
+:ts :S       => axa a axc
+:ts :S :@    => axa a axc
+:ts :S :@ :M => axaxaxaxc
 exit status 0
diff --git a/unit-tests/modts.mk b/unit-tests/modts.mk
index e66dc25a2a02..3219538d5e4a 100644
--- a/unit-tests/modts.mk
+++ b/unit-tests/modts.mk
@@ -7,7 +7,7 @@ FU_mod-ts = a / b / cool
 AAA= a a a
 B.aaa= Baaa
 
-all:   mod-ts
+all:   mod-ts mod-ts-space
 
 # Use print or printf iff they are builtin.
 # XXX note that this causes problems, when make decides 
@@ -21,24 +21,27 @@ PRINT= echo
 .endif
 
 mod-ts:
-	@echo 'LIST="${LIST}"'
-	@echo 'LIST:ts,="${LIST:ts,}"'
-	@echo 'LIST:ts/:tu="${LIST:ts/:tu}"'
-	@echo 'LIST:ts::tu="${LIST:ts::tu}"'
-	@echo 'LIST:ts:tu="${LIST:ts:tu}"'
-	@echo 'LIST:tu:ts/="${LIST:tu:ts/}"'
-	@echo 'LIST:ts:="${LIST:ts:}"'
-	@echo 'LIST:ts="${LIST:ts}"'
-	@echo 'LIST:ts:S/two/2/="${LIST:ts:S/two/2/}"'
-	@echo 'LIST:S/two/2/:ts="${LIST:S/two/2/:ts}"'
-	@echo 'LIST:ts/:S/two/2/="${LIST:ts/:S/two/2/}"'
-	@echo "Pretend the '/' in '/n' etc. below are back-slashes."
-	@${PRINT} 'LIST:ts/n="${LIST:ts\n}"'
-	@${PRINT} 'LIST:ts/t="${LIST:ts\t}"'
-	@${PRINT} 'LIST:ts/012:tu="${LIST:ts\012:tu}"'
-	@${PRINT} 'LIST:ts/xa:tu="${LIST:ts\xa:tu}"'
 	@${PRINT} 'LIST:tx="${LIST:tx}"'
 	@${PRINT} 'LIST:ts/x:tu="${LIST:ts\X:tu}"'
 	@${PRINT} 'FU_$@="${FU_${@:ts}:ts}"'
 	@${PRINT} 'FU_$@:ts:T="${FU_${@:ts}:ts:T}" == cool?'
 	@${PRINT} 'B.$${AAA:ts}="${B.${AAA:ts}}" == Baaa?'
+
+mod-ts-space:
+	# After the :ts modifier, the whole string is interpreted as a single
+	# word since all spaces have been replaced with x.
+	@${PRINT} ':ts :S => '${aa bb aa bb aa bb:L:tsx:S,b,B,:Q}
+
+	# The :ts modifier also applies to word separators that are added
+	# afterwards.
+	@${PRINT} ':ts :S space    => '${a ababa c:L:tsx:S,b, ,g:Q}
+	@${PRINT} ':ts :S space :M => '${a ababa c:L:tsx:S,b, ,g:M*:Q}
+
+	# Not all modifiers behave this way though.  Some of them always use
+	# a space as word separator instead of the :ts separator.
+	# This seems like an oversight during implementation.
+	@${PRINT} ':ts :S       => '${a ababa c:L:tsx:S,b, ,g:Q}
+	@${PRINT} ':ts :S :@    => '${a ababa c:L:tsx:S,b, ,g:@v@${v}@:Q}
+
+	# A final :M* modifier applies the :ts separator again, though.
+	@${PRINT} ':ts :S :@ :M => '${a ababa c:L:tsx:S,b, ,g:@v@${v}@:M*:Q}
diff --git a/unit-tests/opt-backwards.exp b/unit-tests/opt-backwards.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-backwards.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-backwards.mk b/unit-tests/opt-backwards.mk
new file mode 100644
index 000000000000..bc0b1eff8530
--- /dev/null
+++ b/unit-tests/opt-backwards.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-backwards.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -B command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-chdir.exp b/unit-tests/opt-chdir.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-chdir.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-chdir.mk b/unit-tests/opt-chdir.mk
new file mode 100644
index 000000000000..8735fddbef9e
--- /dev/null
+++ b/unit-tests/opt-chdir.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-chdir.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -C command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-debug-g1.exp b/unit-tests/opt-debug-g1.exp
new file mode 100755
index 000000000000..d6d014a0353f
--- /dev/null
+++ b/unit-tests/opt-debug-g1.exp
@@ -0,0 +1,15 @@
+#*** Input graph:
+# all, made UNMADE, type OP_DEPENDS, flags none
+# made-target, made UNMADE, type OP_DEPENDS, flags none
+# made-target-no-sources, made UNMADE, type OP_DEPENDS, flags none
+# made-source, made UNMADE, type OP_DEPENDS, flags none
+# unmade-target, made UNMADE, type OP_DEPENDS, flags none
+# unmade-sources, made UNMADE, type none, flags none
+# unmade-target-no-sources, made UNMADE, type OP_DEPENDS, flags none
+
+
+#
+#   Files that are only sources:
+#	unmade-sources [unmade-sources] 
+#*** Transformations:
+exit status 0
diff --git a/unit-tests/opt-debug-g1.mk b/unit-tests/opt-debug-g1.mk
new file mode 100755
index 000000000000..3104fbf91bc1
--- /dev/null
+++ b/unit-tests/opt-debug-g1.mk
@@ -0,0 +1,19 @@
+# $NetBSD: opt-debug-g1.mk,v 1.1 2020/08/27 19:00:17 rillig Exp $
+#
+# Tests for the -dg1 command line option, which prints the input
+# graph before making anything.
+
+all: made-target made-target-no-sources
+
+made-target: made-source
+
+made-source:
+
+made-target-no-sources:
+
+unmade-target: unmade-sources
+
+unmade-target-no-sources:
+
+all:
+	@:;
diff --git a/unit-tests/opt-debug.exp b/unit-tests/opt-debug.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-debug.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-debug.mk b/unit-tests/opt-debug.mk
new file mode 100644
index 000000000000..afd740a6caab
--- /dev/null
+++ b/unit-tests/opt-debug.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-debug.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -d command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-define.exp b/unit-tests/opt-define.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-define.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-define.mk b/unit-tests/opt-define.mk
new file mode 100644
index 000000000000..ce0516ba44bc
--- /dev/null
+++ b/unit-tests/opt-define.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-define.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -D command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-env.exp b/unit-tests/opt-env.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-env.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-env.mk b/unit-tests/opt-env.mk
new file mode 100644
index 000000000000..32e95ef41f5a
--- /dev/null
+++ b/unit-tests/opt-env.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-env.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -e command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-file.exp b/unit-tests/opt-file.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-file.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-file.mk b/unit-tests/opt-file.mk
new file mode 100644
index 000000000000..86bc100bebc2
--- /dev/null
+++ b/unit-tests/opt-file.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-file.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -f command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-ignore.exp b/unit-tests/opt-ignore.exp
new file mode 100644
index 000000000000..265e143e3471
--- /dev/null
+++ b/unit-tests/opt-ignore.exp
@@ -0,0 +1,12 @@
+dependency 1
+dependency 2
+dependency 3
+other 1
+other 2
+main 1
+main 2
+*** Error code 1 (ignored)
+*** Error code 7 (ignored)
+*** Error code 1 (ignored)
+*** Error code 1 (ignored)
+exit status 0
diff --git a/unit-tests/opt-ignore.mk b/unit-tests/opt-ignore.mk
new file mode 100644
index 000000000000..6d6d8dc3a339
--- /dev/null
+++ b/unit-tests/opt-ignore.mk
@@ -0,0 +1,30 @@
+# $NetBSD: opt-ignore.mk,v 1.3 2020/08/23 14:28:04 rillig Exp $
+#
+# Tests for the -i command line option, which ignores the exit status of the
+# shell commands, and just continues with the next command, even from the same
+# target.
+#
+# Is there a situation in which this option is useful?
+#
+# Why are the "Error code" lines all collected at the bottom of the output
+# file, where they cannot be related to the individual shell commands that
+# failed?
+
+all: dependency other
+
+dependency:
+	@echo dependency 1
+	@false
+	@echo dependency 2
+	@:; exit 7
+	@echo dependency 3
+
+other:
+	@echo other 1
+	@false
+	@echo other 2
+
+all:
+	@echo main 1
+	@false
+	@echo main 2
diff --git a/unit-tests/opt-include-dir.exp b/unit-tests/opt-include-dir.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-include-dir.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-include-dir.mk b/unit-tests/opt-include-dir.mk
new file mode 100644
index 000000000000..d61a6c979ea5
--- /dev/null
+++ b/unit-tests/opt-include-dir.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-include-dir.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -I command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-jobs-internal.exp b/unit-tests/opt-jobs-internal.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-jobs-internal.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-jobs-internal.mk b/unit-tests/opt-jobs-internal.mk
new file mode 100644
index 000000000000..5426807ca98b
--- /dev/null
+++ b/unit-tests/opt-jobs-internal.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-jobs-internal.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the (intentionally undocumented) -J command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-jobs.exp b/unit-tests/opt-jobs.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-jobs.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-jobs.mk b/unit-tests/opt-jobs.mk
new file mode 100644
index 000000000000..7d54d08a8421
--- /dev/null
+++ b/unit-tests/opt-jobs.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-jobs.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -j command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-keep-going.exp b/unit-tests/opt-keep-going.exp
new file mode 100644
index 000000000000..de1b1ae582f4
--- /dev/null
+++ b/unit-tests/opt-keep-going.exp
@@ -0,0 +1,6 @@
+dependency 1
+other 1
+*** Error code 1 (continuing)
+*** Error code 1 (continuing)
+`all' not remade because of errors.
+exit status 0
diff --git a/unit-tests/opt-keep-going.mk b/unit-tests/opt-keep-going.mk
new file mode 100644
index 000000000000..1a2124ccb5f1
--- /dev/null
+++ b/unit-tests/opt-keep-going.mk
@@ -0,0 +1,24 @@
+# $NetBSD: opt-keep-going.mk,v 1.3 2020/08/23 14:28:04 rillig Exp $
+#
+# Tests for the -k command line option, which stops building a target as soon
+# as an error is detected, but continues building the other, independent
+# targets, as far as possible.
+
+all: dependency other
+
+dependency:
+	@echo dependency 1
+	@false
+	@echo dependency 2
+	@:; exit 7
+	@echo dependency 3
+
+other:
+	@echo other 1
+	@false
+	@echo other 2
+
+all:
+	@echo main 1
+	@false
+	@echo main 2
diff --git a/unit-tests/opt-m-include-dir.exp b/unit-tests/opt-m-include-dir.exp
new file mode 100644
index 000000000000..bce0e4be05ac
--- /dev/null
+++ b/unit-tests/opt-m-include-dir.exp
@@ -0,0 +1,2 @@
+ok
+exit status 0
diff --git a/unit-tests/opt-m-include-dir.mk b/unit-tests/opt-m-include-dir.mk
new file mode 100644
index 000000000000..6e0801390395
--- /dev/null
+++ b/unit-tests/opt-m-include-dir.mk
@@ -0,0 +1,61 @@
+# $NetBSD: opt-m-include-dir.mk,v 1.4 2020/09/01 20:14:34 rillig Exp $
+#
+# Tests for the -m command line option, which adds a directory to the
+# search path for the .include <...> directive.
+#
+# The .../canary.mk special argument starts searching in the current
+# directory and walks towards the file system root, until it finds a
+# directory that contains a file called canary.mk.
+#
+# To set up this scenario, the file step2.mk is created deep in a hierarchy
+# of subdirectories.  Another file called opt-m-step3.mk is created a few
+# steps up in the directory hierarchy, serving as the canary file.
+#
+# Next to the canary file, there is opt-m-step3.mk.  This file is found
+# by mentioning its simple name in an .include directive.  It defines the
+# target "step2" that is needed by "step2.mk".
+
+.if ${.PARSEFILE:T} == "opt-m-include-dir.mk"
+
+# Set up the other files needed for this test.
+
+TEST_DIR:=	${.PARSEFILE:R}.tmp/sub/sub/sub/workdir
+CANARY_FILE:=	${.PARSEFILE:R}.tmp/sub/opt-m-canary.mk
+ACTUAL_FILE:=	${.PARSEFILE:R}.tmp/sub/opt-m-step3.mk
+
+_!=	mkdir -p ${TEST_DIR}
+_!=	> ${CANARY_FILE}
+_!=	cp ${MAKEFILE} ${TEST_DIR}/step2.mk
+_!=	cp ${MAKEFILE} ${ACTUAL_FILE}
+
+step1:
+	@${.MAKE} -C ${TEST_DIR} -f step2.mk step2
+
+.END:
+	@rm -rf ${MAKEFILE:R}.tmp
+
+.elif ${.PARSEFILE:T} == "step2.mk"
+
+# This is the file deep in the directory hierarchy.  It sets up the
+# search path for the .include <...> directive and then includes a
+# single file from that search path.
+
+# This option adds .tmp/sub to the search path for .include <...>.
+.MAKEFLAGS: -m .../opt-m-canary.mk
+
+# This option does not add any directory to the search path since the
+# canary file does not exist.
+.MAKEFLAGS: -m .../does-not-exist
+
+.include <opt-m-step3.mk>
+
+.elif ${.PARSEFILE:T} == "opt-m-step3.mk"
+
+# This file is included by step2.mk.
+
+step2:
+	@echo ok
+
+.else
+.  error
+.endif
diff --git a/unit-tests/opt-no-action-at-all.exp b/unit-tests/opt-no-action-at-all.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-no-action-at-all.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-no-action-at-all.mk b/unit-tests/opt-no-action-at-all.mk
new file mode 100644
index 000000000000..6ab385946691
--- /dev/null
+++ b/unit-tests/opt-no-action-at-all.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-no-action-at-all.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -N command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-no-action.exp b/unit-tests/opt-no-action.exp
new file mode 100644
index 000000000000..381d68e2f2d9
--- /dev/null
+++ b/unit-tests/opt-no-action.exp
@@ -0,0 +1,13 @@
+command during parsing
+echo '.BEGIN: hidden command'
+echo '.BEGIN: run always'
+.BEGIN: run always
+echo 'main: hidden command'
+echo 'main: run always'
+main: run always
+run-always: hidden command
+run-always: run always
+echo '.END: hidden command'
+echo '.END: run always'
+.END: run always
+exit status 0
diff --git a/unit-tests/opt-no-action.mk b/unit-tests/opt-no-action.mk
new file mode 100644
index 000000000000..32b3b1564acb
--- /dev/null
+++ b/unit-tests/opt-no-action.mk
@@ -0,0 +1,33 @@
+# $NetBSD: opt-no-action.mk,v 1.3 2020/08/19 05:25:26 rillig Exp $
+#
+# Tests for the -n command line option, which runs almost no commands.
+# It just outputs them, to be inspected by human readers.
+# Only commands that are in a .MAKE target or prefixed by '+' are run.
+
+# This command cannot be prevented from being run since it is used at parse
+# time, and any later variable assignments may depend on its result.
+!=	echo 'command during parsing' 1>&2; echo
+
+all: main
+all: run-always
+
+# Both of these commands are printed, but only the '+' command is run.
+.BEGIN:
+	@echo '$@: hidden command'
+	@+echo '$@: run always'
+
+# Both of these commands are printed, but only the '+' command is run.
+main:
+	@echo '$@: hidden command'
+	@+echo '$@: run always'
+
+# None of these commands is printed, but both are run, because this target
+# depends on the special source ".MAKE".
+run-always: .MAKE
+	@echo '$@: hidden command'
+	@+echo '$@: run always'
+
+# Both of these commands are printed, but only the '+' command is run.
+.END:
+	@echo '$@: hidden command'
+	@+echo '$@: run always'
diff --git a/unit-tests/opt-query.exp b/unit-tests/opt-query.exp
new file mode 100644
index 000000000000..38025dcf4d3a
--- /dev/null
+++ b/unit-tests/opt-query.exp
@@ -0,0 +1,2 @@
+command during parsing
+exit status 1
diff --git a/unit-tests/opt-query.mk b/unit-tests/opt-query.mk
new file mode 100644
index 000000000000..04e605991140
--- /dev/null
+++ b/unit-tests/opt-query.mk
@@ -0,0 +1,24 @@
+# $NetBSD: opt-query.mk,v 1.3 2020/08/19 05:13:18 rillig Exp $
+#
+# Tests for the -q command line option.
+#
+# The -q option only looks at the dependencies between the targets.
+# None of the commands in the targets are run, not even those that are
+# prefixed with '+'.
+
+# This command cannot be prevented from being run since it is used at parse
+# time, and any later variable assignments may depend on its result.
+!=	echo 'command during parsing' 1>&2; echo
+
+# None of these commands are run.
+.BEGIN:
+	@echo '$@: hidden command'
+	@+echo '$@: run always'
+
+# None of these commands are run.
+all:
+	@echo '$@: hidden command'
+	@+echo '$@: run always'
+
+# The exit status 1 is because the "all" target has to be made, that is,
+# it is not up-to-date.
diff --git a/unit-tests/opt-raw.exp b/unit-tests/opt-raw.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-raw.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-raw.mk b/unit-tests/opt-raw.mk
new file mode 100644
index 000000000000..d3591bb99dab
--- /dev/null
+++ b/unit-tests/opt-raw.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-raw.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -r command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-silent.exp b/unit-tests/opt-silent.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-silent.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-silent.mk b/unit-tests/opt-silent.mk
new file mode 100644
index 000000000000..7822d46ac48a
--- /dev/null
+++ b/unit-tests/opt-silent.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-silent.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -s command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-touch.exp b/unit-tests/opt-touch.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-touch.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-touch.mk b/unit-tests/opt-touch.mk
new file mode 100644
index 000000000000..5093c5cad6ac
--- /dev/null
+++ b/unit-tests/opt-touch.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-touch.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -t command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-tracefile.exp b/unit-tests/opt-tracefile.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-tracefile.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-tracefile.mk b/unit-tests/opt-tracefile.mk
new file mode 100644
index 000000000000..b62392ca913c
--- /dev/null
+++ b/unit-tests/opt-tracefile.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-tracefile.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -T command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-var-expanded.exp b/unit-tests/opt-var-expanded.exp
new file mode 100644
index 000000000000..4fcc0b99ae1b
--- /dev/null
+++ b/unit-tests/opt-var-expanded.exp
@@ -0,0 +1,3 @@
+other value $$
+value
+exit status 0
diff --git a/unit-tests/opt-var-expanded.mk b/unit-tests/opt-var-expanded.mk
new file mode 100644
index 000000000000..0b4088a82082
--- /dev/null
+++ b/unit-tests/opt-var-expanded.mk
@@ -0,0 +1,6 @@
+# $NetBSD: opt-var-expanded.mk,v 1.3 2020/08/23 14:28:04 rillig Exp $
+#
+# Tests for the -v command line option.
+
+VAR=	other ${VALUE} $$$$
+VALUE=	value
diff --git a/unit-tests/opt-var-literal.exp b/unit-tests/opt-var-literal.exp
new file mode 100644
index 000000000000..e7f653b769b9
--- /dev/null
+++ b/unit-tests/opt-var-literal.exp
@@ -0,0 +1,3 @@
+other ${VALUE} $$$$
+value
+exit status 0
diff --git a/unit-tests/opt-var-literal.mk b/unit-tests/opt-var-literal.mk
new file mode 100644
index 000000000000..a819e7537105
--- /dev/null
+++ b/unit-tests/opt-var-literal.mk
@@ -0,0 +1,6 @@
+# $NetBSD: opt-var-literal.mk,v 1.3 2020/08/23 14:28:04 rillig Exp $
+#
+# Tests for the -V command line option.
+
+VAR=	other ${VALUE} $$$$
+VALUE=	value
diff --git a/unit-tests/opt-warnings-as-errors.exp b/unit-tests/opt-warnings-as-errors.exp
new file mode 100644
index 000000000000..bd54bb673f08
--- /dev/null
+++ b/unit-tests/opt-warnings-as-errors.exp
@@ -0,0 +1,7 @@
+make: "opt-warnings-as-errors.mk" line 5: warning: message 1
+make: parsing warnings being treated as errors
+make: "opt-warnings-as-errors.mk" line 6: warning: message 2
+parsing continues
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/opt-warnings-as-errors.mk b/unit-tests/opt-warnings-as-errors.mk
new file mode 100644
index 000000000000..905753410db0
--- /dev/null
+++ b/unit-tests/opt-warnings-as-errors.mk
@@ -0,0 +1,11 @@
+# $NetBSD: opt-warnings-as-errors.mk,v 1.3 2020/08/23 14:28:04 rillig Exp $
+#
+# Tests for the -W command line option, which turns warnings into errors.
+
+.warning message 1
+.warning message 2
+
+_!=	echo 'parsing continues' 1>&2
+
+all:
+	@:;
diff --git a/unit-tests/opt-where-am-i.exp b/unit-tests/opt-where-am-i.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-where-am-i.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-where-am-i.mk b/unit-tests/opt-where-am-i.mk
new file mode 100644
index 000000000000..9158a598174c
--- /dev/null
+++ b/unit-tests/opt-where-am-i.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-where-am-i.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -w command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt-x-reduce-exported.exp b/unit-tests/opt-x-reduce-exported.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt-x-reduce-exported.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt-x-reduce-exported.mk b/unit-tests/opt-x-reduce-exported.mk
new file mode 100644
index 000000000000..7ee8e7c7eff0
--- /dev/null
+++ b/unit-tests/opt-x-reduce-exported.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt-x-reduce-exported.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the -x command line option.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/opt.exp b/unit-tests/opt.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/opt.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/opt.mk b/unit-tests/opt.mk
new file mode 100644
index 000000000000..eae430965df7
--- /dev/null
+++ b/unit-tests/opt.mk
@@ -0,0 +1,8 @@
+# $NetBSD: opt.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the command line options.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/phony-end.exp b/unit-tests/phony-end.exp
index c3c517ccc25c..f1bff4ef6d5e 100644
--- a/unit-tests/phony-end.exp
+++ b/unit-tests/phony-end.exp
@@ -1,5 +1,5 @@
 .TARGET="phony" .PREFIX="phony" .IMPSRC=""
-.TARGET="all" .PREFIX="all" .IMPSRC="phony"
+.TARGET="all" .PREFIX="all" .IMPSRC=""
 .TARGET="ok" .PREFIX="ok" .IMPSRC=""
 .TARGET="also.ok" .PREFIX="also.ok" .IMPSRC=""
 .TARGET="bug" .PREFIX="bug" .IMPSRC=""
diff --git a/unit-tests/posix1.mk b/unit-tests/posix1.mk
index 50b0a63ee696..f5d8e21678ea 100644
--- a/unit-tests/posix1.mk
+++ b/unit-tests/posix1.mk
@@ -1,4 +1,4 @@
-# $NetBSD: posix1.mk,v 1.3 2014/08/30 22:21:08 sjg Exp $
+# $NetBSD: posix1.mk,v 1.4 2020/08/10 18:19:58 rillig Exp $
 
 # Keep the default suffixes from interfering, just in case.
 .SUFFIXES:
@@ -9,6 +9,8 @@ all:	line-continuations suffix-substitution localvars
 .BEGIN: clean
 clean:
 	@rm -f lib.a dir/* dummy obj*
+.END:
+	@rm -f lib.a dir/* dummy obj*
 
 #
 # Line continuations
diff --git a/unit-tests/recursive.exp b/unit-tests/recursive.exp
new file mode 100644
index 000000000000..bb5db75a474c
--- /dev/null
+++ b/unit-tests/recursive.exp
@@ -0,0 +1,5 @@
+make: "recursive.mk" line 34: Unclosed variable "MISSING_PAREN"
+make: "recursive.mk" line 35: Unclosed variable "MISSING_BRACE"
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/recursive.mk b/unit-tests/recursive.mk
new file mode 100644
index 000000000000..bc5a2817b333
--- /dev/null
+++ b/unit-tests/recursive.mk
@@ -0,0 +1,37 @@
+# $NetBSD: recursive.mk,v 1.2 2020/08/06 05:52:45 rillig Exp $
+#
+# In -dL mode, a variable may get expanded before it makes sense.
+# This would stop make from doing anything since the "recursive" error
+# is fatal and exits immediately.
+#
+# The purpose of evaluating that variable early was just to detect
+# whether there are unclosed variables.  It might be enough to parse the
+# variable value without VARE_WANTRES for that purpose.
+#
+# Seen in pkgsrc/x11/libXfixes, and probably many more package that use
+# GNU Automake.
+
+AM_V_lt = $(am__v_lt_$(V))
+am__v_lt_ = $(am__v_lt_$(AM_DEFAULT_VERBOSITY))
+am__v_lt_0 = --silent
+am__v_lt_1 =
+
+# On 2020-08-06, make reported: "Variable am__v_lt_ is recursive."
+libXfixes_la_LINK = ... $(AM_V_lt) ...
+
+# somewhere later ...
+AM_DEFAULT_VERBOSITY = 1
+
+
+# The purpose of the -dL flag is to detect unclosed variables.  This
+# can be achieved by just parsing the variable and not evaluating it.
+#
+# When the variable is only parsed but not evaluated, bugs in nested
+# variables are not discovered.  But these are hard to produce anyway,
+# therefore that's acceptable.  In most practical cases, the missing
+# brace would be detected directly in the line where it is produced.
+MISSING_BRACE_INDIRECT:=	${:U\${MISSING_BRACE}
+UNCLOSED = $(MISSING_PAREN
+UNCLOSED = ${MISSING_BRACE
+UNCLOSED = ${MISSING_BRACE_INDIRECT}
+
diff --git a/unit-tests/sh-dots.exp b/unit-tests/sh-dots.exp
new file mode 100755
index 000000000000..19482717087b
--- /dev/null
+++ b/unit-tests/sh-dots.exp
@@ -0,0 +1,15 @@
+first first
+hidden hidden
+make: exec(...) failed (No such file or directory)
+hidden delayed hidden
+repeated repeated
+commented commented
+*** Error code 1 (ignored)
+...	# Run the below commands later
+<normalized: ...: not found>
+commented delayed commented
+first delayed first
+repeated delayed repeated
+repeated delayed twice repeated
+*** Error code 127 (ignored)
+exit status 0
diff --git a/unit-tests/sh-dots.mk b/unit-tests/sh-dots.mk
new file mode 100755
index 000000000000..36da5bce7a53
--- /dev/null
+++ b/unit-tests/sh-dots.mk
@@ -0,0 +1,37 @@
+# $NetBSD: sh-dots.mk,v 1.1 2020/08/22 11:27:02 rillig Exp $
+#
+# Tests for the special shell command line "...", which does not run the
+# commands below it but appends them to the list of commands that are run
+# at the end.
+
+all: first hidden repeated commented
+
+# The ${.TARGET} correctly expands to the target name, even though the
+# commands are run separately from the main commands.
+first:
+	@echo first ${.TARGET}
+	...
+	@echo first delayed ${.TARGET}
+
+# The dots cannot be prefixed by the usual @-+ characters.
+# They must be written exactly as dots.
+hidden: .IGNORE
+	@echo hidden ${.TARGET}
+	@...
+	@echo hidden delayed ${.TARGET}
+
+# Since the shell command lines don't recognize '#' as comment character,
+# the "..." is not interpreted specially here.
+commented: .IGNORE
+	@echo commented ${.TARGET}
+	...	# Run the below commands later
+	@echo commented delayed ${.TARGET}
+
+# The "..." can appear more than once, even though that doesn't make sense.
+# The second "..." is a no-op.
+repeated: .IGNORE
+	@echo repeated ${.TARGET}
+	...
+	@echo repeated delayed ${.TARGET}
+	...
+	@echo repeated delayed twice ${.TARGET}
diff --git a/unit-tests/sh-jobs-error.exp b/unit-tests/sh-jobs-error.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/sh-jobs-error.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/sh-jobs-error.mk b/unit-tests/sh-jobs-error.mk
new file mode 100644
index 000000000000..febc06999366
--- /dev/null
+++ b/unit-tests/sh-jobs-error.mk
@@ -0,0 +1,9 @@
+# $NetBSD: sh-jobs-error.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for error handling in the "run in jobs mode" part of the "Shell
+# Commands" section from the manual page.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/sh-jobs.exp b/unit-tests/sh-jobs.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/sh-jobs.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/sh-jobs.mk b/unit-tests/sh-jobs.mk
new file mode 100644
index 000000000000..62172c2a0c86
--- /dev/null
+++ b/unit-tests/sh-jobs.mk
@@ -0,0 +1,9 @@
+# $NetBSD: sh-jobs.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the "run in jobs mode" part of the "Shell Commands" section
+# from the manual page.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/sh-leading-at.exp b/unit-tests/sh-leading-at.exp
new file mode 100644
index 000000000000..5ffa84690a40
--- /dev/null
+++ b/unit-tests/sh-leading-at.exp
@@ -0,0 +1,5 @@
+ok
+space after @
+echo 'echoed'
+echoed
+exit status 0
diff --git a/unit-tests/sh-leading-at.mk b/unit-tests/sh-leading-at.mk
new file mode 100644
index 000000000000..19a6e59e4e6a
--- /dev/null
+++ b/unit-tests/sh-leading-at.mk
@@ -0,0 +1,10 @@
+# $NetBSD: sh-leading-at.mk,v 1.3 2020/08/22 09:16:08 rillig Exp $
+#
+# Tests for shell commands preceded by an '@', to suppress printing
+# the command to stdout.
+
+all:
+	@
+	@echo 'ok'
+	@ echo 'space after @'
+	echo 'echoed'
diff --git a/unit-tests/sh-leading-hyphen.exp b/unit-tests/sh-leading-hyphen.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/sh-leading-hyphen.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/sh-leading-hyphen.mk b/unit-tests/sh-leading-hyphen.mk
new file mode 100644
index 000000000000..94be43495afb
--- /dev/null
+++ b/unit-tests/sh-leading-hyphen.mk
@@ -0,0 +1,9 @@
+# $NetBSD: sh-leading-hyphen.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for shell commands preceded by a '-', to ignore the exit status of
+# the command line.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/sh-leading-plus.exp b/unit-tests/sh-leading-plus.exp
new file mode 100644
index 000000000000..eb586d29f1c2
--- /dev/null
+++ b/unit-tests/sh-leading-plus.exp
@@ -0,0 +1,4 @@
+echo 'this command is not run'
+echo 'this command is run'
+this command is run
+exit status 0
diff --git a/unit-tests/sh-leading-plus.mk b/unit-tests/sh-leading-plus.mk
new file mode 100644
index 000000000000..75279d7d57fd
--- /dev/null
+++ b/unit-tests/sh-leading-plus.mk
@@ -0,0 +1,8 @@
+# $NetBSD: sh-leading-plus.mk,v 1.3 2020/08/23 14:46:33 rillig Exp $
+#
+# Tests for shell commands preceded by a '+', to run them even if
+# the command line option -n is given.
+
+all:
+	@echo 'this command is not run'
+	@+echo 'this command is run'
diff --git a/unit-tests/sh-meta-chars.exp b/unit-tests/sh-meta-chars.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/sh-meta-chars.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/sh-meta-chars.mk b/unit-tests/sh-meta-chars.mk
new file mode 100644
index 000000000000..126ca2ceb118
--- /dev/null
+++ b/unit-tests/sh-meta-chars.mk
@@ -0,0 +1,11 @@
+# $NetBSD: sh-meta-chars.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for running shell commands that contain meta-characters.
+#
+# These meta-characters decide whether the command is run by the shell
+# or executed directly via execv.  See Cmd_Exec for details.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/sh-multi-line.exp b/unit-tests/sh-multi-line.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/sh-multi-line.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/sh-multi-line.mk b/unit-tests/sh-multi-line.mk
new file mode 100644
index 000000000000..35a773f5dde6
--- /dev/null
+++ b/unit-tests/sh-multi-line.mk
@@ -0,0 +1,9 @@
+# $NetBSD: sh-multi-line.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for multi-line shell commands, to ensure that the line breaks
+# are preserved.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/sh-single-line.exp b/unit-tests/sh-single-line.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/sh-single-line.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/sh-single-line.mk b/unit-tests/sh-single-line.mk
new file mode 100644
index 000000000000..9dae7f80c9a9
--- /dev/null
+++ b/unit-tests/sh-single-line.mk
@@ -0,0 +1,12 @@
+# $NetBSD: sh-single-line.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for running single-line shell commands.
+#
+# In jobs mode, the shell commands are combined into a single shell
+# program, as described in the manual page, section "Shell Commands",
+# "the entire script".
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/sh.exp b/unit-tests/sh.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/sh.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/sh.mk b/unit-tests/sh.mk
new file mode 100644
index 000000000000..f79a4099e990
--- /dev/null
+++ b/unit-tests/sh.mk
@@ -0,0 +1,9 @@
+# $NetBSD: sh.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for running shell commands from the targets, or from the != variable
+# assignment operator or the :sh variable modifier.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/sysv.exp b/unit-tests/sysv.exp
index 780a187783f3..610f97c39e85 100644
--- a/unit-tests/sysv.exp
+++ b/unit-tests/sysv.exp
@@ -12,5 +12,4 @@ asam.c.c
 asam.c
 a.c.c
 
-ax:Q b c d eb
 exit status 0
diff --git a/unit-tests/sysv.mk b/unit-tests/sysv.mk
index 3a987441ee42..5c87579cc11b 100644
--- a/unit-tests/sysv.mk
+++ b/unit-tests/sysv.mk
@@ -1,4 +1,6 @@
-# $Id: sysv.mk,v 1.5 2020/07/04 18:16:55 sjg Exp $
+# $Id: sysv.mk,v 1.9 2020/08/23 16:08:32 sjg Exp $
+
+all: foo fun sam bla
 
 FOO ?=
 FOOBAR = ${FOO:=bar}
@@ -11,8 +13,6 @@ FUN = ${B}${S}fun
 SUN = the Sun
 
 # we expect nothing when FOO is empty
-all: foo fun sam bla words
-
 foo:
 	@echo FOOBAR = ${FOOBAR}
 .if empty(FOO)
@@ -41,8 +41,3 @@ BLA=
 
 bla:
 	@echo $(BLA:%=foo/%x)
-
-# The :Q looks like a modifier but isn't.
-# It is part of the replacement string.
-words:
-	@echo a${a b c d e:L:%a=x:Q}b
diff --git a/unit-tests/unexport-env.mk b/unit-tests/unexport-env.mk
index aaabcd46464a..bc5fb4914ddc 100644
--- a/unit-tests/unexport-env.mk
+++ b/unit-tests/unexport-env.mk
@@ -1,6 +1,7 @@
-# $Id: unexport-env.mk,v 1.1.1.1 2014/08/30 18:57:18 sjg Exp $
+# $Id: unexport-env.mk,v 1.1.1.2 2020/07/28 16:57:18 sjg Exp $
 
 # pick up a bunch of exported vars
+FILTER_CMD=	grep ^UT_
 .include "export.mk"
 
 # an example of setting up a minimal environment.
diff --git a/unit-tests/unexport.mk b/unit-tests/unexport.mk
index 0f1245292ba5..7d2e8f275173 100644
--- a/unit-tests/unexport.mk
+++ b/unit-tests/unexport.mk
@@ -1,8 +1,19 @@
-# $Id: unexport.mk,v 1.1.1.1 2014/08/30 18:57:18 sjg Exp $
+# $Id: unexport.mk,v 1.1.1.3 2020/08/08 22:34:25 sjg Exp $
 
 # pick up a bunch of exported vars
+FILTER_CMD=	grep ^UT_
 .include "export.mk"
 
 .unexport UT_ZOO UT_FOO
 
 UT_TEST = unexport
+
+# Until 2020-08-08, Var_UnExport had special handling for '\n', that code
+# was not reachable though.  At that point, backslash-newline has already
+# been replaced with a simple space, and variables are not yet expanded.
+UT_BEFORE_NL=	before
+UT_AFTER_NL=	after
+.export UT_BEFORE_NL UT_AFTER_NL
+.unexport \
+  UT_BEFORE_NL
+.unexport ${.newline} UT_AFTER_NL
diff --git a/unit-tests/use-inference.exp b/unit-tests/use-inference.exp
new file mode 100644
index 000000000000..14ecf0550574
--- /dev/null
+++ b/unit-tests/use-inference.exp
@@ -0,0 +1,4 @@
+Building use-inference.from from nothing
+make: don't know how to make use-inference.to (continuing)
+`all' not remade because of errors.
+exit status 0
diff --git a/unit-tests/use-inference.mk b/unit-tests/use-inference.mk
new file mode 100644
index 000000000000..b0e5017bc6fb
--- /dev/null
+++ b/unit-tests/use-inference.mk
@@ -0,0 +1,35 @@
+# $NetBSD: use-inference.mk,v 1.1 2020/08/09 16:32:28 rillig Exp $
+#
+# Demonstrate that .USE rules do not have an effect on inference rules.
+# At least not in the special case where the inference rule does not
+# have any associated commands.
+
+.SUFFIXES:
+.SUFFIXES: .from .to
+
+all: use-inference.to
+
+verbose: .USE
+	@echo 'Verbosely making $@ out of $>'
+
+.from.to: verbose
+# Since this inference rule does not have any associated commands, it
+# is ignored.
+#
+#	@echo 'Building $@ from $<'
+
+use-inference.from:		# assume it exists
+	@echo 'Building $@ from nothing'
+
+# Possible but unproven explanation:
+#
+# The main target is "all", which depends on "use-inference.to".
+# The inference connects the .from to the .to file, otherwise make
+# would not know that the .from file would need to be built.
+#
+# The .from file is then built.
+#
+# After this, make stops since it doesn't know how to make the .to file.
+# This is strange since make definitely knows about the .from.to suffix
+# inference rule.  But it seems to ignore it, maybe because it doesn't
+# have any associated commands.
diff --git a/unit-tests/var-class-cmdline.exp b/unit-tests/var-class-cmdline.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/var-class-cmdline.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/var-class-cmdline.mk b/unit-tests/var-class-cmdline.mk
new file mode 100644
index 000000000000..c43b5351c329
--- /dev/null
+++ b/unit-tests/var-class-cmdline.mk
@@ -0,0 +1,8 @@
+# $NetBSD: var-class-cmdline.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for variables specified on the command line.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/var-class-env.exp b/unit-tests/var-class-env.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/var-class-env.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/var-class-env.mk b/unit-tests/var-class-env.mk
new file mode 100644
index 000000000000..6e6b4891d3fd
--- /dev/null
+++ b/unit-tests/var-class-env.mk
@@ -0,0 +1,8 @@
+# $NetBSD: var-class-env.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for variables specified in the process environment.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/var-class-global.exp b/unit-tests/var-class-global.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/var-class-global.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/var-class-global.mk b/unit-tests/var-class-global.mk
new file mode 100644
index 000000000000..81345ffda463
--- /dev/null
+++ b/unit-tests/var-class-global.mk
@@ -0,0 +1,8 @@
+# $NetBSD: var-class-global.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for global variables, which are the most common variables.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/var-class-local-legacy.exp b/unit-tests/var-class-local-legacy.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/var-class-local-legacy.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/var-class-local-legacy.mk b/unit-tests/var-class-local-legacy.mk
new file mode 100644
index 000000000000..bfd9733fd42b
--- /dev/null
+++ b/unit-tests/var-class-local-legacy.mk
@@ -0,0 +1,8 @@
+# $NetBSD: var-class-local-legacy.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for legacy target-local variables, such as ${<F} or ${@D}.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/var-class-local.exp b/unit-tests/var-class-local.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/var-class-local.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/var-class-local.mk b/unit-tests/var-class-local.mk
new file mode 100644
index 000000000000..e75f08ba75a3
--- /dev/null
+++ b/unit-tests/var-class-local.mk
@@ -0,0 +1,8 @@
+# $NetBSD: var-class-local.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for target-local variables, such as ${.TARGET} or $@.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/var-class.exp b/unit-tests/var-class.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/var-class.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/var-class.mk b/unit-tests/var-class.mk
new file mode 100644
index 000000000000..b20fca565e16
--- /dev/null
+++ b/unit-tests/var-class.mk
@@ -0,0 +1,9 @@
+# $NetBSD: var-class.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the different variable classes (local, command-line, global,
+# environment), and which of them takes precedence over the others.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/var-op-append.exp b/unit-tests/var-op-append.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/var-op-append.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/var-op-append.mk b/unit-tests/var-op-append.mk
new file mode 100644
index 000000000000..b75880f95838
--- /dev/null
+++ b/unit-tests/var-op-append.mk
@@ -0,0 +1,9 @@
+# $NetBSD: var-op-append.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the += variable assignment operator, which appends to a variable,
+# creating it if necessary.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/var-op-assign.exp b/unit-tests/var-op-assign.exp
new file mode 100644
index 000000000000..0e9e2d211a5f
--- /dev/null
+++ b/unit-tests/var-op-assign.exp
@@ -0,0 +1,6 @@
+this will be evaluated later
+make: "var-op-assign.mk" line 52: Need an operator
+make: "var-op-assign.mk" line 86: Parsing still continues until here.
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/var-op-assign.mk b/unit-tests/var-op-assign.mk
new file mode 100644
index 000000000000..dadff6ed4b87
--- /dev/null
+++ b/unit-tests/var-op-assign.mk
@@ -0,0 +1,89 @@
+# $NetBSD: var-op-assign.mk,v 1.4 2020/08/25 16:20:32 rillig Exp $
+#
+# Tests for the = variable assignment operator, which overwrites an existing
+# variable or creates it.
+
+# This is a simple variable assignment.
+# To the left of the assignment operator '=' there is the variable name,
+# and to the right is the variable value.
+#
+VAR=	value
+
+# This condition demonstrates that whitespace around the assignment operator
+# is discarded.  Otherwise the value would start with a single tab.
+#
+.if ${VAR} != "value"
+.error
+.endif
+
+# Whitespace to the left of the assignment operator is ignored as well.
+# The variable value can contain arbitrary characters.
+#
+# The '#' needs to be escaped with a backslash, this happens in a very
+# early stage of parsing and applies to all line types, except for the
+# commands, which are indented with a tab.
+#
+# The '$' needs to be escaped with another '$', otherwise it would refer to
+# another variable.
+#
+VAR	=new value and \# some $$ special characters	# comment
+
+# When a string literal appears in a condition, the escaping rules are
+# different.  Run make with the -dc option to see the details.
+.if ${VAR} != "new value and \# some \$ special characters"
+.error ${VAR}
+.endif
+
+# The variable value may contain references to other variables.
+# In this example, the reference is to the variable with the empty name,
+# which always expands to an empty string.  This alone would not produce
+# any side-effects, therefore the variable has a :!...! modifier that
+# executes a shell command.
+VAR=	${:! echo 'not yet evaluated' 1>&2 !}
+VAR=	${:! echo 'this will be evaluated later' 1>&2 !}
+
+# Now force the variable to be evaluated.
+# This outputs the line to stderr.
+.if ${VAR}
+.endif
+
+# In a variable assignment, the variable name must consist of a single word.
+#
+VARIABLE NAME=	variable value
+
+# But if the whitespace appears inside parentheses or braces, everything is
+# fine.
+#
+# XXX: This was not an intentional decision, as variable names typically
+# neither contain parentheses nor braces.  This is only a side-effect from
+# the implementation of the parser, which cheats when parsing a variable
+# name.  It only counts parentheses and braces instead of properly parsing
+# nested variable expressions such as VAR.${param}.
+#
+VAR(spaces in parentheses)=	()
+VAR{spaces in braces}=		{}
+
+# Be careful and use indirect variable names here, to prevent accidentally
+# accepting the test in case the parser just uses "VAR" as the variable name,
+# ignoring all the rest.
+#
+VARNAME_PAREN=	VAR(spaces in parentheses)
+VARNAME_BRACES=	VAR{spaces in braces}
+
+.if ${${VARNAME_PAREN}} != "()"
+.error
+.endif
+
+.if ${${VARNAME_BRACES}} != "{}"
+.error
+.endif
+
+# In safe mode, parsing would stop immediately after the "VARIABLE NAME="
+# line, since any commands run after that are probably working with
+# unexpected variable values.
+#
+# Therefore, just output an info message.
+.info Parsing still continues until here.
+
+all:
+	@:;
diff --git a/unit-tests/var-op-default.exp b/unit-tests/var-op-default.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/var-op-default.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/var-op-default.mk b/unit-tests/var-op-default.mk
new file mode 100644
index 000000000000..afb0c55f827c
--- /dev/null
+++ b/unit-tests/var-op-default.mk
@@ -0,0 +1,9 @@
+# $NetBSD: var-op-default.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the ?= variable assignment operator, which only assigns
+# if the variable is still undefined.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/var-op-expand.exp b/unit-tests/var-op-expand.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/var-op-expand.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/var-op-expand.mk b/unit-tests/var-op-expand.mk
new file mode 100644
index 000000000000..07c5fb647759
--- /dev/null
+++ b/unit-tests/var-op-expand.mk
@@ -0,0 +1,9 @@
+# $NetBSD: var-op-expand.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the := variable assignment operator, which expands its
+# right-hand side.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/var-op-shell.exp b/unit-tests/var-op-shell.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/var-op-shell.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/var-op-shell.mk b/unit-tests/var-op-shell.mk
new file mode 100644
index 000000000000..83580a89e6c2
--- /dev/null
+++ b/unit-tests/var-op-shell.mk
@@ -0,0 +1,9 @@
+# $NetBSD: var-op-shell.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the != variable assignment operator, which runs its right-hand
+# side through the shell.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/var-op.exp b/unit-tests/var-op.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/var-op.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/var-op.mk b/unit-tests/var-op.mk
new file mode 100644
index 000000000000..9c9ffb8782f5
--- /dev/null
+++ b/unit-tests/var-op.mk
@@ -0,0 +1,8 @@
+# $NetBSD: var-op.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the variable assignment operators.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/vardebug.exp b/unit-tests/vardebug.exp
new file mode 100644
index 000000000000..8a9eefad4b0c
--- /dev/null
+++ b/unit-tests/vardebug.exp
@@ -0,0 +1,80 @@
+Global:RELEVANT = yes
+Global:VAR = added
+Global:VAR = overwritten
+Global:delete VAR
+Global:delete VAR (not found)
+Var_Parse: ${:U} with VARE_WANTRES
+Applying ${:U} to "" (eflags = VARE_WANTRES, vflags = VAR_JUNK)
+Result of ${:U} is "" (eflags = VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+Var_Set("${:U}", "empty name", ...) name expands to empty string - ignored
+Var_Parse: ${:U} with VARE_WANTRES
+Applying ${:U} to "" (eflags = VARE_WANTRES, vflags = VAR_JUNK)
+Result of ${:U} is "" (eflags = VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+Var_Append("${:U}", "empty name", ...) name expands to empty string - ignored
+Global:FROM_CMDLINE = overwritten ignored!
+Global:VAR = 1
+Global:VAR = 1 2
+Global:VAR = 1 2 3
+Var_Parse: ${VAR:M[2]} with VARE_UNDEFERR|VARE_WANTRES
+Applying ${VAR:M...} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Pattern[VAR] for [1 2 3] is [[2]]
+ModifyWords: split "1 2 3" into 3 words
+VarMatch [1] [[2]]
+VarMatch [2] [[2]]
+VarMatch [3] [[2]]
+Result of ${VAR:M[2]} is "2" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Var_Parse: ${VAR:N[2]} with VARE_UNDEFERR|VARE_WANTRES
+Applying ${VAR:N...} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Pattern[VAR] for [1 2 3] is [[2]]
+ModifyWords: split "1 2 3" into 3 words
+Result of ${VAR:N[2]} is "1 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Var_Parse: ${VAR:S,2,two,} with VARE_UNDEFERR|VARE_WANTRES
+Applying ${VAR:S...} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Modifier part: "2"
+Modifier part: "two"
+ModifyWords: split "1 2 3" into 3 words
+Result of ${VAR:S,2,two,} is "1 two 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Var_Parse: ${VAR:Q} with VARE_UNDEFERR|VARE_WANTRES
+Applying ${VAR:Q} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+QuoteMeta: [1\ 2\ 3]
+Result of ${VAR:Q} is "1\ 2\ 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Var_Parse: ${VAR:tu:tl:Q} with VARE_UNDEFERR|VARE_WANTRES
+Applying ${VAR:t...} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Result of ${VAR:tu} is "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Applying ${VAR:t...} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Result of ${VAR:tl} is "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Applying ${VAR:Q} to "1 2 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+QuoteMeta: [1\ 2\ 3]
+Result of ${VAR:Q} is "1\ 2\ 3" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = none)
+Var_Parse: ${:Uvalue:${:UM*e}:Mvalu[e]} with VARE_UNDEFERR|VARE_WANTRES
+Applying ${:U...} to "" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK)
+Result of ${:Uvalue} is "value" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+Var_Parse: ${:UM*e}:Mvalu[e]} with VARE_UNDEFERR|VARE_WANTRES
+Applying ${:U...} to "" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK)
+Result of ${:UM*e} is "M*e" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+Indirect modifier "M*e" from "${:UM*e}"
+Applying ${:M...} to "value" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+Pattern[] for [value] is [*e]
+ModifyWords: split "value" into 1 words
+VarMatch [value] [*e]
+Result of ${:M*e} is "value" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+Applying ${:M...} to "value" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+Pattern[] for [value] is [valu[e]]
+ModifyWords: split "value" into 1 words
+VarMatch [value] [valu[e]]
+Result of ${:Mvalu[e]} is "value" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+Var_Parse: ${:UVAR} with VARE_WANTRES
+Applying ${:U...} to "" (eflags = VARE_WANTRES, vflags = VAR_JUNK)
+Result of ${:UVAR} is "VAR" (eflags = VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+Global:delete VAR
+Var_Parse: ${:Uvariable:unknown} with VARE_UNDEFERR|VARE_WANTRES
+Applying ${:U...} to "" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK)
+Result of ${:Uvariable} is "variable" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+Applying ${:u...} to "variable" (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+make: Unknown modifier 'u'
+Result of ${:unknown} is error (eflags = VARE_UNDEFERR|VARE_WANTRES, vflags = VAR_JUNK|VAR_KEEP)
+make: "vardebug.mk" line 44: Malformed conditional (${:Uvariable:unknown})
+Var_Parse: ${UNDEFINED} with VARE_UNDEFERR|VARE_WANTRES
+make: "vardebug.mk" line 53: Malformed conditional (${UNDEFINED})
+Global:RELEVANT = no
+exit status 1
diff --git a/unit-tests/vardebug.mk b/unit-tests/vardebug.mk
new file mode 100644
index 000000000000..f61df98d50de
--- /dev/null
+++ b/unit-tests/vardebug.mk
@@ -0,0 +1,59 @@
+# $NetBSD: vardebug.mk,v 1.3 2020/08/08 14:28:46 rillig Exp $
+#
+# Demonstrates the debugging output for var.c.
+
+RELEVANT=	yes
+
+VAR=		added		# VarAdd
+VAR=		overwritten	# Var_Set
+.undef VAR			# Var_Delete (found)
+.undef VAR			# Var_Delete (not found)
+
+# The variable with the empty name cannot be set at all.
+${:U}=		empty name	# Var_Set
+${:U}+=		empty name	# Var_Append
+
+FROM_CMDLINE=	overwritten	# Var_Set (ignored)
+
+VAR=		1
+VAR+=		2
+VAR+=		3
+
+.if ${VAR:M[2]}			# VarMatch
+.endif
+.if ${VAR:N[2]}			# VarNoMatch (no debug output)
+.endif
+
+.if ${VAR:S,2,two,}		# VarGetPattern
+.endif
+
+.if ${VAR:Q}			# VarQuote
+.endif
+
+.if ${VAR:tu:tl:Q}		# ApplyModifiers
+.endif
+
+# ApplyModifiers, "Got ..."
+.if ${:Uvalue:${:UM*e}:Mvalu[e]}
+.endif
+
+.undef ${:UVAR}			# Var_Delete
+
+# When ApplyModifiers results in an error, this appears in the debug log
+# as "is error", without surrounding quotes.
+.if ${:Uvariable:unknown}
+.endif
+
+# XXX: The error message is "Malformed conditional", which is wrong.
+# The condition is syntactically fine, it just contains an undefined variable.
+#
+# There is a specialized error message for "Undefined variable", but as of
+# 2020-08-08, that is not covered by any unit tests.  It might even be
+# unreachable.
+.if ${UNDEFINED}
+.endif
+
+RELEVANT=	no
+
+all:
+	@:
diff --git a/unit-tests/varfind.exp b/unit-tests/varfind.exp
new file mode 100644
index 000000000000..13d8d60226a9
--- /dev/null
+++ b/unit-tests/varfind.exp
@@ -0,0 +1,15 @@
+VarFind-aliases.to: long explicit-dependency VarFind-aliases.from
+VarFind-aliases.to: abbr explicit-dependency VarFind-aliases.from
+VarFind-aliases.to: long
+VarFind-aliases.to: abbr
+VarFind-aliases.to: long VarFind-aliases.from
+VarFind-aliases.to: abbr VarFind-aliases.from
+VarFind-aliases.to: long
+VarFind-aliases.to: abbr
+VarFind-aliases.to: long explicit-dependency VarFind-aliases.from
+VarFind-aliases.to: abbr explicit-dependency VarFind-aliases.from
+VarFind-aliases.to: long VarFind-aliases
+VarFind-aliases.to: abbr VarFind-aliases
+VarFind-aliases.to: long VarFind-aliases.to
+VarFind-aliases.to: abbr VarFind-aliases.to
+exit status 0
diff --git a/unit-tests/varfind.mk b/unit-tests/varfind.mk
new file mode 100644
index 000000000000..6fb7bc630eaf
--- /dev/null
+++ b/unit-tests/varfind.mk
@@ -0,0 +1,31 @@
+# $NetBSD: varfind.mk,v 1.1 2020/07/25 21:19:29 rillig Exp $
+#
+# Demonstrates variable name aliases in VarFind.
+
+all: VarFind-aliases.to
+
+.SUFFIXES: .from .to
+
+VarFind-aliases.from:
+	@: do nothing
+
+VarFind-aliases.to: explicit-dependency
+
+explicit-dependency:
+	@: do nothing
+
+.from.to:
+	@echo $@: long ${.ALLSRC:Q}
+	@echo $@: abbr ${>:Q}
+	@echo $@: long ${.ARCHIVE:Q}
+	@echo $@: abbr ${!:Q}
+	@echo $@: long ${.IMPSRC:Q}
+	@echo $@: abbr ${<:Q}
+	@echo $@: long ${.MEMBER:Q}
+	@echo $@: abbr ${%:Q}
+	@echo $@: long ${.OODATE:Q}
+	@echo $@: abbr ${?:Q}
+	@echo $@: long ${.PREFIX:Q}
+	@echo $@: abbr ${*:Q}
+	@echo $@: long ${.TARGET:Q}
+	@echo $@: abbr ${@:Q}
diff --git a/unit-tests/varmisc.exp b/unit-tests/varmisc.exp
index b9a29141ce6b..e8f88d9ca51f 100644
--- a/unit-tests/varmisc.exp
+++ b/unit-tests/varmisc.exp
@@ -23,4 +23,52 @@ Version=123.456.789 == 123456789
 Literal=3.4.5 == 3004005
 We have target specific vars
 MAN= make.1
+save-dollars: 0        = $
+save-dollars: 1        = $$
+save-dollars: 2        = $$
+save-dollars: False    = $
+save-dollars: True     = $$
+save-dollars: false    = $
+save-dollars: true     = $$
+save-dollars: Yes      = $$
+save-dollars: No       = $
+save-dollars: yes      = $$
+save-dollars: no       = $
+save-dollars: On       = $$
+save-dollars: Off      = $
+save-dollars: ON       = $$
+save-dollars: OFF      = $
+save-dollars: on       = $$
+save-dollars: off      = $
+export-appended: env
+export-appended: env
+export-appended: env mk
+parse-dynamic: parse-dynamic parse-dynamic before
+parse-dynamic: parse-dynamic parse-dynamic after
+parse-dynamic: parse-dynamic parse-dynamic after
+varerror-unclosed:begin
+make: Unclosed variable ""
+
+make: Unclosed variable "UNCLOSED"
+
+make: Unclosed variable "UNCLOSED"
+
+make: Unclosed variable "PATTERN"
+make: Unclosed variable specification (expecting '}') for "UNCLOSED" (value "") modifier M
+
+make: Unclosed variable "param"
+make: Unclosed variable "UNCLOSED."
+
+
+make: Unclosed variable "UNCLOSED.1"
+
+make: Unclosed variable "UNCLOSED.2"
+
+make: Unclosed variable "UNCLOSED.3"
+
+make: Unclosed variable "UNCLOSED_ORIG"
+
+varerror-unclosed:end
+target1-flags: we have: one two
+target2-flags: we have: one two three four
 exit status 0
diff --git a/unit-tests/varmisc.mk b/unit-tests/varmisc.mk
index ab591db5c4fd..4c58b2a5dc77 100644
--- a/unit-tests/varmisc.mk
+++ b/unit-tests/varmisc.mk
@@ -1,9 +1,13 @@
-# $Id: varmisc.mk,v 1.11 2020/07/02 15:43:43 sjg Exp $
+# $Id: varmisc.mk,v 1.19 2020/08/31 16:28:10 sjg Exp $
 #
 # Miscellaneous variable tests.
 
 all: unmatched_var_paren D_true U_true D_false U_false Q_lhs Q_rhs NQ_none \
 	strftime cmpv manok
+all: save-dollars
+all: export-appended
+all: parse-dynamic
+all: varerror-unclosed
 
 unmatched_var_paren:
 	@echo ${foo::=foo-text}
@@ -74,7 +78,7 @@ manok:
 	@echo MAN=${MAN}
 
 # This is an expanded variant of the above .for loop.
-# Between 2020-08-28 and 2020-07-02 this paragraph generated a wrong
+# Between 2020-06-28 and 2020-07-02 this paragraph generated a wrong
 # error message "Variable VARNAME is recursive".
 # When evaluating the !empty expression, the ${:U1} was not expanded and
 # thus resulted in the seeming definition VARNAME=${VARNAME}, which is
@@ -82,3 +86,142 @@ manok:
 VARNAME=	${VARNAME${:U1}}
 .if defined(VARNAME${:U2}) && !empty(VARNAME${:U2})
 .endif
+
+# begin .MAKE.SAVE_DOLLARS; see Var_Set_with_flags and s2Boolean.
+SD_VALUES=	0 1 2 False True false true Yes No yes no On Off ON OFF on off
+SD_4_DOLLARS=	$$$$
+
+.for val in ${SD_VALUES}
+.MAKE.SAVE_DOLLARS:=	${val}	# Must be := since a simple = has no effect.
+SD.${val}:=		${SD_4_DOLLARS}
+.endfor
+.MAKE.SAVE_DOLLARS:=	yes
+
+save-dollars:
+.for val in ${SD_VALUES}
+	@printf '%s: %-8s = %s\n' $@ ${val} ${SD.${val}:Q}
+.endfor
+
+# Appending to an undefined variable does not add a space in front.
+.undef APPENDED
+APPENDED+=	value
+.if ${APPENDED} != "value"
+.error "${APPENDED}"
+.endif
+
+# Appending to an empty variable adds a space between the old value
+# and the additional value.
+APPENDED=	# empty
+APPENDED+=	value
+.if ${APPENDED} != " value"
+.error "${APPENDED}"
+.endif
+
+# Appending to parameterized variables works as well.
+PARAM=		param
+VAR.${PARAM}=	1
+VAR.${PARAM}+=	2
+.if ${VAR.param} != "1 2"
+.error "${VAR.param}"
+.endif
+
+# The variable name can contain arbitrary characters.
+# If the expanded variable name ends in a +, this still does not influence
+# the parser. The assignment operator is still a simple assignment.
+# Therefore, there is no need to add a space between the variable name
+# and the assignment operator.
+PARAM=		+
+VAR.${PARAM}=	1
+VAR.${PARAM}+=	2
+.if ${VAR.+} != "1 2"
+.error "${VAR.+}"
+.endif
+.for param in + ! ?
+VAR.${param}=	${param}
+.endfor
+.if ${VAR.+} != "+" || ${VAR.!} != "!" || ${VAR.?} != "?"
+.error "${VAR.+}" "${VAR.!}" "${VAR.?}"
+.endif
+
+# Appending to a variable from the environment creates a copy of that variable
+# in the global context.
+# The appended value is not exported automatically.
+# When a variable is exported, the exported value is taken at the time of the
+# .export directive. Later changes to the variable have no effect.
+.export FROM_ENV_BEFORE
+FROM_ENV+=		mk
+FROM_ENV_BEFORE+=	mk
+FROM_ENV_AFTER+=	mk
+.export FROM_ENV_AFTER
+
+export-appended:
+	@echo $@: "$$FROM_ENV"
+	@echo $@: "$$FROM_ENV_BEFORE"
+	@echo $@: "$$FROM_ENV_AFTER"
+
+# begin parse-dynamic
+#
+# Demonstrate that the target-specific variables are not evaluated in
+# the global context. They are preserved until there is a local context
+# in which resolving them makes sense.
+
+# There are different code paths for short names ...
+${:U>}=		before
+GS_TARGET:=	$@
+GS_MEMBER:=	$%
+GS_PREFIX:=	$*
+GS_ARCHIVE:=	$!
+GS_ALLSRC:=	$>
+${:U>}=		after
+# ... and for braced short names ...
+GB_TARGET:=	${@}
+GB_MEMBER:=	${%}
+GB_PREFIX:=	${*}
+GB_ARCHIVE:=	${!}
+GB_ALLSRC:=	${>}
+# ... and for long names.
+GL_TARGET:=	${.TARGET}
+GL_MEMBER:=	${.MEMBER}
+GL_PREFIX:=	${.PREFIX}
+GL_ARCHIVE:=	${.ARCHIVE}
+GL_ALLSRC:=	${.ALLSRC}
+
+parse-dynamic:
+	@echo $@: ${GS_TARGET} ${GS_MEMBER} ${GS_PREFIX} ${GS_ARCHIVE} ${GS_ALLSRC}
+	@echo $@: ${GB_TARGET} ${GB_MEMBER} ${GB_PREFIX} ${GB_ARCHIVE} ${GB_ALLSRC}
+	@echo $@: ${GL_TARGET} ${GL_MEMBER} ${GL_PREFIX} ${GL_ARCHIVE} ${GL_ALLSRC}
+
+# Since 2020-07-28, make complains about unclosed variables.
+# Before that, it had complained about unclosed variables only when
+# parsing the modifiers, but not when parsing the variable name.
+
+UNCLOSED_INDIR_1=	${UNCLOSED_ORIG
+UNCLOSED_INDIR_2=	${UNCLOSED_INDIR_1}
+
+FLAGS=	one two
+FLAGS+= ${FLAGS.${.ALLSRC:M*.c:T:u}}
+FLAGS.target2.c = three four
+
+target1.c:
+target2.c:
+
+all: target1-flags target2-flags
+target1-flags: target1.c
+	@echo $@: we have: ${FLAGS}
+
+target2-flags: target2.c
+	@echo $@: we have: ${FLAGS}
+
+varerror-unclosed:
+	@echo $@:begin
+	@echo $(
+	@echo $(UNCLOSED
+	@echo ${UNCLOSED
+	@echo ${UNCLOSED:M${PATTERN
+	@echo ${UNCLOSED.${param
+	@echo $
+.for i in 1 2 3
+	@echo ${UNCLOSED.${i}
+.endfor
+	@echo ${UNCLOSED_INDIR_2}
+	@echo $@:end
diff --git a/unit-tests/varmod-assign.exp b/unit-tests/varmod-assign.exp
new file mode 100644
index 000000000000..120c775d5f69
--- /dev/null
+++ b/unit-tests/varmod-assign.exp
@@ -0,0 +1,26 @@
+mod-assign: first=1.
+mod-assign: last=3.
+mod-assign: appended=1 2 3.
+1
+2
+3
+mod-assign: ran:3.
+mod-assign: global: 1, 3, 1 2 3, 3.
+mod-assign-nested: then1t1
+mod-assign-nested: else2e2
+mod-assign-nested: then3t3
+mod-assign-nested: else4e4
+make: Bad modifier `:' for 
+value}
+make: Bad modifier `:' for 
+mod-assign-empty: overwritten}
+mod-assign-empty: VAR=overwritten
+make: Unknown modifier ':'
+
+sysv:y
+make: Unfinished modifier for ASSIGN ('}' missing)
+
+ok=word
+make: " echo word; false " returned non-zero status
+err=previous
+exit status 0
diff --git a/unit-tests/varmod-assign.mk b/unit-tests/varmod-assign.mk
new file mode 100644
index 000000000000..82b7d947c484
--- /dev/null
+++ b/unit-tests/varmod-assign.mk
@@ -0,0 +1,81 @@
+# $NetBSD: varmod-assign.mk,v 1.6 2020/08/25 21:16:53 rillig Exp $
+#
+# Tests for the obscure ::= variable modifiers, which perform variable
+# assignments during evaluation, just like the = operator in C.
+
+all:	mod-assign
+all:	mod-assign-nested
+all:	mod-assign-empty
+all:	mod-assign-parse
+all:	mod-assign-shell-error
+
+mod-assign:
+	# The ::?= modifier applies the ?= assignment operator 3 times.
+	# The ?= operator only has an effect for the first time, therefore
+	# the variable FIRST ends up with the value 1.
+	@echo $@: ${1 2 3:L:@i@${FIRST::?=$i}@} first=${FIRST}.
+
+	# The ::= modifier applies the = assignment operator 3 times.
+	# The = operator overwrites the previous value, therefore the
+	# variable LAST ends up with the value 3.
+	@echo $@: ${1 2 3:L:@i@${LAST::=$i}@} last=${LAST}.
+
+	# The ::+= modifier applies the += assignment operator 3 times.
+	# The += operator appends 3 times to the variable, therefore
+	# the variable APPENDED ends up with the value "1 2 3".
+	@echo $@: ${1 2 3:L:@i@${APPENDED::+=$i}@} appended=${APPENDED}.
+
+	# The ::!= modifier applies the != assignment operator 3 times.
+	# The side effects of the shell commands are visible in the output.
+	# Just as with the ::= modifier, the last value is stored in the
+	# RAN variable.
+	@echo $@: ${echo.1 echo.2 echo.3:L:@i@${RAN::!=${i:C,.*,&; & 1>\&2,:S,., ,g}}@} ran:${RAN}.
+
+	# The assignments happen in the global scope and thus are
+	# preserved even after the shell command has been run.
+	@echo $@: global: ${FIRST:Q}, ${LAST:Q}, ${APPENDED:Q}, ${RAN:Q}.
+
+mod-assign-nested:
+	# The condition "1" is true, therefore THEN1 gets assigned a value,
+	# and IT1 as well.  Nothing surprising here.
+	@echo $@: ${1:?${THEN1::=then1${IT1::=t1}}:${ELSE1::=else1${IE1::=e1}}}${THEN1}${ELSE1}${IT1}${IE1}
+
+	# The condition "0" is false, therefore ELSE1 gets assigned a value,
+	# and IE1 as well.  Nothing surprising here as well.
+	@echo $@: ${0:?${THEN2::=then2${IT2::=t2}}:${ELSE2::=else2${IE2::=e2}}}${THEN2}${ELSE2}${IT2}${IE2}
+
+	# The same effects happen when the variables are defined elsewhere.
+	@echo $@: ${SINK3:Q}
+	@echo $@: ${SINK4:Q}
+SINK3:=	${1:?${THEN3::=then3${IT3::=t3}}:${ELSE3::=else3${IE3::=e3}}}${THEN3}${ELSE3}${IT3}${IE3}
+SINK4:=	${0:?${THEN4::=then4${IT4::=t4}}:${ELSE4::=else4${IE4::=e4}}}${THEN4}${ELSE4}${IT4}${IE4}
+
+mod-assign-empty:
+	# Assigning to the empty variable would obviously not work since that variable
+	# is write-protected.  Therefore it is rejected early as a "bad modifier".
+	@echo ${::=value}
+	@echo $@: ${:Uvalue::=overwritten}
+
+	# The :L modifier sets the variable's value to its name.
+	# Since the name is still "VAR", assigning to that variable works.
+	@echo $@: ${VAR:L::=overwritten} VAR=${VAR}
+
+mod-assign-parse:
+	# The modifier for assignment operators starts with a ':'.
+	# An 'x' after that is an invalid modifier.
+	@echo ${ASSIGN::x}	# 'x' is an unknown assignment operator
+
+	# When parsing an assignment operator fails because the operator is
+	# incomplete, make falls back to the SysV modifier.
+	@echo ${SYSV::=sysv\:x}${SYSV::x=:y}
+
+	@echo ${ASSIGN::=value	# missing closing brace
+
+mod-assign-shell-error:
+	# If the command succeeds, the variable is assigned.
+	@${SH_OK::!= echo word; true } echo ok=${SH_OK}
+
+	# If the command fails, the variable keeps its previous value.
+	# FIXME: the error message says: "previous" returned non-zero status
+	@${SH_ERR::=previous}
+	@${SH_ERR::!= echo word; false } echo err=${SH_ERR}
diff --git a/unit-tests/varmod-defined.exp b/unit-tests/varmod-defined.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-defined.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-defined.mk b/unit-tests/varmod-defined.mk
new file mode 100644
index 000000000000..fa5bf5c2245c
--- /dev/null
+++ b/unit-tests/varmod-defined.mk
@@ -0,0 +1,28 @@
+# $NetBSD: varmod-defined.mk,v 1.3 2020/08/25 21:58:08 rillig Exp $
+#
+# Tests for the :D variable modifier, which returns the given string
+# if the variable is defined.  It is closely related to the :U modifier.
+
+DEF=	defined
+.undef UNDEF
+
+# Since DEF is defined, the value of the expression is "value", not
+# "defined".
+#
+.if ${DEF:Dvalue} != "value"
+.error
+.endif
+
+# Since UNDEF is not defined, the "value" is ignored.  Instead of leaving the
+# expression undefined, it is set to "", exactly to allow the expression to
+# be used in .if conditions.  In this place, other undefined expressions
+# would generate an error message.
+# XXX: Ideally the error message would be "undefined variable", but as of
+# 2020-08-25 it is "Malformed conditional".
+#
+.if ${UNDEF:Dvalue} != ""
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/varmod-edge.exp b/unit-tests/varmod-edge.exp
index b3b2e3a92f95..94ba81e2e4f0 100644
--- a/unit-tests/varmod-edge.exp
+++ b/unit-tests/varmod-edge.exp
@@ -1,17 +1,22 @@
+make: "varmod-edge.mk" line omitted: ok M-paren
+make: "varmod-edge.mk" line omitted: ok M-mixed
+make: "varmod-edge.mk" line omitted: ok M-unescape
 make: Unclosed variable specification (expecting '}') for "" (value "*)") modifier U
-make: Unclosed substitution for INP.eq-esc (= missing)
-ok M-paren
-ok M-mixed
-ok M-unescape
-ok M-nest-mix
-ok M-nest-brk
-ok M-pat-err
-ok M-bsbs
-ok M-bs1-par
-ok M-bs2-par
-ok M-128
-ok eq-ext
-ok eq-q
-ok eq-bs
-ok eq-esc
+make: "varmod-edge.mk" line omitted: ok M-nest-mix
+make: "varmod-edge.mk" line omitted: ok M-nest-brk
+make: "varmod-edge.mk" line omitted: ok M-pat-err
+make: "varmod-edge.mk" line omitted: ok M-bsbs
+make: "varmod-edge.mk" line omitted: ok M-bs1-par
+make: "varmod-edge.mk" line omitted: ok M-bs2-par
+make: "varmod-edge.mk" line omitted: ok M-128
+make: "varmod-edge.mk" line omitted: ok eq-ext
+make: "varmod-edge.mk" line omitted: ok eq-q
+make: "varmod-edge.mk" line omitted: ok eq-bs
+make: Unfinished modifier for INP.eq-esc ('=' missing)
+make: "varmod-edge.mk" line omitted: ok eq-esc
+make: "varmod-edge.mk" line omitted: ok colon
+make: Unknown modifier ':'
+make: Unknown modifier ':'
+make: "varmod-edge.mk" line omitted: ok colons
+ok
 exit status 0
diff --git a/unit-tests/varmod-edge.mk b/unit-tests/varmod-edge.mk
index 561eb6116891..e6f7f91c95c7 100644
--- a/unit-tests/varmod-edge.mk
+++ b/unit-tests/varmod-edge.mk
@@ -1,4 +1,4 @@
-# $NetBSD: varmod-edge.mk,v 1.7 2020/04/27 14:07:22 christos Exp $
+# $NetBSD: varmod-edge.mk,v 1.12 2020/08/08 13:29:09 rillig Exp $
 #
 # Tests for edge cases in variable modifiers.
 #
@@ -143,20 +143,31 @@ INP.eq-bs=	file.c file.c=%.o
 MOD.eq-bs=	${INP.eq-bs:%.c\=%.o=%.ext}
 EXP.eq-bs=	file.c file.ext
 
-# Having only an escaped = results in a parse error.
-# The call to "pattern.lhs = VarGetPattern" fails.
+# Having only an escaped '=' results in a parse error.
+# The call to "pattern.lhs = ParseModifierPart" fails.
 TESTS+=		eq-esc
 INP.eq-esc=	file.c file...
 MOD.eq-esc=	${INP.eq-esc:a\=b}
 EXP.eq-esc=	# empty
-# make: Unclosed substitution for INP.eq-esc (= missing)
+# make: Unfinished modifier for INP.eq-esc ('=' missing)
+
+TESTS+=		colon
+INP.colon=	value
+MOD.colon=	${INP.colon:}
+EXP.colon=	value
+
+TESTS+=		colons
+INP.colons=	value
+MOD.colons=	${INP.colons::::}
+EXP.colons=	# empty
 
-all:
 .for test in ${TESTS}
 .  if ${MOD.${test}} == ${EXP.${test}}
-	@printf 'ok %s\n' ${test:Q}''
+.info ok ${test}
 .  else
-	@printf 'error in %s: expected %s, got %s\n' \
-		${test:Q}'' ${EXP.${test}:Q}'' ${MOD.${test}:Q}''
+.warning error in ${test}: expected "${EXP.${test}}", got "${MOD.${test}}"
 .  endif
 .endfor
+
+all:
+	@echo ok
diff --git a/unit-tests/varmod-exclam-shell.exp b/unit-tests/varmod-exclam-shell.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-exclam-shell.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-exclam-shell.mk b/unit-tests/varmod-exclam-shell.mk
new file mode 100644
index 000000000000..2e811ddb4f5c
--- /dev/null
+++ b/unit-tests/varmod-exclam-shell.mk
@@ -0,0 +1,28 @@
+# $NetBSD: varmod-exclam-shell.mk,v 1.2 2020/08/16 12:48:55 rillig Exp $
+#
+# Tests for the :!cmd! variable modifier.
+
+.if ${:!echo hello | tr 'l' 'l'!} != "hello"
+.warning unexpected
+.endif
+
+# The output is truncated at the first null byte.
+# Cmd_Exec returns only a string pointer without length information.
+.if ${:!echo hello | tr 'l' '\0'!} != "he"
+.warning unexpected
+.endif
+
+.if ${:!echo!} != ""
+.warning A newline at the end of the output must be stripped.
+.endif
+
+.if ${:!echo;echo!} != " "
+.warning Only a single newline at the end of the output is stripped.
+.endif
+
+.if ${:!echo;echo;echo;echo!} != "   "
+.warning Other newlines in the output are converted to spaces.
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/varmod-extension.exp b/unit-tests/varmod-extension.exp
new file mode 100644
index 000000000000..24f7403c7f3f
--- /dev/null
+++ b/unit-tests/varmod-extension.exp
@@ -0,0 +1,10 @@
+extension of 'a/b/c' is ''
+extension of 'def' is ''
+extension of 'a.b.c' is 'c'
+extension of 'a.b/c' is 'b/c'
+extension of 'a' is ''
+extension of 'a.a' is 'a'
+extension of '.gitignore' is 'gitignore'
+extension of 'a' is ''
+extension of 'a.a' is 'a'
+exit status 0
diff --git a/unit-tests/varmod-extension.mk b/unit-tests/varmod-extension.mk
new file mode 100644
index 000000000000..db501f7234c7
--- /dev/null
+++ b/unit-tests/varmod-extension.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-extension.mk,v 1.3 2020/08/23 15:09:15 rillig Exp $
+#
+# Tests for the :E variable modifier, which returns the filename extension
+# of each word in the variable.
+
+all:
+.for path in a/b/c def a.b.c a.b/c a a.a .gitignore a a.a
+	@echo "extension of '"${path:Q}"' is '"${path:E:Q}"'"
+.endfor
diff --git a/unit-tests/varmod-gmtime.exp b/unit-tests/varmod-gmtime.exp
new file mode 100644
index 000000000000..373e8de1a271
--- /dev/null
+++ b/unit-tests/varmod-gmtime.exp
@@ -0,0 +1,9 @@
+mod-gmtime:
+%Y
+2020
+%Y
+%Y
+mod-gmtime-indirect:
+make: Unknown modifier '1'
+
+exit status 0
diff --git a/unit-tests/varmod-gmtime.mk b/unit-tests/varmod-gmtime.mk
new file mode 100644
index 000000000000..0a05ad58d982
--- /dev/null
+++ b/unit-tests/varmod-gmtime.mk
@@ -0,0 +1,35 @@
+# $NetBSD: varmod-gmtime.mk,v 1.2 2020/08/16 12:48:55 rillig Exp $
+#
+# Tests for the :gmtime variable modifier, which formats a timestamp
+# using strftime(3).
+
+all:	mod-gmtime
+all:	mod-gmtime-indirect
+
+mod-gmtime:
+	@echo $@:
+	@echo ${%Y:L:gmtim=1593536400}		# modifier name too short
+	@echo ${%Y:L:gmtime=1593536400}		# 2020-07-01T00:00:00Z
+	@echo ${%Y:L:gmtimer=1593536400}	# modifier name too long
+	@echo ${%Y:L:gm=gm:M*}
+
+mod-gmtime-indirect:
+	@echo $@:
+
+	# As of 2020-08-16, it is not possible to pass the seconds via a
+	# variable expression.  This is because parsing of the :gmtime
+	# modifier stops at the '$' and returns to ApplyModifiers.
+	#
+	# There, a colon would be skipped but not a dollar.
+	# Parsing therefore continues at the '$' of the ${:U159...}, looking
+	# for an ordinary variable modifier.
+	#
+	# At this point, the ${:U} is expanded and interpreted as a variable
+	# modifier, which results in the error message "Unknown modifier '1'".
+	#
+	# If ApplyModifier_Gmtime were to pass its argument through
+	# ParseModifierPart, this would work.
+	@echo ${%Y:L:gmtime=${:U1593536400}}
+
+all:
+	@:;
diff --git a/unit-tests/varmod-hash.exp b/unit-tests/varmod-hash.exp
new file mode 100644
index 000000000000..f16f30903539
--- /dev/null
+++ b/unit-tests/varmod-hash.exp
@@ -0,0 +1,9 @@
+make: Unknown modifier 'h'
+
+26bb0f5f
+12345
+make: Unknown modifier 'h'
+
+make: Unknown modifier 'h'
+
+exit status 0
diff --git a/unit-tests/varmod-hash.mk b/unit-tests/varmod-hash.mk
new file mode 100644
index 000000000000..ef59268cb82c
--- /dev/null
+++ b/unit-tests/varmod-hash.mk
@@ -0,0 +1,10 @@
+# $NetBSD: varmod-hash.mk,v 1.3 2020/08/23 15:13:21 rillig Exp $
+#
+# Tests for the :hash variable modifier.
+
+all:
+	@echo ${12345:L:has}			# modifier name too short
+	@echo ${12345:L:hash}			# ok
+	@echo ${12345:L:hash=SHA-256}		# :hash does not accept '='
+	@echo ${12345:L:hasX}			# misspelled
+	@echo ${12345:L:hashed}			# modifier name too long
diff --git a/unit-tests/varmod-head.exp b/unit-tests/varmod-head.exp
new file mode 100644
index 000000000000..f0bf87f03012
--- /dev/null
+++ b/unit-tests/varmod-head.exp
@@ -0,0 +1,10 @@
+head (dirname) of 'a/b/c' is 'a/b'
+head (dirname) of 'def' is '.'
+head (dirname) of 'a.b.c' is '.'
+head (dirname) of 'a.b/c' is 'a.b'
+head (dirname) of 'a' is '.'
+head (dirname) of 'a.a' is '.'
+head (dirname) of '.gitignore' is '.'
+head (dirname) of 'a' is '.'
+head (dirname) of 'a.a' is '.'
+exit status 0
diff --git a/unit-tests/varmod-head.mk b/unit-tests/varmod-head.mk
new file mode 100644
index 000000000000..eda4820c5f14
--- /dev/null
+++ b/unit-tests/varmod-head.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-head.mk,v 1.3 2020/08/23 15:09:15 rillig Exp $
+#
+# Tests for the :H variable modifier, which returns the dirname of
+# each of the words in the variable value.
+
+all:
+.for path in a/b/c def a.b.c a.b/c a a.a .gitignore a a.a
+	@echo "head (dirname) of '"${path:Q}"' is '"${path:H:Q}"'"
+.endfor
diff --git a/unit-tests/varmod-ifelse.exp b/unit-tests/varmod-ifelse.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-ifelse.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-ifelse.mk b/unit-tests/varmod-ifelse.mk
new file mode 100644
index 000000000000..e8e92b35d1c1
--- /dev/null
+++ b/unit-tests/varmod-ifelse.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-ifelse.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the ${cond:?then:else} variable modifier, which evaluates either
+# the then-expression or the else-expression, depending on the condition.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varmod-l-name-to-value.exp b/unit-tests/varmod-l-name-to-value.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-l-name-to-value.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-l-name-to-value.mk b/unit-tests/varmod-l-name-to-value.mk
new file mode 100644
index 000000000000..b8a5877e1100
--- /dev/null
+++ b/unit-tests/varmod-l-name-to-value.mk
@@ -0,0 +1,31 @@
+# $NetBSD: varmod-l-name-to-value.mk,v 1.3 2020/08/25 22:25:05 rillig Exp $
+#
+# Tests for the :L modifier, which returns the variable name as the new value.
+
+# The empty variable name leads to an empty string.
+.if ${:L} != ""
+.error
+.endif
+
+# The variable name is converted into an expression with the variable name
+# "VARNAME" and the value "VARNAME".
+.if ${VARNAME:L} != "VARNAME"
+.error
+.endif
+
+# The value of the expression can be modified afterwards.
+.if ${VARNAME:L:S,VAR,,} != "NAME"
+.error
+.endif
+
+# The name of the expression is still the same as before. Using the :L
+# modifier, it can be restored.
+#
+# Hmmm, this can be used as a double storage or a backup mechanism.
+# Probably unintended, but maybe useful.
+.if ${VARNAME:L:S,VAR,,:L} != "VARNAME"
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/varmod-localtime.exp b/unit-tests/varmod-localtime.exp
new file mode 100644
index 000000000000..69e4335be187
--- /dev/null
+++ b/unit-tests/varmod-localtime.exp
@@ -0,0 +1,4 @@
+%Y
+2020
+%Y
+exit status 0
diff --git a/unit-tests/varmod-localtime.mk b/unit-tests/varmod-localtime.mk
new file mode 100644
index 000000000000..fa4fd4f9cfb1
--- /dev/null
+++ b/unit-tests/varmod-localtime.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-localtime.mk,v 1.3 2020/08/23 15:13:21 rillig Exp $
+#
+# Tests for the :localtime variable modifier, which returns the given time,
+# formatted as a local timestamp.
+
+all:
+	@echo ${%Y:L:localtim=1593536400}	# modifier name too short
+	@echo ${%Y:L:localtime=1593536400}	# 2020-07-01T00:00:00Z
+	@echo ${%Y:L:localtimer=1593536400}	# modifier name too long
diff --git a/unit-tests/varmod-loop.exp b/unit-tests/varmod-loop.exp
new file mode 100644
index 000000000000..ce93eb5bc0c8
--- /dev/null
+++ b/unit-tests/varmod-loop.exp
@@ -0,0 +1,16 @@
+:+one+ +two+ +three+:
+:x1y x2y x3y:
+:x1y x2y x3y:
+:mod-loop-varname: :x1y x2y x3y: ::
+:x1y x2y x3y:
+mod-loop-resolve:w1d2d3w w2i3w w1i2d3 2i${RES3}w w1d2d3 2i${RES3} 1i${RES2}w:
+mod-loop-varname-dollar:(1) (2) (3).
+mod-loop-varname-dollar:() () ().
+mod-loop-varname-dollar:() () ().
+mod-loop-dollar:1:
+mod-loop-dollar:${word}$:
+mod-loop-dollar:$3$:
+mod-loop-dollar:$${word}$$:
+mod-loop-dollar:$$5$$:
+mod-loop-dollar:$$${word}$$$:
+exit status 0
diff --git a/unit-tests/varmod-loop.mk b/unit-tests/varmod-loop.mk
new file mode 100644
index 000000000000..561f4b95baa0
--- /dev/null
+++ b/unit-tests/varmod-loop.mk
@@ -0,0 +1,63 @@
+# $NetBSD: varmod-loop.mk,v 1.2 2020/08/16 12:30:45 rillig Exp $
+#
+# Tests for the :@var@...${var}...@ variable modifier.
+
+all: mod-loop-varname
+all: mod-loop-resolve
+all: mod-loop-varname-dollar
+all: mod-loop-dollar
+
+# In the :@ modifier, the name of the loop variable can even be generated
+# dynamically.  There's no practical use-case for this, and hopefully nobody
+# will ever depend on this, but technically it's possible.
+# Therefore, in -dL mode, this is forbidden, see lint.mk.
+mod-loop-varname:
+	@echo :${:Uone two three:@${:Ubar:S,b,v,}@+${var}+@:Q}:
+	# ":::" is a very creative variable name, unlikely in practice
+	# The expression ${\:\:\:} would not work since backslashes can only
+	# be escaped in the modifiers, but not in the variable name.
+	@echo :${:U1 2 3:@:::@x${${:U\:\:\:}}y@}:
+	# "@@" is another creative variable name.
+	@echo :${:U1 2 3:@\@\@@x${@@}y@}:
+	# Even "@" works as a variable name since the variable is installed
+	# in the "current" scope, which in this case is the one from the
+	# target.
+	@echo :$@: :${:U1 2 3:@\@@x${@}y@}: :$@:
+	# In extreme cases, even the backslash can be used as variable name.
+	# It needs to be doubled though.
+	@echo :${:U1 2 3:@\\@x${${:Ux:S,x,\\,}}y@}:
+
+# The :@ modifier resolves the variables a little more often than expected.
+# In particular, it resolves _all_ variables from the context, and not only
+# the loop variable (in this case v).
+#
+# The d means direct reference, the i means indirect reference.
+RESOLVE=	${RES1} $${RES1}
+RES1=		1d${RES2} 1i$${RES2}
+RES2=		2d${RES3} 2i$${RES3}
+RES3=		3
+
+mod-loop-resolve:
+	@echo $@:${RESOLVE:@v@w${v}w@:Q}:
+
+# Until 2020-07-20, the variable name of the :@ modifier could end with one
+# or two dollar signs, which were silently ignored.
+# There's no point in allowing a dollar sign in that position.
+mod-loop-varname-dollar:
+	@echo $@:${1 2 3:L:@v$@($v)@:Q}.
+	@echo $@:${1 2 3:L:@v$$@($v)@:Q}.
+	@echo $@:${1 2 3:L:@v$$$@($v)@:Q}.
+
+# Demonstrate that it is possible to generate dollar characters using the
+# :@ modifier.
+#
+# These are edge cases that could have resulted in a parse error as well
+# since the $@ at the end could have been interpreted as a variable, which
+# would mean a missing closing @ delimiter.
+mod-loop-dollar:
+	@echo $@:${:U1:@word@${word}$@:Q}:
+	@echo $@:${:U2:@word@$${word}$$@:Q}:
+	@echo $@:${:U3:@word@$$${word}$$$@:Q}:
+	@echo $@:${:U4:@word@$$$${word}$$$$@:Q}:
+	@echo $@:${:U5:@word@$$$$${word}$$$$$@:Q}:
+	@echo $@:${:U6:@word@$$$$$${word}$$$$$$@:Q}:
diff --git a/unit-tests/varmod-match-escape.exp b/unit-tests/varmod-match-escape.exp
new file mode 100755
index 000000000000..1e4c030c5b42
--- /dev/null
+++ b/unit-tests/varmod-match-escape.exp
@@ -0,0 +1,3 @@
+Pattern[SPECIALS] for [\: : \\ * \*] is [\:]
+Pattern[SPECIALS] for [\: : \\ * \*] is [:]
+exit status 0
diff --git a/unit-tests/varmod-match-escape.mk b/unit-tests/varmod-match-escape.mk
new file mode 100755
index 000000000000..7913bb476ae7
--- /dev/null
+++ b/unit-tests/varmod-match-escape.mk
@@ -0,0 +1,20 @@
+# $NetBSD: varmod-match-escape.mk,v 1.1 2020/08/16 20:03:53 rillig Exp $
+#
+# As of 2020-08-01, the :M and :N modifiers interpret backslashes differently,
+# depending on whether there was a variable expression somewhere before the
+# first backslash or not.  See ApplyModifier_Match, "copy = TRUE".
+#
+# Apart from the different and possibly confusing debug output, there is no
+# difference in behavior.  When parsing the modifier text, only \{, \} and \:
+# are unescaped, and in the pattern matching these have the same meaning as
+# their plain variants '{', '}' and ':'.  In the pattern matching from
+# Str_Match, only \*, \? or \[ would make a noticeable difference.
+SPECIALS=	\: : \\ * \*
+RELEVANT=	yes
+.if ${SPECIALS:M${:U}\:} != ${SPECIALS:M\:${:U}}
+.warning unexpected
+.endif
+RELEVANT=	no
+
+all:
+	@:;
diff --git a/unit-tests/varmod-match.exp b/unit-tests/varmod-match.exp
new file mode 100644
index 000000000000..11777e086d4f
--- /dev/null
+++ b/unit-tests/varmod-match.exp
@@ -0,0 +1,5 @@
+match-char-class:
+  uppercase numbers: One Two Three Four
+  all the others: five six seven
+  starts with non-s, ends with [ex]: One Three five
+exit status 0
diff --git a/unit-tests/varmod-match.mk b/unit-tests/varmod-match.mk
new file mode 100644
index 000000000000..805426a0fda9
--- /dev/null
+++ b/unit-tests/varmod-match.mk
@@ -0,0 +1,22 @@
+# $NetBSD: varmod-match.mk,v 1.3 2020/08/16 20:03:53 rillig Exp $
+#
+# Tests for the :M variable modifier, which filters words that match the
+# given pattern.
+
+all: match-char-class
+all: slow
+
+
+NUMBERS=	One Two Three Four five six seven
+
+match-char-class:
+	@echo '$@:'
+	@echo '  uppercase numbers: ${NUMBERS:M[A-Z]*}'
+	@echo '  all the others: ${NUMBERS:M[^A-Z]*}'
+	@echo '  starts with non-s, ends with [ex]: ${NUMBERS:M[^s]*[ex]}'
+
+
+# Before 2020-06-13, this expression took quite a long time in Str_Match,
+# calling itself 601080390 times for 16 asterisks.
+slow:
+	@: ${:U****************:M****************b}
diff --git a/unit-tests/varmod-no-match.exp b/unit-tests/varmod-no-match.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-no-match.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-no-match.mk b/unit-tests/varmod-no-match.mk
new file mode 100644
index 000000000000..2acb27e2e727
--- /dev/null
+++ b/unit-tests/varmod-no-match.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-no-match.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the :N variable modifier, which filters words that do not match
+# the given pattern.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varmod-order-reverse.exp b/unit-tests/varmod-order-reverse.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-order-reverse.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-order-reverse.mk b/unit-tests/varmod-order-reverse.mk
new file mode 100644
index 000000000000..fa9d179560c8
--- /dev/null
+++ b/unit-tests/varmod-order-reverse.mk
@@ -0,0 +1,13 @@
+# $NetBSD: varmod-order-reverse.mk,v 1.3 2020/08/16 20:13:10 rillig Exp $
+#
+# Tests for the :Or variable modifier, which returns the words, sorted in
+# descending order.
+
+NUMBERS=	one two three four five six seven eight nine ten
+
+.if ${NUMBERS:Or} != "two three ten six seven one nine four five eight"
+.error ${NUMBERS:Or}
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/varmod-order-shuffle.exp b/unit-tests/varmod-order-shuffle.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-order-shuffle.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-order-shuffle.mk b/unit-tests/varmod-order-shuffle.mk
new file mode 100644
index 000000000000..b6eb6be4b1bc
--- /dev/null
+++ b/unit-tests/varmod-order-shuffle.mk
@@ -0,0 +1,39 @@
+# $NetBSD: varmod-order-shuffle.mk,v 1.3 2020/08/16 20:43:01 rillig Exp $
+#
+# Tests for the :Ox variable modifier, which returns the words of the
+# variable, shuffled.
+#
+# As of 2020-08-16, make uses random(3) seeded by the current time in seconds.
+# This makes the random numbers completely predictable since there is no other
+# part of make that uses random numbers.
+
+NUMBERS=	one two three four five six seven eight nine ten
+
+# Note that 1 in every 10! trials two independently generated
+# randomized orderings will be the same.  The test framework doesn't
+# support checking probabilistic output, so we accept that each of the
+# 3 :Ox tests will incorrectly fail with probability 2.756E-7, which
+# lets the whole test fail once in 1.209.600 runs, on average.
+
+# Create two shuffles using the := assignment operator.
+shuffled1:=	${NUMBERS:Ox}
+shuffled2:=	${NUMBERS:Ox}
+.if ${shuffled1} == ${shuffled2}
+.error ${shuffled1} == ${shuffled2}
+.endif
+
+# Sorting the list before shuffling it has no effect.
+shuffled1:=	${NUMBERS:O:Ox}
+shuffled2:=	${NUMBERS:O:Ox}
+.if ${shuffled1} == ${shuffled2}
+.error ${shuffled1} == ${shuffled2}
+.endif
+
+# Sorting after shuffling must produce the original numbers.
+sorted:=	${NUMBERS:Ox:O}
+.if ${sorted} != ${NUMBERS:O}
+.error ${sorted} != ${NUMBERS:O}
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/varmod-order.exp b/unit-tests/varmod-order.exp
new file mode 100644
index 000000000000..99d1d6ef164c
--- /dev/null
+++ b/unit-tests/varmod-order.exp
@@ -0,0 +1,7 @@
+make: Bad modifier `:OX' for NUMBERS
+make: "varmod-order.mk" line 13: Undefined variable "${NUMBERS:OX"
+make: Bad modifier `:OxXX' for NUMBERS
+make: "varmod-order.mk" line 16: Undefined variable "${NUMBERS:Ox"
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/varmod-order.mk b/unit-tests/varmod-order.mk
new file mode 100644
index 000000000000..b079bdc34217
--- /dev/null
+++ b/unit-tests/varmod-order.mk
@@ -0,0 +1,19 @@
+# $NetBSD: varmod-order.mk,v 1.4 2020/08/16 20:43:01 rillig Exp $
+#
+# Tests for the :O variable modifier, which returns the words, sorted in
+# ascending order.
+
+NUMBERS=	one two three four five six seven eight nine ten
+
+.if ${NUMBERS:O} != "eight five four nine one seven six ten three two"
+.error ${NUMBERS:O}
+.endif
+
+# Unknown modifier "OX"
+_:=	${NUMBERS:OX}
+
+# Unknown modifier "OxXX"
+_:=	${NUMBERS:OxXX}
+
+all:
+	@:;
diff --git a/unit-tests/varmod-path.exp b/unit-tests/varmod-path.exp
new file mode 100644
index 000000000000..ae7f6cc85748
--- /dev/null
+++ b/unit-tests/varmod-path.exp
@@ -0,0 +1,4 @@
+varmod-path.subdir/varmod-path.phony
+varmod-path.subdir/varmod-path.real
+varmod-path.enoent
+exit status 0
diff --git a/unit-tests/varmod-path.mk b/unit-tests/varmod-path.mk
new file mode 100644
index 000000000000..ebbf755ddbec
--- /dev/null
+++ b/unit-tests/varmod-path.mk
@@ -0,0 +1,35 @@
+# $NetBSD: varmod-path.mk,v 1.3 2020/08/23 08:10:49 rillig Exp $
+#
+# Tests for the :P variable modifier, which looks up the path for a given
+# target.
+#
+# The phony target does not have a corresponding path, therefore ... oops,
+# as of 2020-08-23 it is nevertheless resolved to a path.  This is probably
+# unintended.
+#
+# The real target is located in a subdirectory, and its full path is returned.
+# If it had been in the current directory, the difference between its path and
+# its name would not be visible.
+#
+# The enoent target does not exist, therefore the target name is returned.
+
+.MAIN: all
+
+_!=	rm -rf varmod-path.subdir
+_!=	mkdir varmod-path.subdir
+_!=	> varmod-path.subdir/varmod-path.phony
+_!=	> varmod-path.subdir/varmod-path.real
+
+# To have an effect, this .PATH declaration must be after the directory is created.
+.PATH: varmod-path.subdir
+
+varmod-path.phony: .PHONY
+varmod-path.real:
+
+all: varmod-path.phony varmod-path.real
+	@echo ${varmod-path.phony:P}
+	@echo ${varmod-path.real:P}
+	@echo ${varmod-path.enoent:P}
+
+.END:
+	@rm -rf varmod-path.subdir
diff --git a/unit-tests/varmod-quote-dollar.exp b/unit-tests/varmod-quote-dollar.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-quote-dollar.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-quote-dollar.mk b/unit-tests/varmod-quote-dollar.mk
new file mode 100644
index 000000000000..fedbe8a10f4b
--- /dev/null
+++ b/unit-tests/varmod-quote-dollar.mk
@@ -0,0 +1,10 @@
+# $NetBSD: varmod-quote-dollar.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the :q variable modifier, which quotes the string for the shell
+# and doubles dollar signs, to prevent them from being interpreted by a
+# child process of make.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varmod-quote.exp b/unit-tests/varmod-quote.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-quote.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-quote.mk b/unit-tests/varmod-quote.mk
new file mode 100644
index 000000000000..adf736048e76
--- /dev/null
+++ b/unit-tests/varmod-quote.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-quote.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the :Q variable modifier, which quotes the variable value
+# to be used in a shell program.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varmod-range.exp b/unit-tests/varmod-range.exp
new file mode 100644
index 000000000000..bdbd68edeb45
--- /dev/null
+++ b/unit-tests/varmod-range.exp
@@ -0,0 +1,8 @@
+make: Unknown modifier 'r'
+
+1 2 3
+make: Unknown modifier 'r'
+
+make: Unknown modifier 'r'
+
+exit status 0
diff --git a/unit-tests/varmod-range.mk b/unit-tests/varmod-range.mk
new file mode 100644
index 000000000000..9fd79cc97a81
--- /dev/null
+++ b/unit-tests/varmod-range.mk
@@ -0,0 +1,10 @@
+# $NetBSD: varmod-range.mk,v 1.3 2020/08/23 15:13:21 rillig Exp $
+#
+# Tests for the :range variable modifier, which generates sequences
+# of integers from the given range.
+
+all:
+	@echo ${a b c:L:rang}			# modifier name too short
+	@echo ${a b c:L:range}			# ok
+	@echo ${a b c:L:rango}			# misspelled
+	@echo ${a b c:L:ranger}			# modifier name too long
diff --git a/unit-tests/varmod-remember.exp b/unit-tests/varmod-remember.exp
new file mode 100644
index 000000000000..448f817d8969
--- /dev/null
+++ b/unit-tests/varmod-remember.exp
@@ -0,0 +1,3 @@
+1 2 3 1 2 3 1 2 3
+1 2 3, SAVED=3
+exit status 0
diff --git a/unit-tests/varmod-remember.mk b/unit-tests/varmod-remember.mk
new file mode 100644
index 000000000000..68eb96a122c4
--- /dev/null
+++ b/unit-tests/varmod-remember.mk
@@ -0,0 +1,12 @@
+# $NetBSD: varmod-remember.mk,v 1.3 2020/08/23 15:18:43 rillig Exp $
+#
+# Tests for the :_ modifier, which saves the current variable value
+# in the _ variable or another, to be used later again.
+
+# In the parameterized form, having the variable name on the right side of
+# the = assignment operator is confusing.  In almost all other situations
+# the variable name is on the left-hand side of the = operator.  Luckily
+# this modifier is only rarely needed.
+all:
+	@echo ${1 2 3:L:_:@var@${_}@}
+	@echo ${1 2 3:L:@var@${var:_=SAVED:}@}, SAVED=${SAVED}
diff --git a/unit-tests/varmod-root.exp b/unit-tests/varmod-root.exp
new file mode 100644
index 000000000000..24ecbb875f77
--- /dev/null
+++ b/unit-tests/varmod-root.exp
@@ -0,0 +1,10 @@
+root of 'a/b/c' is 'a/b/c'
+root of 'def' is 'def'
+root of 'a.b.c' is 'a.b'
+root of 'a.b/c' is 'a'
+root of 'a' is 'a'
+root of 'a.a' is 'a'
+root of '.gitignore' is ''
+root of 'a' is 'a'
+root of 'a.a' is 'a'
+exit status 0
diff --git a/unit-tests/varmod-root.mk b/unit-tests/varmod-root.mk
new file mode 100644
index 000000000000..88af42d82510
--- /dev/null
+++ b/unit-tests/varmod-root.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-root.mk,v 1.3 2020/08/23 15:09:15 rillig Exp $
+#
+# Tests for the :R variable modifier, which returns the filename root
+# without the extension.
+
+all:
+.for path in a/b/c def a.b.c a.b/c a a.a .gitignore a a.a
+	@echo "root of '"${path:Q}"' is '"${path:R:Q}"'"
+.endfor
diff --git a/unit-tests/varmod-select-words.exp b/unit-tests/varmod-select-words.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-select-words.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-select-words.mk b/unit-tests/varmod-select-words.mk
new file mode 100644
index 000000000000..a9df25f9ff32
--- /dev/null
+++ b/unit-tests/varmod-select-words.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-select-words.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the :[...] variable modifier, which selects a single word
+# or a range of words from a variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varmod-shell.exp b/unit-tests/varmod-shell.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-shell.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-shell.mk b/unit-tests/varmod-shell.mk
new file mode 100644
index 000000000000..052968004f1b
--- /dev/null
+++ b/unit-tests/varmod-shell.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-shell.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the :sh variable modifier, which runs the shell command
+# given by the variable value and returns its output.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varmod-subst-regex.exp b/unit-tests/varmod-subst-regex.exp
new file mode 100644
index 000000000000..eb9ae7f41fb9
--- /dev/null
+++ b/unit-tests/varmod-subst-regex.exp
@@ -0,0 +1,23 @@
+make: Regex compilation error: (details omitted)
+mod-regex-compile-error: C,word,____,:Q}.
+make: No subexpression \1
+make: No subexpression \1
+make: No subexpression \1
+make: No subexpression \1
+mod-regex-limits:11-missing:1 6
+mod-regex-limits:11-ok:1 22 446
+make: No subexpression \2
+make: No subexpression \2
+make: No subexpression \2
+make: No subexpression \2
+mod-regex-limits:22-missing:1 6
+make: No subexpression \2
+make: No subexpression \2
+make: No subexpression \2
+make: No subexpression \2
+mod-regex-limits:22-missing:1 6
+mod-regex-limits:22-ok:1 33 556
+mod-regex-limits:capture:ihgfedcbaabcdefghijABCDEFGHIJa0a1a2rest
+make: Regex compilation error: (details omitted)
+mod-regex-errors:
+exit status 0
diff --git a/unit-tests/varmod-subst-regex.mk b/unit-tests/varmod-subst-regex.mk
new file mode 100644
index 000000000000..fe2758e401ec
--- /dev/null
+++ b/unit-tests/varmod-subst-regex.mk
@@ -0,0 +1,87 @@
+# $NetBSD: varmod-subst-regex.mk,v 1.3 2020/08/28 17:15:04 rillig Exp $
+#
+# Tests for the :C,from,to, variable modifier.
+
+all: mod-regex-compile-error
+all: mod-regex-limits
+all: mod-regex-errors
+
+# The variable expression expands to 4 words.  Of these words, none matches
+# the regular expression "a b" since these words don't contain any
+# whitespace.
+.if ${:Ua b b c:C,a b,,} != "a b b c"
+.error
+.endif
+
+# Using the '1' modifier does not change anything.  The '1' modifier just
+# means to apply at most 1 replacement in the whole variable expression.
+.if ${:Ua b b c:C,a b,,1} != "a b b c"
+.error
+.endif
+
+# The 'W' modifier treats the whole variable value as a single big word,
+# containing whitespace.  This big word matches the regular expression,
+# therefore it gets replaced.  Whitespace is preserved after replacing.
+.if ${:Ua b b c:C,a b,,W} != " b c"
+.error
+.endif
+
+# The 'g' modifier does not have any effect here since each of the words
+# contains the character 'b' a single time.
+.if ${:Ua b b c:C,b,,g} != "a c"
+.error
+.endif
+
+# The first :C modifier has the 'W' modifier, which makes the whole
+# expression a single word.  The 'g' modifier then replaces all occurrences
+# of "1 2" with "___".  The 'W' modifier only applies to this single :C
+# modifier.  This is demonstrated by the :C modifier that follows.  If the
+# 'W' modifier would be preserved, only a single underscore would have been
+# replaced with an 'x'.
+.if ${:U1 2 3 1 2 3:C,1 2,___,Wg:C,_,x,} != "x__ 3 x__ 3"
+.error
+.endif
+
+# The regular expression does not match in the first word.
+# It matches once in the second word, and the \0\0 doubles that word.
+# In the third word, the regular expression matches as early as possible,
+# and since the matches must not overlap, the next possible match would
+# start at the 6, but at that point, there is only one character left,
+# and that cannot match the regular expression "..".  Therefore only the
+# "45" is doubled in the result.
+.if ${:U1 23 456:C,..,\0\0,} != "1 2323 45456"
+.error
+.endif
+
+# The modifier '1' applies the replacement at most once, across the whole
+# variable value, no matter whether it is a single big word or many small
+# words.
+#
+# Up to 2020-08-28, the manual page said that the modifiers '1' and 'g'
+# were orthogonal, which was wrong.
+.if ${:U12345 12345:C,.,\0\0,1} != "112345 12345"
+.error
+.endif
+
+# Multiple asterisks form an invalid regular expression.  This produces an
+# error message and (as of 2020-08-28) stops parsing in the middle of the
+# variable expression.  The unparsed part of the expression is then copied
+# verbatim to the output, which is unexpected and can lead to strange shell
+# commands being run.
+mod-regex-compile-error:
+	@echo $@: ${:Uword1 word2:C,****,____,g:C,word,____,:Q}.
+
+# These tests generate error messages but as of 2020-08-28 just continue
+# parsing and execution as if nothing bad had happened.
+mod-regex-limits:
+	@echo $@:11-missing:${:U1 23 456:C,..,\1\1,:Q}
+	@echo $@:11-ok:${:U1 23 456:C,(.).,\1\1,:Q}
+	@echo $@:22-missing:${:U1 23 456:C,..,\2\2,:Q}
+	@echo $@:22-missing:${:U1 23 456:C,(.).,\2\2,:Q}
+	@echo $@:22-ok:${:U1 23 456:C,(.)(.),\2\2,:Q}
+	# The :C modifier only handles single-digit capturing groups,
+	# which is more than enough for daily use.
+	@echo $@:capture:${:UabcdefghijABCDEFGHIJrest:C,(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.)(.),\9\8\7\6\5\4\3\2\1\0\10\11\12,}
+
+mod-regex-errors:
+	@echo $@: ${UNDEF:Uvalue:C,[,,}
diff --git a/unit-tests/varmod-subst.exp b/unit-tests/varmod-subst.exp
new file mode 100644
index 000000000000..e752fb8058a8
--- /dev/null
+++ b/unit-tests/varmod-subst.exp
@@ -0,0 +1,51 @@
+mod-subst:
+:a b b c:
+:a b b c:
+: b c:
+:a c:
+:x__ 3 x__ 3:
+12345
+mod-subst-delimiter:
+1 two 3 horizontal tabulator
+1 two 3 space
+1 two 3 exclamation mark
+1 two 3 double quotes
+1 two 3 hash
+1 two 3 dollar
+1 two 3 percent
+1 two 3 apostrophe
+1 two 3 opening parenthesis
+1 two 3 closing parenthesis
+1 two 3 digit
+1 two 3 colon
+1 two 3 less than sign
+1 two 3 equal sign
+1 two 3 greater than sign
+1 two 3 question mark
+1 two 3 at
+1 two 3 letter
+1 two 3 opening bracket
+1 two 3 backslash
+1 two 3 closing bracket
+1 two 3 caret
+1 two 3 opening brace
+1 two 3 vertical line
+1 two 3 closing brace
+1 two 3 tilde
+mod-subst-chain:
+A B c.
+make: Unknown modifier 'i'
+.
+mod-subst-dollar:$1:
+mod-subst-dollar:$2:
+mod-subst-dollar:$3:
+mod-subst-dollar:$4:
+mod-subst-dollar:$5:
+mod-subst-dollar:$6:
+mod-subst-dollar:$7:
+mod-subst-dollar:$8:
+mod-subst-dollar:$40:
+mod-subst-dollar:U8:
+mod-subst-dollar:$$$$:
+mod-subst-dollar:$$$good3
+exit status 0
diff --git a/unit-tests/varmod-subst.mk b/unit-tests/varmod-subst.mk
new file mode 100644
index 000000000000..de8d54df8506
--- /dev/null
+++ b/unit-tests/varmod-subst.mk
@@ -0,0 +1,153 @@
+# $NetBSD: varmod-subst.mk,v 1.3 2020/08/19 06:10:06 rillig Exp $
+#
+# Tests for the :S,from,to, variable modifier.
+
+all: mod-subst
+all: mod-subst-delimiter
+all: mod-subst-chain
+all: mod-subst-dollar
+
+WORDS=		sequences of letters
+.if ${WORDS:S,,,} != ${WORDS}
+.warning The empty pattern matches something.
+.endif
+.if ${WORDS:S,e,*,1} != "s*quences of letters"
+.warning The :S modifier flag '1' is not applied exactly once.
+.endif
+.if ${WORDS:S,f,*,1} != "sequences o* letters"
+.warning The :S modifier flag '1' is only applied to the first word,\
+	 not to the first occurrence.
+.endif
+.if ${WORDS:S,e,*,} != "s*quences of l*tters"
+.warning The :S modifier does not replace every first match per word.
+.endif
+.if ${WORDS:S,e,*,g} != "s*qu*nc*s of l*tt*rs"
+.warning The :S modifier flag 'g' does not replace every occurrence.
+.endif
+.if ${WORDS:S,^sequ,occurr,} != "occurrences of letters"
+.warning The :S modifier fails for a short match anchored at the start.
+.endif
+.if ${WORDS:S,^of,with,} != "sequences with letters"
+.warning The :S modifier fails for an exact match anchored at the start.
+.endif
+.if ${WORDS:S,^office,does not match,} != ${WORDS}
+.warning The :S modifier matches a too long pattern anchored at the start.
+.endif
+.if ${WORDS:S,f$,r,} != "sequences or letters"
+.warning The :S modifier fails for a short match anchored at the end.
+.endif
+.if ${WORDS:S,s$,,} != "sequence of letter"
+.warning The :S modifier fails to replace one occurrence per word.
+.endif
+.if ${WORDS:S,of$,,} != "sequences letters"
+.warning The :S modifier fails for an exact match anchored at the end.
+.endif
+.if ${WORDS:S,eof$,,} != ${WORDS}
+.warning The :S modifier matches a too long pattern anchored at the end.
+.endif
+.if ${WORDS:S,^of$,,} != "sequences letters"
+.warning The :S modifier does not match a word anchored at both ends.
+.endif
+.if ${WORDS:S,^o$,,} != ${WORDS}
+.warning The :S modifier matches a prefix anchored at both ends.
+.endif
+.if ${WORDS:S,^f$,,} != ${WORDS}
+.warning The :S modifier matches a suffix anchored at both ends.
+.endif
+.if ${WORDS:S,^eof$,,} != ${WORDS}
+.warning The :S modifier matches a too long prefix anchored at both ends.
+.endif
+.if ${WORDS:S,^office$,,} != ${WORDS}
+.warning The :S modifier matches a too long suffix anchored at both ends.
+.endif
+
+mod-subst:
+	@echo $@:
+	@echo :${:Ua b b c:S,a b,,:Q}:
+	@echo :${:Ua b b c:S,a b,,1:Q}:
+	@echo :${:Ua b b c:S,a b,,W:Q}:
+	@echo :${:Ua b b c:S,b,,g:Q}:
+	@echo :${:U1 2 3 1 2 3:S,1 2,___,Wg:S,_,x,:Q}:
+	@echo ${:U12345:S,,sep,g:Q}
+
+# The :S and :C modifiers accept an arbitrary character as the delimiter,
+# including characters that are otherwise used as escape characters or
+# interpreted in a special way.  This can be used to confuse humans.
+mod-subst-delimiter:
+	@echo $@:
+	@echo ${:U1 2 3:S	2	two	:Q} horizontal tabulator
+	@echo ${:U1 2 3:S 2 two :Q} space
+	@echo ${:U1 2 3:S!2!two!:Q} exclamation mark
+	@echo ${:U1 2 3:S"2"two":Q} double quotes
+	# In shell command lines, the hash does not need to be escaped.
+	# It needs to be escaped in variable assignment lines though.
+	@echo ${:U1 2 3:S#2#two#:Q} hash
+	@echo ${:U1 2 3:S$2$two$:Q} dollar
+	@echo ${:U1 2 3:S%2%two%:Q} percent
+	@echo ${:U1 2 3:S'2'two':Q} apostrophe
+	@echo ${:U1 2 3:S(2(two(:Q} opening parenthesis
+	@echo ${:U1 2 3:S)2)two):Q} closing parenthesis
+	@echo ${:U1 2 3:S121two1:Q} digit
+	@echo ${:U1 2 3:S:2:two::Q} colon
+	@echo ${:U1 2 3:S<2<two<:Q} less than sign
+	@echo ${:U1 2 3:S=2=two=:Q} equal sign
+	@echo ${:U1 2 3:S>2>two>:Q} greater than sign
+	@echo ${:U1 2 3:S?2?two?:Q} question mark
+	@echo ${:U1 2 3:S@2@two@:Q} at
+	@echo ${:U1 2 3:Sa2atwoa:Q} letter
+	@echo ${:U1 2 3:S[2[two[:Q} opening bracket
+	@echo ${:U1 2 3:S\2\two\:Q} backslash
+	@echo ${:U1 2 3:S]2]two]:Q} closing bracket
+	@echo ${:U1 2 3:S^2^two^:Q} caret
+	@echo ${:U1 2 3:S{2{two{:Q} opening brace
+	@echo ${:U1 2 3:S|2|two|:Q} vertical line
+	@echo ${:U1 2 3:S}2}two}:Q} closing brace
+	@echo ${:U1 2 3:S~2~two~:Q} tilde
+
+# The :S and :C modifiers can be chained without a separating ':'.
+# This is not documented in the manual page.
+# It works because ApplyModifier_Subst scans for the known modifiers g1W
+# and then just returns to ApplyModifiers.  There, the colon is optionally
+# skipped (see the *st.next == ':' at the end of the loop).
+#
+# Most other modifiers cannot be chained since their parsers skip until
+# the next ':' or '}' or ')'.
+mod-subst-chain:
+	@echo $@:
+	@echo ${:Ua b c:S,a,A,S,b,B,}.
+	# There is no 'i' modifier for the :S or :C modifiers.
+	# The error message is "make: Unknown modifier 'i'", which is
+	# kind of correct, although it is mixing the terms for variable
+	# modifiers with the matching modifiers.
+	@echo ${:Uvalue:S,a,x,i}.
+
+# No matter how many dollar characters there are, they all get merged
+# into a single dollar by the :S modifier.
+#
+# As of 2020-08-09, this is because ParseModifierPart sees a '$' and
+# calls Var_Parse to expand the variable.  In all other places, the "$$"
+# is handled outside of Var_Parse.  Var_Parse therefore considers "$$"
+# one of the "really stupid names", skips the first dollar, and parsing
+# continues with the next character.  This repeats for the other dollar
+# signs, except the one before the delimiter.  That one is handled by
+# the code that optionally interprets the '$' as the end-anchor in the
+# first part of the :S modifier.  That code doesn't call Var_Parse but
+# simply copies the dollar to the result.
+mod-subst-dollar:
+	@echo $@:${:U1:S,^,$,:Q}:
+	@echo $@:${:U2:S,^,$$,:Q}:
+	@echo $@:${:U3:S,^,$$$,:Q}:
+	@echo $@:${:U4:S,^,$$$$,:Q}:
+	@echo $@:${:U5:S,^,$$$$$,:Q}:
+	@echo $@:${:U6:S,^,$$$$$$,:Q}:
+	@echo $@:${:U7:S,^,$$$$$$$,:Q}:
+	@echo $@:${:U8:S,^,$$$$$$$$,:Q}:
+	@echo $@:${:U40:S,^,$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$,:Q}:
+# This generates no dollar at all:
+	@echo $@:${:UU8:S,^,${:U$$$$$$$$},:Q}:
+# Here is an alternative way to generate dollar characters.
+# It's unexpectedly complicated though.
+	@echo $@:${:U:range=5:ts\x24:C,[0-9],,g:Q}:
+# In modifiers, dollars are escaped using the backslash, not using another
+# dollar sign.  Therefore, creating a dollar sign is pretty simple:
+	@echo $@:${:Ugood3:S,^,\$\$\$,:Q}
diff --git a/unit-tests/varmod-sysv.exp b/unit-tests/varmod-sysv.exp
new file mode 100644
index 000000000000..d9049c889823
--- /dev/null
+++ b/unit-tests/varmod-sysv.exp
@@ -0,0 +1,8 @@
+ax:Q b c d eb
+bcd.e
+&
+anchor-dollar: value
+anchor-dollar: valux
+mismatch: file.cpp file.h
+mismatch: renamed.c other.c
+exit status 0
diff --git a/unit-tests/varmod-sysv.mk b/unit-tests/varmod-sysv.mk
new file mode 100644
index 000000000000..c8410ed900d8
--- /dev/null
+++ b/unit-tests/varmod-sysv.mk
@@ -0,0 +1,61 @@
+# $NetBSD: varmod-sysv.mk,v 1.3 2020/08/23 14:52:06 rillig Exp $
+#
+# Tests for the ${VAR:from=to} variable modifier, which replaces the suffix
+# "from" with "to".  It can also use '%' as a wildcard.
+#
+# This modifier is applied when the other modifiers don't match exactly.
+
+all: words ampersand anchor-dollar mismatch
+
+# The :Q looks like a modifier but isn't.
+# It is part of the replacement string.
+words:
+	@echo a${a b c d e:L:%a=x:Q}b
+
+# Before 2020-07-19, an ampersand could be used in the replacement part
+# of a SysV substitution modifier.  This was probably a copy-and-paste
+# mistake since the SysV modifier code looked a lot like the code for the
+# :S and :C modifiers.  The ampersand is not mentioned in the manual page.
+ampersand:
+	@echo ${:U${a.bcd.e:L:a.%=%}:Q}
+	@echo ${:U${a.bcd.e:L:a.%=&}:Q}
+
+# Before 2020-07-20, when a SysV modifier was parsed, a single dollar
+# before the '=' was interpreted as an anchor, which doesn't make sense
+# since the anchor was discarded immediately.
+anchor-dollar:
+	@echo $@: ${:U${value:L:e$=x}:Q}
+	@echo $@: ${:U${value:L:e=x}:Q}
+
+# Words that don't match are copied unmodified.
+# The % placeholder can be anywhere in the string.
+mismatch:
+	@echo $@: ${:Ufile.c file.h:%.c=%.cpp}
+	@echo $@: ${:Ufile.c other.c:file.%=renamed.%}
+
+# Trying to cover all possible variants of the SysV modifier.
+LIST=	one two
+EXPR.1=	${LIST:o=X}
+EXP.1=	one twX
+EXPR.2=	${LIST:o=}
+EXP.2=	one tw
+EXPR.3=	${LIST:o=%}
+EXP.3=	one tw%
+EXPR.4=	${LIST:%o=X}
+EXP.4=	one X
+EXPR.5=	${LIST:o%=X}
+EXP.5=	X two
+EXPR.6=	${LIST:o%e=X}
+EXP.6=	X two
+EXPR.7=	${LIST:o%%e=X}		# Only the first '%' is the wildcard.
+EXP.7=	one two			# None of the words contains a literal '%'.
+EXPR.8=	${LIST:%=%%}
+EXP.8=	one% two%
+EXPR.9=	${LIST:%nes=%xxx}	# lhs is longer than the word "one"
+EXP.9=	one two
+
+.for i in ${:U:range=9}
+.if ${EXPR.$i} != ${EXP.$i}
+.warning test case $i expected "${EXP.$i}", got "${EXPR.$i}
+.endif
+.endfor
diff --git a/unit-tests/varmod-tail.exp b/unit-tests/varmod-tail.exp
new file mode 100644
index 000000000000..e25c1cc4b914
--- /dev/null
+++ b/unit-tests/varmod-tail.exp
@@ -0,0 +1,10 @@
+tail (basename) of 'a/b/c' is 'c'
+tail (basename) of 'def' is 'def'
+tail (basename) of 'a.b.c' is 'a.b.c'
+tail (basename) of 'a.b/c' is 'c'
+tail (basename) of 'a' is 'a'
+tail (basename) of 'a.a' is 'a.a'
+tail (basename) of '.gitignore' is '.gitignore'
+tail (basename) of 'a' is 'a'
+tail (basename) of 'a.a' is 'a.a'
+exit status 0
diff --git a/unit-tests/varmod-tail.mk b/unit-tests/varmod-tail.mk
new file mode 100644
index 000000000000..a8078cc67335
--- /dev/null
+++ b/unit-tests/varmod-tail.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-tail.mk,v 1.3 2020/08/23 15:09:15 rillig Exp $
+#
+# Tests for the :T variable modifier, which returns the basename of each of
+# the words in the variable value.
+
+all:
+.for path in a/b/c def a.b.c a.b/c a a.a .gitignore a a.a
+	@echo "tail (basename) of '"${path:Q}"' is '"${path:T:Q}"'"
+.endfor
diff --git a/unit-tests/varmod-to-abs.exp b/unit-tests/varmod-to-abs.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-to-abs.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-to-abs.mk b/unit-tests/varmod-to-abs.mk
new file mode 100644
index 000000000000..7a74e89088e5
--- /dev/null
+++ b/unit-tests/varmod-to-abs.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-to-abs.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the :tA variable modifier, which returns the absolute path for
+# each of the words in the variable value.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varmod-to-lower.exp b/unit-tests/varmod-to-lower.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-to-lower.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-to-lower.mk b/unit-tests/varmod-to-lower.mk
new file mode 100644
index 000000000000..38a4add4cfe5
--- /dev/null
+++ b/unit-tests/varmod-to-lower.mk
@@ -0,0 +1,19 @@
+# $NetBSD: varmod-to-lower.mk,v 1.3 2020/08/28 17:21:02 rillig Exp $
+#
+# Tests for the :tl variable modifier, which returns the words in the
+# variable value, converted to lowercase.
+
+.if ${:UUPPER:tl} != "upper"
+.error
+.endif
+
+.if ${:Ulower:tl} != "lower"
+.error
+.endif
+
+.if ${:UMixeD case.:tl} != "mixed case."
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/varmod-to-many-words.exp b/unit-tests/varmod-to-many-words.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-to-many-words.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-to-many-words.mk b/unit-tests/varmod-to-many-words.mk
new file mode 100644
index 000000000000..10cddb00c5e4
--- /dev/null
+++ b/unit-tests/varmod-to-many-words.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-to-many-words.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the :tw modifier, which treats the variable as many words,
+# to undo a previous :tW modifier.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varmod-to-one-word.exp b/unit-tests/varmod-to-one-word.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-to-one-word.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-to-one-word.mk b/unit-tests/varmod-to-one-word.mk
new file mode 100644
index 000000000000..0865ce8fb41f
--- /dev/null
+++ b/unit-tests/varmod-to-one-word.mk
@@ -0,0 +1,9 @@
+# $NetBSD: varmod-to-one-word.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the :tW variable modifier, which treats the variable value
+# as a single word, for all following modifiers.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varmod-to-separator.exp b/unit-tests/varmod-to-separator.exp
new file mode 100644
index 000000000000..07dd5b5e7c5b
--- /dev/null
+++ b/unit-tests/varmod-to-separator.exp
@@ -0,0 +1,9 @@
+make: Bad modifier `:tx' for WORDS
+make: "varmod-to-separator.mk" line 104: Malformed conditional (${WORDS:tx} != "anything")
+make: "varmod-to-separator.mk" line 108: Parsing continues here.
+make: Bad modifier `:t\X' for WORDS
+make: "varmod-to-separator.mk" line 112: Malformed conditional (${WORDS:t\X} != "anything")
+make: "varmod-to-separator.mk" line 115: Parsing continues here.
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/varmod-to-separator.mk b/unit-tests/varmod-to-separator.mk
new file mode 100644
index 000000000000..68e728c276dd
--- /dev/null
+++ b/unit-tests/varmod-to-separator.mk
@@ -0,0 +1,118 @@
+# $NetBSD: varmod-to-separator.mk,v 1.3 2020/08/31 19:58:21 rillig Exp $
+#
+# Tests for the :ts variable modifier, which joins the words of the variable
+# using an arbitrary character as word separator.
+
+WORDS=	one two three four five six
+
+# The words are separated by a single space, just as usual.
+.if ${WORDS:ts } != "one two three four five six"
+.  warning Space as separator does not work.
+.endif
+
+# The separator can be an arbitrary character, for example a comma.
+.if ${WORDS:ts,} != "one,two,three,four,five,six"
+.  warning Comma as separator does not work.
+.endif
+
+# After the :ts modifier, other modifiers can follow.
+.if ${WORDS:ts/:tu} != "ONE/TWO/THREE/FOUR/FIVE/SIX"
+.  warning Chaining modifiers does not work.
+.endif
+
+# To use the ':' as the separator, just write it normally.
+# The first colon is the separator, the second ends the modifier.
+.if ${WORDS:ts::tu} != "ONE:TWO:THREE:FOUR:FIVE:SIX"
+.  warning Colon as separator does not work.
+.endif
+
+# When there is just a colon but no other character, the words are
+# "separated" by an empty string, that is, they are all squashed
+# together.
+.if ${WORDS:ts:tu} != "ONETWOTHREEFOURFIVESIX"
+.  warning Colon as separator does not work.
+.endif
+
+# Applying the :tu modifier first and then the :ts modifier does not change
+# anything since neither of these modifiers is related to how the string is
+# split into words.  Beware of separating the words using a single or double
+# quote though, or other special characters like dollar or backslash.
+#
+# This example also demonstrates that the closing brace is not interpreted
+# as a separator, but as the closing delimiter of the whole variable
+# expression.
+.if ${WORDS:tu:ts} != "ONETWOTHREEFOURFIVESIX"
+.  warning Colon as separator does not work.
+.endif
+
+# The '}' plays the same role as the ':' in the preceding examples.
+# Since there is a single character before it, that character is taken as
+# the separator.
+.if ${WORDS:tu:ts/} != "ONE/TWO/THREE/FOUR/FIVE/SIX"
+.  warning Colon as separator does not work.
+.endif
+
+# Now it gets interesting and ambiguous:  The separator could either be empty
+# since it is followed by a colon.  Or it could be the colon since that
+# colon is followed by the closing brace.  It's the latter case.
+.if ${WORDS:ts:} != "one:two:three:four:five:six"
+.  warning Colon followed by closing brace does not work.
+.endif
+
+# As in the ${WORDS:tu:ts} example above, the separator is empty.
+.if ${WORDS:ts} != "onetwothreefourfivesix"
+.  warning Empty separator before closing brace does not work.
+.endif
+
+# The :ts modifier can be followed by other modifiers.
+.if ${WORDS:ts:S/two/2/} != "one2threefourfivesix"
+.  warning Separator followed by :S modifier does not work.
+.endif
+
+# The :ts modifier can follow other modifiers.
+.if ${WORDS:S/two/2/:ts} != "one2threefourfivesix"
+.  warning :S modifier followed by :ts modifier does not work.
+.endif
+
+# The :ts modifier with an actual separator can be followed by other
+# modifiers.
+.if ${WORDS:ts/:S/two/2/} != "one/2/three/four/five/six"
+.  warning The :ts modifier followed by an :S modifier does not work.
+.endif
+
+# The separator can be \n, which is a newline.
+.if ${WORDS:[1..3]:ts\n} != "one${.newline}two${.newline}three"
+.  warning The separator \n does not produce a newline.
+.endif
+
+# The separator can be \t, which is a tab.
+.if ${WORDS:[1..3]:ts\t} != "one	two	three"
+.  warning The separator \t does not produce a tab.
+.endif
+
+# The separator can be given as octal number.
+.if ${WORDS:[1..3]:ts\012:tu} != "ONE${.newline}TWO${.newline}THREE"
+.  warning The separator \012 is not interpreted in octal ASCII.
+.endif
+
+# The separator can be given as hexadecimal number.
+.if ${WORDS:[1..3]:ts\xa:tu} != "ONE${.newline}TWO${.newline}THREE"
+.  warning The separator \xa is not interpreted in hexadecimal ASCII.
+.endif
+
+# In the :t modifier, the :t must be followed by any of A, l, s, u.
+.if ${WORDS:tx} != "anything"
+.  info This line is not reached because of the malformed condition.
+.  info If this line were reached, it would be visible in the -dcpv log.
+.endif
+.info Parsing continues here.
+
+# After the backslash, only n, t, an octal number, or x and a hexadecimal
+# number are allowed.
+.if ${WORDS:t\X} != "anything"
+.  info This line is not reached.
+.endif
+.info Parsing continues here.
+
+all:
+	@:;
diff --git a/unit-tests/varmod-to-upper.exp b/unit-tests/varmod-to-upper.exp
new file mode 100644
index 000000000000..8453da538d3b
--- /dev/null
+++ b/unit-tests/varmod-to-upper.exp
@@ -0,0 +1,2 @@
+mod-tu-space: A   B
+exit status 0
diff --git a/unit-tests/varmod-to-upper.mk b/unit-tests/varmod-to-upper.mk
new file mode 100644
index 000000000000..26c0b25e5142
--- /dev/null
+++ b/unit-tests/varmod-to-upper.mk
@@ -0,0 +1,21 @@
+# $NetBSD: varmod-to-upper.mk,v 1.4 2020/08/28 17:21:02 rillig Exp $
+#
+# Tests for the :tu variable modifier, which returns the words in the
+# variable value, converted to uppercase.
+
+.if ${:UUPPER:tu} != "UPPER"
+.error
+.endif
+
+.if ${:Ulower:tu} != "LOWER"
+.error
+.endif
+
+.if ${:UMixeD case.:tu} != "MIXED CASE."
+.error
+.endif
+
+# The :tu and :tl modifiers operate on the variable value as a single string,
+# not as a list of words. Therefore, the adjacent spaces are preserved.
+mod-tu-space:
+	@echo $@: ${a   b:L:tu:Q}
diff --git a/unit-tests/varmod-undefined.exp b/unit-tests/varmod-undefined.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-undefined.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-undefined.mk b/unit-tests/varmod-undefined.mk
new file mode 100644
index 000000000000..9ed35a8406fe
--- /dev/null
+++ b/unit-tests/varmod-undefined.mk
@@ -0,0 +1,55 @@
+# $NetBSD: varmod-undefined.mk,v 1.3 2020/08/23 20:49:33 rillig Exp $
+#
+# Tests for the :U variable modifier, which returns the given string
+# if the variable is undefined.
+#
+# The pattern ${:Uword} is heavily used when expanding .for loops.
+
+# This is how an expanded .for loop looks like.
+# .for word in one
+# .  if ${word} != one
+# .    error ${word}
+# .  endif
+# .endfor
+
+.if ${:Uone} != one
+.  error ${:Uone}
+.endif
+
+# The variable expressions in the text of the :U modifier may be arbitrarily
+# nested.
+
+.if ${:U${:Unested}${${${:Udeeply}}}} != nested
+.error
+.endif
+
+# The nested variable expressions may contain braces, and these braces don't
+# need to match pairwise.  In the following example, the :S modifier uses '{'
+# as delimiter, which confuses both editors and humans because the opening
+# and # closing braces don't match anymore.  It's syntactically valid though.
+# For more similar examples, see varmod-subst.mk, mod-subst-delimiter.
+
+.if ${:U${:Uvalue:S{a{X{}} != vXlue
+.error
+.endif
+
+# The escaping rules for the :U modifier (left-hand side) and condition
+# string literals (right-hand side) are completely different.
+#
+# In the :U modifier, the backslash only escapes very few characters, all
+# other backslashes are retained.
+#
+# In condition string literals, the backslash always escapes the following
+# character, no matter whether it would be necessary or not.
+#
+# In both contexts, \n is an escaped letter n, not a newline; that's what
+# the .newline variable is for.
+#
+# Whitespace at the edges is preserved, on both sides of the comparison.
+
+.if ${:U \: \} \$ \\ \a \b \n } != " : } \$ \\ \\a \\b \\n "
+.error
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/varmod-unique.exp b/unit-tests/varmod-unique.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod-unique.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod-unique.mk b/unit-tests/varmod-unique.mk
new file mode 100644
index 000000000000..ea4698764947
--- /dev/null
+++ b/unit-tests/varmod-unique.mk
@@ -0,0 +1,39 @@
+# $NetBSD: varmod-unique.mk,v 1.4 2020/08/31 17:41:38 rillig Exp $
+#
+# Tests for the :u variable modifier, which discards adjacent duplicate
+# words.
+
+.if ${:U1 2 1:u} != "1 2 1"
+.  warning The :u modifier only merges _adjacent_ duplicate words.
+.endif
+
+.if ${:U1 2 2 3:u} != "1 2 3"
+.  warning The :u modifier must merge adjacent duplicate words.
+.endif
+
+.if ${:U:u} != ""
+.  warning The :u modifier must do nothing with an empty word list.
+.endif
+
+.if ${:U1:u} != "1"
+.  warning The :u modifier must do nothing with a single-element word list.
+.endif
+
+.if ${:U1 1 1 1 1 1 1 1:u} != "1"
+.  warning The :u modifier must merge _all_ adjacent duplicate words.
+.endif
+
+.if ${:U   1    2    1 1  :u} != "1 2 1"
+.  warning The :u modifier must normalize whitespace between the words.
+.endif
+
+.if ${:U1 1 1 1 2:u} != "1 2"
+.  warning Duplicate words at the beginning must be merged.
+.endif
+
+.if ${:U1 2 2 2 2:u} != "1 2"
+.  warning Duplicate words at the end must be merged.
+.endif
+
+all:
+	@:;
diff --git a/unit-tests/varmod.exp b/unit-tests/varmod.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varmod.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varmod.mk b/unit-tests/varmod.mk
new file mode 100644
index 000000000000..68bf165bf72b
--- /dev/null
+++ b/unit-tests/varmod.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varmod.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for variable modifiers, such as :Q, :S,from,to or :Ufallback.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dollar.exp b/unit-tests/varname-dollar.exp
new file mode 100644
index 000000000000..c880e82f0170
--- /dev/null
+++ b/unit-tests/varname-dollar.exp
@@ -0,0 +1,5 @@
+make: "varname-dollar.mk" line 16: dollar is $.
+make: "varname-dollar.mk" line 17: dollar in braces is .
+make: "varname-dollar.mk" line 25: dollar is $.
+make: "varname-dollar.mk" line 26: dollar in braces is dollar.
+exit status 0
diff --git a/unit-tests/varname-dollar.mk b/unit-tests/varname-dollar.mk
new file mode 100644
index 000000000000..d1db9f833306
--- /dev/null
+++ b/unit-tests/varname-dollar.mk
@@ -0,0 +1,29 @@
+# $NetBSD: varname-dollar.mk,v 1.3 2020/08/19 05:40:06 rillig Exp $
+#
+# Tests for the expression "$$", which looks as if it referred to a variable,
+# but simply expands to a single '$' sign.
+#
+# If there really were a special variable named '$', the expressions ${${DOLLAR}}
+# and $$ would always expand to the same value.
+
+# Using the dollar sign in variable names is tricky and not recommended.
+# To see that using this variable indeed affects the variable '$', run the
+# test individually with the -dv option.
+DOLLAR=		$$
+
+# At this point, the variable '$' is not defined. Therefore the second line
+# returns an empty string.
+.info dollar is $$.
+.info dollar in braces is ${${DOLLAR}}.
+
+# Now overwrite the '$' variable to see whether '$$' really expands to that
+# variable, or whether '$$' is handled by the parser.
+${DOLLAR}=	dollar
+
+# At this point, the variable '$' is defined, therefore its value is printed
+# in the second .info directive.
+.info dollar is $$.
+.info dollar in braces is ${${DOLLAR}}.
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-alltargets.exp b/unit-tests/varname-dot-alltargets.exp
new file mode 100644
index 000000000000..992c2573b519
--- /dev/null
+++ b/unit-tests/varname-dot-alltargets.exp
@@ -0,0 +1,4 @@
+
+first second source
+first second source all .END
+exit status 0
diff --git a/unit-tests/varname-dot-alltargets.mk b/unit-tests/varname-dot-alltargets.mk
new file mode 100644
index 000000000000..0d16c8e6fef0
--- /dev/null
+++ b/unit-tests/varname-dot-alltargets.mk
@@ -0,0 +1,25 @@
+# $NetBSD: varname-dot-alltargets.mk,v 1.3 2020/08/25 22:51:54 rillig Exp $
+#
+# Tests for the special .ALLTARGETS variable.
+
+.MAIN: all
+
+TARGETS_1:=	${.ALLTARGETS}
+
+first second: source
+
+TARGETS_2:=	${.ALLTARGETS}
+
+all:
+	# Since the tests are run with the -r option, no targets are
+	# defined at the beginning.
+	@echo ${TARGETS_1}
+
+	# Only first and second are "real" targets.
+	# The .ALLTARGETS variable is not about targets though, but
+	# about all nodes, therefore source is also included.
+	@echo ${TARGETS_2}
+
+	# Interestingly, the .END target is also implicitly defined at
+	# this point.
+	@echo ${.ALLTARGETS}
diff --git a/unit-tests/varname-dot-curdir.exp b/unit-tests/varname-dot-curdir.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-curdir.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-curdir.mk b/unit-tests/varname-dot-curdir.mk
new file mode 100644
index 000000000000..3795d87b030f
--- /dev/null
+++ b/unit-tests/varname-dot-curdir.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-curdir.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .CURDIR variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-includedfromdir.exp b/unit-tests/varname-dot-includedfromdir.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-includedfromdir.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-includedfromdir.mk b/unit-tests/varname-dot-includedfromdir.mk
new file mode 100644
index 000000000000..e5a61793baaf
--- /dev/null
+++ b/unit-tests/varname-dot-includedfromdir.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-includedfromdir.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .INCLUDEDFROMDIR variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-includedfromfile.exp b/unit-tests/varname-dot-includedfromfile.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-includedfromfile.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-includedfromfile.mk b/unit-tests/varname-dot-includedfromfile.mk
new file mode 100644
index 000000000000..403f562c27fb
--- /dev/null
+++ b/unit-tests/varname-dot-includedfromfile.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-includedfromfile.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .INCLUDEDFROMFILE variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-includes.exp b/unit-tests/varname-dot-includes.exp
new file mode 100755
index 000000000000..edc90f068899
--- /dev/null
+++ b/unit-tests/varname-dot-includes.exp
@@ -0,0 +1,2 @@
+.INCLUDES= -I. -I..
+exit status 0
diff --git a/unit-tests/varname-dot-includes.mk b/unit-tests/varname-dot-includes.mk
new file mode 100755
index 000000000000..33e8669c27be
--- /dev/null
+++ b/unit-tests/varname-dot-includes.mk
@@ -0,0 +1,20 @@
+# $NetBSD: varname-dot-includes.mk,v 1.1 2020/08/28 03:51:06 rillig Exp $
+#
+# Tests for the special .INCLUDES variable, which is not documented in the
+# manual page.
+#
+# It is yet unclear in which situations this feature is useful.
+
+.SUFFIXES: .h
+
+.PATH.h: . ..
+
+.INCLUDES: .h
+
+# The .INCLUDES variable is not yet available.
+.if defined(${.INCLUDES:Q})
+.error
+.endif
+
+all:
+	@echo .INCLUDES=${.INCLUDES:Q}
diff --git a/unit-tests/varname-dot-libs.exp b/unit-tests/varname-dot-libs.exp
new file mode 100755
index 000000000000..97b7e720bdd4
--- /dev/null
+++ b/unit-tests/varname-dot-libs.exp
@@ -0,0 +1,2 @@
+.LIBS= -L. -L..
+exit status 0
diff --git a/unit-tests/varname-dot-libs.mk b/unit-tests/varname-dot-libs.mk
new file mode 100755
index 000000000000..80abeb50b48c
--- /dev/null
+++ b/unit-tests/varname-dot-libs.mk
@@ -0,0 +1,20 @@
+# $NetBSD: varname-dot-libs.mk,v 1.1 2020/08/28 03:51:06 rillig Exp $
+#
+# Tests for the special .LIBS variable, which is not documented in the
+# manual page.
+#
+# It is yet unclear in which situations this feature is useful.
+
+.SUFFIXES: .a
+
+.PATH.a: . ..
+
+.LIBS: .a
+
+# The .LIBS variable is not yet available.
+.if defined(${.LIBS:Q})
+.error
+.endif
+
+all:
+	@echo .LIBS=${.LIBS:Q}
diff --git a/unit-tests/varname-dot-make-dependfile.exp b/unit-tests/varname-dot-make-dependfile.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-dependfile.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-dependfile.mk b/unit-tests/varname-dot-make-dependfile.mk
new file mode 100644
index 000000000000..882c5344d9be
--- /dev/null
+++ b/unit-tests/varname-dot-make-dependfile.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-dependfile.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.DEPENDFILE variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-expand_variables.exp b/unit-tests/varname-dot-make-expand_variables.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-expand_variables.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-expand_variables.mk b/unit-tests/varname-dot-make-expand_variables.mk
new file mode 100644
index 000000000000..cff2db4b9a21
--- /dev/null
+++ b/unit-tests/varname-dot-make-expand_variables.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-expand_variables.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.EXPAND_VARIABLES variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-exported.exp b/unit-tests/varname-dot-make-exported.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-exported.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-exported.mk b/unit-tests/varname-dot-make-exported.mk
new file mode 100644
index 000000000000..123762dea9d0
--- /dev/null
+++ b/unit-tests/varname-dot-make-exported.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-exported.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.EXPORTED variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-jobs-prefix.exp b/unit-tests/varname-dot-make-jobs-prefix.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-jobs-prefix.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-jobs-prefix.mk b/unit-tests/varname-dot-make-jobs-prefix.mk
new file mode 100644
index 000000000000..bf445ab2d349
--- /dev/null
+++ b/unit-tests/varname-dot-make-jobs-prefix.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-jobs-prefix.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.JOBS.PREFIX variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-jobs.exp b/unit-tests/varname-dot-make-jobs.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-jobs.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-jobs.mk b/unit-tests/varname-dot-make-jobs.mk
new file mode 100644
index 000000000000..1e99b3d28ea8
--- /dev/null
+++ b/unit-tests/varname-dot-make-jobs.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-jobs.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.JOBS variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-level.exp b/unit-tests/varname-dot-make-level.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-level.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-level.mk b/unit-tests/varname-dot-make-level.mk
new file mode 100644
index 000000000000..c4f2c0db7da6
--- /dev/null
+++ b/unit-tests/varname-dot-make-level.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-level.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.LEVEL variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-makefile_preference.exp b/unit-tests/varname-dot-make-makefile_preference.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-makefile_preference.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-makefile_preference.mk b/unit-tests/varname-dot-make-makefile_preference.mk
new file mode 100644
index 000000000000..58cc8a2bf516
--- /dev/null
+++ b/unit-tests/varname-dot-make-makefile_preference.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-makefile_preference.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.MAKEFILE_PREFERENCE variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-makefiles.exp b/unit-tests/varname-dot-make-makefiles.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-makefiles.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-makefiles.mk b/unit-tests/varname-dot-make-makefiles.mk
new file mode 100644
index 000000000000..f7449bbfc729
--- /dev/null
+++ b/unit-tests/varname-dot-make-makefiles.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-makefiles.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.MAKEFILES variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-meta-bailiwick.exp b/unit-tests/varname-dot-make-meta-bailiwick.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-bailiwick.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-meta-bailiwick.mk b/unit-tests/varname-dot-make-meta-bailiwick.mk
new file mode 100644
index 000000000000..130a7b483291
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-bailiwick.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-meta-bailiwick.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.META.BAILIWICK variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-meta-created.exp b/unit-tests/varname-dot-make-meta-created.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-created.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-meta-created.mk b/unit-tests/varname-dot-make-meta-created.mk
new file mode 100644
index 000000000000..bc149b60ddf4
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-created.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-meta-created.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.META.CREATED variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-meta-files.exp b/unit-tests/varname-dot-make-meta-files.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-files.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-meta-files.mk b/unit-tests/varname-dot-make-meta-files.mk
new file mode 100644
index 000000000000..e6fe9e92bfca
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-files.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-meta-files.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.META.FILES variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-meta-ignore_filter.exp b/unit-tests/varname-dot-make-meta-ignore_filter.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-ignore_filter.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-meta-ignore_filter.mk b/unit-tests/varname-dot-make-meta-ignore_filter.mk
new file mode 100644
index 000000000000..c41aec4acdf8
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-ignore_filter.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-meta-ignore_filter.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.META.IGNORE_FILTER variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-meta-ignore_paths.exp b/unit-tests/varname-dot-make-meta-ignore_paths.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-ignore_paths.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-meta-ignore_paths.mk b/unit-tests/varname-dot-make-meta-ignore_paths.mk
new file mode 100644
index 000000000000..4ae34f51608b
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-ignore_paths.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-meta-ignore_paths.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.META.IGNORE_PATHS variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-meta-ignore_patterns.exp b/unit-tests/varname-dot-make-meta-ignore_patterns.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-ignore_patterns.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-meta-ignore_patterns.mk b/unit-tests/varname-dot-make-meta-ignore_patterns.mk
new file mode 100644
index 000000000000..ea9fc49f1718
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-ignore_patterns.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-meta-ignore_patterns.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.META.IGNORE_PATTERNS variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-meta-prefix.exp b/unit-tests/varname-dot-make-meta-prefix.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-prefix.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-meta-prefix.mk b/unit-tests/varname-dot-make-meta-prefix.mk
new file mode 100644
index 000000000000..d4fdf64e6db3
--- /dev/null
+++ b/unit-tests/varname-dot-make-meta-prefix.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-meta-prefix.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.META.PREFIX variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-mode.exp b/unit-tests/varname-dot-make-mode.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-mode.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-mode.mk b/unit-tests/varname-dot-make-mode.mk
new file mode 100644
index 000000000000..ee75a54ebd74
--- /dev/null
+++ b/unit-tests/varname-dot-make-mode.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-mode.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.MODE variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-path_filemon.exp b/unit-tests/varname-dot-make-path_filemon.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-path_filemon.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-path_filemon.mk b/unit-tests/varname-dot-make-path_filemon.mk
new file mode 100644
index 000000000000..8c07529f1b4c
--- /dev/null
+++ b/unit-tests/varname-dot-make-path_filemon.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-path_filemon.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.PATH_FILEMON variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-pid.exp b/unit-tests/varname-dot-make-pid.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-pid.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-pid.mk b/unit-tests/varname-dot-make-pid.mk
new file mode 100644
index 000000000000..bea114d33547
--- /dev/null
+++ b/unit-tests/varname-dot-make-pid.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-pid.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.PID variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-ppid.exp b/unit-tests/varname-dot-make-ppid.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-ppid.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-ppid.mk b/unit-tests/varname-dot-make-ppid.mk
new file mode 100644
index 000000000000..c9471542ca35
--- /dev/null
+++ b/unit-tests/varname-dot-make-ppid.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-ppid.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.PPID variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-make-save_dollars.exp b/unit-tests/varname-dot-make-save_dollars.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-make-save_dollars.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-make-save_dollars.mk b/unit-tests/varname-dot-make-save_dollars.mk
new file mode 100644
index 000000000000..97f37a646d2d
--- /dev/null
+++ b/unit-tests/varname-dot-make-save_dollars.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-make-save_dollars.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.SAVE_DOLLARS variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-makeoverrides.exp b/unit-tests/varname-dot-makeoverrides.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-makeoverrides.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-makeoverrides.mk b/unit-tests/varname-dot-makeoverrides.mk
new file mode 100644
index 000000000000..a897f4667175
--- /dev/null
+++ b/unit-tests/varname-dot-makeoverrides.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-makeoverrides.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.MAKEOVERRIDES variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-newline.exp b/unit-tests/varname-dot-newline.exp
new file mode 100644
index 000000000000..13943539bf3b
--- /dev/null
+++ b/unit-tests/varname-dot-newline.exp
@@ -0,0 +1,4 @@
+make: "varname-dot-newline.mk" line 16: The .newline variable can be overwritten.  Just don't do that.
+first
+second
+exit status 0
diff --git a/unit-tests/varname-dot-newline.mk b/unit-tests/varname-dot-newline.mk
new file mode 100644
index 000000000000..fa9ac0759649
--- /dev/null
+++ b/unit-tests/varname-dot-newline.mk
@@ -0,0 +1,23 @@
+# $NetBSD: varname-dot-newline.mk,v 1.3 2020/08/19 05:51:18 rillig Exp $
+#
+# Tests for the special .newline variable.
+#
+# Contrary to the special variable named "" that is used in expressions like
+# ${:Usome-value}, the variable ".newline" is not protected against
+# modification.  Nobody exploits that though.
+
+NEWLINE:=	${.newline}
+
+.newline=	overwritten
+
+.if ${.newline} == ${NEWLINE}
+.info The .newline variable cannot be overwritten.  Good.
+.else
+.info The .newline variable can be overwritten.  Just don't do that.
+.endif
+
+# Restore the original value.
+.newline=	${NEWLINE}
+
+all:
+	@echo 'first${.newline}second'
diff --git a/unit-tests/varname-dot-objdir.exp b/unit-tests/varname-dot-objdir.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-objdir.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-objdir.mk b/unit-tests/varname-dot-objdir.mk
new file mode 100644
index 000000000000..e662e8ac56fa
--- /dev/null
+++ b/unit-tests/varname-dot-objdir.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-objdir.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .OBJDIR variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-parsedir.exp b/unit-tests/varname-dot-parsedir.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-parsedir.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-parsedir.mk b/unit-tests/varname-dot-parsedir.mk
new file mode 100644
index 000000000000..6aeb5878a457
--- /dev/null
+++ b/unit-tests/varname-dot-parsedir.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-parsedir.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .PARSEDIR variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-parsefile.exp b/unit-tests/varname-dot-parsefile.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-parsefile.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-parsefile.mk b/unit-tests/varname-dot-parsefile.mk
new file mode 100644
index 000000000000..9deff34a70d0
--- /dev/null
+++ b/unit-tests/varname-dot-parsefile.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-parsefile.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .PARSEFILE variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-path.exp b/unit-tests/varname-dot-path.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-path.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-path.mk b/unit-tests/varname-dot-path.mk
new file mode 100644
index 000000000000..8f4e3a5dcfa4
--- /dev/null
+++ b/unit-tests/varname-dot-path.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-path.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .PATH variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-dot-shell.exp b/unit-tests/varname-dot-shell.exp
new file mode 100755
index 000000000000..af4baaf44c0b
--- /dev/null
+++ b/unit-tests/varname-dot-shell.exp
@@ -0,0 +1,19 @@
+ParseReadLine (8): 'ORIG_SHELL:=	${.SHELL}'
+Var_Parse: ${.SHELL} with VARE_WANTRES|VARE_ASSIGN
+Global:delete .SHELL (not found)
+Command:.SHELL = (details omitted)
+ParseReadLine (10): '.SHELL=		overwritten'
+Global:.SHELL = overwritten
+Var_Parse: ${.SHELL} != ${ORIG_SHELL} with VARE_UNDEFERR|VARE_WANTRES
+ParseReadLine (18): '.undef .SHELL'
+Global:delete .SHELL
+ParseReadLine (19): '.SHELL=		newly overwritten'
+Global:.SHELL = newly overwritten
+Var_Parse: ${.SHELL} != ${ORIG_SHELL} with VARE_UNDEFERR|VARE_WANTRES
+ParseReadLine (24): 'all:'
+ParseReadLine (25): '	@echo ${.SHELL:M*}'
+Var_Parse: ${.SHELL:M*} with VARE_WANTRES
+Applying ${.SHELL:M...} to "(details omitted)" (eflags = VARE_WANTRES, vflags = VAR_READONLY)
+Pattern[.SHELL] for [(details omitted)] is [*]
+Result of ${.SHELL:M*} is "(details omitted)" (eflags = VARE_WANTRES, vflags = VAR_READONLY)
+exit status 0
diff --git a/unit-tests/varname-dot-shell.mk b/unit-tests/varname-dot-shell.mk
new file mode 100755
index 000000000000..4a2c52b39cbc
--- /dev/null
+++ b/unit-tests/varname-dot-shell.mk
@@ -0,0 +1,25 @@
+# $NetBSD: varname-dot-shell.mk,v 1.2 2020/08/23 09:28:52 rillig Exp $
+#
+# Tests for the special .SHELL variable, which contains the shell used for
+# running the commands.
+#
+# This variable is read-only.
+
+ORIG_SHELL:=	${.SHELL}
+
+.SHELL=		overwritten
+.if ${.SHELL} != ${ORIG_SHELL}
+.error
+.endif
+
+# Trying to delete the variable.
+# This has no effect since the variable is not defined in the global context,
+# but in the command-line context.
+.undef .SHELL
+.SHELL=		newly overwritten
+.if ${.SHELL} != ${ORIG_SHELL}
+.error
+.endif
+
+all:
+	@echo ${.SHELL:M*}
diff --git a/unit-tests/varname-dot-targets.exp b/unit-tests/varname-dot-targets.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-dot-targets.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-dot-targets.mk b/unit-tests/varname-dot-targets.mk
new file mode 100644
index 000000000000..2d1665744040
--- /dev/null
+++ b/unit-tests/varname-dot-targets.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-dot-targets.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .TARGETS variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-empty.exp b/unit-tests/varname-empty.exp
new file mode 100644
index 000000000000..77cb7c517e9d
--- /dev/null
+++ b/unit-tests/varname-empty.exp
@@ -0,0 +1,11 @@
+Var_Set("${:U}", "cmdline-u", ...) name expands to empty string - ignored
+Var_Set("", "cmline-plain", ...) name expands to empty string - ignored
+Var_Set("", "default", ...) name expands to empty string - ignored
+Var_Set("", "assigned", ...) name expands to empty string - ignored
+Var_Set("", "appended", ...) name expands to empty string - ignored
+Var_Set("", "", ...) name expands to empty string - ignored
+Var_Set("", "subst", ...) name expands to empty string - ignored
+Var_Set("", "shell-output", ...) name expands to empty string - ignored
+out: fallback
+out: 1 2 3
+exit status 0
diff --git a/unit-tests/varname-empty.mk b/unit-tests/varname-empty.mk
new file mode 100755
index 000000000000..b4ce05c3017b
--- /dev/null
+++ b/unit-tests/varname-empty.mk
@@ -0,0 +1,26 @@
+# $NetBSD: varname-empty.mk,v 1.5 2020/08/22 21:22:24 rillig Exp $
+#
+# Tests for the special variable with the empty name.
+#
+# The variable "" is not supposed to be assigned any value.
+# This is because it is heavily used in the .for loop expansion,
+# as well as to generate arbitrary strings, as in ${:Ufallback}.
+
+# Until 2020-08-22 it was possible to assign a value to the variable with
+# the empty name, leading to all kinds of unexpected effects.
+?=	default
+=	assigned	# undefined behavior until 2020-08-22
++=	appended
+:=	subst
+!=	echo 'shell-output'
+
+# The .for loop expands the expression ${i} to ${:U1}, ${:U2} and so on.
+# This only works if the variable with the empty name is guaranteed to
+# be undefined.
+.for i in 1 2 3
+NUMBERS+=	${i}
+.endfor
+
+all:
+	@echo out: ${:Ufallback}
+	@echo out: ${NUMBERS}
diff --git a/unit-tests/varname-make.exp b/unit-tests/varname-make.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-make.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-make.mk b/unit-tests/varname-make.mk
new file mode 100644
index 000000000000..049f00ef741b
--- /dev/null
+++ b/unit-tests/varname-make.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-make.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special MAKE variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-make_print_var_on_error.exp b/unit-tests/varname-make_print_var_on_error.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-make_print_var_on_error.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-make_print_var_on_error.mk b/unit-tests/varname-make_print_var_on_error.mk
new file mode 100644
index 000000000000..757d55780be1
--- /dev/null
+++ b/unit-tests/varname-make_print_var_on_error.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-make_print_var_on_error.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special .MAKE.PRINT_VAR_ON_ERROR variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-makeflags.exp b/unit-tests/varname-makeflags.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-makeflags.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-makeflags.mk b/unit-tests/varname-makeflags.mk
new file mode 100644
index 000000000000..b2e5f68b4e08
--- /dev/null
+++ b/unit-tests/varname-makeflags.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-makeflags.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special MAKEFLAGS variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-pwd.exp b/unit-tests/varname-pwd.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-pwd.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-pwd.mk b/unit-tests/varname-pwd.mk
new file mode 100644
index 000000000000..e11ee828117b
--- /dev/null
+++ b/unit-tests/varname-pwd.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-pwd.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special PWD variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname-vpath.exp b/unit-tests/varname-vpath.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname-vpath.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname-vpath.mk b/unit-tests/varname-vpath.mk
new file mode 100644
index 000000000000..8924647a5072
--- /dev/null
+++ b/unit-tests/varname-vpath.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname-vpath.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for the special VPATH variable.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varname.exp b/unit-tests/varname.exp
new file mode 100644
index 000000000000..39a9383953dd
--- /dev/null
+++ b/unit-tests/varname.exp
@@ -0,0 +1 @@
+exit status 0
diff --git a/unit-tests/varname.mk b/unit-tests/varname.mk
new file mode 100644
index 000000000000..9dd965083f3f
--- /dev/null
+++ b/unit-tests/varname.mk
@@ -0,0 +1,8 @@
+# $NetBSD: varname.mk,v 1.2 2020/08/16 14:25:16 rillig Exp $
+#
+# Tests for special variables, such as .MAKE or .PARSEDIR.
+
+# TODO: Implementation
+
+all:
+	@:;
diff --git a/unit-tests/varparse-dynamic.exp b/unit-tests/varparse-dynamic.exp
new file mode 100644
index 000000000000..a2ff29413167
--- /dev/null
+++ b/unit-tests/varparse-dynamic.exp
@@ -0,0 +1,5 @@
+make: "varparse-dynamic.mk" line 8: Malformed conditional (${.TARGEX})
+make: "varparse-dynamic.mk" line 10: Malformed conditional (${.TARGXX})
+make: Fatal errors encountered -- cannot continue
+make: stopped in unit-tests
+exit status 1
diff --git a/unit-tests/varparse-dynamic.mk b/unit-tests/varparse-dynamic.mk
new file mode 100644
index 000000000000..724cca3a5035
--- /dev/null
+++ b/unit-tests/varparse-dynamic.mk
@@ -0,0 +1,14 @@
+# $NetBSD: varparse-dynamic.mk,v 1.1 2020/07/26 22:15:36 rillig Exp $
+
+# Before 2020-07-27, there was an off-by-one error in Var_Parse that skipped
+# the last character in the variable name.
+# To trigger the bug, the variable must not be defined.
+.if ${.TARGET}			# exact match, may be undefined
+.endif
+.if ${.TARGEX}			# 1 character difference, must be defined
+.endif
+.if ${.TARGXX}			# 2 characters difference, must be defined
+.endif
+
+all:
+	@:
diff --git a/util.c b/util.c
index d674a6c21aac..2698b91678e4 100644
--- a/util.c
+++ b/util.c
@@ -1,9 +1,9 @@
-/*	$NetBSD: util.c,v 1.57 2020/07/03 08:13:23 rillig Exp $	*/
+/*	$NetBSD: util.c,v 1.58 2020/08/01 14:47:49 rillig Exp $	*/
 
 /*
  * Missing stuff from OS's
  *
- *	$Id: util.c,v 1.35 2020/07/04 18:16:55 sjg Exp $
+ *	$Id: util.c,v 1.36 2020/08/01 23:08:14 sjg Exp $
  */
 #if defined(__MINT__) || defined(__linux__)
 #include <signal.h>
@@ -12,10 +12,10 @@
 #include "make.h"
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: util.c,v 1.57 2020/07/03 08:13:23 rillig Exp $";
+static char rcsid[] = "$NetBSD: util.c,v 1.58 2020/08/01 14:47:49 rillig Exp $";
 #else
 #ifndef lint
-__RCSID("$NetBSD: util.c,v 1.57 2020/07/03 08:13:23 rillig Exp $");
+__RCSID("$NetBSD: util.c,v 1.58 2020/08/01 14:47:49 rillig Exp $");
 #endif
 #endif
 
@@ -178,39 +178,39 @@ strrcpy(char *ptr, char *str)
 
 
 char    *sys_siglist[] = {
-        "Signal 0",
-        "Hangup",                       /* SIGHUP    */
-        "Interrupt",                    /* SIGINT    */
-        "Quit",                         /* SIGQUIT   */
-        "Illegal instruction",          /* SIGILL    */
-        "Trace/BPT trap",               /* SIGTRAP   */
-        "IOT trap",                     /* SIGIOT    */
-        "EMT trap",                     /* SIGEMT    */
-        "Floating point exception",     /* SIGFPE    */
-        "Killed",                       /* SIGKILL   */
-        "Bus error",                    /* SIGBUS    */
-        "Segmentation fault",           /* SIGSEGV   */
-        "Bad system call",              /* SIGSYS    */
-        "Broken pipe",                  /* SIGPIPE   */
-        "Alarm clock",                  /* SIGALRM   */
-        "Terminated",                   /* SIGTERM   */
-        "User defined signal 1",        /* SIGUSR1   */
-        "User defined signal 2",        /* SIGUSR2   */
-        "Child exited",                 /* SIGCLD    */
-        "Power-fail restart",           /* SIGPWR    */
-        "Virtual timer expired",        /* SIGVTALRM */
-        "Profiling timer expired",      /* SIGPROF   */
-        "I/O possible",                 /* SIGIO     */
-        "Window size changes",          /* SIGWINDOW */
-        "Stopped (signal)",             /* SIGSTOP   */
-        "Stopped",                      /* SIGTSTP   */
-        "Continued",                    /* SIGCONT   */
-        "Stopped (tty input)",          /* SIGTTIN   */
-        "Stopped (tty output)",         /* SIGTTOU   */
-        "Urgent I/O condition",         /* SIGURG    */
-        "Remote lock lost (NFS)",       /* SIGLOST   */
-        "Signal 31",                    /* reserved  */
-        "DIL signal"                    /* SIGDIL    */
+	"Signal 0",
+	"Hangup",                       /* SIGHUP    */
+	"Interrupt",                    /* SIGINT    */
+	"Quit",                         /* SIGQUIT   */
+	"Illegal instruction",          /* SIGILL    */
+	"Trace/BPT trap",               /* SIGTRAP   */
+	"IOT trap",                     /* SIGIOT    */
+	"EMT trap",                     /* SIGEMT    */
+	"Floating point exception",     /* SIGFPE    */
+	"Killed",                       /* SIGKILL   */
+	"Bus error",                    /* SIGBUS    */
+	"Segmentation fault",           /* SIGSEGV   */
+	"Bad system call",              /* SIGSYS    */
+	"Broken pipe",                  /* SIGPIPE   */
+	"Alarm clock",                  /* SIGALRM   */
+	"Terminated",                   /* SIGTERM   */
+	"User defined signal 1",        /* SIGUSR1   */
+	"User defined signal 2",        /* SIGUSR2   */
+	"Child exited",                 /* SIGCLD    */
+	"Power-fail restart",           /* SIGPWR    */
+	"Virtual timer expired",        /* SIGVTALRM */
+	"Profiling timer expired",      /* SIGPROF   */
+	"I/O possible",                 /* SIGIO     */
+	"Window size changes",          /* SIGWINDOW */
+	"Stopped (signal)",             /* SIGSTOP   */
+	"Stopped",                      /* SIGTSTP   */
+	"Continued",                    /* SIGCONT   */
+	"Stopped (tty input)",          /* SIGTTIN   */
+	"Stopped (tty output)",         /* SIGTTOU   */
+	"Urgent I/O condition",         /* SIGURG    */
+	"Remote lock lost (NFS)",       /* SIGLOST   */
+	"Signal 31",                    /* reserved  */
+	"DIL signal"                    /* SIGDIL    */
 };
 #endif /* __hpux__ || __hpux */
 
diff --git a/var.c b/var.c
index c7e6b1b34fd4..173bb4f099c9 100644
--- a/var.c
+++ b/var.c
@@ -1,4 +1,4 @@
-/*	$NetBSD: var.c,v 1.255 2020/07/04 17:41:04 rillig Exp $	*/
+/*	$NetBSD: var.c,v 1.484 2020/09/02 06:25:48 rillig Exp $	*/
 
 /*
  * Copyright (c) 1988, 1989, 1990, 1993
@@ -69,14 +69,14 @@
  */
 
 #ifndef MAKE_NATIVE
-static char rcsid[] = "$NetBSD: var.c,v 1.255 2020/07/04 17:41:04 rillig Exp $";
+static char rcsid[] = "$NetBSD: var.c,v 1.484 2020/09/02 06:25:48 rillig Exp $";
 #else
 #include <sys/cdefs.h>
 #ifndef lint
 #if 0
 static char sccsid[] = "@(#)var.c	8.3 (Berkeley) 3/19/94";
 #else
-__RCSID("$NetBSD: var.c,v 1.255 2020/07/04 17:41:04 rillig Exp $");
+__RCSID("$NetBSD: var.c,v 1.484 2020/09/02 06:25:48 rillig Exp $");
 #endif
 #endif /* not lint */
 #endif
@@ -88,8 +88,7 @@ __RCSID("$NetBSD: var.c,v 1.255 2020/07/04 17:41:04 rillig Exp $");
  * Interface:
  *	Var_Set		    Set the value of a variable in the given
  *			    context. The variable is created if it doesn't
- *			    yet exist. The value and variable name need not
- *			    be preserved.
+ *			    yet exist.
  *
  *	Var_Append	    Append more characters to an existing variable
  *			    in the given context. The variable needn't
@@ -99,12 +98,11 @@ __RCSID("$NetBSD: var.c,v 1.255 2020/07/04 17:41:04 rillig Exp $");
  *
  *	Var_Exists	    See if a variable exists.
  *
- *	Var_Value 	    Return the value of a variable in a context or
- *			    NULL if the variable is undefined.
+ *	Var_Value 	    Return the unexpanded value of a variable in a
+ *			    context or NULL if the variable is undefined.
  *
  *	Var_Subst 	    Substitute either a single variable or all
- *			    variables in a string, using the given context as
- *			    the top-most one.
+ *			    variables in a string, using the given context.
  *
  *	Var_Parse 	    Parse a variable expansion from a string and
  *			    return the result and the number of characters
@@ -126,23 +124,32 @@ __RCSID("$NetBSD: var.c,v 1.255 2020/07/04 17:41:04 rillig Exp $");
 #ifndef NO_REGEX
 #include    <regex.h>
 #endif
-#include    <ctype.h>
-#include    <stdlib.h>
-#include    <limits.h>
 #include    <time.h>
 
 #include    "make.h"
 
-#ifdef HAVE_STDINT_H
-#include <stdint.h>
+#ifdef HAVE_INTTYPES_H
+#include    <inttypes.h>
+#elif defined(HAVE_STDINT_H)
+#include    <stdint.h>
 #endif
 
-#include    "buf.h"
+#include    "enum.h"
 #include    "dir.h"
 #include    "job.h"
 #include    "metachar.h"
 
-extern int makelevel;
+#define VAR_DEBUG_IF(cond, fmt, ...)	\
+    if (!(DEBUG(VAR) && (cond)))	\
+	(void) 0;			\
+    else				\
+	fprintf(debug_file, fmt, __VA_ARGS__)
+
+#define VAR_DEBUG(fmt, ...) VAR_DEBUG_IF(TRUE, fmt, __VA_ARGS__)
+
+ENUM_FLAGS_RTTI_3(VarEvalFlags,
+		  VARE_UNDEFERR, VARE_WANTRES, VARE_ASSIGN);
+
 /*
  * This lets us tell if we have replaced the original environ
  * (which we cannot free).
@@ -157,9 +164,11 @@ char **savedEnv = NULL;
 char var_Error[] = "";
 
 /*
- * Similar to var_Error, but returned when the 'VARF_UNDEFERR' flag for
- * Var_Parse is not set. Why not just use a constant? Well, gcc likes
- * to condense identical string instances...
+ * Similar to var_Error, but returned when the 'VARE_UNDEFERR' flag for
+ * Var_Parse is not set.
+ *
+ * Why not just use a constant? Well, GCC likes to condense identical string
+ * instances...
  */
 static char varNoError[] = "";
 
@@ -167,139 +176,107 @@ static char varNoError[] = "";
  * Traditionally we consume $$ during := like any other expansion.
  * Other make's do not.
  * This knob allows controlling the behavior.
- * FALSE for old behavior.
- * TRUE for new compatible.
+ * FALSE to consume $$ during := assignment.
+ * TRUE to preserve $$ during := assignment.
  */
 #define SAVE_DOLLARS ".MAKE.SAVE_DOLLARS"
 static Boolean save_dollars = FALSE;
 
 /*
  * Internally, variables are contained in four different contexts.
- *	1) the environment. They may not be changed. If an environment
- *	    variable is appended-to, the result is placed in the global
+ *	1) the environment. They cannot be changed. If an environment
+ *	    variable is appended to, the result is placed in the global
  *	    context.
  *	2) the global context. Variables set in the Makefile are located in
- *	    the global context. It is the penultimate context searched when
- *	    substituting.
+ *	    the global context.
  *	3) the command-line context. All variables set on the command line
  *	   are placed in this context. They are UNALTERABLE once placed here.
  *	4) the local context. Each target has associated with it a context
  *	   list. On this list are located the structures describing such
  *	   local variables as $(@) and $(*)
  * The four contexts are searched in the reverse order from which they are
- * listed.
+ * listed (but see checkEnvFirst).
  */
 GNode          *VAR_INTERNAL;	/* variables from make itself */
 GNode          *VAR_GLOBAL;	/* variables from the makefile */
 GNode          *VAR_CMD;	/* variables defined on the command-line */
 
-#define FIND_CMD	0x1	/* look in VAR_CMD when searching */
-#define FIND_GLOBAL	0x2	/* look in VAR_GLOBAL as well */
-#define FIND_ENV  	0x4	/* look in the environment also */
+typedef enum {
+    FIND_CMD		= 0x01,	/* look in VAR_CMD when searching */
+    FIND_GLOBAL		= 0x02,	/* look in VAR_GLOBAL as well */
+    FIND_ENV		= 0x04	/* look in the environment also */
+} VarFindFlags;
 
 typedef enum {
-    VAR_IN_USE		= 0x01,	/* Variable's value is currently being used.
-				 * Used to avoid endless recursion */
-    VAR_FROM_ENV	= 0x02,	/* Variable comes from the environment */
-    VAR_JUNK		= 0x04,	/* Variable is a junk variable that
-				 * should be destroyed when done with
-				 * it. Used by Var_Parse for undefined,
-				 * modified variables */
-    VAR_KEEP		= 0x08,	/* Variable is VAR_JUNK, but we found
-				 * a use for it in some modifier and
-				 * the value is therefore valid */
-    VAR_EXPORTED	= 0x10,	/* Variable is exported */
-    VAR_REEXPORT	= 0x20,	/* Indicate if var needs re-export.
-				 * This would be true if it contains $'s */
-    VAR_FROM_CMD	= 0x40	/* Variable came from command line */
-} Var_Flags;
+    /* The variable's value is currently being used by Var_Parse or Var_Subst.
+     * This marker is used to avoid endless recursion. */
+    VAR_IN_USE = 0x01,
+    /* The variable comes from the environment.
+     * These variables are not registered in any GNode, therefore they must
+     * be freed as soon as they are not used anymore. */
+    VAR_FROM_ENV = 0x02,
+    /* The variable is a junk variable that should be destroyed when done with
+     * it.  Used by Var_Parse for undefined, modified variables. */
+    VAR_JUNK = 0x04,
+    /* Variable is VAR_JUNK, but we found a use for it in some modifier and
+     * the value is therefore valid. */
+    VAR_KEEP = 0x08,
+    /* The variable is exported to the environment, to be used by child
+     * processes. */
+    VAR_EXPORTED = 0x10,
+    /* At the point where this variable was exported, it contained an
+     * unresolved reference to another variable.  Before any child process is
+     * started, it needs to be exported again, in the hope that the referenced
+     * variable can then be resolved. */
+    VAR_REEXPORT = 0x20,
+    /* The variable came from command line. */
+    VAR_FROM_CMD = 0x40,
+    VAR_READONLY = 0x80
+} VarFlags;
+
+ENUM_FLAGS_RTTI_8(VarFlags,
+		  VAR_IN_USE, VAR_FROM_ENV, VAR_JUNK, VAR_KEEP,
+		  VAR_EXPORTED, VAR_REEXPORT, VAR_FROM_CMD, VAR_READONLY);
 
 typedef struct Var {
-    char          *name;	/* the variable's name */
+    char          *name;	/* the variable's name; it is allocated for
+				 * environment variables and aliased to the
+				 * Hash_Entry name for all other variables,
+				 * and thus must not be modified */
     Buffer	  val;		/* its value */
-    Var_Flags	  flags;    	/* miscellaneous status flags */
-}  Var;
+    VarFlags	  flags;    	/* miscellaneous status flags */
+} Var;
 
 /*
  * Exporting vars is expensive so skip it if we can
  */
-#define VAR_EXPORTED_NONE	0
-#define VAR_EXPORTED_YES	1
-#define VAR_EXPORTED_ALL	2
-static int var_exportedVars = VAR_EXPORTED_NONE;
-/*
- * We pass this to Var_Export when doing the initial export
- * or after updating an exported var.
- */
-#define VAR_EXPORT_PARENT	1
-/*
- * We pass this to Var_Export1 to tell it to leave the value alone.
- */
-#define VAR_EXPORT_LITERAL	2
+typedef enum {
+    VAR_EXPORTED_NONE,
+    VAR_EXPORTED_YES,
+    VAR_EXPORTED_ALL
+} VarExportedMode;
+
+static VarExportedMode var_exportedVars = VAR_EXPORTED_NONE;
 
 typedef enum {
-	VAR_SUB_GLOBAL	= 0x01,	/* Apply substitution globally */
-	VAR_SUB_ONE	= 0x02,	/* Apply substitution to one word */
-	VAR_SUB_MATCHED	= 0x04,	/* There was a match */
-	VAR_MATCH_START	= 0x08,	/* Match at start of word */
-	VAR_MATCH_END	= 0x10,	/* Match at end of word */
-	VAR_NOSUBST	= 0x20	/* don't expand vars in VarGetPattern */
-} VarPattern_Flags;
-
-typedef enum {
-	VAR_NO_EXPORT	= 0x01	/* do not export */
-} VarSet_Flags;
-
-typedef struct {
     /*
-     * The following fields are set by Var_Parse() when it
-     * encounters modifiers that need to keep state for use by
-     * subsequent modifiers within the same variable expansion.
+     * We pass this to Var_Export when doing the initial export
+     * or after updating an exported var.
      */
-    Byte	varSpace;	/* Word separator in expansions */
-    Boolean	oneBigWord;	/* TRUE if we will treat the variable as a
-				 * single big word, even if it contains
-				 * embedded spaces (as opposed to the
-				 * usual behaviour of treating it as
-				 * several space-separated words). */
-} Var_Parse_State;
+    VAR_EXPORT_PARENT	= 0x01,
+    /*
+     * We pass this to Var_Export1 to tell it to leave the value alone.
+     */
+    VAR_EXPORT_LITERAL	= 0x02
+} VarExportFlags;
 
-/* struct passed as 'void *' to VarSubstitute() for ":S/lhs/rhs/",
- * to VarSYSVMatch() for ":lhs=rhs". */
-typedef struct {
-    const char   *lhs;		/* String to match */
-    int		  leftLen;	/* Length of string */
-    const char   *rhs;		/* Replacement string (w/ &'s removed) */
-    int		  rightLen;	/* Length of replacement */
-    VarPattern_Flags flags;
-} VarPattern;
-
-/* struct passed as 'void *' to VarLoopExpand() for ":@tvar@str@" */
-typedef struct {
-    GNode	*ctxt;		/* variable context */
-    char	*tvar;		/* name of temp var */
-    int		tvarLen;
-    char	*str;		/* string to expand */
-    int		strLen;
-    Varf_Flags	flags;
-} VarLoop;
-
-#ifndef NO_REGEX
-/* struct passed as 'void *' to VarRESubstitute() for ":C///" */
-typedef struct {
-    regex_t	   re;
-    int		   nsub;
-    regmatch_t 	  *matches;
-    char 	  *replace;
-    int		   flags;
-} VarREPattern;
-#endif
-
-/* struct passed to VarSelectWords() for ":[start..end]" */
-typedef struct {
-    int		start;		/* first word to select */
-    int		end;		/* last word to select */
-} VarSelectWords_t;
+/* Flags for pattern matching in the :S and :C modifiers */
+typedef enum {
+    VARP_SUB_GLOBAL	= 0x01,	/* Apply substitution globally */
+    VARP_SUB_ONE	= 0x02,	/* Apply substitution to one word */
+    VARP_ANCHOR_START	= 0x04,	/* Match at start of word */
+    VARP_ANCHOR_END	= 0x08	/* Match at end of word */
+} VarPatternFlags;
 
 #define BROPEN	'{'
 #define BRCLOSE	'}'
@@ -315,24 +292,19 @@ typedef struct {
  * Input:
  *	name		name to find
  *	ctxt		context in which to find it
- *	flags		FIND_GLOBAL set means to look in the
- *			VAR_GLOBAL context as well. FIND_CMD set means
- *			to look in the VAR_CMD context also. FIND_ENV
- *			set means to look in the environment
+ *	flags		FIND_GLOBAL	look in VAR_GLOBAL as well
+ *			FIND_CMD	look in VAR_CMD as well
+ *			FIND_ENV	look in the environment as well
  *
  * Results:
  *	A pointer to the structure describing the desired variable or
  *	NULL if the variable does not exist.
- *
- * Side Effects:
- *	None
  *-----------------------------------------------------------------------
  */
 static Var *
-VarFind(const char *name, GNode *ctxt, int flags)
+VarFind(const char *name, GNode *ctxt, VarFindFlags flags)
 {
-    Hash_Entry         	*var;
-    Var			*v;
+    Hash_Entry *var;
 
     /*
      * If the variable name begins with a '.', it could very well be one of
@@ -340,32 +312,38 @@ VarFind(const char *name, GNode *ctxt, int flags)
      * and substitute the short version in for 'name' if it matches one of
      * them.
      */
-    if (*name == '.' && isupper((unsigned char) name[1])) {
+    if (*name == '.' && isupper((unsigned char)name[1])) {
 	switch (name[1]) {
 	case 'A':
-	    if (!strcmp(name, ".ALLSRC"))
+	    if (strcmp(name, ".ALLSRC") == 0)
 		name = ALLSRC;
-	    if (!strcmp(name, ".ARCHIVE"))
+	    if (strcmp(name, ".ARCHIVE") == 0)
 		name = ARCHIVE;
 	    break;
 	case 'I':
-	    if (!strcmp(name, ".IMPSRC"))
+	    if (strcmp(name, ".IMPSRC") == 0)
 		name = IMPSRC;
 	    break;
 	case 'M':
-	    if (!strcmp(name, ".MEMBER"))
+	    if (strcmp(name, ".MEMBER") == 0)
 		name = MEMBER;
 	    break;
 	case 'O':
-	    if (!strcmp(name, ".OODATE"))
+	    if (strcmp(name, ".OODATE") == 0)
 		name = OODATE;
 	    break;
 	case 'P':
-	    if (!strcmp(name, ".PREFIX"))
+	    if (strcmp(name, ".PREFIX") == 0)
 		name = PREFIX;
 	    break;
+	case 'S':
+	    if (strcmp(name, ".SHELL") == 0 ) {
+		if (!shellPath)
+		    Shell_Init();
+	    }
+	    break;
 	case 'T':
-	    if (!strcmp(name, ".TARGET"))
+	    if (strcmp(name, ".TARGET") == 0)
 		name = TARGET;
 	    break;
 	}
@@ -384,9 +362,9 @@ VarFind(const char *name, GNode *ctxt, int flags)
      */
     var = Hash_FindEntry(&ctxt->context, name);
 
-    if (var == NULL && (flags & FIND_CMD) && ctxt != VAR_CMD) {
+    if (var == NULL && (flags & FIND_CMD) && ctxt != VAR_CMD)
 	var = Hash_FindEntry(&VAR_CMD->context, name);
-    }
+
     if (!checkEnvFirst && var == NULL && (flags & FIND_GLOBAL) &&
 	ctxt != VAR_GLOBAL)
     {
@@ -396,42 +374,40 @@ VarFind(const char *name, GNode *ctxt, int flags)
 	    var = Hash_FindEntry(&VAR_INTERNAL->context, name);
 	}
     }
+
     if (var == NULL && (flags & FIND_ENV)) {
 	char *env;
 
 	if ((env = getenv(name)) != NULL) {
-	    int		len;
-
-	    v = bmake_malloc(sizeof(Var));
+	    Var *v = bmake_malloc(sizeof(Var));
+	    size_t len;
 	    v->name = bmake_strdup(name);
 
 	    len = strlen(env);
-
 	    Buf_Init(&v->val, len + 1);
-	    Buf_AddBytes(&v->val, len, env);
+	    Buf_AddBytes(&v->val, env, len);
 
 	    v->flags = VAR_FROM_ENV;
 	    return v;
-	} else if (checkEnvFirst && (flags & FIND_GLOBAL) &&
-		   ctxt != VAR_GLOBAL)
-	{
-	    var = Hash_FindEntry(&VAR_GLOBAL->context, name);
-	    if (var == NULL && ctxt != VAR_INTERNAL) {
-		var = Hash_FindEntry(&VAR_INTERNAL->context, name);
-	    }
-	    if (var == NULL) {
-		return NULL;
-	    } else {
-		return (Var *)Hash_GetValue(var);
-	    }
-	} else {
-	    return NULL;
 	}
-    } else if (var == NULL) {
+
+	if (checkEnvFirst && (flags & FIND_GLOBAL) && ctxt != VAR_GLOBAL) {
+	    var = Hash_FindEntry(&VAR_GLOBAL->context, name);
+	    if (var == NULL && ctxt != VAR_INTERNAL)
+		var = Hash_FindEntry(&VAR_INTERNAL->context, name);
+	    if (var == NULL)
+		return NULL;
+	    else
+		return (Var *)Hash_GetValue(var);
+	}
+
 	return NULL;
-    } else {
-	return (Var *)Hash_GetValue(var);
     }
+
+    if (var == NULL)
+	return NULL;
+    else
+	return (Var *)Hash_GetValue(var);
 }
 
 /*-
@@ -444,16 +420,13 @@ VarFind(const char *name, GNode *ctxt, int flags)
  *	destroy		true if the value buffer should be destroyed.
  *
  * Results:
- *	1 if it is an environment variable 0 ow.
- *
- * Side Effects:
- *	The variable is free'ed if it is an environent variable.
+ *	TRUE if it is an environment variable, FALSE otherwise.
  *-----------------------------------------------------------------------
  */
 static Boolean
 VarFreeEnv(Var *v, Boolean destroy)
 {
-    if ((v->flags & VAR_FROM_ENV) == 0)
+    if (!(v->flags & VAR_FROM_ENV))
 	return FALSE;
     free(v->name);
     Buf_Destroy(&v->val, destroy);
@@ -461,87 +434,52 @@ VarFreeEnv(Var *v, Boolean destroy)
     return TRUE;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * VarAdd  --
- *	Add a new variable of name name and value val to the given context
- *
- * Input:
- *	name		name of variable to add
- *	val		value to set it to
- *	ctxt		context in which to set it
- *
- * Side Effects:
- *	The new variable is placed at the front of the given context
- *	The name and val arguments are duplicated so they may
- *	safely be freed.
- *-----------------------------------------------------------------------
- */
+/* Add a new variable of the given name and value to the given context.
+ * The name and val arguments are duplicated so they may safely be freed. */
 static void
-VarAdd(const char *name, const char *val, GNode *ctxt)
+VarAdd(const char *name, const char *val, GNode *ctxt, VarSet_Flags flags)
 {
-    Var   	  *v;
-    int		  len;
-    Hash_Entry    *h;
+    Var *v = bmake_malloc(sizeof(Var));
+    size_t len = strlen(val);
+    Hash_Entry *he;
 
-    v = bmake_malloc(sizeof(Var));
-
-    len = val ? strlen(val) : 0;
     Buf_Init(&v->val, len + 1);
-    Buf_AddBytes(&v->val, len, val);
+    Buf_AddBytes(&v->val, val, len);
 
     v->flags = 0;
+    if (flags & VAR_SET_READONLY)
+	v->flags |= VAR_READONLY;
 
-    h = Hash_CreateEntry(&ctxt->context, name, NULL);
-    Hash_SetValue(h, v);
-    v->name = h->name;
-    if (DEBUG(VAR) && (ctxt->flags & INTERNAL) == 0) {
-	fprintf(debug_file, "%s:%s = %s\n", ctxt->name, name, val);
-    }
+    he = Hash_CreateEntry(&ctxt->context, name, NULL);
+    Hash_SetValue(he, v);
+    v->name = he->name;
+    VAR_DEBUG_IF(!(ctxt->flags & INTERNAL),
+		 "%s:%s = %s\n", ctxt->name, name, val);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Var_Delete --
- *	Remove a variable from a context.
- *
- * Side Effects:
- *	The Var structure is removed and freed.
- *
- *-----------------------------------------------------------------------
- */
+/* Remove a variable from a context, freeing the Var structure as well. */
 void
 Var_Delete(const char *name, GNode *ctxt)
 {
-    Hash_Entry 	  *ln;
-    char *cp;
+    char *name_freeIt = NULL;
+    Hash_Entry *he;
 
-    if (strchr(name, '$')) {
-	cp = Var_Subst(NULL, name, VAR_GLOBAL, VARF_WANTRES);
-    } else {
-	cp = (char *)name;
-    }
-    ln = Hash_FindEntry(&ctxt->context, cp);
-    if (DEBUG(VAR)) {
-	fprintf(debug_file, "%s:delete %s%s\n",
-	    ctxt->name, cp, ln ? "" : " (not found)");
-    }
-    if (cp != name) {
-	free(cp);
-    }
-    if (ln != NULL) {
-	Var 	  *v;
+    if (strchr(name, '$') != NULL)
+	name = name_freeIt = Var_Subst(name, VAR_GLOBAL, VARE_WANTRES);
+    he = Hash_FindEntry(&ctxt->context, name);
+    VAR_DEBUG("%s:delete %s%s\n",
+	      ctxt->name, name, he != NULL ? "" : " (not found)");
+    free(name_freeIt);
 
-	v = (Var *)Hash_GetValue(ln);
-	if ((v->flags & VAR_EXPORTED)) {
+    if (he != NULL) {
+	Var *v = (Var *)Hash_GetValue(he);
+	if (v->flags & VAR_EXPORTED)
 	    unsetenv(v->name);
-	}
-	if (strcmp(MAKE_EXPORTED, v->name) == 0) {
+	if (strcmp(v->name, MAKE_EXPORTED) == 0)
 	    var_exportedVars = VAR_EXPORTED_NONE;
-	}
-	if (v->name != ln->name)
+	if (v->name != he->name)
 	    free(v->name);
-	Hash_DeleteEntry(&ctxt->context, ln);
+	Hash_DeleteEntry(&ctxt->context, he);
 	Buf_Destroy(&v->val, TRUE);
 	free(v);
     }
@@ -549,24 +487,22 @@ Var_Delete(const char *name, GNode *ctxt)
 
 
 /*
- * Export a var.
- * We ignore make internal variables (those which start with '.')
+ * Export a single variable.
+ * We ignore make internal variables (those which start with '.').
  * Also we jump through some hoops to avoid calling setenv
  * more than necessary since it can leak.
  * We only manipulate flags of vars if 'parent' is set.
  */
-static int
-Var_Export1(const char *name, int flags)
+static Boolean
+Var_Export1(const char *name, VarExportFlags flags)
 {
-    char tmp[BUFSIZ];
+    VarExportFlags parent = flags & VAR_EXPORT_PARENT;
     Var *v;
-    char *val = NULL;
-    int n;
-    int parent = (flags & VAR_EXPORT_PARENT);
+    char *val;
 
-    if (*name == '.')
-	return 0;		/* skip internals */
-    if (!name[1]) {
+    if (name[0] == '.')
+	return FALSE;		/* skip internals */
+    if (name[1] == '\0') {
 	/*
 	 * A single char.
 	 * If it is one of the vars that should only appear in
@@ -578,48 +514,48 @@ Var_Export1(const char *name, int flags)
 	case '%':
 	case '*':
 	case '!':
-	    return 0;
+	    return FALSE;
 	}
     }
+
     v = VarFind(name, VAR_GLOBAL, 0);
-    if (v == NULL) {
-	return 0;
-    }
-    if (!parent &&
-	(v->flags & (VAR_EXPORTED|VAR_REEXPORT)) == VAR_EXPORTED) {
-	return 0;			/* nothing to do */
-    }
+    if (v == NULL)
+	return FALSE;
+
+    if (!parent && (v->flags & VAR_EXPORTED) && !(v->flags & VAR_REEXPORT))
+	return FALSE;		/* nothing to do */
+
     val = Buf_GetAll(&v->val, NULL);
-    if ((flags & VAR_EXPORT_LITERAL) == 0 && strchr(val, '$')) {
+    if (!(flags & VAR_EXPORT_LITERAL) && strchr(val, '$') != NULL) {
+	char *expr;
+
 	if (parent) {
 	    /*
 	     * Flag this as something we need to re-export.
 	     * No point actually exporting it now though,
 	     * the child can do it at the last minute.
 	     */
-	    v->flags |= (VAR_EXPORTED|VAR_REEXPORT);
-	    return 1;
+	    v->flags |= VAR_EXPORTED | VAR_REEXPORT;
+	    return TRUE;
 	}
 	if (v->flags & VAR_IN_USE) {
 	    /*
 	     * We recursed while exporting in a child.
 	     * This isn't going to end well, just skip it.
 	     */
-	    return 0;
-	}
-	n = snprintf(tmp, sizeof(tmp), "${%s}", name);
-	if (n < (int)sizeof(tmp)) {
-	    val = Var_Subst(NULL, tmp, VAR_GLOBAL, VARF_WANTRES);
-	    setenv(name, val, 1);
-	    free(val);
+	    return FALSE;
 	}
+
+	expr = str_concat3("${", name, "}");
+	val = Var_Subst(expr, VAR_GLOBAL, VARE_WANTRES);
+	setenv(name, val, 1);
+	free(val);
+	free(expr);
     } else {
-	if (parent) {
-	    v->flags &= ~VAR_REEXPORT;	/* once will do */
-	}
-	if (parent || !(v->flags & VAR_EXPORTED)) {
+	if (parent)
+	    v->flags &= ~(unsigned)VAR_REEXPORT;	/* once will do */
+	if (parent || !(v->flags & VAR_EXPORTED))
 	    setenv(name, val, 1);
-	}
     }
     /*
      * This is so Var_Set knows to call Var_Export again...
@@ -627,7 +563,7 @@ Var_Export1(const char *name, int flags)
     if (parent) {
 	v->flags |= VAR_EXPORTED;
     }
-    return 1;
+    return TRUE;
 }
 
 static void
@@ -643,9 +579,7 @@ Var_ExportVars_callback(void *entry, void *unused MAKE_ATTR_UNUSED)
 void
 Var_ExportVars(void)
 {
-    char tmp[BUFSIZ];
     char *val;
-    int n;
 
     /*
      * Several make's support this sort of mechanism for tracking
@@ -653,57 +587,46 @@ Var_ExportVars(void)
      * We allow the makefiles to update MAKELEVEL and ensure
      * children see a correctly incremented value.
      */
+    char tmp[BUFSIZ];
     snprintf(tmp, sizeof(tmp), "%d", makelevel + 1);
     setenv(MAKE_LEVEL_ENV, tmp, 1);
 
-    if (VAR_EXPORTED_NONE == var_exportedVars)
+    if (var_exportedVars == VAR_EXPORTED_NONE)
 	return;
 
-    if (VAR_EXPORTED_ALL == var_exportedVars) {
+    if (var_exportedVars == VAR_EXPORTED_ALL) {
 	/* Ouch! This is crazy... */
 	Hash_ForEach(&VAR_GLOBAL->context, Var_ExportVars_callback, NULL);
 	return;
     }
-    /*
-     * We have a number of exported vars,
-     */
-    n = snprintf(tmp, sizeof(tmp), "${" MAKE_EXPORTED ":O:u}");
-    if (n < (int)sizeof(tmp)) {
-	char **av;
-	char *as;
-	int ac;
-	int i;
 
-	val = Var_Subst(NULL, tmp, VAR_GLOBAL, VARF_WANTRES);
-	if (*val) {
-	    av = brk_string(val, &ac, FALSE, &as);
-	    for (i = 0; i < ac; i++) {
-		Var_Export1(av[i], 0);
-	    }
-	    free(as);
-	    free(av);
-	}
-	free(val);
+    val = Var_Subst("${" MAKE_EXPORTED ":O:u}", VAR_GLOBAL, VARE_WANTRES);
+    if (*val) {
+        Words words = Str_Words(val, FALSE);
+	size_t i;
+
+	for (i = 0; i < words.len; i++)
+	    Var_Export1(words.words[i], 0);
+	Words_Free(words);
     }
+    free(val);
 }
 
 /*
- * This is called when .export is seen or
- * .MAKE.EXPORTED is modified.
- * It is also called when any exported var is modified.
+ * This is called when .export is seen or .MAKE.EXPORTED is modified.
+ *
+ * It is also called when any exported variable is modified.
+ * XXX: Is it really?
+ *
+ * str has the format "[-env|-literal] varname...".
  */
 void
-Var_Export(char *str, int isExport)
+Var_Export(const char *str, Boolean isExport)
 {
-    char *name;
+    VarExportFlags flags;
     char *val;
-    char **av;
-    char *as;
-    int flags;
-    int ac;
-    int i;
 
-    if (isExport && (!str || !str[0])) {
+    if (isExport && str[0] == '\0') {
 	var_exportedVars = VAR_EXPORTED_ALL; /* use with caution! */
 	return;
     }
@@ -717,64 +640,49 @@ Var_Export(char *str, int isExport)
     } else {
 	flags |= VAR_EXPORT_PARENT;
     }
-    val = Var_Subst(NULL, str, VAR_GLOBAL, VARF_WANTRES);
-    if (*val) {
-	av = brk_string(val, &ac, FALSE, &as);
-	for (i = 0; i < ac; i++) {
-	    name = av[i];
-	    if (!name[1]) {
-		/*
-		 * A single char.
-		 * If it is one of the vars that should only appear in
-		 * local context, skip it, else we can get Var_Subst
-		 * into a loop.
-		 */
-		switch (name[0]) {
-		case '@':
-		case '%':
-		case '*':
-		case '!':
-		    continue;
-		}
-	    }
+
+    val = Var_Subst(str, VAR_GLOBAL, VARE_WANTRES);
+    if (val[0] != '\0') {
+        Words words = Str_Words(val, FALSE);
+
+	size_t i;
+	for (i = 0; i < words.len; i++) {
+	    const char *name = words.words[i];
 	    if (Var_Export1(name, flags)) {
-		if (VAR_EXPORTED_ALL != var_exportedVars)
+		if (var_exportedVars != VAR_EXPORTED_ALL)
 		    var_exportedVars = VAR_EXPORTED_YES;
 		if (isExport && (flags & VAR_EXPORT_PARENT)) {
 		    Var_Append(MAKE_EXPORTED, name, VAR_GLOBAL);
 		}
 	    }
 	}
-	free(as);
-	free(av);
+	Words_Free(words);
     }
     free(val);
 }
 
 
-/*
- * This is called when .unexport[-env] is seen.
- */
 extern char **environ;
 
+/*
+ * This is called when .unexport[-env] is seen.
+ *
+ * str must have the form "unexport[-env] varname...".
+ */
 void
-Var_UnExport(char *str)
+Var_UnExport(const char *str)
 {
-    char tmp[BUFSIZ];
-    char *vlist;
-    char *cp;
+    const char *varnames;
+    char *varnames_freeIt;
     Boolean unexport_env;
-    int n;
 
-    if (!str || !str[0]) {
-	return;			/* assert? */
-    }
+    varnames = NULL;
+    varnames_freeIt = NULL;
 
-    vlist = NULL;
-
-    str += 8;
-    unexport_env = (strncmp(str, "-env", 4) == 0);
+    str += strlen("unexport");
+    unexport_env = strncmp(str, "-env", 4) == 0;
     if (unexport_env) {
+	const char *cp;
 	char **newenv;
 
 	cp = getenv(MAKE_LEVEL_ENV);	/* we should preserve this */
@@ -788,8 +696,7 @@ Var_UnExport(char *str)
 	    }
 	    newenv = bmake_malloc(2 * sizeof(char *));
 	}
-	if (!newenv)
-	    return;
+
 	/* Note: we cannot safely free() the original environ. */
 	environ = savedEnv = newenv;
 	newenv[0] = NULL;
@@ -797,102 +704,100 @@ Var_UnExport(char *str)
 	if (cp && *cp)
 	    setenv(MAKE_LEVEL_ENV, cp, 1);
     } else {
-	for (; *str != '\n' && isspace((unsigned char) *str); str++)
+	for (; isspace((unsigned char)*str); str++)
 	    continue;
-	if (str[0] && str[0] != '\n') {
-	    vlist = str;
-	}
+	if (str[0] != '\0')
+	    varnames = str;
     }
 
-    if (!vlist) {
+    if (varnames == NULL) {
 	/* Using .MAKE.EXPORTED */
-	n = snprintf(tmp, sizeof(tmp), "${" MAKE_EXPORTED ":O:u}");
-	if (n < (int)sizeof(tmp)) {
-	    vlist = Var_Subst(NULL, tmp, VAR_GLOBAL, VARF_WANTRES);
-	}
+	varnames = varnames_freeIt = Var_Subst("${" MAKE_EXPORTED ":O:u}",
+					       VAR_GLOBAL, VARE_WANTRES);
     }
-    if (vlist) {
-	Var *v;
-	char **av;
-	char *as;
-	int ac;
-	int i;
 
-	av = brk_string(vlist, &ac, FALSE, &as);
-	for (i = 0; i < ac; i++) {
-	    v = VarFind(av[i], VAR_GLOBAL, 0);
-	    if (!v)
+    {
+	Var *v;
+	size_t i;
+
+	Words words = Str_Words(varnames, FALSE);
+	for (i = 0; i < words.len; i++) {
+	    const char *varname = words.words[i];
+	    v = VarFind(varname, VAR_GLOBAL, 0);
+	    if (v == NULL) {
+		VAR_DEBUG("Not unexporting \"%s\" (not found)\n", varname);
 		continue;
-	    if (!unexport_env &&
-		(v->flags & (VAR_EXPORTED|VAR_REEXPORT)) == VAR_EXPORTED) {
-		unsetenv(v->name);
 	    }
-	    v->flags &= ~(VAR_EXPORTED|VAR_REEXPORT);
+
+	    VAR_DEBUG("Unexporting \"%s\"\n", varname);
+	    if (!unexport_env && (v->flags & VAR_EXPORTED) &&
+		!(v->flags & VAR_REEXPORT))
+		unsetenv(v->name);
+	    v->flags &= ~(unsigned)(VAR_EXPORTED | VAR_REEXPORT);
+
 	    /*
 	     * If we are unexporting a list,
 	     * remove each one from .MAKE.EXPORTED.
 	     * If we are removing them all,
 	     * just delete .MAKE.EXPORTED below.
 	     */
-	    if (vlist == str) {
-		n = snprintf(tmp, sizeof(tmp),
-			     "${" MAKE_EXPORTED ":N%s}", v->name);
-		if (n < (int)sizeof(tmp)) {
-		    cp = Var_Subst(NULL, tmp, VAR_GLOBAL, VARF_WANTRES);
-		    Var_Set(MAKE_EXPORTED, cp, VAR_GLOBAL);
-		    free(cp);
-		}
+	    if (varnames == str) {
+		char *expr = str_concat3("${" MAKE_EXPORTED ":N", v->name, "}");
+		char *cp = Var_Subst(expr, VAR_GLOBAL, VARE_WANTRES);
+		Var_Set(MAKE_EXPORTED, cp, VAR_GLOBAL);
+		free(cp);
+		free(expr);
 	    }
 	}
-	free(as);
-	free(av);
-	if (vlist != str) {
+	Words_Free(words);
+	if (varnames != str) {
 	    Var_Delete(MAKE_EXPORTED, VAR_GLOBAL);
-	    free(vlist);
+	    free(varnames_freeIt);
 	}
     }
 }
 
-static void
+/* See Var_Set for documentation. */
+void
 Var_Set_with_flags(const char *name, const char *val, GNode *ctxt,
 		   VarSet_Flags flags)
 {
+    const char *unexpanded_name = name;
+    char *name_freeIt = NULL;
     Var *v;
-    char *expanded_name = NULL;
+
+    assert(val != NULL);
 
     /*
      * We only look for a variable in the given context since anything set
      * here will override anything in a lower context, so there's not much
      * point in searching them all just to save a bit of memory...
      */
-    if (strchr(name, '$') != NULL) {
-	expanded_name = Var_Subst(NULL, name, ctxt, VARF_WANTRES);
-	if (expanded_name[0] == 0) {
-	    if (DEBUG(VAR)) {
-		fprintf(debug_file, "Var_Set(\"%s\", \"%s\", ...) "
-			"name expands to empty string - ignored\n",
-			name, val);
-	    }
-	    free(expanded_name);
-	    return;
-	}
-	name = expanded_name;
+    if (strchr(name, '$') != NULL)
+	name = name_freeIt = Var_Subst(name, ctxt, VARE_WANTRES);
+
+    if (name[0] == '\0') {
+	VAR_DEBUG("Var_Set(\"%s\", \"%s\", ...) "
+		  "name expands to empty string - ignored\n",
+		  unexpanded_name, val);
+	free(name_freeIt);
+	return;
     }
+
     if (ctxt == VAR_GLOBAL) {
 	v = VarFind(name, VAR_CMD, 0);
 	if (v != NULL) {
-	    if ((v->flags & VAR_FROM_CMD)) {
-		if (DEBUG(VAR)) {
-		    fprintf(debug_file, "%s:%s = %s ignored!\n", ctxt->name, name, val);
-		}
+	    if (v->flags & VAR_FROM_CMD) {
+		VAR_DEBUG("%s:%s = %s ignored!\n", ctxt->name, name, val);
 		goto out;
 	    }
 	    VarFreeEnv(v, TRUE);
 	}
     }
+
     v = VarFind(name, ctxt, 0);
     if (v == NULL) {
-	if (ctxt == VAR_CMD && (flags & VAR_NO_EXPORT) == 0) {
+	if (ctxt == VAR_CMD && !(flags & VAR_NO_EXPORT)) {
 	    /*
 	     * This var would normally prevent the same name being added
 	     * to VAR_GLOBAL, so delete it from there if needed.
@@ -900,24 +805,28 @@ Var_Set_with_flags(const char *name, const char *val, GNode *ctxt,
 	     */
 	    Var_Delete(name, VAR_GLOBAL);
 	}
-	VarAdd(name, val, ctxt);
+	VarAdd(name, val, ctxt, flags);
     } else {
+	if ((v->flags & VAR_READONLY) && !(flags & VAR_SET_READONLY)) {
+	    VAR_DEBUG("%s:%s = %s ignored (read-only)\n",
+	      ctxt->name, name, val);
+	    goto out;
+	}	    
 	Buf_Empty(&v->val);
 	if (val)
-	    Buf_AddBytes(&v->val, strlen(val), val);
+	    Buf_AddStr(&v->val, val);
 
-	if (DEBUG(VAR)) {
-	    fprintf(debug_file, "%s:%s = %s\n", ctxt->name, name, val);
-	}
-	if ((v->flags & VAR_EXPORTED)) {
+	VAR_DEBUG("%s:%s = %s\n", ctxt->name, name, val);
+	if (v->flags & VAR_EXPORTED) {
 	    Var_Export1(name, VAR_EXPORT_PARENT);
 	}
     }
     /*
      * Any variables given on the command line are automatically exported
      * to the environment (as per POSIX standard)
+     * Other than internals.
      */
-    if (ctxt == VAR_CMD && (flags & VAR_NO_EXPORT) == 0) {
+    if (ctxt == VAR_CMD && !(flags & VAR_NO_EXPORT) && name[0] != '.') {
 	if (v == NULL) {
 	    /* we just added it */
 	    v = VarFind(name, ctxt, 0);
@@ -930,18 +839,16 @@ Var_Set_with_flags(const char *name, const char *val, GNode *ctxt,
 	 * that the command-line settings continue to override
 	 * Makefile settings.
 	 */
-	if (varNoExportEnv != TRUE)
+	if (!varNoExportEnv)
 	    setenv(name, val ? val : "", 1);
 
 	Var_Append(MAKEOVERRIDES, name, VAR_GLOBAL);
     }
-    if (*name == '.') {
-	if (strcmp(name, SAVE_DOLLARS) == 0)
-	    save_dollars = s2Boolean(val, save_dollars);
-    }
+    if (name[0] == '.' && strcmp(name, SAVE_DOLLARS) == 0)
+	save_dollars = s2Boolean(val, save_dollars);
 
 out:
-    free(expanded_name);
+    free(name_freeIt);
     if (v != NULL)
 	VarFreeEnv(v, TRUE);
 }
@@ -951,15 +858,14 @@ out:
  * Var_Set --
  *	Set the variable name to the value val in the given context.
  *
+ *	If the variable doesn't yet exist, it is created.
+ *	Otherwise the new value overwrites and replaces the old value.
+ *
  * Input:
  *	name		name of variable to set
  *	val		value to give to the variable
  *	ctxt		context in which to set it
  *
- * Side Effects:
- *	If the variable doesn't yet exist, a new record is created for it.
- *	Else the old value is freed and the new one stuck in its place
- *
  * Notes:
  *	The variable is searched for only in its context before being
  *	created in that context. I.e. if the context is VAR_GLOBAL,
@@ -983,14 +889,13 @@ Var_Set(const char *name, const char *val, GNode *ctxt)
  *	The variable of the given name has the given value appended to it in
  *	the given context.
  *
+ *	If the variable doesn't exist, it is created. Otherwise the strings
+ *	are concatenated, with a space in between.
+ *
  * Input:
  *	name		name of variable to modify
- *	val		String to append to it
- *	ctxt		Context in which this should occur
- *
- * Side Effects:
- *	If the variable doesn't exist, it is created. Else the strings
- *	are concatenated (with a space in between).
+ *	val		string to append to it
+ *	ctxt		context in which this should occur
  *
  * Notes:
  *	Only if the variable is being sought in the global context is the
@@ -1004,83 +909,71 @@ Var_Set(const char *name, const char *val, GNode *ctxt)
 void
 Var_Append(const char *name, const char *val, GNode *ctxt)
 {
+    char *name_freeIt = NULL;
     Var *v;
-    Hash_Entry *h;
-    char *expanded_name = NULL;
+
+    assert(val != NULL);
 
     if (strchr(name, '$') != NULL) {
-	expanded_name = Var_Subst(NULL, name, ctxt, VARF_WANTRES);
-	if (expanded_name[0] == 0) {
-	    if (DEBUG(VAR)) {
-		fprintf(debug_file, "Var_Append(\"%s\", \"%s\", ...) "
-			"name expands to empty string - ignored\n",
-			name, val);
-	    }
-	    free(expanded_name);
+	const char *unexpanded_name = name;
+	name = name_freeIt = Var_Subst(name, ctxt, VARE_WANTRES);
+	if (name[0] == '\0') {
+	    VAR_DEBUG("Var_Append(\"%s\", \"%s\", ...) "
+		      "name expands to empty string - ignored\n",
+		      unexpanded_name, val);
+	    free(name_freeIt);
 	    return;
 	}
-	name = expanded_name;
     }
 
-    v = VarFind(name, ctxt, ctxt == VAR_GLOBAL ? (FIND_CMD|FIND_ENV) : 0);
+    v = VarFind(name, ctxt, ctxt == VAR_GLOBAL ? (FIND_CMD | FIND_ENV) : 0);
 
     if (v == NULL) {
 	Var_Set(name, val, ctxt);
     } else if (ctxt == VAR_CMD || !(v->flags & VAR_FROM_CMD)) {
 	Buf_AddByte(&v->val, ' ');
-	Buf_AddBytes(&v->val, strlen(val), val);
+	Buf_AddStr(&v->val, val);
 
-	if (DEBUG(VAR)) {
-	    fprintf(debug_file, "%s:%s = %s\n", ctxt->name, name,
-		    Buf_GetAll(&v->val, NULL));
-	}
+	VAR_DEBUG("%s:%s = %s\n", ctxt->name, name,
+		  Buf_GetAll(&v->val, NULL));
 
 	if (v->flags & VAR_FROM_ENV) {
+	    Hash_Entry *h;
+
 	    /*
 	     * If the original variable came from the environment, we
 	     * have to install it in the global context (we could place
 	     * it in the environment, but then we should provide a way to
 	     * export other variables...)
 	     */
-	    v->flags &= ~VAR_FROM_ENV;
+	    v->flags &= ~(unsigned)VAR_FROM_ENV;
 	    h = Hash_CreateEntry(&ctxt->context, name, NULL);
 	    Hash_SetValue(h, v);
 	}
     }
-    free(expanded_name);
+    free(name_freeIt);
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Var_Exists --
- *	See if the given variable exists.
+/* See if the given variable exists, in the given context or in other
+ * fallback contexts.
  *
  * Input:
  *	name		Variable to find
  *	ctxt		Context in which to start search
- *
- * Results:
- *	TRUE if it does, FALSE if it doesn't
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
  */
 Boolean
 Var_Exists(const char *name, GNode *ctxt)
 {
-    Var		  *v;
-    char          *cp;
+    char *name_freeIt = NULL;
+    Var *v;
 
-    if ((cp = strchr(name, '$')) != NULL) {
-	cp = Var_Subst(NULL, name, ctxt, VARF_WANTRES);
-    }
-    v = VarFind(cp ? cp : name, ctxt, FIND_CMD|FIND_GLOBAL|FIND_ENV);
-    free(cp);
-    if (v == NULL) {
+    if (strchr(name, '$') != NULL)
+	name = name_freeIt = Var_Subst(name, ctxt, VARE_WANTRES);
+
+    v = VarFind(name, ctxt, FIND_CMD | FIND_GLOBAL | FIND_ENV);
+    free(name_freeIt);
+    if (v == NULL)
 	return FALSE;
-    }
 
     (void)VarFreeEnv(v, TRUE);
     return TRUE;
@@ -1089,614 +982,552 @@ Var_Exists(const char *name, GNode *ctxt)
 /*-
  *-----------------------------------------------------------------------
  * Var_Value --
- *	Return the value of the named variable in the given context
+ *	Return the unexpanded value of the given variable in the given
+ *	context, or the usual contexts.
  *
  * Input:
  *	name		name to find
  *	ctxt		context in which to search for it
  *
  * Results:
- *	The value if the variable exists, NULL if it doesn't
- *
- * Side Effects:
- *	None
+ *	The value if the variable exists, NULL if it doesn't.
+ *	If the returned value is not NULL, the caller must free *freeIt
+ *	as soon as the returned value is no longer needed.
  *-----------------------------------------------------------------------
  */
-char *
-Var_Value(const char *name, GNode *ctxt, char **frp)
+const char *
+Var_Value(const char *name, GNode *ctxt, char **freeIt)
 {
-    Var *v;
+    Var *v = VarFind(name, ctxt, FIND_ENV | FIND_GLOBAL | FIND_CMD);
+    char *p;
 
-    v = VarFind(name, ctxt, FIND_ENV | FIND_GLOBAL | FIND_CMD);
-    *frp = NULL;
+    *freeIt = NULL;
     if (v == NULL)
 	return NULL;
 
-    char *p = (Buf_GetAll(&v->val, NULL));
+    p = Buf_GetAll(&v->val, NULL);
     if (VarFreeEnv(v, FALSE))
-	*frp = p;
+	*freeIt = p;
     return p;
 }
 
 
-/* This callback for VarModify gets a single word from an expression and
- * typically adds a modification of this word to the buffer. It may also do
- * nothing or add several words.
- *
- * If addSpaces is TRUE, it must add a space before adding anything else to
- * the buffer.
- *
- * It returns the addSpace value for the next call of this callback. Typical
- * return values are the current addSpaces or TRUE. */
-typedef Boolean (*VarModifyCallback)(GNode *ctxt, Var_Parse_State *vpstate,
-    const char *word, Boolean addSpace, Buffer *buf, void *data);
+/* SepBuf is a string being built from "words", interleaved with separators. */
+typedef struct {
+    Buffer buf;
+    Boolean needSep;
+    char sep;			/* usually ' ', but see the :ts modifier */
+} SepBuf;
 
-
-/* Callback function for VarModify to implement the :H modifier.
- * Add the dirname of the given word to the buffer. */
-static Boolean
-VarHead(GNode *ctx MAKE_ATTR_UNUSED, Var_Parse_State *vpstate,
-	const char *word, Boolean addSpace, Buffer *buf,
-	void *dummy MAKE_ATTR_UNUSED)
+static void
+SepBuf_Init(SepBuf *buf, char sep)
 {
-    const char *slash = strrchr(word, '/');
-
-    if (addSpace && vpstate->varSpace)
-	Buf_AddByte(buf, vpstate->varSpace);
-    if (slash != NULL)
-	Buf_AddBytes(buf, slash - word, word);
-    else
-	Buf_AddByte(buf, '.');
-
-    return TRUE;
+    Buf_Init(&buf->buf, 32 /* bytes */);
+    buf->needSep = FALSE;
+    buf->sep = sep;
 }
 
-/* Callback function for VarModify to implement the :T modifier.
+static void
+SepBuf_Sep(SepBuf *buf)
+{
+    buf->needSep = TRUE;
+}
+
+static void
+SepBuf_AddBytes(SepBuf *buf, const char *mem, size_t mem_size)
+{
+    if (mem_size == 0)
+	return;
+    if (buf->needSep && buf->sep != '\0') {
+	Buf_AddByte(&buf->buf, buf->sep);
+	buf->needSep = FALSE;
+    }
+    Buf_AddBytes(&buf->buf, mem, mem_size);
+}
+
+static void
+SepBuf_AddBytesBetween(SepBuf *buf, const char *start, const char *end)
+{
+    SepBuf_AddBytes(buf, start, (size_t)(end - start));
+}
+
+static void
+SepBuf_AddStr(SepBuf *buf, const char *str)
+{
+    SepBuf_AddBytes(buf, str, strlen(str));
+}
+
+static char *
+SepBuf_Destroy(SepBuf *buf, Boolean free_buf)
+{
+    return Buf_Destroy(&buf->buf, free_buf);
+}
+
+
+/* This callback for ModifyWords gets a single word from an expression and
+ * typically adds a modification of this word to the buffer. It may also do
+ * nothing or add several words. */
+typedef void (*ModifyWordsCallback)(const char *word, SepBuf *buf, void *data);
+
+
+/* Callback for ModifyWords to implement the :H modifier.
+ * Add the dirname of the given word to the buffer. */
+static void
+ModifyWord_Head(const char *word, SepBuf *buf, void *dummy MAKE_ATTR_UNUSED)
+{
+    const char *slash = strrchr(word, '/');
+    if (slash != NULL)
+	SepBuf_AddBytesBetween(buf, word, slash);
+    else
+	SepBuf_AddStr(buf, ".");
+}
+
+/* Callback for ModifyWords to implement the :T modifier.
  * Add the basename of the given word to the buffer. */
-static Boolean
-VarTail(GNode *ctx MAKE_ATTR_UNUSED, Var_Parse_State *vpstate,
-	const char *word, Boolean addSpace, Buffer *buf,
-	void *dummy MAKE_ATTR_UNUSED)
+static void
+ModifyWord_Tail(const char *word, SepBuf *buf, void *dummy MAKE_ATTR_UNUSED)
 {
     const char *slash = strrchr(word, '/');
     const char *base = slash != NULL ? slash + 1 : word;
-
-    if (addSpace && vpstate->varSpace)
-	Buf_AddByte(buf, vpstate->varSpace);
-    Buf_AddBytes(buf, strlen(base), base);
-    return TRUE;
+    SepBuf_AddStr(buf, base);
 }
 
-/* Callback function for VarModify to implement the :E modifier.
+/* Callback for ModifyWords to implement the :E modifier.
  * Add the filename suffix of the given word to the buffer, if it exists. */
-static Boolean
-VarSuffix(GNode *ctx MAKE_ATTR_UNUSED, Var_Parse_State *vpstate,
-	  const char *word, Boolean addSpace, Buffer *buf,
-	  void *dummy MAKE_ATTR_UNUSED)
+static void
+ModifyWord_Suffix(const char *word, SepBuf *buf, void *dummy MAKE_ATTR_UNUSED)
 {
     const char *dot = strrchr(word, '.');
-    if (dot == NULL)
-	return addSpace;
-
-    if (addSpace && vpstate->varSpace)
-	Buf_AddByte(buf, vpstate->varSpace);
-    Buf_AddBytes(buf, strlen(dot + 1), dot + 1);
-    return TRUE;
+    if (dot != NULL)
+	SepBuf_AddStr(buf, dot + 1);
 }
 
-/* Callback function for VarModify to implement the :R modifier.
- * Add the filename basename of the given word to the buffer. */
-static Boolean
-VarRoot(GNode *ctx MAKE_ATTR_UNUSED, Var_Parse_State *vpstate,
-	const char *word, Boolean addSpace, Buffer *buf,
-	void *dummy MAKE_ATTR_UNUSED)
+/* Callback for ModifyWords to implement the :R modifier.
+ * Add the basename of the given word to the buffer. */
+static void
+ModifyWord_Root(const char *word, SepBuf *buf, void *dummy MAKE_ATTR_UNUSED)
 {
-    char *dot = strrchr(word, '.');
+    const char *dot = strrchr(word, '.');
     size_t len = dot != NULL ? (size_t)(dot - word) : strlen(word);
-
-    if (addSpace && vpstate->varSpace)
-	Buf_AddByte(buf, vpstate->varSpace);
-    Buf_AddBytes(buf, len, word);
-    return TRUE;
+    SepBuf_AddBytes(buf, word, len);
 }
 
-/* Callback function for VarModify to implement the :M modifier.
+/* Callback for ModifyWords to implement the :M modifier.
  * Place the word in the buffer if it matches the given pattern. */
-static Boolean
-VarMatch(GNode *ctx MAKE_ATTR_UNUSED, Var_Parse_State *vpstate,
-	 const char *word, Boolean addSpace, Buffer *buf,
-	 void *data)
+static void
+ModifyWord_Match(const char *word, SepBuf *buf, void *data)
+{
+    const char *pattern = data;
+    VAR_DEBUG("VarMatch [%s] [%s]\n", word, pattern);
+    if (Str_Match(word, pattern))
+	SepBuf_AddStr(buf, word);
+}
+
+/* Callback for ModifyWords to implement the :N modifier.
+ * Place the word in the buffer if it doesn't match the given pattern. */
+static void
+ModifyWord_NoMatch(const char *word, SepBuf *buf, void *data)
 {
     const char *pattern = data;
-    if (DEBUG(VAR))
-	fprintf(debug_file, "VarMatch [%s] [%s]\n", word, pattern);
     if (!Str_Match(word, pattern))
-	return addSpace;
-    if (addSpace && vpstate->varSpace)
-	Buf_AddByte(buf, vpstate->varSpace);
-    Buf_AddBytes(buf, strlen(word), word);
-    return TRUE;
+	SepBuf_AddStr(buf, word);
 }
 
 #ifdef SYSVVARSUB
-/* Callback function for VarModify to implement the :%.from=%.to modifier. */
-static Boolean
-VarSYSVMatch(GNode *ctx, Var_Parse_State *vpstate,
-	     const char *word, Boolean addSpace, Buffer *buf,
-	     void *data)
+/*-
+ *-----------------------------------------------------------------------
+ * Str_SYSVMatch --
+ *	Check word against pattern for a match (% is wild),
+ *
+ * Input:
+ *	word		Word to examine
+ *	pattern		Pattern to examine against
+ *
+ * Results:
+ *	Returns the start of the match, or NULL.
+ *	*match_len returns the length of the match, if any.
+ *	*hasPercent returns whether the pattern contains a percent.
+ *-----------------------------------------------------------------------
+ */
+static const char *
+Str_SYSVMatch(const char *word, const char *pattern, size_t *match_len,
+	      Boolean *hasPercent)
 {
-    size_t len;
-    char *ptr;
-    Boolean hasPercent;
-    VarPattern *pat = data;
+    const char *p = pattern;
+    const char *w = word;
+    const char *percent;
+    size_t w_len;
+    size_t p_len;
+    const char *w_tail;
 
-    if (addSpace && vpstate->varSpace)
-	Buf_AddByte(buf, vpstate->varSpace);
-
-    if ((ptr = Str_SYSVMatch(word, pat->lhs, &len, &hasPercent)) != NULL) {
-	char *varexp = Var_Subst(NULL, pat->rhs, ctx, VARF_WANTRES);
-	Str_SYSVSubst(buf, varexp, ptr, len, hasPercent);
-	free(varexp);
-    } else {
-	Buf_AddBytes(buf, strlen(word), word);
+    *hasPercent = FALSE;
+    if (*p == '\0') {		/* ${VAR:=suffix} */
+	*match_len = strlen(w);	/* Null pattern is the whole string */
+	return w;
     }
 
-    return TRUE;
+    percent = strchr(p, '%');
+    if (percent != NULL) {	/* ${VAR:...%...=...} */
+	*hasPercent = TRUE;
+	if (*w == '\0')
+	    return NULL;	/* empty word does not match pattern */
+
+	/* check that the prefix matches */
+	for (; p != percent && *w != '\0' && *w == *p; w++, p++)
+	    continue;
+	if (p != percent)
+	    return NULL;	/* No match */
+
+	p++;			/* Skip the percent */
+	if (*p == '\0') {
+	    /* No more pattern, return the rest of the string */
+	    *match_len = strlen(w);
+	    return w;
+	}
+    }
+
+    /* Test whether the tail matches */
+    w_len = strlen(w);
+    p_len = strlen(p);
+    if (w_len < p_len)
+	return NULL;
+
+    w_tail = w + w_len - p_len;
+    if (memcmp(p, w_tail, p_len) != 0)
+	return NULL;
+
+    *match_len = (size_t)(w_tail - w);
+    return w;
+}
+
+typedef struct {
+    GNode *ctx;
+    const char *lhs;
+    const char *rhs;
+} ModifyWord_SYSVSubstArgs;
+
+/* Callback for ModifyWords to implement the :%.from=%.to modifier. */
+static void
+ModifyWord_SYSVSubst(const char *word, SepBuf *buf, void *data)
+{
+    const ModifyWord_SYSVSubstArgs *args = data;
+    char *rhs_expanded;
+    const char *rhs;
+    const char *percent;
+
+    size_t match_len;
+    Boolean lhsPercent;
+    const char *match = Str_SYSVMatch(word, args->lhs, &match_len, &lhsPercent);
+    if (match == NULL) {
+	SepBuf_AddStr(buf, word);
+	return;
+    }
+
+    /* Append rhs to the buffer, substituting the first '%' with the
+     * match, but only if the lhs had a '%' as well. */
+
+    rhs_expanded = Var_Subst(args->rhs, args->ctx, VARE_WANTRES);
+
+    rhs = rhs_expanded;
+    percent = strchr(rhs, '%');
+
+    if (percent != NULL && lhsPercent) {
+	/* Copy the prefix of the replacement pattern */
+	SepBuf_AddBytesBetween(buf, rhs, percent);
+	rhs = percent + 1;
+    }
+    if (percent != NULL || !lhsPercent)
+	SepBuf_AddBytes(buf, match, match_len);
+
+    /* Append the suffix of the replacement pattern */
+    SepBuf_AddStr(buf, rhs);
+
+    free(rhs_expanded);
 }
 #endif
 
-/* Callback function for VarModify to implement the :N modifier.
- * Place the word in the buffer if it doesn't match the given pattern. */
-static Boolean
-VarNoMatch(GNode *ctx MAKE_ATTR_UNUSED, Var_Parse_State *vpstate,
-	   const char *word, Boolean addSpace, Buffer *buf,
-	   void *data)
-{
-    const char *pattern = data;
-    if (Str_Match(word, pattern))
-	return addSpace;
-    if (addSpace && vpstate->varSpace)
-	Buf_AddByte(buf, vpstate->varSpace);
-    Buf_AddBytes(buf, strlen(word), word);
-    return TRUE;
-}
 
-/* Callback function for VarModify to implement the :S,from,to, modifier.
+typedef struct {
+    const char	*lhs;
+    size_t	lhsLen;
+    const char	*rhs;
+    size_t	rhsLen;
+    VarPatternFlags pflags;
+    Boolean	matched;
+} ModifyWord_SubstArgs;
+
+/* Callback for ModifyWords to implement the :S,from,to, modifier.
  * Perform a string substitution on the given word. */
-static Boolean
-VarSubstitute(GNode *ctx MAKE_ATTR_UNUSED, Var_Parse_State *vpstate,
-	      const char *word, Boolean addSpace, Buffer *buf,
-	      void *data)
+static void
+ModifyWord_Subst(const char *word, SepBuf *buf, void *data)
 {
-    int wordLen = strlen(word);
-    const char *cp;		/* General pointer */
-    VarPattern *pattern = data;
+    size_t wordLen = strlen(word);
+    ModifyWord_SubstArgs *args = data;
+    const char *match;
 
-    if ((pattern->flags & (VAR_SUB_ONE|VAR_SUB_MATCHED)) !=
-	(VAR_SUB_ONE|VAR_SUB_MATCHED)) {
-	/*
-	 * Still substituting -- break it down into simple anchored cases
-	 * and if none of them fits, perform the general substitution case.
-	 */
-	if ((pattern->flags & VAR_MATCH_START) &&
-	    (strncmp(word, pattern->lhs, pattern->leftLen) == 0)) {
-	    /*
-	     * Anchored at start and beginning of word matches pattern
-	     */
-	    if ((pattern->flags & VAR_MATCH_END) &&
-	        (wordLen == pattern->leftLen)) {
-		/*
-		 * Also anchored at end and matches to the end (word
-		 * is same length as pattern) add space and rhs only
-		 * if rhs is non-null.
-		 */
-		if (pattern->rightLen != 0) {
-		    if (addSpace && vpstate->varSpace) {
-			Buf_AddByte(buf, vpstate->varSpace);
-		    }
-		    addSpace = TRUE;
-		    Buf_AddBytes(buf, pattern->rightLen, pattern->rhs);
-		}
-		pattern->flags |= VAR_SUB_MATCHED;
-	    } else if (pattern->flags & VAR_MATCH_END) {
-		/*
-		 * Doesn't match to end -- copy word wholesale
-		 */
-		goto nosub;
-	    } else {
-		/*
-		 * Matches at start but need to copy in trailing characters
-		 */
-		if ((pattern->rightLen + wordLen - pattern->leftLen) != 0) {
-		    if (addSpace && vpstate->varSpace) {
-			Buf_AddByte(buf, vpstate->varSpace);
-		    }
-		    addSpace = TRUE;
-		}
-		Buf_AddBytes(buf, pattern->rightLen, pattern->rhs);
-		Buf_AddBytes(buf, wordLen - pattern->leftLen,
-			     (word + pattern->leftLen));
-		pattern->flags |= VAR_SUB_MATCHED;
-	    }
-	} else if (pattern->flags & VAR_MATCH_START) {
-	    /*
-	     * Had to match at start of word and didn't -- copy whole word.
-	     */
+    if ((args->pflags & VARP_SUB_ONE) && args->matched)
+	goto nosub;
+
+    if (args->pflags & VARP_ANCHOR_START) {
+	if (wordLen < args->lhsLen ||
+	    memcmp(word, args->lhs, args->lhsLen) != 0)
 	    goto nosub;
-	} else if (pattern->flags & VAR_MATCH_END) {
-	    /*
-	     * Anchored at end, Find only place match could occur (leftLen
-	     * characters from the end of the word) and see if it does. Note
-	     * that because the $ will be left at the end of the lhs, we have
-	     * to use strncmp.
-	     */
-	    cp = word + (wordLen - pattern->leftLen);
-	    if ((cp >= word) &&
-		(strncmp(cp, pattern->lhs, pattern->leftLen) == 0)) {
-		/*
-		 * Match found. If we will place characters in the buffer,
-		 * add a space before hand as indicated by addSpace, then
-		 * stuff in the initial, unmatched part of the word followed
-		 * by the right-hand-side.
-		 */
-		if (((cp - word) + pattern->rightLen) != 0) {
-		    if (addSpace && vpstate->varSpace) {
-			Buf_AddByte(buf, vpstate->varSpace);
-		    }
-		    addSpace = TRUE;
-		}
-		Buf_AddBytes(buf, cp - word, word);
-		Buf_AddBytes(buf, pattern->rightLen, pattern->rhs);
-		pattern->flags |= VAR_SUB_MATCHED;
-	    } else {
-		/*
-		 * Had to match at end and didn't. Copy entire word.
-		 */
-		goto nosub;
-	    }
-	} else {
-	    /*
-	     * Pattern is unanchored: search for the pattern in the word using
-	     * String_FindSubstring, copying unmatched portions and the
-	     * right-hand-side for each match found, handling non-global
-	     * substitutions correctly, etc. When the loop is done, any
-	     * remaining part of the word (word and wordLen are adjusted
-	     * accordingly through the loop) is copied straight into the
-	     * buffer.
-	     * addSpace is set FALSE as soon as a space is added to the
-	     * buffer.
-	     */
-	    Boolean done;
-	    int origSize;
 
-	    done = FALSE;
-	    origSize = Buf_Size(buf);
-	    while (!done) {
-		cp = Str_FindSubstring(word, pattern->lhs);
-		if (cp != NULL) {
-		    if (addSpace && (((cp - word) + pattern->rightLen) != 0)) {
-			Buf_AddByte(buf, vpstate->varSpace);
-			addSpace = FALSE;
-		    }
-		    Buf_AddBytes(buf, cp - word, word);
-		    Buf_AddBytes(buf, pattern->rightLen, pattern->rhs);
-		    wordLen -= (cp - word) + pattern->leftLen;
-		    word = cp + pattern->leftLen;
-		    if (wordLen == 0) {
-			done = TRUE;
-		    }
-		    if ((pattern->flags & VAR_SUB_GLOBAL) == 0) {
-			done = TRUE;
-		    }
-		    pattern->flags |= VAR_SUB_MATCHED;
-		} else {
-		    done = TRUE;
-		}
-	    }
-	    if (wordLen != 0) {
-		if (addSpace && vpstate->varSpace) {
-		    Buf_AddByte(buf, vpstate->varSpace);
-		}
-		Buf_AddBytes(buf, wordLen, word);
-	    }
-	    /*
-	     * If added characters to the buffer, need to add a space
-	     * before we add any more. If we didn't add any, just return
-	     * the previous value of addSpace.
-	     */
-	    return (Buf_Size(buf) != origSize) || addSpace;
+	if (args->pflags & VARP_ANCHOR_END) {
+	    if (wordLen != args->lhsLen)
+		goto nosub;
+
+	    /* :S,^whole$,replacement, */
+	    SepBuf_AddBytes(buf, args->rhs, args->rhsLen);
+	    args->matched = TRUE;
+	} else {
+	    /* :S,^prefix,replacement, */
+	    SepBuf_AddBytes(buf, args->rhs, args->rhsLen);
+	    SepBuf_AddBytes(buf, word + args->lhsLen, wordLen - args->lhsLen);
+	    args->matched = TRUE;
 	}
-	return addSpace;
+	return;
+    }
+
+    if (args->pflags & VARP_ANCHOR_END) {
+	const char *start;
+
+	if (wordLen < args->lhsLen)
+	    goto nosub;
+
+	start = word + (wordLen - args->lhsLen);
+	if (memcmp(start, args->lhs, args->lhsLen) != 0)
+	    goto nosub;
+
+	/* :S,suffix$,replacement, */
+	SepBuf_AddBytesBetween(buf, word, start);
+	SepBuf_AddBytes(buf, args->rhs, args->rhsLen);
+	args->matched = TRUE;
+	return;
+    }
+
+    /* unanchored case, may match more than once */
+    while ((match = Str_FindSubstring(word, args->lhs)) != NULL) {
+	SepBuf_AddBytesBetween(buf, word, match);
+	SepBuf_AddBytes(buf, args->rhs, args->rhsLen);
+	args->matched = TRUE;
+	wordLen -= (size_t)(match - word) + args->lhsLen;
+	word += (size_t)(match - word) + args->lhsLen;
+	if (wordLen == 0 || !(args->pflags & VARP_SUB_GLOBAL))
+	    break;
     }
 nosub:
-    if (addSpace && vpstate->varSpace) {
-	Buf_AddByte(buf, vpstate->varSpace);
-    }
-    Buf_AddBytes(buf, wordLen, word);
-    return TRUE;
+    SepBuf_AddBytes(buf, word, wordLen);
 }
 
 #ifndef NO_REGEX
-/*-
- *-----------------------------------------------------------------------
- * VarREError --
- *	Print the error caused by a regcomp or regexec call.
- *
- * Side Effects:
- *	An error gets printed.
- *
- *-----------------------------------------------------------------------
- */
+/* Print the error caused by a regcomp or regexec call. */
 static void
 VarREError(int reerr, regex_t *pat, const char *str)
 {
-    char *errbuf;
-    int errlen;
-
-    errlen = regerror(reerr, pat, 0, 0);
-    errbuf = bmake_malloc(errlen);
+    size_t errlen = regerror(reerr, pat, 0, 0);
+    char *errbuf = bmake_malloc(errlen);
     regerror(reerr, pat, errbuf, errlen);
     Error("%s: %s", str, errbuf);
     free(errbuf);
 }
 
-/* Callback function for VarModify to implement the :C/from/to/ modifier.
+typedef struct {
+    regex_t	   re;
+    size_t	   nsub;
+    char 	  *replace;
+    VarPatternFlags pflags;
+    Boolean	   matched;
+} ModifyWord_SubstRegexArgs;
+
+/* Callback for ModifyWords to implement the :C/from/to/ modifier.
  * Perform a regex substitution on the given word. */
-static Boolean
-VarRESubstitute(GNode *ctx MAKE_ATTR_UNUSED,
-		Var_Parse_State *vpstate MAKE_ATTR_UNUSED,
-		const char *word, Boolean addSpace, Buffer *buf,
-		void *data)
+static void
+ModifyWord_SubstRegex(const char *word, SepBuf *buf, void *data)
 {
-    VarREPattern *pat = data;
+    ModifyWord_SubstRegexArgs *args = data;
     int xrv;
     const char *wp = word;
     char *rp;
-    int added = 0;
     int flags = 0;
+    regmatch_t m[10];
 
-#define MAYBE_ADD_SPACE()		\
-	if (addSpace && !added)		\
-	    Buf_AddByte(buf, ' ');	\
-	added = 1
+    if ((args->pflags & VARP_SUB_ONE) && args->matched)
+	goto nosub;
 
-    if ((pat->flags & (VAR_SUB_ONE|VAR_SUB_MATCHED)) ==
-	(VAR_SUB_ONE|VAR_SUB_MATCHED))
-	xrv = REG_NOMATCH;
-    else {
-    tryagain:
-	xrv = regexec(&pat->re, wp, pat->nsub, pat->matches, flags);
-    }
+tryagain:
+    xrv = regexec(&args->re, wp, args->nsub, m, flags);
 
     switch (xrv) {
     case 0:
-	pat->flags |= VAR_SUB_MATCHED;
-	if (pat->matches[0].rm_so > 0) {
-	    MAYBE_ADD_SPACE();
-	    Buf_AddBytes(buf, pat->matches[0].rm_so, wp);
-	}
+	args->matched = TRUE;
+	SepBuf_AddBytes(buf, wp, (size_t)m[0].rm_so);
 
-	for (rp = pat->replace; *rp; rp++) {
-	    if ((*rp == '\\') && ((rp[1] == '&') || (rp[1] == '\\'))) {
-		MAYBE_ADD_SPACE();
-		Buf_AddByte(buf, rp[1]);
+	for (rp = args->replace; *rp; rp++) {
+	    if (*rp == '\\' && (rp[1] == '&' || rp[1] == '\\')) {
+		SepBuf_AddBytes(buf, rp + 1, 1);
 		rp++;
-	    } else if ((*rp == '&') ||
-		((*rp == '\\') && isdigit((unsigned char)rp[1]))) {
-		int n;
-		const char *subbuf;
-		int sublen;
-		char errstr[3];
+		continue;
+	    }
 
-		if (*rp == '&') {
-		    n = 0;
-		    errstr[0] = '&';
-		    errstr[1] = '\0';
+	    if (*rp == '&') {
+		SepBuf_AddBytesBetween(buf, wp + m[0].rm_so, wp + m[0].rm_eo);
+		continue;
+	    }
+
+	    if (*rp != '\\' || !isdigit((unsigned char)rp[1])) {
+		SepBuf_AddBytes(buf, rp, 1);
+		continue;
+	    }
+
+	    {			/* \0 to \9 backreference */
+		size_t n = (size_t)(rp[1] - '0');
+		rp++;
+
+		if (n >= args->nsub) {
+		    Error("No subexpression \\%zu", n);
+		} else if (m[n].rm_so == -1 && m[n].rm_eo == -1) {
+		    Error("No match for subexpression \\%zu", n);
 		} else {
-		    n = rp[1] - '0';
-		    errstr[0] = '\\';
-		    errstr[1] = rp[1];
-		    errstr[2] = '\0';
-		    rp++;
+		    SepBuf_AddBytesBetween(buf, wp + m[n].rm_so,
+					   wp + m[n].rm_eo);
 		}
-
-		if (n > pat->nsub) {
-		    Error("No subexpression %s", &errstr[0]);
-		    subbuf = "";
-		    sublen = 0;
-		} else if ((pat->matches[n].rm_so == -1) &&
-			   (pat->matches[n].rm_eo == -1)) {
-		    Error("No match for subexpression %s", &errstr[0]);
-		    subbuf = "";
-		    sublen = 0;
-		} else {
-		    subbuf = wp + pat->matches[n].rm_so;
-		    sublen = pat->matches[n].rm_eo - pat->matches[n].rm_so;
-		}
-
-		if (sublen > 0) {
-		    MAYBE_ADD_SPACE();
-		    Buf_AddBytes(buf, sublen, subbuf);
-		}
-	    } else {
-		MAYBE_ADD_SPACE();
-		Buf_AddByte(buf, *rp);
 	    }
 	}
-	wp += pat->matches[0].rm_eo;
-	if (pat->flags & VAR_SUB_GLOBAL) {
-	    flags |= REG_NOTBOL;
-	    if (pat->matches[0].rm_so == 0 && pat->matches[0].rm_eo == 0) {
-		MAYBE_ADD_SPACE();
-		Buf_AddByte(buf, *wp);
-		wp++;
 
+	wp += m[0].rm_eo;
+	if (args->pflags & VARP_SUB_GLOBAL) {
+	    flags |= REG_NOTBOL;
+	    if (m[0].rm_so == 0 && m[0].rm_eo == 0) {
+		SepBuf_AddBytes(buf, wp, 1);
+		wp++;
 	    }
 	    if (*wp)
 		goto tryagain;
 	}
 	if (*wp) {
-	    MAYBE_ADD_SPACE();
-	    Buf_AddBytes(buf, strlen(wp), wp);
+	    SepBuf_AddStr(buf, wp);
 	}
 	break;
     default:
-	VarREError(xrv, &pat->re, "Unexpected regex error");
+	VarREError(xrv, &args->re, "Unexpected regex error");
 	/* fall through */
     case REG_NOMATCH:
-	if (*wp) {
-	    MAYBE_ADD_SPACE();
-	    Buf_AddBytes(buf, strlen(wp), wp);
-	}
+    nosub:
+	SepBuf_AddStr(buf, wp);
 	break;
     }
-    return addSpace || added;
 }
 #endif
 
 
-/* Callback function for VarModify to implement the :@var@...@ modifier of
- * ODE make. We set the temp variable named in pattern.lhs to word and
- * expand pattern.rhs. */
-static Boolean
-VarLoopExpand(GNode *ctx MAKE_ATTR_UNUSED,
-	      Var_Parse_State *vpstate MAKE_ATTR_UNUSED,
-	      const char *word, Boolean addSpace, Buffer *buf,
-	      void *data)
-{
-    VarLoop *loop = data;
-    char *s;
-    int slen;
+typedef struct {
+    GNode	*ctx;
+    char	*tvar;		/* name of temporary variable */
+    char	*str;		/* string to expand */
+    VarEvalFlags eflags;
+} ModifyWord_LoopArgs;
 
-    if (*word) {
-	Var_Set_with_flags(loop->tvar, word, loop->ctxt, VAR_NO_EXPORT);
-	s = Var_Subst(NULL, loop->str, loop->ctxt, loop->flags);
-	if (DEBUG(VAR)) {
-	    fprintf(debug_file,
-		    "VarLoopExpand: in \"%s\", replace \"%s\" with \"%s\" "
-		    "to \"%s\"\n",
-		    word, loop->tvar, loop->str, s ? s : "(null)");
-	}
-	if (s != NULL && *s != '\0') {
-	    if (addSpace && *s != '\n')
-		Buf_AddByte(buf, ' ');
-	    Buf_AddBytes(buf, (slen = strlen(s)), s);
-	    addSpace = (slen > 0 && s[slen - 1] != '\n');
-	}
-	free(s);
-    }
-    return addSpace;
+/* Callback for ModifyWords to implement the :@var@...@ modifier of ODE make. */
+static void
+ModifyWord_Loop(const char *word, SepBuf *buf, void *data)
+{
+    const ModifyWord_LoopArgs *args;
+    char *s;
+
+    if (word[0] == '\0')
+	return;
+
+    args = data;
+    Var_Set_with_flags(args->tvar, word, args->ctx, VAR_NO_EXPORT);
+    s = Var_Subst(args->str, args->ctx, args->eflags);
+
+    VAR_DEBUG("ModifyWord_Loop: in \"%s\", replace \"%s\" with \"%s\" "
+	      "to \"%s\"\n",
+	      word, args->tvar, args->str, s);
+
+    if (s[0] == '\n' || (buf->buf.count > 0 &&
+			 buf->buf.buffer[buf->buf.count - 1] == '\n'))
+	buf->needSep = FALSE;
+    SepBuf_AddStr(buf, s);
+    free(s);
 }
 
 
 /*-
- *-----------------------------------------------------------------------
- * VarSelectWords --
- *	Implements the :[start..end] modifier.
- *	This is a special case of VarModify since we want to be able
- *	to scan the list backwards if start > end.
- *
- * Input:
- *	str		String whose words should be trimmed
- *	seldata		words to select
- *
- * Results:
- *	A string of all the words selected.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
+ * Implements the :[first..last] modifier.
+ * This is a special case of ModifyWords since we want to be able
+ * to scan the list backwards if first > last.
  */
 static char *
-VarSelectWords(GNode *ctx MAKE_ATTR_UNUSED, Var_Parse_State *vpstate,
-	       const char *str, VarSelectWords_t *seldata)
+VarSelectWords(char sep, Boolean oneBigWord, const char *str, int first,
+	       int last)
 {
-    Buffer buf;			/* Buffer for the new string */
-    Boolean addSpace;		/* TRUE if need to add a space to the
-				 * buffer before adding the trimmed
-				 * word */
-    char **av;			/* word list */
-    char *as;			/* word list memory */
-    int ac, i;
+    Words words;
     int start, end, step;
+    int i;
 
-    Buf_Init(&buf, 0);
-    addSpace = FALSE;
+    SepBuf buf;
+    SepBuf_Init(&buf, sep);
 
-    if (vpstate->oneBigWord) {
-	/* fake what brk_string() would do if there were only one word */
-	ac = 1;
-	av = bmake_malloc((ac + 1) * sizeof(char *));
-	as = bmake_strdup(str);
-	av[0] = as;
-	av[1] = NULL;
+    if (oneBigWord) {
+	/* fake what Str_Words() would do if there were only one word */
+	words.len = 1;
+	words.words = bmake_malloc((words.len + 1) * sizeof(char *));
+	words.freeIt = bmake_strdup(str);
+	words.words[0] = words.freeIt;
+	words.words[1] = NULL;
     } else {
-	av = brk_string(str, &ac, FALSE, &as);
+	words = Str_Words(str, FALSE);
     }
 
     /*
-     * Now sanitize seldata.
-     * If seldata->start or seldata->end are negative, convert them to
-     * the positive equivalents (-1 gets converted to argc, -2 gets
-     * converted to (argc-1), etc.).
+     * Now sanitize the given range.
+     * If first or last are negative, convert them to the positive equivalents
+     * (-1 gets converted to ac, -2 gets converted to (ac - 1), etc.).
      */
-    if (seldata->start < 0)
-	seldata->start = ac + seldata->start + 1;
-    if (seldata->end < 0)
-	seldata->end = ac + seldata->end + 1;
+    if (first < 0)
+	first += (int)words.len + 1;
+    if (last < 0)
+	last += (int)words.len + 1;
 
     /*
      * We avoid scanning more of the list than we need to.
      */
-    if (seldata->start > seldata->end) {
-	start = MIN(ac, seldata->start) - 1;
-	end = MAX(0, seldata->end - 1);
+    if (first > last) {
+	start = MIN((int)words.len, first) - 1;
+	end = MAX(0, last - 1);
 	step = -1;
     } else {
-	start = MAX(0, seldata->start - 1);
-	end = MIN(ac, seldata->end);
+	start = MAX(0, first - 1);
+	end = MIN((int)words.len, last);
 	step = 1;
     }
 
-    for (i = start;
-	 (step < 0 && i >= end) || (step > 0 && i < end);
-	 i += step) {
-	if (av[i] && *av[i]) {
-	    if (addSpace && vpstate->varSpace) {
-		Buf_AddByte(&buf, vpstate->varSpace);
-	    }
-	    Buf_AddBytes(&buf, strlen(av[i]), av[i]);
-	    addSpace = TRUE;
-	}
+    for (i = start; (step < 0) == (i >= end); i += step) {
+	SepBuf_AddStr(&buf, words.words[i]);
+	SepBuf_Sep(&buf);
     }
 
-    free(as);
-    free(av);
+    Words_Free(words);
 
-    return Buf_Destroy(&buf, FALSE);
+    return SepBuf_Destroy(&buf, FALSE);
 }
 
 
-/* Callback function for VarModify to implement the :tA modifier.
+/* Callback for ModifyWords to implement the :tA modifier.
  * Replace each word with the result of realpath() if successful. */
-static Boolean
-VarRealpath(GNode *ctx MAKE_ATTR_UNUSED, Var_Parse_State *vpstate,
-	    const char *word, Boolean addSpace, Buffer *buf,
-	    void *patternp MAKE_ATTR_UNUSED)
+static void
+ModifyWord_Realpath(const char *word, SepBuf *buf, void *data MAKE_ATTR_UNUSED)
 {
     struct stat st;
     char rbuf[MAXPATHLEN];
-    char *rp;
 
-    if (addSpace && vpstate->varSpace)
-	Buf_AddByte(buf, vpstate->varSpace);
-    rp = cached_realpath(word, rbuf);
-    if (rp && *rp == '/' && stat(rp, &st) == 0)
+    const char *rp = cached_realpath(word, rbuf);
+    if (rp != NULL && *rp == '/' && stat(rp, &st) == 0)
 	word = rp;
 
-    Buf_AddBytes(buf, strlen(word), word);
-    return TRUE;
+    SepBuf_AddStr(buf, word);
 }
 
 /*-
@@ -1704,273 +1535,116 @@ VarRealpath(GNode *ctx MAKE_ATTR_UNUSED, Var_Parse_State *vpstate,
  * Modify each of the words of the passed string using the given function.
  *
  * Input:
- *	str		String whose words should be trimmed
- *	modProc		Function to use to modify them
- *	data		Custom data for the modProc
+ *	str		String whose words should be modified
+ *	modifyWord	Function that modifies a single word
+ *	modifyWord_args Custom arguments for modifyWord
  *
  * Results:
  *	A string of all the words modified appropriately.
- *
- * Side Effects:
- *	None.
- *
  *-----------------------------------------------------------------------
  */
 static char *
-VarModify(GNode *ctx, Var_Parse_State *vpstate,
-    const char *str, VarModifyCallback modProc, void *datum)
+ModifyWords(GNode *ctx, char sep, Boolean oneBigWord, const char *str,
+	    ModifyWordsCallback modifyWord, void *modifyWord_args)
 {
-    Buffer buf;			/* Buffer for the new string */
-    Boolean addSpace; 		/* TRUE if need to add a space to the
-				 * buffer before adding the trimmed word */
-    char **av;			/* word list */
-    char *as;			/* word list memory */
-    int ac, i;
+    SepBuf result;
+    Words words;
+    size_t i;
+
+    if (oneBigWord) {
+	SepBuf_Init(&result, sep);
+	modifyWord(str, &result, modifyWord_args);
+	return SepBuf_Destroy(&result, FALSE);
+    }
+
+    SepBuf_Init(&result, sep);
+
+    words = Str_Words(str, FALSE);
+
+    VAR_DEBUG("ModifyWords: split \"%s\" into %zu words\n", str, words.len);
+
+    for (i = 0; i < words.len; i++) {
+	modifyWord(words.words[i], &result, modifyWord_args);
+	if (result.buf.count > 0)
+	    SepBuf_Sep(&result);
+    }
+
+    Words_Free(words);
+
+    return SepBuf_Destroy(&result, FALSE);
+}
+
+
+static char *
+Words_JoinFree(Words words)
+{
+    Buffer buf;
+    size_t i;
 
     Buf_Init(&buf, 0);
-    addSpace = FALSE;
 
-    if (vpstate->oneBigWord) {
-	/* fake what brk_string() would do if there were only one word */
-	ac = 1;
-	av = bmake_malloc((ac + 1) * sizeof(char *));
-	as = bmake_strdup(str);
-	av[0] = as;
-	av[1] = NULL;
-    } else {
-	av = brk_string(str, &ac, FALSE, &as);
+    for (i = 0; i < words.len; i++) {
+	if (i != 0)
+	    Buf_AddByte(&buf, ' ');	/* XXX: st->sep, for consistency */
+	Buf_AddStr(&buf, words.words[i]);
     }
 
-    if (DEBUG(VAR)) {
-	fprintf(debug_file, "VarModify: split \"%s\" into %d words\n",
-		str, ac);
-    }
-
-    for (i = 0; i < ac; i++)
-	addSpace = modProc(ctx, vpstate, av[i], addSpace, &buf, datum);
-
-    free(as);
-    free(av);
+    Words_Free(words);
 
     return Buf_Destroy(&buf, FALSE);
 }
 
-
-static int
-VarWordCompare(const void *a, const void *b)
-{
-    int r = strcmp(*(const char * const *)a, *(const char * const *)b);
-    return r;
-}
-
-static int
-VarWordCompareReverse(const void *a, const void *b)
-{
-    int r = strcmp(*(const char * const *)b, *(const char * const *)a);
-    return r;
-}
-
-/*-
- *-----------------------------------------------------------------------
- * VarOrder --
- *	Order the words in the string.
- *
- * Input:
- *	str		String whose words should be sorted.
- *	otype		How to order: s - sort, x - random.
- *
- * Results:
- *	A string containing the words ordered.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-static char *
-VarOrder(const char *str, const char otype)
-{
-    Buffer buf;			/* Buffer for the new string */
-    char **av;			/* word list [first word does not count] */
-    char *as;			/* word list memory */
-    int ac, i;
-
-    Buf_Init(&buf, 0);
-
-    av = brk_string(str, &ac, FALSE, &as);
-
-    if (ac > 0) {
-	switch (otype) {
-	case 'r':		/* reverse sort alphabetically */
-	    qsort(av, ac, sizeof(char *), VarWordCompareReverse);
-	    break;
-	case 's':		/* sort alphabetically */
-	    qsort(av, ac, sizeof(char *), VarWordCompare);
-	    break;
-	case 'x':		/* randomize */
-	    {
-		/*
-		 * We will use [ac..2] range for mod factors. This will produce
-		 * random numbers in [(ac-1)..0] interval, and minimal
-		 * reasonable value for mod factor is 2 (the mod 1 will produce
-		 * 0 with probability 1).
-		 */
-		for (i = ac - 1; i > 0; i--) {
-		    int rndidx = random() % (i + 1);
-		    char *t = av[i];
-		    av[i] = av[rndidx];
-		    av[rndidx] = t;
-		}
-	    }
-	}
-    }
-
-    for (i = 0; i < ac; i++) {
-	Buf_AddBytes(&buf, strlen(av[i]), av[i]);
-	if (i != ac - 1)
-	    Buf_AddByte(&buf, ' ');
-    }
-
-    free(as);
-    free(av);
-
-    return Buf_Destroy(&buf, FALSE);
-}
-
-
-/*-
- *-----------------------------------------------------------------------
- * VarUniq --
- *	Remove adjacent duplicate words.
- *
- * Input:
- *	str		String whose words should be sorted
- *
- * Results:
- *	A string containing the resulting words.
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
+/* Remove adjacent duplicate words. */
 static char *
 VarUniq(const char *str)
 {
-    Buffer	  buf;		/* Buffer for new string */
-    char 	**av;		/* List of words to affect */
-    char 	 *as;		/* Word list memory */
-    int 	  ac, i, j;
+    Words words = Str_Words(str, FALSE);
 
-    Buf_Init(&buf, 0);
-    av = brk_string(str, &ac, FALSE, &as);
-
-    if (ac > 1) {
-	for (j = 0, i = 1; i < ac; i++)
-	    if (strcmp(av[i], av[j]) != 0 && (++j != i))
-		av[j] = av[i];
-	ac = j + 1;
+    if (words.len > 1) {
+	size_t i, j;
+	for (j = 0, i = 1; i < words.len; i++)
+	    if (strcmp(words.words[i], words.words[j]) != 0 && (++j != i))
+		words.words[j] = words.words[i];
+	words.len = j + 1;
     }
 
-    for (i = 0; i < ac; i++) {
-	Buf_AddBytes(&buf, strlen(av[i]), av[i]);
-	if (i != ac - 1)
-	    Buf_AddByte(&buf, ' ');
-    }
-
-    free(as);
-    free(av);
-
-    return Buf_Destroy(&buf, FALSE);
-}
-
-/*-
- *-----------------------------------------------------------------------
- * VarRange --
- *	Return an integer sequence
- *
- * Input:
- *	str		String whose words provide default range
- *	ac		range length, if 0 use str words
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
-static char *
-VarRange(const char *str, int ac)
-{
-    Buffer	  buf;		/* Buffer for new string */
-    char	  tmp[32];	/* each element */
-    char 	**av;		/* List of words to affect */
-    char 	 *as;		/* Word list memory */
-    int 	  i, n;
-
-    Buf_Init(&buf, 0);
-    if (ac > 0) {
-	as = NULL;
-	av = NULL;
-    } else {
-	av = brk_string(str, &ac, FALSE, &as);
-    }
-    for (i = 0; i < ac; i++) {
-	n = snprintf(tmp, sizeof(tmp), "%d", 1 + i);
-	if (n >= (int)sizeof(tmp))
-	    break;
-	Buf_AddBytes(&buf, n, tmp);
-	if (i != ac - 1)
-	    Buf_AddByte(&buf, ' ');
-    }
-
-    free(as);
-    free(av);
-
-    return Buf_Destroy(&buf, FALSE);
+    return Words_JoinFree(words);
 }
 
 
 /*-
- *-----------------------------------------------------------------------
- * VarGetPattern --
- *	During the parsing of a part of a modifier such as :S or :@,
- *	pass through the tstr looking for 1) escaped delimiters,
- *	'$'s and backslashes (place the escaped character in
- *	uninterpreted) and 2) unescaped $'s that aren't before
- *	the delimiter (expand the variable substitution unless flags
- *	has VAR_NOSUBST set).
- *	Return the expanded string or NULL if the delimiter was missing
- *	If pattern is specified, handle escaped ampersands, and replace
- *	unescaped ampersands with the lhs of the pattern.
+ * Parse a part of a modifier such as the "from" and "to" in :S/from/to/
+ * or the "var" or "replacement" in :@var@replacement+${var}@, up to and
+ * including the next unescaped delimiter.  The delimiter, as well as the
+ * backslash or the dollar, can be escaped with a backslash.
  *
- * Results:
- *	A string of all the words modified appropriately.
- *	If length is specified, return the string length of the buffer
- *	If flags is specified and the last character of the pattern is a
- *	$ set the VAR_MATCH_END bit of flags.
- *
- * Side Effects:
- *	None.
- *-----------------------------------------------------------------------
+ * Return the parsed (and possibly expanded) string, or NULL if no delimiter
+ * was found.  On successful return, the parsing position pp points right
+ * after the delimiter.  The delimiter is not included in the returned
+ * value though.
  */
 static char *
-VarGetPattern(GNode *ctxt, Var_Parse_State *vpstate MAKE_ATTR_UNUSED,
-	      VarPattern_Flags flags, const char **tstr, int delim,
-	      VarPattern_Flags *vflags, int *length, VarPattern *pattern)
-{
-    const char *cp;
-    char *rstr;
+ParseModifierPart(
+    const char **pp,		/* The parsing position, updated upon return */
+    int delim,			/* Parsing stops at this delimiter */
+    VarEvalFlags eflags,	/* Flags for evaluating nested variables;
+				 * if VARE_WANTRES is not set, the text is
+				 * only parsed */
+    GNode *ctxt,		/* For looking up nested variables */
+    size_t *out_length,		/* Optionally stores the length of the returned
+				 * string, just to save another strlen call. */
+    VarPatternFlags *out_pflags,/* For the first part of the :S modifier,
+				 * sets the VARP_ANCHOR_END flag if the last
+				 * character of the pattern is a $. */
+    ModifyWord_SubstArgs *subst	/* For the second part of the :S modifier,
+				 * allow ampersands to be escaped and replace
+				 * unescaped ampersands with subst->lhs. */
+) {
     Buffer buf;
-    int junk;
-    int errnum = flags & VARF_UNDEFERR;
+    const char *p;
+    char *rstr;
 
     Buf_Init(&buf, 0);
-    if (length == NULL)
-	length = &junk;
-
-#define IS_A_MATCH(cp, delim) \
-    ((cp[0] == '\\') && ((cp[1] == delim) ||  \
-     (cp[1] == '\\') || (cp[1] == '$') || (pattern && (cp[1] == '&'))))
 
     /*
      * Skim through until the matching delimiter is found;
@@ -1978,162 +1652,147 @@ VarGetPattern(GNode *ctxt, Var_Parse_State *vpstate MAKE_ATTR_UNUSED,
      * backslashes to quote the delimiter, $, and \, but don't
      * touch other backslashes.
      */
-    for (cp = *tstr; *cp && (*cp != delim); cp++) {
-	if (IS_A_MATCH(cp, delim)) {
-	    Buf_AddByte(&buf, cp[1]);
-	    cp++;
-	} else if (*cp == '$') {
-	    if (cp[1] == delim) {
-		if (vflags == NULL)
-		    Buf_AddByte(&buf, *cp);
-		else
-		    /*
-		     * Unescaped $ at end of pattern => anchor
-		     * pattern at end.
-		     */
-		    *vflags |= VAR_MATCH_END;
-	    } else {
-		if (vflags == NULL || (*vflags & VAR_NOSUBST) == 0) {
-		    char   *cp2;
-		    int     len;
-		    void   *freeIt;
+    p = *pp;
+    while (*p != '\0' && *p != delim) {
+	const char *varstart;
 
-		    /*
-		     * If unescaped dollar sign not before the
-		     * delimiter, assume it's a variable
-		     * substitution and recurse.
-		     */
-		    cp2 = Var_Parse(cp, ctxt, errnum | (flags & VARF_WANTRES),
-				    &len, &freeIt);
-		    Buf_AddBytes(&buf, strlen(cp2), cp2);
-		    free(freeIt);
-		    cp += len - 1;
-		} else {
-		    const char *cp2 = &cp[1];
+	Boolean is_escaped = p[0] == '\\' && (
+	    p[1] == delim || p[1] == '\\' || p[1] == '$' ||
+	    (p[1] == '&' && subst != NULL));
+	if (is_escaped) {
+	    Buf_AddByte(&buf, p[1]);
+	    p += 2;
+	    continue;
+	}
 
-		    if (*cp2 == PROPEN || *cp2 == BROPEN) {
-			/*
-			 * Find the end of this variable reference
-			 * and suck it in without further ado.
-			 * It will be interpreted later.
-			 */
-			int have = *cp2;
-			int want = (*cp2 == PROPEN) ? PRCLOSE : BRCLOSE;
-			int depth = 1;
+	if (*p != '$') {	/* Unescaped, simple text */
+	    if (subst != NULL && *p == '&')
+		Buf_AddBytes(&buf, subst->lhs, subst->lhsLen);
+	    else
+		Buf_AddByte(&buf, *p);
+	    p++;
+	    continue;
+	}
 
-			for (++cp2; *cp2 != '\0' && depth > 0; ++cp2) {
-			    if (cp2[-1] != '\\') {
-				if (*cp2 == have)
-				    ++depth;
-				if (*cp2 == want)
-				    --depth;
-			    }
-			}
-			Buf_AddBytes(&buf, cp2 - cp, cp);
-			cp = --cp2;
-		    } else
-			Buf_AddByte(&buf, *cp);
+	if (p[1] == delim) {	/* Unescaped $ at end of pattern */
+	    if (out_pflags != NULL)
+		*out_pflags |= VARP_ANCHOR_END;
+	    else
+		Buf_AddByte(&buf, *p);
+	    p++;
+	    continue;
+	}
+
+	if (eflags & VARE_WANTRES) {	/* Nested variable, evaluated */
+	    const char *cp2;
+	    int len;
+	    void *freeIt;
+	    VarEvalFlags nested_eflags = eflags & ~(unsigned)VARE_ASSIGN;
+
+	    cp2 = Var_Parse(p, ctxt, nested_eflags, &len, &freeIt);
+	    Buf_AddStr(&buf, cp2);
+	    free(freeIt);
+	    p += len;
+	    continue;
+	}
+
+	/* XXX: This whole block is very similar to Var_Parse without
+	 * VARE_WANTRES.  There may be subtle edge cases though that are
+	 * not yet covered in the unit tests and that are parsed differently,
+	 * depending on whether they are evaluated or not.
+	 *
+	 * This subtle difference is not documented in the manual page,
+	 * neither is the difference between parsing :D and :M documented.
+	 * No code should ever depend on these details, but who knows. */
+
+	varstart = p;		/* Nested variable, only parsed */
+	if (p[1] == PROPEN || p[1] == BROPEN) {
+	    /*
+	     * Find the end of this variable reference
+	     * and suck it in without further ado.
+	     * It will be interpreted later.
+	     */
+	    int have = p[1];
+	    int want = have == PROPEN ? PRCLOSE : BRCLOSE;
+	    int depth = 1;
+
+	    for (p += 2; *p != '\0' && depth > 0; p++) {
+		if (p[-1] != '\\') {
+		    if (*p == have)
+			depth++;
+		    if (*p == want)
+			depth--;
 		}
 	    }
-	} else if (pattern && *cp == '&')
-	    Buf_AddBytes(&buf, pattern->leftLen, pattern->lhs);
-	else
-	    Buf_AddByte(&buf, *cp);
+	    Buf_AddBytesBetween(&buf, varstart, p);
+	} else {
+	    Buf_AddByte(&buf, *varstart);
+	    p++;
+	}
     }
 
-    if (*cp != delim) {
-	*tstr = cp;
-	*length = 0;
+    if (*p != delim) {
+	*pp = p;
 	return NULL;
     }
 
-    *tstr = ++cp;
-    *length = Buf_Size(&buf);
+    *pp = ++p;
+    if (out_length != NULL)
+	*out_length = Buf_Size(&buf);
+
     rstr = Buf_Destroy(&buf, FALSE);
-    if (DEBUG(VAR))
-	fprintf(debug_file, "Modifier pattern: \"%s\"\n", rstr);
+    VAR_DEBUG("Modifier part: \"%s\"\n", rstr);
     return rstr;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * VarQuote --
- *	Quote shell meta-characters and space characters in the string
- *	if quoteDollar is set, also quote and double any '$' characters.
- *
- * Results:
- *	The quoted string
- *
- * Side Effects:
- *	None.
- *
- *-----------------------------------------------------------------------
- */
+/* Quote shell meta-characters and space characters in the string.
+ * If quoteDollar is set, also quote and double any '$' characters. */
 static char *
-VarQuote(char *str, Boolean quoteDollar)
+VarQuote(const char *str, Boolean quoteDollar)
 {
-
-    Buffer  	  buf;
-    const char	*newline;
-    size_t nlen;
-
-    if ((newline = Shell_GetNewline()) == NULL)
-	newline = "\\\n";
-    nlen = strlen(newline);
-
+    char *res;
+    Buffer buf;
     Buf_Init(&buf, 0);
 
     for (; *str != '\0'; str++) {
 	if (*str == '\n') {
-	    Buf_AddBytes(&buf, nlen, newline);
+	    const char *newline = Shell_GetNewline();
+	    if (newline == NULL)
+		newline = "\\\n";
+	    Buf_AddStr(&buf, newline);
 	    continue;
 	}
 	if (isspace((unsigned char)*str) || ismeta((unsigned char)*str))
 	    Buf_AddByte(&buf, '\\');
 	Buf_AddByte(&buf, *str);
 	if (quoteDollar && *str == '$')
-	    Buf_AddBytes(&buf, 2, "\\$");
+	    Buf_AddStr(&buf, "\\$");
     }
 
-    str = Buf_Destroy(&buf, FALSE);
-    if (DEBUG(VAR))
-	fprintf(debug_file, "QuoteMeta: [%s]\n", str);
-    return str;
+    res = Buf_Destroy(&buf, FALSE);
+    VAR_DEBUG("QuoteMeta: [%s]\n", res);
+    return res;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * VarHash --
- *      Hash the string using the MurmurHash3 algorithm.
- *      Output is computed using 32bit Little Endian arithmetic.
- *
- * Input:
- *	str		String to modify
- *
- * Results:
- *      Hash value of str, encoded as 8 hex digits.
- *
- * Side Effects:
- *      None.
- *
- *-----------------------------------------------------------------------
- */
+/* Compute the 32-bit hash of the given string, using the MurmurHash3
+ * algorithm. Output is encoded as 8 hex digits, in Little Endian order. */
 static char *
 VarHash(const char *str)
 {
     static const char    hexdigits[16] = "0123456789abcdef";
-    Buffer         buf;
-    size_t         len, len2;
     const unsigned char *ustr = (const unsigned char *)str;
-    uint32_t       h, k, c1, c2;
 
-    h  = 0x971e137bU;
-    c1 = 0x95543787U;
-    c2 = 0x2ad7eb25U;
-    len2 = strlen(str);
+    uint32_t h  = 0x971e137bU;
+    uint32_t c1 = 0x95543787U;
+    uint32_t c2 = 0x2ad7eb25U;
+    size_t len2 = strlen(str);
 
+    char *buf;
+    size_t i;
+
+    size_t len;
     for (len = len2; len; ) {
-	k = 0;
+	uint32_t k = 0;
 	switch (len) {
 	default:
 	    k = ((uint32_t)ustr[3] << 24) |
@@ -2162,1112 +1821,960 @@ VarHash(const char *str)
 	h = h * 5 + 0x52dce729U;
 	h ^= k;
     }
-    h ^= len2;
+    h ^= (uint32_t)len2;
     h *= 0x85ebca6b;
     h ^= h >> 13;
     h *= 0xc2b2ae35;
     h ^= h >> 16;
 
-    Buf_Init(&buf, 0);
-    for (len = 0; len < 8; ++len) {
-	Buf_AddByte(&buf, hexdigits[h & 15]);
+    buf = bmake_malloc(9);
+    for (i = 0; i < 8; i++) {
+	buf[i] = hexdigits[h & 0x0f];
 	h >>= 4;
     }
-
-    return Buf_Destroy(&buf, FALSE);
+    buf[8] = '\0';
+    return buf;
 }
 
 static char *
-VarStrftime(const char *fmt, int zulu, time_t utc)
+VarStrftime(const char *fmt, Boolean zulu, time_t tim)
 {
     char buf[BUFSIZ];
 
-    if (!utc)
-	time(&utc);
+    if (!tim)
+	time(&tim);
     if (!*fmt)
 	fmt = "%c";
-    strftime(buf, sizeof(buf), fmt, zulu ? gmtime(&utc) : localtime(&utc));
+    strftime(buf, sizeof(buf), fmt, zulu ? gmtime(&tim) : localtime(&tim));
 
     buf[sizeof(buf) - 1] = '\0';
     return bmake_strdup(buf);
 }
 
+/* The ApplyModifier functions all work in the same way.  They get the
+ * current parsing position (pp) and parse the modifier from there.  The
+ * modifier typically lasts until the next ':', or a closing '}' or ')'
+ * (taken from st->endc), or the end of the string (parse error).
+ *
+ * The high-level behavior of these functions is:
+ *
+ * 1. parse the modifier
+ * 2. evaluate the modifier
+ * 3. housekeeping
+ *
+ * Parsing the modifier
+ *
+ * If parsing succeeds, the parsing position *pp is updated to point to the
+ * first character following the modifier, which typically is either ':' or
+ * st->endc.
+ *
+ * If parsing fails because of a missing delimiter (as in the :S, :C or :@
+ * modifiers), set st->missing_delim and return AMR_CLEANUP.
+ *
+ * If parsing fails because the modifier is unknown, return AMR_UNKNOWN to
+ * try the SysV modifier ${VAR:from=to} as fallback.  This should only be
+ * done as long as there have been no side effects from evaluating nested
+ * variables, to avoid evaluating them more than once.  In this case, the
+ * parsing position must not be updated.  (XXX: Why not? The original parsing
+ * position is well-known in ApplyModifiers.)
+ *
+ * If parsing fails and the SysV modifier ${VAR:from=to} should not be used
+ * as a fallback, either issue an error message using Error or Parse_Error
+ * and then return AMR_CLEANUP, or return AMR_BAD for the default error
+ * message.  Both of these return values will stop processing the variable
+ * expression.  (XXX: As of 2020-08-23, evaluation of the whole string
+ * continues nevertheless after skipping a few bytes, which essentially is
+ * undefined behavior.  Not in the sense of C, but still it's impossible to
+ * predict what happens in the parser.)
+ *
+ * Evaluating the modifier
+ *
+ * After parsing, the modifier is evaluated.  The side effects from evaluating
+ * nested variable expressions in the modifier text often already happen
+ * during parsing though.
+ *
+ * Evaluating the modifier usually takes the current value of the variable
+ * expression from st->val, or the variable name from st->v->name and stores
+ * the result in st->newVal.
+ *
+ * If evaluating fails (as of 2020-08-23), an error message is printed using
+ * Error.  This function has no side-effects, it really just prints the error
+ * message.  Processing the expression continues as if everything were ok.
+ * XXX: This should be fixed by adding proper error handling to Var_Subst,
+ * Var_Parse, ApplyModifiers and ModifyWords.
+ *
+ * Housekeeping
+ *
+ * Some modifiers such as :D and :U turn undefined variables into useful
+ * variables (VAR_JUNK, VAR_KEEP).
+ *
+ * Some modifiers need to free some memory.
+ */
+
 typedef struct {
-    /* const parameters */
-    int startc;
-    int endc;
-    Var *v;
-    GNode *ctxt;
-    int flags;
-    int *lengthPtr;
-    void **freePtr;
+    const char startc;		/* '\0' or '{' or '(' */
+    const char endc;		/* '\0' or '}' or ')' */
+    Var * const v;
+    GNode * const ctxt;
+    const VarEvalFlags eflags;
 
-    /* read-write */
-    char *nstr;
-    const char *tstr;
-    const char *start;
-    const char *cp;		/* Secondary pointer into str (place marker
-				 * for tstr) */
-    char termc;			/* Character which terminated scan */
-    int cnt;			/* Used to count brace pairs when variable in
-				 * in parens or braces */
-    char delim;
-    int modifier;		/* that we are processing */
-    Var_Parse_State parsestate;	/* Flags passed to helper functions */
+    char *val;			/* The old value of the expression,
+				 * before applying the modifier, never NULL */
+    char *newVal;		/* The new value of the expression,
+				 * after applying the modifier, never NULL */
+    char missing_delim;		/* For error reporting */
 
-    /* result */
-    char *newStr;		/* New value to return */
+    char sep;			/* Word separator in expansions
+				 * (see the :ts modifier) */
+    Boolean oneBigWord;		/* TRUE if some modifiers that otherwise split
+				 * the variable value into words, like :S and
+				 * :C, treat the variable value as a single big
+				 * word, possibly containing spaces. */
 } ApplyModifiersState;
 
-/* we now have some modifiers with long names */
-#define STRMOD_MATCH(s, want, n) \
-    (strncmp(s, want, n) == 0 && (s[n] == st->endc || s[n] == ':'))
-#define STRMOD_MATCHX(s, want, n) \
-    (strncmp(s, want, n) == 0 && \
-     (s[n] == st->endc || s[n] == ':' || s[n] == '='))
-#define CHARMOD_MATCH(c) (c == st->endc || c == ':')
+typedef enum {
+    AMR_OK,			/* Continue parsing */
+    AMR_UNKNOWN,		/* Not a match, try other modifiers as well */
+    AMR_BAD,			/* Error out with "Bad modifier" message */
+    AMR_CLEANUP			/* Error out, with "Unfinished modifier"
+				 * if st->missing_delim is set. */
+} ApplyModifierResult;
+
+/* Test whether mod starts with modname, followed by a delimiter. */
+static Boolean
+ModMatch(const char *mod, const char *modname, char endc)
+{
+    size_t n = strlen(modname);
+    return strncmp(mod, modname, n) == 0 &&
+	   (mod[n] == endc || mod[n] == ':');
+}
+
+/* Test whether mod starts with modname, followed by a delimiter or '='. */
+static inline Boolean
+ModMatchEq(const char *mod, const char *modname, char endc)
+{
+    size_t n = strlen(modname);
+    return strncmp(mod, modname, n) == 0 &&
+	   (mod[n] == endc || mod[n] == ':' || mod[n] == '=');
+}
 
 /* :@var@...${var}...@ */
-static Boolean
-ApplyModifier_At(ApplyModifiersState *st) {
-    VarLoop loop;
-    VarPattern_Flags vflags = VAR_NOSUBST;
+static ApplyModifierResult
+ApplyModifier_Loop(const char **pp, ApplyModifiersState *st)
+{
+    ModifyWord_LoopArgs args;
+    char delim;
+    char prev_sep;
+    VarEvalFlags eflags = st->eflags & ~(unsigned)VARE_WANTRES;
 
-    st->cp = ++(st->tstr);
-    st->delim = '@';
-    loop.tvar = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	&vflags, &loop.tvarLen, NULL);
-    if (loop.tvar == NULL)
-	return FALSE;
+    args.ctx = st->ctxt;
 
-    loop.str = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	&vflags, &loop.strLen, NULL);
-    if (loop.str == NULL)
-	return FALSE;
+    (*pp)++;			/* Skip the first '@' */
+    delim = '@';
+    args.tvar = ParseModifierPart(pp, delim, eflags,
+				  st->ctxt, NULL, NULL, NULL);
+    if (args.tvar == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
+    if (DEBUG(LINT) && strchr(args.tvar, '$') != NULL) {
+	Parse_Error(PARSE_FATAL,
+		    "In the :@ modifier of \"%s\", the variable name \"%s\" "
+		    "must not contain a dollar.",
+		    st->v->name, args.tvar);
+	return AMR_CLEANUP;
+    }
 
-    st->termc = *st->cp;
-    st->delim = '\0';
+    args.str = ParseModifierPart(pp, delim, eflags,
+				 st->ctxt, NULL, NULL, NULL);
+    if (args.str == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
 
-    loop.flags = st->flags & (VARF_UNDEFERR | VARF_WANTRES);
-    loop.ctxt = st->ctxt;
-    st->newStr = VarModify(
-	st->ctxt, &st->parsestate, st->nstr, VarLoopExpand, &loop);
-    Var_Delete(loop.tvar, st->ctxt);
-    free(loop.tvar);
-    free(loop.str);
-    return TRUE;
+    args.eflags = st->eflags & (VARE_UNDEFERR | VARE_WANTRES);
+    prev_sep = st->sep;
+    st->sep = ' ';		/* XXX: should be st->sep for consistency */
+    st->newVal = ModifyWords(st->ctxt, st->sep, st->oneBigWord, st->val,
+			     ModifyWord_Loop, &args);
+    st->sep = prev_sep;
+    Var_Delete(args.tvar, st->ctxt);
+    free(args.tvar);
+    free(args.str);
+    return AMR_OK;
 }
 
 /* :Ddefined or :Uundefined */
-static void
-ApplyModifier_Defined(ApplyModifiersState *st)
+static ApplyModifierResult
+ApplyModifier_Defined(const char **pp, ApplyModifiersState *st)
 {
-    Buffer buf;			/* Buffer for patterns */
-    int nflags;
+    Buffer buf;
+    const char *p;
 
-    if (st->flags & VARF_WANTRES) {
-	int wantres;
-	if (*st->tstr == 'U')
-	    wantres = ((st->v->flags & VAR_JUNK) != 0);
-	else
-	    wantres = ((st->v->flags & VAR_JUNK) == 0);
-	nflags = st->flags & ~VARF_WANTRES;
-	if (wantres)
-	    nflags |= VARF_WANTRES;
-    } else
-	nflags = st->flags;
-
-    /*
-     * Pass through tstr looking for 1) escaped delimiters,
-     * '$'s and backslashes (place the escaped character in
-     * uninterpreted) and 2) unescaped $'s that aren't before
-     * the delimiter (expand the variable substitution).
-     * The result is left in the Buffer buf.
-     */
-    Buf_Init(&buf, 0);
-    for (st->cp = st->tstr + 1;
-	 *st->cp != st->endc && *st->cp != ':' && *st->cp != '\0';
-	 st->cp++) {
-	if (*st->cp == '\\' &&
-	    (st->cp[1] == ':' || st->cp[1] == '$' || st->cp[1] == st->endc ||
-	     st->cp[1] == '\\')) {
-	    Buf_AddByte(&buf, st->cp[1]);
-	    st->cp++;
-	} else if (*st->cp == '$') {
-	    /*
-	     * If unescaped dollar sign, assume it's a
-	     * variable substitution and recurse.
-	     */
-	    char    *cp2;
-	    int	    len;
-	    void    *freeIt;
-
-	    cp2 = Var_Parse(st->cp, st->ctxt, nflags, &len, &freeIt);
-	    Buf_AddBytes(&buf, strlen(cp2), cp2);
-	    free(freeIt);
-	    st->cp += len - 1;
-	} else {
-	    Buf_AddByte(&buf, *st->cp);
-	}
+    VarEvalFlags eflags = st->eflags & ~(unsigned)VARE_WANTRES;
+    if (st->eflags & VARE_WANTRES) {
+	if ((**pp == 'D') == !(st->v->flags & VAR_JUNK))
+	    eflags |= VARE_WANTRES;
     }
 
-    st->termc = *st->cp;
+    Buf_Init(&buf, 0);
+    p = *pp + 1;
+    while (*p != st->endc && *p != ':' && *p != '\0') {
 
-    if ((st->v->flags & VAR_JUNK) != 0)
+        /* Escaped delimiter or other special character */
+	if (*p == '\\') {
+	    char c = p[1];
+	    if (c == st->endc || c == ':' || c == '$' || c == '\\') {
+		Buf_AddByte(&buf, c);
+		p += 2;
+		continue;
+	    }
+	}
+
+	/* Nested variable expression */
+	if (*p == '$') {
+	    const char *cp2;
+	    int len;
+	    void *freeIt;
+
+	    cp2 = Var_Parse(p, st->ctxt, eflags, &len, &freeIt);
+	    Buf_AddStr(&buf, cp2);
+	    free(freeIt);
+	    p += len;
+	    continue;
+	}
+
+	/* Ordinary text */
+	Buf_AddByte(&buf, *p);
+	p++;
+    }
+    *pp = p;
+
+    if (st->v->flags & VAR_JUNK)
 	st->v->flags |= VAR_KEEP;
-    if (nflags & VARF_WANTRES) {
-	st->newStr = Buf_Destroy(&buf, FALSE);
+    if (eflags & VARE_WANTRES) {
+	st->newVal = Buf_Destroy(&buf, FALSE);
     } else {
-	st->newStr = st->nstr;
+	st->newVal = st->val;
 	Buf_Destroy(&buf, TRUE);
     }
+    return AMR_OK;
 }
 
 /* :gmtime */
-static Boolean
-ApplyModifier_Gmtime(ApplyModifiersState *st)
+static ApplyModifierResult
+ApplyModifier_Gmtime(const char **pp, ApplyModifiersState *st)
 {
     time_t utc;
-    char *ep;
 
-    st->cp = st->tstr + 1;	/* make sure it is set */
-    if (!STRMOD_MATCHX(st->tstr, "gmtime", 6))
-	return FALSE;
-    if (st->tstr[6] == '=') {
-	utc = strtoul(&st->tstr[7], &ep, 10);
-	st->cp = ep;
+    const char *mod = *pp;
+    if (!ModMatchEq(mod, "gmtime", st->endc))
+	return AMR_UNKNOWN;
+
+    if (mod[6] == '=') {
+	char *ep;
+	utc = (time_t)strtoul(mod + 7, &ep, 10);
+	*pp = ep;
     } else {
 	utc = 0;
-	st->cp = st->tstr + 6;
+	*pp = mod + 6;
     }
-    st->newStr = VarStrftime(st->nstr, 1, utc);
-    st->termc = *st->cp;
-    return TRUE;
+    st->newVal = VarStrftime(st->val, TRUE, utc);
+    return AMR_OK;
 }
 
 /* :localtime */
 static Boolean
-ApplyModifier_Localtime(ApplyModifiersState *st)
+ApplyModifier_Localtime(const char **pp, ApplyModifiersState *st)
 {
     time_t utc;
-    char *ep;
 
-    st->cp = st->tstr + 1;	/* make sure it is set */
-    if (!STRMOD_MATCHX(st->tstr, "localtime", 9))
-	return FALSE;
+    const char *mod = *pp;
+    if (!ModMatchEq(mod, "localtime", st->endc))
+	return AMR_UNKNOWN;
 
-    if (st->tstr[9] == '=') {
-	utc = strtoul(&st->tstr[10], &ep, 10);
-	st->cp = ep;
+    if (mod[9] == '=') {
+	char *ep;
+	utc = (time_t)strtoul(mod + 10, &ep, 10);
+	*pp = ep;
     } else {
 	utc = 0;
-	st->cp = st->tstr + 9;
+	*pp = mod + 9;
     }
-    st->newStr = VarStrftime(st->nstr, 0, utc);
-    st->termc = *st->cp;
-    return TRUE;
+    st->newVal = VarStrftime(st->val, FALSE, utc);
+    return AMR_OK;
 }
 
 /* :hash */
-static Boolean
-ApplyModifier_Hash(ApplyModifiersState *st)
+static ApplyModifierResult
+ApplyModifier_Hash(const char **pp, ApplyModifiersState *st)
 {
-    st->cp = st->tstr + 1;	/* make sure it is set */
-    if (!STRMOD_MATCH(st->tstr, "hash", 4))
-	return FALSE;
-    st->newStr = VarHash(st->nstr);
-    st->cp = st->tstr + 4;
-    st->termc = *st->cp;
-    return TRUE;
+    if (!ModMatch(*pp, "hash", st->endc))
+	return AMR_UNKNOWN;
+
+    st->newVal = VarHash(st->val);
+    *pp += 4;
+    return AMR_OK;
 }
 
 /* :P */
-static void
-ApplyModifier_Path(ApplyModifiersState *st)
+static ApplyModifierResult
+ApplyModifier_Path(const char **pp, ApplyModifiersState *st)
 {
     GNode *gn;
+    char *path;
 
-    if ((st->v->flags & VAR_JUNK) != 0)
+    if (st->v->flags & VAR_JUNK)
 	st->v->flags |= VAR_KEEP;
+
     gn = Targ_FindNode(st->v->name, TARG_NOCREATE);
     if (gn == NULL || gn->type & OP_NOPATH) {
-	st->newStr = NULL;
+	path = NULL;
     } else if (gn->path) {
-	st->newStr = bmake_strdup(gn->path);
+	path = bmake_strdup(gn->path);
     } else {
-	st->newStr = Dir_FindFile(st->v->name, Suff_FindPath(gn));
+	Lst searchPath = Suff_FindPath(gn);
+	path = Dir_FindFile(st->v->name, searchPath);
     }
-    if (!st->newStr)
-	st->newStr = bmake_strdup(st->v->name);
-    st->cp = ++st->tstr;
-    st->termc = *st->tstr;
+    if (path == NULL)
+	path = bmake_strdup(st->v->name);
+    st->newVal = path;
+
+    (*pp)++;
+    return AMR_OK;
 }
 
 /* :!cmd! */
-static Boolean
-ApplyModifier_Exclam(ApplyModifiersState *st)
+static ApplyModifierResult
+ApplyModifier_Exclam(const char **pp, ApplyModifiersState *st)
 {
-    const char *emsg;
-    VarPattern pattern;
+    char delim;
+    char *cmd;
+    const char *errfmt;
 
-    pattern.flags = 0;
+    (*pp)++;
+    delim = '!';
+    cmd = ParseModifierPart(pp, delim, st->eflags, st->ctxt,
+			    NULL, NULL, NULL);
+    if (cmd == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
 
-    st->delim = '!';
-    emsg = NULL;
-    st->cp = ++st->tstr;
-    pattern.rhs = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	NULL, &pattern.rightLen, NULL);
-    if (pattern.rhs == NULL)
-	return FALSE;
-    if (st->flags & VARF_WANTRES)
-	st->newStr = Cmd_Exec(pattern.rhs, &emsg);
+    errfmt = NULL;
+    if (st->eflags & VARE_WANTRES)
+	st->newVal = Cmd_Exec(cmd, &errfmt);
     else
-	st->newStr = varNoError;
-    free(UNCONST(pattern.rhs));
-    if (emsg)
-	Error(emsg, st->nstr);
-    st->termc = *st->cp;
-    st->delim = '\0';
+	st->newVal = varNoError;
+    free(cmd);
+
+    if (errfmt != NULL)
+	Error(errfmt, st->val);	/* XXX: why still return AMR_OK? */
+
     if (st->v->flags & VAR_JUNK)
 	st->v->flags |= VAR_KEEP;
-    return TRUE;
+    return AMR_OK;
 }
 
-/* :range */
-static Boolean
-ApplyModifier_Range(ApplyModifiersState *st)
+/* The :range modifier generates an integer sequence as long as the words.
+ * The :range=7 modifier generates an integer sequence from 1 to 7. */
+static ApplyModifierResult
+ApplyModifier_Range(const char **pp, ApplyModifiersState *st)
 {
-    int n;
-    char *ep;
+    size_t n;
+    Buffer buf;
+    size_t i;
 
-    st->cp = st->tstr + 1;	/* make sure it is set */
-    if (!STRMOD_MATCHX(st->tstr, "range", 5))
-	return FALSE;
+    const char *mod = *pp;
+    if (!ModMatchEq(mod, "range", st->endc))
+	return AMR_UNKNOWN;
 
-    if (st->tstr[5] == '=') {
-	n = strtoul(&st->tstr[6], &ep, 10);
-	st->cp = ep;
+    if (mod[5] == '=') {
+	char *ep;
+	n = (size_t)strtoul(mod + 6, &ep, 10);
+	*pp = ep;
     } else {
 	n = 0;
-	st->cp = st->tstr + 5;
+	*pp = mod + 5;
     }
-    st->newStr = VarRange(st->nstr, n);
-    st->termc = *st->cp;
-    return TRUE;
+
+    if (n == 0) {
+        Words words = Str_Words(st->val, FALSE);
+        n = words.len;
+        Words_Free(words);
+    }
+
+    Buf_Init(&buf, 0);
+
+    for (i = 0; i < n; i++) {
+	if (i != 0)
+	    Buf_AddByte(&buf, ' ');	/* XXX: st->sep, for consistency */
+	Buf_AddInt(&buf, 1 + (int)i);
+    }
+
+    st->newVal = Buf_Destroy(&buf, FALSE);
+    return AMR_OK;
 }
 
 /* :Mpattern or :Npattern */
-static void
-ApplyModifier_Match(ApplyModifiersState *st)
+static ApplyModifierResult
+ApplyModifier_Match(const char **pp, ApplyModifiersState *st)
 {
-    char    *pattern;
-    const char *endpat;		/* points just after end of pattern */
-    char    *cp2;
-    Boolean copy;		/* pattern should be, or has been, copied */
-    Boolean needSubst;
-    int nest;
+    const char *mod = *pp;
+    Boolean copy = FALSE;	/* pattern should be, or has been, copied */
+    Boolean needSubst = FALSE;
+    const char *endpat;
+    char *pattern;
+    ModifyWordsCallback callback;
 
-    copy = FALSE;
-    needSubst = FALSE;
-    nest = 1;
     /*
-     * In the loop below, ignore ':' unless we are at
-     * (or back to) the original brace level.
-     * XXX This will likely not work right if $() and ${}
-     * are intermixed.
+     * In the loop below, ignore ':' unless we are at (or back to) the
+     * original brace level.
+     * XXX This will likely not work right if $() and ${} are intermixed.
      */
-    for (st->cp = st->tstr + 1;
-	 *st->cp != '\0' && !(*st->cp == ':' && nest == 1);
-	 st->cp++) {
-	if (*st->cp == '\\' &&
-	    (st->cp[1] == ':' || st->cp[1] == st->endc ||
-	     st->cp[1] == st->startc)) {
+    int nest = 0;
+    const char *p;
+    for (p = mod + 1; *p != '\0' && !(*p == ':' && nest == 0); p++) {
+	if (*p == '\\' &&
+	    (p[1] == ':' || p[1] == st->endc || p[1] == st->startc)) {
 	    if (!needSubst)
 		copy = TRUE;
-	    st->cp++;
+	    p++;
 	    continue;
 	}
-	if (*st->cp == '$')
+	if (*p == '$')
 	    needSubst = TRUE;
-	if (*st->cp == '(' || *st->cp == '{')
-	    ++nest;
-	if (*st->cp == ')' || *st->cp == '}') {
-	    --nest;
-	    if (nest == 0)
+	if (*p == '(' || *p == '{')
+	    nest++;
+	if (*p == ')' || *p == '}') {
+	    nest--;
+	    if (nest < 0)
 		break;
 	}
     }
-    st->termc = *st->cp;
-    endpat = st->cp;
+    *pp = p;
+    endpat = p;
+
     if (copy) {
-	/*
-	 * Need to compress the \:'s out of the pattern, so
-	 * allocate enough room to hold the uncompressed
-	 * pattern (note that st->cp started at st->tstr+1, so
-	 * st->cp - st->tstr takes the null byte into account) and
-	 * compress the pattern into the space.
-	 */
-	pattern = bmake_malloc(st->cp - st->tstr);
-	for (cp2 = pattern, st->cp = st->tstr + 1;
-	     st->cp < endpat;
-	     st->cp++, cp2++) {
-	    if ((*st->cp == '\\') && (st->cp+1 < endpat) &&
-		(st->cp[1] == ':' || st->cp[1] == st->endc))
-		st->cp++;
-	    *cp2 = *st->cp;
+	char *dst;
+	const char *src;
+
+	/* Compress the \:'s out of the pattern. */
+	pattern = bmake_malloc((size_t)(endpat - (mod + 1)) + 1);
+	dst = pattern;
+	src = mod + 1;
+	for (; src < endpat; src++, dst++) {
+	    if (src[0] == '\\' && src + 1 < endpat &&
+		/* XXX: st->startc is missing here; see above */
+		(src[1] == ':' || src[1] == st->endc))
+		src++;
+	    *dst = *src;
 	}
-	*cp2 = '\0';
-	endpat = cp2;
+	*dst = '\0';
+	endpat = dst;
     } else {
-	/*
-	 * Either Var_Subst or VarModify will need a
-	 * nul-terminated string soon, so construct one now.
-	 */
-	pattern = bmake_strndup(st->tstr+1, endpat - (st->tstr + 1));
+	pattern = bmake_strsedup(mod + 1, endpat);
     }
+
     if (needSubst) {
 	/* pattern contains embedded '$', so use Var_Subst to expand it. */
-	cp2 = pattern;
-	pattern = Var_Subst(NULL, cp2, st->ctxt, st->flags);
-	free(cp2);
-    }
-    if (DEBUG(VAR))
-	fprintf(debug_file, "Pattern[%s] for [%s] is [%s]\n",
-	    st->v->name, st->nstr, pattern);
-    if (*st->tstr == 'M') {
-	st->newStr = VarModify(st->ctxt, &st->parsestate, st->nstr, VarMatch,
-			       pattern);
-    } else {
-	st->newStr = VarModify(st->ctxt, &st->parsestate, st->nstr, VarNoMatch,
-			       pattern);
+	char *old_pattern = pattern;
+	pattern = Var_Subst(pattern, st->ctxt, st->eflags);
+	free(old_pattern);
     }
+
+    VAR_DEBUG("Pattern[%s] for [%s] is [%s]\n", st->v->name, st->val, pattern);
+
+    callback = mod[0] == 'M' ? ModifyWord_Match : ModifyWord_NoMatch;
+    st->newVal = ModifyWords(st->ctxt, st->sep, st->oneBigWord, st->val,
+			     callback, pattern);
     free(pattern);
+    return AMR_OK;
 }
 
 /* :S,from,to, */
-static Boolean
-ApplyModifier_Subst(ApplyModifiersState *st)
+static ApplyModifierResult
+ApplyModifier_Subst(const char **pp, ApplyModifiersState *st)
 {
-    VarPattern 	    pattern;
-    Var_Parse_State tmpparsestate;
+    ModifyWord_SubstArgs args;
+    char *lhs, *rhs;
+    Boolean oneBigWord;
 
-    pattern.flags = 0;
-    tmpparsestate = st->parsestate;
-    st->delim = st->tstr[1];
-    st->tstr += 2;
+    char delim = (*pp)[1];
+    if (delim == '\0') {
+	Error("Missing delimiter for :S modifier");
+	(*pp)++;
+	return AMR_CLEANUP;
+    }
+
+    *pp += 2;
+
+    args.pflags = 0;
+    args.matched = FALSE;
 
     /*
      * If pattern begins with '^', it is anchored to the
      * start of the word -- skip over it and flag pattern.
      */
-    if (*st->tstr == '^') {
-	pattern.flags |= VAR_MATCH_START;
-	st->tstr += 1;
+    if (**pp == '^') {
+	args.pflags |= VARP_ANCHOR_START;
+	(*pp)++;
     }
 
-    st->cp = st->tstr;
-    pattern.lhs = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	&pattern.flags, &pattern.leftLen, NULL);
-    if (pattern.lhs == NULL)
-	return FALSE;
+    lhs = ParseModifierPart(pp, delim, st->eflags, st->ctxt,
+			    &args.lhsLen, &args.pflags, NULL);
+    if (lhs == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
+    args.lhs = lhs;
 
-    pattern.rhs = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	NULL, &pattern.rightLen, &pattern);
-    if (pattern.rhs == NULL)
-	return FALSE;
+    rhs = ParseModifierPart(pp, delim, st->eflags, st->ctxt,
+			    &args.rhsLen, NULL, &args);
+    if (rhs == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
+    args.rhs = rhs;
 
-    /*
-     * Check for global substitution. If 'g' after the final
-     * delimiter, substitution is global and is marked that
-     * way.
-     */
-    for (;; st->cp++) {
-	switch (*st->cp) {
+    oneBigWord = st->oneBigWord;
+    for (;; (*pp)++) {
+	switch (**pp) {
 	case 'g':
-	    pattern.flags |= VAR_SUB_GLOBAL;
+	    args.pflags |= VARP_SUB_GLOBAL;
 	    continue;
 	case '1':
-	    pattern.flags |= VAR_SUB_ONE;
+	    args.pflags |= VARP_SUB_ONE;
 	    continue;
 	case 'W':
-	    tmpparsestate.oneBigWord = TRUE;
+	    oneBigWord = TRUE;
 	    continue;
 	}
 	break;
     }
 
-    st->termc = *st->cp;
-    st->newStr = VarModify(
-	st->ctxt, &tmpparsestate, st->nstr, VarSubstitute, &pattern);
+    st->newVal = ModifyWords(st->ctxt, st->sep, oneBigWord, st->val,
+			     ModifyWord_Subst, &args);
 
-    /* Free the two strings. */
-    free(UNCONST(pattern.lhs));
-    free(UNCONST(pattern.rhs));
-    st->delim = '\0';
-    return TRUE;
+    free(lhs);
+    free(rhs);
+    return AMR_OK;
 }
 
 #ifndef NO_REGEX
+
 /* :C,from,to, */
-static Boolean
-ApplyModifier_Regex(ApplyModifiersState *st)
+static ApplyModifierResult
+ApplyModifier_Regex(const char **pp, ApplyModifiersState *st)
 {
-    VarREPattern    pattern;
-    char           *re;
-    int             error;
-    Var_Parse_State tmpparsestate;
+    char *re;
+    ModifyWord_SubstRegexArgs args;
+    Boolean oneBigWord;
+    int error;
 
-    pattern.flags = 0;
-    tmpparsestate = st->parsestate;
-    st->delim = st->tstr[1];
-    st->tstr += 2;
-
-    st->cp = st->tstr;
-
-    re = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	NULL, NULL, NULL);
-    if (re == NULL)
-	return FALSE;
-
-    pattern.replace = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	NULL, NULL, NULL);
-    if (pattern.replace == NULL) {
-	free(re);
-	return FALSE;
+    char delim = (*pp)[1];
+    if (delim == '\0') {
+	Error("Missing delimiter for :C modifier");
+	(*pp)++;
+	return AMR_CLEANUP;
     }
 
-    for (;; st->cp++) {
-	switch (*st->cp) {
+    *pp += 2;
+
+    re = ParseModifierPart(pp, delim, st->eflags, st->ctxt, NULL, NULL, NULL);
+    if (re == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
+
+    args.replace = ParseModifierPart(pp, delim, st->eflags, st->ctxt,
+				     NULL, NULL, NULL);
+    if (args.replace == NULL) {
+	free(re);
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
+
+    args.pflags = 0;
+    args.matched = FALSE;
+    oneBigWord = st->oneBigWord;
+    for (;; (*pp)++) {
+	switch (**pp) {
 	case 'g':
-	    pattern.flags |= VAR_SUB_GLOBAL;
+	    args.pflags |= VARP_SUB_GLOBAL;
 	    continue;
 	case '1':
-	    pattern.flags |= VAR_SUB_ONE;
+	    args.pflags |= VARP_SUB_ONE;
 	    continue;
 	case 'W':
-	    tmpparsestate.oneBigWord = TRUE;
+	    oneBigWord = TRUE;
 	    continue;
 	}
 	break;
     }
 
-    st->termc = *st->cp;
-
-    error = regcomp(&pattern.re, re, REG_EXTENDED);
+    error = regcomp(&args.re, re, REG_EXTENDED);
     free(re);
     if (error) {
-	*st->lengthPtr = st->cp - st->start + 1;
-	VarREError(error, &pattern.re, "RE substitution error");
-	free(pattern.replace);
-	return FALSE;
+	VarREError(error, &args.re, "Regex compilation error");
+	free(args.replace);
+	return AMR_CLEANUP;
     }
 
-    pattern.nsub = pattern.re.re_nsub + 1;
-    if (pattern.nsub < 1)
-	pattern.nsub = 1;
-    if (pattern.nsub > 10)
-	pattern.nsub = 10;
-    pattern.matches = bmake_malloc(pattern.nsub * sizeof(regmatch_t));
-    st->newStr = VarModify(
-	st->ctxt, &tmpparsestate, st->nstr, VarRESubstitute, &pattern);
-    regfree(&pattern.re);
-    free(pattern.replace);
-    free(pattern.matches);
-    st->delim = '\0';
-    return TRUE;
+    args.nsub = args.re.re_nsub + 1;
+    if (args.nsub > 10)
+	args.nsub = 10;
+    st->newVal = ModifyWords(st->ctxt, st->sep, oneBigWord, st->val,
+			     ModifyWord_SubstRegex, &args);
+    regfree(&args.re);
+    free(args.replace);
+    return AMR_OK;
 }
 #endif
 
-/* :tA, :tu, :tl, etc. */
-static Boolean
-ApplyModifier_To(ApplyModifiersState *st)
+static void
+ModifyWord_Copy(const char *word, SepBuf *buf, void *data MAKE_ATTR_UNUSED)
 {
-    st->cp = st->tstr + 1;	/* make sure it is set */
-    if (st->tstr[1] != st->endc && st->tstr[1] != ':') {
-	if (st->tstr[1] == 's') {
-	    /* Use the char (if any) at st->tstr[2] as the word separator. */
-	    VarPattern pattern;
+    SepBuf_AddStr(buf, word);
+}
 
-	    if (st->tstr[2] != st->endc &&
-		(st->tstr[3] == st->endc || st->tstr[3] == ':')) {
-		/* ":ts<unrecognised><endc>" or
-		 * ":ts<unrecognised>:" */
-		st->parsestate.varSpace = st->tstr[2];
-		st->cp = st->tstr + 3;
-	    } else if (st->tstr[2] == st->endc || st->tstr[2] == ':') {
-		/* ":ts<endc>" or ":ts:" */
-		st->parsestate.varSpace = 0;	/* no separator */
-		st->cp = st->tstr + 2;
-	    } else if (st->tstr[2] == '\\') {
-		const char *xp = &st->tstr[3];
-		int base = 8;	/* assume octal */
+/* :ts<separator> */
+static ApplyModifierResult
+ApplyModifier_ToSep(const char **pp, ApplyModifiersState *st)
+{
+    /* XXX: pp points to the 's', for historic reasons only.
+     * Changing this will influence the error messages. */
+    const char *sep = *pp + 1;
 
-		switch (st->tstr[3]) {
-		case 'n':
-		    st->parsestate.varSpace = '\n';
-		    st->cp = st->tstr + 4;
-		    break;
-		case 't':
-		    st->parsestate.varSpace = '\t';
-		    st->cp = st->tstr + 4;
-		    break;
-		case 'x':
-		    base = 16;
-		    xp++;
-		    goto get_numeric;
-		case '0':
-		    base = 0;
-		    goto get_numeric;
-		default:
-		    if (isdigit((unsigned char)st->tstr[3])) {
-			char *ep;
-		    get_numeric:
-			st->parsestate.varSpace = strtoul(xp, &ep, base);
-			if (*ep != ':' && *ep != st->endc)
-			    return FALSE;
-			st->cp = ep;
-		    } else {
-			/* ":ts<backslash><unrecognised>". */
-			return FALSE;
-		    }
-		    break;
-		}
-	    } else {
-		/* Found ":ts<unrecognised><unrecognised>". */
-		return FALSE;
-	    }
-
-	    st->termc = *st->cp;
-
-	    /*
-	     * We cannot be certain that VarModify will be used - even if there
-	     * is a subsequent modifier, so do a no-op VarSubstitute now to for
-	     * str to be re-expanded without the spaces.
-	     */
-	    pattern.flags = VAR_SUB_ONE;
-	    pattern.lhs = pattern.rhs = "\032";
-	    pattern.leftLen = pattern.rightLen = 1;
-
-	    st->newStr = VarModify(
-		st->ctxt, &st->parsestate, st->nstr, VarSubstitute, &pattern);
-	} else if (st->tstr[2] == st->endc || st->tstr[2] == ':') {
-	    /* Check for two-character options: ":tu", ":tl" */
-	    if (st->tstr[1] == 'A') {	/* absolute path */
-		st->newStr = VarModify(
-			st->ctxt, &st->parsestate, st->nstr, VarRealpath, NULL);
-		st->cp = st->tstr + 2;
-		st->termc = *st->cp;
-	    } else if (st->tstr[1] == 'u') {
-		char *dp = bmake_strdup(st->nstr);
-		for (st->newStr = dp; *dp; dp++)
-		    *dp = toupper((unsigned char)*dp);
-		st->cp = st->tstr + 2;
-		st->termc = *st->cp;
-	    } else if (st->tstr[1] == 'l') {
-		char *dp = bmake_strdup(st->nstr);
-		for (st->newStr = dp; *dp; dp++)
-		    *dp = tolower((unsigned char)*dp);
-		st->cp = st->tstr + 2;
-		st->termc = *st->cp;
-	    } else if (st->tstr[1] == 'W' || st->tstr[1] == 'w') {
-		st->parsestate.oneBigWord = (st->tstr[1] == 'W');
-		st->newStr = st->nstr;
-		st->cp = st->tstr + 2;
-		st->termc = *st->cp;
-	    } else {
-		/* Found ":t<unrecognised>:" or ":t<unrecognised><endc>". */
-		return FALSE;
-	    }
-	} else {
-	    /* Found ":t<unrecognised><unrecognised>". */
-	    return FALSE;
-	}
-    } else {
-	/* Found ":t<endc>" or ":t:". */
-	return FALSE;
+    /* ":ts<any><endc>" or ":ts<any>:" */
+    if (sep[0] != st->endc && (sep[1] == st->endc || sep[1] == ':')) {
+	st->sep = sep[0];
+	*pp = sep + 1;
+	goto ok;
     }
-    return TRUE;
+
+    /* ":ts<endc>" or ":ts:" */
+    if (sep[0] == st->endc || sep[0] == ':') {
+	st->sep = '\0';		/* no separator */
+	*pp = sep;
+	goto ok;
+    }
+
+    /* ":ts<unrecognised><unrecognised>". */
+    if (sep[0] != '\\')
+	return AMR_BAD;
+
+    /* ":ts\n" */
+    if (sep[1] == 'n') {
+	st->sep = '\n';
+	*pp = sep + 2;
+	goto ok;
+    }
+
+    /* ":ts\t" */
+    if (sep[1] == 't') {
+	st->sep = '\t';
+	*pp = sep + 2;
+	goto ok;
+    }
+
+    /* ":ts\x40" or ":ts\100" */
+    {
+	const char *numStart = sep + 1;
+	int base = 8;		/* assume octal */
+	char *end;
+
+	if (sep[1] == 'x') {
+	    base = 16;
+	    numStart++;
+	} else if (!isdigit((unsigned char)sep[1]))
+	    return AMR_BAD;	/* ":ts<backslash><unrecognised>". */
+
+	st->sep = (char)strtoul(numStart, &end, base);
+	if (*end != ':' && *end != st->endc)
+	    return AMR_BAD;
+	*pp = end;
+    }
+
+ok:
+    st->newVal = ModifyWords(st->ctxt, st->sep, st->oneBigWord, st->val,
+			     ModifyWord_Copy, NULL);
+    return AMR_OK;
+}
+
+/* :tA, :tu, :tl, :ts<separator>, etc. */
+static ApplyModifierResult
+ApplyModifier_To(const char **pp, ApplyModifiersState *st)
+{
+    const char *mod = *pp;
+    assert(mod[0] == 't');
+
+    *pp = mod + 1;		/* make sure it is set */
+    if (mod[1] == st->endc || mod[1] == ':' || mod[1] == '\0')
+	return AMR_BAD;		/* Found ":t<endc>" or ":t:". */
+
+    if (mod[1] == 's')
+	return ApplyModifier_ToSep(pp, st);
+
+    if (mod[2] != st->endc && mod[2] != ':')
+	return AMR_BAD;		/* Found ":t<unrecognised><unrecognised>". */
+
+    /* Check for two-character options: ":tu", ":tl" */
+    if (mod[1] == 'A') {	/* absolute path */
+	st->newVal = ModifyWords(st->ctxt, st->sep, st->oneBigWord, st->val,
+				 ModifyWord_Realpath, NULL);
+	*pp = mod + 2;
+	return AMR_OK;
+    }
+
+    if (mod[1] == 'u') {
+	size_t i;
+	size_t len = strlen(st->val);
+	st->newVal = bmake_malloc(len + 1);
+	for (i = 0; i < len + 1; i++)
+	    st->newVal[i] = (char)toupper((unsigned char)st->val[i]);
+	*pp = mod + 2;
+	return AMR_OK;
+    }
+
+    if (mod[1] == 'l') {
+	size_t i;
+	size_t len = strlen(st->val);
+	st->newVal = bmake_malloc(len + 1);
+	for (i = 0; i < len + 1; i++)
+	    st->newVal[i] = (char)tolower((unsigned char)st->val[i]);
+	*pp = mod + 2;
+	return AMR_OK;
+    }
+
+    if (mod[1] == 'W' || mod[1] == 'w') {
+	st->oneBigWord = mod[1] == 'W';
+	st->newVal = st->val;
+	*pp = mod + 2;
+	return AMR_OK;
+    }
+
+    /* Found ":t<unrecognised>:" or ":t<unrecognised><endc>". */
+    return AMR_BAD;
 }
 
 /* :[#], :[1], etc. */
-static int
-ApplyModifier_Words(ApplyModifiersState *st)
+static ApplyModifierResult
+ApplyModifier_Words(const char **pp, ApplyModifiersState *st)
 {
-    /*
-     * Look for the closing ']', recursively
-     * expanding any embedded variables.
-     *
-     * estr is a pointer to the expanded result,
-     * which we must free().
-     */
+    char delim;
     char *estr;
+    char *ep;
+    int first, last;
 
-    st->cp = st->tstr + 1;	/* point to char after '[' */
-    st->delim = ']';		/* look for closing ']' */
-    estr = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	NULL, NULL, NULL);
-    if (estr == NULL)
-	return 'c';		/* report missing ']' */
-    /* now st->cp points just after the closing ']' */
-    st->delim = '\0';
-    if (st->cp[0] != ':' && st->cp[0] != st->endc) {
-	/* Found junk after ']' */
-	free(estr);
-	return 'b';
+    (*pp)++;			/* skip the '[' */
+    delim = ']';		/* look for closing ']' */
+    estr = ParseModifierPart(pp, delim, st->eflags, st->ctxt,
+			     NULL, NULL, NULL);
+    if (estr == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
     }
-    if (estr[0] == '\0') {
-	/* Found empty square brackets in ":[]". */
-	free(estr);
-	return 'b';
-    } else if (estr[0] == '#' && estr[1] == '\0') {
-	/* Found ":[#]" */
 
-	/*
-	 * We will need enough space for the decimal
-	 * representation of an int.  We calculate the
-	 * space needed for the octal representation,
-	 * and add enough slop to cope with a '-' sign
-	 * (which should never be needed) and a '\0'
-	 * string terminator.
-	 */
-	int newStrSize = (sizeof(int) * CHAR_BIT + 2) / 3 + 2;
+    /* now *pp points just after the closing ']' */
+    if (**pp != ':' && **pp != st->endc)
+	goto bad_modifier;	/* Found junk after ']' */
 
-	st->newStr = bmake_malloc(newStrSize);
-	if (st->parsestate.oneBigWord) {
-	    strncpy(st->newStr, "1", newStrSize);
+    if (estr[0] == '\0')
+	goto bad_modifier;	/* empty square brackets in ":[]". */
+
+    if (estr[0] == '#' && estr[1] == '\0') { /* Found ":[#]" */
+	if (st->oneBigWord) {
+	    st->newVal = bmake_strdup("1");
 	} else {
-	    /* XXX: brk_string() is a rather expensive
-	     * way of counting words. */
-	    char **av;
-	    char *as;
-	    int ac;
+	    Buffer buf;
 
-	    av = brk_string(st->nstr, &ac, FALSE, &as);
-	    snprintf(st->newStr, newStrSize, "%d", ac);
-	    free(as);
-	    free(av);
+	    Words words = Str_Words(st->val, FALSE);
+	    size_t ac = words.len;
+	    Words_Free(words);
+	    	
+	    Buf_Init(&buf, 4);	/* 3 digits + '\0' is usually enough */
+	    Buf_AddInt(&buf, (int)ac);
+	    st->newVal = Buf_Destroy(&buf, FALSE);
 	}
-	st->termc = *st->cp;
-	free(estr);
-	return 0;
-    } else if (estr[0] == '*' && estr[1] == '\0') {
+	goto ok;
+    }
+
+    if (estr[0] == '*' && estr[1] == '\0') {
 	/* Found ":[*]" */
-	st->parsestate.oneBigWord = TRUE;
-	st->newStr = st->nstr;
-	st->termc = *st->cp;
-	free(estr);
-	return 0;
-    } else if (estr[0] == '@' && estr[1] == '\0') {
-	/* Found ":[@]" */
-	st->parsestate.oneBigWord = FALSE;
-	st->newStr = st->nstr;
-	st->termc = *st->cp;
-	free(estr);
-	return 0;
-    } else {
-	char *ep;
-	/*
-	 * We expect estr to contain a single
-	 * integer for :[N], or two integers
-	 * separated by ".." for :[start..end].
-	 */
-	VarSelectWords_t seldata = { 0, 0 };
-
-	seldata.start = strtol(estr, &ep, 0);
-	if (ep == estr) {
-	    /* Found junk instead of a number */
-	    free(estr);
-	    return 'b';
-	} else if (ep[0] == '\0') {
-	    /* Found only one integer in :[N] */
-	    seldata.end = seldata.start;
-	} else if (ep[0] == '.' && ep[1] == '.' && ep[2] != '\0') {
-	    /* Expecting another integer after ".." */
-	    ep += 2;
-	    seldata.end = strtol(ep, &ep, 0);
-	    if (ep[0] != '\0') {
-		/* Found junk after ".." */
-		free(estr);
-		return 'b';
-	    }
-	} else {
-	    /* Found junk instead of ".." */
-	    free(estr);
-	    return 'b';
-	}
-	/*
-	 * Now seldata is properly filled in,
-	 * but we still have to check for 0 as
-	 * a special case.
-	 */
-	if (seldata.start == 0 && seldata.end == 0) {
-	    /* ":[0]" or perhaps ":[0..0]" */
-	    st->parsestate.oneBigWord = TRUE;
-	    st->newStr = st->nstr;
-	    st->termc = *st->cp;
-	    free(estr);
-	    return 0;
-	} else if (seldata.start == 0 || seldata.end == 0) {
-	    /* ":[0..N]" or ":[N..0]" */
-	    free(estr);
-	    return 'b';
-	}
-	/* Normal case: select the words described by seldata. */
-	st->newStr = VarSelectWords(
-	    st->ctxt, &st->parsestate, st->nstr, &seldata);
-
-	st->termc = *st->cp;
-	free(estr);
-	return 0;
+	st->oneBigWord = TRUE;
+	st->newVal = st->val;
+	goto ok;
     }
+
+    if (estr[0] == '@' && estr[1] == '\0') {
+	/* Found ":[@]" */
+	st->oneBigWord = FALSE;
+	st->newVal = st->val;
+	goto ok;
+    }
+
+    /*
+     * We expect estr to contain a single integer for :[N], or two integers
+     * separated by ".." for :[start..end].
+     */
+    first = (int)strtol(estr, &ep, 0);
+    if (ep == estr)		/* Found junk instead of a number */
+	goto bad_modifier;
+
+    if (ep[0] == '\0') {	/* Found only one integer in :[N] */
+	last = first;
+    } else if (ep[0] == '.' && ep[1] == '.' && ep[2] != '\0') {
+	/* Expecting another integer after ".." */
+	ep += 2;
+	last = (int)strtol(ep, &ep, 0);
+	if (ep[0] != '\0')	/* Found junk after ".." */
+	    goto bad_modifier;
+    } else
+	goto bad_modifier;	/* Found junk instead of ".." */
+
+    /*
+     * Now seldata is properly filled in, but we still have to check for 0 as
+     * a special case.
+     */
+    if (first == 0 && last == 0) {
+	/* ":[0]" or perhaps ":[0..0]" */
+	st->oneBigWord = TRUE;
+	st->newVal = st->val;
+	goto ok;
+    }
+
+    /* ":[0..N]" or ":[N..0]" */
+    if (first == 0 || last == 0)
+	goto bad_modifier;
+
+    /* Normal case: select the words described by seldata. */
+    st->newVal = VarSelectWords(st->sep, st->oneBigWord, st->val, first, last);
+
+ok:
+    free(estr);
+    return AMR_OK;
+
+bad_modifier:
+    free(estr);
+    return AMR_BAD;
 }
 
-/* :O or :Ox */
-static Boolean
-ApplyModifier_Order(ApplyModifiersState *st)
+static int
+str_cmp_asc(const void *a, const void *b)
 {
-    char otype;
+    return strcmp(*(const char * const *)a, *(const char * const *)b);
+}
 
-    st->cp = st->tstr + 1;	/* skip to the rest in any case */
-    if (st->tstr[1] == st->endc || st->tstr[1] == ':') {
-	otype = 's';
-	st->termc = *st->cp;
-    } else if ((st->tstr[1] == 'r' || st->tstr[1] == 'x') &&
-	       (st->tstr[2] == st->endc || st->tstr[2] == ':')) {
-	otype = st->tstr[1];
-	st->cp = st->tstr + 2;
-	st->termc = *st->cp;
+static int
+str_cmp_desc(const void *a, const void *b)
+{
+    return strcmp(*(const char * const *)b, *(const char * const *)a);
+}
+
+/* :O (order ascending) or :Or (order descending) or :Ox (shuffle) */
+static ApplyModifierResult
+ApplyModifier_Order(const char **pp, ApplyModifiersState *st)
+{
+    const char *mod = (*pp)++;	/* skip past the 'O' in any case */
+
+    Words words = Str_Words(st->val, FALSE);
+
+    if (mod[1] == st->endc || mod[1] == ':') {
+	/* :O sorts ascending */
+	qsort(words.words, words.len, sizeof(char *), str_cmp_asc);
+
+    } else if ((mod[1] == 'r' || mod[1] == 'x') &&
+	       (mod[2] == st->endc || mod[2] == ':')) {
+	(*pp)++;
+
+	if (mod[1] == 'r') {
+	    /* :Or sorts descending */
+	    qsort(words.words, words.len, sizeof(char *), str_cmp_desc);
+
+	} else {
+	    /* :Ox shuffles
+	     *
+	     * We will use [ac..2] range for mod factors. This will produce
+	     * random numbers in [(ac-1)..0] interval, and minimal
+	     * reasonable value for mod factor is 2 (the mod 1 will produce
+	     * 0 with probability 1).
+	     */
+	    size_t i;
+	    for (i = words.len - 1; i > 0; i--) {
+		size_t rndidx = (size_t)random() % (i + 1);
+		char *t = words.words[i];
+		words.words[i] = words.words[rndidx];
+		words.words[rndidx] = t;
+	    }
+	}
     } else {
-	return FALSE;
+	Words_Free(words);
+	return AMR_BAD;
     }
-    st->newStr = VarOrder(st->nstr, otype);
-    return TRUE;
+
+    st->newVal = Words_JoinFree(words);
+    return AMR_OK;
 }
 
 /* :? then : else */
-static Boolean
-ApplyModifier_IfElse(ApplyModifiersState *st)
+static ApplyModifierResult
+ApplyModifier_IfElse(const char **pp, ApplyModifiersState *st)
 {
-    VarPattern pattern;
-    Boolean value;
-    int cond_rc;
-    VarPattern_Flags lhs_flags, rhs_flags;
+    char delim;
+    char *then_expr, *else_expr;
 
-    /* find ':', and then substitute accordingly */
-    if (st->flags & VARF_WANTRES) {
+    Boolean value = FALSE;
+    VarEvalFlags then_eflags = st->eflags & ~(unsigned)VARE_WANTRES;
+    VarEvalFlags else_eflags = st->eflags & ~(unsigned)VARE_WANTRES;
+
+    int cond_rc = COND_PARSE;	/* anything other than COND_INVALID */
+    if (st->eflags & VARE_WANTRES) {
 	cond_rc = Cond_EvalExpression(NULL, st->v->name, &value, 0, FALSE);
-	if (cond_rc == COND_INVALID) {
-	    lhs_flags = rhs_flags = VAR_NOSUBST;
-	} else if (value) {
-	    lhs_flags = 0;
-	    rhs_flags = VAR_NOSUBST;
-	} else {
-	    lhs_flags = VAR_NOSUBST;
-	    rhs_flags = 0;
-	}
-    } else {
-	/* we are just consuming and discarding */
-	cond_rc = value = 0;
-	lhs_flags = rhs_flags = VAR_NOSUBST;
+	if (cond_rc != COND_INVALID && value)
+	    then_eflags |= VARE_WANTRES;
+	if (cond_rc != COND_INVALID && !value)
+	    else_eflags |= VARE_WANTRES;
     }
-    pattern.flags = 0;
 
-    st->cp = ++st->tstr;
-    st->delim = ':';
-    pattern.lhs = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	&lhs_flags, &pattern.leftLen, NULL);
-    if (pattern.lhs == NULL)
-	return FALSE;
+    (*pp)++;			/* skip past the '?' */
+    delim = ':';
+    then_expr = ParseModifierPart(pp, delim, then_eflags, st->ctxt,
+				  NULL, NULL, NULL);
+    if (then_expr == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
 
-    /* BROPEN or PROPEN */
-    st->delim = st->endc;
-    pattern.rhs = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	&rhs_flags, &pattern.rightLen, NULL);
-    if (pattern.rhs == NULL)
-	return FALSE;
+    delim = st->endc;		/* BRCLOSE or PRCLOSE */
+    else_expr = ParseModifierPart(pp, delim, else_eflags, st->ctxt,
+				  NULL, NULL, NULL);
+    if (else_expr == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
 
-    st->termc = *--st->cp;
-    st->delim = '\0';
+    (*pp)--;
     if (cond_rc == COND_INVALID) {
 	Error("Bad conditional expression `%s' in %s?%s:%s",
-	    st->v->name, st->v->name, pattern.lhs, pattern.rhs);
-	return FALSE;
+	      st->v->name, st->v->name, then_expr, else_expr);
+	return AMR_CLEANUP;
     }
 
     if (value) {
-	st->newStr = UNCONST(pattern.lhs);
-	free(UNCONST(pattern.rhs));
+	st->newVal = then_expr;
+	free(else_expr);
     } else {
-	st->newStr = UNCONST(pattern.rhs);
-	free(UNCONST(pattern.lhs));
+	st->newVal = else_expr;
+	free(then_expr);
     }
     if (st->v->flags & VAR_JUNK)
 	st->v->flags |= VAR_KEEP;
-    return TRUE;
+    return AMR_OK;
 }
 
-/* "::=", "::!=", "::+=", or "::?=" */
-static int
-ApplyModifier_Assign(ApplyModifiersState *st)
-{
-    if (st->tstr[1] == '=' ||
-	(st->tstr[2] == '=' &&
-	 (st->tstr[1] == '!' || st->tstr[1] == '+' || st->tstr[1] == '?'))) {
-	GNode *v_ctxt;		/* context where v belongs */
-	const char *emsg;
-	char *sv_name;
-	VarPattern pattern;
-	int how;
-	VarPattern_Flags vflags;
-
-	if (st->v->name[0] == 0)
-	    return 'b';
-
-	v_ctxt = st->ctxt;
-	sv_name = NULL;
-	++st->tstr;
-	if (st->v->flags & VAR_JUNK) {
-	    /*
-	     * We need to bmake_strdup() it incase
-	     * VarGetPattern() recurses.
-	     */
-	    sv_name = st->v->name;
-	    st->v->name = bmake_strdup(st->v->name);
-	} else if (st->ctxt != VAR_GLOBAL) {
-	    Var *gv = VarFind(st->v->name, st->ctxt, 0);
-	    if (gv == NULL)
-		v_ctxt = VAR_GLOBAL;
-	    else
-		VarFreeEnv(gv, TRUE);
-	}
-
-	switch ((how = *st->tstr)) {
-	case '+':
-	case '?':
-	case '!':
-	    st->cp = &st->tstr[2];
-	    break;
-	default:
-	    st->cp = ++st->tstr;
-	    break;
-	}
-	st->delim = st->startc == PROPEN ? PRCLOSE : BRCLOSE;
-	pattern.flags = 0;
-
-	vflags = (st->flags & VARF_WANTRES) ? 0 : VAR_NOSUBST;
-	pattern.rhs = VarGetPattern(
-	    st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	    &vflags, &pattern.rightLen, NULL);
-	if (st->v->flags & VAR_JUNK) {
-	    /* restore original name */
-	    free(st->v->name);
-	    st->v->name = sv_name;
-	}
-	if (pattern.rhs == NULL)
-	    return 'c';
-
-	st->termc = *--st->cp;
-	st->delim = '\0';
-
-	if (st->flags & VARF_WANTRES) {
-	    switch (how) {
-	    case '+':
-		Var_Append(st->v->name, pattern.rhs, v_ctxt);
-		break;
-	    case '!':
-		st->newStr = Cmd_Exec(pattern.rhs, &emsg);
-		if (emsg)
-		    Error(emsg, st->nstr);
-		else
-		    Var_Set(st->v->name, st->newStr, v_ctxt);
-		free(st->newStr);
-		break;
-	    case '?':
-		if ((st->v->flags & VAR_JUNK) == 0)
-		    break;
-		/* FALLTHROUGH */
-	    default:
-		Var_Set(st->v->name, pattern.rhs, v_ctxt);
-		break;
-	    }
-	}
-	free(UNCONST(pattern.rhs));
-	st->newStr = varNoError;
-	return 0;
-    }
-    return 'd';			/* "::<unrecognised>" */
-}
-
-/* remember current value */
-static Boolean
-ApplyModifier_Remember(ApplyModifiersState *st)
-{
-    st->cp = st->tstr + 1;	/* make sure it is set */
-    if (!STRMOD_MATCHX(st->tstr, "_", 1))
-	return FALSE;
-
-    if (st->tstr[1] == '=') {
-	char *np;
-	int n;
-
-	st->cp++;
-	n = strcspn(st->cp, ":)}");
-	np = bmake_strndup(st->cp, n + 1);
-	np[n] = '\0';
-	st->cp = st->tstr + 2 + n;
-	Var_Set(np, st->nstr, st->ctxt);
-	free(np);
-    } else {
-	Var_Set("_", st->nstr, st->ctxt);
-    }
-    st->newStr = st->nstr;
-    st->termc = *st->cp;
-    return TRUE;
-}
-
-#ifdef SYSVVARSUB
-/* :from=to */
-static int
-ApplyModifier_SysV(ApplyModifiersState *st)
-{
-    /*
-     * This can either be a bogus modifier or a System-V
-     * substitution command.
-     */
-    VarPattern      pattern;
-    Boolean         eqFound = FALSE;
-
-    pattern.flags = 0;
-
-    /*
-     * First we make a pass through the string trying
-     * to verify it is a SYSV-make-style translation:
-     * it must be: <string1>=<string2>)
-     */
-    st->cp = st->tstr;
-    st->cnt = 1;
-    while (*st->cp != '\0' && st->cnt) {
-	if (*st->cp == '=') {
-	    eqFound = TRUE;
-	    /* continue looking for st->endc */
-	} else if (*st->cp == st->endc)
-	    st->cnt--;
-	else if (*st->cp == st->startc)
-	    st->cnt++;
-	if (st->cnt)
-	    st->cp++;
-    }
-    if (*st->cp != st->endc || !eqFound)
-	return 0;
-
-    st->delim = '=';
-    st->cp = st->tstr;
-    pattern.lhs = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	&pattern.flags, &pattern.leftLen, NULL);
-    if (pattern.lhs == NULL)
-	return 'c';
-
-    st->delim = st->endc;
-    pattern.rhs = VarGetPattern(
-	st->ctxt, &st->parsestate, st->flags, &st->cp, st->delim,
-	NULL, &pattern.rightLen, &pattern);
-    if (pattern.rhs == NULL)
-	return 'c';
-
-    /*
-     * SYSV modifications happen through the whole
-     * string. Note the pattern is anchored at the end.
-     */
-    st->termc = *--st->cp;
-    st->delim = '\0';
-    if (pattern.leftLen == 0 && *st->nstr == '\0') {
-	st->newStr = st->nstr;	/* special case */
-    } else {
-	st->newStr = VarModify(
-	    st->ctxt, &st->parsestate, st->nstr, VarSYSVMatch, &pattern);
-    }
-    free(UNCONST(pattern.lhs));
-    free(UNCONST(pattern.rhs));
-    return '=';
-}
-#endif
-
 /*
- * Now we need to apply any modifiers the user wants applied.
- * These are:
- *  	  :M<pattern>	words which match the given <pattern>.
- *  			<pattern> is of the standard file
- *  			wildcarding form.
- *  	  :N<pattern>	words which do not match the given <pattern>.
- *  	  :S<d><pat1><d><pat2><d>[1gW]
- *  			Substitute <pat2> for <pat1> in the value
- *  	  :C<d><pat1><d><pat2><d>[1gW]
- *  			Substitute <pat2> for regex <pat1> in the value
- *  	  :H		Substitute the head of each word
- *  	  :T		Substitute the tail of each word
- *  	  :E		Substitute the extension (minus '.') of
- *  			each word
- *  	  :R		Substitute the root of each word
- *  			(pathname minus the suffix).
- *	  :O		("Order") Alphabeticaly sort words in variable.
- *	  :Ox		("intermiX") Randomize words in variable.
- *	  :u		("uniq") Remove adjacent duplicate words.
- *	  :tu		Converts the variable contents to uppercase.
- *	  :tl		Converts the variable contents to lowercase.
- *	  :ts[c]	Sets varSpace - the char used to
- *			separate words to 'c'. If 'c' is
- *			omitted then no separation is used.
- *	  :tW		Treat the variable contents as a single
- *			word, even if it contains spaces.
- *			(Mnemonic: one big 'W'ord.)
- *	  :tw		Treat the variable contents as multiple
- *			space-separated words.
- *			(Mnemonic: many small 'w'ords.)
- *	  :[index]	Select a single word from the value.
- *	  :[start..end]	Select multiple words from the value.
- *	  :[*] or :[0]	Select the entire value, as a single
- *			word.  Equivalent to :tW.
- *	  :[@]		Select the entire value, as multiple
- *			words.	Undoes the effect of :[*].
- *			Equivalent to :tw.
- *	  :[#]		Returns the number of words in the value.
- *
- *	  :?<true-value>:<false-value>
- *			If the variable evaluates to true, return
- *			true value, else return the second value.
- *    	  :lhs=rhs  	Like :S, but the rhs goes to the end of
- *    			the invocation.
- *	  :sh		Treat the current value as a command
- *			to be run, new value is its output.
- * The following added so we can handle ODE makefiles.
- *	  :@<tmpvar>@<newval>@
- *			Assign a temporary local variable <tmpvar>
- *			to the current value of each word in turn
- *			and replace each word with the result of
- *			evaluating <newval>
- *	  :D<newval>	Use <newval> as value if variable defined
- *	  :U<newval>	Use <newval> as value if variable undefined
- *	  :L		Use the name of the variable as the value.
- *	  :P		Use the path of the node that has the same
- *			name as the variable as the value.  This
- *			basically includes an implied :L so that
- *			the common method of refering to the path
- *			of your dependent 'x' in a rule is to use
- *			the form '${x:P}'.
- *	  :!<cmd>!	Run cmd much the same as :sh run's the
- *			current value of the variable.
- * The ::= modifiers, actually assign a value to the variable.
+ * The ::= modifiers actually assign a value to the variable.
  * Their main purpose is in supporting modifiers of .for loop
  * iterators and other obscure uses.  They always expand to
  * nothing.  In a target rule that would otherwise expand to an
@@ -3287,358 +2794,589 @@ ApplyModifier_SysV(ApplyModifiersState *st)
  *	  ::!=<cmd>	Assigns output of <cmd> as the new value of
  *			variable.
  */
-static char *
-ApplyModifiers(char *nstr, const char *tstr,
-	       int const startc, int const endc,
-	       Var * const v, GNode * const ctxt, int const flags,
-	       int * const lengthPtr, void ** const freePtr)
+static ApplyModifierResult
+ApplyModifier_Assign(const char **pp, ApplyModifiersState *st)
 {
+    GNode *v_ctxt;
+    char *sv_name;
+    char delim;
+    char *val;
+
+    const char *mod = *pp;
+    const char *op = mod + 1;
+    if (!(op[0] == '=' ||
+	  (op[1] == '=' &&
+	   (op[0] == '!' || op[0] == '+' || op[0] == '?'))))
+	return AMR_UNKNOWN;	/* "::<unrecognised>" */
+
+
+    if (st->v->name[0] == 0) {
+	*pp = mod + 1;
+	return AMR_BAD;
+    }
+
+    v_ctxt = st->ctxt;		/* context where v belongs */
+    sv_name = NULL;
+    if (st->v->flags & VAR_JUNK) {
+	/*
+	 * We need to bmake_strdup() it in case ParseModifierPart() recurses.
+	 */
+	sv_name = st->v->name;
+	st->v->name = bmake_strdup(st->v->name);
+    } else if (st->ctxt != VAR_GLOBAL) {
+	Var *gv = VarFind(st->v->name, st->ctxt, 0);
+	if (gv == NULL)
+	    v_ctxt = VAR_GLOBAL;
+	else
+	    VarFreeEnv(gv, TRUE);
+    }
+
+    switch (op[0]) {
+    case '+':
+    case '?':
+    case '!':
+	*pp = mod + 3;
+	break;
+    default:
+	*pp = mod + 2;
+	break;
+    }
+
+    delim = st->startc == PROPEN ? PRCLOSE : BRCLOSE;
+    val = ParseModifierPart(pp, delim, st->eflags, st->ctxt, NULL, NULL, NULL);
+    if (st->v->flags & VAR_JUNK) {
+	/* restore original name */
+	free(st->v->name);
+	st->v->name = sv_name;
+    }
+    if (val == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
+
+    (*pp)--;
+
+    if (st->eflags & VARE_WANTRES) {
+	switch (op[0]) {
+	case '+':
+	    Var_Append(st->v->name, val, v_ctxt);
+	    break;
+	case '!': {
+	    const char *errfmt;
+	    char *cmd_output = Cmd_Exec(val, &errfmt);
+	    if (errfmt)
+		Error(errfmt, val);
+	    else
+		Var_Set(st->v->name, cmd_output, v_ctxt);
+	    free(cmd_output);
+	    break;
+	}
+	case '?':
+	    if (!(st->v->flags & VAR_JUNK))
+		break;
+	    /* FALLTHROUGH */
+	default:
+	    Var_Set(st->v->name, val, v_ctxt);
+	    break;
+	}
+    }
+    free(val);
+    st->newVal = varNoError;	/* XXX: varNoError is kind of an error,
+				 * the intention here is to just return
+				 * an empty string. */
+    return AMR_OK;
+}
+
+/* remember current value */
+static ApplyModifierResult
+ApplyModifier_Remember(const char **pp, ApplyModifiersState *st)
+{
+    const char *mod = *pp;
+    if (!ModMatchEq(mod, "_", st->endc))
+	return AMR_UNKNOWN;
+
+    if (mod[1] == '=') {
+	size_t n = strcspn(mod + 2, ":)}");
+	char *name = bmake_strldup(mod + 2, n);
+	Var_Set(name, st->val, st->ctxt);
+	free(name);
+	*pp = mod + 2 + n;
+    } else {
+	Var_Set("_", st->val, st->ctxt);
+	*pp = mod + 1;
+    }
+    st->newVal = st->val;
+    return AMR_OK;
+}
+
+/* Apply the given function to each word of the variable value. */
+static ApplyModifierResult
+ApplyModifier_WordFunc(const char **pp, ApplyModifiersState *st,
+		       ModifyWordsCallback modifyWord)
+{
+    char delim = (*pp)[1];
+    if (delim != st->endc && delim != ':')
+	return AMR_UNKNOWN;
+
+    st->newVal = ModifyWords(st->ctxt, st->sep, st->oneBigWord,
+			    st->val, modifyWord, NULL);
+    (*pp)++;
+    return AMR_OK;
+}
+
+#ifdef SYSVVARSUB
+/* :from=to */
+static ApplyModifierResult
+ApplyModifier_SysV(const char **pp, ApplyModifiersState *st)
+{
+    char delim;
+    char *lhs, *rhs;
+
+    const char *mod = *pp;
+    Boolean eqFound = FALSE;
+
+    /*
+     * First we make a pass through the string trying
+     * to verify it is a SYSV-make-style translation:
+     * it must be: <string1>=<string2>)
+     */
+    int nest = 1;
+    const char *next = mod;
+    while (*next != '\0' && nest > 0) {
+	if (*next == '=') {
+	    eqFound = TRUE;
+	    /* continue looking for st->endc */
+	} else if (*next == st->endc)
+	    nest--;
+	else if (*next == st->startc)
+	    nest++;
+	if (nest > 0)
+	    next++;
+    }
+    if (*next != st->endc || !eqFound)
+	return AMR_UNKNOWN;
+
+    delim = '=';
+    *pp = mod;
+    lhs = ParseModifierPart(pp, delim, st->eflags, st->ctxt, NULL, NULL, NULL);
+    if (lhs == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
+
+    delim = st->endc;
+    rhs = ParseModifierPart(pp, delim, st->eflags, st->ctxt, NULL, NULL, NULL);
+    if (rhs == NULL) {
+	st->missing_delim = delim;
+	return AMR_CLEANUP;
+    }
+
+    /*
+     * SYSV modifications happen through the whole
+     * string. Note the pattern is anchored at the end.
+     */
+    (*pp)--;
+    if (lhs[0] == '\0' && *st->val == '\0') {
+	st->newVal = st->val;	/* special case */
+    } else {
+	ModifyWord_SYSVSubstArgs args = {st->ctxt, lhs, rhs};
+	st->newVal = ModifyWords(st->ctxt, st->sep, st->oneBigWord, st->val,
+				 ModifyWord_SYSVSubst, &args);
+    }
+    free(lhs);
+    free(rhs);
+    return AMR_OK;
+}
+#endif
+
+/* Apply any modifiers (such as :Mpattern or :@var@loop@ or :Q or ::=value). */
+static char *
+ApplyModifiers(
+    const char **pp,		/* the parsing position, updated upon return */
+    char *val,			/* the current value of the variable */
+    char const startc,		/* '(' or '{', or '\0' for indirect modifiers */
+    char const endc,		/* ')' or '}', or '\0' for indirect modifiers */
+    Var * const v,		/* the variable may have its flags changed */
+    GNode * const ctxt,		/* for looking up and modifying variables */
+    VarEvalFlags const eflags,
+    void ** const freePtr	/* free this after using the return value */
+) {
     ApplyModifiersState st = {
-	startc, endc, v, ctxt, flags, lengthPtr, freePtr,
-	nstr, tstr, tstr, tstr,
-	'\0', 0, '\0', 0, {' ', FALSE}, NULL
+	startc, endc, v, ctxt, eflags, val,
+	var_Error,		/* .newVal */
+	'\0',			/* .missing_delim */
+	' ',			/* .sep */
+	FALSE			/* .oneBigWord */
     };
+    const char *p;
+    const char *mod;
+    ApplyModifierResult res;
 
-    while (*st.tstr && *st.tstr != st.endc) {
+    assert(startc == '(' || startc == '{' || startc == '\0');
+    assert(endc == ')' || endc == '}' || endc == '\0');
+    assert(val != NULL);
 
-	if (*st.tstr == '$') {
+    p = *pp;
+    while (*p != '\0' && *p != endc) {
+
+	if (*p == '$') {
 	    /*
 	     * We may have some complex modifiers in a variable.
 	     */
-	    void *freeIt;
-	    char *rval;
 	    int rlen;
-	    int c;
-
-	    rval = Var_Parse(st.tstr, st.ctxt, st.flags, &rlen, &freeIt);
+	    void *freeIt;
+	    const char *rval = Var_Parse(p, st.ctxt, st.eflags, &rlen, &freeIt);
 
 	    /*
 	     * If we have not parsed up to st.endc or ':',
 	     * we are not interested.
 	     */
-	    if (rval != NULL && *rval &&
-		(c = st.tstr[rlen]) != '\0' &&
-		c != ':' &&
-		c != st.endc) {
+	    int c;
+	    if (rval[0] != '\0' &&
+		(c = p[rlen]) != '\0' && c != ':' && c != st.endc) {
 		free(freeIt);
 		goto apply_mods;
 	    }
 
-	    if (DEBUG(VAR)) {
-		fprintf(debug_file, "Got '%s' from '%.*s'%.*s\n",
-		       rval, rlen, st.tstr, rlen, st.tstr + rlen);
-	    }
+	    VAR_DEBUG("Indirect modifier \"%s\" from \"%.*s\"\n",
+		      rval, rlen, p);
 
-	    st.tstr += rlen;
+	    p += rlen;
 
-	    if (rval != NULL && *rval) {
-		int used;
-
-		st.nstr = ApplyModifiers(st.nstr, rval, 0, 0, st.v,
-				      st.ctxt, st.flags, &used, st.freePtr);
-		if (st.nstr == var_Error
-		    || (st.nstr == varNoError && (st.flags & VARF_UNDEFERR) == 0)
-		    || strlen(rval) != (size_t) used) {
+	    if (rval[0] != '\0') {
+		const char *rval_pp = rval;
+		st.val = ApplyModifiers(&rval_pp, st.val, '\0', '\0', v,
+					ctxt, eflags, freePtr);
+		if (st.val == var_Error
+		    || (st.val == varNoError && !(st.eflags & VARE_UNDEFERR))
+		    || *rval_pp != '\0') {
 		    free(freeIt);
 		    goto out;	/* error already reported */
 		}
 	    }
 	    free(freeIt);
-	    if (*st.tstr == ':')
-		st.tstr++;
-	    else if (!*st.tstr && st.endc) {
+	    if (*p == ':')
+		p++;
+	    else if (*p == '\0' && endc != '\0') {
 		Error("Unclosed variable specification after complex "
-		    "modifier (expecting '%c') for %s", st.endc, st.v->name);
+		      "modifier (expecting '%c') for %s", st.endc, st.v->name);
 		goto out;
 	    }
 	    continue;
 	}
     apply_mods:
+	st.newVal = var_Error;	/* default value, in case of errors */
+	res = AMR_BAD;		/* just a safe fallback */
+	mod = p;
+
 	if (DEBUG(VAR)) {
-	    fprintf(debug_file, "Applying[%s] :%c to \"%s\"\n", st.v->name,
-		*st.tstr, st.nstr);
+	    char eflags_str[VarEvalFlags_ToStringSize];
+	    char vflags_str[VarFlags_ToStringSize];
+	    Boolean is_single_char = mod[0] != '\0' &&
+		(mod[1] == endc || mod[1] == ':');
+
+	    /* At this point, only the first character of the modifier can
+	     * be used since the end of the modifier is not yet known. */
+	    VAR_DEBUG("Applying ${%s:%c%s} to \"%s\" "
+		      "(eflags = %s, vflags = %s)\n",
+		      st.v->name, mod[0], is_single_char ? "" : "...", st.val,
+		      Enum_FlagsToString(eflags_str, sizeof eflags_str,
+					 st.eflags, VarEvalFlags_ToStringSpecs),
+		      Enum_FlagsToString(vflags_str, sizeof vflags_str,
+					 st.v->flags, VarFlags_ToStringSpecs));
 	}
-	st.newStr = var_Error;
-	switch ((st.modifier = *st.tstr)) {
+
+	switch (*mod) {
 	case ':':
-	    {
-		int res = ApplyModifier_Assign(&st);
-		if (res == 'b')
-		    goto bad_modifier;
-		if (res == 'c')
-		    goto cleanup;
-		if (res == 'd')
-		    goto default_case;
-		break;
-	    }
+	    res = ApplyModifier_Assign(&p, &st);
+	    break;
 	case '@':
-	    ApplyModifier_At(&st);
+	    res = ApplyModifier_Loop(&p, &st);
 	    break;
 	case '_':
-	    if (!ApplyModifier_Remember(&st))
-		goto default_case;
+	    res = ApplyModifier_Remember(&p, &st);
 	    break;
 	case 'D':
 	case 'U':
-	    ApplyModifier_Defined(&st);
+	    res = ApplyModifier_Defined(&p, &st);
 	    break;
 	case 'L':
-	    {
-		if ((st.v->flags & VAR_JUNK) != 0)
-		    st.v->flags |= VAR_KEEP;
-		st.newStr = bmake_strdup(st.v->name);
-		st.cp = ++st.tstr;
-		st.termc = *st.tstr;
-		break;
-	    }
+	    if (st.v->flags & VAR_JUNK)
+		st.v->flags |= VAR_KEEP;
+	    st.newVal = bmake_strdup(st.v->name);
+	    p++;
+	    res = AMR_OK;
+	    break;
 	case 'P':
-	    ApplyModifier_Path(&st);
+	    res = ApplyModifier_Path(&p, &st);
 	    break;
 	case '!':
-	    if (!ApplyModifier_Exclam(&st))
-		goto cleanup;
+	    res = ApplyModifier_Exclam(&p, &st);
 	    break;
 	case '[':
-	    {
-		int res = ApplyModifier_Words(&st);
-		if (res == 'b')
-		    goto bad_modifier;
-		if (res == 'c')
-		    goto cleanup;
-		break;
-	    }
+	    res = ApplyModifier_Words(&p, &st);
+	    break;
 	case 'g':
-	    if (!ApplyModifier_Gmtime(&st))
-		goto default_case;
+	    res = ApplyModifier_Gmtime(&p, &st);
 	    break;
 	case 'h':
-	    if (!ApplyModifier_Hash(&st))
-		goto default_case;
+	    res = ApplyModifier_Hash(&p, &st);
 	    break;
 	case 'l':
-	    if (!ApplyModifier_Localtime(&st))
-		goto default_case;
+	    res = ApplyModifier_Localtime(&p, &st);
 	    break;
 	case 't':
-	    if (!ApplyModifier_To(&st))
-		goto bad_modifier;
+	    res = ApplyModifier_To(&p, &st);
 	    break;
 	case 'N':
 	case 'M':
-	    ApplyModifier_Match(&st);
+	    res = ApplyModifier_Match(&p, &st);
 	    break;
 	case 'S':
-	    if (!ApplyModifier_Subst(&st))
-		goto cleanup;
+	    res = ApplyModifier_Subst(&p, &st);
 	    break;
 	case '?':
-	    if (!ApplyModifier_IfElse(&st))
-		goto cleanup;
+	    res = ApplyModifier_IfElse(&p, &st);
 	    break;
 #ifndef NO_REGEX
 	case 'C':
-	    if (!ApplyModifier_Regex(&st))
-		goto cleanup;
+	    res = ApplyModifier_Regex(&p, &st);
 	    break;
 #endif
 	case 'q':
 	case 'Q':
-	    if (st.tstr[1] == st.endc || st.tstr[1] == ':') {
-		st.newStr = VarQuote(st.nstr, st.modifier == 'q');
-		st.cp = st.tstr + 1;
-		st.termc = *st.cp;
-		break;
-	    }
-	    goto default_case;
+	    if (p[1] == st.endc || p[1] == ':') {
+		st.newVal = VarQuote(st.val, *mod == 'q');
+		p++;
+		res = AMR_OK;
+	    } else
+		res = AMR_UNKNOWN;
+	    break;
 	case 'T':
-	    if (st.tstr[1] == st.endc || st.tstr[1] == ':') {
-		st.newStr = VarModify(st.ctxt, &st.parsestate, st.nstr, VarTail,
-				   NULL);
-		st.cp = st.tstr + 1;
-		st.termc = *st.cp;
-		break;
-	    }
-	    goto default_case;
+	    res = ApplyModifier_WordFunc(&p, &st, ModifyWord_Tail);
+	    break;
 	case 'H':
-	    if (st.tstr[1] == st.endc || st.tstr[1] == ':') {
-		st.newStr = VarModify(st.ctxt, &st.parsestate, st.nstr, VarHead,
-				   NULL);
-		st.cp = st.tstr + 1;
-		st.termc = *st.cp;
-		break;
-	    }
-	    goto default_case;
+	    res = ApplyModifier_WordFunc(&p, &st, ModifyWord_Head);
+	    break;
 	case 'E':
-	    if (st.tstr[1] == st.endc || st.tstr[1] == ':') {
-		st.newStr = VarModify(st.ctxt, &st.parsestate, st.nstr, VarSuffix,
-				   NULL);
-		st.cp = st.tstr + 1;
-		st.termc = *st.cp;
-		break;
-	    }
-	    goto default_case;
+	    res = ApplyModifier_WordFunc(&p, &st, ModifyWord_Suffix);
+	    break;
 	case 'R':
-	    if (st.tstr[1] == st.endc || st.tstr[1] == ':') {
-		st.newStr = VarModify(st.ctxt, &st.parsestate, st.nstr, VarRoot,
-				   NULL);
-		st.cp = st.tstr + 1;
-		st.termc = *st.cp;
-		break;
-	    }
-	    goto default_case;
+	    res = ApplyModifier_WordFunc(&p, &st, ModifyWord_Root);
+	    break;
 	case 'r':
-	    if (!ApplyModifier_Range(&st))
-		goto default_case;
+	    res = ApplyModifier_Range(&p, &st);
 	    break;
 	case 'O':
-	    if (!ApplyModifier_Order(&st))
-		goto bad_modifier;
+	    res = ApplyModifier_Order(&p, &st);
 	    break;
 	case 'u':
-	    if (st.tstr[1] == st.endc || st.tstr[1] == ':') {
-		st.newStr = VarUniq(st.nstr);
-		st.cp = st.tstr + 1;
-		st.termc = *st.cp;
-		break;
-	    }
-	    goto default_case;
+	    if (p[1] == st.endc || p[1] == ':') {
+		st.newVal = VarUniq(st.val);
+		p++;
+		res = AMR_OK;
+	    } else
+		res = AMR_UNKNOWN;
+	    break;
 #ifdef SUNSHCMD
 	case 's':
-	    if (st.tstr[1] == 'h' && (st.tstr[2] == st.endc || st.tstr[2] == ':')) {
-		const char *emsg;
-		if (st.flags & VARF_WANTRES) {
-		    st.newStr = Cmd_Exec(st.nstr, &emsg);
-		    if (emsg)
-			Error(emsg, st.nstr);
+	    if (p[1] == 'h' && (p[2] == st.endc || p[2] == ':')) {
+		if (st.eflags & VARE_WANTRES) {
+		    const char *errfmt;
+		    st.newVal = Cmd_Exec(st.val, &errfmt);
+		    if (errfmt)
+			Error(errfmt, st.val);
 		} else
-		    st.newStr = varNoError;
-		st.cp = st.tstr + 2;
-		st.termc = *st.cp;
-		break;
-	    }
-	    goto default_case;
+		    st.newVal = varNoError;
+		p += 2;
+		res = AMR_OK;
+	    } else
+		res = AMR_UNKNOWN;
+	    break;
 #endif
 	default:
-	default_case:
-	    {
-#ifdef SYSVVARSUB
-		int res = ApplyModifier_SysV(&st);
-		if (res == 'c')
-		    goto cleanup;
-		if (res != '=')
-#endif
-		{
-		    Error("Unknown modifier '%c'", *st.tstr);
-		    for (st.cp = st.tstr+1;
-			 *st.cp != ':' && *st.cp != st.endc && *st.cp != '\0';
-			 st.cp++)
-			continue;
-		    st.termc = *st.cp;
-		    st.newStr = var_Error;
-		}
-	    }
-	}
-	if (DEBUG(VAR)) {
-	    fprintf(debug_file, "Result[%s] of :%c is \"%s\"\n",
-		st.v->name, st.modifier, st.newStr);
+	    res = AMR_UNKNOWN;
 	}
 
-	if (st.newStr != st.nstr) {
-	    if (*st.freePtr) {
-		free(st.nstr);
-		*st.freePtr = NULL;
+#ifdef SYSVVARSUB
+	if (res == AMR_UNKNOWN) {
+	    assert(p == mod);
+	    res = ApplyModifier_SysV(&p, &st);
+	}
+#endif
+
+	if (res == AMR_UNKNOWN) {
+	    Error("Unknown modifier '%c'", *mod);
+	    for (p++; *p != ':' && *p != st.endc && *p != '\0'; p++)
+		continue;
+	    st.newVal = var_Error;
+	}
+	if (res == AMR_CLEANUP)
+	    goto cleanup;
+	if (res == AMR_BAD)
+	    goto bad_modifier;
+
+	if (DEBUG(VAR)) {
+	    char eflags_str[VarEvalFlags_ToStringSize];
+	    char vflags_str[VarFlags_ToStringSize];
+	    const char *quot = st.newVal == var_Error ? "" : "\"";
+	    const char *newVal = st.newVal == var_Error ? "error" : st.newVal;
+
+	    VAR_DEBUG("Result of ${%s:%.*s} is %s%s%s "
+		      "(eflags = %s, vflags = %s)\n",
+		      st.v->name, (int)(p - mod), mod, quot, newVal, quot,
+		      Enum_FlagsToString(eflags_str, sizeof eflags_str,
+					 st.eflags, VarEvalFlags_ToStringSpecs),
+		      Enum_FlagsToString(vflags_str, sizeof vflags_str,
+					 st.v->flags, VarFlags_ToStringSpecs));
+	}
+
+	if (st.newVal != st.val) {
+	    if (*freePtr) {
+		free(st.val);
+		*freePtr = NULL;
 	    }
-	    st.nstr = st.newStr;
-	    if (st.nstr != var_Error && st.nstr != varNoError) {
-		*st.freePtr = st.nstr;
+	    st.val = st.newVal;
+	    if (st.val != var_Error && st.val != varNoError) {
+		*freePtr = st.val;
 	    }
 	}
-	if (st.termc == '\0' && st.endc != '\0') {
+	if (*p == '\0' && st.endc != '\0') {
 	    Error("Unclosed variable specification (expecting '%c') "
-		"for \"%s\" (value \"%s\") modifier %c",
-		st.endc, st.v->name, st.nstr, st.modifier);
-	} else if (st.termc == ':') {
-	    st.cp++;
+		  "for \"%s\" (value \"%s\") modifier %c",
+		  st.endc, st.v->name, st.val, *mod);
+	} else if (*p == ':') {
+	    p++;
 	}
-	st.tstr = st.cp;
+	mod = p;
     }
 out:
-    *st.lengthPtr = st.tstr - st.start;
-    return st.nstr;
+    *pp = p;
+    assert(st.val != NULL);	/* Use var_Error or varNoError instead. */
+    return st.val;
 
 bad_modifier:
-    /* "{(" */
-    Error("Bad modifier `:%.*s' for %s", (int)strcspn(st.tstr, ":)}"), st.tstr,
-	  st.v->name);
+    Error("Bad modifier `:%.*s' for %s",
+	  (int)strcspn(mod, ":)}"), mod, st.v->name);
 
 cleanup:
-    *st.lengthPtr = st.cp - st.start;
-    if (st.delim != '\0')
-	Error("Unclosed substitution for %s (%c missing)",
-	      st.v->name, st.delim);
-    free(*st.freePtr);
-    *st.freePtr = NULL;
+    *pp = p;
+    if (st.missing_delim != '\0')
+	Error("Unfinished modifier for %s ('%c' missing)",
+	      st.v->name, st.missing_delim);
+    free(*freePtr);
+    *freePtr = NULL;
     return var_Error;
 }
 
+static Boolean
+VarIsDynamic(GNode *ctxt, const char *varname, size_t namelen)
+{
+    if ((namelen == 1 ||
+	 (namelen == 2 && (varname[1] == 'F' || varname[1] == 'D'))) &&
+	(ctxt == VAR_CMD || ctxt == VAR_GLOBAL))
+    {
+	/*
+	 * If substituting a local variable in a non-local context,
+	 * assume it's for dynamic source stuff. We have to handle
+	 * this specially and return the longhand for the variable
+	 * with the dollar sign escaped so it makes it back to the
+	 * caller. Only four of the local variables are treated
+	 * specially as they are the only four that will be set
+	 * when dynamic sources are expanded.
+	 */
+	switch (varname[0]) {
+	case '@':
+	case '%':
+	case '*':
+	case '!':
+	    return TRUE;
+	}
+	return FALSE;
+    }
+
+    if ((namelen == 7 || namelen == 8) && varname[0] == '.' &&
+	isupper((unsigned char)varname[1]) &&
+	(ctxt == VAR_CMD || ctxt == VAR_GLOBAL))
+    {
+	return strcmp(varname, ".TARGET") == 0 ||
+	       strcmp(varname, ".ARCHIVE") == 0 ||
+	       strcmp(varname, ".PREFIX") == 0 ||
+	       strcmp(varname, ".MEMBER") == 0;
+    }
+
+    return FALSE;
+}
+
 /*-
  *-----------------------------------------------------------------------
  * Var_Parse --
- *	Given the start of a variable invocation, extract the variable
- *	name and find its value, then modify it according to the
- *	specification.
+ *	Given the start of a variable invocation (such as $v, $(VAR),
+ *	${VAR:Mpattern}), extract the variable name, possibly some
+ *	modifiers and find its value by applying the modifiers to the
+ *	original value.
  *
  * Input:
  *	str		The string to parse
  *	ctxt		The context for the variable
- *	flags		VARF_UNDEFERR	if undefineds are an error
- *			VARF_WANTRES	if we actually want the result
- *			VARF_ASSIGN	if we are in a := assignment
+ *	flags		VARE_UNDEFERR	if undefineds are an error
+ *			VARE_WANTRES	if we actually want the result
+ *			VARE_ASSIGN	if we are in a := assignment
  *	lengthPtr	OUT: The length of the specification
  *	freePtr		OUT: Non-NULL if caller should free *freePtr
  *
  * Results:
- *	The (possibly-modified) value of the variable or var_Error if the
- *	specification is invalid. The length of the specification is
- *	placed in *lengthPtr (for invalid specifications, this is just
- *	2...?).
- *	If *freePtr is non-NULL then it's a pointer that the caller
- *	should pass to free() to free memory used by the result.
+ *	Returns the value of the variable expression, never NULL.
+ *	var_Error if there was a parse error and VARE_UNDEFERR was set.
+ *	varNoError if there was a parse error and VARE_UNDEFERR was not set.
+ *
+ *	Parsing should continue at str + *lengthPtr.
+ *
+ *	After using the returned value, *freePtr must be freed, preferably
+ *	using bmake_free since it is NULL in most cases.
  *
  * Side Effects:
- *	None.
- *
+ *	Any effects from the modifiers, such as :!cmd! or ::=value.
  *-----------------------------------------------------------------------
  */
 /* coverity[+alloc : arg-*4] */
-char *
-Var_Parse(const char *str, GNode *ctxt, Varf_Flags flags,
+const char *
+Var_Parse(const char * const str, GNode *ctxt, VarEvalFlags eflags,
 	  int *lengthPtr, void **freePtr)
 {
     const char	*tstr;		/* Pointer into str */
-    Var		*v;		/* Variable in invocation */
     Boolean 	 haveModifier;	/* TRUE if have modifiers for the variable */
-    char	 endc;		/* Ending character when variable in parens
+    char	 startc;	/* Starting character if variable in parens
 				 * or braces */
-    char	 startc;	/* Starting character when variable in parens
+    char	 endc;		/* Ending character if variable in parens
 				 * or braces */
-    int		 vlen;		/* Length of variable name */
-    const char 	*start;		/* Points to original start of str */
-    char	*nstr;		/* New string, used during expansion */
     Boolean	 dynamic;	/* TRUE if the variable is local and we're
 				 * expanding it in a non-local context. This
 				 * is done to support dynamic sources. The
 				 * result is just the invocation, unaltered */
-    const char	*extramodifiers; /* extra modifiers to apply first */
-    char	 name[2];
+    const char *extramodifiers;
+    Var *v;
+    char *nstr;
+    char eflags_str[VarEvalFlags_ToStringSize];
+
+    VAR_DEBUG("%s: %s with %s\n", __func__, str,
+	      Enum_FlagsToString(eflags_str, sizeof eflags_str, eflags,
+				 VarEvalFlags_ToStringSpecs));
 
     *freePtr = NULL;
-    extramodifiers = NULL;
+    extramodifiers = NULL;	/* extra modifiers to apply first */
     dynamic = FALSE;
-    start = str;
+
+#ifdef USE_DOUBLE_BOOLEAN
+    /* Appease GCC 5.5.0, which thinks that the variable might not be
+     * initialized. */
+    endc = '\0';
+#endif
 
     startc = str[1];
     if (startc != PROPEN && startc != BROPEN) {
+	char name[2];
+
 	/*
 	 * If it's not bounded by braces of some sort, life is much simpler.
 	 * We just need to check for the first character and return the
@@ -3650,14 +3388,14 @@ Var_Parse(const char *str, GNode *ctxt, Varf_Flags flags,
 	    *lengthPtr = 1;
 	    return var_Error;
 	}
+
 	name[0] = startc;
 	name[1] = '\0';
-
 	v = VarFind(name, ctxt, FIND_ENV | FIND_GLOBAL | FIND_CMD);
 	if (v == NULL) {
 	    *lengthPtr = 2;
 
-	    if ((ctxt == VAR_CMD) || (ctxt == VAR_GLOBAL)) {
+	    if (ctxt == VAR_CMD || ctxt == VAR_GLOBAL) {
 		/*
 		 * If substituting a local variable in a non-local context,
 		 * assume it's for dynamic source stuff. We have to handle
@@ -3669,31 +3407,34 @@ Var_Parse(const char *str, GNode *ctxt, Varf_Flags flags,
 		 */
 		switch (str[1]) {
 		case '@':
-		    return UNCONST("$(.TARGET)");
+		    return "$(.TARGET)";
 		case '%':
-		    return UNCONST("$(.MEMBER)");
+		    return "$(.MEMBER)";
 		case '*':
-		    return UNCONST("$(.PREFIX)");
+		    return "$(.PREFIX)";
 		case '!':
-		    return UNCONST("$(.ARCHIVE)");
+		    return "$(.ARCHIVE)";
 		}
 	    }
-	    return (flags & VARF_UNDEFERR) ? var_Error : varNoError;
+	    return (eflags & VARE_UNDEFERR) ? var_Error : varNoError;
 	} else {
 	    haveModifier = FALSE;
-	    tstr = &str[1];
-	    endc = str[1];
+	    tstr = str + 1;
 	}
     } else {
-	Buffer buf;		/* Holds the variable name */
-	int depth = 1;
+	Buffer namebuf;		/* Holds the variable name */
+	int depth;
+	size_t namelen;
+	char *varname;
 
 	endc = startc == PROPEN ? PRCLOSE : BRCLOSE;
-	Buf_Init(&buf, 0);
+
+	Buf_Init(&namebuf, 0);
 
 	/*
 	 * Skip to the end character or a colon, whichever comes first.
 	 */
+	depth = 1;
 	for (tstr = str + 2; *tstr != '\0'; tstr++) {
 	    /* Track depth so we can spot parse errors. */
 	    if (*tstr == startc)
@@ -3702,39 +3443,42 @@ Var_Parse(const char *str, GNode *ctxt, Varf_Flags flags,
 		if (--depth == 0)
 		    break;
 	    }
-	    if (depth == 1 && *tstr == ':')
+	    if (*tstr == ':' && depth == 1)
 		break;
 	    /* A variable inside a variable, expand. */
 	    if (*tstr == '$') {
 		int rlen;
 		void *freeIt;
-		char *rval = Var_Parse(tstr, ctxt, flags, &rlen, &freeIt);
-		if (rval != NULL)
-		    Buf_AddBytes(&buf, strlen(rval), rval);
+		const char *rval = Var_Parse(tstr, ctxt, eflags, &rlen,
+					     &freeIt);
+		Buf_AddStr(&namebuf, rval);
 		free(freeIt);
 		tstr += rlen - 1;
 	    } else
-		Buf_AddByte(&buf, *tstr);
+		Buf_AddByte(&namebuf, *tstr);
 	}
 	if (*tstr == ':') {
 	    haveModifier = TRUE;
 	} else if (*tstr == endc) {
 	    haveModifier = FALSE;
 	} else {
+	    Parse_Error(PARSE_FATAL, "Unclosed variable \"%s\"",
+			Buf_GetAll(&namebuf, NULL));
 	    /*
 	     * If we never did find the end character, return NULL
 	     * right now, setting the length to be the distance to
 	     * the end of the string, since that's what make does.
 	     */
-	    *lengthPtr = tstr - str;
-	    Buf_Destroy(&buf, TRUE);
+	    *lengthPtr = (int)(size_t)(tstr - str);
+	    Buf_Destroy(&namebuf, TRUE);
 	    return var_Error;
 	}
-	str = Buf_GetAll(&buf, &vlen);
+
+	varname = Buf_GetAll(&namebuf, &namelen);
 
 	/*
-	 * At this point, str points into newly allocated memory from
-	 * buf, containing only the name of the variable.
+	 * At this point, varname points into newly allocated memory from
+	 * namebuf, containing only the name of the variable.
 	 *
 	 * start and tstr point into the const string that was pointed
 	 * to by the original value of the str parameter.  start points
@@ -3743,23 +3487,23 @@ Var_Parse(const char *str, GNode *ctxt, Varf_Flags flags,
 	 * will be '\0', ':', PRCLOSE, or BRCLOSE.
 	 */
 
-	v = VarFind(str, ctxt, FIND_ENV | FIND_GLOBAL | FIND_CMD);
+	v = VarFind(varname, ctxt, FIND_ENV | FIND_GLOBAL | FIND_CMD);
 	/*
 	 * Check also for bogus D and F forms of local variables since we're
 	 * in a local context and the name is the right length.
 	 */
-	if ((v == NULL) && (ctxt != VAR_CMD) && (ctxt != VAR_GLOBAL) &&
-		(vlen == 2) && (str[1] == 'F' || str[1] == 'D') &&
-		strchr("@%?*!<>", str[0]) != NULL) {
+	if (v == NULL && ctxt != VAR_CMD && ctxt != VAR_GLOBAL &&
+	    namelen == 2 && (varname[1] == 'F' || varname[1] == 'D') &&
+	    strchr("@%?*!<>", varname[0]) != NULL)
+	{
 	    /*
 	     * Well, it's local -- go look for it.
 	     */
-	    name[0] = *str;
-	    name[1] = '\0';
+	    char name[] = { varname[0], '\0' };
 	    v = VarFind(name, ctxt, 0);
 
 	    if (v != NULL) {
-		if (str[1] == 'D') {
+		if (varname[1] == 'D') {
 		    extramodifiers = "H:";
 		} else { /* F */
 		    extramodifiers = "T:";
@@ -3768,55 +3512,22 @@ Var_Parse(const char *str, GNode *ctxt, Varf_Flags flags,
 	}
 
 	if (v == NULL) {
-	    if (((vlen == 1) ||
-		 (((vlen == 2) && (str[1] == 'F' || str[1] == 'D')))) &&
-		((ctxt == VAR_CMD) || (ctxt == VAR_GLOBAL)))
-	    {
-		/*
-		 * If substituting a local variable in a non-local context,
-		 * assume it's for dynamic source stuff. We have to handle
-		 * this specially and return the longhand for the variable
-		 * with the dollar sign escaped so it makes it back to the
-		 * caller. Only four of the local variables are treated
-		 * specially as they are the only four that will be set
-		 * when dynamic sources are expanded.
-		 */
-		switch (*str) {
-		case '@':
-		case '%':
-		case '*':
-		case '!':
-		    dynamic = TRUE;
-		    break;
-		}
-	    } else if (vlen > 2 && *str == '.' &&
-		       isupper((unsigned char) str[1]) &&
-		       (ctxt == VAR_CMD || ctxt == VAR_GLOBAL))
-	    {
-		int len = vlen - 1;
-		if ((strncmp(str, ".TARGET", len) == 0) ||
-		    (strncmp(str, ".ARCHIVE", len) == 0) ||
-		    (strncmp(str, ".PREFIX", len) == 0) ||
-		    (strncmp(str, ".MEMBER", len) == 0))
-		{
-		    dynamic = TRUE;
-		}
-	    }
+	    dynamic = VarIsDynamic(ctxt, varname, namelen);
 
 	    if (!haveModifier) {
 		/*
 		 * No modifiers -- have specification length so we can return
 		 * now.
 		 */
-		*lengthPtr = tstr - start + 1;
+		*lengthPtr = (int)(size_t)(tstr - str) + 1;
 		if (dynamic) {
-		    char *pstr = bmake_strndup(start, *lengthPtr);
+		    char *pstr = bmake_strldup(str, (size_t)*lengthPtr);
 		    *freePtr = pstr;
-		    Buf_Destroy(&buf, TRUE);
+		    Buf_Destroy(&namebuf, TRUE);
 		    return pstr;
 		} else {
-		    Buf_Destroy(&buf, TRUE);
-		    return (flags & VARF_UNDEFERR) ? var_Error : varNoError;
+		    Buf_Destroy(&namebuf, TRUE);
+		    return (eflags & VARE_UNDEFERR) ? var_Error : varNoError;
 		}
 	    } else {
 		/*
@@ -3824,13 +3535,13 @@ Var_Parse(const char *str, GNode *ctxt, Varf_Flags flags,
 		 * so kludge up a Var structure for the modifications
 		 */
 		v = bmake_malloc(sizeof(Var));
-		v->name = UNCONST(str);
+		v->name = varname;
 		Buf_Init(&v->val, 1);
 		v->flags = VAR_JUNK;
-		Buf_Destroy(&buf, FALSE);
+		Buf_Destroy(&namebuf, FALSE);
 	    }
 	} else
-	    Buf_Destroy(&buf, TRUE);
+	    Buf_Destroy(&namebuf, TRUE);
     }
 
     if (v->flags & VAR_IN_USE) {
@@ -3839,6 +3550,7 @@ Var_Parse(const char *str, GNode *ctxt, Varf_Flags flags,
     } else {
 	v->flags |= VAR_IN_USE;
     }
+
     /*
      * Before doing any modification, we have to make sure the value
      * has been fully expanded. If it looks like recursion might be
@@ -3849,66 +3561,64 @@ Var_Parse(const char *str, GNode *ctxt, Varf_Flags flags,
      * return.
      */
     nstr = Buf_GetAll(&v->val, NULL);
-    if (strchr(nstr, '$') != NULL && (flags & VARF_WANTRES) != 0) {
-	nstr = Var_Subst(NULL, nstr, ctxt, flags);
+    if (strchr(nstr, '$') != NULL && (eflags & VARE_WANTRES) != 0) {
+	nstr = Var_Subst(nstr, ctxt, eflags);
 	*freePtr = nstr;
     }
 
-    v->flags &= ~VAR_IN_USE;
+    v->flags &= ~(unsigned)VAR_IN_USE;
 
-    if (nstr != NULL && (haveModifier || extramodifiers != NULL)) {
+    if (haveModifier || extramodifiers != NULL) {
 	void *extraFree;
-	int used;
 
 	extraFree = NULL;
 	if (extramodifiers != NULL) {
-	    nstr = ApplyModifiers(nstr, extramodifiers, '(', ')',
-				  v, ctxt, flags, &used, &extraFree);
+	    const char *em = extramodifiers;
+	    nstr = ApplyModifiers(&em, nstr, '(', ')',
+				  v, ctxt, eflags, &extraFree);
 	}
 
 	if (haveModifier) {
 	    /* Skip initial colon. */
 	    tstr++;
 
-	    nstr = ApplyModifiers(nstr, tstr, startc, endc,
-				  v, ctxt, flags, &used, freePtr);
-	    tstr += used;
+	    nstr = ApplyModifiers(&tstr, nstr, startc, endc,
+				  v, ctxt, eflags, freePtr);
 	    free(extraFree);
 	} else {
 	    *freePtr = extraFree;
 	}
     }
-    *lengthPtr = tstr - start + (*tstr ? 1 : 0);
+
+    /* Skip past endc if possible. */
+    *lengthPtr = (int)(size_t)(tstr + (*tstr ? 1 : 0) - str);
 
     if (v->flags & VAR_FROM_ENV) {
-	Boolean destroy = FALSE;
-
-	if (nstr != Buf_GetAll(&v->val, NULL)) {
-	    destroy = TRUE;
-	} else {
+	Boolean destroy = nstr != Buf_GetAll(&v->val, NULL);
+	if (!destroy) {
 	    /*
 	     * Returning the value unmodified, so tell the caller to free
 	     * the thing.
 	     */
 	    *freePtr = nstr;
 	}
-	VarFreeEnv(v, destroy);
+	(void)VarFreeEnv(v, destroy);
     } else if (v->flags & VAR_JUNK) {
 	/*
-	 * Perform any free'ing needed and set *freePtr to NULL so the caller
+	 * Perform any freeing needed and set *freePtr to NULL so the caller
 	 * doesn't try to free a static pointer.
-	 * If VAR_KEEP is also set then we want to keep str as is.
+	 * If VAR_KEEP is also set then we want to keep str(?) as is.
 	 */
 	if (!(v->flags & VAR_KEEP)) {
-	    if (*freePtr) {
-		free(nstr);
+	    if (*freePtr != NULL) {
+		free(*freePtr);
 		*freePtr = NULL;
 	    }
 	    if (dynamic) {
-		nstr = bmake_strndup(start, *lengthPtr);
+		nstr = bmake_strldup(str, (size_t)*lengthPtr);
 		*freePtr = nstr;
 	    } else {
-		nstr = (flags & VARF_UNDEFERR) ? var_Error : varNoError;
+		nstr = (eflags & VARE_UNDEFERR) ? var_Error : varNoError;
 	    }
 	}
 	if (nstr != Buf_GetAll(&v->val, NULL))
@@ -3919,58 +3629,52 @@ Var_Parse(const char *str, GNode *ctxt, Varf_Flags flags,
     return nstr;
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Var_Subst  --
- *	Substitute for all variables in the given string in the given context.
- *	If flags & VARF_UNDEFERR, Parse_Error will be called when an undefined
- *	variable is encountered.
+/* Substitute for all variables in the given string in the given context.
+ *
+ * If eflags & VARE_UNDEFERR, Parse_Error will be called when an undefined
+ * variable is encountered.
+ *
+ * If eflags & VARE_WANTRES, any effects from the modifiers, such as ::=,
+ * :sh or !cmd! take place.
  *
  * Input:
- *	var		Named variable || NULL for all
  *	str		the string which to substitute
  *	ctxt		the context wherein to find variables
- *	flags		VARF_UNDEFERR	if undefineds are an error
- *			VARF_WANTRES	if we actually want the result
- *			VARF_ASSIGN	if we are in a := assignment
+ *	eflags		VARE_UNDEFERR	if undefineds are an error
+ *			VARE_WANTRES	if we actually want the result
+ *			VARE_ASSIGN	if we are in a := assignment
  *
  * Results:
  *	The resulting string.
- *
- * Side Effects:
- *	None.
- *-----------------------------------------------------------------------
  */
 char *
-Var_Subst(const char *var, const char *str, GNode *ctxt, Varf_Flags flags)
+Var_Subst(const char *str, GNode *ctxt, VarEvalFlags eflags)
 {
-    Buffer	buf;		/* Buffer for forming things */
-    char	*val;		/* Value to substitute for a variable */
-    int		length;		/* Length of the variable invocation */
-    Boolean	trailingBslash;	/* variable ends in \ */
-    void	*freeIt = NULL;	/* Set if it should be freed */
-    static Boolean errorReported; /* Set true if an error has already
-				 * been reported to prevent a plethora
-				 * of messages when recursing */
+    Buffer buf;			/* Buffer for forming things */
+    Boolean trailingBslash;
+
+    /* Set true if an error has already been reported,
+     * to prevent a plethora of messages when recursing */
+    static Boolean errorReported;
 
     Buf_Init(&buf, 0);
     errorReported = FALSE;
-    trailingBslash = FALSE;
+    trailingBslash = FALSE;	/* variable ends in \ */
 
     while (*str) {
 	if (*str == '\n' && trailingBslash)
 	    Buf_AddByte(&buf, ' ');
-	if (var == NULL && (*str == '$') && (str[1] == '$')) {
+
+	if (*str == '$' && str[1] == '$') {
 	    /*
-	     * A dollar sign may be escaped either with another dollar sign.
+	     * A dollar sign may be escaped with another dollar sign.
 	     * In such a case, we skip over the escape character and store the
 	     * dollar sign into the buffer directly.
 	     */
-	    if (save_dollars && (flags & VARF_ASSIGN))
-		Buf_AddByte(&buf, *str);
-	    str++;
-	    Buf_AddByte(&buf, *str);
-	    str++;
+	    if (save_dollars && (eflags & VARE_ASSIGN))
+		Buf_AddByte(&buf, '$');
+	    Buf_AddByte(&buf, '$');
+	    str += 2;
 	} else if (*str != '$') {
 	    /*
 	     * Skip as many characters as possible -- either to the end of
@@ -3980,71 +3684,12 @@ Var_Subst(const char *var, const char *str, GNode *ctxt, Varf_Flags flags)
 
 	    for (cp = str++; *str != '$' && *str != '\0'; str++)
 		continue;
-	    Buf_AddBytes(&buf, str - cp, cp);
+	    Buf_AddBytesBetween(&buf, cp, str);
 	} else {
-	    if (var != NULL) {
-		int expand;
-		for (;;) {
-		    if (str[1] == '\0') {
-			/* A trailing $ is kind of a special case */
-			Buf_AddByte(&buf, str[0]);
-			str++;
-			expand = FALSE;
-		    } else if (str[1] != PROPEN && str[1] != BROPEN) {
-			if (str[1] != *var || strlen(var) > 1) {
-			    Buf_AddBytes(&buf, 2, str);
-			    str += 2;
-			    expand = FALSE;
-			} else
-			    expand = TRUE;
-			break;
-		    } else {
-			const char *p;
+	    int length;
+	    void *freeIt;
+	    const char *val = Var_Parse(str, ctxt, eflags, &length, &freeIt);
 
-			/* Scan up to the end of the variable name. */
-			for (p = &str[2]; *p &&
-			     *p != ':' && *p != PRCLOSE && *p != BRCLOSE; p++)
-			    if (*p == '$')
-				break;
-			/*
-			 * A variable inside the variable. We cannot expand
-			 * the external variable yet, so we try again with
-			 * the nested one
-			 */
-			if (*p == '$') {
-			    Buf_AddBytes(&buf, p - str, str);
-			    str = p;
-			    continue;
-			}
-
-			if (strncmp(var, str + 2, p - str - 2) != 0 ||
-			    var[p - str - 2] != '\0') {
-			    /*
-			     * Not the variable we want to expand, scan
-			     * until the next variable
-			     */
-			    for (; *p != '$' && *p != '\0'; p++)
-				continue;
-			    Buf_AddBytes(&buf, p - str, str);
-			    str = p;
-			    expand = FALSE;
-			} else
-			    expand = TRUE;
-			break;
-		    }
-		}
-		if (!expand)
-		    continue;
-	    }
-
-	    val = Var_Parse(str, ctxt, flags, &length, &freeIt);
-
-	    /*
-	     * When we come down here, val should either point to the
-	     * value of this variable, suitably modified, or be NULL.
-	     * Length should be the total length of the potential
-	     * variable invocation (from $ to end character...)
-	     */
 	    if (val == var_Error || val == varNoError) {
 		/*
 		 * If performing old-time variable substitution, skip over
@@ -4054,7 +3699,7 @@ Var_Subst(const char *var, const char *str, GNode *ctxt, Varf_Flags flags)
 		 */
 		if (oldVars) {
 		    str += length;
-		} else if ((flags & VARF_UNDEFERR) || val == var_Error) {
+		} else if ((eflags & VARE_UNDEFERR) || val == var_Error) {
 		    /*
 		     * If variable is undefined, complain and skip the
 		     * variable. The complaint will stop us from doing anything
@@ -4071,19 +3716,13 @@ Var_Subst(const char *var, const char *str, GNode *ctxt, Varf_Flags flags)
 		    str += 1;
 		}
 	    } else {
-		/*
-		 * We've now got a variable structure to store in. But first,
-		 * advance the string pointer.
-		 */
+		size_t val_len;
+
 		str += length;
 
-		/*
-		 * Copy all the characters from the variable value straight
-		 * into the new string.
-		 */
-		length = strlen(val);
-		Buf_AddBytes(&buf, length, val);
-		trailingBslash = length > 0 && val[length - 1] == '\\';
+		val_len = strlen(val);
+		Buf_AddBytes(&buf, val, val_len);
+		trailingBslash = val_len > 0 && val[val_len - 1] == '\\';
 	    }
 	    free(freeIt);
 	    freeIt = NULL;
@@ -4106,6 +3745,13 @@ Var_Init(void)
 void
 Var_End(void)
 {
+    Var_Stats();
+}
+
+void
+Var_Stats(void)
+{
+    Hash_DebugStats(&VAR_GLOBAL->context, "VAR_GLOBAL");
 }
 
 
@@ -4117,12 +3763,7 @@ VarPrintVar(void *vp, void *data MAKE_ATTR_UNUSED)
     fprintf(debug_file, "%-16s = %s\n", v->name, Buf_GetAll(&v->val, NULL));
 }
 
-/*-
- *-----------------------------------------------------------------------
- * Var_Dump --
- *	print all variables in a context
- *-----------------------------------------------------------------------
- */
+/* Print all variables in a context, unordered. */
 void
 Var_Dump(GNode *ctxt)
 {