Compile generic arrow bindings inside functions that use no enclosing frame (#1627)

This commit is contained in:
Chris Tate
2026-10-21 02:54:32 -05:00
committed by GitHub
5 changed files with 161 additions and 60 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",
@@ -2210,29 +2224,54 @@ export function implicitLocalFnNodeOf(
);
if (redeclared) return null;
if (!isConst && !bindingNeverReassigned(lowerer, sym, decl)) return null;
// The capture scan: instances are module functions with no frame.
let captures = false;
const scan = (n: ts.Node): void => {
if (captures) return;
if (n.kind === ts.SyntaxKind.ThisKeyword || n.kind === ts.SyntaxKind.SuperKeyword) {
// Arrow bodies see the ENCLOSING this; function expressions
// rebind their own — but a bare `this` there is untyped JS
// dynamism either way. Reject both, cheaply and soundly.
captures = true;
return;
}
if (ts.isIdentifier(n)) {
const s = lowerer.checker.getSymbolAtLocation(n);
const d = s ? s.valueDeclaration : undefined;
if (
d &&
ts.getSourceFileOfNode(d) === ts.getSourceFileOfNode(decl) &&
!(d.pos >= init.pos && d.end <= init.end)
) {
// Declared outside the initializer, in this file: a capture
// exactly when some enclosing FUNCTION scope declares it —
// module-scope declarations are reachable from any module
// function.
// Instances are module functions with no frame.
return capturesEnclosingFrame(lowerer, init, decl, init.body) ? null : init;
};
const node = probe();
if (node === null) {
lowerer.implicitLocalFns.set(decl, null);
return null;
}
return node;
}
/** True when `fn`, bound by `decl`, uses its enclosing function's frame
* somewhere in `scope`: `this`, `super`, `arguments` or `new.target`, or
* a value or type declared in an enclosing function (a local, a parameter,
* an enclosing generic's type parameter). Module-scope declarations and
* the binding itself are reachable from the module functions the
* instances compile to. */
function capturesEnclosingFrame(
lowerer: Lowerer,
fn: ts.FunctionExpression | ts.ArrowFunction,
decl: ts.VariableDeclaration,
scope: ts.Node,
): boolean {
const file = ts.getSourceFileOfNode(decl);
const self = lowerer.checker.getSymbolAtLocation(decl.name);
let captures = false;
const scan = (n: ts.Node): void => {
if (captures) return;
if (
n.kind === ts.SyntaxKind.ThisKeyword ||
n.kind === ts.SyntaxKind.SuperKeyword ||
ts.isMetaProperty(n)
) {
// Arrow bodies see the ENCLOSING this; function expressions
// rebind their own — but a bare `this` there is untyped JS
// dynamism either way. Reject both, cheaply and soundly.
captures = true;
return;
}
if (ts.isIdentifier(n)) {
const s = lowerer.checker.getSymbolAtLocation(n);
if (s === undefined) {
if (n.text === "arguments") captures = true;
} else if (s !== self) {
for (const d of s.declarations) {
if (ts.getSourceFileOfNode(d) !== file || (d.pos >= fn.pos && d.end <= fn.end)) continue;
// Declared outside the function, in this file: a capture
// exactly when some enclosing FUNCTION scope declares it.
for (
let p: ts.Node | undefined = d.parent;
p !== undefined && !ts.isSourceFile(p);
@@ -2245,17 +2284,11 @@ export function implicitLocalFnNodeOf(
}
}
}
ts.forEachChild(n, scan);
};
scan(init.body);
return captures ? null : init;
}
ts.forEachChild(n, scan);
};
const node = probe();
if (node === null) {
lowerer.implicitLocalFns.set(decl, null);
return null;
}
return node;
scan(scope);
return captures;
}
/** Registers (or returns) the GenericFnInfo of a qualifying implicit-any
@@ -2468,17 +2501,24 @@ export function bindingGenericFnInfoOf(
if (existing) return existing;
const name = (decl.name as ts.Identifier).text;
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);
n = n.parent
) {
if (ts.isFunctionLike(n)) {
lowerer.unsupported(
"SC1090",
fnNode,
`generic arrow/function-expression bindings declared inside functions (the compiled instantiations of '${name}' are module functions and cannot capture the enclosing frame — declare the binding at module scope)`,
);
if (capturesEnclosingFrame(lowerer, fnNode, decl, fnNode)) {
lowerer.unsupported(
"SC1090",
fnNode,
`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;
}
}
const sym = lowerer.checker.getSymbolAtLocation(decl.name);
@@ -2510,15 +2550,21 @@ export function bindingGenericFnInfoOf(
}
checkGenericParameterNames(lowerer, fnNode.parameters.nodes, true);
const stmt = decl.parent?.parent; // declarator → list → statement (nsPathPrefix wants the statement)
// A binding in a function or block can share its name with another; its
// instances take the declaration's position.
const topLevel =
stmt?.parent !== undefined && (ts.isSourceFile(stmt.parent) || ts.isModuleBlock(stmt.parent));
const info: GenericFnInfo = {
decl: fnNode,
baseName: name,
qualifiedName: lowerer.qualify(
ts.getSourceFileOfNode(decl),
nsPathPrefix(stmt ?? decl, decl) + name,
nsPathPrefix(stmt ?? decl, decl) +
(topLevel ? name : `${name}%l${ts.tokenStartOfNode(decl)}`),
),
typeParams,
instances: new Map(),
...(functionScoped ? { functionScoped: true as const } : {}),
};
lowerer.bindingGenericFns.set(fnNode, info);
lowerer.genericFnsBySymbol.set(sym, info);
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/generic-local-function-values.ts"
],
"diags": []
}
@@ -0,0 +1,33 @@
// Generic arrow and function-expression bindings declared inside functions
// and methods that use nothing from the enclosing frame compile like
// module-scope bindings: each call instantiates them, they may call
// themselves and read module-scope bindings, and two functions may declare
// bindings of the same name.
function append(): void {
const put = <T>(xs: T[], x: T): T[] => [...xs, x];
console.log(put([1, 2], 3).join(","), put(["a"], "b").join(","));
}
append();
const LIMIT = 3;
function prepend(): void {
const put = <T>(xs: T[], x: T): T[] => [x, ...xs];
console.log(put([1], 0).join(","));
const twice = function <T>(x: T): T[] {
return [x, x];
};
console.log(twice("z").join(","), twice(7).length);
const count = <T>(xs: T[], n: number): number => (n <= 0 ? xs.length : 1 + count(xs, n - 1));
console.log(count([1], 3));
const firstFew = <T>(xs: T[]): T[] => xs.slice(0, LIMIT);
console.log(firstFew([1, 2, 3, 4]).join(","), firstFew(["p", "q"]).join(","));
}
prepend();
class Box {
run(): string {
const wrap = <T>(x: T): { v: T } => ({ v: x });
return JSON.stringify([wrap(1), wrap("s"), wrap([true])]);
}
}
console.log(new Box().run());
+15 -6
View File
@@ -19,13 +19,22 @@ console.log(typeof stored);
const flipAlias = flip;
console.log(flipAlias(5));
// Declared inside a function: instances are module functions and cannot
// capture the enclosing frame.
function outer(): number {
const inner = <T>(x: T): T => x;
return inner(3);
// Declared inside a function and reading its parameter: instances are
// module functions and cannot capture the enclosing frame.
function outer(base: number): number {
const inner = <T>(x: T): [T, number] => [x, base];
return inner(3)[1];
}
console.log(outer());
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
@@ -57,32 +57,39 @@ generic-value-bindings.ts:20:13 - error SC2004: uses of 'flipAlias' inherit the
hint: the declaration of 'flipAlias' did not compile — fix the diagnostic reported there and these sites clear with it
generic-value-bindings.ts:25:17 - error SC1090: generic arrow/function-expression bindings declared inside functions (the compiled instantiations of 'inner' are module functions and cannot capture the enclosing frame — declare the binding at module scope) are not supported yet
generic-value-bindings.ts:25:17 - error SC1090: generic arrow/function-expression bindings that use the frames of their enclosing functions (the compiled instantiations of 'inner' are module functions and cannot capture its locals, parameters, type parameters or 'this' — pass them as arguments, or declare the binding at module scope) are not supported yet
24 | function outer(): number {
25 | const inner = <T>(x: T): T => x;
| ^~~~~~~~~~~~~~~~~
26 | return inner(3);
24 | function outer(base: number): number {
25 | const inner = <T>(x: T): [T, number] => [x, base];
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26 | return inner(3)[1];
generic-value-bindings.ts:26:10 - error SC2004: uses of 'inner' inherit the blocker on its declaration
25 | const inner = <T>(x: T): T => x;
26 | return inner(3);
25 | const inner = <T>(x: T): [T, number] => [x, base];
26 | return inner(3)[1];
| ^~~~~
27 | }
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 | }