Rule 8.17
On this page

Rule 8.17

Chapter 8, Functions and entry points Test

A local function (a const that holds an arrow function or a function expression, or a function declaration, written inside a function’s body) is a function of the module named after the body that declares it (surface §14), and it may read and write the variables of the functions around it, as a TypeScript closure does. A name its body reads is looked up as TypeScript looks it up, from the innermost block outward; one that lands on a let, a const or a parameter of a function around it is a capture, which the emitted function takes as a parameter ahead of its own and which every call of it passes: by value while neither it nor a local function it calls writes the variable, and by reference once one does; a const keeps its constant, so a loop it bounds is still counted (Rule 7.5). A whole write to a captured value parameter shares the local copy of Rule 8.8 with the enclosing body and every other closure; its reference never reaches the enclosing caller. this in an arrow function is the object of the method around it, captured the same way, and the class the call names in a static member (Rule 8.13). A write through a capture follows the variable’s own declaration: a let may be written, a const only through what it holds and only when its initializer built the value (Rule 6.10), and a whole-rebound value parameter through its mutable local copy (Rule 8.8). A function declaration may be called anywhere in its block, as TypeScript hoists it, and a local function may be declared in a generic function, once for each of its instances. A call of a local function at a point where a variable it captures is not declared yet must be refused, since TypeScript throws there; so must a function named where a value is read (held in a variable, returned, compared or chosen at run time), there being no function value, other than as an argument of a parameter that takes a function (Rule 8.18). A function named as the callback of a fold (any(xs, isBig), zip(xs, ys, f)) passes what it captures to every call the fold makes.

The rule text and the parts under it are the compiler's own English, as the design document writes them.

Rationale

a function is no value here: nothing can return one, store one or choose one at run time, so every call of a local function is written inside the scope that declares it, where every variable it reads is in scope too. Passing those variables at each call is all a closure’s environment holds, and passing a written one by reference is what makes the write visible to the function around it and to the next call, as it is in TypeScript. A call is where TypeScript would read a variable that is not declared yet, so that is where the refusal stands. Before this rule a local function that read a name from the function around it was refused, with the parameter to add instead.

Derives from

ECMAScript function environment records (a closure reads the binding, not a copy of it), FunctionDeclarationInstantiation (a function declaration is instantiated before its block runs), let and const declarations (a binding may not be read before its declaration is evaluated) and arrow functions (this is the enclosing one); Rule 6.10; Rule 8.8; Rule 8.10; Rule 8.13; surface §14.

How it is verified

Checked by a test. A test, a gate script or a CI workflow names this rule, and the traceability check fails when a file listed below stops naming it.

Where the rule says the compiler enforces it:

TS8022 UNKNOWN_NAME for a call before a captured variable’s declaration ("f" reads "y", which is not declared yet where "f" is called: a let or a const is not there before its declaration, and TypeScript throws. Call "f" after "y" is declared.), TS8099 for a function named as a value ("f" is a function, and a shader has no function values: nothing at run time can hold one, return one or choose between two. Call it where its value is needed, "f(...)".), TS8018 ASSIGN_TARGET and TS8005 CONST_ASSIGN for a write through a parameter or a const; pinned by src/compiler/ts/closures.test.ts, which holds WGSL, GLSL ES 3.00, the CPU oracle, the codegen and the debugger to one value for each form; examples/closures.shade.ts in the compile gate.

The files that verify it at commit 26de7be8, each at the first line that names the rule:

Explained in

The sections of the surface document that explain this rule, at commit 26de7be8:

Error codes that enforce it

The diagnostic codes the rule names under Enforced by, or whose registry text names the rule:

TS8005 CONST_ASSIGN
An assignment to a name that cannot change, such as a const or a read-only resource.
TS8018 ASSIGN_TARGET
An assignment or ++/-- target that is not a writable place, such as a temporary or an input that has no writable value local.
TS8022 UNKNOWN_NAME
Reference to a name TypeShade cannot resolve (identifier, struct field, or struct shape) that is not a function call (UNKNOWN_FN) or a type name (UNKNOWN_TYPE), in a body a call lowers or in one no call lowers; a name the library declares for TypeScript's own use (Object, Math, Symbol), or the file declares as an enum, a namespace, a class or a type, read as a value says what it is.
TS8099 UNSUPPORTED
UNSUPPORTED (TS8099) is the one deliberate exception to "sequential": it is the catch-all for a diagnostic whose site does not yet deserve its own code, so it stays parked past the sequential range instead of at its head.

See also

Source

The rule at commit 26de7be8:

Edit this page Report a problem