From 3e0863246766be37fb8184f27add8462ff7b84fe Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 02:51:36 -0500 Subject: [PATCH] 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. --- .../frontend/lowering/generic-functions.ts | 17 ++++++++++++++ tests/diagnostics/generic-value-bindings.ts | 9 ++++++++ .../generic-value-bindings.ts.txt | 23 ++++++++++++------- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 2cc162342..abcd5a584 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -87,6 +87,10 @@ export interface GenericFnInfo { * defining literal must sit at module scope (no enclosing frame to * capture). */ objectLiteral?: true; + /** A binding declared inside a function: each evaluation of the + * declaration makes a new function object in JavaScript, while the + * compiled instances are shared module functions, so only calls lower. */ + functionScoped?: true; /** IMPLICIT-ANY monomorphization (npm-static JS): parallel to * decl.parameters — the param's own symbol when the slot is a BINDABLE * implicit-any parameter (untyped, identifier-named, never written in @@ -1184,6 +1188,16 @@ export function lowerGenericFnValue( lowerer.noteEdge(inst.name); return { kind: "closure", fnName: inst.name, captures: [], type: funcType, loc }; } + // Each call of the enclosing function makes a new function object for a + // binding declared inside it (`make() !== make()` when make returns + // it), which one shared compiled instance cannot give. + if (info.functionScoped) { + lowerer.unsupported( + "SC1090", + ref, + `generic arrow/function-expression bindings declared inside functions as values (each call of the enclosing function makes a new '${info.baseName}' function object, while its compiled instances are shared module functions — call '${info.baseName}' directly, or declare it at module scope)`, + ); + } const fenceUnpinned: () => never = () => lowerer.unsupported( "SC1090", @@ -2489,6 +2503,7 @@ export function bindingGenericFnInfoOf( if (fnNode.asteriskToken) lowerer.unsupported("SC1071", fnNode); // Inside a function, the compiled instances are module functions, so // the initializer may not use the enclosing frame. + let functionScoped = false; for ( let n: ts.Node | undefined = decl.parent; n !== undefined && !ts.isSourceFile(n); @@ -2502,6 +2517,7 @@ export function bindingGenericFnInfoOf( `generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of '${name}' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope)`, ); } + functionScoped = true; break; } } @@ -2548,6 +2564,7 @@ export function bindingGenericFnInfoOf( ), typeParams, instances: new Map(), + ...(functionScoped ? { functionScoped: true as const } : {}), }; lowerer.bindingGenericFns.set(fnNode, info); lowerer.genericFnsBySymbol.set(sym, info); diff --git a/tests/diagnostics/generic-value-bindings.ts b/tests/diagnostics/generic-value-bindings.ts index 6f8e50fd2..a7de7aa1b 100644 --- a/tests/diagnostics/generic-value-bindings.ts +++ b/tests/diagnostics/generic-value-bindings.ts @@ -27,6 +27,15 @@ function outer(base: number): number { } console.log(outer(4)); +// Used as a value inside a function: each call of the enclosing function +// makes a new function object (`make() !== make()`), and the compiled +// instances are shared module functions. +function make(): (x: number) => number { + const same = (x: T): T => x; + return same; +} +console.log(make() === make()); + // An ambient binding of generic function type now compiles to Node's exact // ReferenceError at the access (the declare-erasure stance — no diagnostic // here; the optional chain cannot guard the root's own throw). diff --git a/tests/harness/__snapshots__/generic-value-bindings.ts.txt b/tests/harness/__snapshots__/generic-value-bindings.ts.txt index 702ead841..04fa9d069 100644 --- a/tests/harness/__snapshots__/generic-value-bindings.ts.txt +++ b/tests/harness/__snapshots__/generic-value-bindings.ts.txt @@ -73,16 +73,23 @@ generic-value-bindings.ts:26:10 - error SC2004: uses of 'inner' inherit the bloc hint: the declaration of 'inner' did not compile — fix the diagnostic reported there and these sites clear with it -generic-value-bindings.ts:39:13 - error SC1090: results that are themselves generic functions ('(y: U) => string' keeps its type parameters — no call-site instantiation pins them, and the returned value would need the producing call's frame; restructure to one generic function taking all arguments) are not supported yet +generic-value-bindings.ts:35:10 - error SC1090: generic arrow/function-expression bindings declared inside functions as values (each call of the enclosing function makes a new 'same' function object, while its compiled instances are shared module functions — call 'same' directly, or declare it at module scope) are not supported yet - 38 | const curried = (x: T) => (y: U): string => `${x}|${y}`; - 39 | console.log(curried(1)("a")); + 34 | const same = (x: T): T => x; + 35 | return same; + | ^~~~~~~~~~~~ + 36 | } + +generic-value-bindings.ts:48:13 - error SC1090: results that are themselves generic functions ('(y: U) => string' keeps its type parameters — no call-site instantiation pins them, and the returned value would need the producing call's frame; restructure to one generic function taking all arguments) are not supported yet + + 47 | const curried = (x: T) => (y: U): string => `${x}|${y}`; + 48 | console.log(curried(1)("a")); | ^~~~~~~~~~ - 40 | + 49 | -generic-value-bindings.ts:51:15 - error SC1090: calls of the generic method 'm' through this receiver (no compiled declaration with a body resolves statically here — ambient 'declare class' and interface-only methods are signature-only, and only class, static, and object-literal generic methods with bodies monomorphize) is not supported yet +generic-value-bindings.ts:60:15 - error SC1090: calls of the generic method 'm' through this receiver (no compiled declaration with a body resolves statically here — ambient 'declare class' and interface-only methods are signature-only, and only class, static, and object-literal generic methods with bodies monomorphize) is not supported yet - 50 | function throughParam(a: Amb): void { - 51 | console.log(a.m(4)); + 59 | function throughParam(a: Amb): void { + 60 | console.log(a.m(4)); | ^~~~~~ - 52 | } \ No newline at end of file + 61 | } \ No newline at end of file