summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorkx <kx@radix-linux.su>2026-10-01 12:02:28 +0300
committerkx <kx@radix-linux.su>2026-10-01 12:02:28 +0300
commitba1b04d64bdfaf377915b22f77520216ebe47674 (patch)
tree044343035c28b2b04ca9725f26e9b7c39342d19c /tests
parent124b140798456778e2a96851cc7268aa9a726698 (diff)
downloadmcpu-cpp-1.0.2.tar.xz
Version 1.0.21.0.2
Diffstat (limited to 'tests')
-rw-r--r--tests/Makefile.am101
-rw-r--r--tests/data/base-path-main.c1
-rw-r--r--tests/data/common-precedence/same.h1
-rw-r--r--tests/data/config-main.c2
-rw-r--r--tests/data/config.conf2
-rw-r--r--tests/data/include-main.c6
-rw-r--r--tests/data/include/bridge.h4
-rw-r--r--tests/data/include/one.h1
-rw-r--r--tests/data/lang-as-precedence/same.h1
-rw-r--r--tests/data/lang-as/lang.h1
-rw-r--r--tests/data/lang-base/base.h1
-rw-r--r--tests/data/lang-base/diff/explicit.h1
-rw-r--r--tests/data/lang-diff/lang.h1
-rw-r--r--tests/data/lang-path-main.c6
-rw-r--r--tests/data/lang.c9
-rw-r--r--tests/data/nonucs2.c1
-rw-r--r--tests/data/order-main.c1
-rw-r--r--tests/data/system/order.h1
-rw-r--r--tests/data/system/system.h1
-rw-r--r--tests/data/unbalanced.c2
-rw-r--r--tests/data/user/order.h1
-rw-r--r--tests/data/utf8.c2
-rw-r--r--tests/mcpp-options-test.c24
-rwxr-xr-xtests/t0001-utf8.sh6
-rwxr-xr-xtests/t0002-lang.sh8
-rwxr-xr-xtests/t0003-include.sh11
-rwxr-xr-xtests/t0004-config.sh12
-rwxr-xr-xtests/t0005-errors.sh22
-rwxr-xr-xtests/t0006-path-order.sh13
-rwxr-xr-xtests/t0007-language-path.sh12
-rwxr-xr-xtests/t0008-comments.sh41
-rwxr-xr-xtests/t0009-text-scanner.sh37
-rwxr-xr-xtests/t0010-language-names.sh38
-rwxr-xr-xtests/t0011-base-include-path.sh19
-rwxr-xr-xtests/t0012-user-config-path.sh26
-rwxr-xr-xtests/t0013-include-precedence.sh18
-rwxr-xr-xtests/t0014-preprocessing-phases.sh21
-rwxr-xr-xtests/t0015-object-macros.sh27
-rwxr-xr-xtests/t0016-macro-include.sh16
-rwxr-xr-xtests/t0017-macro-errors.sh13
-rwxr-xr-xtests/t0018-include-lexing.sh17
-rwxr-xr-xtests/t0019-recursive-macro.sh13
-rwxr-xr-xtests/t0020-function-macros.sh42
-rwxr-xr-xtests/t0021-function-macro-errors.sh89
-rwxr-xr-xtests/t0022-predefined-macros.sh56
-rwxr-xr-xtests/t0023-predefined-redefine.sh20
-rwxr-xr-xtests/t0024-abi-predefined.sh191
-rwxr-xr-xtests/t0025-dump-macros.sh99
-rwxr-xr-xtests/t0026-dump-config.sh22
-rwxr-xr-xtests/t0027-system-language-path.sh44
-rwxr-xr-xtests/t0028-predefined-ranges.sh95
-rwxr-xr-xtests/t0029-predefined-abi-names.sh38
-rwxr-xr-xtests/t0030-integer-decimal-digits.sh33
-rwxr-xr-xtests/t0031-stringification.sh50
-rwxr-xr-xtests/t0032-command-line.sh119
-rwxr-xr-xtests/t0033-dump-definitions.sh51
-rwxr-xr-xtests/t0034-conditionals.sh136
-rwxr-xr-xtests/t0035-line-control.sh41
-rwxr-xr-xtests/t0036-lang-string.sh120
-rwxr-xr-xtests/t0037-token-concatenation.sh122
-rwxr-xr-xtests/t0038-ucs2-identifiers.sh122
-rwxr-xr-xtests/t0039-command-line-macros.sh174
-rwxr-xr-xtests/t0040-zubr-expression.sh79
-rwxr-xr-xtests/t0041-integer-width-suffix.sh95
-rwxr-xr-xtests/t0042-diagnostics.sh68
-rwxr-xr-xtests/t0043-include-next.sh87
-rwxr-xr-xtests/t0044-configured-search-order.sh114
-rwxr-xr-xtests/t0045-search-dirs-verbose.sh89
-rwxr-xr-xtests/t0046-pragma-once.sh67
-rwxr-xr-xtests/t0047-dependencies.sh140
-rwxr-xr-xtests/t0048-no-config-defaults.sh73
-rwxr-xr-xtests/t0049-relocatable-root.sh55
-rwxr-xr-xtests/t0050-dependency-side-effects.sh136
-rwxr-xr-xtests/t0051-dump-macro-origin.sh63
-rwxr-xr-xtests/t0052-warning-control.sh108
-rwxr-xr-xtests/t0053-interface-cleanup.sh34
-rwxr-xr-xtests/t0054-inhibit-warnings.sh81
-rwxr-xr-xtests/t0055-forced-files.sh242
-rwxr-xr-xtests/t0056-dependency-targets.sh103
-rwxr-xr-xtests/t0057-missing-generated-dependencies.sh166
-rwxr-xr-xtests/t0058-variadic-macros.sh135
-rwxr-xr-xtests/t0059-va-opt.sh150
-rwxr-xr-xtests/t0060-macro-whitespace.sh72
-rwxr-xr-xtests/t0061-output-line-compaction.sh131
84 files changed, 4493 insertions, 0 deletions
diff --git a/tests/Makefile.am b/tests/Makefile.am
new file mode 100644
index 0000000..ade9bae
--- /dev/null
+++ b/tests/Makefile.am
@@ -0,0 +1,101 @@
+
+TESTS = \
+ t0001-utf8.sh \
+ t0002-lang.sh \
+ t0003-include.sh \
+ t0004-config.sh \
+ t0005-errors.sh \
+ t0006-path-order.sh \
+ t0007-language-path.sh \
+ t0008-comments.sh \
+ t0009-text-scanner.sh \
+ t0010-language-names.sh \
+ t0011-base-include-path.sh \
+ t0012-user-config-path.sh \
+ t0013-include-precedence.sh \
+ t0014-preprocessing-phases.sh \
+ t0015-object-macros.sh \
+ t0016-macro-include.sh \
+ t0017-macro-errors.sh \
+ t0018-include-lexing.sh \
+ t0019-recursive-macro.sh \
+ t0020-function-macros.sh \
+ t0021-function-macro-errors.sh \
+ t0022-predefined-macros.sh \
+ t0023-predefined-redefine.sh \
+ t0024-abi-predefined.sh \
+ t0025-dump-macros.sh \
+ t0026-dump-config.sh \
+ t0027-system-language-path.sh \
+ t0028-predefined-ranges.sh \
+ t0029-predefined-abi-names.sh \
+ t0030-integer-decimal-digits.sh \
+ t0031-stringification.sh \
+ t0032-command-line.sh \
+ t0033-dump-definitions.sh \
+ t0034-conditionals.sh \
+ t0035-line-control.sh \
+ t0036-lang-string.sh \
+ t0037-token-concatenation.sh \
+ t0038-ucs2-identifiers.sh \
+ t0039-command-line-macros.sh \
+ t0040-zubr-expression.sh \
+ t0041-integer-width-suffix.sh \
+ t0042-diagnostics.sh \
+ t0043-include-next.sh \
+ t0044-configured-search-order.sh \
+ t0045-search-dirs-verbose.sh \
+ t0046-pragma-once.sh \
+ t0047-dependencies.sh \
+ t0048-no-config-defaults.sh \
+ t0049-relocatable-root.sh \
+ t0050-dependency-side-effects.sh \
+ t0051-dump-macro-origin.sh \
+ t0052-warning-control.sh \
+ t0053-interface-cleanup.sh \
+ t0054-inhibit-warnings.sh \
+ t0055-forced-files.sh \
+ t0056-dependency-targets.sh \
+ t0057-missing-generated-dependencies.sh \
+ t0058-variadic-macros.sh \
+ t0059-va-opt.sh \
+ t0060-macro-whitespace.sh \
+ t0061-output-line-compaction.sh
+
+noinst_PROGRAMS = mcpp-options-test
+
+mcpp_options_test_SOURCES = \
+ mcpp-options-test.c \
+ ../src/mcpp-options.c
+
+mcpp_options_test_CPPFLAGS = -I$(top_srcdir)/src $(LIBMPUIO_CFLAGS)
+mcpp_options_test_LDADD = $(LIBMPUIO_LIBS)
+
+EXTRA_DIST = $(TESTS) \
+ data/utf8.c \
+ data/lang.c \
+ data/include-main.c \
+ data/include/one.h \
+ data/include/bridge.h \
+ data/system/system.h \
+ data/config.conf \
+ data/config-main.c \
+ data/unbalanced.c \
+ data/nonucs2.c \
+ data/order-main.c \
+ data/user/order.h \
+ data/system/order.h \
+ data/lang-path-main.c \
+ data/lang-as/lang.h \
+ data/lang-diff/lang.h \
+ data/base-path-main.c \
+ data/lang-base/base.h \
+ data/lang-base/diff/explicit.h \
+ data/common-precedence/same.h \
+ data/lang-as-precedence/same.h
+
+TESTS_ENVIRONMENT = MCPU_CPP='$(abs_top_builddir)/src/mcpu-cpp'; export MCPU_CPP; \
+ MCPP_OPTIONS_TEST='$(abs_top_builddir)/tests/mcpp-options-test'; export MCPP_OPTIONS_TEST;
+
+distclean-local:
+ -rm -rf $(DEPDIR)
diff --git a/tests/data/base-path-main.c b/tests/data/base-path-main.c
new file mode 100644
index 0000000..6ca1ffb
--- /dev/null
+++ b/tests/data/base-path-main.c
@@ -0,0 +1 @@
+#include <base.h>
diff --git a/tests/data/common-precedence/same.h b/tests/data/common-precedence/same.h
new file mode 100644
index 0000000..8a7b2c8
--- /dev/null
+++ b/tests/data/common-precedence/same.h
@@ -0,0 +1 @@
+int from_common_include_root;
diff --git a/tests/data/config-main.c b/tests/data/config-main.c
new file mode 100644
index 0000000..892eb5e
--- /dev/null
+++ b/tests/data/config-main.c
@@ -0,0 +1,2 @@
+#include "one.h"
+#include <system.h>
diff --git a/tests/data/config.conf b/tests/data/config.conf
new file mode 100644
index 0000000..e395802
--- /dev/null
+++ b/tests/data/config.conf
@@ -0,0 +1,2 @@
+MCPU_CPP_INCLUDE_PATH = ./include;
+MCPU_CPP_SYSTEM_INCLUDE_PATH = ./system;
diff --git a/tests/data/include-main.c b/tests/data/include-main.c
new file mode 100644
index 0000000..93bca59
--- /dev/null
+++ b/tests/data/include-main.c
@@ -0,0 +1,6 @@
+#include "one.h"
+#include <system.h>
+#lang "diff"
+#include "bridge.h"
+y' = 1;
+#endlang
diff --git a/tests/data/include/bridge.h b/tests/data/include/bridge.h
new file mode 100644
index 0000000..a878df1
--- /dev/null
+++ b/tests/data/include/bridge.h
@@ -0,0 +1,4 @@
+/* Entered while current language is diff. */
+#endlang
+int bridge_c;
+#lang "diff"
diff --git a/tests/data/include/one.h b/tests/data/include/one.h
new file mode 100644
index 0000000..6200094
--- /dev/null
+++ b/tests/data/include/one.h
@@ -0,0 +1 @@
+int from_one;
diff --git a/tests/data/lang-as-precedence/same.h b/tests/data/lang-as-precedence/same.h
new file mode 100644
index 0000000..f139324
--- /dev/null
+++ b/tests/data/lang-as-precedence/same.h
@@ -0,0 +1 @@
+int from_as_specific_include_path;
diff --git a/tests/data/lang-as/lang.h b/tests/data/lang-as/lang.h
new file mode 100644
index 0000000..f1f9283
--- /dev/null
+++ b/tests/data/lang-as/lang.h
@@ -0,0 +1 @@
+MCPU_AS_LANGUAGE_PATH from_as_language_path;
diff --git a/tests/data/lang-base/base.h b/tests/data/lang-base/base.h
new file mode 100644
index 0000000..1d8a123
--- /dev/null
+++ b/tests/data/lang-base/base.h
@@ -0,0 +1 @@
+int from_base_language_include_root;
diff --git a/tests/data/lang-base/diff/explicit.h b/tests/data/lang-base/diff/explicit.h
new file mode 100644
index 0000000..fa9157f
--- /dev/null
+++ b/tests/data/lang-base/diff/explicit.h
@@ -0,0 +1 @@
+int from_explicit_diff_subdirectory;
diff --git a/tests/data/lang-diff/lang.h b/tests/data/lang-diff/lang.h
new file mode 100644
index 0000000..54603aa
--- /dev/null
+++ b/tests/data/lang-diff/lang.h
@@ -0,0 +1 @@
+y' = from_diff_language_path;
diff --git a/tests/data/lang-path-main.c b/tests/data/lang-path-main.c
new file mode 100644
index 0000000..864bc07
--- /dev/null
+++ b/tests/data/lang-path-main.c
@@ -0,0 +1,6 @@
+#lang "as"
+#include "lang.h"
+#endlang
+#lang "diff"
+#include "lang.h"
+#endlang
diff --git a/tests/data/lang.c b/tests/data/lang.c
new file mode 100644
index 0000000..75cdc70
--- /dev/null
+++ b/tests/data/lang.c
@@ -0,0 +1,9 @@
+int before;
+#lang "diff"
+y' = omega;
+#lang "alg"
+f = x + y;
+#endlang
+z' = y;
+#endlang
+int after;
diff --git a/tests/data/nonucs2.c b/tests/data/nonucs2.c
new file mode 100644
index 0000000..d8cc27c
--- /dev/null
+++ b/tests/data/nonucs2.c
@@ -0,0 +1 @@
+int x; /* 😀 */
diff --git a/tests/data/order-main.c b/tests/data/order-main.c
new file mode 100644
index 0000000..04cac0c
--- /dev/null
+++ b/tests/data/order-main.c
@@ -0,0 +1 @@
+#include <order.h>
diff --git a/tests/data/system/order.h b/tests/data/system/order.h
new file mode 100644
index 0000000..8e7f307
--- /dev/null
+++ b/tests/data/system/order.h
@@ -0,0 +1 @@
+int system_order;
diff --git a/tests/data/system/system.h b/tests/data/system/system.h
new file mode 100644
index 0000000..c94d2ab
--- /dev/null
+++ b/tests/data/system/system.h
@@ -0,0 +1 @@
+int from_system;
diff --git a/tests/data/unbalanced.c b/tests/data/unbalanced.c
new file mode 100644
index 0000000..1bf56c1
--- /dev/null
+++ b/tests/data/unbalanced.c
@@ -0,0 +1,2 @@
+#lang "diff"
+y' = 1;
diff --git a/tests/data/user/order.h b/tests/data/user/order.h
new file mode 100644
index 0000000..f6313ca
--- /dev/null
+++ b/tests/data/user/order.h
@@ -0,0 +1 @@
+int user_order;
diff --git a/tests/data/utf8.c b/tests/data/utf8.c
new file mode 100644
index 0000000..d5153ea
--- /dev/null
+++ b/tests/data/utf8.c
@@ -0,0 +1,2 @@
+/* UTF-8 source, UCS-2 internal representation. */
+const char *message = "Привет, MCPU";
diff --git a/tests/mcpp-options-test.c b/tests/mcpp-options-test.c
new file mode 100644
index 0000000..a0e8311
--- /dev/null
+++ b/tests/mcpp-options-test.c
@@ -0,0 +1,24 @@
+#include <defs.h>
+
+int
+main( int argc, char **argv )
+{
+ mcpp_options options;
+ const char *p;
+ int rc = 1;
+
+ mcpp_options_init( &options, argc );
+ if( options.actions == NULL )
+ return( 1 );
+
+ p = argv[0] + strlen( argv[0] );
+ while( p != argv[0] && p[-1] != '/' ) --p;
+ options.progname = p;
+
+ if( mcpp_handle_options(&options, argc - 1, argv + 1) == 0 &&
+ mcpp_options_dump(&options, stdout) == 0 )
+ rc = 0;
+
+ mcpp_options_free( &options );
+ return( rc );
+}
diff --git a/tests/t0001-utf8.sh b/tests/t0001-utf8.sh
new file mode 100755
index 0000000..6726cfe
--- /dev/null
+++ b/tests/t0001-utf8.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+out="${TMPDIR-/tmp}/mcpu-cpp-utf8-$$.out"
+trap 'rm -f "$out"' EXIT HUP INT TERM
+"$MCPU_CPP" --no-config "$srcdir/data/utf8.c" -o "$out"
+grep 'Привет, MCPU' "$out" >/dev/null
diff --git a/tests/t0002-lang.sh b/tests/t0002-lang.sh
new file mode 100755
index 0000000..1d0102e
--- /dev/null
+++ b/tests/t0002-lang.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -eu
+out="${TMPDIR-/tmp}/mcpu-cpp-lang-$$.out"
+trap 'rm -f "$out"' EXIT HUP INT TERM
+"$MCPU_CPP" --no-config "$srcdir/data/lang.c" -o "$out"
+grep '#lang "diff"' "$out" >/dev/null
+grep '#lang "alg"' "$out" >/dev/null
+grep '#endlang' "$out" >/dev/null
diff --git a/tests/t0003-include.sh b/tests/t0003-include.sh
new file mode 100755
index 0000000..b8ddcec
--- /dev/null
+++ b/tests/t0003-include.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -eu
+out="${TMPDIR-/tmp}/mcpu-cpp-include-$$.out"
+trap 'rm -f "$out"' EXIT HUP INT TERM
+"$MCPU_CPP" --no-config -I "$srcdir/data/include" \
+ -isystem "$srcdir/data/system" \
+ "$srcdir/data/include-main.c" -o "$out"
+grep 'from_one' "$out" >/dev/null
+grep 'from_system' "$out" >/dev/null
+grep 'bridge_c' "$out" >/dev/null
+grep "y' = 1" "$out" >/dev/null
diff --git a/tests/t0004-config.sh b/tests/t0004-config.sh
new file mode 100755
index 0000000..d025d99
--- /dev/null
+++ b/tests/t0004-config.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+out="${TMPDIR-/tmp}/mcpu-cpp-config-$$.out"
+conf="${TMPDIR-/tmp}/mcpu-cpp-config-$$.conf"
+trap 'rm -f "$out" "$conf"' EXIT HUP INT TERM
+cat > "$conf" <<EOT
+MCPU_CPP_INCLUDE_PATH = $srcdir/data/include;
+MCPU_CPP_SYSTEM_INCLUDE_PATH = $srcdir/data/system;
+EOT
+"$MCPU_CPP" --config-file "$conf" "$srcdir/data/config-main.c" -o "$out"
+grep 'from_one' "$out" >/dev/null
+grep 'from_system' "$out" >/dev/null
diff --git a/tests/t0005-errors.sh b/tests/t0005-errors.sh
new file mode 100755
index 0000000..458b4c8
--- /dev/null
+++ b/tests/t0005-errors.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+set -eu
+out="${TMPDIR-/tmp}/mcpu-cpp-error-$$.out"
+err="${TMPDIR-/tmp}/mcpu-cpp-error-$$.err"
+bad="${TMPDIR-/tmp}/mcpu-cpp-nonucs2-$$.c"
+trap 'rm -f "$out" "$err" "$bad"' EXIT HUP INT TERM
+
+if "$MCPU_CPP" --no-config "$srcdir/data/unbalanced.c" -o "$out" 2>"$err"; then
+ exit 1
+fi
+grep 'unbalanced #lang/#endlang' "$err" >/dev/null
+
+# A valid UTF-8 scalar outside UCS-2 is harmless inside a comment.
+"$MCPU_CPP" --no-config "$srcdir/data/nonucs2.c" -o "$out"
+grep '^int x;$' "$out" >/dev/null
+
+# The same scalar in program text is rejected after comment removal.
+printf 'int x = \360\237\230\200;\n' > "$bad"
+if "$MCPU_CPP" --no-config "$bad" -o "$out" 2>"$err"; then
+ exit 1
+fi
+grep 'character outside UCS-2' "$err" >/dev/null
diff --git a/tests/t0006-path-order.sh b/tests/t0006-path-order.sh
new file mode 100755
index 0000000..0b9f76e
--- /dev/null
+++ b/tests/t0006-path-order.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -eu
+out="${TMPDIR-/tmp}/mcpu-cpp-order-$$.out"
+trap 'rm -f "$out"' EXIT HUP INT TERM
+# Deliberately put -isystem first: -I must still have higher semantic priority.
+"$MCPU_CPP" --no-config \
+ -isystem "$srcdir/data/system" \
+ -I "$srcdir/data/user" \
+ "$srcdir/data/order-main.c" -o "$out"
+grep 'user_order' "$out" >/dev/null
+if grep 'system_order' "$out" >/dev/null; then
+ exit 1
+fi
diff --git a/tests/t0007-language-path.sh b/tests/t0007-language-path.sh
new file mode 100755
index 0000000..6a75797
--- /dev/null
+++ b/tests/t0007-language-path.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+out="${TMPDIR-/tmp}/mcpu-cpp-langpath-$$.out"
+conf="${TMPDIR-/tmp}/mcpu-cpp-langpath-$$.conf"
+trap 'rm -f "$out" "$conf"' EXIT HUP INT TERM
+cat > "$conf" <<EOT
+MCPU_CPP_AS_INCLUDE_PATH = $srcdir/data/lang-as;
+MCPU_CPP_DIFF_INCLUDE_PATH = $srcdir/data/lang-diff;
+EOT
+"$MCPU_CPP" --config-file "$conf" "$srcdir/data/lang-path-main.c" -o "$out"
+grep 'from_as_language_path' "$out" >/dev/null
+grep 'from_diff_language_path' "$out" >/dev/null
diff --git a/tests/t0008-comments.sh b/tests/t0008-comments.sh
new file mode 100755
index 0000000..26386ba
--- /dev/null
+++ b/tests/t0008-comments.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+set -eu
+src="${TMPDIR-/tmp}/mcpu-cpp-comments-$$.c"
+out="${TMPDIR-/tmp}/mcpu-cpp-comments-$$.out"
+trap 'rm -f "$src" "$out"' EXIT HUP INT TERM
+cat > "$src" <<'EOT'
+/*
+#lang "diff"
+*/
+int c_side;
+/* comment before directive */ #lang "diff"
+y' = 1;
+#endlang
+ // one-line comment
+ /*
+ multi-line comment
+ */
+LEFT/**/RIGHT
+int block_tail; /* block tail */
+int line_tail; // line tail
+int multi_tail; /*
+ block tail
+ */
+EOT
+"$MCPU_CPP" --no-config "$src" -o "$out"
+grep 'int c_side' "$out" >/dev/null
+grep "y' = 1" "$out" >/dev/null
+grep '^LEFT RIGHT$' "$out" >/dev/null
+
+# Comment-only lines must be really empty; no synthetic or leading spaces remain.
+if grep '^[[:blank:]][[:blank:]]*$' "$out" >/dev/null; then
+ exit 1
+fi
+
+# A comment ending a non-empty line must not leave trailing whitespace.
+grep '^int block_tail;$' "$out" >/dev/null
+grep '^int line_tail;$' "$out" >/dev/null
+grep '^int multi_tail;$' "$out" >/dev/null
+if grep '[[:blank:]]$' "$out" >/dev/null; then
+ exit 1
+fi
diff --git a/tests/t0009-text-scanner.sh b/tests/t0009-text-scanner.sh
new file mode 100755
index 0000000..773968f
--- /dev/null
+++ b/tests/t0009-text-scanner.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+set -eu
+src="${TMPDIR-/tmp}/mcpu-cpp-scan-$$.c"
+out="${TMPDIR-/tmp}/mcpu-cpp-scan-$$.out"
+err="${TMPDIR-/tmp}/mcpu-cpp-scan-$$.err"
+bom="${TMPDIR-/tmp}/mcpu-cpp-bom-$$.c"
+nul="${TMPDIR-/tmp}/mcpu-cpp-nul-$$.c"
+trap 'rm -f "$src" "$out" "$err" "$bom" "$nul"' EXIT HUP INT TERM
+
+cat > "$src" <<'EOT'
+const char *a = "/* not a comment";
+#lang "diff"
+y' = 1;
+const char *b = "*/ still not a comment";
+#endlang
+const char *c = "// not a comment";
+/* real comment starts here
+#lang "alg"
+*/
+int c_side;
+EOT
+"$MCPU_CPP" --no-config "$src" -o "$out"
+grep '#lang "diff"' "$out" >/dev/null
+grep "y' = 1" "$out" >/dev/null
+grep 'int c_side' "$out" >/dev/null
+
+# UTF-8 BOM is accepted and removed at the external text boundary.
+printf '\357\273\277int bom_ok;\n' > "$bom"
+"$MCPU_CPP" --no-config "$bom" -o "$out"
+grep 'int bom_ok' "$out" >/dev/null
+
+# Embedded NUL is not text and must never be silently truncated.
+printf 'int before;\000int after;\n' > "$nul"
+if "$MCPU_CPP" --no-config "$nul" -o "$out" 2>"$err"; then
+ exit 1
+fi
+grep 'NUL character is not allowed' "$err" >/dev/null
diff --git a/tests/t0010-language-names.sh b/tests/t0010-language-names.sh
new file mode 100755
index 0000000..83d4325
--- /dev/null
+++ b/tests/t0010-language-names.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+set -eu
+dir="${TMPDIR-/tmp}/mcpu-cpp-languages-$$"
+mkdir -p "$dir"
+trap 'rm -rf "$dir"' EXIT HUP INT TERM
+
+cat > "$dir/ok.c" <<'EOT'
+#lang "diff"
+#endlang
+#lang "DIFT"
+#endlang
+#lang "Alg"
+#endlang
+#lang "aS"
+#endlang
+#lang "AvM"
+#endlang
+#lang "acs"
+#endlang
+EOT
+"$MCPU_CPP" --no-config "$dir/ok.c" -o "$dir/ok.out"
+grep '^#lang "diff"$' "$dir/ok.out" >/dev/null
+grep '^#lang "DIFT"$' "$dir/ok.out" >/dev/null
+grep '^#lang "Alg"$' "$dir/ok.out" >/dev/null
+grep '^#lang "aS"$' "$dir/ok.out" >/dev/null
+grep '^#lang "AvM"$' "$dir/ok.out" >/dev/null
+grep '^#lang "acs"$' "$dir/ok.out" >/dev/null
+
+for lang in 0 c vasm unknown
+do
+ printf '#lang "%s"\n#endlang\n' "$lang" > "$dir/bad.c"
+ if "$MCPU_CPP" --no-config "$dir/bad.c" -o "$dir/bad.out" 2> "$dir/bad.err"
+ then
+ echo "#lang \"$lang\" unexpectedly accepted" >&2
+ exit 1
+ fi
+ grep "unknown language '$lang'" "$dir/bad.err" >/dev/null
+done
diff --git a/tests/t0011-base-include-path.sh b/tests/t0011-base-include-path.sh
new file mode 100755
index 0000000..f862e88
--- /dev/null
+++ b/tests/t0011-base-include-path.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+set -eu
+dir="${TMPDIR-/tmp}/mcpu-cpp-basepath-$$"
+mkdir -p "$dir"
+trap 'rm -rf "$dir"' EXIT HUP INT TERM
+cat > "$dir/config" <<EOT
+MCPU_CPP_INCLUDE_PATH = $srcdir/data/lang-base;
+EOT
+"$MCPU_CPP" --config-file "$dir/config" "$srcdir/data/base-path-main.c" -o "$dir/base.out"
+grep 'from_base_language_include_root' "$dir/base.out" >/dev/null
+cat > "$dir/as.c" <<'EOT'
+#lang "as"
+#include <base.h>
+#include <diff/explicit.h>
+#endlang
+EOT
+"$MCPU_CPP" --config-file "$dir/config" "$dir/as.c" -o "$dir/as.out"
+grep 'from_base_language_include_root' "$dir/as.out" >/dev/null
+grep 'from_explicit_diff_subdirectory' "$dir/as.out" >/dev/null
diff --git a/tests/t0012-user-config-path.sh b/tests/t0012-user-config-path.sh
new file mode 100755
index 0000000..54b6e64
--- /dev/null
+++ b/tests/t0012-user-config-path.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+set -eu
+dir="${TMPDIR-/tmp}/mcpu-cpp-userconf-$$"
+mkdir -p "$dir/home/.mcpu" "$dir/include" "$dir/system/as"
+trap 'rm -rf "$dir"' EXIT HUP INT TERM
+cat > "$dir/home/.mcpu/mcpu-cpp.conf" <<EOT
+MCPU_CPP_INCLUDE_PATH = $dir/include;
+MCPU_CPP_SYSTEM_INCLUDE_PATH = $dir/system;
+EOT
+cat > "$dir/include/userconf.h" <<'EOT'
+int from_user_mcpu_configuration;
+EOT
+cat > "$dir/system/as/system-userconf.h" <<'EOT'
+int from_user_system_root;
+EOT
+cat > "$dir/main.c" <<'EOT'
+#include <userconf.h>
+#lang "as"
+#include <system-userconf.h>
+#endlang
+EOT
+HOME="$dir/home" "$MCPU_CPP" "$dir/main.c" -o "$dir/out"
+grep 'from_user_mcpu_configuration' "$dir/out" >/dev/null
+grep 'from_user_system_root' "$dir/out" >/dev/null
+HOME="$dir/home" "$MCPU_CPP" -dconfig > "$dir/config.out"
+grep "^MCPU_CPP_SYSTEM_INCLUDE_PATH = $dir/system;$" "$dir/config.out" >/dev/null
diff --git a/tests/t0013-include-precedence.sh b/tests/t0013-include-precedence.sh
new file mode 100755
index 0000000..0bfc1e5
--- /dev/null
+++ b/tests/t0013-include-precedence.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+set -eu
+dir="${TMPDIR-/tmp}/mcpu-cpp-precedence-$$"
+mkdir -p "$dir"
+trap 'rm -rf "$dir"' EXIT HUP INT TERM
+cat > "$dir/config" <<EOT
+MCPU_CPP_INCLUDE_PATH = $srcdir/data/common-precedence;
+MCPU_CPP_AS_INCLUDE_PATH = $srcdir/data/lang-as-precedence;
+EOT
+cat > "$dir/main.c" <<'EOT'
+#include <same.h>
+#lang "as"
+#include <same.h>
+#endlang
+EOT
+"$MCPU_CPP" --config-file "$dir/config" "$dir/main.c" -o "$dir/out"
+grep 'from_common_include_root' "$dir/out" >/dev/null
+grep 'from_as_specific_include_path' "$dir/out" >/dev/null
diff --git a/tests/t0014-preprocessing-phases.sh b/tests/t0014-preprocessing-phases.sh
new file mode 100755
index 0000000..5cc98c8
--- /dev/null
+++ b/tests/t0014-preprocessing-phases.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -eu
+src="${TMPDIR-/tmp}/mcpu-cpp-phases-$$.c"
+out="${TMPDIR-/tmp}/mcpu-cpp-phases-$$.out"
+trap 'rm -f "$src" "$out"' EXIT HUP INT TERM
+cat > "$src" <<'EOT'
+#defi\
+ne VALUE 10\
+20
+/* comment */ VALUE // trailing comment
+const char *s = "VALUE /* not a comment */";
+EOT
+"$MCPU_CPP" --no-config "$src" -o "$out"
+grep '1020' "$out" >/dev/null
+grep 'VALUE /\* not a comment \*/' "$out" >/dev/null
+if grep 'trailing comment' "$out" >/dev/null; then
+ exit 1
+fi
+if grep '/\* comment \*/' "$out" >/dev/null; then
+ exit 1
+fi
diff --git a/tests/t0015-object-macros.sh b/tests/t0015-object-macros.sh
new file mode 100755
index 0000000..a59bd23
--- /dev/null
+++ b/tests/t0015-object-macros.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+set -eu
+src="${TMPDIR-/tmp}/mcpu-cpp-macro-$$.c"
+out="${TMPDIR-/tmp}/mcpu-cpp-macro-$$.out"
+trap 'rm -f "$src" "$out"' EXIT HUP INT TERM
+cat > "$src" <<'EOT'
+#define A B
+#define B 42
+#define EMPTY
+A
+x EMPTY y
+"A B"
+#define SELF SELF
+SELF
+#undef B
+A
+#lang "diff"
+y' = A;
+#endlang
+EOT
+"$MCPU_CPP" --no-config "$src" -o "$out"
+grep '^42$' "$out" >/dev/null
+grep '^x y$' "$out" >/dev/null
+grep '^"A B"$' "$out" >/dev/null
+grep '^SELF$' "$out" >/dev/null
+grep '^B$' "$out" >/dev/null
+grep "y' = B;" "$out" >/dev/null
diff --git a/tests/t0016-macro-include.sh b/tests/t0016-macro-include.sh
new file mode 100755
index 0000000..0c1b56e
--- /dev/null
+++ b/tests/t0016-macro-include.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-minclude-$$"
+mkdir -p "$base/inc"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+cat > "$base/main.c" <<'EOT'
+#define HEADER <macro.h>
+#include HEADER
+VALUE
+EOT
+cat > "$base/inc/macro.h" <<'EOT'
+#define VALUE 77
+EOT
+"$MCPU_CPP" --no-config -I "$base/inc" "$base/main.c" -o "$base/out"
+grep '^77$' "$base/out" >/dev/null
+grep "^# 3 \"$base/main.c\" 2$" "$base/out" >/dev/null
diff --git a/tests/t0017-macro-errors.sh b/tests/t0017-macro-errors.sh
new file mode 100755
index 0000000..f54f74c
--- /dev/null
+++ b/tests/t0017-macro-errors.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -eu
+src="${TMPDIR-/tmp}/mcpu-cpp-macro-errors-$$.c"
+out="${TMPDIR-/tmp}/mcpu-cpp-macro-errors-$$.out"
+err="${TMPDIR-/tmp}/mcpu-cpp-macro-errors-$$.err"
+trap 'rm -f "$src" "$out" "$err"' EXIT HUP INT TERM
+cat > "$src" <<'EOT'
+#define
+EOT
+if "$MCPU_CPP" --no-config "$src" -o "$out" 2>"$err"; then
+ exit 1
+fi
+grep 'macro name expected after #define' "$err" >/dev/null
diff --git a/tests/t0018-include-lexing.sh b/tests/t0018-include-lexing.sh
new file mode 100755
index 0000000..ed1561d
--- /dev/null
+++ b/tests/t0018-include-lexing.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-include-lex-$$"
+mkdir -p "$base/inc/x"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+cat > "$base/main.c" <<'EOT'
+#include \
+<x/*y>
+AFTER
+EOT
+cat > "$base/inc/x/*y" <<'EOT'
+INSIDE
+EOT
+"$MCPU_CPP" --no-config -I "$base/inc" "$base/main.c" -o "$base/out"
+grep '^INSIDE$' "$base/out" >/dev/null
+grep '^AFTER$' "$base/out" >/dev/null
+grep "^# 3 \"$base/main.c\" 2$" "$base/out" >/dev/null
diff --git a/tests/t0019-recursive-macro.sh b/tests/t0019-recursive-macro.sh
new file mode 100755
index 0000000..ffba144
--- /dev/null
+++ b/tests/t0019-recursive-macro.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -eu
+src="${TMPDIR-/tmp}/mcpu-cpp-recursive-$$.c"
+out="${TMPDIR-/tmp}/mcpu-cpp-recursive-$$.out"
+trap 'rm -f "$src" "$out"' EXIT HUP INT TERM
+cat > "$src" <<'EOT'
+int foo;
+#define foo (4 + foo)
+f = foo;
+EOT
+"$MCPU_CPP" --no-config "$src" -o "$out"
+grep '^int foo;$' "$out" >/dev/null
+grep '^f = (4 + foo);$' "$out" >/dev/null
diff --git a/tests/t0020-function-macros.sh b/tests/t0020-function-macros.sh
new file mode 100755
index 0000000..aba3fc5
--- /dev/null
+++ b/tests/t0020-function-macros.sh
@@ -0,0 +1,42 @@
+#!/bin/sh
+set -eu
+src="${TMPDIR-/tmp}/mcpu-cpp-fmacro-$$.c"
+out="${TMPDIR-/tmp}/mcpu-cpp-fmacro-$$.out"
+trap 'rm -f "$src" "$out"' EXIT HUP INT TERM
+cat > "$src" <<'EOT'
+#define min(X, Y) ((X) < (Y) ? (X) : (Y))
+#define A 7
+#define ZERO() zero-text
+#define INNER(X) [X]
+#define OUTER(X, Y) INNER(X) + INNER(Y)
+#define OBJECT (argument)
+#define WRAP(X) <X>
+#define TWICE(X) (X) + \
+(X)
+min(1, 2)
+min(A, 3)
+min(min(a, b), c)
+OUTER(A, min(4, 5))
+ZERO ()
+ZERO
+OBJECT
+WRAP("a,b")
+TWICE(3)
+"min(A, 3)"
+#lang "diff"
+#define DERIV(X) X' + X
+DERIV(y)
+#endlang
+EOT
+"$MCPU_CPP" --no-config "$src" -o "$out"
+grep '^((1) < (2) ? (1) : (2))$' "$out" >/dev/null
+grep '^((7) < (3) ? (7) : (3))$' "$out" >/dev/null
+grep '^((((a) < (b) ? (a) : (b))) < (c) ? (((a) < (b) ? (a) : (b))) : (c))$' "$out" >/dev/null
+grep '^\[7\] + \[((4) < (5) ? (4) : (5))\]$' "$out" >/dev/null
+grep '^zero-text$' "$out" >/dev/null
+grep '^ZERO$' "$out" >/dev/null
+grep '^(argument)$' "$out" >/dev/null
+grep '^<"a,b">$' "$out" >/dev/null
+grep '^(3) + (3)$' "$out" >/dev/null
+grep '^"min(A, 3)"$' "$out" >/dev/null
+grep "^y' + y$" "$out" >/dev/null
diff --git a/tests/t0021-function-macro-errors.sh b/tests/t0021-function-macro-errors.sh
new file mode 100755
index 0000000..2353b35
--- /dev/null
+++ b/tests/t0021-function-macro-errors.sh
@@ -0,0 +1,89 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-fmacro-errors-$$"
+trap 'rm -f "$base".*' EXIT HUP INT TERM
+
+cat > "$base.dup.c" <<'EOT'
+#define F(X, X) X
+EOT
+if "$MCPU_CPP" --no-config "$base.dup.c" -o "$base.out" 2>"$base.err"; then
+ echo "duplicate parameter name accepted" >&2
+ exit 1
+fi
+grep "duplicate argument name 'X'" "$base.err" >/dev/null
+
+cat > "$base.few.c" <<'EOT'
+#define F(X, Y) X + Y
+F(1)
+EOT
+if "$MCPU_CPP" --no-config "$base.few.c" -o "$base.out" 2>"$base.err"; then
+ echo "too few macro arguments accepted" >&2
+ exit 1
+fi
+grep "used with too few arguments" "$base.err" >/dev/null
+
+cat > "$base.many.c" <<'EOT'
+#define F(X) X
+F(1, 2)
+EOT
+if "$MCPU_CPP" --no-config "$base.many.c" -o "$base.out" 2>"$base.err"; then
+ echo "too many macro arguments accepted" >&2
+ exit 1
+fi
+grep "used with too many arguments" "$base.err" >/dev/null
+
+cat > "$base.unterm.c" <<'EOT'
+#define F(X) X
+F(1
+EOT
+if "$MCPU_CPP" --no-config "$base.unterm.c" -o "$base.out" 2>"$base.err"; then
+ echo "unterminated macro call accepted" >&2
+ exit 1
+fi
+grep "unterminated argument list" "$base.err" >/dev/null
+
+cat > "$base.concat-first.c" <<'EOT'
+#define CAT(X, Y) ## X
+EOT
+if "$MCPU_CPP" --no-config "$base.concat-first.c" -o "$base.out" 2>"$base.err"; then
+ echo "leading ## in replacement list accepted" >&2
+ exit 1
+fi
+grep "'##' cannot appear at the beginning of a macro replacement list" "$base.err" >/dev/null
+
+cat > "$base.concat-last.c" <<'EOT'
+#define CAT(X, Y) Y ##
+EOT
+if "$MCPU_CPP" --no-config "$base.concat-last.c" -o "$base.out" 2>"$base.err"; then
+ echo "trailing ## in replacement list accepted" >&2
+ exit 1
+fi
+grep "'##' cannot appear at the end of a macro replacement list" "$base.err" >/dev/null
+
+cat > "$base.sharp-name.c" <<'EOT'
+#define S(X) #Y
+EOT
+if "$MCPU_CPP" --no-config "$base.sharp-name.c" -o "$base.out" 2>"$base.err"; then
+ echo "stringification of a non-parameter accepted" >&2
+ exit 1
+fi
+grep "'#' operator should be followed by a macro argument name" "$base.err" >/dev/null
+
+cat > "$base.sharp-end.c" <<'EOT'
+#define S(X) #
+EOT
+if "$MCPU_CPP" --no-config "$base.sharp-end.c" -o "$base.out" 2>"$base.err"; then
+ echo "unterminated stringification operator accepted" >&2
+ exit 1
+fi
+grep "'#' operator is not followed by a macro argument name" "$base.err" >/dev/null
+
+cat > "$base.bracket.c" <<'EOT'
+#define F(X) X
+F(array[x = y, x + 1])
+EOT
+if "$MCPU_CPP" --no-config "$base.bracket.c" -o "$base.out" 2>"$base.err"; then
+ echo "comma in brackets incorrectly protected macro argument" >&2
+ exit 1
+fi
+grep "used with too many arguments" "$base.err" >/dev/null
diff --git a/tests/t0022-predefined-macros.sh b/tests/t0022-predefined-macros.sh
new file mode 100755
index 0000000..960e1e8
--- /dev/null
+++ b/tests/t0022-predefined-macros.sh
@@ -0,0 +1,56 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-predef-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/main.c" <<'EOT'
+root_file = __FILE__;
+root_base = __BASE_FILE__;
+root_level = __INCLUDE_LEVEL__;
+#define HERE() __LINE__
+root_line = HERE();
+cpp_version = __MCPU_CPP_VERSION__;
+old_version_name = __VERSION__;
+date = __DATE__;
+time = __TIME__;
+#include "one.h"
+after_line = __LINE__;
+EOT
+
+cat > "$base/one.h" <<'EOT'
+one_file = __FILE__;
+one_base = __BASE_FILE__;
+one_level = __INCLUDE_LEVEL__;
+one_line = __LINE__;
+#include "two.h"
+EOT
+
+cat > "$base/two.h" <<'EOT'
+two_file = __FILE__;
+two_base = __BASE_FILE__;
+two_level = __INCLUDE_LEVEL__;
+two_line = __LINE__;
+EOT
+
+"$MCPU_CPP" --no-config "$base/main.c" -o "$base/out"
+
+grep -F "root_file = \"$base/main.c\";" "$base/out" >/dev/null
+grep -F "root_base = \"$base/main.c\";" "$base/out" >/dev/null
+grep '^root_level = 0;$' "$base/out" >/dev/null
+grep '^root_line = 5;$' "$base/out" >/dev/null
+grep '^cpp_version = "1\.0\.2";$' "$base/out" >/dev/null
+grep '^old_version_name = __VERSION__;$' "$base/out" >/dev/null
+grep '^date = "[A-Z][a-z][a-z] [0-9] [0-9][0-9][0-9][0-9]";$\|^date = "[A-Z][a-z][a-z] [12][0-9] [0-9][0-9][0-9][0-9]";$\|^date = "[A-Z][a-z][a-z] 3[01] [0-9][0-9][0-9][0-9]";$' "$base/out" >/dev/null
+grep '^time = "[0-2][0-9]:[0-5][0-9]:[0-5][0-9]";$' "$base/out" >/dev/null
+
+grep -F "one_file = \"$base/one.h\";" "$base/out" >/dev/null
+grep -F "one_base = \"$base/main.c\";" "$base/out" >/dev/null
+grep '^one_level = 1;$' "$base/out" >/dev/null
+grep '^one_line = 4;$' "$base/out" >/dev/null
+
+grep -F "two_file = \"$base/two.h\";" "$base/out" >/dev/null
+grep -F "two_base = \"$base/main.c\";" "$base/out" >/dev/null
+grep '^two_level = 2;$' "$base/out" >/dev/null
+grep '^two_line = 4;$' "$base/out" >/dev/null
+grep '^after_line = 11;$' "$base/out" >/dev/null
diff --git a/tests/t0023-predefined-redefine.sh b/tests/t0023-predefined-redefine.sh
new file mode 100755
index 0000000..b30a1d9
--- /dev/null
+++ b/tests/t0023-predefined-redefine.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+set -eu
+src="${TMPDIR-/tmp}/mcpu-cpp-predef-redef-$$.c"
+out="${TMPDIR-/tmp}/mcpu-cpp-predef-redef-$$.out"
+err="${TMPDIR-/tmp}/mcpu-cpp-predef-redef-$$.err"
+trap 'rm -f "$src" "$out" "$err"' EXIT HUP INT TERM
+cat > "$src" <<'EOT'
+before = __LINE__;
+#define __LINE__ 77
+after = __LINE__;
+#undef __LINE__
+literal = __LINE__;
+"__FILE__ __LINE__ __MCPU_CPP_VERSION__"
+EOT
+"$MCPU_CPP" --no-config "$src" -o "$out" 2>"$err"
+grep '^before = 1;$' "$out" >/dev/null
+grep '^literal = __LINE__;$' "$out" >/dev/null
+grep '^after = 77;$' "$out" >/dev/null
+grep '^"__FILE__ __LINE__ __MCPU_CPP_VERSION__"$' "$out" >/dev/null
+grep "warning: macro '__LINE__' redefined" "$err" >/dev/null
diff --git a/tests/t0024-abi-predefined.sh b/tests/t0024-abi-predefined.sh
new file mode 100755
index 0000000..984f0a2
--- /dev/null
+++ b/tests/t0024-abi-predefined.sh
@@ -0,0 +1,191 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-abi-predef-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/main.c" <<'EOT'
+arch = _ARCH_MCPU;
+cpp_version = __MCPU_CPP_VERSION__;
+old_version = __VERSION__;
+byte_order = __BYTE_ORDER__;
+mcpu_byte_order = __MCPU_BYTE_ORDER__;
+word_order = __MCPU_WORD_ORDER__;
+old_float_word_order = __FLOAT_WORD_ORDER__;
+register_width = __MCPU_MACHINE_REGISTER_WIDTH__;
+real_io_limit = __MCPU_REAL_IO_LIMIT__;
+math_fn_limit = __MCPU_MATH_FN_LIMIT__;
+pointer_width = __MCPU_POINTER_WIDTH__;
+int_max_width = __MCPU_INT_MAX_WIDTH__;
+real_max_width = __MCPU_REAL_MAX_WIDTH__;
+complex_max_width = __MCPU_COMPLEX_MAX_WIDTH__;
+size_type = __MCPU_SIZE_TYPE__;
+size_width = __MCPU_SIZE_WIDTH__;
+size_sizeof = __MCPU_SIZEOF_SIZE__;
+size_max = __MCPU_SIZE_MAX__;
+ssize_type = __MCPU_SSIZE_TYPE__;
+ssize_width = __MCPU_SSIZE_WIDTH__;
+ssize_sizeof = __MCPU_SIZEOF_SSIZE__;
+ssize_max = __MCPU_SSIZE_MAX__;
+ptrdiff_type = __PTRDIFF_TYPE__;
+ptrdiff_width = __PTRDIFF_WIDTH__;
+ptrdiff_max = __PTRDIFF_MAX__;
+sizeof_ptrdiff = __SIZEOF_PTRDIFF__;
+old_sizeof_ptrdiff_t = __SIZEOF_PTRDIFF_T__;
+intptr_type = __INTPTR_TYPE__;
+uintptr_type = __UINTPTR_TYPE__;
+intptr_width = __INTPTR_WIDTH__;
+uintptr_width = __UINTPTR_WIDTH__;
+intptr_max = __INTPTR_MAX__;
+uintptr_max = __UINTPTR_MAX__;
+char8_type = __CHAR8_TYPE__;
+char16_type = __CHAR16_TYPE__;
+char8_width = __CHAR8_WIDTH__;
+char16_width = __CHAR16_WIDTH__;
+sizeof_char8 = __SIZEOF_CHAR8__;
+sizeof_char16 = __SIZEOF_CHAR16__;
+register_prefix = [__REGISTER_PREFIX__];
+local_label_prefix = [__LOCAL_LABEL_PREFIX__];
+user_label_prefix = [__USER_LABEL_PREFIX__];
+immediate_prefix = [__IMMEDIATE_PREFIX__];
+int128_type = __INT128_TYPE__;
+uint128_type = __UINT128_TYPE__;
+int128_width = __INT128_WIDTH__;
+int128_decimal = __INT128_DECIMAL_DIG__;
+uint128_decimal = __UINT128_DECIMAL_DIG__;
+sizeof_int128 = __SIZEOF_INT128__;
+sizeof_uint128 = __SIZEOF_UINT128__;
+int128_max = __INT128_MAX__;
+int128_min_must_not_exist = __INT128_MIN__;
+uint128_max = __UINT128_MAX__;
+int1024_type = __INT1024_TYPE__;
+int1024_width = __INT1024_WIDTH__;
+int1024_max_must_not_exist = __INT1024_MAX__;
+int1024_decimal = __INT1024_DECIMAL_DIG__;
+sizeof_int1024 = __SIZEOF_INT1024__;
+real128_type = __REAL128_TYPE__;
+complex128_type = __COMPLEX128_TYPE__;
+real128_width = __REAL128_WIDTH__;
+complex128_width = __COMPLEX128_WIDTH__;
+sizeof_real128 = __SIZEOF_REAL128__;
+sizeof_real128_exp = __SIZEOF_REAL128_EXP__;
+real128_max_strlen = __REAL128_MAX_STRLEN__;
+sizeof_complex128 = __SIZEOF_COMPLEX128__;
+real128_mant = __REAL128_MANT_DIG__;
+real128_decimal = __REAL128_DECIMAL_DIG__;
+real128_old_dig_must_not_exist = __REAL128_DIG__;
+real128_max = __REAL128_MAX__;
+real128_min = __REAL128_MIN__;
+real128_epsilon = __REAL128_EPSILON__;
+real128_max_exp = __REAL128_MAX_EXP__;
+real128_min_exp = __REAL128_MIN_EXP__;
+real1024_type = __REAL1024_TYPE__;
+real1024_width = __REAL1024_WIDTH__;
+real1024_max_must_not_exist = __REAL1024_MAX__;
+real1024_mant = __REAL1024_MANT_DIG__;
+real1024_decimal = __REAL1024_DECIMAL_DIG__;
+sizeof_real1024 = __SIZEOF_REAL1024__;
+sizeof_real1024_exp = __SIZEOF_REAL1024_EXP__;
+real1024_max_strlen = __REAL1024_MAX_STRLEN__;
+complex1024_type = __COMPLEX1024_TYPE__;
+complex1024_width = __COMPLEX1024_WIDTH__;
+sizeof_complex1024 = __SIZEOF_COMPLEX1024__;
+int65536_type = __INT65536_TYPE__;
+int65536_width = __INT65536_WIDTH__;
+sizeof_int65536 = __SIZEOF_INT65536__;
+char_type_must_not_exist = __CHAR_TYPE__;
+wchar_type_must_not_exist = __WCHAR_TYPE__;
+EOT
+
+"$MCPU_CPP" --no-config "$base/main.c" -o "$base/out"
+
+grep '^arch = 1;$' "$base/out" >/dev/null
+grep '^cpp_version = "1\.0\.2";$' "$base/out" >/dev/null
+grep '^old_version = __VERSION__;$' "$base/out" >/dev/null
+grep '^byte_order = \(1234\|4321\);$' "$base/out" >/dev/null
+grep '^mcpu_byte_order = \(1234\|4321\);$' "$base/out" >/dev/null
+grep '^word_order = \(1234\|4321\);$' "$base/out" >/dev/null
+grep '^old_float_word_order = __FLOAT_WORD_ORDER__;$' "$base/out" >/dev/null
+grep '^register_width = 64;$' "$base/out" >/dev/null
+grep '^pointer_width = 64;$' "$base/out" >/dev/null
+grep '^int_max_width = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^real_max_width = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^complex_max_width = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^size_type = uint[0-9][0-9]*;$' "$base/out" >/dev/null
+grep '^size_width = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^size_sizeof = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^size_max = 0x[0-9a-f][0-9a-f]*;$' "$base/out" >/dev/null
+grep '^ssize_type = int[0-9][0-9]*;$' "$base/out" >/dev/null
+grep '^ssize_width = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^ssize_sizeof = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^ssize_max = 0x[0-9a-f][0-9a-f]*;$' "$base/out" >/dev/null
+grep '^ptrdiff_type = int64;$' "$base/out" >/dev/null
+grep '^ptrdiff_width = 64;$' "$base/out" >/dev/null
+grep '^ptrdiff_max = 0x7fffffffffffffff;$' "$base/out" >/dev/null
+grep '^sizeof_ptrdiff = 8;$' "$base/out" >/dev/null
+grep '^old_sizeof_ptrdiff_t = __SIZEOF_PTRDIFF_T__;$' "$base/out" >/dev/null
+grep '^intptr_type = int64;$' "$base/out" >/dev/null
+grep '^uintptr_type = uint64;$' "$base/out" >/dev/null
+grep '^intptr_width = 64;$' "$base/out" >/dev/null
+grep '^uintptr_width = 64;$' "$base/out" >/dev/null
+grep '^intptr_max = 0x7fffffffffffffff;$' "$base/out" >/dev/null
+grep '^uintptr_max = 0xffffffffffffffff;$' "$base/out" >/dev/null
+grep '^char8_type = char8;$' "$base/out" >/dev/null
+grep '^char16_type = char16;$' "$base/out" >/dev/null
+grep '^char8_width = 8;$' "$base/out" >/dev/null
+grep '^char16_width = 16;$' "$base/out" >/dev/null
+grep '^sizeof_char8 = 1;$' "$base/out" >/dev/null
+grep '^sizeof_char16 = 2;$' "$base/out" >/dev/null
+grep '^register_prefix = \[\];$' "$base/out" >/dev/null
+grep '^local_label_prefix = \[\];$' "$base/out" >/dev/null
+grep '^user_label_prefix = \[\];$' "$base/out" >/dev/null
+grep '^immediate_prefix = \[\];$' "$base/out" >/dev/null
+grep '^int128_type = int128;$' "$base/out" >/dev/null
+grep '^uint128_type = uint128;$' "$base/out" >/dev/null
+grep '^int128_width = 128;$' "$base/out" >/dev/null
+grep '^int128_decimal = 39;$' "$base/out" >/dev/null
+grep '^uint128_decimal = 39;$' "$base/out" >/dev/null
+grep '^sizeof_int128 = 16;$' "$base/out" >/dev/null
+grep '^sizeof_uint128 = 16;$' "$base/out" >/dev/null
+grep '^sizeof_real128_exp = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^real128_max_strlen = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^int128_max = 0x7fffffffffffffffffffffffffffffff;$' "$base/out" >/dev/null
+grep '^int128_min_must_not_exist = __INT128_MIN__;$' "$base/out" >/dev/null
+grep '^uint128_max = 0xffffffffffffffffffffffffffffffff;$' "$base/out" >/dev/null
+grep '^int1024_type = int1024;$' "$base/out" >/dev/null
+grep '^int1024_width = 1024;$' "$base/out" >/dev/null
+grep '^int1024_max_must_not_exist = __INT1024_MAX__;$' "$base/out" >/dev/null
+grep '^int1024_decimal = 308;$' "$base/out" >/dev/null
+grep '^sizeof_int1024 = 128;$' "$base/out" >/dev/null
+grep '^real128_type = real128;$' "$base/out" >/dev/null
+grep '^complex128_type = complex128;$' "$base/out" >/dev/null
+grep '^real128_width = 128;$' "$base/out" >/dev/null
+grep '^complex128_width = 128;$' "$base/out" >/dev/null
+grep '^sizeof_real128 = 16;$' "$base/out" >/dev/null
+grep '^sizeof_complex128 = 32;$' "$base/out" >/dev/null
+grep '^real128_mant = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^real128_decimal = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^real128_old_dig_must_not_exist = __REAL128_DIG__;$' "$base/out" >/dev/null
+grep '^real128_max = [0-9][0-9.]*e+[0-9][0-9]*;$' "$base/out" >/dev/null
+grep '^real128_min = [0-9][0-9.]*e-[0-9][0-9]*;$' "$base/out" >/dev/null
+grep '^real128_epsilon = [0-9][0-9.]*e-[0-9][0-9]*;$' "$base/out" >/dev/null
+grep '^real128_max_exp = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^real128_min_exp = -[1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^real1024_type = real1024;$' "$base/out" >/dev/null
+grep '^real1024_width = 1024;$' "$base/out" >/dev/null
+grep '^real1024_max_must_not_exist = __REAL1024_MAX__;$' "$base/out" >/dev/null
+grep '^real1024_mant = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^real1024_decimal = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^sizeof_real1024 = 128;$' "$base/out" >/dev/null
+grep '^sizeof_real1024_exp = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^real1024_max_strlen = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^complex1024_type = complex1024;$' "$base/out" >/dev/null
+grep '^complex1024_width = 1024;$' "$base/out" >/dev/null
+grep '^sizeof_complex1024 = 256;$' "$base/out" >/dev/null
+grep '^int65536_type = int65536;$' "$base/out" >/dev/null
+grep '^int65536_width = 65536;$' "$base/out" >/dev/null
+grep '^sizeof_int65536 = 8192;$' "$base/out" >/dev/null
+grep '^real_io_limit = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^math_fn_limit = [1-9][0-9]*;$' "$base/out" >/dev/null
+grep '^char_type_must_not_exist = __CHAR_TYPE__;$' "$base/out" >/dev/null
+grep '^wchar_type_must_not_exist = __WCHAR_TYPE__;$' "$base/out" >/dev/null
diff --git a/tests/t0025-dump-macros.sh b/tests/t0025-dump-macros.sh
new file mode 100755
index 0000000..ea0ede5
--- /dev/null
+++ b/tests/t0025-dump-macros.sh
@@ -0,0 +1,99 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-dm-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+"$MCPU_CPP" --no-config -dMP < /dev/null > "$base/macros"
+
+test -s "$base/macros"
+LC_ALL=C sort -c "$base/macros"
+grep '^#define _ARCH_MCPU 1$' "$base/macros" >/dev/null
+grep '^#define __MCPU_CPP_VERSION__ "1\.0\.2"$' "$base/macros" >/dev/null
+grep '^#define __MCPU_MACHINE_REGISTER_WIDTH__ 64$' "$base/macros" >/dev/null
+grep '^#define __MCPU_REAL_IO_LIMIT__ [0-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_MATH_FN_LIMIT__ [0-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_BYTE_ORDER__ __ORDER_\(LITTLE\|BIG\)_ENDIAN__$' "$base/macros" >/dev/null
+grep '^#define __MCPU_WORD_ORDER__ __ORDER_\(LITTLE\|BIG\)_ENDIAN__$' "$base/macros" >/dev/null
+grep '^#define __BYTE_ORDER__ __MCPU_BYTE_ORDER__$' "$base/macros" >/dev/null
+grep '^#define __MCPU_INT_MAX_WIDTH__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_REAL_MAX_WIDTH__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_COMPLEX_MAX_WIDTH__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __REGISTER_PREFIX__$' "$base/macros" >/dev/null
+grep '^#define __USER_LABEL_PREFIX__$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SIZE_TYPE__ uint[0-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SIZE_WIDTH__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SIZEOF_SIZE__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SIZE_MAX__ 0x[0-9a-f][0-9a-f]*$' "$base/macros" >/dev/null
+grep '^#define __PTRDIFF_TYPE__ int64$' "$base/macros" >/dev/null
+grep '^#define __PTRDIFF_WIDTH__ 64$' "$base/macros" >/dev/null
+grep '^#define __PTRDIFF_MAX__ 0x7fffffffffffffff$' "$base/macros" >/dev/null
+grep '^#define __SIZEOF_PTRDIFF__ 8$' "$base/macros" >/dev/null
+grep '^#define __INTPTR_TYPE__ int64$' "$base/macros" >/dev/null
+grep '^#define __INTPTR_WIDTH__ 64$' "$base/macros" >/dev/null
+grep '^#define __INTPTR_MAX__ 0x7fffffffffffffff$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SSIZE_TYPE__ int[0-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SSIZE_WIDTH__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SIZEOF_SSIZE__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SSIZE_MAX__ 0x[0-9a-f][0-9a-f]*$' "$base/macros" >/dev/null
+grep '^#define __UINTPTR_TYPE__ uint64$' "$base/macros" >/dev/null
+grep '^#define __UINTPTR_WIDTH__ 64$' "$base/macros" >/dev/null
+grep '^#define __UINTPTR_MAX__ 0xffffffffffffffff$' "$base/macros" >/dev/null
+grep '^#define __CHAR8_TYPE__ char8$' "$base/macros" >/dev/null
+grep '^#define __CHAR16_TYPE__ char16$' "$base/macros" >/dev/null
+grep '^#define __SIZEOF_CHAR8__ 1$' "$base/macros" >/dev/null
+grep '^#define __SIZEOF_CHAR16__ 2$' "$base/macros" >/dev/null
+grep '^#define __INT128_DECIMAL_DIG__ 39$' "$base/macros" >/dev/null
+grep '^#define __UINT128_DECIMAL_DIG__ 39$' "$base/macros" >/dev/null
+grep '^#define __REAL128_DECIMAL_DIG__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __REAL128_MANT_DIG__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __SIZEOF_REAL128_EXP__ 4$' "$base/macros" >/dev/null
+grep '^#define __REAL128_MAX_STRLEN__ 60$' "$base/macros" >/dev/null
+grep '^#define __REAL128_MAX__ [0-9][0-9.]*e+[0-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __INT1024_TYPE__ int1024$' "$base/macros" >/dev/null
+grep '^#define __INT1024_WIDTH__ 1024$' "$base/macros" >/dev/null
+grep '^#define __SIZEOF_INT1024__ 128$' "$base/macros" >/dev/null
+grep '^#define __INT1024_DECIMAL_DIG__ 308$' "$base/macros" >/dev/null
+grep '^#define __UINT1024_DECIMAL_DIG__ 309$' "$base/macros" >/dev/null
+grep '^#define __INT65536_TYPE__ int65536$' "$base/macros" >/dev/null
+grep '^#define __INT65536_WIDTH__ 65536$' "$base/macros" >/dev/null
+grep '^#define __SIZEOF_INT65536__ 8192$' "$base/macros" >/dev/null
+grep '^#define __REAL1024_TYPE__ real1024$' "$base/macros" >/dev/null
+grep '^#define __REAL1024_WIDTH__ 1024$' "$base/macros" >/dev/null
+grep '^#define __SIZEOF_REAL1024__ 128$' "$base/macros" >/dev/null
+grep '^#define __REAL1024_DECIMAL_DIG__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __REAL1024_MANT_DIG__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __COMPLEX128_TYPE__ complex128$' "$base/macros" >/dev/null
+grep '^#define __COMPLEX128_WIDTH__ 128$' "$base/macros" >/dev/null
+grep '^#define __SIZEOF_COMPLEX128__ 32$' "$base/macros" >/dev/null
+real_limit=`sed -n 's/^#define __MCPU_REAL_IO_LIMIT__ \([0-9][0-9]*\)$/\1/p' "$base/macros"`
+test -n "$real_limit"
+real_size=`expr "$real_limit" / 8`
+complex_size=`expr "$real_limit" / 4`
+grep "^#define __REAL${real_limit}_TYPE__ real${real_limit}$" "$base/macros" >/dev/null
+grep "^#define __REAL${real_limit}_WIDTH__ ${real_limit}$" "$base/macros" >/dev/null
+grep "^#define __SIZEOF_REAL${real_limit}__ ${real_size}$" "$base/macros" >/dev/null
+grep "^#define __SIZEOF_REAL${real_limit}_EXP__ [1-9][0-9]*$" "$base/macros" >/dev/null
+grep "^#define __REAL${real_limit}_MAX_STRLEN__ [1-9][0-9]*$" "$base/macros" >/dev/null
+grep "^#define __REAL${real_limit}_DECIMAL_DIG__ [1-9][0-9]*$" "$base/macros" >/dev/null
+grep "^#define __REAL${real_limit}_MANT_DIG__ [1-9][0-9]*$" "$base/macros" >/dev/null
+grep "^#define __COMPLEX${real_limit}_TYPE__ complex${real_limit}$" "$base/macros" >/dev/null
+grep "^#define __COMPLEX${real_limit}_WIDTH__ ${real_limit}$" "$base/macros" >/dev/null
+grep "^#define __SIZEOF_COMPLEX${real_limit}__ ${complex_size}$" "$base/macros" >/dev/null
+
+for name in \
+ __VERSION__ __MPU_MACHINE_REGISTER_WIDTH__ __MPU_REAL_IO_LIMIT__ \
+ __MPU_MATH_FN_LIMIT__ __INT128_MIN__ __INT1024_MAX__ \
+ __REAL128_DIG__ __REAL1024_MAX__ __FLOAT_WORD_ORDER__ \
+ __SIZE_TYPE__ __SIZE_WIDTH__ __SIZE_MAX__ __SIZEOF_SIZE_T__ \
+ __MCPU_SIZEOF_SSIZE_T__ __SIZEOF_CHAR8_T__ __SIZEOF_CHAR16_T__ \
+ __SIZEOF_PTRDIFF_T__; do
+ if grep "^#define $name\>" "$base/macros" >/dev/null; then exit 1; fi
+done
+
+if grep '^#define __FILE__' "$base/macros" >/dev/null; then exit 1; fi
+if grep '^#define __LINE__' "$base/macros" >/dev/null; then exit 1; fi
+if grep '^#define __DATE__' "$base/macros" >/dev/null; then exit 1; fi
+if grep '^#define __TIME__' "$base/macros" >/dev/null; then exit 1; fi
+if grep '^#define __BASE_FILE__' "$base/macros" >/dev/null; then exit 1; fi
+if grep '^#define __INCLUDE_LEVEL__' "$base/macros" >/dev/null; then exit 1; fi
diff --git a/tests/t0026-dump-config.sh b/tests/t0026-dump-config.sh
new file mode 100755
index 0000000..0778f28
--- /dev/null
+++ b/tests/t0026-dump-config.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-dconfig-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/test.conf" <<'EOT'
+ZETA = /zeta;
+ROOT = /opt/mcpu;
+ALPHA = $ROOT/include;
+MCPU_CPP_INCLUDE_PATH = $ROOT/include/mcpu;
+EOT
+
+"$MCPU_CPP" --config-file "$base/test.conf" -dconfig > "$base/config"
+
+test -s "$base/config"
+LC_ALL=C sort -c "$base/config"
+grep '^ALPHA = /opt/mcpu/include;$' "$base/config" >/dev/null
+grep '^MCPU_CPP_INCLUDE_PATH = /opt/mcpu/include/mcpu;$' "$base/config" >/dev/null
+grep '^ROOT = /opt/mcpu;$' "$base/config" >/dev/null
+grep '^ZETA = /zeta;$' "$base/config" >/dev/null
+grep '^MCPU_CPP_SYSTEM_INCLUDE_PATH = ' "$base/config" >/dev/null
diff --git a/tests/t0027-system-language-path.sh b/tests/t0027-system-language-path.sh
new file mode 100755
index 0000000..0947c07
--- /dev/null
+++ b/tests/t0027-system-language-path.sh
@@ -0,0 +1,44 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-system-lang-$$"
+mkdir -p "$base/system/diff" "$base/system/as"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/test.conf" <<EOT
+MCPU_CPP_SYSTEM_INCLUDE_PATH = $base/system;
+EOT
+
+cat > "$base/system/common.h" <<'EOT'
+common_system_header
+EOT
+cat > "$base/system/diff/lang.h" <<'EOT'
+diff_system_header
+EOT
+cat > "$base/system/as/lang.h" <<'EOT'
+as_system_header
+EOT
+cat > "$base/system/diff/explicit.h" <<'EOT'
+explicit_diff_system_header
+EOT
+
+cat > "$base/main.c" <<'EOT'
+#include <common.h>
+#lang "diff"
+#include <lang.h>
+#endlang
+#lang "as"
+#include <lang.h>
+#include <diff/explicit.h>
+#endlang
+EOT
+
+"$MCPU_CPP" --config-file "$base/test.conf" "$base/main.c" -o "$base/out"
+
+grep '^common_system_header$' "$base/out" >/dev/null
+grep '^diff_system_header$' "$base/out" >/dev/null
+grep '^as_system_header$' "$base/out" >/dev/null
+grep '^explicit_diff_system_header$' "$base/out" >/dev/null
+
+if "$MCPU_CPP" --config-file "$base/test.conf" -nostdinc "$base/main.c" -o "$base/no" 2>/dev/null; then
+ exit 1
+fi
diff --git a/tests/t0028-predefined-ranges.sh b/tests/t0028-predefined-ranges.sh
new file mode 100755
index 0000000..72693a2
--- /dev/null
+++ b/tests/t0028-predefined-ranges.sh
@@ -0,0 +1,95 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-predef-ranges-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+"$MCPU_CPP" --no-config -dMP < /dev/null > "$base/macros"
+
+# Integer families: derive the largest advertised width, then verify every
+# power-of-two family from int8 through that LibMPU limit.
+int_limit=`sed -n 's/^#define __INT\([0-9][0-9]*\)_TYPE__ int[0-9][0-9]*$/\1/p' "$base/macros" | sort -n | tail -1`
+test -n "$int_limit"
+grep "^#define __MCPU_INT_MAX_WIDTH__ ${int_limit}$" "$base/macros" >/dev/null
+
+bits=8
+while test "$bits" -le "$int_limit"; do
+ bytes=`expr "$bits" / 8`
+ grep "^#define __INT${bits}_TYPE__ int${bits}$" "$base/macros" >/dev/null
+ grep "^#define __UINT${bits}_TYPE__ uint${bits}$" "$base/macros" >/dev/null
+ grep "^#define __INT${bits}_WIDTH__ ${bits}$" "$base/macros" >/dev/null
+ grep "^#define __UINT${bits}_WIDTH__ ${bits}$" "$base/macros" >/dev/null
+ grep "^#define __SIZEOF_INT${bits}__ ${bytes}$" "$base/macros" >/dev/null
+ grep "^#define __SIZEOF_UINT${bits}__ ${bytes}$" "$base/macros" >/dev/null
+
+ grep "^#define __INT${bits}_DECIMAL_DIG__ [1-9][0-9]*$" "$base/macros" >/dev/null
+ grep "^#define __UINT${bits}_DECIMAL_DIG__ [1-9][0-9]*$" "$base/macros" >/dev/null
+
+ if test "$bits" -le 256; then
+ grep "^#define __INT${bits}_MAX__ 0x[0-9a-f][0-9a-f]*$" "$base/macros" >/dev/null
+ grep "^#define __UINT${bits}_MAX__ 0x[0-9a-f][0-9a-f]*$" "$base/macros" >/dev/null
+ else
+ for name in "__INT${bits}_MAX__" "__UINT${bits}_MAX__"; do
+ if grep "^#define ${name}\\>" "$base/macros" >/dev/null; then exit 1; fi
+ done
+ fi
+
+ if grep "^#define __INT${bits}_MIN__\\>" "$base/macros" >/dev/null; then exit 1; fi
+ bits=`expr "$bits" \* 2`
+done
+
+# Real/Complex families: their complete structural metadata extends through
+# MPU_REAL_IO_LIMIT. Complex WIDTH is the language parameter, not storage bits.
+real_limit=`sed -n 's/^#define __MCPU_REAL_IO_LIMIT__ \([0-9][0-9]*\)$/\1/p' "$base/macros"`
+test -n "$real_limit"
+grep "^#define __MCPU_REAL_MAX_WIDTH__ ${real_limit}$" "$base/macros" >/dev/null
+grep "^#define __MCPU_COMPLEX_MAX_WIDTH__ ${real_limit}$" "$base/macros" >/dev/null
+
+bits=32
+while test "$bits" -le "$real_limit"; do
+ real_bytes=`expr "$bits" / 8`
+ complex_bytes=`expr "$bits" / 4`
+ grep "^#define __REAL${bits}_TYPE__ real${bits}$" "$base/macros" >/dev/null
+ grep "^#define __REAL${bits}_WIDTH__ ${bits}$" "$base/macros" >/dev/null
+ grep "^#define __SIZEOF_REAL${bits}__ ${real_bytes}$" "$base/macros" >/dev/null
+ case "$bits" in
+ 32) exp_bytes=1; max_strlen=20 ;;
+ 64) exp_bytes=2; max_strlen=40 ;;
+ 128) exp_bytes=4; max_strlen=60 ;;
+ 256) exp_bytes=4; max_strlen=80 ;;
+ 512) exp_bytes=8; max_strlen=160 ;;
+ 1024) exp_bytes=8; max_strlen=320 ;;
+ 2048) exp_bytes=16; max_strlen=640 ;;
+ 4096) exp_bytes=16; max_strlen=1280 ;;
+ 8192) exp_bytes=32; max_strlen=2560 ;;
+ 16384) exp_bytes=32; max_strlen=5120 ;;
+ 32768) exp_bytes=64; max_strlen=10240 ;;
+ 65536) exp_bytes=64; max_strlen=20480 ;;
+ *) exit 1 ;;
+ esac
+ grep "^#define __SIZEOF_REAL${bits}_EXP__ ${exp_bytes}$" "$base/macros" >/dev/null
+ grep "^#define __REAL${bits}_MAX_STRLEN__ ${max_strlen}$" "$base/macros" >/dev/null
+ grep "^#define __COMPLEX${bits}_TYPE__ complex${bits}$" "$base/macros" >/dev/null
+ grep "^#define __COMPLEX${bits}_WIDTH__ ${bits}$" "$base/macros" >/dev/null
+ grep "^#define __SIZEOF_COMPLEX${bits}__ ${complex_bytes}$" "$base/macros" >/dev/null
+
+ grep "^#define __REAL${bits}_DECIMAL_DIG__ [1-9][0-9]*$" "$base/macros" >/dev/null
+ grep "^#define __REAL${bits}_MANT_DIG__ [1-9][0-9]*$" "$base/macros" >/dev/null
+
+ if test "$bits" -le 256; then
+ grep "^#define __REAL${bits}_MAX__ " "$base/macros" >/dev/null
+ grep "^#define __REAL${bits}_MIN__ " "$base/macros" >/dev/null
+ grep "^#define __REAL${bits}_EPSILON__ " "$base/macros" >/dev/null
+ grep "^#define __REAL${bits}_MAX_EXP__ " "$base/macros" >/dev/null
+ grep "^#define __REAL${bits}_MIN_EXP__ " "$base/macros" >/dev/null
+ grep "^#define __REAL${bits}_MAX_10_EXP__ " "$base/macros" >/dev/null
+ grep "^#define __REAL${bits}_MIN_10_EXP__ " "$base/macros" >/dev/null
+ else
+ for suffix in MAX MIN EPSILON MAX_EXP MIN_EXP MAX_10_EXP MIN_10_EXP; do
+ if grep "^#define __REAL${bits}_${suffix}__\\>" "$base/macros" >/dev/null; then exit 1; fi
+ done
+ fi
+
+ if grep "^#define __REAL${bits}_DIG__\\>" "$base/macros" >/dev/null; then exit 1; fi
+ bits=`expr "$bits" \* 2`
+done
diff --git a/tests/t0029-predefined-abi-names.sh b/tests/t0029-predefined-abi-names.sh
new file mode 100755
index 0000000..37b2f55
--- /dev/null
+++ b/tests/t0029-predefined-abi-names.sh
@@ -0,0 +1,38 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-predef-names-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+"$MCPU_CPP" --no-config -dMP < /dev/null > "$base/macros"
+
+grep '^#define __MCPU_SIZE_TYPE__ uint[0-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SIZE_WIDTH__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SIZEOF_SIZE__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SIZE_MAX__ 0x[0-9a-f][0-9a-f]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SSIZE_TYPE__ int[0-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SSIZE_WIDTH__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SIZEOF_SSIZE__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_SSIZE_MAX__ 0x[0-9a-f][0-9a-f]*$' "$base/macros" >/dev/null
+grep '^#define __SIZEOF_PTRDIFF__ 8$' "$base/macros" >/dev/null
+grep '^#define __BYTE_ORDER__ __MCPU_BYTE_ORDER__$' "$base/macros" >/dev/null
+grep '^#define __MCPU_BYTE_ORDER__ __ORDER_\(LITTLE\|BIG\)_ENDIAN__$' "$base/macros" >/dev/null
+grep '^#define __MCPU_WORD_ORDER__ __ORDER_\(LITTLE\|BIG\)_ENDIAN__$' "$base/macros" >/dev/null
+grep '^#define __MCPU_INT_MAX_WIDTH__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_REAL_MAX_WIDTH__ [1-9][0-9]*$' "$base/macros" >/dev/null
+grep '^#define __MCPU_COMPLEX_MAX_WIDTH__ [1-9][0-9]*$' "$base/macros" >/dev/null
+
+grep '^#define __SIZEOF_CHAR8__ 1$' "$base/macros" >/dev/null
+grep '^#define __SIZEOF_CHAR16__ 2$' "$base/macros" >/dev/null
+
+real_limit=`sed -n 's/^#define __MCPU_REAL_IO_LIMIT__ \([0-9][0-9]*\)$/\1/p' "$base/macros"`
+test -n "$real_limit"
+grep "^#define __SIZEOF_REAL${real_limit}_EXP__ [1-9][0-9]*$" "$base/macros" >/dev/null
+grep "^#define __REAL${real_limit}_MAX_STRLEN__ [1-9][0-9]*$" "$base/macros" >/dev/null
+
+for name in \
+ __SIZE_TYPE__ __SIZE_WIDTH__ __SIZE_MAX__ __SIZEOF_SIZE_T__ \
+ __MCPU_SIZEOF_SSIZE_T__ __SIZEOF_CHAR8_T__ __SIZEOF_CHAR16_T__ \
+ __SIZEOF_PTRDIFF_T__ __FLOAT_WORD_ORDER__; do
+ if grep "^#define ${name}\\>" "$base/macros" >/dev/null; then exit 1; fi
+done
diff --git a/tests/t0030-integer-decimal-digits.sh b/tests/t0030-integer-decimal-digits.sh
new file mode 100755
index 0000000..08f7fe2
--- /dev/null
+++ b/tests/t0030-integer-decimal-digits.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-int-digs-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+"$MCPU_CPP" --no-config -dMP < /dev/null > "$base/macros"
+
+check_digs()
+{
+ bits="$1"
+ int_digs="$2"
+ uint_digs="$3"
+
+ grep "^#define __INT${bits}_DECIMAL_DIG__ ${int_digs}$" "$base/macros" >/dev/null
+ grep "^#define __UINT${bits}_DECIMAL_DIG__ ${uint_digs}$" "$base/macros" >/dev/null
+}
+
+# Decimal digits of the numeric maxima only: no sign and no terminating NUL.
+check_digs 8 3 3
+check_digs 16 5 5
+check_digs 32 10 10
+check_digs 64 19 20
+check_digs 128 39 39
+check_digs 256 77 78
+check_digs 512 154 155
+check_digs 1024 308 309
+check_digs 2048 617 617
+check_digs 4096 1233 1234
+check_digs 8192 2466 2467
+check_digs 16384 4932 4933
+check_digs 32768 9864 9865
+check_digs 65536 19729 19729
diff --git a/tests/t0031-stringification.sh b/tests/t0031-stringification.sh
new file mode 100755
index 0000000..213d787
--- /dev/null
+++ b/tests/t0031-stringification.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+set -eu
+src="${TMPDIR-/tmp}/mcpu-cpp-stringify-$$.c"
+out="${TMPDIR-/tmp}/mcpu-cpp-stringify-$$.out"
+dump="${TMPDIR-/tmp}/mcpu-cpp-stringify-$$.dump"
+trap 'rm -f "$src" "$out" "$dump"' EXIT HUP INT TERM
+
+cat > "$src" <<'EOT'
+#define A 7
+#define STR(X) #X
+#define WSTR(X) # X
+#define XSTR(X) STR(X)
+#define MIX(X) #X | X
+#define BAD(X, Y) X + Y
+#define HASH(X) "#" X
+#define EMPTY(X) <X>
+STR(A)
+WSTR(alpha)
+XSTR(A)
+STR( a + b )
+STR(Привет мир)
+STR("a\\b")
+STR()
+STR(BAD(1))
+MIX(A)
+HASH(A)
+EMPTY()
+#lang "diff"
+#define DSTR(X) #X
+DSTR(y' + z)
+#endlang
+EOT
+
+"$MCPU_CPP" --no-config "$src" -o "$out"
+
+grep -Fx '"A"' "$out" >/dev/null
+grep -Fx '"alpha"' "$out" >/dev/null
+grep -Fx '"7"' "$out" >/dev/null
+grep -Fx '"a + b"' "$out" >/dev/null
+grep -Fx '"Привет мир"' "$out" >/dev/null
+grep -Fx '"\"a\\\\b\""' "$out" >/dev/null
+grep -Fx '""' "$out" >/dev/null
+grep -Fx '"BAD(1)"' "$out" >/dev/null
+grep -Fx '"A" | 7' "$out" >/dev/null
+grep -Fx '"#" 7' "$out" >/dev/null
+grep -Fx '<>' "$out" >/dev/null
+grep -Fx '"y'"'"' + z"' "$out" >/dev/null
+
+"$MCPU_CPP" --no-config -dM "$src" > "$dump"
+grep -Fx '#define STR(X) #X' "$dump" >/dev/null
diff --git a/tests/t0032-command-line.sh b/tests/t0032-command-line.sh
new file mode 100755
index 0000000..e0deb91
--- /dev/null
+++ b/tests/t0032-command-line.sh
@@ -0,0 +1,119 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-options-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+check()
+{
+ name=$1
+ shift
+ "$MCPP_OPTIONS_TEST" "$@" > "$base/$name"
+}
+
+check pipe
+cat > "$base/pipe.expected" <<'EOT'
+input=<stdin>
+output=<stdout>
+verbose=0
+nostdinc=0
+dump_config=0
+dump_search_dirs=0
+dump_macros=0
+actions=0
+EOT
+cmp "$base/pipe.expected" "$base/pipe"
+
+check files input.S output.s
+sed -n '1,2p' "$base/files" > "$base/files.io"
+printf '%s\n' 'input=input.S' 'output=output.s' > "$base/files.expected"
+cmp "$base/files.expected" "$base/files.io"
+
+check dash - -
+sed -n '1,2p' "$base/dash" > "$base/dash.io"
+printf '%s\n' 'input=<stdin>' 'output=<stdout>' > "$base/dash.expected"
+cmp "$base/dash.expected" "$base/dash.io"
+
+check outfile input.S -o result.s
+sed -n '1,2p' "$base/outfile" > "$base/outfile.io"
+printf '%s\n' 'input=input.S' 'output=result.s' > "$base/outfile.expected"
+cmp "$base/outfile.expected" "$base/outfile.io"
+
+check options -v -nostdinc -Iinc1 -I inc2 -isystem sys -idirafter after input.S -
+grep '^verbose=1$' "$base/options" >/dev/null
+grep '^nostdinc=1$' "$base/options" >/dev/null
+grep '^actions=4$' "$base/options" >/dev/null
+grep '^action\[0\]=include-user:inc1$' "$base/options" >/dev/null
+grep '^action\[1\]=include-user:inc2$' "$base/options" >/dev/null
+grep '^action\[2\]=include-system:sys$' "$base/options" >/dev/null
+grep '^action\[3\]=include-after:after$' "$base/options" >/dev/null
+
+check ordered -DA=1 -UA -DA=2
+sed -n '/^actions=/,$p' "$base/ordered" > "$base/ordered.actions"
+cat > "$base/ordered.expected" <<'EOT'
+actions=3
+action[0]=define:A=1
+action[1]=undef:A
+action[2]=define:A=2
+EOT
+cmp "$base/ordered.expected" "$base/ordered.actions"
+
+check searchdirs -dsearch-dirs
+grep '^dump_search_dirs=1$' "$base/searchdirs" >/dev/null
+
+check dump -dMP input.S -o macros.txt
+grep '^dump_macros=1$' "$base/dump" >/dev/null
+
+check retained -include force.h -imacros macros.h -DA=1 -UOLD -M -MG -w -Wall -Werror input.S output.s
+grep '^input=input.S$' "$base/retained" >/dev/null
+grep '^output=output.s$' "$base/retained" >/dev/null
+grep '^action\[[0-9][0-9]*\]=include:force.h$' "$base/retained" >/dev/null
+grep '^action\[[0-9][0-9]*\]=imacros:macros.h$' "$base/retained" >/dev/null
+grep '^action\[[0-9][0-9]*\]=define:A=1$' "$base/retained" >/dev/null
+grep '^action\[[0-9][0-9]*\]=undef:OLD$' "$base/retained" >/dev/null
+
+check dumpD -dD input.S
+grep '^dump_macros=2$' "$base/dumpD" >/dev/null
+
+check md -MD input.S
+grep '^input=input.S$' "$base/md" >/dev/null
+check mmd -MMD input.S
+grep '^input=input.S$' "$base/mmd" >/dev/null
+check mf -MD -MF deps.mk input.S
+grep '^input=input.S$' "$base/mf" >/dev/null
+check mfattached -MMD -MFdeps.mk input.S
+grep '^input=input.S$' "$base/mfattached" >/dev/null
+check mt -M -MT target.o input.S
+grep '^action\[[0-9][0-9]*\]=dep-target:target.o$' "$base/mt" >/dev/null
+check mtattached -MM -MTtarget.o input.S
+grep '^action\[[0-9][0-9]*\]=dep-target:target.o$' "$base/mtattached" >/dev/null
+check mq -MD -MQ '$(OBJDIR)/target.o' input.S
+grep '^action\[[0-9][0-9]*\]=dep-target-quoted:$(OBJDIR)/target.o$' "$base/mq" >/dev/null
+check mqattached -MMD '-MQ$(OBJDIR)/target.o' input.S
+grep '^action\[[0-9][0-9]*\]=dep-target-quoted:$(OBJDIR)/target.o$' "$base/mqattached" >/dev/null
+if "$MCPP_OPTIONS_TEST" a b c > /dev/null 2>&1; then exit 1; fi
+if "$MCPP_OPTIONS_TEST" -o > /dev/null 2>&1; then exit 1; fi
+if "$MCPP_OPTIONS_TEST" -isystem > /dev/null 2>&1; then exit 1; fi
+if "$MCPP_OPTIONS_TEST" --bad-option > /dev/null 2>&1; then exit 1; fi
+if "$MCPP_OPTIONS_TEST" -MF deps.mk input.S > /dev/null 2>&1; then exit 1; fi
+if "$MCPP_OPTIONS_TEST" -MT target.o input.S > /dev/null 2>&1; then exit 1; fi
+if "$MCPP_OPTIONS_TEST" -MQ target.o input.S > /dev/null 2>&1; then exit 1; fi
+if "$MCPP_OPTIONS_TEST" -M -MT > /dev/null 2>&1; then exit 1; fi
+if "$MCPP_OPTIONS_TEST" -M -MQ > /dev/null 2>&1; then exit 1; fi
+
+# Unsupported legacy assertion options remain rejected.
+if "$MCPP_OPTIONS_TEST" -Aquestion input.S > /dev/null 2>&1; then exit 1; fi
+if "$MCPP_OPTIONS_TEST" -dMA input.S > /dev/null 2>&1; then exit 1; fi
+
+# Removed compatibility options are not recognized by mcpu-cpp.
+for opt in \
+ -x -X -out-unix-mode -I- -H -C -P '-$' -r -R -undef -u -dN -g3 -dark \
+ -Wimport -Wno-import -pedantic-ANSI-C -pedantic-ANSI-C-errors \
+ -lang-k -lang-k++ -lang-k-k++-comments -lang-asm -+ \
+ -iprefix -iwithprefix -iwithprefixbefore
+do
+ if "$MCPP_OPTIONS_TEST" "$opt" input.S > /dev/null 2>&1; then
+ echo "removed option unexpectedly accepted: $opt" >&2
+ exit 1
+ fi
+done
diff --git a/tests/t0033-dump-definitions.sh b/tests/t0033-dump-definitions.sh
new file mode 100755
index 0000000..2637e53
--- /dev/null
+++ b/tests/t0033-dump-definitions.sh
@@ -0,0 +1,51 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-dD-$$"
+mkdir -p "$base/inc"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/inc/defs.h" <<'EOT'
+#ifndef MCPP_TEST_DEFS_H
+#define MCPP_TEST_DEFS_H 1
+#define FROM_HEADER 17 /* header definition */
+#endif /* MCPP_TEST_DEFS_H */
+EOT
+
+cat > "$base/main.c" <<'EOT'
+#include <defs.h>
+#define LOCAL_VALUE 25 // local definition
+LOCAL_VALUE + FROM_HEADER
+EOT
+
+"$MCPU_CPP" --no-config -I"$base/inc" -dD "$base/main.c" -o "$base/out"
+
+# -dD starts with an input marker, emits predefined definitions with
+# <built-in> markers, keeps source #define directives, and still emits the
+# ordinary preprocessed result.
+grep '^# 0 "'"$base/main.c"'"$' "$base/out" >/dev/null
+grep '^# 0 "<built-in>"$' "$base/out" >/dev/null
+grep '^#define __MCPU_CPP_VERSION__ "1.0.2"$' "$base/out" >/dev/null
+grep '^#define FROM_HEADER 17$' "$base/out" >/dev/null
+grep '^#define LOCAL_VALUE 25$' "$base/out" >/dev/null
+if grep '^[[:blank:]]*#[[:blank:]]*\(if\|ifdef\|ifndef\|elif\|else\|endif\)\>' "$base/out" >/dev/null; then
+ exit 1
+fi
+grep '^25 + 17$' "$base/out" >/dev/null
+if grep '[[:blank:]]$' "$base/out" >/dev/null; then
+ exit 1
+fi
+
+# Every predefined definition in the initial block has its own built-in marker.
+awk '
+ /^# 1 / { exit }
+ /^#define / {
+ if( previous != "# 0 \"<built-in>\"") exit 1
+ }
+ { previous = $0 }
+' "$base/out"
+
+# Without -dD the same source definitions are removed from normal output.
+"$MCPU_CPP" --no-config -I"$base/inc" "$base/main.c" -o "$base/normal"
+if grep '^#define LOCAL_VALUE 25$' "$base/normal" >/dev/null; then
+ exit 1
+fi
diff --git a/tests/t0034-conditionals.sh b/tests/t0034-conditionals.sh
new file mode 100755
index 0000000..85f3d66
--- /dev/null
+++ b/tests/t0034-conditionals.sh
@@ -0,0 +1,136 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-conditionals-$$"
+mkdir -p "$base/inc"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/inc/guard.h" <<'EOT'
+#ifndef MCPP_TEST_GUARD_H
+#define MCPP_TEST_GUARD_H 1
+#define HEADER_VALUE 17
+header_line
+#endif
+EOT
+
+cat > "$base/main.c" <<'EOT'
+#define A 1
+#define B 2
+#define F(x) ((x) + 1)
+
+#include <guard.h>
+#include <guard.h>
+
+#if A + B * 3 == 7 && defined(A) && !defined(NO_SUCH_MACRO)
+if_line
+#else
+bad_if
+#endif
+
+#if 0
+#define BAD_SKIPPED 1
+bad_elif_0
+#elif F(B) == 3
+elif_line
+#else
+bad_elif_else
+#endif
+
+#ifdef A
+ifdef_line
+#else
+bad_ifdef
+#endif
+
+#ifndef NO_SUCH_MACRO
+ifndef_line
+#endif
+
+#if 0
+# if 1 / 0
+bad_nested
+# endif
+#else
+nested_skip_line
+#endif
+
+#if 0 && (1 / 0)
+bad_short_and
+#endif
+
+#if 1 || (1 / 0)
+short_or_line
+#endif
+
+#if 1 ? 2 : 1 / 0
+ternary_true_line
+#endif
+
+#if 0 ? 1 / 0 : 3
+ternary_false_line
+#endif
+
+#if UNKNOWN_IDENTIFIER
+bad_unknown
+#else
+unknown_zero_line
+#endif
+EOT
+
+"$MCPU_CPP" --no-config -I"$base/inc" -dD "$base/main.c" -o "$base/out"
+
+for word in \
+ header_line if_line elif_line ifdef_line ifndef_line nested_skip_line \
+ short_or_line ternary_true_line ternary_false_line unknown_zero_line; do
+ test "`grep -c "^${word}$" "$base/out"`" -eq 1
+done
+
+for word in \
+ bad_if bad_elif_0 bad_elif_else bad_ifdef bad_nested bad_short_and bad_unknown; do
+ if grep "^${word}$" "$base/out" >/dev/null; then exit 1; fi
+done
+
+# Conditional-control directives are consumed by the preprocessor and never
+# survive in the normal output, including -dD output.
+if grep '^[[:blank:]]*#[[:blank:]]*\(if\|ifdef\|ifndef\|elif\|else\|endif\)\>' "$base/out" >/dev/null; then
+ exit 1
+fi
+
+# Active definitions are retained by -dD, skipped ones are not.
+grep '^#define A 1$' "$base/out" >/dev/null
+grep '^#define HEADER_VALUE 17$' "$base/out" >/dev/null
+if grep '^#define BAD_' "$base/out" >/dev/null; then exit 1; fi
+
+cat > "$base/bad-endif.c" <<'EOT'
+#endif
+EOT
+if "$MCPU_CPP" --no-config "$base/bad-endif.c" -o "$base/bad-out" 2>/dev/null; then
+ exit 1
+fi
+
+cat > "$base/bad-else.c" <<'EOT'
+#if 1
+#else
+#else
+#endif
+EOT
+if "$MCPU_CPP" --no-config "$base/bad-else.c" -o "$base/bad-out" 2>/dev/null; then
+ exit 1
+fi
+
+cat > "$base/bad-elif.c" <<'EOT'
+#if 0
+#else
+#elif 1
+#endif
+EOT
+if "$MCPU_CPP" --no-config "$base/bad-elif.c" -o "$base/bad-out" 2>/dev/null; then
+ exit 1
+fi
+
+cat > "$base/unterminated.c" <<'EOT'
+#if 1
+unterminated
+EOT
+if "$MCPU_CPP" --no-config "$base/unterminated.c" -o "$base/bad-out" 2>/dev/null; then
+ exit 1
+fi
diff --git a/tests/t0035-line-control.sh b/tests/t0035-line-control.sh
new file mode 100755
index 0000000..b892015
--- /dev/null
+++ b/tests/t0035-line-control.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-line-control-$$"
+mkdir -p "$base/inc"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/inc/one.h" <<'EOT'
+int header_line = __LINE__;
+EOT
+
+cat > "$base/main.c" <<'EOT'
+int before = __LINE__;
+#line 62 "main.y"
+int after = __LINE__;
+const char *file_after = __FILE__;
+#include "one.h"
+int returned = __LINE__;
+#define LINE_NUMBER 90
+#line LINE_NUMBER "macro-line.y"
+int macro_line = __LINE__;
+EOT
+
+"$MCPU_CPP" --no-config -I"$base/inc" "$base/main.c" -o "$base/out"
+
+# Generated line information uses GNU linemarker syntax, not input #line syntax.
+if grep '^#line[[:blank:]]' "$base/out" >/dev/null; then exit 1; fi
+
+grep "^# 1 \"$base/main.c\"$" "$base/out" >/dev/null
+grep '^# 62 "main.y"$' "$base/out" >/dev/null
+grep '^int after = 62;$' "$base/out" >/dev/null
+grep '^const char \*file_after = "main.y";$' "$base/out" >/dev/null
+
+# Flag 1 means entering an included file; flag 2 means returning to its includer.
+grep "^# 1 \"$base/inc/one.h\" 1$" "$base/out" >/dev/null
+grep '^# 65 "main.y" 2$' "$base/out" >/dev/null
+grep '^int returned = 65;$' "$base/out" >/dev/null
+
+# Arguments of #line are macro-expanded before interpretation.
+grep '^# 90 "macro-line.y"$' "$base/out" >/dev/null
+grep '^int macro_line = 90;$' "$base/out" >/dev/null
+
diff --git a/tests/t0036-lang-string.sh b/tests/t0036-lang-string.sh
new file mode 100755
index 0000000..8e40022
--- /dev/null
+++ b/tests/t0036-lang-string.sh
@@ -0,0 +1,120 @@
+#!/bin/sh
+set -eu
+dir="${TMPDIR-/tmp}/mcpu-cpp-lang-string-$$"
+mkdir -p "$dir"
+trap 'rm -rf "$dir"' EXIT HUP INT TERM
+
+cat > "$dir/normalize.c" <<'EOT'
+ # lang "DiFf"
+#endlang
+ #lang "dIfT"
+#endlang
+EOT
+"$MCPU_CPP" --no-config "$dir/normalize.c" -o "$dir/normalize.out"
+grep '^#lang "DiFf"$' "$dir/normalize.out" >/dev/null
+grep '^#lang "dIfT"$' "$dir/normalize.out" >/dev/null
+if grep '^[[:space:]][[:space:]]*#lang' "$dir/normalize.out" >/dev/null
+then
+ echo '#lang leading whitespace was not normalized' >&2
+ exit 1
+fi
+
+cat > "$dir/case.c" <<'EOT'
+#lang "diff"
+#endlang
+#lang "Diff"
+#endlang
+#lang "DIFF"
+#endlang
+#lang "dIfF"
+y' = 1;
+#endlang
+#lang "dIfT"
+#endlang
+#lang "ALG"
+#endlang
+#lang "As"
+#endlang
+#lang "aVm"
+#endlang
+#lang "aCs"
+#endlang
+EOT
+"$MCPU_CPP" --no-config "$dir/case.c" -o "$dir/case.out"
+for spelling in diff Diff DIFF dIfF dIfT ALG As aVm aCs
+do
+ grep "^#lang \"$spelling\"$" "$dir/case.out" >/dev/null
+done
+grep "^y' = 1;$" "$dir/case.out" >/dev/null
+
+check_bad()
+{
+ name=$1
+ pattern=$2
+ if "$MCPU_CPP" --no-config "$dir/$name.c" -o "$dir/$name.out" 2> "$dir/$name.err"
+ then
+ echo "$name unexpectedly accepted" >&2
+ exit 1
+ fi
+ grep -F "$pattern" "$dir/$name.err" >/dev/null
+}
+
+cat > "$dir/unquoted.c" <<'EOT'
+#lang diff
+EOT
+check_bad unquoted 'string constant expected after #lang'
+
+cat > "$dir/empty.c" <<'EOT'
+#lang ""
+EOT
+check_bad empty 'empty language name in #lang'
+
+cat > "$dir/leading-space.c" <<'EOT'
+#lang " diff"
+EOT
+check_bad leading-space 'one word without whitespace'
+
+cat > "$dir/trailing-space.c" <<'EOT'
+#lang "diff "
+EOT
+check_bad trailing-space 'one word without whitespace'
+
+cat > "$dir/internal-space.c" <<'EOT'
+#lang "di ff"
+EOT
+check_bad internal-space 'one word without whitespace'
+
+printf '#lang "di\tff"\n' > "$dir/internal-tab.c"
+check_bad internal-tab 'one word without whitespace'
+
+cat > "$dir/unknown.c" <<'EOT'
+#lang "cplusplus"
+EOT
+check_bad unknown "unknown language 'cplusplus'"
+
+cat > "$dir/zero.c" <<'EOT'
+#lang "0"
+EOT
+check_bad zero "unknown language '0'"
+
+cat > "$dir/extra.c" <<'EOT'
+#lang "diff" extra
+EOT
+check_bad extra 'extra text after #lang string constant'
+
+cat > "$dir/multiline.c" <<'EOT'
+#lang "di
+ff"
+EOT
+check_bad multiline 'unterminated #lang string constant'
+
+cat > "$dir/spliced.c" <<'EOT'
+#lang "di\
+ff"
+EOT
+check_bad spliced 'one physical source line'
+
+cat > "$dir/no-escape.c" <<'EOT'
+#lang "d\iff"
+EOT
+check_bad no-escape "unknown language 'd\\iff'"
diff --git a/tests/t0037-token-concatenation.sh b/tests/t0037-token-concatenation.sh
new file mode 100755
index 0000000..f2d2c3f
--- /dev/null
+++ b/tests/t0037-token-concatenation.sh
@@ -0,0 +1,122 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-concat-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/main.c" <<'EOT'
+#define A left
+#define B right
+#define AB raw_result
+#define leftright expanded_result
+#define CAT(X, Y) X ## Y
+#define XCAT(X, Y) CAT(X, Y)
+#define AFTERX(X) X_ ## X
+#define XAFTERX(X) AFTERX(X)
+#define TABLESIZE 1024
+#define BUFSIZE TABLESIZE
+#define MADE 91
+#define MK(A, B) A ## B
+#define L(X) X ## tail
+#define R(X) head ## X
+#define CHAIN(A, B, C) A ## B ## C
+#define OBJECT obj ## ect
+#define object object_result
+#define COMMAND(NAME) #NAME | NAME ## _command
+#define quit_command command_result
+#define CALL(NAME) NAME ## _fn(1)
+#define run_fn(X) called_result
+#define ONE1 1
+#define PCAT(A, B) A ## B
+#define RUN(A, B) A ## ## B
+#define ab adjacent_result
+#define TEXT "a ## b"
+#define COMMENT_CAT(A, B) A /* left */ ## /* right */ B
+#define DIRECT_EMPTY
+#define xDIRECT_EMPTY direct_empty_raw
+#define EXPAND_EMPTY(A, B) CAT(A, B)
+
+CAT(A, B)
+XCAT(A, B)
+AFTERX(BUFSIZE)
+XAFTERX(BUFSIZE)
+MK(MA, DE)
+CAT(x y, z)
+CAT(x, y z)
+L()
+R()
+CHAIN(x, , z)
+OBJECT
+CAT(1.5, e3)
+CAT(+, =)
+CAT(L, "wide")
+CAT(u8, "utf8")
+COMMAND(quit)
+CALL(run)
+RUN(a, b)
+CAT(x, DIRECT_EMPTY)
+EXPAND_EMPTY(x, DIRECT_EMPTY)
+TEXT
+COMMENT_CAT(c, d)
+CAT(e /* argument comment */, f)
+#if PCAT(ONE, 1)
+conditional_result
+#endif
+EOT
+
+"$MCPU_CPP" --no-config "$base/main.c" -o "$base/out"
+
+grep -Fx 'raw_result' "$base/out" >/dev/null
+grep -Fx 'expanded_result' "$base/out" >/dev/null
+grep -Fx 'X_BUFSIZE' "$base/out" >/dev/null
+grep -Fx 'X_1024' "$base/out" >/dev/null
+grep -Fx '91' "$base/out" >/dev/null
+grep -Fx 'x yz' "$base/out" >/dev/null
+grep -Fx 'xy z' "$base/out" >/dev/null
+grep -Fx 'tail' "$base/out" >/dev/null
+grep -Fx 'head' "$base/out" >/dev/null
+grep -Fx 'xz' "$base/out" >/dev/null
+grep -Fx 'object_result' "$base/out" >/dev/null
+grep -Fx '1.5e3' "$base/out" >/dev/null
+grep -Fx '+=' "$base/out" >/dev/null
+grep -Fx 'L"wide"' "$base/out" >/dev/null
+grep -Fx 'u8"utf8"' "$base/out" >/dev/null
+grep -Fx '"quit" | command_result' "$base/out" >/dev/null
+grep -Fx 'called_result' "$base/out" >/dev/null
+grep -Fx 'adjacent_result' "$base/out" >/dev/null
+grep -Fx 'direct_empty_raw' "$base/out" >/dev/null
+grep -Fx 'x' "$base/out" >/dev/null
+grep -Fx '"a ## b"' "$base/out" >/dev/null
+grep -Fx 'cd' "$base/out" >/dev/null
+grep -Fx 'ef' "$base/out" >/dev/null
+grep -Fx 'conditional_result' "$base/out" >/dev/null
+
+"$MCPU_CPP" --no-config -dM "$base/main.c" > "$base/dump"
+grep -Fx '#define CAT(X,Y) X ## Y' "$base/dump" >/dev/null
+grep -Fx '#define COMMAND(NAME) #NAME | NAME ## _command' "$base/dump" >/dev/null
+
+cat > "$base/invalid.c" <<'EOT'
+#define BAD(X) X ## +
+BAD(foo)
+EOT
+"$MCPU_CPP" --no-config "$base/invalid.c" -o "$base/invalid.out" 2>"$base/invalid.err"
+grep 'pasting "foo" and "+" does not give a valid preprocessing token' "$base/invalid.err" >/dev/null
+grep 'foo' "$base/invalid.out" | tr -d '[:space:]' | grep -Fx 'foo+' >/dev/null
+
+cat > "$base/first.c" <<'EOT'
+#define FIRST(X) ## X
+EOT
+if "$MCPU_CPP" --no-config "$base/first.c" -o "$base/first.out" 2>"$base/first.err"; then
+ echo "leading ## accepted" >&2
+ exit 1
+fi
+grep "'##' cannot appear at the beginning of a macro replacement list" "$base/first.err" >/dev/null
+
+cat > "$base/last.c" <<'EOT'
+#define LAST(X) X ##
+EOT
+if "$MCPU_CPP" --no-config "$base/last.c" -o "$base/last.out" 2>"$base/last.err"; then
+ echo "trailing ## accepted" >&2
+ exit 1
+fi
+grep "'##' cannot appear at the end of a macro replacement list" "$base/last.err" >/dev/null
diff --git a/tests/t0038-ucs2-identifiers.sh b/tests/t0038-ucs2-identifiers.sh
new file mode 100755
index 0000000..d1ae5db
--- /dev/null
+++ b/tests/t0038-ucs2-identifiers.sh
@@ -0,0 +1,122 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-ucs2-identifiers-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+# The source file is UTF-8 externally. mcpu-cpp decodes it to strict UCS-2,
+# and identifier classification is then performed with the locale-independent
+# Unicode XID_Start/XID_Continue predicates provided by LibMPUIO.
+cat > "$base/main.c" <<'EOT'
+#define АНДРЕЙ 11
+#define résumé 12
+#define Élève 13
+#define ΩΜΕΓΑ 14
+#define переменная2 15
+#define a١ 16
+#define é 17
+#define СУММА(лево, право) лево + право
+#define СТРОКА(имя) #имя
+#define СКЛЕИТЬ(лево, право) лево ## право
+#define АНДРЕЙ2 18
+#define ВРЕМЕННЫЙ 19
+#undef ВРЕМЕННЫЙ
+#define VALUE$OLD 20
+#define ДОЛЛАР$ИМЯ 21
+#define DOLLAR_PARAM(x$old) x$old
+#define CAT_DOLLAR(a,b) a ## b
+#define LEFT$RIGHT 22
+
+АНДРЕЙ
+résumé
+Élève
+ΩΜΕΓΑ
+переменная2
+a١
+é
+СУММА(АНДРЕЙ, résumé)
+СТРОКА(АНДРЕЙ)
+СКЛЕИТЬ(АНДР, ЕЙ)
+СКЛЕИТЬ(АНДРЕЙ, 2)
+андрей
+ВРЕМЕННЫЙ
+VALUE$OLD
+ДОЛЛАР$ИМЯ
+DOLLAR_PARAM(23)
+CAT_DOLLAR(LEFT$, RIGHT)
+
+#ifdef АНДРЕЙ
+ifdef_ok
+#endif
+
+#ifndef ВРЕМЕННЫЙ
+ifndef_ok
+#endif
+
+#if defined(résumé) && defined(ΩΜΕΓΑ)
+defined_ok
+#endif
+
+#ifdef VALUE$OLD
+dollar_ifdef_ok
+#endif
+
+#if defined(ДОЛЛАР$ИМЯ)
+dollar_defined_ok
+#endif
+EOT
+
+"$MCPU_CPP" --no-config "$base/main.c" -o "$base/out"
+
+grep -Fx '11' "$base/out" >/dev/null
+grep -Fx '12' "$base/out" >/dev/null
+grep -Fx '13' "$base/out" >/dev/null
+grep -Fx '14' "$base/out" >/dev/null
+grep -Fx '15' "$base/out" >/dev/null
+grep -Fx '16' "$base/out" >/dev/null
+grep -Fx '17' "$base/out" >/dev/null
+grep -Fx '11 + 12' "$base/out" >/dev/null
+grep -Fx '"АНДРЕЙ"' "$base/out" >/dev/null
+# Raw operands of ## are concatenated, then the result is rescanned.
+grep -Fx '11' "$base/out" >/dev/null
+grep -Fx '18' "$base/out" >/dev/null
+grep -Fx 'андрей' "$base/out" >/dev/null
+grep -Fx 'ВРЕМЕННЫЙ' "$base/out" >/dev/null
+grep -Fx 'ifdef_ok' "$base/out" >/dev/null
+grep -Fx 'ifndef_ok' "$base/out" >/dev/null
+grep -Fx 'defined_ok' "$base/out" >/dev/null
+grep -Fx '20' "$base/out" >/dev/null
+grep -Fx '21' "$base/out" >/dev/null
+grep -Fx '23' "$base/out" >/dev/null
+grep -Fx '22' "$base/out" >/dev/null
+grep -Fx 'dollar_ifdef_ok' "$base/out" >/dev/null
+grep -Fx 'dollar_defined_ok' "$base/out" >/dev/null
+
+# XID_Continue admits combining marks after an identifier start. The same
+# combining mark is not XID_Start and therefore cannot begin a macro name.
+printf '#define \314\201bad 1\n' > "$base/bad-start.c"
+if "$MCPU_CPP" --no-config "$base/bad-start.c" -o "$base/bad-start.out" 2>"$base/bad-start.err"; then
+ echo "combining mark accepted as identifier start" >&2
+ exit 1
+fi
+
+grep 'macro name expected after #define' "$base/bad-start.err" >/dev/null
+
+# A decimal digit is XID_Continue but not XID_Start. Non-ASCII digits are
+# therefore legal after the first character but cannot start an identifier.
+printf '#define \331\241bad 1\n' > "$base/bad-digit-start.c"
+if "$MCPU_CPP" --no-config "$base/bad-digit-start.c" -o "$base/bad-digit-start.out" 2>"$base/bad-digit-start.err"; then
+ echo "non-ASCII digit accepted as identifier start" >&2
+ exit 1
+fi
+
+grep 'macro name expected after #define' "$base/bad-digit-start.err" >/dev/null
+
+
+# Dollar is an identifier-continuation extension, never an identifier start.
+printf '#define $BAD 1\n' > "$base/bad-dollar-start.c"
+if "$MCPU_CPP" --no-config "$base/bad-dollar-start.c" -o "$base/bad-dollar-start.out" 2>"$base/bad-dollar-start.err"; then
+ echo "dollar accepted as identifier start" >&2
+ exit 1
+fi
+grep 'macro name expected after #define' "$base/bad-dollar-start.err" >/dev/null
diff --git a/tests/t0039-command-line-macros.sh b/tests/t0039-command-line-macros.sh
new file mode 100755
index 0000000..94a5ec3
--- /dev/null
+++ b/tests/t0039-command-line-macros.sh
@@ -0,0 +1,174 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-command-line-macros-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/main.c" <<'EOT'
+OBJECT
+DEFAULT
+EMPTY marker
+СУММА(10, 7)
+АНДРЕЙ
+résumé
+ПОРЯДОК
+СТРОКА(АНДРЕЙ)
+СКЛЕИТЬ(АНД,РЕЙ)
+#ifdef __MCPU_CPP_VERSION__
+BUILTIN_PRESENT
+#else
+BUILTIN_REMOVED
+#endif
+#if defined(КОМАНДА) && КОМАНДА == 31
+CONDITIONAL_OK
+#else
+CONDITIONAL_BAD
+#endif
+EOT
+
+"$MCPU_CPP" --no-config \
+ -D OBJECT=17 \
+ -DDEFAULT \
+ -DEMPTY= \
+ '-DСУММА(лево,право)=лево + право' \
+ -DАНДРЕЙ=23 \
+ -Drésumé=29 \
+ -DПОРЯДОК=1 -U ПОРЯДОК -DПОРЯДОК=2 \
+ '-DСТРОКА(x)=#x' \
+ '-DСКЛЕИТЬ(a,b)=a ## b' \
+ -DАНДРЕЙ=23 \
+ -U__MCPU_CPP_VERSION__ \
+ -DКОМАНДА=31 \
+ "$base/main.c" -o "$base/out"
+
+grep '^17$' "$base/out" >/dev/null
+grep '^1$' "$base/out" >/dev/null
+grep '^ marker$' "$base/out" >/dev/null
+grep '^10 + 7$' "$base/out" >/dev/null
+test "`grep -c '^23$' "$base/out"`" -eq 2
+grep '^29$' "$base/out" >/dev/null
+grep '^2$' "$base/out" >/dev/null
+grep '^"АНДРЕЙ"$' "$base/out" >/dev/null
+grep '^BUILTIN_REMOVED$' "$base/out" >/dev/null
+if grep '^BUILTIN_PRESENT$' "$base/out" >/dev/null; then exit 1; fi
+grep '^CONDITIONAL_OK$' "$base/out" >/dev/null
+if grep '^CONDITIONAL_BAD$' "$base/out" >/dev/null; then exit 1; fi
+
+# A function-like -D without '=' has the GNU meaning "replacement is 1".
+printf '%s\n' 'F(99)' > "$base/default-func.c"
+"$MCPU_CPP" --no-config '-DF(x)' "$base/default-func.c" -o "$base/default-func.out"
+grep '^1$' "$base/default-func.out" >/dev/null
+
+# Unicode XID continuation works on the command line as it does in source.
+combining=$(printf 'e\314\201')
+printf '%s\n' "$combining" > "$base/combining.c"
+"$MCPU_CPP" --no-config "-D$combining=37" "$base/combining.c" -o "$base/combining.out"
+grep '^37$' "$base/combining.out" >/dev/null
+
+arabic_three=$(printf '\331\243')
+name="ИМЯ$arabic_three"
+printf '%s\n' "$name" > "$base/digit.c"
+"$MCPU_CPP" --no-config "-D$name=43" "$base/digit.c" -o "$base/digit.out"
+grep '^43$' "$base/digit.out" >/dev/null
+
+# Command-line definitions pass through the same phase-3 comment cleanup as
+# source #define directives.
+printf '%s\n' 'COMMENTED' > "$base/comment.c"
+"$MCPU_CPP" --no-config '-DCOMMENTED=left/* comment */right' \
+ "$base/comment.c" -o "$base/comment.out"
+grep '^left right$' "$base/comment.out" >/dev/null
+
+# -dM observes the final command-line macro state.
+printf '%s\n' '' > "$base/empty.c"
+"$MCPU_CPP" --no-config -dM -DПОРЯДОК=1 -UПОРЯДОК -DПОРЯДОК=2 \
+ "$base/empty.c" -o "$base/macros"
+grep '^#define ПОРЯДОК 2$' "$base/macros" >/dev/null
+
+# '$' is accepted after the first identifier character. Single quotes protect
+# it from shell parameter expansion; without quotes a backslash can do the same.
+"$MCPU_CPP" --no-config -dD '-DАНДРЕЙ$_Y=62' -DОЛЬГА\$OLD=37 \
+ "$base/empty.c" -o "$base/dollar-command-line"
+grep '^#define АНДРЕЙ$_Y 62$' "$base/dollar-command-line" >/dev/null
+grep '^#define ОЛЬГА$OLD 37$' "$base/dollar-command-line" >/dev/null
+
+# '$' is not an identifier-start character.
+if "$MCPU_CPP" --no-config '-D$BAD=1' "$base/empty.c" -o "$base/bad" \
+ >"$base/dollar-start.stdout" 2>"$base/dollar-start.stderr"; then
+ exit 1
+fi
+grep 'macro name expected after #define' "$base/dollar-start.stderr" >/dev/null
+
+# For -D/-U an invalid tail after a valid identifier is discarded rather than
+# becoming part of the replacement list. The value after '=' is preserved.
+"$MCPU_CPP" --no-config -dD '-DАНДРЕЙ@XYZ=62' '-DABC+TAIL=17' \
+ '-UREMOVE@TAIL' -DREMOVE=99 "$base/empty.c" -o "$base/invalid-tail"
+grep '^#define АНДРЕЙ 62$' "$base/invalid-tail" >/dev/null
+grep '^#define ABC 17$' "$base/invalid-tail" >/dev/null
+grep '^#define REMOVE 99$' "$base/invalid-tail" >/dev/null
+if grep '^#define АНДРЕЙ @XYZ 62$' "$base/invalid-tail" >/dev/null; then
+ exit 1
+fi
+
+# Explicit '=' in an -U tail is just another invalid suffix and is discarded.
+"$MCPU_CPP" --no-config -DNAME=1 '-UNAME=bad' -dM "$base/empty.c" -o "$base/undef-tail"
+if grep '^#define NAME ' "$base/undef-tail" >/dev/null; then
+ exit 1
+fi
+
+# In -dD output command-line definitions have their own GNU-style origin
+# marker and must not be reported as predefined/builtin macros.
+"$MCPU_CPP" --no-config -dD -DАНДРЕЙ_К=62 -DОЛЬГА=37 \
+ "$base/empty.c" -o "$base/dD-command-line"
+awk '
+ /^#define АНДРЕЙ_К 62$/ {
+ if( previous != "# 0 \"<command-line>\"") exit 1
+ andrey = 1
+ }
+ /^#define ОЛЬГА 37$/ {
+ if( previous != "# 0 \"<command-line>\"") exit 1
+ olga = 1
+ }
+ /^#define __MCPU_CPP_VERSION__ / {
+ if( previous != "# 0 \"<built-in>\"") exit 1
+ builtin = 1
+ }
+ { previous = $0 }
+ END { if( !andrey || !olga || !builtin ) exit 1 }
+' "$base/dD-command-line"
+
+# An unterminated block comment in a command-line definition is diagnosed.
+if "$MCPU_CPP" --no-config '-DBROKEN=left/*' "$base/empty.c" -o "$base/bad" \
+ >"$base/comment-bad.stdout" 2>"$base/comment-bad.stderr"; then
+ exit 1
+fi
+grep -- '-D: unterminated comment' "$base/comment-bad.stderr" >/dev/null
+
+# Malformed macro names are rejected by the ordinary #define/#undef parsers.
+if "$MCPU_CPP" --no-config -D1ABC=2 "$base/empty.c" -o "$base/bad" \
+ >"$base/bad.stdout" 2>"$base/bad.stderr"; then
+ exit 1
+fi
+grep 'macro name expected after #define' "$base/bad.stderr" >/dev/null
+
+
+# Only -D/-U payloads are decoded. Invalid UTF-8 and valid supplementary-plane
+# UTF-8 are rejected because the internal text model is strict UCS-2.
+bad_utf8=$(printf '\377')
+if "$MCPU_CPP" --no-config "-D${bad_utf8}=1" "$base/empty.c" -o "$base/bad" \
+ >"$base/utf8.stdout" 2>"$base/utf8.stderr"; then
+ exit 1
+fi
+grep -- '-D: invalid UTF-8 or non-UCS-2 character' "$base/utf8.stderr" >/dev/null
+
+non_ucs2=$(printf '\360\237\230\200')
+if "$MCPU_CPP" --no-config "-D${non_ucs2}=1" "$base/empty.c" -o "$base/bad" \
+ >"$base/nonucs2.stdout" 2>"$base/nonucs2.stderr"; then
+ exit 1
+fi
+grep -- '-D: invalid UTF-8 or non-UCS-2 character' "$base/nonucs2.stderr" >/dev/null
+
+if "$MCPU_CPP" --no-config "-U${bad_utf8}" "$base/empty.c" -o "$base/bad" \
+ >"$base/utf8u.stdout" 2>"$base/utf8u.stderr"; then
+ exit 1
+fi
+grep -- '-U: invalid UTF-8 or non-UCS-2 character' "$base/utf8u.stderr" >/dev/null
diff --git a/tests/t0040-zubr-expression.sh b/tests/t0040-zubr-expression.sh
new file mode 100755
index 0000000..d95c629
--- /dev/null
+++ b/tests/t0040-zubr-expression.sh
@@ -0,0 +1,79 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-zubr-expression-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/main.c" <<'EOT'
+#if 0b1010 == 10 && 077 == 63 && 0x2a == 42
+radix_ok
+#else
+bad_radix
+#endif
+
+#if 9223372036854775807 == 0x7fffffffffffffff
+int64_ok
+#else
+bad_int64
+#endif
+
+#if 0xffffffffffffffffU > 9223372036854775807
+uint64_ok
+#else
+bad_uint64
+#endif
+
+#if 'A' == 65 && 'Я' == 0x42f && '\x42f' == 0x42f && '\101' == 65
+ucs2_char_ok
+#else
+bad_ucs2_char
+#endif
+
+#if -1 < 0 && (8 << -1) == 4 && (4 >> -1) == 8
+signed_shift_ok
+#else
+bad_signed_shift
+#endif
+
+#if 0 && (1 / 0)
+bad_short_and
+#endif
+
+#if 1 || (1 / 0)
+short_or_ok
+#endif
+
+#if 1 ? 7 : 1 / 0
+ternary_ok
+#endif
+EOT
+
+"$MCPU_CPP" --no-config "$base/main.c" -o "$base/out"
+
+for word in \
+ radix_ok int64_ok uint64_ok ucs2_char_ok signed_shift_ok short_or_ok ternary_ok; do
+ test "`grep -c "^${word}$" "$base/out"`" -eq 1
+done
+
+for word in \
+ bad_radix bad_int64 bad_uint64 bad_ucs2_char bad_signed_shift bad_short_and; do
+ if grep "^${word}$" "$base/out" >/dev/null; then exit 1; fi
+done
+
+cat > "$base/bad-float.c" <<'EOT'
+#if 1.0
+bad
+#endif
+EOT
+if "$MCPU_CPP" --no-config "$base/bad-float.c" -o "$base/bad-out" 2>/dev/null; then
+ exit 1
+fi
+
+cat > "$base/bad-inc.c" <<'EOT'
+#if 1++2
+bad
+#endif
+EOT
+if "$MCPU_CPP" --no-config "$base/bad-inc.c" -o "$base/bad-out" 2>/dev/null; then
+ exit 1
+fi
diff --git a/tests/t0041-integer-width-suffix.sh b/tests/t0041-integer-width-suffix.sh
new file mode 100755
index 0000000..a59483f
--- /dev/null
+++ b/tests/t0041-integer-width-suffix.sh
@@ -0,0 +1,95 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-integer-width-suffix-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/main.c" <<'EOT'
+#if 0x7fz8 == 127
+z8_positive
+#endif
+#if 0x80z8 == -128
+z8_sign_extend
+#endif
+#if 0xffz8 == -1
+z8_minus_one
+#endif
+#if 0xffz8u == 255
+z8_zero_extend
+#endif
+#if 0x1ffz8 == -1 && 0x1ffz8U == 255
+z8_truncate
+#endif
+#if 0x8000z16 == -32768 && 0xffffz16U == 65535
+z16_ok
+#endif
+#if 0x80000000z32 < 0 && 0xffffffffz32u == 4294967295U
+z32_ok
+#endif
+#if 0xffffffffffffffffz64 == -1
+z64_signed
+#endif
+#if 0xffffffffffffffffZ064U > 0
+z64_unsigned_leading_zero_width
+#endif
+#if 0x7fz8 + 1 == 128
+operations_are_64_bit
+#endif
+#if (~0xffz8u) == 0xffffffffffffff00U
+compl_is_64_bit
+#endif
+#if (0x80z8 >> 1) == -64 && (0x80z8u >> 1) == 64
+shift_uses_promoted_value
+#endif
+#if 1z24 == 1
+invalid_width_ignored
+#endif
+#if 1z024U == 1U
+invalid_width_u_preserved
+#endif
+#if 1U == 1 && 1u == 1
+plain_u_ok
+#endif
+EOT
+
+"$MCPU_CPP" --no-config "$base/main.c" -o "$base/out" 2>"$base/err"
+
+for word in \
+ z8_positive z8_sign_extend z8_minus_one z8_zero_extend z8_truncate \
+ z16_ok z32_ok z64_signed z64_unsigned_leading_zero_width \
+ operations_are_64_bit compl_is_64_bit shift_uses_promoted_value \
+ invalid_width_ignored invalid_width_u_preserved plain_u_ok; do
+ test "`grep -c "^${word}$" "$base/out"`" -eq 1
+done
+
+test "`grep -c 'warning: invalid zNNN integer-width suffix; suffix ignored' "$base/err"`" -eq 2
+
+for bad in \
+ '1z128' \
+ '1z256u' \
+ '1z32undefined' \
+ '1z32ufoo' \
+ '1z32$foo' \
+ '1L' \
+ '1LL' \
+ '18446744073709551616'; do
+ cat > "$base/bad.c" <<EOT
+#if $bad
+bad
+#endif
+EOT
+ if "$MCPU_CPP" --no-config "$base/bad.c" -o "$base/bad-out" 2>"$base/bad-err"; then
+ echo "accepted invalid integer constant: $bad" >&2
+ exit 1
+ fi
+done
+
+cat > "$base/wide.c" <<'EOT'
+#if 1z128
+bad
+#endif
+EOT
+if "$MCPU_CPP" --no-config "$base/wide.c" -o "$base/bad-out" 2>"$base/wide.err"; then
+ exit 1
+fi
+grep 'error: integer constants wider than 64 bits are not allowed in conditional directives' "$base/wide.err" >/dev/null
diff --git a/tests/t0042-diagnostics.sh b/tests/t0042-diagnostics.sh
new file mode 100755
index 0000000..9510332
--- /dev/null
+++ b/tests/t0042-diagnostics.sh
@@ -0,0 +1,68 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-diagnostics-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/warning.c" <<'EOT'
+#define MESSAGE expanded_message
+before
+#warning MESSAGE is not expanded
+#warning one /* removed comment */ two
+#warning "spaces inside string"
+#warning Привет мир
+#if 0
+#warning hidden warning
+#error hidden error
+#endif
+after
+EOT
+
+"$MCPU_CPP" --no-config "$base/warning.c" -o "$base/warning.out" 2>"$base/warning.err"
+
+grep '^before$' "$base/warning.out" >/dev/null
+grep '^after$' "$base/warning.out" >/dev/null
+! grep '#warning' "$base/warning.out" >/dev/null
+
+grep 'warning: #warning MESSAGE is not expanded$' "$base/warning.err" >/dev/null
+grep 'warning: #warning one two$' "$base/warning.err" >/dev/null
+grep 'warning: #warning "spaces inside string"$' "$base/warning.err" >/dev/null
+grep 'warning: #warning Привет мир$' "$base/warning.err" >/dev/null
+! grep 'expanded_message' "$base/warning.err" >/dev/null
+! grep 'hidden warning' "$base/warning.err" >/dev/null
+! grep 'hidden error' "$base/warning.err" >/dev/null
+
+"$MCPU_CPP" --no-config -dD "$base/warning.c" -o "$base/warning-dd.out" 2>"$base/warning-dd.err"
+grep '^#define MESSAGE expanded_message$' "$base/warning-dd.out" >/dev/null
+! grep '#warning' "$base/warning-dd.out" >/dev/null
+grep 'warning: #warning MESSAGE is not expanded$' "$base/warning-dd.err" >/dev/null
+
+cat > "$base/line.c" <<'EOT'
+#line 62 "generated.mc"
+#warning logical location
+ok
+EOT
+"$MCPU_CPP" --no-config "$base/line.c" -o "$base/line.out" 2>"$base/line.err"
+grep '^generated\.mc:62: warning: #warning logical location$' "$base/line.err" >/dev/null
+grep '^ok$' "$base/line.out" >/dev/null
+
+cat > "$base/error.c" <<'EOT'
+#define WHY replacement
+#error WHY failed
+never
+EOT
+if "$MCPU_CPP" --no-config "$base/error.c" -o "$base/error.out" 2>"$base/error.err"; then
+ echo '#error did not stop preprocessing' >&2
+ exit 1
+fi
+grep 'error: #error WHY failed$' "$base/error.err" >/dev/null
+! grep 'replacement' "$base/error.err" >/dev/null
+
+
+cat > "$base/empty.c" <<'EOT'
+#warning
+ok
+EOT
+"$MCPU_CPP" --no-config "$base/empty.c" -o "$base/empty.out" 2>"$base/empty.err"
+grep 'warning: #warning$' "$base/empty.err" >/dev/null
+grep '^ok$' "$base/empty.out" >/dev/null
diff --git a/tests/t0043-include-next.sh b/tests/t0043-include-next.sh
new file mode 100755
index 0000000..a2d86c3
--- /dev/null
+++ b/tests/t0043-include-next.sh
@@ -0,0 +1,87 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-include-next-$$"
+mkdir -p "$base/wrapper" "$base/system" "$base/after" "$base/local"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/config" <<EOT
+MCPU_CPP_SYSTEM_INCLUDE_PATH = $base/system;
+EOT
+
+cat > "$base/main.c" <<'EOT'
+#include <wrapped.h>
+#include "local/local.h"
+#if 0
+#include_next <never.h>
+#endif
+EOT
+
+cat > "$base/wrapper/wrapped.h" <<'EOT'
+wrapper_begin
+#line 500 "virtual/wrapped.h"
+#define NEXT_WRAPPED <wrapped.h>
+#include_next NEXT_WRAPPED
+wrapper_end
+EOT
+
+cat > "$base/system/wrapped.h" <<'EOT'
+system_begin
+#include_next "wrapped.h"
+system_end
+EOT
+
+cat > "$base/after/wrapped.h" <<'EOT'
+after_header
+EOT
+
+cat > "$base/local/local.h" <<'EOT'
+local_header
+#include_next <from_chain.h>
+EOT
+
+cat > "$base/wrapper/from_chain.h" <<'EOT'
+from_chain_wrapper
+EOT
+
+"$MCPU_CPP" --config-file "$base/config" \
+ -isystem "$base/wrapper" -idirafter "$base/after" \
+ "$base/main.c" -o "$base/out"
+
+# The wrapper header found through -isystem continues into the configured
+# system include root, then that header continues into -idirafter. The quote
+# form of #include_next must not re-search the current source directory.
+awk '
+ /wrapper_begin/ { a = NR }
+ /system_begin/ { b = NR }
+ /after_header/ { c = NR }
+ /system_end/ { d = NR }
+ /wrapper_end/ { e = NR }
+ END { exit !(a && a < b && b < c && c < d && d < e) }
+' "$base/out"
+
+# A header found by ordinary quoted-source-directory lookup has no configured
+# search entry to skip, so include_next begins with the configured chain.
+grep '^local_header$' "$base/out" >/dev/null
+grep '^from_chain_wrapper$' "$base/out" >/dev/null
+
+# Macro-expanded include_next operands use the same include operand parser.
+grep '^system_begin$' "$base/out" >/dev/null
+
+# Inactive conditional branches must not execute include_next.
+! grep 'never.h' "$base/out" >/dev/null
+
+# If no later search entry contains the file, include_next is an error rather
+# than falling back to the directory that supplied the current header.
+cat > "$base/wrapper/last.h" <<'EOT'
+#include_next <last.h>
+EOT
+cat > "$base/last-main.c" <<'EOT'
+#include <last.h>
+EOT
+if "$MCPU_CPP" --config-file "$base/config" \
+ -isystem "$base/wrapper" "$base/last-main.c" \
+ -o "$base/last.out" 2>"$base/last.err"; then
+ echo '#include_next unexpectedly found the current header again' >&2
+ exit 1
+fi
+grep "cannot find include_next file 'last.h'" "$base/last.err" >/dev/null
diff --git a/tests/t0044-configured-search-order.sh b/tests/t0044-configured-search-order.sh
new file mode 100755
index 0000000..980790f
--- /dev/null
+++ b/tests/t0044-configured-search-order.sh
@@ -0,0 +1,114 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-config-order-$$"
+mkdir -p \
+ "$base/cmd-I" \
+ "$base/cmd-isystem" \
+ "$base/config-as" \
+ "$base/config-user" \
+ "$base/system/as" \
+ "$base/cmd-after" \
+ "$base/config-after"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/config" <<EOT
+MCPU_CPP_AS_INCLUDE_PATH = $base/config-as;
+MCPU_CPP_INCLUDE_PATH = $base/config-user;
+MCPU_CPP_SYSTEM_INCLUDE_PATH = $base/system;
+MCPU_CPP_AFTER_INCLUDE_PATH = $base/config-after;
+EOT
+
+cat > "$base/main.c" <<'EOT'
+#lang "as"
+#include <same.h>
+#endlang
+EOT
+
+cat > "$base/cmd-I/same.h" <<'EOT'
+from_explicit_I
+#include_next <same.h>
+EOT
+cat > "$base/cmd-isystem/same.h" <<'EOT'
+from_explicit_isystem
+#include_next <same.h>
+EOT
+cat > "$base/config-as/same.h" <<'EOT'
+from_config_language
+#include_next <same.h>
+EOT
+cat > "$base/config-user/same.h" <<'EOT'
+from_config_user
+#include_next <same.h>
+EOT
+cat > "$base/system/as/same.h" <<'EOT'
+from_system_language
+#include_next <same.h>
+EOT
+cat > "$base/system/same.h" <<'EOT'
+from_system_root
+#include_next <same.h>
+EOT
+cat > "$base/cmd-after/same.h" <<'EOT'
+from_explicit_idirafter
+#include_next <same.h>
+EOT
+cat > "$base/config-after/same.h" <<'EOT'
+from_config_after
+EOT
+
+# Deliberately scramble command-line option order. Semantic classes, not
+# argv order, define precedence.
+"$MCPU_CPP" --config-file "$base/config" \
+ -idirafter "$base/cmd-after" \
+ -isystem "$base/cmd-isystem" \
+ -I "$base/cmd-I" \
+ "$base/main.c" -o "$base/out"
+
+awk '
+ /from_explicit_I/ { a = NR }
+ /from_explicit_isystem/ { b = NR }
+ /from_config_language/ { c = NR }
+ /from_config_user/ { d = NR }
+ /from_system_language/ { e = NR }
+ /from_system_root/ { f = NR }
+ /from_explicit_idirafter/ { g = NR }
+ /from_config_after/ { h = NR }
+ END { exit !(a && a < b && b < c && c < d && d < e && e < f && f < g && g < h) }
+' "$base/out"
+
+# An empty MCPU_CPP_SYSTEM_INCLUDE_PATH disables the complete configured
+# system tree, including the automatically derived <lang> subdirectory.
+cat > "$base/no-system.conf" <<EOT
+MCPU_CPP_SYSTEM_INCLUDE_PATH = ;
+EOT
+cat > "$base/system-only.c" <<'EOT'
+#lang "as"
+#include <system-only.h>
+#endlang
+EOT
+cat > "$base/system/as/system-only.h" <<'EOT'
+system_only_header
+EOT
+if "$MCPU_CPP" --config-file "$base/no-system.conf" \
+ "$base/system-only.c" -o "$base/no-system.out" 2>"$base/no-system.err"; then
+ echo 'empty MCPU_CPP_SYSTEM_INCLUDE_PATH did not disable system includes' >&2
+ exit 1
+fi
+
+# -nostdinc suppresses the configured system tree for one invocation but does
+# not suppress explicit -isystem or either after class.
+cat > "$base/nostdinc.c" <<'EOT'
+#lang "as"
+#include <nostdinc.h>
+#endlang
+EOT
+cat > "$base/system/as/nostdinc.h" <<'EOT'
+wrong_configured_system
+EOT
+cat > "$base/cmd-after/nostdinc.h" <<'EOT'
+from_after_with_nostdinc
+EOT
+"$MCPU_CPP" --config-file "$base/config" -nostdinc \
+ -idirafter "$base/cmd-after" "$base/nostdinc.c" -o "$base/nostdinc.out"
+grep '^from_after_with_nostdinc$' "$base/nostdinc.out" >/dev/null
+! grep '^wrong_configured_system$' "$base/nostdinc.out" >/dev/null
diff --git a/tests/t0045-search-dirs-verbose.sh b/tests/t0045-search-dirs-verbose.sh
new file mode 100755
index 0000000..03a6393
--- /dev/null
+++ b/tests/t0045-search-dirs-verbose.sh
@@ -0,0 +1,89 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-search-dirs-$$"
+mkdir -p \
+ "$base/cmd-user" \
+ "$base/cmd-system" \
+ "$base/config-as" \
+ "$base/config-user" \
+ "$base/system" \
+ "$base/cmd-after" \
+ "$base/config-after"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+runtime_root=$(cd "$(dirname "$MCPU_CPP")/.." && pwd -P)
+
+cat > "$base/test.conf" <<EOT
+MCPU_CPP_INCLUDE_PATH = $base/old-user;
+MCPU_CPP_AS_INCLUDE_PATH = $base/config-as;
+MCPU_CPP_INCLUDE_PATH = $base/config-user;
+MCPU_CPP_SYSTEM_INCLUDE_PATH = $base/system;
+MCPU_CPP_AFTER_INCLUDE_PATH = $base/config-after;
+EOT
+
+"$MCPU_CPP" \
+ --config-file "$base/test.conf" \
+ -I "$base/cmd-user" \
+ -isystem "$base/cmd-system" \
+ -idirafter "$base/cmd-after" \
+ -v -dsearch-dirs \
+ > "$base/search.out" 2> "$base/verbose.out"
+
+cat > "$base/search.expected" <<EOT
+search: $base/cmd-user
+search: $base/cmd-system
+search: $base/config-as
+search: $base/config-user
+search: $base/system/diff
+search: $base/system/dift
+search: $base/system/alg
+search: $base/system/as
+search: $base/system/avm
+search: $base/system/acs
+search: $base/system
+search: $base/cmd-after
+search: $base/config-after
+EOT
+cmp "$base/search.expected" "$base/search.out"
+
+# -v prints one effective value, not every assignment encountered while
+# reading configuration files.
+test "$(grep -c '^config: MCPU_CPP_INCLUDE_PATH=' "$base/verbose.out")" -eq 1
+grep "^config: MCPU_CPP_INCLUDE_PATH=$base/config-user$" "$base/verbose.out" >/dev/null
+if grep "$base/old-user" "$base/verbose.out" >/dev/null; then
+ echo 'verbose output contains an overridden configuration value' >&2
+ exit 1
+fi
+grep "^config: MCPU_CPP_AS_INCLUDE_PATH=$base/config-as$" "$base/verbose.out" >/dev/null
+grep "^config: MCPU_CPP_SYSTEM_INCLUDE_PATH=$base/system$" "$base/verbose.out" >/dev/null
+grep "^config: MCPU_CPP_AFTER_INCLUDE_PATH=$base/config-after$" "$base/verbose.out" >/dev/null
+
+# Even when an explicitly selected configuration file is empty, -v and
+# -dsearch-dirs must expose the runtime-derived installation system root.
+: > "$base/empty.conf"
+"$MCPU_CPP" --config-file "$base/empty.conf" -v -dsearch-dirs \
+ > "$base/default-search.out" 2> "$base/default-verbose.out"
+grep '^config: MCPU_CPP_SYSTEM_INCLUDE_PATH=' "$base/default-verbose.out" >/dev/null
+grep "^search: $runtime_root/include$" "$base/default-search.out" >/dev/null
+
+# -nostdinc removes both the generated <lang> system directories and the
+# configured system root from the directory dump, but keeps explicit -isystem.
+"$MCPU_CPP" \
+ --config-file "$base/test.conf" \
+ -isystem "$base/cmd-system" \
+ -nostdinc -dsearch-dirs > "$base/nostdinc.out"
+grep "^search: $base/cmd-system$" "$base/nostdinc.out" >/dev/null
+if grep "^search: $base/system" "$base/nostdinc.out" >/dev/null; then
+ echo '-nostdinc left configured system directories in -dsearch-dirs' >&2
+ exit 1
+fi
+
+# A dump action stops preprocessing. An input name may be present but need
+# not exist.
+"$MCPU_CPP" --config-file "$base/test.conf" -dsearch-dirs \
+ "$base/does-not-exist.c" > /dev/null
+
+if "$MCPU_CPP" --config-file "$base/test.conf" -dsearch-dirs -o "$base/out" \
+ > /dev/null 2>&1; then
+ echo '-dsearch-dirs accepted an output file' >&2
+ exit 1
+fi
diff --git a/tests/t0046-pragma-once.sh b/tests/t0046-pragma-once.sh
new file mode 100755
index 0000000..9429dd7
--- /dev/null
+++ b/tests/t0046-pragma-once.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-pragma-once-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/once.h" <<'EOT'
+#pragma once
+ONCE_BODY
+EOT
+ln -s once.h "$base/once-link.h"
+ln "$base/once.h" "$base/once-hard.h"
+
+cat > "$base/self.h" <<'EOT'
+#pragma once
+SELF_BEGIN
+#include "self.h"
+SELF_END
+EOT
+
+cat > "$base/inactive.h" <<'EOT'
+#if 0
+#pragma once
+#endif
+INACTIVE_BODY
+EOT
+
+cat > "$base/main.c" <<'EOT'
+#include "once.h"
+#include "once.h"
+#include "once-link.h"
+#include "once-hard.h"
+#include "self.h"
+#include "inactive.h"
+#include "inactive.h"
+#pragma pack(push, 1)
+EOT
+
+"$MCPU_CPP" --no-config "$base/main.c" -o "$base/out"
+
+# The same physical file is processed only once, even through a symbolic link
+# and a hard link. #pragma once itself is consumed by the preprocessor.
+test "$(grep -c '^ONCE_BODY$' "$base/out")" -eq 1
+! grep '^#pragma once$' "$base/out" >/dev/null
+
+# Marking happens as soon as the active directive is seen, so a file may safely
+# include itself after #pragma once without recursive inclusion.
+test "$(grep -c '^SELF_BEGIN$' "$base/out")" -eq 1
+test "$(grep -c '^SELF_END$' "$base/out")" -eq 1
+
+# An inactive #pragma once has no effect.
+test "$(grep -c '^INACTIVE_BODY$' "$base/out")" -eq 2
+
+# Other pragmas are not consumed; they remain available to the later compiler.
+grep '^#pragma pack(push, 1)$' "$base/out" >/dev/null
+
+# -dD does not make #pragma once reappear; unrelated pragmas are still kept.
+"$MCPU_CPP" --no-config -dD "$base/main.c" -o "$base/dd.out"
+! grep '^#pragma once$' "$base/dd.out" >/dev/null
+grep '^#pragma pack(push, 1)$' "$base/dd.out" >/dev/null
+
+# A non-file input stream has no physical identity. The directive is simply
+# consumed and must not turn stdin preprocessing into an error.
+printf '#pragma once\nSTDIN_BODY\n' | \
+ "$MCPU_CPP" --no-config > "$base/stdin.out"
+grep '^STDIN_BODY$' "$base/stdin.out" >/dev/null
+! grep '^#pragma once$' "$base/stdin.out" >/dev/null
diff --git a/tests/t0047-dependencies.sh b/tests/t0047-dependencies.sh
new file mode 100755
index 0000000..87ecb63
--- /dev/null
+++ b/tests/t0047-dependencies.sh
@@ -0,0 +1,140 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-deps-$$"
+mkdir -p "$base/user" "$base/sys" "$base/cfg-user" "$base/cfg-sys" "$base/after" "$base/cfg-after"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/config.conf" <<EOT
+MCPU_CPP_INCLUDE_PATH = $base/cfg-user;
+MCPU_CPP_SYSTEM_INCLUDE_PATH = $base/cfg-sys;
+MCPU_CPP_AFTER_INCLUDE_PATH = $base/cfg-after;
+EOT
+
+cat > "$base/main.c" <<'EOT'
+#include "local.h"
+#include "local-alias.h"
+#include <user.h>
+#include <cfg-user.h>
+#include "sys-explicit.h"
+#include <sys-config.h>
+#include <after-explicit.h>
+#include <after-config.h>
+#include <same.h>
+#include <shared.h>
+SHOULD_NOT_APPEAR_IN_DEP_OUTPUT
+EOT
+
+cat > "$base/local.h" <<'EOT'
+#pragma once
+#line 100 "logical-local.h"
+LOCAL_BODY
+EOT
+ln "$base/local.h" "$base/local-alias.h"
+
+cat > "$base/user/user.h" <<'EOT'
+USER_HEADER
+EOT
+cat > "$base/cfg-user/cfg-user.h" <<'EOT'
+CONFIG_USER_HEADER
+EOT
+cat > "$base/user/shared.h" <<'EOT'
+SHARED_USER_HEADER
+EOT
+cat > "$base/user/transitive-only.h" <<'EOT'
+TRANSITIVE_USER_HEADER
+EOT
+
+cat > "$base/sys/sys-explicit.h" <<'EOT'
+#include <transitive-only.h>
+#include <shared.h>
+#include "sys-child.h"
+EOT
+cat > "$base/sys/sys-child.h" <<'EOT'
+SYSTEM_CHILD
+EOT
+cat > "$base/cfg-sys/sys-config.h" <<'EOT'
+SYSTEM_CONFIG
+EOT
+cat > "$base/after/after-explicit.h" <<'EOT'
+AFTER_EXPLICIT
+EOT
+cat > "$base/cfg-after/after-config.h" <<'EOT'
+AFTER_CONFIG
+EOT
+
+cat > "$base/user/same.h" <<'EOT'
+USER_WRAPPER
+#include_next <same.h>
+EOT
+cat > "$base/sys/same.h" <<'EOT'
+SYSTEM_WRAPPED
+EOT
+
+"$MCPU_CPP" --config-file "$base/config.conf" \
+ -I "$base/user" -isystem "$base/sys" -idirafter "$base/after" \
+ -M "$base/main.c" > "$base/M.out"
+
+# -M emits only a make rule and includes every physical dependency.
+grep "^main\\.o:" "$base/M.out" >/dev/null
+! grep 'SHOULD_NOT_APPEAR_IN_DEP_OUTPUT' "$base/M.out" >/dev/null
+for f in \
+ "$base/main.c" "$base/local.h" "$base/user/user.h" \
+ "$base/cfg-user/cfg-user.h" "$base/sys/sys-explicit.h" \
+ "$base/user/transitive-only.h" "$base/user/shared.h" \
+ "$base/sys/sys-child.h" "$base/cfg-sys/sys-config.h" \
+ "$base/after/after-explicit.h" "$base/cfg-after/after-config.h" \
+ "$base/user/same.h" "$base/sys/same.h"
+do
+ grep -F "$f" "$base/M.out" >/dev/null || {
+ echo "-M omitted dependency: $f" >&2
+ exit 1
+ }
+done
+
+# local-alias.h is a hard link to local.h and must not create a duplicate
+# physical dependency. The logical #line name is not a dependency either.
+! grep -F "$base/local-alias.h" "$base/M.out" >/dev/null
+! grep -F 'logical-local.h' "$base/M.out" >/dev/null
+
+"$MCPU_CPP" --config-file "$base/config.conf" \
+ -I "$base/user" -isystem "$base/sys" -idirafter "$base/after" \
+ -MM "$base/main.c" > "$base/MM.out"
+
+grep "^main\\.o:" "$base/MM.out" >/dev/null
+for f in "$base/main.c" "$base/local.h" "$base/user/user.h" \
+ "$base/cfg-user/cfg-user.h" "$base/user/same.h" "$base/user/shared.h"
+do
+ grep -F "$f" "$base/MM.out" >/dev/null || {
+ echo "-MM omitted user dependency: $f" >&2
+ exit 1
+ }
+done
+
+# Quoting style does not define system-ness. sys-explicit.h was included
+# with quotes but found in -isystem, and all descendants reachable only from
+# that system header are excluded. -idirafter/config AFTER are system class.
+for f in "$base/sys/sys-explicit.h" "$base/user/transitive-only.h" \
+ "$base/sys/sys-child.h" "$base/cfg-sys/sys-config.h" \
+ "$base/after/after-explicit.h" "$base/cfg-after/after-config.h" \
+ "$base/sys/same.h"
+do
+ if grep -F "$f" "$base/MM.out" >/dev/null; then
+ echo "-MM kept system dependency: $f" >&2
+ exit 1
+ fi
+done
+
+# The same physical shared.h is first reached transitively from a system
+# header, then directly from main. The direct user inclusion makes it a user
+# dependency, so -MM must retain it (checked above).
+
+# Normal mcpu-cpp output-file syntax remains usable for dependency output.
+"$MCPU_CPP" --config-file "$base/config.conf" -I "$base/user" \
+ -isystem "$base/sys" -idirafter "$base/after" \
+ -MM "$base/main.c" -o "$base/deps.mk"
+test -s "$base/deps.mk"
+grep '^main\.o:' "$base/deps.mk" >/dev/null
+
+# stdin follows GNU cpp's useful default target convention.
+printf 'int x;\n' | "$MCPU_CPP" --no-config -M > "$base/stdin.dep"
+grep '^-: -$' "$base/stdin.dep" >/dev/null
diff --git a/tests/t0048-no-config-defaults.sh b/tests/t0048-no-config-defaults.sh
new file mode 100755
index 0000000..a6d2c75
--- /dev/null
+++ b/tests/t0048-no-config-defaults.sh
@@ -0,0 +1,73 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-no-config-defaults-$$"
+mkdir -p "$base/home/.mcpu" "$base/I" "$base/isystem" "$base/after"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+runtime_root=$(cd "$(dirname "$MCPU_CPP")/.." && pwd -P)
+
+cat > "$base/home/.mcpu/mcpu-cpp.conf" <<EOT
+MCPU_CPP_INCLUDE_PATH = $base/forbidden-user;
+MCPU_CPP_SYSTEM_INCLUDE_PATH = $base/forbidden-system;
+MCPU_CPP_AFTER_INCLUDE_PATH = $base/forbidden-after;
+EOT
+
+# --no-config skips every configuration file, but the runtime-derived installation
+# default remains the effective system root.
+HOME="$base/home" "$MCPU_CPP" --no-config -dsearch-dirs > "$base/search.out"
+grep "^search: $runtime_root/include/diff$" "$base/search.out" >/dev/null
+grep "^search: $runtime_root/include/dift$" "$base/search.out" >/dev/null
+grep "^search: $runtime_root/include/alg$" "$base/search.out" >/dev/null
+grep "^search: $runtime_root/include/as$" "$base/search.out" >/dev/null
+grep "^search: $runtime_root/include/avm$" "$base/search.out" >/dev/null
+grep "^search: $runtime_root/include/acs$" "$base/search.out" >/dev/null
+grep "^search: $runtime_root/include$" "$base/search.out" >/dev/null
+if grep "$base/forbidden" "$base/search.out" >/dev/null; then
+ echo '--no-config read the user configuration file' >&2
+ exit 1
+fi
+
+# The same runtime-derived value is visible through -dconfig and -v.
+HOME="$base/home" "$MCPU_CPP" --no-config -dconfig > "$base/config.out"
+grep "^MCPU_CPP_SYSTEM_INCLUDE_PATH = $runtime_root/include;$" "$base/config.out" >/dev/null
+if grep "$base/forbidden" "$base/config.out" >/dev/null; then
+ echo '--no-config leaked user configuration into -dconfig' >&2
+ exit 1
+fi
+
+HOME="$base/home" "$MCPU_CPP" --no-config -v -dsearch-dirs \
+ > "$base/verbose-search.out" 2> "$base/verbose.out"
+grep "^config: MCPU_CPP_SYSTEM_INCLUDE_PATH=$runtime_root/include$" "$base/verbose.out" >/dev/null
+if grep "$base/forbidden" "$base/verbose.out" >/dev/null; then
+ echo '--no-config leaked user configuration into -v' >&2
+ exit 1
+fi
+
+# Explicit command-line classes retain their normative priority around the
+# runtime-derived system root.
+HOME="$base/home" "$MCPU_CPP" --no-config \
+ -I "$base/I" -isystem "$base/isystem" -idirafter "$base/after" \
+ -dsearch-dirs > "$base/ordered.out"
+first=$(sed -n '1p' "$base/ordered.out")
+second=$(sed -n '2p' "$base/ordered.out")
+last=$(tail -n 1 "$base/ordered.out")
+test "$first" = "search: $base/I"
+test "$second" = "search: $base/isystem"
+test "$last" = "search: $base/after"
+grep "^search: $runtime_root/include$" "$base/ordered.out" >/dev/null
+
+# -nostdinc, not --no-config, is the switch that suppresses the runtime-derived
+# standard-system include tree. Explicit classes remain visible.
+HOME="$base/home" "$MCPU_CPP" --no-config -nostdinc \
+ -I "$base/I" -isystem "$base/isystem" -idirafter "$base/after" \
+ -dsearch-dirs > "$base/nostdinc.out"
+cat > "$base/nostdinc.expected" <<EOT
+search: $base/I
+search: $base/isystem
+search: $base/after
+EOT
+cmp "$base/nostdinc.expected" "$base/nostdinc.out"
+
+# With no explicit directories, --no-config -nostdinc has no search entries.
+HOME="$base/home" "$MCPU_CPP" --no-config -nostdinc -dsearch-dirs \
+ > "$base/empty.out"
+test ! -s "$base/empty.out"
diff --git a/tests/t0049-relocatable-root.sh b/tests/t0049-relocatable-root.sh
new file mode 100755
index 0000000..d64a9b8
--- /dev/null
+++ b/tests/t0049-relocatable-root.sh
@@ -0,0 +1,55 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-relocatable-root-$$"
+root_a="$base/mcpu-a"
+root_b="$base/mcpu-b"
+mkdir -p "$root_a/bin" "$root_a/etc" "$root_a/include/diff" "$base/home"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cp "$MCPU_CPP" "$root_a/bin/mcpu-cpp"
+chmod +x "$root_a/bin/mcpu-cpp"
+
+cat > "$root_a/include/diff/relocated.h" <<'EOT'
+int relocated_header = 37;
+EOT
+
+cat > "$root_a/etc/mcpu-cpp.conf" <<'EOT'
+MCPU_CPP_RELOCATION_SENTINEL = runtime-config;
+EOT
+
+# The copied executable first derives its defaults from its new physical root.
+HOME="$base/home" "$root_a/bin/mcpu-cpp" --no-config -dsearch-dirs \
+ > "$base/root-a.search"
+grep "^search: $root_a/include/diff$" "$base/root-a.search" >/dev/null
+grep "^search: $root_a/include$" "$base/root-a.search" >/dev/null
+
+# Move the complete MCPU tree without reconfiguring or rebuilding anything.
+mv "$root_a" "$root_b"
+
+HOME="$base/home" "$root_b/bin/mcpu-cpp" --no-config -dsearch-dirs \
+ > "$base/root-b.search"
+grep "^search: $root_b/include/diff$" "$base/root-b.search" >/dev/null
+grep "^search: $root_b/include$" "$base/root-b.search" >/dev/null
+if grep "$root_a" "$base/root-b.search" >/dev/null; then
+ echo 'relocated mcpu-cpp retained its previous installation root' >&2
+ exit 1
+fi
+
+cat > "$base/main.c" <<'EOT'
+#lang "diff"
+#include <relocated.h>
+#endlang
+EOT
+HOME="$base/home" "$root_b/bin/mcpu-cpp" --no-config \
+ "$base/main.c" -o "$base/main.out"
+grep '^int relocated_header = 37;$' "$base/main.out" >/dev/null
+
+# The packaged configuration is found relative to the relocated root as well.
+HOME="$base/home" "$root_b/bin/mcpu-cpp" -dconfig > "$base/config.out"
+grep '^MCPU_CPP_RELOCATION_SENTINEL = runtime-config;$' "$base/config.out" >/dev/null
+
+# Invoking through a symbolic link must still resolve the physical executable
+# and therefore the same relocated MCPU root.
+ln -s "$root_b/bin/mcpu-cpp" "$base/mcpu-cpp"
+HOME="$base/home" "$base/mcpu-cpp" --no-config -dconfig > "$base/link.out"
+grep "^MCPU_CPP_SYSTEM_INCLUDE_PATH = $root_b/include;$" "$base/link.out" >/dev/null
diff --git a/tests/t0050-dependency-side-effects.sh b/tests/t0050-dependency-side-effects.sh
new file mode 100755
index 0000000..67cbcf6
--- /dev/null
+++ b/tests/t0050-dependency-side-effects.sh
@@ -0,0 +1,136 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-dep-side-effects-$$"
+mkdir -p "$base/src" "$base/user" "$base/sys" "$base/out" "$base/stdin"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/src/main.c" <<'EOT'
+#include "user.h"
+#include <system.h>
+MAIN_BODY
+EOT
+cat > "$base/user/user.h" <<'EOT'
+USER_BODY
+EOT
+cat > "$base/sys/system.h" <<'EOT'
+#include "system-child.h"
+SYSTEM_BODY
+EOT
+cat > "$base/sys/system-child.h" <<'EOT'
+SYSTEM_CHILD_BODY
+EOT
+
+# -MD keeps preprocessing output and writes basename.d in the current
+# directory when no -o/-MF is present. The input directory is not copied to
+# the default dependency filename.
+(
+ cd "$base"
+ "$MCPU_CPP" --no-config -I "$base/user" -isystem "$base/sys" \
+ -MD "$base/src/main.c" > md.out
+)
+test -s "$base/md.out"
+grep 'MAIN_BODY' "$base/md.out" >/dev/null
+test -s "$base/main.d"
+grep '^main\.o:' "$base/main.d" >/dev/null
+for f in "$base/src/main.c" "$base/user/user.h" \
+ "$base/sys/system.h" "$base/sys/system-child.h"
+do
+ grep -F "$f" "$base/main.d" >/dev/null || {
+ echo "-MD omitted dependency: $f" >&2
+ exit 1
+ }
+done
+
+# -MMD has the same side-effect behavior but applies the already established
+# -MM user-dependency filter.
+rm -f "$base/main.d"
+(
+ cd "$base"
+ "$MCPU_CPP" --no-config -I "$base/user" -isystem "$base/sys" \
+ -MMD "$base/src/main.c" > mmd.out
+)
+test -s "$base/mmd.out"
+grep 'MAIN_BODY' "$base/mmd.out" >/dev/null
+test -s "$base/main.d"
+grep -F "$base/src/main.c" "$base/main.d" >/dev/null
+grep -F "$base/user/user.h" "$base/main.d" >/dev/null
+! grep -F "$base/sys/system.h" "$base/main.d" >/dev/null
+! grep -F "$base/sys/system-child.h" "$base/main.d" >/dev/null
+
+# With ordinary -o, preprocessing goes to that file and the automatic
+# dependency filename is obtained by replacing its suffix with .d.
+rm -f "$base/main.d" "$base/out/result.i" "$base/out/result.d"
+"$MCPU_CPP" --no-config -I "$base/user" -isystem "$base/sys" \
+ -MD "$base/src/main.c" -o "$base/out/result.i"
+test -s "$base/out/result.i"
+grep 'MAIN_BODY' "$base/out/result.i" >/dev/null
+test -s "$base/out/result.d"
+grep '^main\.o:' "$base/out/result.d" >/dev/null
+test ! -e "$base/main.d"
+
+# -MF overrides automatic dependency naming while leaving normal preprocessing
+# output semantics untouched.
+rm -f "$base/custom.d" "$base/main.d"
+(
+ cd "$base"
+ "$MCPU_CPP" --no-config -I "$base/user" -isystem "$base/sys" \
+ -MD -MF custom.d "$base/src/main.c" > mf.out
+)
+test -s "$base/mf.out"
+grep 'MAIN_BODY' "$base/mf.out" >/dev/null
+test -s "$base/custom.d"
+test ! -e "$base/main.d"
+
+# Attached -MFfile spelling is accepted as by GNU cpp.
+rm -f "$base/attached.d"
+(
+ cd "$base"
+ "$MCPU_CPP" --no-config -I "$base/user" -isystem "$base/sys" \
+ -MMD -MFattached.d "$base/src/main.c" > attached.out
+)
+test -s "$base/attached.d"
+! grep -F "$base/sys/system.h" "$base/attached.d" >/dev/null
+
+# -MF also redirects the already implemented dependency-only -M/-MM modes.
+rm -f "$base/only.d" "$base/only.out"
+"$MCPU_CPP" --no-config -I "$base/user" -isystem "$base/sys" \
+ -M -MF "$base/only.d" "$base/src/main.c" > "$base/only.out"
+test ! -s "$base/only.out"
+test -s "$base/only.d"
+grep -F "$base/sys/system.h" "$base/only.d" >/dev/null
+
+rm -f "$base/only-user.d" "$base/only-user.out"
+"$MCPU_CPP" --no-config -I "$base/user" -isystem "$base/sys" \
+ -MM -MF "$base/only-user.d" "$base/src/main.c" > "$base/only-user.out"
+test ! -s "$base/only-user.out"
+test -s "$base/only-user.d"
+! grep -F "$base/sys/system.h" "$base/only-user.d" >/dev/null
+
+# -MF - means dependency stdout. Put normal preprocessing in a regular -o
+# file so the two streams are easy to verify independently.
+rm -f "$base/out/stdout.i" "$base/out/stdout.d"
+"$MCPU_CPP" --no-config -I "$base/user" -isystem "$base/sys" \
+ -MMD -MF - "$base/src/main.c" -o "$base/out/stdout.i" \
+ > "$base/mf-dash.out"
+test -s "$base/out/stdout.i"
+grep 'MAIN_BODY' "$base/out/stdout.i" >/dev/null
+grep '^main\.o:' "$base/mf-dash.out" >/dev/null
+test ! -e "$base/out/stdout.d"
+
+# stdin follows the existing '-' target convention and gets '-.d' when -MD
+# needs an automatic dependency filename.
+(
+ cd "$base/stdin"
+ printf 'STDIN_BODY\n' | "$MCPU_CPP" --no-config -MD > stdout.i
+)
+test -s "$base/stdin/stdout.i"
+grep 'STDIN_BODY' "$base/stdin/stdout.i" >/dev/null
+test -s "$base/stdin/-.d"
+grep '^-: -$' "$base/stdin/-.d" >/dev/null
+
+# -MF alone is a command-line error: there is no dependency mode to redirect.
+if "$MCPU_CPP" --no-config -MF "$base/orphan.d" "$base/src/main.c" \
+ > /dev/null 2>&1; then
+ echo "-MF without a dependency mode unexpectedly succeeded" >&2
+ exit 1
+fi
diff --git a/tests/t0051-dump-macro-origin.sh b/tests/t0051-dump-macro-origin.sh
new file mode 100755
index 0000000..1a2c504
--- /dev/null
+++ b/tests/t0051-dump-macro-origin.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-dump-origin-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/main.c" <<'EOT'
+#define Z_USER 62
+#define A_USER 37
+#define ФУНКЦИЯ(x) x + 1
+#undef __MCPU_CPP_VERSION__
+EOT
+
+# -dM contains only ordinary final-state macros. Source and command-line
+# definitions are ordinary; built-ins are not.
+"$MCPU_CPP" --no-config -dM -DCMD_USER=91 "$base/main.c" > "$base/dM"
+grep -Fx '#define A_USER 37' "$base/dM" >/dev/null
+grep -Fx '#define CMD_USER 91' "$base/dM" >/dev/null
+grep -Fx '#define Z_USER 62' "$base/dM" >/dev/null
+grep -Fx '#define ФУНКЦИЯ(x) x + 1' "$base/dM" >/dev/null
+if grep '^#define _ARCH_MCPU ' "$base/dM" >/dev/null; then exit 1; fi
+if grep '^#define __MCPU_' "$base/dM" >/dev/null; then exit 1; fi
+LC_ALL=C sort -c "$base/dM"
+
+# Empty input has no ordinary definitions.
+"$MCPU_CPP" --no-config -dM < /dev/null > "$base/empty-dM"
+test ! -s "$base/empty-dM"
+
+# -dMP contains predefined macros first and ordinary macros second. Both
+# groups are deterministic and the #undef above removes the version builtin.
+"$MCPU_CPP" --no-config -dMP -DCMD_USER=91 "$base/main.c" > "$base/dMP"
+grep -Fx '#define _ARCH_MCPU 1' "$base/dMP" >/dev/null
+grep -Fx '#define A_USER 37' "$base/dMP" >/dev/null
+grep -Fx '#define CMD_USER 91' "$base/dMP" >/dev/null
+grep -Fx '#define Z_USER 62' "$base/dMP" >/dev/null
+grep -Fx '#define ФУНКЦИЯ(x) x + 1' "$base/dMP" >/dev/null
+if grep '^#define __MCPU_CPP_VERSION__ ' "$base/dMP" >/dev/null; then exit 1; fi
+
+predefined_line=`grep -n '^#define _ARCH_MCPU 1$' "$base/dMP" | sed 's/:.*//'`
+ordinary_line=`grep -n '^#define A_USER 37$' "$base/dMP" | sed 's/:.*//'`
+test "$predefined_line" -lt "$ordinary_line"
+
+# Verify ordering inside the ordinary tail explicitly.
+tail -n +"$ordinary_line" "$base/dMP" > "$base/ordinary-tail"
+LC_ALL=C sort -c "$base/ordinary-tail"
+
+# A former builtin name, if explicitly redefined after #undef, is an ordinary
+# macro because origin follows the current definition rather than the spelling.
+cat > "$base/redefine.c" <<'EOT'
+#undef _ARCH_MCPU
+#define _ARCH_MCPU 777
+#define LOCAL 1
+EOT
+"$MCPU_CPP" --no-config -dM "$base/redefine.c" > "$base/redefine-dM"
+grep -Fx '#define _ARCH_MCPU 777' "$base/redefine-dM" >/dev/null
+"$MCPU_CPP" --no-config -dMP "$base/redefine.c" > "$base/redefine-dMP"
+grep -Fx '#define _ARCH_MCPU 777' "$base/redefine-dMP" >/dev/null
+test "`grep -c '^#define _ARCH_MCPU ' "$base/redefine-dMP"`" -eq 1
+
+# Context-dependent special builtins remain outside the static dump.
+for name in __FILE__ __LINE__ __DATE__ __TIME__ __BASE_FILE__ __INCLUDE_LEVEL__; do
+ if grep "^#define ${name}\\>" "$base/dMP" >/dev/null; then exit 1; fi
+done
diff --git a/tests/t0052-warning-control.sh b/tests/t0052-warning-control.sh
new file mode 100755
index 0000000..8214d96
--- /dev/null
+++ b/tests/t0052-warning-control.sh
@@ -0,0 +1,108 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-warning-control-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/comments.c" <<'EOT'
+/* outer /* nested */
+// continued \
+comment
+ok
+EOT
+
+# Comment warnings are optional and disabled by default.
+"$MCPU_CPP" --no-config "$base/comments.c" -o "$base/default.out" 2>"$base/default.err"
+test ! -s "$base/default.err"
+grep '^ok$' "$base/default.out" >/dev/null
+
+# -Wcomment and -Wcomments are synonyms.
+for option in -Wcomment -Wcomments -Wall; do
+ "$MCPU_CPP" --no-config "$option" "$base/comments.c" \
+ -o "$base/comment.out" 2>"$base/comment.err"
+ test "`grep -c 'warning: \"/\*\" within comment' "$base/comment.err"`" -eq 1
+ test "`grep -c 'warning: multi-line comment' "$base/comment.err"`" -eq 1
+done
+
+# A specific -Wno-comment is stronger than the -Wall group regardless of order.
+for options in '-Wall -Wno-comment' '-Wno-comment -Wall' \
+ '-Wall -Wno-comments' '-Wno-comments -Wall'; do
+ # shellcheck disable=SC2086
+ "$MCPU_CPP" --no-config $options "$base/comments.c" \
+ -o "$base/no-comment.out" 2>"$base/no-comment.err"
+ test ! -s "$base/no-comment.err"
+done
+
+# Same-specificity comment options use the last occurrence.
+"$MCPU_CPP" --no-config -Wno-comment -Wcomment "$base/comments.c" \
+ -o "$base/comment-last.out" 2>"$base/comment-last.err"
+grep 'warning: multi-line comment' "$base/comment-last.err" >/dev/null
+
+"$MCPU_CPP" --no-config -Wcomment -Wno-comment "$base/comments.c" \
+ -o "$base/no-comment-last.out" 2>"$base/no-comment-last.err"
+test ! -s "$base/no-comment-last.err"
+
+# -Werror does not enable optional comment diagnostics by itself.
+"$MCPU_CPP" --no-config -Werror "$base/comments.c" \
+ -o "$base/werror-only.out" 2>"$base/werror-only.err"
+test ! -s "$base/werror-only.err"
+
+# When comment diagnostics are enabled, -Werror promotes them to errors.
+if "$MCPU_CPP" --no-config -Wcomment -Werror "$base/comments.c" \
+ -o "$base/comment-error.out" 2>"$base/comment-error.err"; then
+ echo '-Wcomment -Werror did not fail' >&2
+ exit 1
+fi
+grep 'error: "/\*" within comment' "$base/comment-error.err" >/dev/null
+
+# -Werror/-Wno-error have equal specificity; the last occurrence wins.
+cat > "$base/directive.c" <<'EOT'
+#warning requested warning
+ok
+EOT
+"$MCPU_CPP" --no-config -Werror -Wno-error "$base/directive.c" \
+ -o "$base/no-error.out" 2>"$base/no-error.err"
+grep 'warning: #warning requested warning$' "$base/no-error.err" >/dev/null
+
+if "$MCPU_CPP" --no-config -Wno-error -Werror "$base/directive.c" \
+ -o "$base/error-last.out" 2>"$base/error-last.err"; then
+ echo '-Wno-error -Werror did not fail' >&2
+ exit 1
+fi
+grep 'error: #warning requested warning$' "$base/error-last.err" >/dev/null
+
+# Existing always-issued warnings are also promoted by -Werror.
+cat > "$base/width.c" <<'EOT'
+#if 1z24 == 1
+ok
+#endif
+EOT
+if "$MCPU_CPP" --no-config -Werror "$base/width.c" \
+ -o "$base/width.out" 2>"$base/width.err"; then
+ echo 'zNNN warning was not promoted by -Werror' >&2
+ exit 1
+fi
+grep 'error: invalid zNNN integer-width suffix; suffix ignored$' "$base/width.err" >/dev/null
+
+cat > "$base/redefine.c" <<'EOT'
+#define VALUE 1
+#define VALUE 2
+EOT
+if "$MCPU_CPP" --no-config -Werror "$base/redefine.c" \
+ -o "$base/redefine.out" 2>"$base/redefine.err"; then
+ echo 'macro redefinition warning was not promoted by -Werror' >&2
+ exit 1
+fi
+grep "error: macro 'VALUE' redefined$" "$base/redefine.err" >/dev/null
+
+cat > "$base/paste.c" <<'EOT'
+#define BAD(X) X ## +
+BAD(foo)
+EOT
+if "$MCPU_CPP" --no-config -Werror "$base/paste.c" \
+ -o "$base/paste.out" 2>"$base/paste.err"; then
+ echo 'invalid paste warning was not promoted by -Werror' >&2
+ exit 1
+fi
+grep 'error: pasting "foo" and "+" does not give a valid preprocessing token$' \
+ "$base/paste.err" >/dev/null
diff --git a/tests/t0053-interface-cleanup.sh b/tests/t0053-interface-cleanup.sh
new file mode 100755
index 0000000..80d5410
--- /dev/null
+++ b/tests/t0053-interface-cleanup.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-interface-cleanup-$$"
+mkdir -p "$base/wrapper" "$base/after"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+# --version identifies the independent MCPU languages preprocessor.
+"$MCPU_CPP" --version > "$base/version"
+printf '%s\n' 'mcpu-cpp 1.0.2' 'MCPU languages preprocessor' > "$base/version.expected"
+cmp "$base/version.expected" "$base/version"
+
+# --help contains the current public interface, including --object-suffix,
+# and puts the stdin/stdout notes after the option list.
+"$MCPU_CPP" --help > "$base/help"
+grep '^mcpu-cpp 1\.0\.2$' "$base/help" >/dev/null
+grep '^MCPU languages preprocessor$' "$base/help" >/dev/null
+grep '^ --object-suffix SFX set object suffix used for dependency targets$' "$base/help" >/dev/null
+help_version_line=$(grep -n '^ --version ' "$base/help" | cut -d: -f1)
+input_note_line=$(grep -n "^If input is omitted or '-', read standard input\.$" "$base/help" | cut -d: -f1)
+test "$input_note_line" -gt "$help_version_line"
+
+grep "^If output is omitted or '-', write standard output\.$" "$base/help" >/dev/null
+
+# --object-suffix requires an argument and changes the default dependency target.
+cat > "$base/object.c" <<'SRC'
+int x;
+SRC
+if "$MCPU_CPP" --no-config --object-suffix > /dev/null 2> "$base/object.err"; then
+ echo '--object-suffix unexpectedly accepted without an argument' >&2
+ exit 1
+fi
+grep "argument missing after '--object-suffix'" "$base/object.err" >/dev/null
+"$MCPU_CPP" --no-config --object-suffix .obj -M "$base/object.c" > "$base/object.d"
+grep '^object\.obj:' "$base/object.d" >/dev/null
diff --git a/tests/t0054-inhibit-warnings.sh b/tests/t0054-inhibit-warnings.sh
new file mode 100755
index 0000000..339d098
--- /dev/null
+++ b/tests/t0054-inhibit-warnings.sh
@@ -0,0 +1,81 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-inhibit-warnings-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/directive.c" <<'EOT'
+#warning requested warning
+ok
+EOT
+
+# -w suppresses #warning completely.
+"$MCPU_CPP" --no-config -w "$base/directive.c" \
+ -o "$base/directive.out" 2>"$base/directive.err"
+test ! -s "$base/directive.err"
+grep '^ok$' "$base/directive.out" >/dev/null
+
+# -w is a global warning gate, so command-line order relative to -Werror
+# does not matter and there is no warning left to promote.
+for options in '-w -Werror' '-Werror -w'; do
+ # shellcheck disable=SC2086
+ "$MCPU_CPP" --no-config $options "$base/directive.c" \
+ -o "$base/werror.out" 2>"$base/werror.err"
+ test ! -s "$base/werror.err"
+done
+
+cat > "$base/width.c" <<'EOT'
+#if 1z24 == 1
+ok
+#endif
+EOT
+"$MCPU_CPP" --no-config -w "$base/width.c" \
+ -o "$base/width.out" 2>"$base/width.err"
+test ! -s "$base/width.err"
+grep '^ok$' "$base/width.out" >/dev/null
+
+cat > "$base/redefine.c" <<'EOT'
+#define VALUE 1
+#define VALUE 2
+VALUE
+EOT
+"$MCPU_CPP" --no-config -w "$base/redefine.c" \
+ -o "$base/redefine.out" 2>"$base/redefine.err"
+test ! -s "$base/redefine.err"
+grep '^2$' "$base/redefine.out" >/dev/null
+
+cat > "$base/paste.c" <<'EOT'
+#define BAD(X) X ## +
+BAD(foo)
+EOT
+"$MCPU_CPP" --no-config -w "$base/paste.c" \
+ -o "$base/paste.out" 2>"$base/paste.err"
+test ! -s "$base/paste.err"
+
+cat > "$base/comments.c" <<'EOT'
+/* outer /* nested */
+// continued \
+comment
+ok
+EOT
+
+# A specific warning class may be enabled, but -w still suppresses its output.
+# Command-line order does not matter.
+for options in '-w -Wcomment' '-Wcomment -w' '-w -Wall' '-Wall -w'; do
+ # shellcheck disable=SC2086
+ "$MCPU_CPP" --no-config $options "$base/comments.c" \
+ -o "$base/comments.out" 2>"$base/comments.err"
+ test ! -s "$base/comments.err"
+ grep '^ok$' "$base/comments.out" >/dev/null
+done
+
+# Errors are not warnings and must remain visible/fatal under -w.
+cat > "$base/error.c" <<'EOT'
+#error requested error
+EOT
+if "$MCPU_CPP" --no-config -w "$base/error.c" \
+ -o "$base/error.out" 2>"$base/error.err"; then
+ echo '-w suppressed a real preprocessing error' >&2
+ exit 1
+fi
+grep 'error: #error requested error$' "$base/error.err" >/dev/null
diff --git a/tests/t0055-forced-files.sh b/tests/t0055-forced-files.sh
new file mode 100755
index 0000000..454fa55
--- /dev/null
+++ b/tests/t0055-forced-files.sh
@@ -0,0 +1,242 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-forced-files-$$"
+mkdir -p "$base/project/src" "$base/project/user" "$base/project/sys"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/project/defs1.h" <<'EOT'
+IMACROS1_BODY_MUST_NOT_APPEAR
+#if CLI_VALUE != 9
+#error command-line macros were not applied before -imacros
+#endif
+#if __INCLUDE_LEVEL__ != 1
+#error -imacros must run at include level 1
+#endif
+#define ORDER_VALUE 1
+#define FROM_IMACROS1 11
+#include "imacros-child.h"
+#lang "as"
+#define FROM_AS_LANG 13
+#endlang
+EOT
+
+cat > "$base/project/imacros-child.h" <<'EOT'
+IMACROS_CHILD_BODY_MUST_NOT_APPEAR
+#if __INCLUDE_LEVEL__ != 2
+#error nested include from -imacros must run at include level 2
+#endif
+#define FROM_IMACROS_CHILD 12
+EOT
+
+cat > "$base/project/defs2.h" <<'EOT'
+IMACROS2_BODY_MUST_NOT_APPEAR
+#if ORDER_VALUE != 1
+#error -imacros command-line order is broken
+#endif
+#if FROM_IMACROS_CHILD != 12 || FROM_AS_LANG != 13
+#error -imacros did not retain preprocessing state
+#endif
+#undef ORDER_VALUE
+#define ORDER_VALUE 2
+EOT
+
+cat > "$base/project/a.h" <<'EOT'
+#if ORDER_VALUE != 2
+#error all -imacros files must precede all -include files
+#endif
+INCLUDE_A_BODY
+FORCED_LEVEL __INCLUDE_LEVEL__
+FORCED_BASE __BASE_FILE__
+#undef ORDER_VALUE
+#define ORDER_VALUE 3
+EOT
+
+cat > "$base/project/b.h" <<'EOT'
+#if ORDER_VALUE != 3
+#error -include command-line order is broken
+#endif
+INCLUDE_B_BODY
+#undef ORDER_VALUE
+#define ORDER_VALUE 4
+EOT
+
+cat > "$base/project/src/main.c" <<'EOT'
+#if ORDER_VALUE != 4
+#error forced files did not complete before primary input
+#endif
+MAIN_BODY
+MAIN_LEVEL __INCLUDE_LEVEL__
+FROM_IMACROS1 FROM_IMACROS_CHILD FROM_AS_LANG CLI_VALUE
+EOT
+
+# Deliberately interleave -include and -imacros. The semantic order is all
+# command-line -D/-U first, then all -imacros in their own argv order, then all
+# -include in their own argv order, and only then the primary input.
+(
+ cd "$base/project"
+ "$MCPU_CPP" --no-config \
+ -include a.h -DCLI_VALUE=9 -imacros defs1.h \
+ -include b.h -imacros defs2.h \
+ src/main.c -o "$base/order.out"
+)
+
+! grep 'IMACROS1_BODY_MUST_NOT_APPEAR' "$base/order.out" >/dev/null
+! grep 'IMACROS2_BODY_MUST_NOT_APPEAR' "$base/order.out" >/dev/null
+! grep 'IMACROS_CHILD_BODY_MUST_NOT_APPEAR' "$base/order.out" >/dev/null
+! grep '^#lang ' "$base/order.out" >/dev/null
+! grep '^#endlang' "$base/order.out" >/dev/null
+
+awk '
+ /^INCLUDE_A_BODY$/ { a = NR }
+ /^INCLUDE_B_BODY$/ { b = NR }
+ /^MAIN_BODY$/ { c = NR }
+ END { exit !(a && a < b && b < c) }
+' "$base/order.out"
+grep '^11 12 13 9$' "$base/order.out" >/dev/null
+grep '^FORCED_LEVEL 1$' "$base/order.out" >/dev/null
+grep '^FORCED_BASE "src/main.c"$' "$base/order.out" >/dev/null
+grep '^MAIN_LEVEL 0$' "$base/order.out" >/dev/null
+# Each visible forced include returns to the primary file in the line-marker
+# stack before another forced include or the primary body continues.
+test `grep -c '^# 1 "src/main.c" 2$' "$base/order.out"` -eq 2
+
+# A forced command-line file is searched from the current working directory,
+# not from the physical directory of the primary input.
+cat > "$base/project/cwd-first.h" <<'EOT'
+CWD_FORCED_HEADER
+EOT
+cat > "$base/project/src/cwd-first.h" <<'EOT'
+PRIMARY_DIRECTORY_MUST_NOT_WIN
+EOT
+cat > "$base/project/src/cwd-main.c" <<'EOT'
+CWD_MAIN
+EOT
+(
+ cd "$base/project"
+ "$MCPU_CPP" --no-config -include cwd-first.h src/cwd-main.c \
+ -o "$base/cwd.out"
+)
+grep '^CWD_FORCED_HEADER$' "$base/cwd.out" >/dev/null
+! grep 'PRIMARY_DIRECTORY_MUST_NOT_WIN' "$base/cwd.out" >/dev/null
+
+# After CWD, the ordinary quoted include chain is used. A forced file found
+# through -I retains its physical provenance, so quoted includes inside it are
+# resolved relative to that file's own directory.
+cat > "$base/project/user/forced-user.h" <<'EOT'
+FORCED_USER_BEGIN
+#include "forced-child.h"
+FORCED_USER_END
+EOT
+cat > "$base/project/user/forced-child.h" <<'EOT'
+FORCED_USER_CHILD
+EOT
+(
+ cd "$base/project"
+ "$MCPU_CPP" --no-config -I user -include forced-user.h src/cwd-main.c \
+ -o "$base/user.out"
+)
+grep '^FORCED_USER_BEGIN$' "$base/user.out" >/dev/null
+grep '^FORCED_USER_CHILD$' "$base/user.out" >/dev/null
+grep '^FORCED_USER_END$' "$base/user.out" >/dev/null
+
+# Search-chain provenance of a forced file is retained for #include_next.
+cat > "$base/project/user/wrapped.h" <<'EOT'
+FORCED_WRAPPER
+#include_next <wrapped.h>
+EOT
+cat > "$base/project/sys/wrapped.h" <<'EOT'
+FORCED_INCLUDE_NEXT
+EOT
+(
+ cd "$base/project"
+ "$MCPU_CPP" --no-config -I user -isystem sys -include wrapped.h \
+ src/cwd-main.c -o "$base/next.out"
+)
+grep '^FORCED_WRAPPER$' "$base/next.out" >/dev/null
+grep '^FORCED_INCLUDE_NEXT$' "$base/next.out" >/dev/null
+
+# -dD does not make normal output from -imacros visible; macro state still
+# reaches the primary input.
+cat > "$base/project/only-macros.h" <<'EOT'
+IMACROS_DD_BODY_MUST_NOT_APPEAR
+#define DD_VALUE 77
+EOT
+cat > "$base/project/src/dd-main.c" <<'EOT'
+DD_VALUE
+EOT
+(
+ cd "$base/project"
+ "$MCPU_CPP" --no-config -dD -imacros only-macros.h src/dd-main.c \
+ -o "$base/dd.out"
+)
+! grep 'IMACROS_DD_BODY_MUST_NOT_APPEAR' "$base/dd.out" >/dev/null
+! grep '^#define DD_VALUE 77$' "$base/dd.out" >/dev/null
+grep '^77$' "$base/dd.out" >/dev/null
+
+# Forced files participate in physical dependency tracking. The primary input
+# remains the first dependency even though forced files are preprocessed first.
+(
+ cd "$base/project"
+ "$MCPU_CPP" --no-config -M -DCLI_VALUE=9 \
+ -imacros defs1.h -imacros defs2.h -include a.h -include b.h src/main.c \
+ > "$base/deps.out"
+)
+grep '^main\.o:' "$base/deps.out" >/dev/null
+first_dep=`sed 's/^[^:]*: //' "$base/deps.out" | awk '{print $1}'`
+test "$first_dep" = 'src/main.c' || {
+ echo 'primary input is not the first forced-file dependency' >&2
+ exit 1
+}
+for f in defs1.h imacros-child.h defs2.h a.h b.h; do
+ grep -E "(^|[[:space:]])(\./)?$f([[:space:]]|$)" "$base/deps.out" >/dev/null || {
+ echo "forced dependency missing: $f" >&2
+ exit 1
+ }
+done
+
+# A forced file found through -isystem has system dependency class and is
+# therefore omitted by -MM while the primary user source remains.
+cat > "$base/project/sys/system-forced.h" <<'EOT'
+SYSTEM_FORCED_BODY
+EOT
+(
+ cd "$base/project"
+ "$MCPU_CPP" --no-config -MM -isystem sys -include system-forced.h \
+ src/cwd-main.c > "$base/mm.out"
+)
+grep -E '(^|[[:space:]])src/cwd-main\.c([[:space:]]|$)' "$base/mm.out" >/dev/null
+! grep -F "$base/project/sys/system-forced.h" "$base/mm.out" >/dev/null
+
+# Public help advertises both implemented forced-file options.
+"$MCPU_CPP" --help > "$base/help"
+grep '^ -imacros FILE ' "$base/help" >/dev/null
+grep '^ -include FILE ' "$base/help" >/dev/null
+
+# Both option families require an argument.
+for opt in -include -imacros; do
+ if "$MCPU_CPP" --no-config "$opt" > /dev/null 2>"$base/missing-arg.err"; then
+ echo "$opt accepted a missing argument" >&2
+ exit 1
+ fi
+ grep -- "argument missing after '$opt'" "$base/missing-arg.err" >/dev/null
+done
+
+# Missing forced files are preprocessing errors for both option families.
+for opt in -include -imacros; do
+ if (
+ cd "$base/project"
+ "$MCPU_CPP" --no-config "$opt" no-such-forced-file.h src/cwd-main.c \
+ -o "$base/missing.out" 2>"$base/missing.err"
+ ); then
+ echo "$opt accepted a missing forced file" >&2
+ exit 1
+ fi
+ grep -- "$opt cannot find file 'no-such-forced-file.h'" "$base/missing.err" >/dev/null
+done
+
+# The same forced-file pipeline also applies when the primary source is stdin.
+printf 'DD_VALUE\n' | (
+ cd "$base/project"
+ "$MCPU_CPP" --no-config -imacros only-macros.h -
+) > "$base/stdin.out"
+grep '^77$' "$base/stdin.out" >/dev/null
diff --git a/tests/t0056-dependency-targets.sh b/tests/t0056-dependency-targets.sh
new file mode 100755
index 0000000..ea5f4b9
--- /dev/null
+++ b/tests/t0056-dependency-targets.sh
@@ -0,0 +1,103 @@
+#!/bin/sh
+set -eu
+
+base="${TMPDIR-/tmp}/mcpu-cpp-dependency-targets-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cd "$base"
+
+cat > main.c <<'EOT'
+#include "local.h"
+int main_value;
+EOT
+
+cat > local.h <<'EOT'
+#define LOCAL_VALUE 1
+EOT
+
+# The established default target remains make-quoted and uses the configured
+# object suffix only when no explicit -MT/-MQ target is present.
+"$MCPU_CPP" --no-config -M main.c > default.d
+grep '^main\.o: main\.c \./local\.h$' default.d >/dev/null
+
+"$MCPU_CPP" --no-config --object-suffix .obj -M main.c > suffix.d
+grep '^main\.obj: main\.c \./local\.h$' suffix.d >/dev/null
+
+# -MT replaces the default target and is emitted exactly as supplied.
+"$MCPU_CPP" --no-config -M -MT build/main.o main.c > mt.d
+grep '^build/main\.o: main\.c \./local\.h$' mt.d >/dev/null
+
+"$MCPU_CPP" --no-config -M -MTattached.o main.c > mt-attached.d
+grep '^attached\.o: main\.c \./local\.h$' mt-attached.d >/dev/null
+
+"$MCPU_CPP" --no-config -M -MT 'raw one raw#two$' main.c > mt-raw.d
+grep '^raw one raw#two\$: main\.c \./local\.h$' mt-raw.d >/dev/null
+
+# -MQ applies Make quoting to the target.
+"$MCPU_CPP" --no-config -M -MQ '$(OBJDIR)/main.o' main.c > mq.d
+grep '^\$\$(OBJDIR)/main\.o: main\.c \./local\.h$' mq.d >/dev/null
+
+"$MCPU_CPP" --no-config -M '-MQ$(OBJDIR)/attached.o' main.c > mq-attached.d
+grep '^\$\$(OBJDIR)/attached\.o: main\.c \./local\.h$' mq-attached.d >/dev/null
+
+"$MCPU_CPP" --no-config -M -MQ 'dir name/#x$.o' main.c > mq-special.d
+grep '^dir\\ name/\\#x\$\$\.o: main\.c \./local\.h$' mq-special.d >/dev/null
+
+"$MCPU_CPP" --no-config -M -MQ 'path\name.o' main.c > mq-backslash.d
+grep -F 'path\name.o: main.c ./local.h' mq-backslash.d >/dev/null
+
+# Multiple targets produce one rule. As in GNU CPP, all -MT targets precede
+# all -MQ targets; command-line order is preserved inside each class.
+"$MCPU_CPP" --no-config -M \
+ -MQ '$(DIR)/quoted1.o' \
+ -MT raw1.o \
+ -MQ 'quoted two.o' \
+ -MT 'raw2.o raw3.o' \
+ main.c > multiple.d
+grep '^raw1\.o raw2\.o raw3\.o \$\$(DIR)/quoted1\.o quoted\\ two\.o: main\.c \./local\.h$' multiple.d >/dev/null
+
+# Explicit targets override the automatic target even when an object suffix was
+# requested.
+"$MCPU_CPP" --no-config --object-suffix .obj -M -MT explicit.target main.c > explicit.d
+grep '^explicit\.target: main\.c \./local\.h$' explicit.d >/dev/null
+
+# -MT/-MQ work in side-effect dependency modes and with -MF.
+"$MCPU_CPP" --no-config -MD -MF md.d -MT md-target.o main.c > md.i
+grep '^md-target\.o: main\.c \./local\.h$' md.d >/dev/null
+grep '^int main_value;$' md.i >/dev/null
+
+"$MCPU_CPP" --no-config -MMD -MF mmd.d -MQ '$(OUT)/mmd.o' main.c > mmd.i
+grep '^\$\$(OUT)/mmd\.o: main\.c \./local\.h$' mmd.d >/dev/null
+grep '^int main_value;$' mmd.i >/dev/null
+
+# Both options require an active dependency-generation mode.
+if "$MCPU_CPP" --no-config -MT orphan.o main.c > orphan.out 2> orphan.err; then
+ echo "-MT without dependency generation unexpectedly succeeded" >&2
+ exit 1
+fi
+grep -- '-MT/-MQ require one of -M, -MM, -MD or -MMD' orphan.err >/dev/null
+
+if "$MCPU_CPP" --no-config -MQ orphan.o main.c > orphan-q.out 2> orphan-q.err; then
+ echo "-MQ without dependency generation unexpectedly succeeded" >&2
+ exit 1
+fi
+grep -- '-MT/-MQ require one of -M, -MM, -MD or -MMD' orphan-q.err >/dev/null
+
+# Missing arguments remain command-line errors.
+if "$MCPU_CPP" --no-config -M -MT > missing-mt.out 2> missing-mt.err; then
+ echo "-MT without an argument unexpectedly succeeded" >&2
+ exit 1
+fi
+grep "argument missing after '-MT'" missing-mt.err >/dev/null
+
+if "$MCPU_CPP" --no-config -M -MQ > missing-mq.out 2> missing-mq.err; then
+ echo "-MQ without an argument unexpectedly succeeded" >&2
+ exit 1
+fi
+grep "argument missing after '-MQ'" missing-mq.err >/dev/null
+
+# Public help documents both options.
+"$MCPU_CPP" --help > help.out
+grep '^ -MT TARGET set unquoted make dependency target$' help.out >/dev/null
+grep '^ -MQ TARGET set make-quoted dependency target$' help.out >/dev/null
diff --git a/tests/t0057-missing-generated-dependencies.sh b/tests/t0057-missing-generated-dependencies.sh
new file mode 100755
index 0000000..7ad5001
--- /dev/null
+++ b/tests/t0057-missing-generated-dependencies.sh
@@ -0,0 +1,166 @@
+#!/bin/sh
+set -eu
+
+base="${TMPDIR-/tmp}/mcpu-cpp-missing-generated-deps-$$"
+mkdir -p "$base/user" "$base/sys"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cd "$base"
+
+cat > main.c <<'EOT'
+#include "generated-user.h"
+#include <generated-system.h>
+#define GENERATED_MACRO_HEADER "generated-macro.h"
+#include GENERATED_MACRO_HEADER
+EOT
+
+# Without -MG, the first unresolved include remains a hard error.
+if "$MCPU_CPP" --no-config -M main.c > no-mg.out 2> no-mg.err; then
+ echo "missing include unexpectedly succeeded without -MG" >&2
+ exit 1
+fi
+grep "cannot find include file 'generated-user.h'" no-mg.err >/dev/null
+
+# -M -MG records unresolved operands exactly as written and does not diagnose
+# them as missing files. Macro-expanded include operands use their expanded
+# filename.
+"$MCPU_CPP" --no-config -M -MG main.c > M.d 2> M.err
+test ! -s M.err
+grep '^main\.o:' M.d >/dev/null
+for f in main.c generated-user.h generated-system.h generated-macro.h
+do
+ grep -F "$f" M.d >/dev/null || {
+ echo "-M -MG omitted unresolved dependency: $f" >&2
+ exit 1
+ }
+done
+
+# -MM retains unresolved quoted/user includes but filters unresolved angle
+# includes as system-class dependencies.
+"$MCPU_CPP" --no-config -MM -MG main.c > MM.d
+grep -F 'generated-user.h' MM.d >/dev/null
+grep -F 'generated-macro.h' MM.d >/dev/null
+if grep -F 'generated-system.h' MM.d >/dev/null; then
+ echo "-MM -MG kept unresolved angle/system dependency" >&2
+ exit 1
+fi
+
+# Missing dependencies reached from a real system header inherit system
+# context, even when the nested directive uses quotes. User-header quoted
+# descendants remain user dependencies.
+cat > sys/system-parent.h <<'EOT'
+#include "generated-from-system.h"
+EOT
+cat > user/user-parent.h <<'EOT'
+#include "generated-from-user.h"
+EOT
+cat > nested.c <<'EOT'
+#include <system-parent.h>
+#include <user-parent.h>
+EOT
+
+"$MCPU_CPP" --no-config -MM -MG -I user -isystem sys nested.c > nested-MM.d
+grep -F 'user/user-parent.h' nested-MM.d >/dev/null
+grep -F 'generated-from-user.h' nested-MM.d >/dev/null
+if grep -F 'sys/system-parent.h' nested-MM.d >/dev/null ||
+ grep -F 'generated-from-system.h' nested-MM.d >/dev/null; then
+ echo "-MM -MG kept system-context dependency" >&2
+ exit 1
+fi
+
+"$MCPU_CPP" --no-config -M -MG -I user -isystem sys nested.c > nested-M.d
+grep -F 'sys/system-parent.h' nested-M.d >/dev/null
+grep -F 'generated-from-system.h' nested-M.d >/dev/null
+
+# Unresolved dependency identity is textual and separate from physical
+# st_dev/st_ino identity. shadow.h exists in CWD, but <shadow.h> is not found
+# through the angle search chain under --no-config; a later quoted ./shadow.h
+# resolves that physical file. Both registry entries must survive under -M.
+cat > shadow.h <<'EOT'
+#define SHADOW_VALUE 1
+EOT
+cat > identity.c <<'EOT'
+#include <shadow.h>
+#include "./shadow.h"
+EOT
+"$MCPU_CPP" --no-config -M -MG identity.c > identity-M.d
+grep '^identity\.o: identity\.c shadow\.h \./\./shadow\.h$' identity-M.d >/dev/null || {
+ echo "unresolved and physical dependency identities were incorrectly merged" >&2
+ cat identity-M.d >&2
+ exit 1
+}
+
+# In -MM the unresolved angle entry is filtered, while the resolved quoted
+# physical dependency remains.
+"$MCPU_CPP" --no-config -MM -MG identity.c > identity-MM.d
+grep -F '././shadow.h' identity-MM.d >/dev/null
+case "$(cat identity-MM.d)" in
+ *' shadow.h '*)
+ echo "-MM kept the unresolved angle dependency" >&2
+ exit 1
+ ;;
+esac
+
+# Repeated unresolved operands are deduplicated textually. Their first
+# classification is retained, matching GNU CPP: angle-first stays system-only,
+# while quote-first stays visible to -MM.
+cat > first-system.c <<'EOT'
+#include <same-missing.h>
+#include "same-missing.h"
+EOT
+"$MCPU_CPP" --no-config -MM -MG first-system.c > first-system.d
+if grep -F 'same-missing.h' first-system.d >/dev/null; then
+ echo "angle-first unresolved dependency lost its first classification" >&2
+ exit 1
+fi
+
+cat > first-user.c <<'EOT'
+#include "same-missing.h"
+#include <same-missing.h>
+EOT
+"$MCPU_CPP" --no-config -MM -MG first-user.c > first-user.d
+grep -F 'same-missing.h' first-user.d >/dev/null
+
+# Different unresolved spellings are distinct because no physical identity is
+# available to canonicalize them.
+cat > spelling.c <<'EOT'
+#include "generated.h"
+#include "./generated.h"
+EOT
+"$MCPU_CPP" --no-config -M -MG spelling.c > spelling.d
+grep -F ' generated.h' spelling.d >/dev/null
+grep -F ' ./generated.h' spelling.d >/dev/null
+
+# -MG also applies to missing command-line forced files. They enter the same
+# unresolved registry as user dependencies and preserve the command-line
+# operand; no synthetic path is prepended.
+: > forced.c
+"$MCPU_CPP" --no-config -M -MG \
+ -imacros missing-macros.h -include missing-include.h forced.c > forced-M.d
+grep -F 'missing-macros.h' forced-M.d >/dev/null
+grep -F 'missing-include.h' forced-M.d >/dev/null
+
+"$MCPU_CPP" --no-config -MM -MG \
+ -imacros missing-macros.h -include missing-include.h forced.c > forced-MM.d
+grep -F 'missing-macros.h' forced-MM.d >/dev/null
+grep -F 'missing-include.h' forced-MM.d >/dev/null
+
+# -MG is dependency-only: GNU permits it with -M/-MM, not side-effect -MD/-MMD
+# and not without dependency generation.
+for opts in '-MG' '-MD -MG' '-MMD -MG'
+do
+ if "$MCPU_CPP" --no-config $opts forced.c > invalid.out 2> invalid.err; then
+ echo "$opts unexpectedly accepted" >&2
+ exit 1
+ fi
+ grep -- '-MG may only be used with -M or -MM' invalid.err >/dev/null
+done
+
+# Explicit dependency targets continue to compose with -MG.
+"$MCPU_CPP" --no-config -M -MG -MT generated-target.o main.c > target.d
+grep '^generated-target\.o:' target.d >/dev/null
+grep -F 'generated-user.h' target.d >/dev/null
+
+# Public help exposes the implemented option.
+"$MCPU_CPP" --help > help.out
+grep '^ -MG treat missing headers as generated dependencies$' help.out >/dev/null
diff --git a/tests/t0058-variadic-macros.sh b/tests/t0058-variadic-macros.sh
new file mode 100755
index 0000000..584e980
--- /dev/null
+++ b/tests/t0058-variadic-macros.sh
@@ -0,0 +1,135 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-variadic-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/main.c" <<'EOT'
+#define A 7
+#define SUM(X, Y) X + Y
+#define V(...) <__VA_ARGS__>
+#define F(first, ...) first | __VA_ARGS__
+#define STRV(...) #__VA_ARGS__
+#define LEFT(...) pre ## __VA_ARGS__
+#define RIGHT(...) __VA_ARGS__ ## post
+#define CALL(FN, ...) FN(__VA_ARGS__)
+#define WRAP(...) V(__VA_ARGS__)
+#define ID(X) X
+#define РУССКИЙ(первый, ...) первый : __VA_ARGS__
+
+V(alpha, beta, gamma)
+V()
+F(one, two, three)
+F(one)
+F(one,)
+F(A, SUM(1, 2))
+STRV(A, b + c)
+LEFT(fix)
+LEFT()
+RIGHT(fix)
+RIGHT()
+CALL(ID, A)
+WRAP(A, SUM(3, 4))
+РУССКИЙ(один, два, три)
+EOT
+
+"$MCPU_CPP" --no-config "$base/main.c" -o "$base/out"
+
+grep -Fx '<alpha, beta, gamma>' "$base/out" >/dev/null
+grep -Fx '<>' "$base/out" >/dev/null
+grep -Fx 'one | two, three' "$base/out" >/dev/null
+# Both an omitted variadic tail and an explicitly empty one expand to no tokens.
+test "$(grep -Fxc 'one | ' "$base/out")" -eq 2
+grep -Fx '7 | 1 + 2' "$base/out" >/dev/null
+grep -Fx '"A, b + c"' "$base/out" >/dev/null
+grep -Fx 'prefix' "$base/out" >/dev/null
+grep -Fx 'pre' "$base/out" >/dev/null
+grep -Fx 'fixpost' "$base/out" >/dev/null
+grep -Fx 'post' "$base/out" >/dev/null
+grep -Fx '7' "$base/out" >/dev/null
+grep -Fx '<7, 3 + 4>' "$base/out" >/dev/null
+grep -Fx 'один : два, три' "$base/out" >/dev/null
+
+# The variable argument is preserved as such when macro definitions are dumped.
+"$MCPU_CPP" --no-config -dM "$base/main.c" > "$base/dump"
+grep -Fx '#define V(...) <__VA_ARGS__>' "$base/dump" >/dev/null
+grep -Fx '#define F(first,...) first | __VA_ARGS__' "$base/dump" >/dev/null
+grep -Fx '#define STRV(...) #__VA_ARGS__' "$base/dump" >/dev/null
+grep -Fx '#define РУССКИЙ(первый,...) первый : __VA_ARGS__' "$base/dump" >/dev/null
+
+
+# Variadic-ness is part of a macro definition. Repeating the same variadic
+# definition is harmless, while changing a non-variadic definition into a
+# variadic one is a real redefinition and therefore participates in -Werror.
+cat > "$base/redefine-same.c" <<'EOT'
+#define SAME(x, ...) x | __VA_ARGS__
+#define SAME(x, ...) x | __VA_ARGS__
+SAME(1, 2)
+EOT
+"$MCPU_CPP" --no-config "$base/redefine-same.c" \
+ -o "$base/redefine-same.out" 2>"$base/redefine-same.err"
+test ! -s "$base/redefine-same.err"
+grep -Fx '1 | 2' "$base/redefine-same.out" >/dev/null
+
+cat > "$base/redefine-kind.c" <<'EOT'
+#define CHANGE(x) x
+#define CHANGE(x, ...) x | __VA_ARGS__
+EOT
+if "$MCPU_CPP" --no-config -Werror "$base/redefine-kind.c" \
+ -o "$base/redefine-kind.out" 2>"$base/redefine-kind.err"; then
+ echo 'variadic/non-variadic redefinition escaped -Werror' >&2
+ exit 1
+fi
+grep "error: macro 'CHANGE' redefined" "$base/redefine-kind.err" >/dev/null
+
+# A variadic macro may have fixed parameters, but those fixed parameters are
+# still required. An empty spelling counts as an argument, just as it does
+# for ordinary function-like macros.
+cat > "$base/few.c" <<'EOT'
+#define NEEDS_TWO(a, b, ...) a + b + __VA_ARGS__
+NEEDS_TWO(1)
+EOT
+if "$MCPU_CPP" --no-config "$base/few.c" -o "$base/few.out" 2>"$base/few.err"; then
+ echo 'variadic macro accepted too few fixed arguments' >&2
+ exit 1
+fi
+grep "macro 'NEEDS_TWO' used with too few arguments" "$base/few.err" >/dev/null
+
+# The old GNU named variadic form is deliberately outside the 0.0.46
+# contract. Only the C99-style ... + __VA_ARGS__ form is accepted.
+cat > "$base/named.c" <<'EOT'
+#define OLD(args...) args
+EOT
+if "$MCPU_CPP" --no-config "$base/named.c" -o "$base/named.out" 2>"$base/named.err"; then
+ echo 'GNU named variadic macro form unexpectedly accepted' >&2
+ exit 1
+fi
+grep 'badly punctuated parameter list in #define' "$base/named.err" >/dev/null
+
+cat > "$base/malformed.c" <<'EOT'
+#define BAD(..., x) x
+EOT
+if "$MCPU_CPP" --no-config "$base/malformed.c" -o "$base/malformed.out" 2>"$base/malformed.err"; then
+ echo 'parameters after ... unexpectedly accepted' >&2
+ exit 1
+fi
+grep 'badly punctuated parameter list in #define' "$base/malformed.err" >/dev/null
+
+cat > "$base/reserved.c" <<'EOT'
+#define BAD(__VA_ARGS__) __VA_ARGS__
+EOT
+if "$MCPU_CPP" --no-config "$base/reserved.c" -o "$base/reserved.out" 2>"$base/reserved.err"; then
+ echo '__VA_ARGS__ unexpectedly accepted as an ordinary parameter name' >&2
+ exit 1
+fi
+grep "'__VA_ARGS__' cannot be used as a macro parameter name" "$base/reserved.err" >/dev/null
+
+# A replacement using #__VA_ARGS__ is valid only for a variadic macro.
+cat > "$base/not-variadic.c" <<'EOT'
+#define BAD(x) #__VA_ARGS__
+EOT
+if "$MCPU_CPP" --no-config "$base/not-variadic.c" -o "$base/not-variadic.out" 2>"$base/not-variadic.err"; then
+ echo '#__VA_ARGS__ unexpectedly accepted in a non-variadic macro' >&2
+ exit 1
+fi
+grep "'#' operator should be followed by a macro argument name" "$base/not-variadic.err" >/dev/null
diff --git a/tests/t0059-va-opt.sh b/tests/t0059-va-opt.sh
new file mode 100755
index 0000000..052df6c
--- /dev/null
+++ b/tests/t0059-va-opt.sh
@@ -0,0 +1,150 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-va-opt-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/main.c" <<'EOT'
+#define EMPTY
+#define X 123
+#define xy RESCANNED
+#define HAS(...) __VA_OPT__(yes)
+#define COMMA(format, ...) call(format __VA_OPT__(,) __VA_ARGS__)
+#define STR(...) #__VA_OPT__(__VA_ARGS__)
+#define STRFIX(a, ...) #__VA_OPT__(a __VA_ARGS__)
+#define STRPASTE(a, ...) #__VA_OPT__(a ## z)
+#define LEFT(...) pre ## __VA_OPT__(__VA_ARGS__)
+#define RIGHT(...) __VA_OPT__(__VA_ARGS__) ## post
+#define INNER(a, ...) __VA_OPT__(a ## y)
+#define BALANCED(...) __VA_OPT__((a, (b, c)))
+#define EMPTY_LEFT(...) x ## __VA_OPT__()
+#define EMPTY_RIGHT(...) __VA_OPT__() ## y
+#define TEXT(...) "__VA_OPT__(not syntax)" __VA_OPT__(ok)
+
+[HAS()]
+[HAS(EMPTY)]
+[HAS(token)]
+COMMA("zero")
+COMMA("two", X, 7)
+STR()
+STR(X)
+STRFIX(X, y)
+STRPASTE(X, y)
+LEFT()
+LEFT(X)
+RIGHT()
+RIGHT(X)
+INNER(x, token)
+BALANCED(token)
+EMPTY_LEFT(token)
+EMPTY_RIGHT(token)
+TEXT()
+TEXT(token)
+EOT
+
+"$MCPU_CPP" --no-config "$base/main.c" -o "$base/out"
+sed '/^#/d; /^[[:space:]]*$/d; s/[[:space:]]//g' "$base/out" > "$base/norm"
+
+cat > "$base/expected" <<'EOT'
+[]
+[]
+[yes]
+call("zero")
+call("two",123,7)
+""
+"123"
+"123y"
+"Xz"
+pre
+pre123
+post
+123post
+RESCANNED
+(a,(b,c))
+x
+y
+"__VA_OPT__(notsyntax)"
+"__VA_OPT__(notsyntax)"ok
+EOT
+
+diff -u "$base/expected" "$base/norm"
+
+# __VA_OPT__ remains visible in macro dumps as part of the replacement list.
+"$MCPU_CPP" --no-config -dM "$base/main.c" > "$base/dump"
+grep -Fx '#define HAS(...) __VA_OPT__(yes)' "$base/dump" >/dev/null
+grep -Fx '#define COMMA(format,...) call(format __VA_OPT__(,) __VA_ARGS__)' "$base/dump" >/dev/null
+grep -Fx '#define STR(...) #__VA_OPT__(__VA_ARGS__)' "$base/dump" >/dev/null
+
+# __VA_OPT__ is structural syntax, not a general identifier-like extension.
+cat > "$base/nonvariadic.c" <<'EOT'
+#define BAD(x) __VA_OPT__(x)
+EOT
+if "$MCPU_CPP" --no-config "$base/nonvariadic.c" -o "$base/nonvariadic.out" 2>"$base/nonvariadic.err"; then
+ echo '__VA_OPT__ unexpectedly accepted in a non-variadic macro' >&2
+ exit 1
+fi
+grep "'__VA_OPT__' may appear only in a variadic macro replacement list" "$base/nonvariadic.err" >/dev/null
+
+cat > "$base/object.c" <<'EOT'
+#define BAD __VA_OPT__(x)
+EOT
+if "$MCPU_CPP" --no-config "$base/object.c" -o "$base/object.out" 2>"$base/object.err"; then
+ echo '__VA_OPT__ unexpectedly accepted in an object-like macro' >&2
+ exit 1
+fi
+grep "'__VA_OPT__' may appear only in a variadic macro replacement list" "$base/object.err" >/dev/null
+
+cat > "$base/nested.c" <<'EOT'
+#define BAD(...) __VA_OPT__(a __VA_OPT__(b))
+EOT
+if "$MCPU_CPP" --no-config "$base/nested.c" -o "$base/nested.out" 2>"$base/nested.err"; then
+ echo 'nested __VA_OPT__ unexpectedly accepted' >&2
+ exit 1
+fi
+grep "'__VA_OPT__' may not appear inside another '__VA_OPT__'" "$base/nested.err" >/dev/null
+
+cat > "$base/no-open.c" <<'EOT'
+#define BAD(...) __VA_OPT__ x
+EOT
+if "$MCPU_CPP" --no-config "$base/no-open.c" -o "$base/no-open.out" 2>"$base/no-open.err"; then
+ echo '__VA_OPT__ without opening parenthesis unexpectedly accepted' >&2
+ exit 1
+fi
+grep "'__VA_OPT__' must be followed by '('" "$base/no-open.err" >/dev/null
+
+cat > "$base/unterminated.c" <<'EOT'
+#define BAD(...) __VA_OPT__((x)
+EOT
+if "$MCPU_CPP" --no-config "$base/unterminated.c" -o "$base/unterminated.out" 2>"$base/unterminated.err"; then
+ echo 'unterminated __VA_OPT__ unexpectedly accepted' >&2
+ exit 1
+fi
+grep "unterminated '__VA_OPT__'" "$base/unterminated.err" >/dev/null
+
+cat > "$base/paste-first.c" <<'EOT'
+#define BAD(...) __VA_OPT__(## x)
+EOT
+if "$MCPU_CPP" --no-config "$base/paste-first.c" -o "$base/paste-first.out" 2>"$base/paste-first.err"; then
+ echo 'leading ## inside __VA_OPT__ unexpectedly accepted' >&2
+ exit 1
+fi
+grep "'##' cannot appear at the beginning of '__VA_OPT__'" "$base/paste-first.err" >/dev/null
+
+cat > "$base/paste-last.c" <<'EOT'
+#define BAD(...) __VA_OPT__(x ##)
+EOT
+if "$MCPU_CPP" --no-config "$base/paste-last.c" -o "$base/paste-last.out" 2>"$base/paste-last.err"; then
+ echo 'trailing ## inside __VA_OPT__ unexpectedly accepted' >&2
+ exit 1
+fi
+grep "'##' cannot appear at the end of '__VA_OPT__'" "$base/paste-last.err" >/dev/null
+
+# The historical GNU comma-swallow extension remains deliberately absent.
+# __VA_OPT__ is the supported way to make a separator conditional.
+cat > "$base/no-gnu-comma.c" <<'EOT'
+#define OLD(format, ...) call(format, ## __VA_ARGS__)
+OLD("x")
+EOT
+"$MCPU_CPP" --no-config "$base/no-gnu-comma.c" -o "$base/no-gnu-comma.out" 2>"$base/no-gnu-comma.err"
+sed '/^#/d; /^[[:space:]]*$/d; s/[[:space:]]//g' "$base/no-gnu-comma.out" > "$base/no-gnu-comma.norm"
+grep -Fx 'call("x",)' "$base/no-gnu-comma.norm" >/dev/null
diff --git a/tests/t0060-macro-whitespace.sh b/tests/t0060-macro-whitespace.sh
new file mode 100755
index 0000000..8f870ba
--- /dev/null
+++ b/tests/t0060-macro-whitespace.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-macro-whitespace-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+cat > "$base/main.c" <<'EOT'
+#define MULTI(fmt, ...) \
+ do \
+ { \
+ output(fmt __VA_OPT__(,) __VA_ARGS__); \
+ done(); \
+ } \
+ while( 0 )
+
+#define TEXT "left right"
+#define OPS + + - > < <
+#define ID(x) x
+#define V(...) __VA_OPT__(__VA_ARGS__)
+
+void
+f( int x )
+{
+ MULTI("x=%d", x);
+ MULTI("hello");
+ ID(alpha + beta);
+ V(gamma + delta);
+ const char *s = TEXT;
+ OPS
+}
+EOT
+
+"$MCPU_CPP" --no-config "$base/main.c" -o "$base/out"
+sed '/^#/d; /^[[:space:]]*$/d' "$base/out" > "$base/norm"
+
+cat > "$base/expected" <<'EOT'
+void
+f( int x )
+{
+ do { output("x=%d" , x); done(); } while( 0 );
+ do { output("hello" ); done(); } while( 0 );
+ alpha + beta;
+ gamma + delta;
+ const char *s = "left right";
+ + + - > < <
+}
+EOT
+
+diff -u "$base/expected" "$base/norm"
+
+# Macro dumps use the same normalized replacement-list whitespace.
+"$MCPU_CPP" --no-config -dM "$base/main.c" > "$base/dump"
+grep -Fx '#define MULTI(fmt,...) do { output(fmt __VA_OPT__(,) __VA_ARGS__); done(); } while( 0 )' "$base/dump" >/dev/null
+grep -Fx '#define TEXT "left right"' "$base/dump" >/dev/null
+grep -Fx '#define OPS + + - > < <' "$base/dump" >/dev/null
+grep -Fx '#define ID(x) x' "$base/dump" >/dev/null
+grep -Fx '#define V(...) __VA_OPT__(__VA_ARGS__)' "$base/dump" >/dev/null
+
+# Ordinary source text that is not replacement-list formatting stays untouched.
+cat > "$base/plain.c" <<'EOT'
+int plain = 1;
+#define ID(x) x
+#define V(...) __VA_OPT__(__VA_ARGS__)
+ID(a + b)
+EOT
+"$MCPU_CPP" --no-config "$base/plain.c" -o "$base/plain.out"
+sed '/^#/d; /^[[:space:]]*$/d' "$base/plain.out" > "$base/plain.norm"
+cat > "$base/plain.expected" <<'EOT'
+int plain = 1;
+a + b
+EOT
+diff -u "$base/plain.expected" "$base/plain.norm"
diff --git a/tests/t0061-output-line-compaction.sh b/tests/t0061-output-line-compaction.sh
new file mode 100755
index 0000000..e40b1ee
--- /dev/null
+++ b/tests/t0061-output-line-compaction.sh
@@ -0,0 +1,131 @@
+#!/bin/sh
+set -eu
+base="${TMPDIR-/tmp}/mcpu-cpp-output-line-compaction-$$"
+mkdir -p "$base"
+trap 'rm -rf "$base"' EXIT HUP INT TERM
+
+# Seven invisible lines stay as seven ordinary newlines. The next visible
+# source line is line 9, and no corrective marker is needed.
+cat > "$base/gap7.c" <<'EOT'
+int a;
+// one
+#define A 1
+#if 0
+hidden
+#endif
+/* six */
+
+int b;
+EOT
+"$MCPU_CPP" --no-config "$base/gap7.c" -o "$base/gap7.out"
+if grep -F "# 9 \"$base/gap7.c\"" "$base/gap7.out" >/dev/null; then
+ echo "unexpected line marker for seven-line invisible gap" >&2
+ exit 1
+fi
+gap7_count=`awk '
+ $0 == "int a;" { inside = 1; next }
+ $0 == "int b;" { print count; exit }
+ inside { ++count }
+' "$base/gap7.out"`
+test "$gap7_count" -eq 7
+
+# Eight invisible lines cross the GNU CPP threshold. They disappear from the
+# byte stream and are represented by a line marker for the next visible line.
+cat > "$base/gap8.c" <<'EOT'
+int a;
+// one
+#define A 1
+#if 0
+hidden
+#endif
+/* six */
+// seven
+
+int b;
+EOT
+"$MCPU_CPP" --no-config "$base/gap8.c" -o "$base/gap8.out"
+grep -F "# 10 \"$base/gap8.c\"" "$base/gap8.out" >/dev/null
+awk '
+ /^# 10 "/ { getline; if( $0 == "int b;" ) ok = 1 }
+ END { exit ok ? 0 : 1 }
+' "$base/gap8.out"
+
+# A completely invisible header has no synthetic end-of-header marker. Only
+# the structural enter/return markers survive. A long invisible gap in the
+# parent is then represented by the parent's next visible source position.
+cat > "$base/silent.h" <<'EOT'
+#ifndef SILENT_H
+#define SILENT_H 1
+#define H1 1
+#define H2 2
+#define H3 3
+#define H4 4
+#define H5 5
+#define H6 6
+#define H7 7
+#define H8 8
+#define H9 9
+#endif
+EOT
+cat > "$base/include.c" <<'EOT'
+// leading comment
+#include "silent.h"
+// one
+#define P1 1
+#if 0
+hidden
+#endif
+// six
+// seven
+
+int visible;
+EOT
+"$MCPU_CPP" --no-config -I"$base" "$base/include.c" -o "$base/include.out"
+grep -F "# 1 \"$base/silent.h\" 1" "$base/include.out" >/dev/null
+grep -F "# 3 \"$base/include.c\" 2" "$base/include.out" >/dev/null
+grep -F "# 11 \"$base/include.c\"" "$base/include.out" >/dev/null
+if grep -E "^# (2|3|4|5|6|7|8|9|10|11|12) \"$base/silent.h\"" "$base/include.out" >/dev/null; then
+ echo "silent header acquired a synthetic progress marker" >&2
+ exit 1
+fi
+awk '
+ /\/silent\.h" 1$/ { getline; if( $0 ~ /\/include\.c" 2$/ ) adjacent = 1 }
+ /^# 11 "/ { getline; if( $0 == "int visible;" ) visible = 1 }
+ END { exit adjacent && visible ? 0 : 1 }
+' "$base/include.out"
+
+# The threshold is based on source position, not on why the lines are
+# invisible. A long run of comments alone is compacted in exactly the same
+# way as directives or inactive conditional text.
+cat > "$base/comments.c" <<'EOT'
+int before;
+// 1
+// 2
+// 3
+// 4
+// 5
+// 6
+// 7
+// 8
+int after;
+EOT
+"$MCPU_CPP" --no-config "$base/comments.c" -o "$base/comments.out"
+grep -F "# 10 \"$base/comments.c\"" "$base/comments.out" >/dev/null
+
+# __LINE__ observes source coordinates, not the compacted output layout.
+cat > "$base/line.c" <<'EOT'
+int a = __LINE__;
+// 1
+// 2
+// 3
+// 4
+// 5
+// 6
+// 7
+// 8
+int b = __LINE__;
+EOT
+"$MCPU_CPP" --no-config "$base/line.c" -o "$base/line.out"
+grep -Fx 'int a = 1;' "$base/line.out" >/dev/null
+grep -Fx 'int b = 10;' "$base/line.out" >/dev/null
+grep -F "# 10 \"$base/line.c\"" "$base/line.out" >/dev/null