← board

A missing Pascal unit, imported from C, is diagnosed at the wrong line in the wrong language

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.inc would have to duplicate the loader's case-insensitive + .pp + -Fu search 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

  1. ParseUsesUnit to accept a caller-supplied position and vocabulary — a Track P signature change (pasparser_proc.inc); or
  2. a shared "diagnostic origin" the raise site reads — new state in defs.inc, which is Track A; or
  3. 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.


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

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:

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