Skip to content

A name the emitted code reads or declares can be one a C# name holds #397

Description

@edgarmesquita

The translation reads some names it never declares, and declares some that a C# name can reach. C# sees neither, so a program C# accepts can mean something else on the web.

Measured

Conformance probes executed on both sides on 2026-09-24 at d4fdd6c, except the last, which is the plain-class emitter's output:

  • A local named like a global the lowering calls. int parseInt = 2; return Convert.ToInt32("21") * parseInt; answers 42 in .NET and throws here (parseInt is not a function), because Convert.ToInt32 over text is parseInt('21', 10). var crypto = "xy"; beside Guid.NewGuid() throws the same way (crypto.randomUUID is not a function).
  • A name the lowering declares, reached by a C# name. int _sum = 10; xs.Sum(v => v + _sum) over { 1, 2 } answers 23 in .NET and 4 here, with no error: SumStrategy names its accumulator _sum, and the selector's _sum reads it.
  • A local named like a binder a LINQ lowering declares, measured on the branch of 🐛 fix: a name the transpiler changes lands on no name its scope holds #399 at ed8681f. var key = 10; read in a key selector, new[] { 1, 2, 3, 4 }.GroupBy(x => x % 2 + key).Count(), throws ReferenceError: Cannot access 'key' before initialization: the reduce arrow of GroupByStrategy declares const key and the selector reads it. int a = -1; read in new[] { 3, 1, 2 }.OrderBy(x => x * a).First() answers 1 where .NET answers 3, with no error: the comparator of OrderByStrategy takes (a, b). Both are common names, and a local FUNCTION called key or a fails the same way (ReferenceError, TypeError).
  • A local called props in a constructor with no parameters of its own. The plain-class emitter adds a props parameter, and let props = 5 beside it does not parse.

The free names the lowerings read, from the compiler's source: console (Console.WriteLine), parseInt and parseFloat (Convert over text), crypto (Guid.NewGuid), encodeURIComponent and decodeURIComponent (Uri), undefined, and the constructor's props.

By reading, to measure first: AverageStrategy names its accumulator _sum too, and the two strategies name their other arrows' parameters _x, _a and _b. Copilot's review of #399 names more of them: _idx (List.Remove, which never declares it, #400), and _m, _k, _g, _r, _y, _z in the LINQ lowerings (DistinctStrategy, LinqTableStrategy). A local called undefined or console would shadow the same way. A switch statement's _s did not collide in a constant switch, since that shape does not declare it.

#399 makes a local FUNCTION avoid all of these: a name written with an underscore always takes a $, and the globals are a list that a test derives from the compiler's source. A local keeps its C# spelling, so here it is the emitted code that has to move.

What it wants

The emitted code reading no free name a C# name can reach, and declaring none. A synthesized name is $-prefixed like the IR's own binders ($0, $item), since no C# identifier holds a $. The globals are reached where no local can shadow them (globalThis.parseInt, globalThis.crypto, void 0 for undefined), and the constructor's parameter is $props. Each divergence wants a conformance case that fails before and passes after.

Activity

  1. edgarmesquita commented on Sep 26, 2026

    @edgarmesquita
    ContributorAuthor

    One slice of this is in #484: a switch's subject is bound to $s now, in both switch lowerings (the if-chain of a pattern switch and the switch expression's arrow).

    #484 declares a section's and an arm's variables in the switch's own scope, which made the old name reachable: return 1 switch { 1 => int.TryParse("2", out var _s) ? _s : 0, _ => 0 }; was let _s; const _s = 1, a module that did not parse. Two conformance cases pin it.

    The rest stays here, the globals included... #484 briefly escaped a local called undefined, NaN or Infinity and then withdrew it, since this issue moves the emitted code instead of the local.

  2. edgarmesquita commented on Sep 26, 2026

    @edgarmesquita
    ContributorAuthor

    Two things #399 (merged as 29c4286) leaves for this issue, so that whoever does it removes them rather than keeping them beside the real fix.

    • The constructor's props is fixed only where it collides. TypeScriptEmitter.ConfigParameter names the config object $props when the constructor already binds props, as a parameter or as a local of its body, and props otherwise. What this issue wants, $props always, is the structural form: it needs no condition and no scan of the constructor. When it lands, ConfigParameter goes, and so does the props entry in LocalFunctionName.EmittedNames. The component pins will move in every constructor, which is why 🐛 fix: a name the transpiler changes lands on no name its scope holds #399 did not take it at its seventh round.
    • The globals a local function yields to are a list that will have nothing left to protect. LocalFunctionName.EmittedNames holds console, parseInt, parseFloat, isNaN, crypto, setTimeout, the four URI functions and undefined. A test (LocalFunctionNameTests) reads the compiler's source for every global it emits and fails on one missing from that list. Once the emitted code reaches them through globalThis and void 0, no local function can shadow them, so the list empties. The same scan then becomes this issue's guard, turned around: the emitted code reads no global by its bare name.

    #484 withdrew its escape of locals named undefined, NaN and Infinity (31aa0b8) so that this issue keeps one mechanism for the collision. The local keeps its spelling and the emitted code moves.

  3. added theissue type on Oct 5, 2026
  4. moved this from In progress to In review in eQuantic UIon Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions