A missing Pascal unit, imported from C, is diagnosed at the wrong line in the wrong language
- Track C by motive, Track P by file — the message is raised at
compiler/pasparser_proc.inc:3522and:4121. Filed underCfor visibility per the precedent already set onrefactor-c-string-literal-decay-belongs-at-the-producer: keep the frontend's letter, note in the body which lane owns the file. - Found: 2026-08-30 by frankC, re-measuring
feature-c-import-a-pascal-unit-under-a-mangled-nameat HEAD after re-claiming it.
The measurement
Compiler aa78a7faf63a (self-host fixedpoint at HEAD, converged 1 round).
#include "nosuch.pas"
int main(void){return 0;}
pascal26:2: error: uses: unit source not found: /abs/path/to/scratch/nosuch
near: __pxx_pascal_unit /abs/path/to/scratch/ nosuch.pas >>> main
Four defects in two lines of output, and they are not the one the parent ticket recorded.
1. The line number is wrong, and it points at innocent code
pascal26:2 — the #include is on line 1. Verified as a consistent offset,
not a coincidence of this file:
#include on line |
reported |
|---|---|
| 1 | 2 |
| 5 | 6 |
Always include-line + 1. The mechanism is visible in
CParsePascalUnitMarker (cparser.inc): it consumes the marker identifier, both
string tokens and the semicolon before calling ParseUsesUnit, so CurTok is
already the first token of the next line when the error is raised, and the
raise reports the current token's position.
This is the defect that costs time. The line it names is a real, valid line of
the user's own source — int main(void){return 0;} — so the author reads a
correct line looking for a fault in it. A diagnostic that points at innocent code
is worse than one that points nowhere.
2. It leaks __pxx_pascal_unit, which the user never wrote
The near: context prints the token-stream marker the preprocessor substitutes
for #include "<x>.pas" (cpreproc.inc:2526). It is an internal representation:
the author cannot find that token in their source, cannot grep for it, and has no
way to know it stands for the line they did write.
Note this is a direct cost of the marker design — and that design is otherwise
right (cparser.inc:421 records why the state rides in the token stream rather
than a preprocessor global: import ORDER is then preserved for free). The leak is
not an argument against it, only an unpaid bill.
3. It speaks Pascal at a C author
uses: — the author wrote #include. This is the only one of the four the parent
ticket recorded.
4. It renames the unit the author asked for
nosuch — an absolute path with the .pas stripped, where the author wrote
"nosuch.pas". The stripping is deliberate (the loader re-derives the extension)
and correct as input to the loader; it is only wrong as output to a human.
Why this is not fixable under Track C, checked rather than assumed
The parent ticket left this alone with a stated reason, and that reason is still correct but is not the whole of it:
a pre-check in
cparser.incwould have to duplicate the loader's case-insensitive +.pp+-Fusearch to avoid refusing files that do exist.
That rules out C refusing on its own check. It does not, on its face, rule out C checking only to produce a better message — so I looked for that escape and there isn't one: the loader's failure is fatal at the raise site, so there is nothing for C to intercept and reword. Every actual fix needs one of
ParseUsesUnitto accept a caller-supplied position and vocabulary — a Track P signature change (pasparser_proc.inc); or- a shared "diagnostic origin" the raise site reads — new state in
defs.inc, which is Track A; or - C resolving the unit itself before the call — the duplicated search the parent ticket already rejected, and rightly.
(1) is the right one and it is small: the marker handler already knows both
things the message is missing — the author's own spelling (spec) and the line
the marker sat on, which it can capture before Next consumes it.
Scope note — this is error REPORTING, and prio 30 reflects that
CLAUDE.md is explicit that diagnostic wording is deferred work ("we seek LANGUAGE compliance, not error-handling compliance"), and nothing here changes what compiles: every one of these four is cosmetic in the sense that no correct program behaves differently.
It is filed anyway, at a low prio, for the one reason that survives that rule: defect 1 is not wording, it is a wrong answer to "where". The line number is data, the data is wrong by a fixed offset, and the wrongness sends the reader to code that is fine. Defects 2-4 are wording and would not have earned a ticket on their own.
Gate
Track P's, since the edit is P's file: make compiler/pascal26 to fixedpoint +
the repro above reporting line 1, naming nosuch.pas, without __pxx_pascal_unit
in the context. A test/ case belongs beside the c_pasunit_*_fail block in
test-core, which already has eight refusal recipes to copy the shape from.
Related
- Parent:
feature-c-import-a-pascal-unit-under-a-mangled-name(this was its last open non-§6 item; §6 is blocked on a user permission grant). - Precedent for the track/file split:
refactor-c-string-literal-decay-belongs-at-the-producer.
The frontmatter's routing was wrong, and frankC measured it (2026-08-30)
This ticket said "the raise site is pasparser_proc.inc — a Track P file — so
the edit is not Track C's." Half right, and the wrong half is the one a
dispatcher reads. The coordinator routed on it and sent frankC to a file that
cannot hold the fix.
Pascal's own uses through the same raise site is CORRECT:
program p; / (blank) / uses nosuchunit; -> pascal26:3 correct, with a near: showing the user's own text
#include "nosuch.pas" on line 1 -> pascal26:2 wrong
So pasparser_proc.inc:3567 is not defective. It reports CurTok, and for a
Pascal caller CurTok is still on the unit name when the loader fails. The
offset is manufactured entirely on the calling side: CParsePascalUnitMarker
(cparser.inc:432-500) consumes the marker identifier, both strings and the
semicolon before it calls, so CurTok has already crossed to the next line.
One cause, one victim — the coordinator's "a consistent off-by-one usually has one cause and several victims" guess was wrong in the useful direction, and it was checked rather than assumed.
There is no P-only path. Capturing at entry to ParseUsesUnit does not help:
the caller has already advanced, so P reads line 2 either way. The information is
gone before P is entered. Defects 3 and 4 need the caller too — CProgramMode is
deliberately set False by the marker handler before the call (reason documented
at cparser.inc:483) and PyImportLang='pas' is also set by NilPy's
import "x.pas", so neither flag distinguishes a C author from a Pascal one.
Banked so the fix is short when cparser.inc frees
- Caller→callee state already flows across this exact call by an established
pattern:
PyImportLangis set by the marker handler, anddefs.inc:3442documentsPyDottedImportdoing the same, "cleared by ParseUsesUnitBody so a stale value cannot leak." A diagnostic origin is another rider on a proven mechanism, not a new one. ErrorAt(line, msg)already exists atlexer.inc:99and passeswithContext=False, which fixes defect 2 (the leaked__pxx_pascal_unitin thenear:context) for free at the same time.
Do not land a P-side half alone — with no caller to supply the position it is dead code that reads as a finished change.
RESOLVED — all four defects, one mechanism, 2026-08-30 (frankC)
Compiler 61deff86a046, converged after 1 round(s); tools/gate.sh quick GREEN.
before: pascal26:2: error: uses: unit source not found: /abs/path/to/nosuch
near: __pxx_pascal_unit /abs/path/to/ nosuch.pas >>> main
after: pascal26:1: error: include: Pascal unit source not found: nosuch.pas
Line 1 for an include on line 1, line 5 for one on line 5 — the fixed +1 is gone,
the C author is addressed in C's vocabulary, the spelling is the author's own,
and the internal marker is no longer printed. One change fixed all four,
because they had one cause: the raise site reported CurTok, and the C caller
had already moved it.
How, without touching defs.inc or the other frontends
The obvious implementation is a global set by the caller and read at the raise
site — the pattern PyImportLang and PyDottedImport already use across this
exact call. It was avoidable, and avoiding it kept the change inside two
files. The globals live in defs.inc, which is Track A's and which frankA was
editing at the time.
Instead the origin is a parameter, which turned out to be cheap because both
raise sites are inside ParseUsesUnitBody and it has exactly one caller:
ParseUsesUnitBody(name, diagLine, diagSpell)— the two raise sites useErrorAt(diagLine, ...)whendiagLine > 0, and fall through to the existingError(...)when it is 0.ParseUsesUnitAt(name, diagLine, diagSpell)— new entry, holds whatParseUsesUnitused to.ParseUsesUnit(name)— now a one-line delegate passing(0, '').
So bparser.inc (the BASIC frontend, the third caller) is untouched, and every
existing call keeps today's behaviour explicitly: origin 0 means "report
CurTok", which is exactly right for a Pascal uses and is why that path was
never broken.
ErrorAt earned its place twice: it takes the line, and it passes
withContext=False, which drops the near: window — so defect 2, the leaked
__pxx_pascal_unit, was fixed by the same call rather than by teaching the
context printer about markers.
Regression test
test/c_pasunit_missing_fail.c + two recipes beside the c_pasunit_*_fail
block. The include sits on line 13, not line 1, deliberately — a line-1
include would report "1" under the very off-by-one this fixes, so that test
would pass against the bug. And it is two assertions rather than one: the
line/vocabulary/spelling grep, and a separate negative grep for
__pxx_pascal_unit. The fix has separable halves and a single grep would let
one of them rot silently.
Verified no regression on the paths that must keep working, named individually
rather than swept: c_pasunit.c, c_pasunit_twice.c, c_pasunit_strings.c all
build and run rc=0; c_pasunit_case_fail.c and c_pasunit_collide_fail.c still
refuse; Pascal's own uses still reports line 3 with its normal near: window.
Log
- 2026-08-30 — resolved, commit 4794d1251.