Skip to main content

Phase 6. Closures and higher-order functions

FieldValue
MEPMEP-52 §Phases · Phase 6
StatusNOT STARTED
Startedn/a
Landedn/a
Tracking issuen/a
Tracking PRn/a

Gate

TestPhase6Closures: 30 fixtures green on Node 22, Deno 2, Bun 1.1, Chromium 130. Secondary gate: @typescript-eslint/no-shadow clean; strictFunctionTypes and strictBindCallApply enforced; no emitted Function constructor.

Goal-alignment audit

Closures are how Mochi parameterises behaviour: every map, filter, fold, every event handler, every agent message handler is a closure that captures surrounding scope. The TypeScript surface gives us arrow functions ((x) => x + 1) with lexical this capture, which matches Mochi's closure semantics exactly. The closure-conversion pass shared with MEP-45 through MEP-51 already explicitates the captured environment as a record; the TypeScript emitter does not need to reproduce it. Higher-order functions land here because they are the consumer for closures.

Sub-phases

#ScopeStatusCommit
6.0Anonymous closures: (x: int) -> x + 1 to (x: bigint): bigint => x + 1nNOT STARTEDn/a
6.1Named function declarations at module scope to export function f(...)NOT STARTEDn/a
6.2Nested function declarations to inner const f = (...) => ... (block-scoped const)NOT STARTEDn/a
6.3Higher-order parameters and returns; function-type lowering to (t: T) => R (sync) and (t: T) => Promise<R> (async, Phase 11 colours)NOT STARTEDn/a
6.4Captured-mutable-variable lowering: let mut x = 0; (() -> x = x + 1)() via boxed cell ({ value: bigint }) when capture is shared and mutatedNOT STARTEDn/a

Sub-phase 6.0, Anonymous closures

Decisions made (6.0)

Mochi: (x: int) -> x + 1

TypeScript: (x: bigint): bigint => x + 1n

Arrow vs function: arrow function is preferred. It captures this lexically (matches Mochi semantics; Mochi has no implicit this). It is more concise. eslint (prefer-arrow-callback) prefers it.

Return type annotation: always emitted. tsc --strict infers but the explicit annotation surfaces the IR-derived return type in the source for code review.

Implicit return: for single-expression bodies, the emitter uses the concise form ((x: bigint): bigint => x + 1n). For multi-statement bodies it uses the block form with explicit return.

Sub-phase 6.1, Named function declarations

Decisions made (6.1)

Mochi: fun add(a: int, b: int) -> int { a + b }

TypeScript (module scope):

export function add(a: bigint, b: bigint): bigint {
return a + b;
}

Why function keyword for module-level, arrow for nested: function declarations are hoisted (callable before declaration in the same module). This matches Mochi's "all module functions are simultaneously in scope" semantic. Nested functions inside another function are not hoisted in Mochi (they only exist after their let line); arrow functions assigned to const give exactly that.

export: every module-scope function is exported. Mochi's visibility rules (defaulting to private unless declared pub) are enforced at the type level via TypeScript's module boundary plus per-symbol re-export filtering in src/index.ts.

Sub-phase 6.2, Nested function declarations

Decisions made (6.2)

Mochi: fun outer() { fun inner(x: int) -> int { x + 1 }; print(inner(2)) }

TypeScript:

export function outer(): void {
const inner = (x: bigint): bigint => x + 1n;
print(inner(2n));
}

const (block-scoped) is the right binding form: an inner function is not hoisted to the top of the surrounding function, only to the line of its declaration.

Sub-phase 6.3, Higher-order parameters and returns

Decisions made (6.3)

Function type lowering:

MochiTypeScript
fun(int) -> int(x: bigint) => bigint
fun(int, int) -> int(a: bigint, b: bigint) => bigint
async fun(int) -> int(x: bigint) => Promise<bigint> (Phase 11)
fun(fun(int) -> int) -> int(f: (x: bigint) => bigint) => bigint

Higher-order example:

export function apply(f: (x: bigint) => bigint, x: bigint): bigint {
return f(x);
}

Currying: Mochi does not have language-level auto-currying. A Mochi fun add(a, b) returns a 2-ary function; partial application uses an explicit closure ((b) -> add(5, b)). The TS emitter does not synthesise curried forms.

Variance: TypeScript's strictFunctionTypes makes function parameter positions contravariant. The IR-level variance analysis (shared with MEP-50 Kotlin) feeds this; the TS emitter annotates <in T, out R> modifiers when the IR signals an explicitly-variant type parameter on a generic function.

Sub-phase 6.4, Captured mutable variables

Decisions made (6.4)

Problem: TypeScript's let lets a closure capture a mutable binding lexically:

let x = 0;
const inc = () => { x = x + 1; };
inc();
console.log(x); // 1

This works for single-function capture but breaks when the captured value is held in a long-lived shape (e.g. an agent's state) and the closure-conversion pass demands a boxed cell. The closure-conversion pass (shared with MEP-45 etc.) tags every captured mutable variable as either "shared mutable" (multiple closures mutate; needs a cell) or "exclusive mutable" (one closure mutates, others may read; the let binding suffices).

Shared mutable: lower to a cell record:

type Cell<T> = { value: T };
const x: Cell<bigint> = { value: 0n };
const inc = () => { x.value = x.value + 1n; };
const get = (): bigint => x.value;

Exclusive mutable: stay with let:

let x: bigint = 0n;
const inc = () => { x = x + 1n; };

The IR analysis is the source of truth; the emitter never makes the call independently.

Files (planned)

FilePurpose
transpiler3/typescript/lower/closures.goArrow vs function lowering; nested const; capture rewriting
transpiler3/typescript/lower/funtype.goMochi function type to TS arrow type lowering
transpiler3/typescript/lower/cells.goShared-mutable capture to Cell rewriting
transpiler3/typescript/build/phase06_test.goTestPhase6Closures
tests/transpiler3/typescript/fixtures/phase06-closures/30 fixtures

Test set

  • TestPhase6Closures, 30 fixtures four-runtime.
  • TestPhase6NoFunctionConstructor, asserts no emitted .ts contains new Function(...) or Function(...).
  • TestPhase6Variance, fixtures with contravariant function parameter positions exercise tsc strictFunctionTypes.

Deferred work

  • Function-type variance annotations (<in T, out R>) where the IR derives invariance vs variance. The Phase 6 emitter ships the default invariant form; explicit in/out annotations are added when the IR demands.
  • Tagged template literals (html\...``). Not in Mochi surface.
  • Generators inside closures (function*). Generators land in Phase 7 (query DSL) and Phase 10 (streams).