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.
This commit is contained in:
Chris Tate
2026-10-21 02:51:36 -05:00
parent 45d3d02bf0
commit 3e08632467
3 changed files with 41 additions and 8 deletions
@@ -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);
@@ -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 = <T>(x: T): T => x;
return same<number>;
}
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).
@@ -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 ('<U>(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 = <T>(x: T) => <U>(y: U): string => `${x}|${y}`;
39 | console.log(curried(1)("a"));
34 | const same = <T>(x: T): T => x;
35 | return same<number>;
| ^~~~~~~~~~~~
36 | }
generic-value-bindings.ts:48:13 - error SC1090: results that are themselves generic functions ('<U>(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 = <T>(x: T) => <U>(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 | }
61 | }