Commit Graph
9 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 3e2b4ab25b Share reassigned bindings with generic arrow instances through closures
A generic arrow declared inside a function compiles to shared module
instances that took the enclosing values they read as trailing
parameters: a copy, so any binding written in the file was refused,
including a counter the arrow itself increments. When one of its
captures is reassigned, the instance is now a lifted function that
receives every capture through a closure environment, and each call site
calls a closure over its own boxes, the representation ordinary closures
use. Writes from the instance, the frame and other closures meet in the
same box, as in Node. Arrows whose captures never change keep the
trailing parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 19:49:30 -07:00
Malte Ubl 93765cd6a2 Pass enclosing-frame values to generic arrow instances as trailing parameters
A generic arrow or function-expression binding declared inside a function
used to be refused (SC1090) whenever its body read anything from the
enclosing frame, because its monomorphized instances are module functions
with no closure.

Give each instance the frame it needs: every value the binding reads from
the enclosing function becomes a TRAILING parameter of the instance, and
every call site (which sits in the declaring frame or in a closure over it)
passes the current value after the declared arguments. The instance body
binds the same symbol, so it resolves the reference like any parameter.

Sound only while the original binding never changes, so the fence now
covers exactly what a copy cannot stand in for: `this`, `super`,
`new.target`, `arguments`, an enclosing type parameter, an enclosing
generic scope (per-instantiation types), and any binding with a write
anywhere in the file or declared after the binding (temporal dead zone).

Corpus: withClientIdentity/withIdentity, physicalModels (captured Set),
listTwo (captured local function), runTracked (async, call from a nested
closure) and twice (two instances, different capture values). The
generic-value-bindings diagnostics fixture now pins the fences that remain
(a capture the file reassigns, and an enclosing generic scope) instead of
the never-reassigned parameter it used to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5b23e6ccf110c66e96fad472265d7b53db0f342a)
2026-10-28 19:49:28 -07:00
Chris Tate 70697b45c3 Give generic arrow bindings Node's behavior before their declarations run
Generic arrow and function-expression bindings compile to shared
instances, and their declarations emit no store. Code that can run before
the declaration then diverged from Node.

- Inside a function, a nested function lowered before the declaration
  reads the binding through a TDZ box that nothing initialized. That
  covers closures above the declaration and hoisted function
  declarations. Such a read threw a ReferenceError even after the
  declaration ran. The declaration now stores the instance its declared
  signature pins into that box.
- At module scope, calls and pinned value reads went straight to the
  instances even when a hoisted function or an earlier closure ran them
  before the declaration. A binding with such a use now has a flag that
  its declaration sets.
  - Uses that can run first check the flag.
  - A let or const throws Node's ReferenceError before the arguments
    run.
  - A var runs the arguments and then throws "is not a function".
- Uses that follow the declaration stay direct calls: later statements,
  closures created after it, its own initializer, and function
  declarations that only such code calls.

A var binding inside a function that is called from a place that can run
before its declaration would need a per-call flag shared with its
closures. That shape now fails with SC1090 instead of calling the
instance.
2026-10-22 02:56:39 -05:00
Chris Tate 3e08632467 Refuse values of generic bindings declared inside functions
Each call of a function makes a new function object for a generic arrow
or function expression bound inside it, so `make() !== make()` when make
returns the binding, and such values are distinct Map and Set keys. The
compiled instances are shared module functions, which cannot give that
identity, so a value use of such a binding keeps an SC1090 fence that
names the reason. Calls of the binding still compile.
2026-10-21 02:51:36 -05:00
Chris Tateandejc3 45d3d02bf0 Compile generic arrow bindings inside functions that use no enclosing frame
A generic arrow or function expression bound inside a function or method
compiles when it uses nothing from the enclosing function's frame: no
`this`, `super`, `arguments` or `new.target`, and no local, parameter or
type parameter of an enclosing function. Its instantiations are module
functions, as for a module-scope binding, and may call the binding itself
and read module-scope bindings. Instances of bindings in functions and
blocks take the declaration's position in their names, so two functions
may declare bindings of the same name. Bindings that use the enclosing
frame keep their SC1090 fence.

Fixes #1624

Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>
2026-10-21 01:58:22 -05:00
Chris Tate 95b0e2132d Call generic members through receivers that are not statically known
Generic arrow properties and generic methods now have an erased runtime
value, their instance with every type parameter bound to unknown. Calls
through a round trip via any, a function result, an array element or a
class-backed interface call that value and convert the result to the
call's type, as JavaScript does. Direct calls on a known definition keep
their monomorphized path.

Fixes #905
Co-authored-by: Konstantin Mikheev <8560582+konmik@users.noreply.github.com>
2026-10-17 18:36:45 -05:00
Chris Tate e81e4f5ab5 Ambient roots trap whole chains; dead and nullish generic bindings lower to their runtime truth
- ambientUndefVarRootOf walks calls, `new`, optional chains, assertions, instantiation expressions, and tagged templates, and accepts `declare function` and trap-binding roots: Node erases the declaration, so the root read IS the crash — lowerCall, member reads, assignments, and variable declarations lower the whole statement to the catchable ReferenceError, arguments never evaluate, and the never-initialized binding registers as a trap whose references can never run
- never-read unmappable bindings with value-only initializers and writes emit nothing: Node builds the value and drops it, so `var xs2: typeof Array;` and the write-only `var f2: { <T, U>(x: T, y: U): T }` compile instead of fencing on a type the program never consumes
- null!/as-any bindings whose types keep generic signatures (or intern empty record shapes) register no storage; generic-method calls and member reads through them compile to Node's exact TypeError, nullish-to-nullish flows to nothing, heritage-clause uses decline the family so mixin resolution keeps its fence, and checked-dynamic (`any`) bindings keep their dyn story untouched
- assertion-shaped generic arrows (`< <T>(x: T) => T >((x) => x)`) register like annotated bindings and monomorphize per call; stdlib generic members (Promise.then, Object.defineProperty) take the SC2020 lib fence, and Function.prototype.apply/call/bind on compiled function values name the direct-call spelling
- diagnostics fixtures that used `declare` to conjure unconstructible values now conjure them by cast so each fence family keeps its pin; snapshots re-recorded, the deferred checker-panic diagnostic now flushes at its first reference as designed
2026-07-24 02:29:48 -05:00
Chris Tate 0386d8c00f keyof-constrained generics monomorphize per literal key; generic-fn aliases register their target
- Instantiations carry ts-level bindings alongside the mapped IrTypes: a `K extends keyof T` parameter keys its instance on the bound literal (no cross-literal sharing — the bodies read different fields), and literals forward through generic-to-generic calls.
- `T[K]` inside instance bodies resolves through the bound checker types (mapBoundIndexedAccess via the new resolveTypeParamTs hook), and `o[k]` reads whose key TYPE proves one literal — a keyof-bound parameter or a literal-typed const — lower to static field reads.
- `const h = id` registers the target's GenericFnInfo under the alias's symbol (chains, block scope, pinned values, and calls all resolve like the target's own name); non-const/reassigned aliases fence by name, and unpinned VALUE uses keep their fence at the reference.
- Fixtures 2550 (keyof/pick shapes, forwarding, runtime keys, literal-typed key vars) and 2551 (alias chains, contextual pinning, annotated generic aliases), byte-identical vs Node on both backends.
2026-07-23 17:20:03 -05:00
Chris Tate 23d5918a53 init 2026-07-22 18:08:04 -05:00