mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Give generic arrow bindings Node's behavior before their declarations run
Generic arrow and function-expression bindings compile to shared
instances, and their declarations emit no store. Code that can run before
the declaration then diverged from Node.
- Inside a function, a nested function lowered before the declaration
reads the binding through a TDZ box that nothing initialized. That
covers closures above the declaration and hoisted function
declarations. Such a read threw a ReferenceError even after the
declaration ran. The declaration now stores the instance its declared
signature pins into that box.
- At module scope, calls and pinned value reads went straight to the
instances even when a hoisted function or an earlier closure ran them
before the declaration. A binding with such a use now has a flag that
its declaration sets.
- Uses that can run first check the flag.
- A let or const throws Node's ReferenceError before the arguments
run.
- A var runs the arguments and then throws "is not a function".
- Uses that follow the declaration stay direct calls: later statements,
closures created after it, its own initializer, and function
declarations that only such code calls.
A var binding inside a function that is called from a place that can run
before its declaration would need a per-call flag shared with its
closures. That shape now fails with SC1090 instead of calling the
instance.
This commit is contained in:
@@ -3,15 +3,20 @@ import * as ts from "../ts7/adapter.js";
|
||||
import { bodyReadsArguments } from "../arguments-usage.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrParam,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
STRING,
|
||||
VOID,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { nodeThrowExpr } from "../../ir/build.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { typeKey } from "../type-mapper.js";
|
||||
import { functionCanReturnUndefined } from "../function-completion.js";
|
||||
@@ -22,7 +27,7 @@ import { nsPathPrefix } from "./lower-namespaces.js";
|
||||
import { declSymbolOf } from "./lower-modules.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
import { rejectStaticThis } from "./static-this.js";
|
||||
import { jsBindingHasOpenWrites } from "./lower-stmts.js";
|
||||
import { isVarDeclared, jsBindingHasOpenWrites } from "./lower-stmts.js";
|
||||
import {
|
||||
type ParamShape,
|
||||
paramShapes,
|
||||
@@ -91,6 +96,15 @@ export interface GenericFnInfo {
|
||||
* declaration makes a new function object in JavaScript, while the
|
||||
* compiled instances are shared module functions, so only calls lower. */
|
||||
functionScoped?: true;
|
||||
/** The declaration of a generic arrow/function-expression binding. */
|
||||
bindingDecl?: ts.VariableDeclaration;
|
||||
/** A module-scope binding that code can use before its declaration
|
||||
* runs: the BOOL global its declaration sets, and whether it is a
|
||||
* `let`/`const` (a temporal dead zone) or a `var` (undefined). */
|
||||
presence?: { flagId: string; lexical: boolean };
|
||||
/** runsAfterDeclaration's answers for the function declarations whose
|
||||
* calls it placed. */
|
||||
ordering?: Map<ts.Node, boolean>;
|
||||
/** 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
|
||||
@@ -257,7 +271,14 @@ export function lowerGenericCall(
|
||||
? implicitCallInstance(lowerer, expr, info)
|
||||
: genericCallInstance(lowerer, expr, info);
|
||||
const args = lowerer.completeArgs(expr.arguments.nodes, instance.params, loc, expr);
|
||||
return { kind: "call", callee: instance.name, args, type: instance.returnType, loc };
|
||||
const call: IrExpr & { kind: "call" } = {
|
||||
kind: "call",
|
||||
callee: instance.name,
|
||||
args,
|
||||
type: instance.returnType,
|
||||
loc,
|
||||
};
|
||||
return guardEarlyBindingCall(lowerer, info, expr, call);
|
||||
}
|
||||
|
||||
/** The instance a CALL of a generic function-like names: resolved
|
||||
@@ -1233,6 +1254,47 @@ export function lowerGenericFnValue(
|
||||
}
|
||||
}
|
||||
if (!target) fenceUnpinned();
|
||||
return guardEarlyBindingValue(
|
||||
lowerer,
|
||||
info,
|
||||
ref,
|
||||
pinnedGenericClosure(lowerer, ref, info, target, fenceUnpinned),
|
||||
);
|
||||
}
|
||||
|
||||
/** The value a generic binding declared inside a function stores when a
|
||||
* nested function read it before the declaration lowered (`const read =
|
||||
* () => id; const id: (x: number) => number = <T>(x: T): T => x`): such
|
||||
* a read resolves to an ordinary TDZ local, so the declaration assigns it
|
||||
* the instance its concrete declared signature pins. */
|
||||
export function genericBindingStoredValue(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
info: GenericFnInfo,
|
||||
): IrExpr {
|
||||
const name = decl.name as ts.Identifier;
|
||||
const fenceUnpinned = (): never =>
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
name,
|
||||
`generic function bindings read before their declaration without a concrete declared signature (annotate '${info.baseName}' with one, or declare it before its uses)`,
|
||||
);
|
||||
const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(name));
|
||||
if (sigs.length !== 1) fenceUnpinned();
|
||||
return pinnedGenericClosure(lowerer, name, info, sigs[0]!, fenceUnpinned);
|
||||
}
|
||||
|
||||
/** The closure of the instance of `info` that `target` pins, for a value
|
||||
* read at `ref`. `fenceUnpinned` reports a signature that leaves a type
|
||||
* parameter unbound. */
|
||||
function pinnedGenericClosure(
|
||||
lowerer: Lowerer,
|
||||
ref: ts.Expression,
|
||||
info: GenericFnInfo,
|
||||
target: ts.Signature,
|
||||
fenceUnpinned: () => never,
|
||||
): IrExpr {
|
||||
const loc = locOf(ref);
|
||||
const bindings = new Map<ts.Symbol, IrType>();
|
||||
unifySignatureBindings(lowerer, info, target, bindings);
|
||||
const hofBinding = runtimeOptionalHofGenericBinding(lowerer, ref, info);
|
||||
@@ -2565,9 +2627,18 @@ export function bindingGenericFnInfoOf(
|
||||
typeParams,
|
||||
instances: new Map(),
|
||||
...(functionScoped ? { functionScoped: true as const } : {}),
|
||||
bindingDecl: decl,
|
||||
};
|
||||
lowerer.bindingGenericFns.set(fnNode, info);
|
||||
lowerer.genericFnsBySymbol.set(sym, info);
|
||||
// Module-scope uses resolve to the instances from the start, so those
|
||||
// that can run first need the flag. Inside a function the declaration
|
||||
// registers before any later use lowers, and earlier uses read a TDZ
|
||||
// box (genericBindingStoredValue).
|
||||
if (!functionScoped) {
|
||||
const presence = bindingPresenceOf(lowerer, info, decl, sym);
|
||||
if (presence !== undefined) info.presence = presence;
|
||||
}
|
||||
if (ts.isFunctionExpression(fnNode) && fnNode.name !== undefined) {
|
||||
const inner = lowerer.checker.getSymbolAtLocation(fnNode.name);
|
||||
if (inner) lowerer.genericFnsBySymbol.set(inner, info);
|
||||
@@ -2575,6 +2646,261 @@ export function bindingGenericFnInfoOf(
|
||||
return info;
|
||||
}
|
||||
|
||||
/** Whether `use`, a reference to the binding `info` declares, can only
|
||||
* run after the declaration has run: it follows the declaration in its
|
||||
* statement list (closures count where they are created), sits inside the
|
||||
* initializer itself, or is inside a function declaration whose every
|
||||
* reference does. References from other modules run after this module's
|
||||
* body, and export specifiers only export. A use the check cannot place
|
||||
* answers false. */
|
||||
export function runsAfterDeclaration(lowerer: Lowerer, info: GenericFnInfo, use: ts.Node): boolean {
|
||||
const decl = info.bindingDecl;
|
||||
if (decl === undefined) return true;
|
||||
info.ordering ??= new Map();
|
||||
return useRunsAfter(lowerer, decl, use, info.ordering, new Set());
|
||||
}
|
||||
|
||||
function useRunsAfter(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
use: ts.Node,
|
||||
memo: Map<ts.Node, boolean>,
|
||||
inProgress: Set<ts.Node>,
|
||||
): boolean {
|
||||
if (ts.getSourceFileOfNode(use) !== ts.getSourceFileOfNode(decl)) return true;
|
||||
const init = decl.initializer;
|
||||
if (init !== undefined && use.pos >= init.pos && use.end <= init.end) return true;
|
||||
const stmt = decl.parent?.parent;
|
||||
const list = stmt?.parent;
|
||||
if (stmt === undefined || list === undefined) return false;
|
||||
for (let n: ts.Node = use; n.parent !== undefined; n = n.parent) {
|
||||
if (n.parent === list) return n === stmt ? use.pos >= decl.end : n.pos >= stmt.end;
|
||||
if (ts.isFunctionDeclaration(n)) return functionRunsAfter(lowerer, decl, n, memo, inProgress);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether every call of the function declaration `fn` runs after `decl`
|
||||
* has run. A function in progress counts as running after: its calls of
|
||||
* itself, directly or through other functions, add no earlier caller.
|
||||
* Only answers that depend on no function in progress are memoized. */
|
||||
function functionRunsAfter(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
fn: ts.FunctionDeclaration,
|
||||
memo: Map<ts.Node, boolean>,
|
||||
inProgress: Set<ts.Node>,
|
||||
): boolean {
|
||||
const known = memo.get(fn);
|
||||
if (known !== undefined) return known;
|
||||
if (inProgress.has(fn)) return true;
|
||||
// `export default function () {}`: only importers call it, after this
|
||||
// module's body has run.
|
||||
if (fn.name === undefined) return true;
|
||||
inProgress.add(fn);
|
||||
let result = true;
|
||||
try {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(fn.name);
|
||||
if (symbol === undefined) result = false;
|
||||
else {
|
||||
for (const ref of bindingReferences(lowerer, ts.getSourceFileOfNode(fn), symbol, fn.name)) {
|
||||
if (ref.pos >= fn.pos && ref.end <= fn.end) continue;
|
||||
if (!useRunsAfter(lowerer, decl, ref, memo, inProgress)) {
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
inProgress.delete(fn);
|
||||
}
|
||||
if (inProgress.size === 0) memo.set(fn, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** The value references of `symbol` in `file` other than `name` (its
|
||||
* declaration name): identifiers spelled like it that resolve to it,
|
||||
* outside type positions and export specifiers. */
|
||||
function bindingReferences(
|
||||
lowerer: Lowerer,
|
||||
file: ts.SourceFile,
|
||||
symbol: ts.Symbol,
|
||||
name: ts.Identifier,
|
||||
): ts.Identifier[] {
|
||||
let byName = lowerer.bindingIdentifiers.get(file);
|
||||
if (byName === undefined) {
|
||||
byName = new Map();
|
||||
lowerer.bindingIdentifiers.set(file, byName);
|
||||
}
|
||||
let idents = byName.get(name.text);
|
||||
if (idents === undefined) {
|
||||
const found: ts.Identifier[] = [];
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (ts.isIdentifier(node) && node.text === name.text) found.push(node);
|
||||
return undefined;
|
||||
});
|
||||
lowerer.checker.prefetchSymbolNodesExact(found);
|
||||
byName.set(name.text, found);
|
||||
idents = found;
|
||||
}
|
||||
return idents.filter((ident) => {
|
||||
if (ident === name) return false;
|
||||
const parent = ident.parent;
|
||||
if (parent === undefined || ts.isExportSpecifier(parent) || ts.isExportAssignment(parent))
|
||||
return false;
|
||||
let resolved = lowerer.checker.getSymbolAtLocation(ident);
|
||||
if (ts.isShorthandPropertyAssignment(parent) && parent.name === ident)
|
||||
resolved = lowerer.checker.getShorthandAssignmentValueSymbol(parent) ?? resolved;
|
||||
if (resolved !== symbol) return false;
|
||||
for (let p: ts.Node | undefined = parent; p !== undefined; p = p.parent) {
|
||||
if (ts.isTypeNode(p) && p.kind !== ts.SyntaxKind.ExpressionWithTypeArguments) return false;
|
||||
if (ts.isStatement(p) || ts.isFunctionLike(p)) break;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** The presence flag of a module-scope generic binding that code can use
|
||||
* before its declaration runs (a hoisted function or an earlier closure
|
||||
* called during module evaluation), or undefined when every use runs
|
||||
* after it. The declaration statement sets the flag. */
|
||||
function bindingPresenceOf(
|
||||
lowerer: Lowerer,
|
||||
info: GenericFnInfo,
|
||||
decl: ts.VariableDeclaration,
|
||||
symbol: ts.Symbol,
|
||||
): GenericFnInfo["presence"] {
|
||||
const name = decl.name as ts.Identifier;
|
||||
const early = bindingReferences(lowerer, ts.getSourceFileOfNode(decl), symbol, name).some(
|
||||
(ref) => !runsAfterDeclaration(lowerer, info, ref),
|
||||
);
|
||||
if (!early) return undefined;
|
||||
const flagId = `%g.${info.qualifiedName}%initialized`;
|
||||
lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true });
|
||||
return { flagId, lexical: !isVarDeclared(decl) };
|
||||
}
|
||||
|
||||
/** The statement that records a generic binding's declaration as run,
|
||||
* for a binding with a presence flag. */
|
||||
export function genericBindingPresenceStore(
|
||||
info: GenericFnInfo,
|
||||
decl: ts.VariableDeclaration,
|
||||
): IrStmt | null {
|
||||
if (info.presence === undefined) return null;
|
||||
const loc = locOf(decl);
|
||||
return {
|
||||
kind: "assign",
|
||||
localId: info.presence.flagId,
|
||||
value: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A call of a generic binding where the binding may not be initialized
|
||||
* yet. A `let`/`const` binding in its temporal dead zone throws Node's
|
||||
* ReferenceError before the arguments run; an unassigned `var` is
|
||||
* undefined, so the arguments run and the call throws "is not a
|
||||
* function". Calls that run after the declaration stay direct. */
|
||||
function guardEarlyBindingCall(
|
||||
lowerer: Lowerer,
|
||||
info: GenericFnInfo,
|
||||
expr: ts.CallExpression,
|
||||
call: IrExpr & { kind: "call" },
|
||||
): IrExpr {
|
||||
if (info.bindingDecl === undefined || runsAfterDeclaration(lowerer, info, expr)) return call;
|
||||
const loc = call.loc;
|
||||
const presence = requirePresence(lowerer, info, expr.expression);
|
||||
const flag: IrExpr = { kind: "varRef", localId: presence.flagId, type: BOOL, loc };
|
||||
if (presence.lexical)
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [tdzCheck(flag, info.baseName, loc)],
|
||||
result: call,
|
||||
type: call.type,
|
||||
loc,
|
||||
};
|
||||
// Arguments first, in order, then the check: Node evaluates them
|
||||
// before it finds the callee is not callable.
|
||||
const stmts: IrStmt[] = [];
|
||||
const args = call.args.map((arg): IrExpr => {
|
||||
const id = `%earlyCallArg.${lowerer.ctx.locals.length}`;
|
||||
lowerer.ctx.locals.push({ id, name: "%earlyCallArg", type: arg.type, mutable: false });
|
||||
stmts.push({ kind: "varDecl", localId: id, init: arg, loc });
|
||||
return { kind: "varRef", localId: id, type: arg.type, loc };
|
||||
});
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts,
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: flag,
|
||||
then: { ...call, args },
|
||||
else_: nodeThrowExpr(1, "", `${info.baseName} is not a function`, call.type, loc),
|
||||
type: call.type,
|
||||
loc,
|
||||
},
|
||||
type: call.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A generic binding read as a value where it may not be initialized
|
||||
* yet: a `let`/`const` binding checks its temporal dead zone first. */
|
||||
function guardEarlyBindingValue(
|
||||
lowerer: Lowerer,
|
||||
info: GenericFnInfo,
|
||||
ref: ts.Expression,
|
||||
value: IrExpr,
|
||||
): IrExpr {
|
||||
if (info.bindingDecl === undefined || runsAfterDeclaration(lowerer, info, ref)) return value;
|
||||
const presence = requirePresence(lowerer, info, ref);
|
||||
if (!presence.lexical)
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
ref,
|
||||
`generic 'var' bindings read as values before their declaration runs (Node reads undefined there; declare '${info.baseName}' with const)`,
|
||||
);
|
||||
const loc = value.loc;
|
||||
const flag: IrExpr = { kind: "varRef", localId: presence.flagId, type: BOOL, loc };
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [tdzCheck(flag, info.baseName, loc)],
|
||||
result: value,
|
||||
type: value.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The presence flag a use that can run before the declaration checks.
|
||||
* Only module-scope bindings have one: a binding inside a function would
|
||||
* need a per-call flag that its closures share. */
|
||||
function requirePresence(
|
||||
lowerer: Lowerer,
|
||||
info: GenericFnInfo,
|
||||
blame: ts.Node,
|
||||
): NonNullable<GenericFnInfo["presence"]> {
|
||||
if (info.presence !== undefined) return info.presence;
|
||||
return lowerer.unsupported(
|
||||
"SC1090",
|
||||
blame,
|
||||
`generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding '${info.baseName}' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses)`,
|
||||
);
|
||||
}
|
||||
|
||||
function tdzCheck(flag: IrExpr, name: string, loc: SrcLoc): IrStmt {
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "intrinsic",
|
||||
name: "module.tdzCheck",
|
||||
args: [flag, { kind: "strLit", value: name, type: STRING, loc }],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `const h = id` — a binding ALIASING a generic function (a top-level
|
||||
* declaration, a registered generic binding, or another alias — resolved
|
||||
* left to right in declaration order). The alias registers the SAME info
|
||||
|
||||
@@ -138,6 +138,8 @@ import {
|
||||
bindingGenericFnAliasInfoOf,
|
||||
bindingGenericFnInfoOf,
|
||||
bindingGenericFnNodeOf,
|
||||
genericBindingPresenceStore,
|
||||
genericBindingStoredValue,
|
||||
implicitLocalFnInfoOf,
|
||||
implicitLocalFnNodeOf,
|
||||
implicitMethodCallInfersReturn,
|
||||
@@ -5108,8 +5110,19 @@ export function lowerVarDecl(
|
||||
{
|
||||
const gfnNode = bindingGenericFnNodeOf(decl) ?? bindingContextualGenericFnNodeOf(lowerer, decl);
|
||||
if (gfnNode) {
|
||||
bindingGenericFnInfoOf(lowerer, decl, gfnNode);
|
||||
return null;
|
||||
const info = bindingGenericFnInfoOf(lowerer, decl, gfnNode);
|
||||
// A nested function that lowered before this declaration read the
|
||||
// binding as an ordinary TDZ box (predeclareForwardCapture); the
|
||||
// declaration initializes it, so those reads see the function once
|
||||
// it runs and a ReferenceError before.
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
|
||||
const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined;
|
||||
if (pre) {
|
||||
lowerer.ctx.tdzPredeclared.delete(symbol!);
|
||||
const value = genericBindingStoredValue(lowerer, decl, info);
|
||||
return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(decl) };
|
||||
}
|
||||
return genericBindingPresenceStore(info, decl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1906,6 +1906,9 @@ export class Lowerer {
|
||||
* pinned values resolve through genericFnOf exactly like top-level
|
||||
* generic function declarations (bindingGenericFnInfoOf). */
|
||||
readonly bindingGenericFns = new Map<ts.Node, GenericFnInfo>();
|
||||
/** Identifiers by spelling, per file, for the generic bindings' use
|
||||
* ordering (runsAfterDeclaration). */
|
||||
readonly bindingIdentifiers = new Map<ts.SourceFile, Map<string, ts.Identifier[]>>();
|
||||
/** Per-symbol result of the never-reassigned file scan
|
||||
* (bindingNeverReassigned — object-literal generic-method receivers). */
|
||||
readonly neverReassignedCache = new Map<ts.Symbol, boolean>();
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7180-generic-local-binding-early-uses.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7181-generic-module-binding-tdz.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7182-generic-binding-direct-calls.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Generic arrow bindings declared inside functions, used by code that runs
|
||||
// before or after the declaration: closures created above the declaration
|
||||
// and hoisted function declarations read the binding like any const, so a
|
||||
// use before the declaration throws a ReferenceError and a later one sees
|
||||
// the function.
|
||||
function describe(error: unknown): string {
|
||||
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
||||
}
|
||||
|
||||
function make(): (x: number) => number {
|
||||
const read = (): ((x: number) => number) => id;
|
||||
const id: (x: number) => number = <T>(x: T): T => x;
|
||||
return read();
|
||||
}
|
||||
console.log(make()(7));
|
||||
|
||||
function outer(flag: boolean): number {
|
||||
if (flag) return helper();
|
||||
const id: (x: number) => number = <T>(x: T): T => x;
|
||||
return helper();
|
||||
|
||||
function helper(): number {
|
||||
return id(1);
|
||||
}
|
||||
}
|
||||
console.log(outer(false));
|
||||
try {
|
||||
console.log(outer(true));
|
||||
} catch (error) {
|
||||
console.log(describe(error));
|
||||
}
|
||||
|
||||
function run(): void {
|
||||
const early = (): number => id(2);
|
||||
try {
|
||||
early();
|
||||
} catch (error) {
|
||||
console.log(describe(error));
|
||||
}
|
||||
const id: (x: number) => number = <T>(x: T): T => x;
|
||||
console.log(early());
|
||||
}
|
||||
run();
|
||||
|
||||
function counter(): () => number {
|
||||
let total = 0;
|
||||
const step = (): number => (total = add(total, 1));
|
||||
const add: (a: number, b: number) => number = <T extends number>(a: T, b: T): number => a + b;
|
||||
step();
|
||||
step();
|
||||
return step;
|
||||
}
|
||||
console.log(counter()());
|
||||
@@ -0,0 +1,49 @@
|
||||
// A module-scope generic arrow binding is in its temporal dead zone until
|
||||
// its declaration runs: a hoisted function or an earlier closure that calls
|
||||
// it during module evaluation throws a ReferenceError, and calls made after
|
||||
// the declaration reach the function. A `var` binding is undefined until
|
||||
// then: the call's arguments run, then it throws "is not a function".
|
||||
function describe(error: unknown): string {
|
||||
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
||||
}
|
||||
|
||||
function early(): number {
|
||||
return id(1);
|
||||
}
|
||||
const before = (): string => wrap("b");
|
||||
|
||||
try {
|
||||
console.log(early());
|
||||
} catch (error) {
|
||||
console.log(describe(error));
|
||||
}
|
||||
try {
|
||||
console.log(before());
|
||||
} catch (error) {
|
||||
console.log(describe(error));
|
||||
}
|
||||
|
||||
const id = <T>(x: T): T => x;
|
||||
let wrap = <T>(x: T): string => `[${String(x)}]`;
|
||||
|
||||
console.log(early(), before(), id("direct"), wrap(2));
|
||||
|
||||
function later(): string {
|
||||
return wrap(id(3));
|
||||
}
|
||||
console.log(later());
|
||||
|
||||
function callMissing(): number {
|
||||
return missing(log("argument"));
|
||||
}
|
||||
function log(text: string): number {
|
||||
console.log(text);
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
console.log(callMissing());
|
||||
} catch (error) {
|
||||
console.log(describe(error));
|
||||
}
|
||||
var missing = <T>(x: T): T => x;
|
||||
console.log(callMissing(), missing(5));
|
||||
@@ -0,0 +1,42 @@
|
||||
// Generic arrow bindings whose uses all run after the declaration: later
|
||||
// statements, closures created after it, later declarators, recursion and
|
||||
// function declarations that only later code calls. These calls go
|
||||
// straight to the compiled instances.
|
||||
const pair = <T>(x: T): [T, T] => [x, x];
|
||||
const doubled = pair(2), labels = pair("a");
|
||||
console.log(doubled.join(","), labels.join(","));
|
||||
|
||||
function useLater(): string {
|
||||
return pair(true).join(",");
|
||||
}
|
||||
console.log(useLater());
|
||||
|
||||
export function exported(n: number): number {
|
||||
return pair(n)[0] + 1;
|
||||
}
|
||||
console.log(exported(4));
|
||||
|
||||
function local(): string {
|
||||
var twice = <T extends number>(x: T): number => x * 2;
|
||||
const call = (): number => twice(4);
|
||||
const depth = <T>(xs: T[], n: number): number => (n <= 0 ? xs.length : depth(xs, n - 1) + 1);
|
||||
return `${twice(1)} ${call()} ${depth([1, 2], 3)} ${helper()}`;
|
||||
|
||||
function helper(): string {
|
||||
return pair("h").join("");
|
||||
}
|
||||
}
|
||||
console.log(local());
|
||||
|
||||
function loop(): string[] {
|
||||
const out: string[] = [];
|
||||
const later: (() => string)[] = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const tag = <T>(x: T): string => `#${String(x)}`;
|
||||
later.push(() => tag("late"));
|
||||
out.push(tag(i * 10));
|
||||
}
|
||||
for (const f of later) out.push(f());
|
||||
return out;
|
||||
}
|
||||
console.log(loop().join(" "));
|
||||
@@ -60,3 +60,15 @@ function throughParam(a: Amb): void {
|
||||
console.log(a.m(4));
|
||||
}
|
||||
throughParam(amb);
|
||||
|
||||
// A `var` binding inside a function, declared in a branch and called from
|
||||
// a closure: when the branch does not run, Node finds the binding undefined.
|
||||
// The compiled instances have no per-call flag for that, so the call fences.
|
||||
function branchVar(flag: boolean): number {
|
||||
if (flag) {
|
||||
var picked = <T>(x: T): T => x;
|
||||
}
|
||||
const invoke = (): number => picked(1);
|
||||
return invoke();
|
||||
}
|
||||
console.log(branchVar(true));
|
||||
|
||||
@@ -92,4 +92,11 @@ generic-value-bindings.ts:60:15 - error SC1090: calls of the generic method 'm'
|
||||
59 | function throughParam(a: Amb): void {
|
||||
60 | console.log(a.m(4));
|
||||
| ^~~~~~
|
||||
61 | }
|
||||
61 | }
|
||||
|
||||
generic-value-bindings.ts:71:32 - error SC1090: generic arrow/function-expression bindings declared inside functions that code can call before their declaration runs (the binding 'picked' is a 'var' in a branch or block, or is called from a place that can run first — declare it with const before its uses) are not supported yet
|
||||
|
||||
70 | }
|
||||
71 | const invoke = (): number => picked(1);
|
||||
| ^~~~~~
|
||||
72 | return invoke();
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2542, "san": 2911 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2500, "san": 2870 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2686, "san": 3069 }
|
||||
Reference in New Issue
Block a user