Repository navigation
Speculative Foreign Names
Speculative design. Open, not settled — thinking and proposals, not shipped behavior. See Roadmap for what's in flight.
Status: speculative (2026-10-01). Every system we read from or write to has globals: names that already mean something there. A variable that crosses into a system where its name is a global gets captured, and silently means something else. This page is about finding those globals per system and renaming around them at the boundary, once, in the format, rather than in every symbol.
Inbound (a foreign expression read as ours). A foreign variable whose name is one of our
globals is captured by our definition. The case we've hit: Wolfram's documentation uses e
and i as plain variables (a list element, a loop index), and compute-engine reads the
MathJSON symbols e and i as Euler's number and the imaginary unit wherever it boxes them.
About seventy farmed rows read {a, b, c, d, e} as a list ending in Euler's number.
The Wolfram adapter now renames a variable e or i to a spare letter (#518, applied to the
farmed rows in #541).
Outbound (our expression written for another system). One of our free variables whose name is a global there is captured by that system:
-
Wolfram. Emit passes a free name through by name. A free
N,D,C,E,I,OorKof ours would be Wolfram's function or constant. -
SymPy and Sage. They don't auto-declare names, so emit already spells a free variable
explicitly (
Symbol("x"),SR.var("x")), and capture can't happen there. Bound names still can: aSumindex, aFunctionparameter, a loop variable in a harness. One scan has already collided a harness loop variable with theiinsidei_1(see Algebras, Units and Places §5). -
Code lanes (Python with mpmath, Rust, Julia, WGSL, JavaScript). They emit bare
identifiers, so a language's builtins and keywords (
sum,max,len,lambda), and the globals the lane imports (from mpmath import *brings ine,pi,j,inf,epsand more), are all candidates. Most aren't audited.
Not every system binds e. The point is that each binds something, and Sage binds a great
deal. So the work per system is to know its globals, not to assume they look like ours.
The answer coming back. An answer that mentions a renamed variable is renamed back before it's compared or shown. So a rename is a map that goes out with the expression and inverts on the answer, never a one-way edit.
A symbol package contributes how its heads are spelled in a format (registerFormat rules, or a
row in a fixed syntax's table: Kernels and Front Ends §6,
Formats). It shouldn't have to know that N is taken in Wolfram, or lambda in Python. The
format package owns its language's globals and the rename pass around them. Every symbol's
spelling, including one from an external registry
(Lazy Engine §3), goes through that pass without knowing it's there.
So a format package gains:
-
A reserved set: the language's globals, generated where the language can list them:
- Wolfram:
Names["System*"](we already keep this,system-names.ts`); - Python:
keyword.kwlist,dir(builtins), and the lane's namespace after its imports; - Sage:
globals()at startup; - Julia:
names(Base)plus the lane'susings; - Lean: what the opened namespaces export;
- ours: the names compute-engine has a definition for (
DEFINED_NAMES, fromlookupDefinition).
A generated set is committed with a drift check, as the system names are now.
- Wolfram:
-
A rename pass, on the way out and on the way in: any free or bound name that collides with the other side's reserved set is renamed to a fresh one, which both sides leave free and the expression doesn't already use. The choice is deterministic (a fixed spare-letter order, as the Wolfram adapter's is), so ids, goldens and captions stay stable.
-
The inverse map, applied to whatever comes back.
The free-variable rule the oracles already use (a name is free exactly when compute-engine has no definition for it) is the inbound reserved set's other face: one table answers both "is this ours?" and "would this be captured here?".
| piece | today | moves to |
|---|---|---|
| Wolfram's globals | wolfram/src/system-names.ts |
the Wolfram format package's reserved set |
| inbound rename, Wolfram |
renameConstants in reference/scripts/wolfram-examples.ts, e and i only |
the Wolfram format's inbound pass, over our whole reserved set |
| outbound, SymPy/Sage |
oracle/src/emit.ts wraps free symbols explicitly |
stays, plus the pass for bound names |
| outbound, Wolfram | passes free names through | the pass, against system-names
|
| code lanes | bare identifiers | each lane's format: language keywords, builtins, imports |
| our globals | oracle/src/defined-names-data.ts |
shared by every format's inbound pass |
Separate from this, and already raised with compute-engine: Epsil itself reads a bare e or
i as a constant, so a hand-written example can't use either as a variable. Until that's
settled upstream, hand-written rows rename (e and i to k in At, Do, Module), and our
reserved set includes both. When Epsil changes, only the set changes.
Per format, a generated round trip: an expression whose free and bound variables are exactly the other side's reserved names. Out, evaluated there, back: the result must name the original variables and leave them free. It's cheap, needs no examples, and an external package that arrives with a new spelling gets checked by it for free.
-
How far to generate.
globals()in Sage is large and includes anything a lane imports. A set that's too wide renames needlessly; one that's too narrow misses captures. Possibly: the generated set, intersected with the names our expressions actually use (asDEFINED_NAMESis scoped to the reference data). - Names inside strings and options. An option key or a string argument isn't a variable; the pass must not rename it.
- Captions and prose that mention a renamed letter.
-
Wolfram's capitals. Wolfram's own documentation never uses
E/I/Nas variables, but other sources (Fungrim, a user notebook) might.