commit c69c97e24cedb77bac58398c95fec87a0c06328b
parent c289ef65e1f1eaa78ef7a2370c97a9d7653a6eb6
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 12 Jun 2026 09:42:54 -0700
feat(pp): implement __has_include, __has_include_next, and #include_next
__has_include / __has_include_next were never implemented, so in a #if they
fell through to the identifier->0 rewrite and always read false. Modern system
headers (e.g. the macOS SDK) then took their fallback #else typedefs, which
collided with the real declarations pulled in elsewhere. Add a prepass_has_include
pass (mirroring prepass_defined, run before macro expansion so the <...> operand
isn't mistaken for comparison operators) that resolves the header against the
include search path; report defined(__has_include) as 1 for the guarded idiom.
#include_next was recognized by the lexer but dispatched nowhere. Add the
directive, thread the originating search-dir index through TokSrc.inc_next_start
(memoized via IncResolved.next_start), and resume the search one dir past where
the current file was found.
Diffstat:
8 files changed, 352 insertions(+), 37 deletions(-)
diff --git a/lang/cpp/pp/pp.c b/lang/cpp/pp/pp.c
@@ -246,6 +246,10 @@ static void pp_intern_keywords(Pp* pp) {
pp->sym_define = kit_sym_intern(p->c, KIT_SLICE_LIT("define"));
pp->sym_undef = kit_sym_intern(p->c, KIT_SLICE_LIT("undef"));
pp->sym_include = kit_sym_intern(p->c, KIT_SLICE_LIT("include"));
+ pp->sym_include_next = kit_sym_intern(p->c, KIT_SLICE_LIT("include_next"));
+ pp->sym_has_include = kit_sym_intern(p->c, KIT_SLICE_LIT("__has_include"));
+ pp->sym_has_include_next =
+ kit_sym_intern(p->c, KIT_SLICE_LIT("__has_include_next"));
pp->sym_if = kit_sym_intern(p->c, KIT_SLICE_LIT("if"));
pp->sym_ifdef = kit_sym_intern(p->c, KIT_SLICE_LIT("ifdef"));
pp->sym_ifndef = kit_sym_intern(p->c, KIT_SLICE_LIT("ifndef"));
diff --git a/lang/cpp/pp/pp_directive.c b/lang/cpp/pp/pp_directive.c
@@ -121,13 +121,20 @@ static void prepass_defined(Pp* pp, const Tok* in, u32 nin, TokVec* out) {
}
{
Tok t;
+ /* `__has_include` / `__has_include_next` are not macros (their operand
+ * isn't a normal token sequence) but, like clang/GCC, they answer
+ * `defined()` with 1 so the guarded idiom
+ * #if defined(__has_include) && __has_include(<x>)
+ * — common in system headers — takes its intended branch. */
+ int is_defined = mt_get(pp, ident) != NULL ||
+ ident == pp->sym_has_include ||
+ ident == pp->sym_has_include_next;
memset(&t, 0, sizeof(t));
t.kind = TOK_NUM;
t.flags = in[i].flags & (TF_AT_BOL | TF_HAS_SPACE);
t.loc = in[i].loc;
- t.spelling =
- kit_sym_intern(pp->pool->c, mt_get(pp, ident) ? KIT_SLICE_LIT("1")
- : KIT_SLICE_LIT("0"));
+ t.spelling = kit_sym_intern(
+ pp->pool->c, is_defined ? KIT_SLICE_LIT("1") : KIT_SLICE_LIT("0"));
tv_push(pp, out, t);
}
i = j - 1;
@@ -388,15 +395,114 @@ static i64 ee_ternary(EE* e) {
return c;
}
+/* Header-resolution helpers used by the __has_include pre-pass below; the
+ * definitions live further down with the #include machinery. */
+static void parse_include_path(Pp* pp, const Tok* line, u32 n, SrcLoc loc,
+ char* path_out, size_t cap, int* system_out);
+static int find_and_open_include(Pp* pp, const char* path, int system,
+ SrcLoc loc, const u8** data, size_t* size,
+ char* resolved, size_t resolved_cap,
+ int* resolved_system_out, u32* next_start_out);
+static int find_and_open_include_next(Pp* pp, const char* path, u32 start,
+ const u8** data, size_t* size,
+ char* resolved, size_t resolved_cap,
+ int* resolved_system_out,
+ u32* next_start_out);
+
+/* Pre-pass: replace `__has_include(<h>)` / `__has_include("h")` (and the
+ * `__has_include_next` variant) with the pp-number 1 or 0 per whether the
+ * header resolves against the include search path — the same search a real
+ * #include would perform. Runs BEFORE macro expansion so the `<...>` form's
+ * `<` and `>` are never mistaken for comparison operators, and so the header
+ * tokens are not subject to the surrounding #if's identifier→0 rewrite. The
+ * operand itself is still macro-expanded (via parse_include_path), matching
+ * the `__has_include(HEADER_MACRO)` idiom. Output is a fresh TokVec. */
+static void prepass_has_include(Pp* pp, const Tok* in, u32 nin, TokVec* out) {
+ u32 i;
+ for (i = 0; i < nin; ++i) {
+ int is_has = 0, is_next = 0;
+ u32 j, op_n, depth, s;
+ const Tok* op_first;
+ char path[4096];
+ char resolved[4096];
+ int system_form = 0, resolved_system = 0, present = 0;
+ u32 next_start = 0, next_search = 0;
+ const u8* data;
+ size_t size;
+ Tok t;
+ const char* what;
+
+ if (in[i].kind == TOK_IDENT) {
+ if (in[i].v.ident == pp->sym_has_include)
+ is_has = 1;
+ else if (in[i].v.ident == pp->sym_has_include_next)
+ is_has = is_next = 1;
+ }
+ if (!is_has) {
+ tv_push(pp, out, in[i]);
+ continue;
+ }
+ what = is_next ? "__has_include_next" : "__has_include";
+
+ j = i + 1;
+ if (j >= nin || in[j].kind != TOK_PUNCT || in[j].v.punct != '(') {
+ compiler_panic(pp->c, in[i].loc, "expected '(' after %s", what);
+ }
+ ++j; /* past '(' */
+ op_first = &in[j];
+ depth = 1;
+ op_n = 0;
+ for (; j < nin; ++j, ++op_n) {
+ if (in[j].kind == TOK_PUNCT && in[j].v.punct == '(') {
+ ++depth;
+ } else if (in[j].kind == TOK_PUNCT && in[j].v.punct == ')') {
+ if (--depth == 0) break;
+ }
+ }
+ if (j >= nin) compiler_panic(pp->c, in[i].loc, "unterminated %s", what);
+ /* in[j] is the matching ')'. */
+
+ parse_include_path(pp, op_first, op_n, in[i].loc, path, sizeof(path),
+ &system_form);
+ if (is_next) {
+ for (s = pp->nsources; s > 0; --s) {
+ TokSrc* tp = &pp->sources[s - 1];
+ if (tp->kind == SRC_LEX && tp->lex) {
+ next_search = tp->inc_next_start;
+ break;
+ }
+ }
+ present = find_and_open_include_next(pp, path, next_search, &data, &size,
+ resolved, sizeof(resolved),
+ &resolved_system, &next_start);
+ } else {
+ present = find_and_open_include(pp, path, system_form, in[i].loc, &data,
+ &size, resolved, sizeof(resolved),
+ &resolved_system, &next_start);
+ }
+
+ memset(&t, 0, sizeof(t));
+ t.kind = TOK_NUM;
+ t.flags = in[i].flags & (TF_AT_BOL | TF_HAS_SPACE);
+ t.loc = in[i].loc;
+ t.spelling = kit_sym_intern(
+ pp->pool->c, present ? KIT_SLICE_LIT("1") : KIT_SLICE_LIT("0"));
+ tv_push(pp, out, t);
+ i = j; /* resume past the matching ')' */
+ }
+}
+
i64 eval_if_expr(Pp* pp, const Tok* line, u32 n, SrcLoc loc) {
TokVec defs = {0};
+ TokVec hasinc = {0};
TokVec exp = {0};
TokVec defs2 = {0};
EE e;
i64 v;
prepass_defined(pp, line, n, &defs);
- expand_for_if(pp, defs.data, defs.n, &exp);
+ prepass_has_include(pp, defs.data, defs.n, &hasinc);
+ expand_for_if(pp, hasinc.data, hasinc.n, &exp);
prepass_defined(pp, exp.data, exp.n, &defs2);
replace_remaining_if_identifiers(pp, &defs2);
@@ -759,13 +865,16 @@ static Sym inc_resolve_key(Pp* pp, const char* path, size_t plen, int system,
/* Record a successful dir-search resolution under its spelling key, so the
* next request for the same spelling skips the search. No-op when the key
- * was unavailable (rkey == 0). `resolved` is a NUL-terminated path. */
+ * was unavailable (rkey == 0). `resolved` is a NUL-terminated path.
+ * `next_start` is the inc_dirs index a #include_next from the resolved file
+ * resumes at (0 for the includer-relative / absolute wins). */
static void inc_resolve_record(Pp* pp, Sym rkey, const char* resolved,
- int resolved_system) {
+ int resolved_system, u32 next_start) {
IncResolved e;
if (!rkey) return;
e.path = kit_sym_intern(pp->pool->c, kit_slice_cstr(resolved));
e.system = (u8)(resolved_system ? 1 : 0);
+ e.next_start = next_start;
IncResolveMap_set(&pp->inc_resolve, rkey, e);
}
@@ -778,18 +887,50 @@ static void inc_resolve_record(Pp* pp, Sym rkey, const char* resolved,
* (with the same system flag and, for quoted form, the same includer dir)
* was resolved before, we go straight to the winning path — skipping the
* dir-by-dir ENOENT storm — and the content cache serves its bytes. */
+/* Walk pp->inc_dirs[start ..] looking for `path`, opening the first hit.
+ * On success fills data/size, writes the resolved path and its -isystem
+ * flag, and sets *next_start to one past the winning dir (the resume point
+ * for a subsequent #include_next from the resolved file). Returns 0 if no
+ * configured dir holds the header. */
+static int search_inc_dirs(Pp* pp, const char* path, u32 start,
+ const u8** data, size_t* size, char* resolved,
+ size_t resolved_cap, int* resolved_system_out,
+ u32* next_start) {
+ char buf[4096];
+ size_t plen = kit_slice_cstr(path).len;
+ u32 i;
+ for (i = start; i < pp->ninc_dirs; ++i) {
+ const char* d = pp->inc_dirs[i].path;
+ size_t dlen = kit_slice_cstr(d).len;
+ if (dlen + 1 + plen + 1 > sizeof(buf)) continue;
+ memcpy(buf, d, dlen);
+ buf[dlen] = '/';
+ memcpy(buf + dlen + 1, path, plen);
+ buf[dlen + 1 + plen] = 0;
+ if (open_include_cached(pp, buf, data, size)) {
+ if (dlen + 1 + plen + 1 > resolved_cap) return 0;
+ memcpy(resolved, buf, dlen + 1 + plen + 1);
+ *resolved_system_out = pp->inc_dirs[i].system ? 1 : 0;
+ *next_start = i + 1;
+ return 1;
+ }
+ }
+ return 0;
+}
+
static int find_and_open_include(Pp* pp, const char* path, int system,
SrcLoc loc, const u8** data, size_t* size,
char* resolved, size_t resolved_cap,
- int* resolved_system_out) {
+ int* resolved_system_out, u32* next_start_out) {
char buf[4096];
- u32 i;
size_t plen = kit_slice_cstr(path).len;
Sym rkey = 0;
/* Absolute paths and the includer-relative ("...") step are not system
- * search dirs; only a configured -isystem dir flips this to 1 below. */
+ * search dirs; only a configured -isystem dir flips this to 1 below. A
+ * #include_next from such a file resumes at the head of the search path. */
*resolved_system_out = 0;
+ *next_start_out = 0;
if (plen > 0 && path[0] == '/') {
if (open_include_cached(pp, path, data, size)) {
@@ -814,6 +955,7 @@ static int find_and_open_include(Pp* pp, const char* path, int system,
if (!open_include_cached(pp, rp.s, data, size)) return 0;
memcpy(resolved, rp.s, rp.len + 1);
*resolved_system_out = hit->system ? 1 : 0;
+ *next_start_out = hit->next_start;
return 1;
}
}
@@ -830,31 +972,37 @@ static int find_and_open_include(Pp* pp, const char* path, int system,
if (open_include_cached(pp, buf, data, size)) {
if (dlen + 1 + plen + 1 > resolved_cap) return 0;
memcpy(resolved, buf, dlen + 1 + plen + 1);
- inc_resolve_record(pp, rkey, resolved, 0);
+ inc_resolve_record(pp, rkey, resolved, 0, 0);
return 1;
}
}
}
}
- for (i = 0; i < pp->ninc_dirs; ++i) {
- const char* d = pp->inc_dirs[i].path;
- size_t dlen = kit_slice_cstr(d).len;
- if (dlen + 1 + plen + 1 > sizeof(buf)) continue;
- memcpy(buf, d, dlen);
- buf[dlen] = '/';
- memcpy(buf + dlen + 1, path, plen);
- buf[dlen + 1 + plen] = 0;
- if (open_include_cached(pp, buf, data, size)) {
- if (dlen + 1 + plen + 1 > resolved_cap) return 0;
- memcpy(resolved, buf, dlen + 1 + plen + 1);
- *resolved_system_out = pp->inc_dirs[i].system ? 1 : 0;
- inc_resolve_record(pp, rkey, resolved, *resolved_system_out);
- return 1;
- }
+ if (search_inc_dirs(pp, path, 0, data, size, resolved, resolved_cap,
+ resolved_system_out, next_start_out)) {
+ inc_resolve_record(pp, rkey, resolved, *resolved_system_out,
+ *next_start_out);
+ return 1;
}
return 0;
}
+/* #include_next: like #include, but the search resumes at `start` (one past
+ * the dir the including file was found in) and skips both the absolute-path
+ * fast path and the includer-relative step. Bypasses the resolution memo —
+ * the memo key does not encode the resume point, and #include_next is rare. */
+static int find_and_open_include_next(Pp* pp, const char* path, u32 start,
+ const u8** data, size_t* size,
+ char* resolved, size_t resolved_cap,
+ int* resolved_system_out,
+ u32* next_start_out) {
+ *resolved_system_out = 0;
+ *next_start_out = 0;
+ inc_cache_ensure(pp);
+ return search_inc_dirs(pp, path, start, data, size, resolved, resolved_cap,
+ resolved_system_out, next_start_out);
+}
+
/* Decode a directly-lexed TOK_HEADER name into a NUL-terminated path.
* Classifies <...> (system) vs "..." (local), enforces the destination
* capacity, and writes the unwrapped contents to out. `what` is the
@@ -942,42 +1090,59 @@ static void parse_include_path(Pp* pp, const Tok* line, u32 n, SrcLoc loc,
}
}
-static void do_include(Pp* pp, const Tok* line, u32 n, SrcLoc loc) {
+/* Shared core for #include and #include_next (is_next == 1). */
+static void do_include(Pp* pp, const Tok* line, u32 n, SrcLoc loc,
+ int is_next) {
char path[4096];
char resolved[4096];
int system_form = 0;
int resolved_system = 0;
+ u32 next_start = 0;
const u8* data;
size_t size;
Lexer* lex;
u32 includer_id = 0;
+ u32 next_search = 0;
u32 included_id;
u32 i;
+ int found;
TokSrc s;
parse_include_path(pp, line, n, loc, path, sizeof(path), &system_form);
- if (!find_and_open_include(pp, path, system_form, loc, &data, &size, resolved,
- sizeof(resolved), &resolved_system)) {
- compiler_panic(pp->c, loc, "#include: file not found: %.*s",
- KIT_SLICE_ARG(kit_slice_cstr(path)));
- }
-
- /* Walk the source stack to find the current includer's file_id. */
+ /* Locate the current includer (topmost SRC_LEX source): its file_id seeds
+ * the include graph, and — for #include_next — its inc_next_start is the
+ * point in the search path from which we resume. */
for (i = pp->nsources; i > 0; --i) {
TokSrc* tp = &pp->sources[i - 1];
if (tp->kind == SRC_LEX && tp->lex) {
includer_id = lex_file_id(tp->lex);
+ next_search = tp->inc_next_start;
break;
}
}
+ if (is_next)
+ found = find_and_open_include_next(pp, path, next_search, &data, &size,
+ resolved, sizeof(resolved),
+ &resolved_system, &next_start);
+ else
+ found = find_and_open_include(pp, path, system_form, loc, &data, &size,
+ resolved, sizeof(resolved), &resolved_system,
+ &next_start);
+ if (!found) {
+ compiler_panic(pp->c, loc, "%s: file not found: %.*s",
+ is_next ? "#include_next" : "#include",
+ KIT_SLICE_ARG(kit_slice_cstr(path)));
+ }
+
lex = lex_open_mem(pp->c, resolved, (const char*)data, size);
included_id = lex_file_id(lex);
memset(&s, 0, sizeof(s));
s.kind = SRC_LEX;
s.lex = lex;
+ s.inc_next_start = next_start;
src_push(pp, s);
kit_source_add_include(pp->c, includer_id, included_id, loc, system_form,
@@ -1391,11 +1556,12 @@ static void do_embed(Pp* pp, const Tok* line, u32 n, SrcLoc loc) {
{
/* #embed does not record a dependency edge, so the resolved-dir system
- * flag is not consumed here. */
+ * flag and #include_next resume point are not consumed here. */
int embed_resolved_system = 0;
+ u32 embed_next_start = 0;
if (!find_and_open_include(pp, path, system_form, loc, &data, &size,
resolved, sizeof(resolved),
- &embed_resolved_system)) {
+ &embed_resolved_system, &embed_next_start)) {
compiler_panic(pp->c, loc, "#embed: file not found: %.*s",
KIT_SLICE_ARG(kit_slice_cstr(path)));
}
@@ -1505,7 +1671,9 @@ void process_directive(Pp* pp, SrcLoc hash_loc) {
else if (name == pp->sym_endif)
do_endif(pp, hash_loc);
else if (name == pp->sym_include)
- do_include(pp, line + 1, n - 1, hash_loc);
+ do_include(pp, line + 1, n - 1, hash_loc, 0);
+ else if (name == pp->sym_include_next)
+ do_include(pp, line + 1, n - 1, hash_loc, 1);
else if (name == pp->sym_line)
do_line(pp, line + 1, n - 1, hash_loc);
else if (name == pp->sym_pragma)
diff --git a/lang/cpp/pp/pp_priv.h b/lang/cpp/pp/pp_priv.h
@@ -66,6 +66,13 @@ typedef struct TokSrc {
* surrounding quotes) used by __FILE__ when set. */
i32 line_delta;
Sym file_override;
+ /* SRC_LEX only: the index into pp->inc_dirs from which a `#include_next`
+ * (or `__has_include_next`) appearing in this file begins its search —
+ * i.e. one past the search dir this file was itself found in. 0 for the
+ * top-level translation unit and for files found via the includer-relative
+ * ("...") step or an absolute path, so an `#include_next` there scans the
+ * whole search path (matching GCC). */
+ u32 inc_next_start;
} TokSrc;
typedef enum IfState {
@@ -111,6 +118,8 @@ KIT_HASHMAP_DEFINE(IncCache, Sym, IncEntry, inc_hash_);
typedef struct IncResolved {
Sym path; /* interned resolved path string */
u8 system; /* the resolved_system flag (winning -isystem) */
+ u32 next_start; /* inc_dirs index a #include_next from the */
+ /* resolved file starts at (see TokSrc) */
} IncResolved;
static inline u32 incres_hash_(Sym s) { return kit_hash_u32((u32)s); }
KIT_HASHMAP_DEFINE(IncResolveMap, Sym, IncResolved, incres_hash_);
@@ -184,6 +193,9 @@ struct Pp {
Sym sym_define;
Sym sym_undef;
Sym sym_include;
+ Sym sym_include_next; /* GCC/clang #include_next extension */
+ Sym sym_has_include; /* __has_include() #if operator */
+ Sym sym_has_include_next; /* __has_include_next() #if operator */
Sym sym_if;
Sym sym_ifdef;
Sym sym_ifndef;
diff --git a/mk/test.mk b/mk/test.mk
@@ -111,6 +111,7 @@ TEST_TARGETS = \
test-pp-err \
test-pp-ok \
test-pp-file-escape \
+ test-pp-include-next \
test-rt-headers \
test-rt-runtime \
test-rt-backtrace \
@@ -259,7 +260,7 @@ test-wasm-toy: bin
test-wasm-c: bin $(PARSE_RUNNER)
@KIT_TEST_PATHS=W KIT_TEST_ALLOW_SKIP=1 KIT=$(abspath $(BIN)) bash test/parse/run.sh
-test-pp: test-pp-ok test-pp-err test-pp-file-escape
+test-pp: test-pp-ok test-pp-err test-pp-file-escape test-pp-include-next
test-pp-ok: bin
@KIT=$(abspath $(BIN)) test/pp/run.sh
@@ -270,6 +271,9 @@ test-pp-err: bin
test-pp-file-escape: bin
@KIT=$(abspath $(BIN)) bash test/pp/run_file_escape.sh
+test-pp-include-next: bin
+ @KIT=$(abspath $(BIN)) bash test/pp/run_include_next.sh
+
# Best-effort kit binary build: Layer D needs build/kit, but the
# binary may not link until enough libkit symbols exist. The harness
# detects a missing binary and skips that layer; don't break test-elf
diff --git a/test/pp/cases/95_has_include.c b/test/pp/cases/95_has_include.c
@@ -0,0 +1,26 @@
+/* __has_include / __has_include_next operator (§6.10.1, clang/GCC extension).
+ * Run with -I . so the angle-bracket search dir is the cases directory. */
+#if __has_include("95_inc.h")
+quote_yes
+#else
+quote_no
+#endif
+#if __has_include(<95_inc.h>)
+angle_yes
+#else
+angle_no
+#endif
+#if __has_include(<no_such_header_zzqq.h>)
+missing_yes
+#else
+missing_no
+#endif
+#if defined(__has_include)
+have_op
+#endif
+#if defined(__has_include) && __has_include("95_inc.h")
+guarded_yes
+#endif
+#if __has_include(<sys/types.h>) ? 0 : 1
+no_sysroot_ok
+#endif
diff --git a/test/pp/cases/95_has_include.expected b/test/pp/cases/95_has_include.expected
@@ -0,0 +1,6 @@
+quote_yes
+angle_yes
+missing_no
+have_op
+guarded_yes
+no_sysroot_ok
diff --git a/test/pp/cases/95_inc.h b/test/pp/cases/95_inc.h
@@ -0,0 +1 @@
+/* Sibling header probed by 95_has_include.c via __has_include. */
diff --git a/test/pp/run_include_next.sh b/test/pp/run_include_next.sh
@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+# test/pp/run_include_next.sh — regression for `#include_next` and
+# `__has_include_next` (clang/GCC preprocessor extensions).
+#
+# #include_next resumes the header search in the directories *after* the one the
+# current file was found in — the mechanism system-header wrappers use to pull in
+# the "real" header of the same name further down the search path. It cannot be
+# exercised by the single-`-I` golden corpus (test/pp/run.sh), so it gets its own
+# two-directory harness here.
+#
+# Layout: dirs a/ and b/ both hold widget.h. Compiling `#include <widget.h>` with
+# `-I a -I b` resolves to a/widget.h; its `#include_next <widget.h>` must then
+# reach b/widget.h (the next dir), not loop back on itself. __has_include_next
+# from a/ must see b/widget.h as present, and from b/ (the last dir) as absent.
+#
+# Host-target only: no sysroot needed (all headers are local).
+
+set -u
+
+ROOT=${KIT_TEST_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}
+KIT=${KIT:-"$ROOT/build/kit"}
+export KIT
+
+KIT_KIT_DIR="$ROOT/test/lib"
+. "$ROOT/test/lib/kit_sh_kit.sh"
+kit_require_kit pp-include-next
+
+kit_report_init
+work=$(mktemp -d "${TMPDIR:-/tmp}/kit-pp-incnext.XXXXXX")
+trap 'rm -rf "$work"' EXIT
+cd "$work" || exit 2
+
+mkdir -p a b
+cat > a/widget.h <<'EOF'
+marker_a_before
+#if __has_include_next(<widget.h>)
+a_sees_next
+#else
+a_no_next
+#endif
+#include_next <widget.h>
+marker_a_after
+EOF
+cat > b/widget.h <<'EOF'
+marker_b
+#if __has_include_next(<widget.h>)
+b_sees_next
+#else
+b_no_next
+#endif
+EOF
+printf '#include <widget.h>\ndone\n' > main.c
+
+# emit_seq NAME : `kit cc -E -I a -I b main.c` must succeed and its output tokens
+# must appear in the exact relative order of the remaining args.
+emit_seq() {
+ name=$1; shift
+ if ! "$KIT" cc -E -I a -I b main.c -o "$work/$name.pp" \
+ >"$work/$name.out" 2>"$work/$name.err"; then
+ kit_fail "$name" "kit cc -E failed; see $work/$name.err"
+ return
+ fi
+ # Collapse to one token per line, then check the needles appear in order.
+ tr -s '[:space:]' '\n' < "$work/$name.pp" > "$work/$name.toks"
+ local prev=0 line
+ for tok in "$@"; do
+ line=$(grep -nxF "$tok" "$work/$name.toks" | head -1 | cut -d: -f1)
+ if [ -z "$line" ]; then
+ kit_fail "$name" "missing token '$tok'"; return
+ fi
+ if [ "$line" -le "$prev" ]; then
+ kit_fail "$name" "token '$tok' out of order (line $line <= $prev)"; return
+ fi
+ prev=$line
+ done
+ kit_pass "$name"
+}
+
+# include_next from a/ reaches b/ (and a/ continues after); __has_include_next
+# is true in a/ (b follows) and false in b/ (last dir).
+emit_seq include_next_order \
+ marker_a_before a_sees_next marker_b b_no_next marker_a_after done
+
+# A token that must NOT appear: a/ must not loop back onto itself (a_no_next),
+# and b/ must not think another widget.h follows it (b_sees_next).
+if grep -qxF a_no_next "$work/include_next_order.toks" 2>/dev/null \
+ || grep -qxF b_sees_next "$work/include_next_order.toks" 2>/dev/null; then
+ kit_fail include_next_no_loop "unexpected a_no_next/b_sees_next token"
+else
+ kit_pass include_next_no_loop
+fi
+
+kit_summary pp-include-next
+kit_exit