diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index ac5949656..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", @@ -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); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-local-function-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-local-function-values.ts.json new file mode 100644 index 000000000..c7f9cd86a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/generic-local-function-values.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/generic-local-function-values.ts" + ], + "diags": [] +} diff --git a/tests/corpus/generic-local-function-values.ts b/tests/corpus/generic-local-function-values.ts new file mode 100644 index 000000000..7ca835a7d --- /dev/null +++ b/tests/corpus/generic-local-function-values.ts @@ -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 = (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 = (xs: T[], x: T): T[] => [x, ...xs]; + console.log(put([1], 0).join(",")); + const twice = function (x: T): T[] { + return [x, x]; + }; + console.log(twice("z").join(","), twice(7).length); + const count = (xs: T[], n: number): number => (n <= 0 ? xs.length : 1 + count(xs, n - 1)); + console.log(count([1], 3)); + const firstFew = (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 = (x: T): { v: T } => ({ v: x }); + return JSON.stringify([wrap(1), wrap("s"), wrap([true])]); + } +} +console.log(new Box().run()); diff --git a/tests/diagnostics/generic-value-bindings.ts b/tests/diagnostics/generic-value-bindings.ts index 28a8fb366..a7de7aa1b 100644 --- a/tests/diagnostics/generic-value-bindings.ts +++ b/tests/diagnostics/generic-value-bindings.ts @@ -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 = (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 = (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 = (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 diff --git a/tests/harness/__snapshots__/generic-value-bindings.ts.txt b/tests/harness/__snapshots__/generic-value-bindings.ts.txt index 88c886d78..04fa9d069 100644 --- a/tests/harness/__snapshots__/generic-value-bindings.ts.txt +++ b/tests/harness/__snapshots__/generic-value-bindings.ts.txt @@ -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 = (x: T): T => x; - | ^~~~~~~~~~~~~~~~~ - 26 | return inner(3); + 24 | function outer(base: number): number { + 25 | const inner = (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 = (x: T): T => x; - 26 | return inner(3); + 25 | const inner = (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 ('(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