← board

RESOLVED-BY-CAUSE 2026-08-01

Closing per this ticket's own 2026-07-28 "second look" conclusion: FPC's real answer (proper unit-scoped uses, not a merge-declaration language feature) is correct, and the fork here dissolves once [[bug-pascal-uses-is-transitive]] is fixed. That root fix is now sequenced via [[decide-pascal-uses-campaign-scope]] (option 2, combined effort). The five-faces checklist above stays as the acceptance list for the root fix — each per-site patch should be reverted as it lands, not kept.

Decide: how should two libraries be allowed to export the same class name?

Raised 2026-07-28 from [[feature-demo-songformatter-pxx-target]], where it is the blocker: convertrawtext.py imports tkinter AND reportlab, both of which export a class called Canvas, and both spellings are fixed by the applications that use them. Python scopes them per module. pxx has ONE flat class namespace, resolved first-match (FindUClass, compiler/symtab.inc).

The fork

Two same-named classes can mean two opposite things, and today's rule cannot tell them apart:

Preferring "the class declared in the unit being parsed" was implemented and reverted: it fixes the first case and breaks the second, turning a compile error into a silently uncaught exception. Details and the exact failure are in [[bug-pascal-duplicate-class-name-silently-shadows]].

Options

  1. Make the merge explicit, then scope per unit. A class states that it replaces a same-named one (Exception = class ... ; replaces system.Exception or a pragma), and every other name becomes unit-scoped. Most correct, and the only one that lets two libraries coexist. Cost: an RTL change plus a new piece of language surface, and every existing accidental merge has to be found.
  2. Unit-scoped classes, with the RTL merges hard-coded. Same effect without new syntax: a short list in the compiler naming the classes that are deliberately shared. Cheap, and it is a list that will rot.
  3. Leave it flat; make the collision an ERROR rather than a silent capture. Honest and small, but it does not let songformatter compile — tkinter plus reportlab is simply rejected. It would at least stop the silent-wrong-binding case, which is the dangerous half.
  4. Do nothing. Today's behaviour: first-match wins, collisions are silent.

Recommendation

Option 1, with option 3 landed first as a stopgap — the silent binding is the part that can produce wrong output, and making it loud is independently correct and small. The qualified-reference case is the sharpest edge: renaming the shim's class made canvas.Canvas(...) bind to tkinter's Canvas and compile clean.

Scope note

This is not only a NilPy question. It is the Pascal class namespace, so any two Pascal libraries with a same-named class have it; NilPy just meets it sooner because Python code imports several libraries into one module as a matter of course.

Option 2 implemented as the stopgap (2026-07-28)

Classes are now resolved per unit — a class declared in the unit being parsed wins — EXCEPT for a named list of deliberately shared names, which today holds exactly one entry, Exception. ClassNameIsDeliberatelyShared in compiler/symtab.inc is that list, and it exists because pylib's and sysutils' Exception mean ONE class and the tree relies on it.

This unblocks the reportlab shim next to tkinter, and it keeps test_nilpy_rtl_exception_surface green. It does NOT settle the fork: the list is exactly the thing that will rot, and option 1 (a class declares that it replaces a same-named one) retires it. The qualified-reference case is also still first-match.

So the decision stands open; what changed is that the cost of leaving it open is now a maintenance list rather than a blocked application.

2026-07-28, second look: this is a SYMPTOM, and the fork is a false one

Reviewing the options above against the actual resolver turned up the cause, and it retires most of this ticket. Filed as [[bug-pascal-uses-is-transitive]].

uses is not transitive in Pascal — and it is in pxx. If A uses B, a unit using A must not see B's names, whichever section A imported B in. pxx has one flat global namespace for routines and for classes, so every unit's imports leak to its consumers. Measured in pure Pascal, no NilPy involved: a program that uses only priv resolves IntToStr although priv imported sysutils in its IMPLEMENTATION section and the program never mentions sysutils at all.

Given that, the "fork" in this ticket is not a language-design question:

So option 1's language surface is unnecessary — the replaces declaration exists to reintroduce, by hand and per class, the scoping the resolver is missing wholesale. Option 2 (the current stopgap) stays as-is until the root fix lands; it is doing its job and costs one list entry.

Also worth recording, since a replaces-style re-export may still be wanted for its own sake: the FPC spelling already parses. type Exception = excbase.Exception; goes through the unit-qualified type path (parser.inc:18667, Synapse's TInAddr6 = sockets.Tin6_addr) and registers a UClsAlias row. What is missing is plumbing, not syntax — alias rows carry NOff/NLen/Ci only (defs.inc:2249), no unit index, so FindUClassInUnit cannot see them and the NilPy qualified-ctor path (pyparser.inc:2785) skips them. No keyword needed.

Routes considered and rejected

Recommendation, revised

Do nothing here. Keep option 2's list. Fix [[bug-pascal-uses-is-transitive]] (size it first with the warn-only pass described there) and [[bug-pascal-defines-leak-across-units]], then close this ticket as resolved-by-cause rather than deciding it.

The one piece NOT covered by the root fix is the qualified-reference case (canvas.Canvas(...) binding first-match, noted above and in [[bug-pascal-duplicate-class-name-silently-shadows]]) — that needs the alias unit-index plumbing regardless, and is independent of the decision.

Side finding, recorded for whoever touches Exception

Exception.CreateFmt has TWO bodies — sysutils.pas:589 (full Format: %d %u %x %X %s %f %g %c, width, precision, padding) and pylib.pas:3612 (a dependency-free substituter handling %s, %d, %% only, leaving any other spec verbatim so a wrong message is visible rather than silently lost). Which one runs is decided by link order. Measured: a .npy reaching a Pascal unit that raises CreateFmt('hex=%x pad=[%5s]', [255,'ab']) prints hex=FF pad=[ ab], so sysutils' body wins whenever both are linked; pylib's is the standalone fallback, and pylib never calls CreateFmt itself. No RTL raise site currently uses a spec outside %s/%d, so the two agree on everything exercised today — the divergence is latent, not live. Worth a %x-and-width raise added to test_nilpy_rtl_exception_surface to make it a guarded case rather than a coincidence.

The same leak, five faces (recorded 2026-07-28 from the songformatter track)

Every one of these was patched at its own call site while walking convertrawtext.py, i.e. treated as five bugs. They are one: [[bug-pascal-uses-is-transitive]]. Kept here so the root fix has a checklist of what should stop needing a patch.

collision what broke patched at
crtl's C atexit vs Python's atexit module atexit.register(fn) stopped parsing once ANY C unit was pulled the function-value branch
crtl's C exit(int) vs Pascal's Exit lib/pcl/tkinter.pas's own exit; became "undefined variable", depending on IMPORT ORDER the Halt/Exit soft-keyword guard
the RTL's Text record vs tkinter's Text widget a construction parsed as a record TYPECAST — fine with one argument, broken with two the NilPy construction sites
tkinter's Canvas vs reportlab's Canvas the shim's own methods bound to the OTHER unit's class and could not see their own fields per-unit preference at construction
pylib's Exception vs sysutils' Exception relied on the leak: they are one class only BECAUSE of first-match — (would break if scoping landed naively)

The pattern: the first registration wins, silently, and the answer depends on import order. That is the property to remove — and each per-site patch above should be reverted as the root fix lands, not kept.