← board

What this ticket is NOT

It is not "pointer width has no single answer". It does. TARGET_PTR_SIZE: Integer is declared at compiler/defs.inc:1758, assigned at compiler.pas:788 (default 8) and compiler.pas:1508/1510 (4 vs 8 per target), and read at 129 sites across compiler/ — including symtab.inc (32), cparser.inc (22), ir.inc (17), pasparser_decl.inc (15).

An earlier draft of this ticket claimed otherwise. It was wrong, and it was wrong because its grep only looked for function TargetPtrSize and could never have matched a variable. Recorded here so the next person greps for the concept, not the spelling.

It is also not "collapse the raw TargetArch sites". There are ~180 of them (ir_codegen.inc 83, symtab.inc 47, cparser.inc 31, elfwriter.inc 27, lexer.inc 19), and most are legitimately per-arch codegen — a case over five ISAs is the correct shape for emitting five instruction encodings.

compiler/util.inc:87 (TargetIsEspClass) already did the collapse that was worth doing — 24 hand-written copies of one predicate, three of them found only by a grep-for-the-sibling pass — and its comment documents the refusal: 13 sites spelling (TargetArch = TARGET_XTENSA) or (TargetArch = TARGET_RISCV32) are deliberately left alone in emit.inc, elfwriter.inc, exception_emit.inc, pasparser_decl.inc, lexer.inc, pasparser_prog.inc, because there the one spelling means four different things — "no DWARF", "the only two --emit-obj targets", "Real is Single", "no hardware FPU". One predicate would assert a sameness that is not there.

That refusal is the strongest thing in the tree on this subject and this ticket does not touch it.

The actual finding, verified on origin/master@8787cfe42

Several per-target chains fail open. compiler/lexer.inc:936:

  if TargetArch <> TARGET_X86_64 then
  begin
    ...
    if TargetArch = TARGET_AARCH64 then      begin ... end
    else if TargetArch = TARGET_ARM32 then   begin ... end
    else if TargetArch = TARGET_I386 then    begin ... end
    else if TargetArch = TARGET_XTENSA then  begin ... end
    else if TargetArch = TARGET_RISCV32 then begin ... end;
  end;   { …end of the non-x86-64 CPU-define swap }

There is no final else. The chain is complete for the six targets we have, which is exactly what hides the shape.

Add a 7th target and it enters the outer block (it is not x86-64), matches no arm, and falls out having set no CPU defines at all. Not "the x86-64 answer" — nothing. No error, no warning, and the failure surfaces much later as source that compiles under the wrong conditional defines.

Two more sites, measured 2026-08-27

A heuristic scan for TargetArch chains of >=3 arms with no final bare else:

  6 arms  compiler/exception_emit.inc:8
  4 arms  compiler/coroutine_emit.inc:25

Both emit nothing at all for an unrecognised target — no exception runtime, no coroutine runtime, no diagnostic. Together with lexer.inc:936 above that is three confirmed, and the scan undercounts: it missed lexer.inc entirely because that chain sits inside an outer if TargetArch <> TARGET_X86_64 guard the scan did not follow. So the audit is genuinely the work here; the scan is a starting point and must not be mistaken for an inventory.

Why it is worth fixing before target #7 or #8, not after

Two tickets add a target and both hit this:

Fixing it after a target exists means auditing which chains silently skipped the new target, from the symptoms. Fixing it before means the chains say so.

Shape of the fix

Not new predicates. A mandatory else on the chains that dispatch on target identity and currently have none:

    else Error('lexer: no CPU-define profile for this target');

so that adding a target is loud at each site that needs a decision, at the moment it is added.

The work is the audit — which chains are exhaustive-by-intent (need the else) versus which are genuinely "x86-64 does this extra thing" (correct as-is). That distinction is the ticket; the edit is trivial once it is made.

Where a chain turns out to be asking a property rather than an identity, the established method is util.inc's: one named predicate with a comment saying which bug it prevents (TargetIsEspClass, RealTypeKind) — and an explicit list of the sites it refuses to absorb. Follow that, including the refusals.

Acceptance

  1. Every chain that dispatches on target identity and is exhaustive-by-intent has a final else that Errors.
  2. make compiler/pascal26 converges byte-identical.
  3. Compile a fixed corpus for all six targets before and after; every output binary byte-identical. This is a pure refactor — any diff is a chain that was doing something other than what it appeared to.

Overlap with feature-a-wasm32-target-registration-skeleton

That ticket applies this medicine to one concrete target (wasm32) and therefore fixes the chains wasm32 reaches, with a consumer and a date. Sensible order: it first, then this general sweep using the site list it produces. Doing both independently is the only bad option.

Log


AUDIT RESULT — 2026-08-31, frank-rust, on 4b3f5bfbb3e5

Two chains failed open out of 27. Both fixed. The ticket's own three named sites were all stale, which is the finding worth carrying forward more than the fix is.

The site list rotted in four days

Verified at 8787cfe42 (2026-08-27), read again 2026-08-31:

site as named what is actually there now
lexer.inc:936 — "the worked example" already fixed. The chain moved to lexer.inc:1215, and feature-a-wasm32-target-registration-skeleton gave it a TARGET_WASM32 arm and a final else Error(...). Line 936 is now PxxLibApply.
exception_emit.inc:8 — "6 arms, no else" not a chain. The file's only TargetArch mention near there is an early-exit guard at :27. The real 6-arm chain is at :75 and it HAS an else (line 533).
coroutine_emit.inc:25 — "4 arms, no else" real (5 arms by now). Fixed here. Its own comment already named this ticket as the owner.

I lost about twenty minutes treating the ticket's coordinates as an inventory and my scanner as broken when it disagreed with them. It was not broken; the ticket was four days old. The ticket already warned that the scan "must not be mistaken for an inventory" — it did not warn that the SITES were equally perishable. They are more perishable: a scan can be re-run, a line number cannot be re-checked without re-reading.

The inventory

if/else-if TargetArch = TARGET_* chains with >=2 arms, plus every case TargetArch of. 27 constructs. 4 with no bare final else:

site arms verdict
coroutine_emit.inc:25 5 FAIL-OPEN — fixed. riscv32/xtensa fell through.
ir_codegen.inc:949 EmitSignalRuntimeForTarget 6 FAIL-OPEN — fixed. wasm32 fell through; found by this audit, not named anywhere.
ir_codegen.inc:2469 I/O-lock stubs 4 correct as-is. Subset-by-intent and fail-closed by symmetry: ir.inc:13262 gates IR_IO_LOCK emission on the same positive four-target list, so a 7th target emits no lock and needs no stub. An else Error here would be actively wrong.
compiler.pas:1678 SoC defaulting 2 correct as-is. Only ESP targets have a SoC; SOC_NONE on x86-64 is the right answer.

All five case TargetArch of (all in cparser.inc) already have an else. No cross-lane edit was needed.

The distinction that no scanner can make

Exhaustive-by-intent vs subset-by-intent. A missing else is a bug in the first and correct in the second, and the two are syntactically identical. That is why this ticket's real content was always the audit, and why the fix is four sites read rather than 27 sites patched. Half of my scanner's hits were correct code.

Applying the ABSENCE rule to my own two fixes

A chain with no final else and a chain whose else is correct look identical from any probe that only compiles targets 1-6. So each fix got a control that must fire:

Without that second control I would have shipped a guard that cannot fail, which is the thing CLAUDE.md warns prints PASS.

A correction to my own first comment, and it is the useful part

I first wrote that the coroutine fall-through left CoSwitchAddr at 0 and made every CoSwitch a call to address zero. Measured, that is false today. On the baseline compiler (25178873db17) a riscv32 program touching __pxxcoswitch already errors — unsupported node in IR codegen: coswitch — because both backends refuse IR_COSWITCH. The fall-through is real but masked downstream.

Two reasons it is still worth closing, and neither is the one I nearly recorded:

  1. The diagnostic. "coroutines are not implemented for target riscv32" is the true statement; the codegen message names an internal node for something the user did not do wrong.
  2. The masking is the fragile half. It holds only while the backends have no coswitch lowering. Give riscv32 one without adding a stub arm here and the downstream guard vanishes, leaving exactly the call-to-address-zero that does not exist yet. That is the "second path that stays broken" shape from normalise-dont-special-case.md.

Verification