Name resolution
PXX compiles several languages — Pascal, C, Nil Python — through one shared intermediate representation, and a program may mix them in a single build. That raises a question a single-language compiler never has to answer: if two declarations in two different languages share a name, which one does a bare call bind to?
This page answers it for the programmer. The short version:
- Your own language wins.
- A cross-language name has to agree on case to match at all.
- A declaration in your own scope shadows an imported one — and that is allowed, not a wart.
- Each frontend follows its own reference implementation, so the same-looking
construct can legitimately give different answers in a
.pasand a.npy.
Two different questions
"Own language first" is the slogan for both halves of the problem, but they are separate machinery and it is worth keeping them apart:
| question | answer | |
|---|---|---|
| Module resolution | which file does this import name load? | each frontend searches its own extension only |
| Symbol resolution | which declaration does this name bind to? | own language first, then scope hiding, then overload matching |
Module resolution — which file
A bare import name means this language's own module. uses math in Pascal
looks for math.pas; #include <math.h> in C searches the C include path; a
Nil Python import tkhtmlview looks for tkhtmlview.py or .npy and only
those. No frontend implicitly searches another language's files, and that is
deliberate: importing another language is something you say out loud.
Nil Python enforces this with a diagnostic rather than a silent fallback. A name that happens to match a Pascal RTL unit is refused, and the message names the file it found and the spelling that reaches it:
error: import: classes is the Pascal unit …/lib/rtl/classes.pas, not a Python
module — a bare NilPy import resolves to Python (.py/.npy) only. To reach the
Pascal unit, name it with its extension: import 'classes.pas' as classes
That refusal is the point. Before it, such an import loaded the Pascal unit and failed somewhere inside a file the program never mentioned.
The one exception: a Python extension module
A Pascal unit that declares itself a cpyext extension module is never the
rule's subject. It is a Python module whose body happens to be Pascal and C —
the same thing _socket or _json is to CPython — so a bare import of it is
the correct spelling and resolves. Two things must both be true of the unit:
- a line consisting of exactly
{$PYEXTENSION}— the declaration; - its
usesclause binds the cpyext runtime,lib/cpyext/src/pyruntime.c— the check on that declaration.
The declaration is what makes the unit a Python module; the runtime binding
only confirms the claim. A unit that binds the runtime and does not declare
itself is still refused with the message above, so the exception cannot widen
into "anything that touches CPython". Nothing in the module's name is a
signal — _ext, a leading underscore, or neither, it makes no difference.
See Nil Python for what an extension module is for and what a declaration looks like.
To reach another language, name the file
A quoted string with a foreign extension routes that file through the other frontend. What the string may look like differs by language, because each frontend keeps its own convention rather than a symmetry imposed on all three:
| you are writing | spelling | what it does |
|---|---|---|
| Pascal | uses './mymath.c'; |
compiles mymath.c through the C frontend; its functions are callable unqualified |
| Pascal | uses './mymath.c' as cmath; |
the same, and gives the file a scope name — see name collisions |
| Pascal | uses './mymod.pas'; |
an ordinary Pascal unit named by path rather than by unit name |
| C | #include "./lib2.c" |
ordinary C inclusion |
| Nil Python | import './mymod.pas' as m |
a path (it contains a /), resolved against the importing file's directory |
| Nil Python | import 'sysutils.pas' as su |
a unit name carrying an extension, resolved through the normal search chain — this is how a .npy reaches lib/rtl |
program cubes;
uses './mymath.c'; { C's cube, deliberately }
begin
WriteLn(cube(3.0):0:1);
end.
The C functions the file defines become callable from Pascal.
Two asymmetries worth knowing rather than discovering:
- The slash is the difference in Nil Python. With a
/the string is an authoritative path; without one it is a unit name and goes through the search chain. Both are unambiguous because both are string literals. - Pascal has only the path form.
uses 'sysutils.pas' as su;— a quoted name with no slash — is not accepted from Pascal (uses: unit source not found), because Pascal already reaches its own RTL by bare unit name and needs no second route to it. - The dotted spelling is not built.
import mymod.pas as mfails, and deliberately so:a.bis Python's package-submodule syntax, and making an extension distinguishable from a submodule would need a whitelist that a real package with a submodule namedcorpaswould defeat. The quoted form is the supported one.
What cross-import is for
Cross-import exists so a program can reach the other language's real
libraries — import sqlite.c from Nil Python to compile SQLite in
statically, or pulling a C numerics routine into a Pascal program.
It is not a route for a .npy to borrow Pascal's RTL. Each frontend has its own
runtime library, and each keeps its own semantics; see
Which reference implementation applies
below.
Symbol resolution — which declaration
Case must agree across languages
A cross-language match is case-sensitive, even though Pascal itself is not. Carrying the earlier example forward:
program t2;
uses './mymath.c';
begin
WriteLn(Cube(3.0):0:1); { error: undefined variable (Cube) }
end.
cube matches C's cube; Cube does not. Within Pascal, Cube and cube
remain the same name as always — the case rule applies only when the candidate
comes from another language.
This one rule closes most of the collision class on its own, because the two
spelling conventions do not overlap: Pascal library names are capitalised
(Exp, Round, Sin) and C library names are lowercase (exp, round,
sin).
Your own language wins
Where a name genuinely exists in both languages, the call site's own language takes precedence. It outranks import order.
A C call to exp binds C's exp; a Pascal call to Exp binds Pascal's. They
are different functions and they are allowed to be — the two implementations
have different accuracy targets and different edge cases, and neither is a
degraded copy of the other. Round(2.5) is 2 in Pascal and 3 in C, and both
are correct for their language.
Where a real ambiguity survives, the compiler's obligation is to warn and name what it picked, not to guess quietly — though that warning is not emitted yet (see current status). Qualification (below) is the escape, and it works today.
If you deliberately pull both math.pas and math.c into scope and then write
an ambiguous bare call, you own that outcome. The compiler will tell you what it
did; it will not pretend the question does not exist.
Shadowing is allowed — and preferred
A declaration hides a same-named one from an earlier or outer scope, unless it
is marked overload. This is the ordinary Object Pascal rule, and it applies to
imported cross-language names too:
program shadow;
uses './mymath.c';
function cube(x: Double): Double;
begin
cube := 999.0;
end;
begin
WriteLn(cube(3.0):0:1); { 999.0 — the local one }
end.
For a uses a, b clause, the last unit named wins, matching FPC.
Shadowing is not something to apologise for. The reference implementations allow it, so PXX does, and the shadowed routine stays reachable under a qualified name.
Qualification reaches past the shadow
A qualified reference has already named its scope, so scope hiding does not apply to it:
program qual2;
uses pu; { pu declares Cube, returning 222.0 }
function Cube(x: Double): Double;
begin
Cube := 999.0;
end;
begin
WriteLn(Cube(3.0):0:1); { 999.0 — the local one shadows }
WriteLn(pu.Cube(3.0):0:1); { 222.0 — qualified, reaches pu's }
end.
Same rule for the built-ins: System.Random(i + 1) reaches the builtin even
when a used unit declares its own Random.
Across languages, give the file a scope name
This is the escape that makes own-language-first a rule rather than a wall. Import the foreign file with a scope name, and the name you could not reach unqualified is reachable through it:
{ cm.c: double exp(double x) { return 42.0; }
double cube(double x) { return x*x*x; } }
program q;
uses math, './cm.c' as cmath;
begin
WriteLn(Exp(1.0):0:4); { 2.7183 — Pascal's own, as own-language-first says }
WriteLn(cmath.exp(1.0):0:4); { 42.0000 — C's, asked for by name }
WriteLn(cube(3.0):0:1); { 27.0 — no collision, so no qualification needed }
end.
Nothing becomes unreachable; a colliding name just has to be asked for. A
foreign name that collides with nothing — cube here — still binds unqualified.
Which reference implementation applies
Each frontend follows its own language's reference implementation: CPython
for .npy, FPC for .pas, and ISO C / the C library for .c. Deviations from
that default live behind explicit --strict-* flags.
This is the rule that explains most surprises, so it is worth seeing measured. The same rounding call, in three languages, on the same compiler:
{ r.pas }
program r;
begin
WriteLn(Round(2.5)); { 2 }
WriteLn(Round(3.5)); { 4 }
end.
# r.npy
print(round(2.5)) # 2
print(round(3.5)) # 4
/* r.c */
#include <stdio.h>
#include <math.h>
int main(void) {
printf("%.0f\n%.0f\n", round(2.5), round(3.5)); /* 3, then 4 */
return 0;
}
Pascal and Nil Python round half to even, because FPC and CPython do. C rounds half away from zero, because C does. Nothing here is a bug in the other two.
The practical consequence for mixed-language programs: when you cross a language boundary, you cross a semantics boundary too. The function you imported behaves the way its own language says it should, not the way the calling language would have.
Current status
Two parts of the above are settled rules that the compiler does not fully enforce yet. They are documented here because they are the rules the language has, but do not write code that depends on them landing:
- Own-language-first is implemented, but it does not warn. The precedence
is real and it does outrank import order — a Pascal
Expstays Pascal's whether the C file is named before or aftermathin theusesclause. What is still missing is the diagnostic: an ambiguous cross-language bare call resolves silently rather than announcing what it picked. from '<file>' import <name>is not built. Theasform is how a foreign file is imported;from 'sysutils.pas' import Trimis refused with expected a module name after from.
Module scoping — specifically whether a uses clause is transitive — is not
documented here yet, because it is still being settled in the compiler.