← board

A nested include is silently dropped

Symptom

{$I outer.inc}      { outer.inc contains: {$I inner.inc} }

Everything declared in inner.inc is undefined. No diagnostic names the include; the error surfaces far away, at the first use:

pascal26:116119: error: undefined variable (DetectPascalRuntimeNeeds)

FPC compiles the same three files and prints l1 l2 l3. This is not a missing feature that fails loudly — it is silent divergence from the reference implementation, the shape the compat rules say to promote to a bug- ticket rather than file as parity work.

Root cause

ExpandIncludes (elfwriter.inc) said so in its own header comment: "Replace active dollar-include directives with file contents, one level deep." It scans src and appends to IncExpanded, but never scans the text it splices in. A directive inside that text therefore survives into the real lexer pass, where an unrecognised {$...} is skipped like a comment.

The one-level limit was invisible because no .inc in the compiler included another .inc — 58 files, and the shape had never been exercised in-tree. User code hits it immediately: nested includes are ordinary Pascal.

Fix

Recursion, not a re-scan of the result. Both were available; the re-scan is wrong because of -g: the line markers name the file a line came from, and a second pass over already-spliced text attributes the inner file's lines to the outer one. Recursing lets each file emit its own markers, and the existing IncEmitLineMarker(fullPath, 1) / IncEmitLineMarker(selfPath, markLine) pair around the splice then bracket the inner text correctly with no change.

The catch is that IncExpanded / IncIncluded are module-level buffers, not locals, and the callee does SetLength(IncExpanded, 0) — so the splice site hands the recursive call copies and puts its own accumulator back afterwards.

Each nesting step resolves relative to its own file's directory (derived from the already-normalized fullPath), which is what FPC does and what makes a bare {$I sibling.inc} inside a subdirectory work.

MAX_INC_DEPTH = 32 is the brake circular includes need (FPC's own limit is 16). Two files that include each other are a legal thing to write and an infinite thing to expand; it now errors instead of exhausting the stack.

Conditional handling is unchanged and gets the nesting for free: the recursive call simulates the inner file's own {$ifdef}s, so an include in a dead branch is never opened.

The bootstrap constraint this exposed (worth knowing before the next carve-out)

make compiler/pascal26 compiles compiler.pas with the pinned binary. So the compiler's own source cannot use a feature the pinned binary lacks, and this fix does not become usable in-tree until a compiler carrying it is pinned. The parser.inc carve-out therefore lists its slices flat in compiler.pas rather than nesting them under parser.inc — which also matches how every other frontend is spelled there.

Tests

Gate

make compiler/pascal26 (byte-identical self-host fixedpoint) + both tests against the FPC oracle + tools/gate.sh quick GREEN.