mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Compile generic arrow bindings inside functions that use no enclosing frame (#1627)
This commit is contained in:
@@ -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);
|
||||
|
||||
+6
@@ -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());
|
||||
@@ -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 | }
|
||||
Reference in New Issue
Block a user