Skip to main content

MEP 52. Mochi-to-TypeScript/JavaScript transpiler: TS 5.6 strict source + ES2024 JS dist, AsyncIterableQueue + AbortController agents, npm + tsc canonical, JSR + Deno + Bun + browser secondary, npm Trusted Publishing

FieldValue
MEP52
TitleMochi-to-TypeScript/JavaScript transpiler
AuthorMochi core
StatusDraft
TypeStandards Track
Created2026-05-23 16:50 (GMT+7)
DependsMEP-4 (Type System), MEP-5 (Type Inference), MEP-13 (ADTs and Match), MEP-45 (C transpiler, IR reuse), MEP-46 (BEAM transpiler, IR reuse), MEP-47 (JVM transpiler, IR reuse), MEP-48 (.NET transpiler, IR reuse), MEP-49 (Swift transpiler, IR reuse), MEP-50 (Kotlin transpiler, IR reuse), MEP-51 (Python transpiler, IR reuse)
Research~/notes/Spec/0052/01..12
Tracking/docs/implementation/0052/

Abstract

Mochi today ships vm3 (mochi run); an ahead-of-time C transpiler producing native single-file binaries (MEP-45); an Erlang/BEAM transpiler producing supervised concurrent runtimes (MEP-46); a JVM transpiler that emits Java bytecode directly via ASM and reaches Maven Central and Project Loom (MEP-47); a .NET transpiler that emits C# source and reaches NuGet and NativeAOT (MEP-48); a Swift transpiler that emits Swift 6.0 source and reaches the App Store and the Static Linux SDK (MEP-49); a Kotlin transpiler that emits Kotlin 2.1 source and reaches Google Play and Kotlin Multiplatform (MEP-50); and a Python transpiler that emits CPython 3.12+ source and reaches PyPI, NumPy / pandas / PyTorch / JAX, FastAPI, and Jupyter ipykernel (MEP-51). None of these paths reaches the JavaScript and TypeScript ecosystem: npmjs.org (over 3 million packages as of 2026 Q1, growing roughly 600,000 per year), the React / Vue / Svelte / SolidJS UI tier, the Node.js server tier (Express, Fastify, Hono, Elysia, NestJS), the Deno Deploy and Cloudflare Workers edge tier, the Bun-native server and bundler tier, the Electron and Tauri desktop tier, the Jupyter notebook tier via Deno's official kernel (deno jupyter --install, GA April 2024), and, uniquely, the browser. The browser is the only platform where Mochi can ship to a user without that user installing a runtime first; every other target requires a CPython, JVM, .NET, Swift, or Erlang install on the receiving side. MEP-52 specifies an eighth transpiler pipeline (the ninth backend counting vm3) that emits typed TypeScript 5.6 source under strict mode, drives the produced package.json project through npm 10+ as canonical, runs tsc --build to emit ES2024 ECMAScript modules under dist/{node,deno,bun,browser}/, and publishes to npm with Trusted Publishing (Sigstore + GitHub OIDC, GA April 2024) plus to JSR (jsr.io) for Deno-native consumption.

The pipeline reuses MEP-45's typed-AST and aotir IR, plus the monomorphisation, match-to-decision-tree, closure-conversion, exhaustiveness-check, and sendability-inference passes shared with MEP-46 through MEP-51. It forks at the emit stage: instead of emitting ISO C23 (MEP-45), Core Erlang via cerl (MEP-46), JVM bytecode via ASM (MEP-47), C# source via Roslyn SyntaxFactory (MEP-48), Swift 6.0 source via a Mochi-side syntax tree (MEP-49), Kotlin 2.1 source via KotlinPoet (MEP-50), or Python 3.12 source via stdlib ast.unparse (MEP-51), it emits TypeScript 5.6 source text by lowering aotir to a Mochi-side TypeScript syntax tree (the same tree-printer approach used by MEP-49 and MEP-50; no dependency on the TypeScript Compiler API, which would pull tsc into the Go build chain). The Mochi-built tree is serialised to .ts text; prettier 3.x runs as a layout normaliser; eslint --fix runs for import sorting and --strict lint passes. Five packaging targets ship together: --target=typescript-source emits .ts plus package.json plus the tsconfig chain without invoking tsc (for IDE-driven library authors and downstream bundlers); --target=npm-package runs the full tsc --build and emits a publishable npm package with dist/{node,deno,bun,browser}/*.js + *.d.ts plus a sourcemap chain; --target=deno-jsr emits the same .ts source plus a jsr.json (the Deno-native registry's manifest) and invokes deno publish --dry-run for validation; --target=browser-bundle runs esbuild on the emitted source to produce a single tree-shaken ESM file plus an importmap; --target=deno-jupyter produces a Deno kernelspec under ~/.local/share/jupyter/kernels/mochi-deno-<pkg>/ that the Deno Jupyter kernel (shipped since Deno 1.37, April 2024) executes per cell. One codegen pass feeds all five targets.

The master correctness gate is byte-equal stdout from the produced npm package (installed via npm pack plus npm install <tarball> into a fresh directory and run with node dist/node/index.js), from the Deno path (deno run dist/deno/index.js), from the Bun path (bun dist/bun/index.js), and from the browser path (Playwright + Chromium 130+ executing the esbuild bundle with console.log captured), versus vm3 on the entire fixture corpus across Node 22.11.0 LTS, Deno 2.0.0, Bun 1.1.30, and Chromium 130 on x86_64-linux-gnu, aarch64-linux-gnu, aarch64-darwin, and x86_64-windows. vm3 is the recording oracle for expect.txt; the transpiler does not link against or depend on vm3. Three static-analysis gates run alongside: tsc --noEmit --strict --noUncheckedIndexedAccess --exactOptionalPropertyTypes --noImplicitOverride --noFallthroughCasesInSwitch --noPropertyAccessFromIndexSignature (zero diagnostics on every emitted source file), eslint --max-warnings 0 with the @typescript-eslint/recommended-type-checked config plus the @typescript-eslint/no-explicit-any rule pinned to error, and prettier --check fixed-point (running prettier twice produces no diff). Lowering choices are constrained to the intersection of "what tsc accepts under all of the strict flags" and "what eslint's typed lint surface accepts without warnings". This narrows the emitter, but the result is TypeScript that any production codebase reviewer would accept as fully-typed application code.

Five load-bearing decisions:

  1. TypeScript 5.6 floor + ECMAScript 2024 target. TypeScript 5.6 (released September 2024) brings iterator helpers (Iterator.from, Iterator.prototype.map/filter/take), --noUncheckedSideEffectImports, --rewriteRelativeImportExtensions (so .ts source can import ./foo.ts and tsc rewrites to ./foo.js in dist/), and regions-aware narrowing for using declarations (Symbol.dispose, TC39 Stage 4). ES2024 target gives Promise.withResolvers (load-bearing for the agent's call(req) reply future), Object.groupBy / Map.groupBy, the new Set methods (intersection, union, difference, symmetricDifference, isSubsetOf, isSupersetOf, isDisjointFrom), the v regex flag, and Atomics.waitAsync. ECMA TC39 Stage-4 by 2026 also includes the Iterator constructor, Promise.try, decorators (Stage 4 since 2023), explicit-resource-management (using, await using), and array grouping. ES2025 is rejected as a v1 floor (only a subset of the four runtimes ship every Stage-4 feature at parity by Q1 2026); Node 20 LTS is below the floor because it lacks Promise.withResolvers natively (the agent shape requires it). See 02-design-philosophy §3, 06-type-lowering §1.

  2. tsc --strict AND --noUncheckedIndexedAccess AND --exactOptionalPropertyTypes as compile gates. All three flags are non-negotiable. --strict is itself a meta-flag enabling strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitAny, noImplicitThis, useUnknownInCatchVariables, and alwaysStrict. --noUncheckedIndexedAccess makes arr[i] return T | undefined, forcing Mochi's bounds-checked array semantics to surface in the type system (the codegen emits a runtime check or a non-null assertion driven by IR provenance). --exactOptionalPropertyTypes prevents T | undefined from masquerading as T? (an absent property and a property set to undefined are not the same; Mochi's Option lowers to T | null, never T | undefined). --noImplicitOverride, --noFallthroughCasesInSwitch, and --noPropertyAccessFromIndexSignature round out the gate. Eslint runs @typescript-eslint/recommended-type-checked (the type-aware ruleset) plus no-floating-promises, no-misused-promises, await-thenable, no-unnecessary-condition, and consistent-type-imports. The emitter is constrained to the intersection of "what all of these accept". See 02-design-philosophy §4, 06-type-lowering §2, 11-testing-gates §2 to §4.

  3. AsyncIterableQueue + AbortController, not RxJS, not Web Streams as mailbox. A hand-rolled queue class wraps a T[] buffer plus a Promise<IteratorResult<T>> waker queue and exposes [Symbol.asyncIterator]() to drain messages until closed. AbortController provides cooperative cancellation: the parent scope's AbortSignal is forked to each child agent constructor; on parent abort all children observe signal.aborted === true, exit their for await loops, and release resources. Promise.withResolvers() (ES2024) gives the call(req) reply future. The shape mirrors asyncio.Queue + TaskGroup (MEP-51), Channel + SupervisorJob (MEP-50), AsyncStream + actor (MEP-49), and Task + Channel (MEP-48). RxJS is rejected because it adds a 35KB minified dep to every Mochi user and its Subject abstraction is push-only with no native backpressure. Web Streams (ReadableStream<T>) is rejected as a mailbox because its ReadableByteStreamController semantics and the controller/queue split are heavier than the agent needs; Web Streams remain in scope only for the --target=stream-pipe future surface (interop with fetch().body and DOM streams). See 02-design-philosophy §5, 09-agent-streams §1 to §14.

  4. Reuse MEP-45's aotir IR plus all shared passes. The IR is target-agnostic; monomorphisation, match-to-decision-tree, closure-conversion, exhaustiveness checking, and sendability inference all run once and feed eight backends (counting vm3). The fork is at the emit pass: transpiler3/typescript/lower/ lowers aotir to a Mochi-built TypeScript syntax tree (the same approach as MEP-49's Swift-side syntax tree and MEP-50's KotlinPoet-shaped tree); transpiler3/typescript/emit/ runs the tree printer to produce .ts text plus package.json plus the tsconfig chain, then invokes prettier 3.x for layout normalisation and eslint --fix --rule 'import/order: error' for import sorting. Sharing the IR keeps the eight targets semantically aligned and amortises pass-implementation work. See 05-codegen-design §5.

  5. npm 10+ as canonical, but four-runtime conditional exports. package.json is the canonical project manifest ("type": "module", "engines" pinning Node >=22.0.0 plus Bun >=1.1.0 plus Deno >=2.0.0, "exports" conditional map with "node", "deno", "bun", "browser", "types", and "default" keys). tsc --build is the type-checker and emitter; no Babel, no Rollup as primary, no Webpack. npm publish --provenance uses Sigstore plus GitHub OIDC (GA April 2024) to attach a cryptographic provenance statement to every published tarball. pnpm and Bun are documented as alt install drivers but are not gated; the package-lock.json is the canonical lockfile (Bun and pnpm read it). For Deno consumption two paths exist: Deno can consume the npm package via the npm: specifier (import {x} from "npm:@mochi/[email protected]"), or the same TypeScript source can be published to JSR (jsr.io, Deno's native registry, GA September 2024) via deno publish. Browsers receive an esbuild-bundled single-file ESM with Node-only imports (node:fs, node:net, node:path) tree-shaken out under the "browser" export condition. See 10-build-system §1 to §9, 02-design-philosophy §8.

The gate for each delivery phase is empirical: every Mochi source file in tests/transpiler3/typescript/fixtures/ must compile via the TypeScript pipeline and produce stdout that diffs clean against the expect.txt recorded by vm3, on all four runtimes. tsc --strict --noUncheckedIndexedAccess --exactOptionalPropertyTypes on generated code is the secondary gate. eslint --max-warnings 0 is the tertiary gate. prettier --check fixed-point is the quaternary gate. Four-runtime execution (Node 22, Deno 2, Bun 1.1, headless Chromium 130 via Playwright) is the quinary gate. npm pack plus install plus execute is the senary gate. Reproducibility (bit-identical .tgz SHA256 across two CI hosts with SOURCE_DATE_EPOCH pinned and npm pack outputs sorted tarball entries) is the septenary gate. JSR dry-run (deno publish --dry-run against a verdaccio-style JSR mirror) is the octonary gate. npm publish --dry-run --provenance against a verdaccio local registry is the nonary gate.

Motivation

Mochi today targets vm3 (mochi run), MEP-45 (C, single-binary AOT), MEP-46 (BEAM, supervision and hot reload), MEP-47 (JVM, Maven Central via direct bytecode and Loom), MEP-48 (.NET, NuGet and NativeAOT), MEP-49 (Swift, App Store and Static Linux SDK), MEP-50 (Kotlin, Google Play and Kotlin Multiplatform), and MEP-51 (Python, PyPI and Jupyter ipykernel). None deliver JavaScript or TypeScript:

  1. npm is the largest package ecosystem. npmjs.org hosts over 3 million packages as of 2026 Q1, growing roughly 600,000 per year. By package count it is roughly 5x PyPI (MEP-51, ~600k), 30x Maven Central (MEP-47, ~100k), and orders of magnitude larger than NuGet (MEP-48), Hex (MEP-46), Swift Package Index (MEP-49), and Maven for Kotlin (MEP-50). A Mochi user who needs Stripe SDK, OpenAI SDK, Discord SDK, Slack SDK, Twilio SDK, Apollo GraphQL client, drizzle-orm, prisma, zod, valibot, lodash, date-fns, or any of the tens of thousands of small composable utility libraries is locked out unless Mochi reaches npm. MEP-52 produces idiomatic typed TypeScript indistinguishable from hand-written code after prettier, ready to import any npm package directly.

  2. React, Vue, Svelte, SolidJS: the JavaScript UI tier. React 19 (December 2024) ships server components, the new compiler, and use() for promises. Vue 3.5 (September 2024), Svelte 5 (October 2024, with runes), and SolidJS 1.9 (2024) dominate the modern reactive-UI tier. No other Mochi target reaches a UI rendering on a web canvas. A Mochi data model lowered to a class with readonly fields plus a Symbol.observable adapter slots cleanly into any of these frameworks; the runtime ships small @mochi/runtime/react, @mochi/runtime/vue, @mochi/runtime/svelte, and @mochi/runtime/solid sub-paths so the user picks their framework via subpath import without bundling all four.

  3. Node.js is the dominant server-side JavaScript runtime. Node 22 LTS (released April 2024) ships node:test builtin testing, stabilised node:fs/promises glob, native .env loading via --env-file, native fetch (WHATWG-compliant; stable since Node 18), the Permission Model, Web Crypto API parity, and the V8 12.4 engine with ES2024 features. Express remains the dominant minimalist framework; Fastify is the high-performance default; Hono (released 2022, ~50KB) is the runtime-portable framework that runs identically on Node, Deno, Bun, Cloudflare Workers, and Fastly Compute; Elysia (released 2023) is the Bun-native typed-handler framework; NestJS is the dominant opinionated framework. Mochi-emitted handlers slot into any of these.

  4. Deno 2.x is the standards-aligned, secure-by-default runtime. Deno 2.0 (October 2024) ships full Node compatibility (npm:, node:, package.json), the JSR registry as a first-class publish target, FFI, KV (Deno KV is a first-party key-value store), and the Deno Deploy edge runtime. Deno's standards alignment (Web APIs first, then Node APIs) makes it the reference target for "would this run in a browser without modification". Deno's Jupyter kernel (deno jupyter --install, GA April 2024) gives Mochi the second notebook path after MEP-51's ipykernel. JSR (jsr.io, GA September 2024) is the Deno-native registry; it accepts TypeScript source directly, transpiles on the server, generates the type declarations automatically, and supports semver scopes.

  5. Bun 1.1.x is the all-in-one toolkit. Bun (1.0 September 2023, 1.1 February 2024) bundles a runtime, package manager (replaces npm install with bun install, 10-25x faster), bundler (bun build), test runner (bun test), and transpiler. Bun's FFI is a first-class native-function API; bun:sqlite is a built-in SQLite binding; Bun.serve is the canonical HTTP server. By Q1 2026 Bun is at production scale for both standalone applications and as a CI/CD driver.

  6. Cloudflare Workers, Vercel Edge, Deno Deploy: the edge tier. Cloudflare Workers (V8 isolates, ~5ms cold start), Vercel Edge Functions, Deno Deploy, Fastly Compute@Edge, and Netlify Edge are the modern edge-compute platforms. All accept ESM TypeScript or JavaScript directly. None accept a Python wheel, a JVM bytecode JAR, a .NET assembly, a Swift binary, or a Kotlin/JS-IR module (Kotlin/JS exists but is a fraction of the JS-runtime market). MEP-52 is the edge story for Mochi.

  7. The browser: the only zero-install platform Mochi can reach. Every other MEP-45 through MEP-51 target requires the receiver to install a runtime: CPython for MEP-51, JVM for MEP-47, .NET for MEP-48, Swift for MEP-49, Erlang/OTP for MEP-46, or the C binary itself for MEP-45. The browser is universal. A user opens a URL and the Mochi code runs. This is unique. MEP-52 with --target=browser-bundle produces a single tree-shaken ESM file plus an importmap; drop it in an HTML <script type="module" src="..."> tag and the Mochi program runs.

  8. Electron and Tauri: cross-platform desktop. Electron (Atom-shell since 2013) and Tauri (Rust + web view, 2.0 October 2024) are the dominant cross-platform desktop frameworks. Both consume web technologies for the UI and a JavaScript runtime for the renderer. Tauri additionally allows Rust on the backend. Mochi-emitted TypeScript slots into both.

  9. Jupyter notebooks via Deno's official kernel. deno jupyter --install (GA April 2024) registers Deno as a Jupyter kernel; users select "Deno" in the kernelspec menu and write TypeScript cells. MEP-52's --target=deno-jupyter produces a Mochi-wrapper kernelspec that transpiles each cell to TypeScript on receipt and forwards to Deno's kernel. This is the second notebook path; MEP-51's ipykernel is the first.

  10. TypeScript is now the default for new JavaScript projects. TypeScript 5.6 (September 2024) is the reference; State of JS 2024 reports approximately 80% of new projects start in TypeScript. React, Vue, Svelte, SolidJS, Next.js, Remix, Astro, Vite, esbuild, Rollup, and Webpack all support TypeScript natively. Definitely Typed hosts type declarations for almost every legacy JavaScript-only npm package. Mochi-emitted TypeScript lands as fully-typed production code.

The C target (MEP-45) remains the right choice for embedded targets, single-file distribution, and minimal runtime footprint. The BEAM target (MEP-46) remains the right choice for hot-reload services, distributed pubsub, and OTP supervision. The JVM target (MEP-47) remains the right choice for Maven Central, Loom concurrency, and direct-bytecode performance. The .NET target (MEP-48) remains the right choice for NuGet and NativeAOT. The Swift target (MEP-49) remains the right choice for Apple platforms. The Kotlin target (MEP-50) remains the right choice for Android and KMP. The Python target (MEP-51) remains the right choice for data science, ML, Jupyter ipykernel, and PyPI. The TypeScript/JavaScript target (MEP-52) is the right choice for npm, the browser, Node and Deno and Bun servers, the edge tier, Electron and Tauri desktop, the React/Vue/Svelte/SolidJS UI tier, and the Deno Jupyter kernel. All eight ship; the user picks per workload.

Specification

This section is normative. Sub-notes under ~/notes/Spec/0052/01..12 are informative.

1. Pipeline overview

MEP-52 shares the front-end and aotir passes with MEP-45 through MEP-51 and forks at the emit stage:

Mochi source
| parser (MEP-1/2/3, reused)
v
typed AST
| type checker (MEP-4/5, reused)
v
aotir IR
| monomorphisation pass (shared)
| closure conversion pass (shared)
| match decision tree (shared)
| exhaustiveness check (shared)
| sendability inference (shared)
v
TypeScript AST codegen pass (transpiler3/typescript/lower/)
| build Mochi-side TS syntax tree
| emit `import` statements (ESM, no CJS)
| emit `class` / `type` / `interface` / `const` / `function` nodes
| emit per-runtime conditional code under build flags
v
TS tree printer (transpiler3/typescript/emit/) -> .ts source text
| prettier 3.x (deterministic layout)
| eslint --fix --rule 'import/order: error' (import sort)
v
.ts files under src/generated/
| emit package.json at project root
| emit tsconfig.base.json plus per-runtime tsconfig.{node,deno,bun,browser}.json
| emit src/index.ts (public entry)
v
tsc --build -> dist/{node,deno,bun,browser}/*.js + *.d.ts + *.js.map
| esbuild for --target=browser-bundle (single file ESM)
| npm pack -> <pkg>-<ver>.tgz
| npm install dist tarball -> fresh dir
| node / deno / bun / chromium -> stdout captured for vm3 differential
| npm publish --provenance (Sigstore + GitHub OIDC)
v
npm + JSR release

Both mochi run (vm3) and mochi build (transpiler3) share the same parser, type system, and IR.

2. Output: TypeScript source plus JavaScript dist

The canonical source is .ts under TypeScript 5.6 strict mode. The canonical dist is ES2024 .js plus .d.ts plus .js.map. Both ship.

  • --target=typescript-source: emits .ts source plus package.json plus the tsconfig chain plus the src/ layout, without invoking tsc. The user (or downstream tooling like Vite, Next.js, or a custom bundler) is expected to run tsc or to consume the .ts directly through esbuild's --loader=ts or Bun's native TypeScript support. This target is the lowest-friction path for library authors who want to ship .ts and let their consumers compile.
  • --target=npm-package: emits the same .ts source plus runs tsc --build to produce dist/{node,deno,bun,browser}/*.js + *.d.ts + *.js.map, then runs npm pack to produce <pkg>-<ver>.tgz ready for npm publish. This target is the canonical library-distribution path.

Both targets use the same src/generated/ layout. The package.json exports field carries the conditional map:

{
"exports": {
".": {
"deno": "./dist/deno/index.js",
"bun": "./dist/bun/index.js",
"browser": "./dist/browser/index.js",
"node": "./dist/node/index.js",
"types": "./dist/index.d.ts",
"default": "./dist/node/index.js"
}
}
}

The types condition surfaces a single .d.ts per public entry; per-runtime declaration files are not necessary because the public API is identical across runtimes (runtime-specific code is selected at runtime, not at type-check time).

3. Runtime matrix: Node 22 + Deno 2 + Bun 1.1 + browser

Four tier-1 runtimes:

  • Node.js 22 LTS (released April 2024, active LTS through 2027). The primary runtime. package.json declares "engines": { "node": ">=22.0.0" }. Node 22 ships node:test, stable fetch, --env-file, Web Crypto API parity, and V8 12.4.
  • Deno 2.x (released October 2024). Pure ESM, npm compat (npm: specifier), JSR support, FFI, and the official Jupyter kernel. The runtime can also consume the same .ts source directly (no compilation step needed when running .ts files through Deno).
  • Bun 1.1.x (released February 2024, current 1.1.30+ as of Q1 2026). Native TypeScript support, bun:ffi, bun:sqlite, Bun.serve. The all-in-one toolkit.
  • Browser: ES2024-compliant browsers (Chromium 130+, Firefox 130+, Safari 18+). Distributed via the esbuild bundle target. Node-only imports (node:fs, node:net, node:path) are stripped by the "browser" export condition and replaced with browser-compatible stubs from @mochi/runtime/browser.

Node-only APIs (node:fs, node:net, node:path, node:child_process, node:worker_threads) are isolated under @mochi/runtime/io/node. Deno-only APIs (Deno.openKv, Deno.serve) under @mochi/runtime/io/deno. Bun-only APIs (Bun.serve, bun:sqlite) under @mochi/runtime/io/bun. Browser-only DOM under @mochi/runtime/io/browser. The conditional exports route to the right subpath per runtime. Tree-shaking removes unused branches at build time.

4. Type lowering

Per 06-type-lowering:

MochiTypeScript
intbigint (arbitrary precision, default) OR number (when fits in i53, chosen per IR type by monomorphisation)
floatnumber (IEEE 754 double)
boolboolean
stringstring (UTF-16 internal; len(s) returns code points via runtime helper)
bytesUint8Array
list<T>readonly T[] for immutable view; T[] when mutated
map<K, V>Map<K, V> (insertion order guaranteed by ES spec; Object literals not used as maps)
set<T>Set<T> (insertion order guaranteed; ES2024 set methods available)
recordclass with readonly fields plus private constructor plus static of() factory
sum typediscriminated union: type Foo = A | B | C over a literal kind tag; exhaustive switch
T? (option)T | null (NOT T | undefined; exactOptionalPropertyTypes keeps the two distinct)
Result<T, E>MochiResult<T, E> (alias) over {kind: 'ok', value: T} | {kind: 'err', error: E}
fun(T) -> R(t: T) => R
async fun(T) -> R(t: T) => Promise<R>
agentclass wrapping AsyncIterableQueue<Message> plus an AbortController
streamAsyncIterable<T> (often AsyncGenerator<T, void, undefined>)
timeTemporal.ZonedDateTime (Temporal polyfill until native ships; see Q4)
durationTemporal.Duration

int: defaults to bigint for safety; monomorphisation specialises to number when the IR proves the value fits in [-(2^53-1), 2^53-1] and the producer never overflows. bigint literal: 42n; number literal: 42. Mixed bigint + number is a TS error; the emitter never mixes.

String length: Mochi len(s) returns code points, not UTF-16 code units. String.prototype.length returns code units, so the emitter emits mochiStrLen(s) (a runtime helper that iterates the string via [...s].length or via a code-point counter). Indexing follows the same rule: s[i] in Mochi is mochiStrIndex(s, i) in emitted code.

Generics: TypeScript generics (function f<T>(x: T): T, class Box<T> { ... }, type Vec<T> = T[]) cover the surface. Variance is annotated explicitly via in and out modifiers (TS 4.7+, stable in 5.6) when the IR provenance demands it.

5. Concurrency: AsyncIterableQueue + AbortController

Per 09-agent-streams §1 to §14:

class AsyncIterableQueue<T> {
private buffer: T[] = [];
private waiters: Array<(v: IteratorResult<T>) => void> = [];
private closed = false;

push(value: T): void {
const waiter = this.waiters.shift();
if (waiter) waiter({ value, done: false });
else this.buffer.push(value);
}

close(): void {
this.closed = true;
for (const w of this.waiters) w({ value: undefined, done: true });
this.waiters = [];
}

[Symbol.asyncIterator](): AsyncIterator<T> {
return {
next: (): Promise<IteratorResult<T>> => {
if (this.buffer.length > 0) {
return Promise.resolve({ value: this.buffer.shift()!, done: false });
}
if (this.closed) return Promise.resolve({ value: undefined, done: true });
const { promise, resolve } = Promise.withResolvers<IteratorResult<T>>();
this.waiters.push(resolve);
return promise;
},
};
}
}

class CounterAgent {
private mailbox = new AsyncIterableQueue<Message>();
private state = 0;

constructor(private signal: AbortSignal) {
void this.loop();
signal.addEventListener("abort", () => this.mailbox.close());
}

cast(msg: Message): void { this.mailbox.push(msg); }

async call(req: Omit<Message, "reply">): Promise<Reply> {
const { promise, resolve } = Promise.withResolvers<Reply>();
this.mailbox.push({ ...req, reply: resolve } as Message);
return promise;
}

private async loop(): Promise<void> {
for await (const msg of this.mailbox) {
if (this.signal.aborted) break;
this.handle(msg);
}
}

private handle(msg: Message): void { /* ... */ }
}

Supervision: a parent AbortController whose AbortSignal is passed to every child agent constructor. On any child failure the parent calls controller.abort(); all siblings observe signal.aborted === true, exit their for await loops, and release resources. This matches OTP's one_for_all. one_for_one (per-child restart without sibling cancellation) is custom: wrap the loop in try / catch that restarts the agent on caught errors.

6. Errors: MochiResult (Ok/Err discriminated union)

Per 06-type-lowering §7, 02-design-philosophy §11:

export type MochiResult<T, E> =
| { readonly kind: "ok"; readonly value: T }
| { readonly kind: "err"; readonly error: E };

export const Ok = <T>(value: T): MochiResult<T, never> => ({ kind: "ok", value });
export const Err = <E>(error: E): MochiResult<never, E> => ({ kind: "err", error });
  • Mochi Result<T, E> to MochiResult<T, E>. A throwing Mochi function fun parse() -> AST throws ParseError lowers to TS function parse(): MochiResult<AST, ParseError> (no exceptions; thrown errors are reserved for panic and for FFI boundary crossings).
  • Mochi panic lowers to throw new MochiPanic(msg) where MochiPanic is a runtime class MochiPanic extends Error. The browser, Node, Deno, and Bun all treat unhandled MochiPanic as a hard failure (terminate the process, log the stack).
  • Multi-failure aggregation: when sibling agents fail concurrently and the parent aborts, the parent collects child errors via AggregateError (ES2021+, available on all four runtimes) and surfaces a MochiResult.Err carrying the array of inner errors. This matches MEP-51's ExceptionGroup story.

7. Module system: ESM only, conditional exports

Per 10-build-system §3:

  • All emitted modules use ESM (import, export, export default). No CommonJS (require, module.exports) is emitted, ever. No UMD. No AMD. No SystemJS.
  • package.json declares "type": "module". All .js files in dist/ are treated as ESM.
  • import paths use the .ts extension in source; tsc --rewriteRelativeImportExtensions (TS 5.6) rewrites to .js in dist. Relative imports use ./foo.ts; cross-package imports use the bare specifier (@mochi/runtime).
  • Conditional exports route per runtime via the package.json "exports" field (§2).
  • Each emitted .ts file is declaration-emit-safe (no implicit any, no unknown leaking unannotated). tsc --declaration produces .d.ts per module.
  • No barrel files larger than ~50 re-exports (avoids import cost regressions per Vite, esbuild, and Next.js perf guidance).

8. Runtime library @mochi/runtime

Per 04-runtime:

  • Package name: @mochi/runtime on npm, @mochi/runtime on JSR (same scope).
  • License: dual MIT plus Apache-2.0 (matches Rust's std, broadest acceptance in JS-ecosystem permissive-license culture).
  • Size: ~6500 LOC of TypeScript across approximately 35 modules. Zero npm dependencies in the default build.
  • Sub-paths: @mochi/runtime/queue (AsyncIterableQueue), @mochi/runtime/result (MochiResult), @mochi/runtime/string (mochiStrLen, mochiStrIndex), @mochi/runtime/datalog (datalog engine, ~700 LOC), @mochi/runtime/llm (LLM provider dispatch), @mochi/runtime/io/node (Node-only IO), @mochi/runtime/io/deno (Deno-only IO), @mochi/runtime/io/bun (Bun-only IO), @mochi/runtime/io/browser (browser IO stubs), @mochi/runtime/ffi (FFI dispatch across Node N-API, Bun FFI, Deno FFI), @mochi/runtime/temporal (Temporal polyfill wrapper).
  • Optional native bindings: @mochi/runtime-native-node (Node N-API addon), @mochi/runtime-native-bun (Bun FFI), @mochi/runtime-native-deno (Deno FFI). Each is a separate npm package the user opts into when native acceleration is needed.

9. Build: package.json, tsconfig chain, tsc --build

Per 10-build-system §1 to §9:

  • package.json declares "type": "module", "engines", "exports", "main" (fallback for CommonJS-only consumers, points at the Node ESM build), "types", "files" (lists dist/, README.md, LICENSE), "scripts" (build, test, lint, format, pack, publish), and "keywords".
  • tsconfig.base.json extends @tsconfig/strictest style: "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitOverride": true, "noFallthroughCasesInSwitch": true, "noPropertyAccessFromIndexSignature": true, "isolatedModules": true, "verbatimModuleSyntax": true, "target": "es2024", "module": "esnext", "moduleResolution": "bundler", "declaration": true, "declarationMap": true, "sourceMap": true, "rewriteRelativeImportExtensions": true.
  • Per-runtime tsconfig.node.json, tsconfig.deno.json, tsconfig.bun.json, tsconfig.browser.json extend the base. Each sets "outDir" to dist/<runtime>/ and "lib" to the runtime-appropriate set (["es2024", "dom"] for browser; ["es2024"] for Node, Deno, Bun).
  • Root tsconfig.json is a project-references composite: "references": [{"path": "./tsconfig.node.json"}, {"path": "./tsconfig.deno.json"}, ...]. tsc --build walks the chain.
  • npm run build -> tsc --build -> dist/{node,deno,bun,browser}/*.js + *.d.ts + *.js.map.
  • For the browser bundle: mochi build --target=browser-bundle invokes esbuild with --bundle --format=esm --target=es2024 --platform=browser --tree-shaking=true --minify on the dist/browser/ entry; output is dist/bundle/index.js (a single ESM file).

10. Reproducibility

Per 10-build-system §7, 11-testing-gates §7:

  • SOURCE_DATE_EPOCH environment variable pinned (e.g., to commit timestamp). npm pack honours it for all timestamp fields in the tarball.
  • npm pack output is sorted: all tarball entries written in lexicographic byte order. The tarball mtime, uid, gid, and permission bits are normalised. npm pack since npm 9.5 supports --pack-destination and reproducible output natively under SOURCE_DATE_EPOCH.
  • tsc output is deterministic provided source is deterministic; the emitter guarantees deterministic source by sorting imports, sorting object keys where order is not semantic, and emitting a stable line-ending normalisation.
  • Build gate: byte-identical .tgz SHA256 across two CI hosts running with the same SOURCE_DATE_EPOCH, same Node version, same npm version, same source tree.
  • The same reproducibility applies to JSR: deno publish honours SOURCE_DATE_EPOCH for the JSR tarball.

11. Publishing: npm Trusted Publishing + JSR

Per 10-build-system §8 to §9:

  • npm Trusted Publishing: npm publish --provenance (since npm 9.5, GA April 2024 with Sigstore + GitHub OIDC). The workflow runs in GitHub Actions with id-token: write permission; the OIDC token is exchanged with npm for a publishing credential; Sigstore signs the provenance statement; npm registry attaches the statement to the published tarball. No long-lived API tokens stored in CI.
  • The provenance statement carries: repository URL, commit SHA, workflow file, runner identity, build invocation, and the SHA256 of the published tarball. Consumers verify via npm audit signatures.
  • JSR Publishing: deno publish against jsr.io. JSR accepts TypeScript source directly; it transpiles on the server and generates .d.ts automatically. JSR also accepts OIDC tokens from GitHub Actions; no long-lived API tokens. jsr.json is the manifest ({"name": "@mochi/foo", "version": "1.0.0", "exports": "./mod.ts"}).
  • Both publishes run in the same release workflow; semver versions are kept in sync via a single source-of-truth in package.json's version field.

12. Identifier mangling

Per 01-language-surface §8:

  • TypeScript reserved words (class, function, import, export, new, delete, void, typeof, instanceof, if, else, for, while, do, switch, case, default, break, continue, return, throw, try, catch, finally, var, let, const, null, true, false, this, super, extends, implements, interface, enum, async, await, yield, static, public, private, protected, readonly, abstract, as, is, from, of, in, type, namespace, module, declare, package, with) trailing-underscored on emit when they collide with Mochi identifiers (class_, import_).
  • JavaScript globals (Object, Array, Function, Promise, Map, Set, Symbol, Error, console, globalThis) are not reserved by TS but the emitter trailing-underscores any Mochi identifier matching them to avoid the per-line confusion in user-facing IDEs.
  • The original Mochi name is preserved as a JSDoc @mochiName annotation on the emitted declaration for round-tripping by Mochi tooling.

13. FFI: Node N-API + Bun FFI + Deno FFI dispatch

Per 01-language-surface §10, 06-type-lowering §16:

  • Mochi extern fun foo(...) -> ... from a C library lowers to a runtime-dispatched FFI call. The runtime detects the current runtime (globalThis.process?.versions?.node for Node, globalThis.Deno for Deno, globalThis.Bun for Bun) and routes to:
    • Node: node-api (formerly N-API), via a pre-compiled .node addon under @mochi/runtime-native-node.
    • Bun: bun:ffi (dlopen, CFunction, JSCallback).
    • Deno: Deno.dlopen(path, symbols).
    • Browser: FFI is not supported; the emitter rejects FFI declarations under --target=browser-bundle.
  • The shared FFI declaration emits a single typed wrapper that surfaces the right backend at runtime.
  • Pure-TypeScript FFI (Mochi extern fun foo(...) -> ... from an npm package): lowers to a direct import { foo } from "<pkg>" plus a typed wrapper. Type stubs from DefinitelyTyped or shipped with the package supply the declarations.

14. fetch / LLM

Per 01-language-surface §11 to §12:

  • Mochi fetch(...) lowers to the platform built-in fetch (WHATWG; available on Node 18+, Deno, Bun, and the browser). No node-fetch, no axios, no got dep. Streaming responses use the ReadableStream API directly.
  • Mochi llm.generate(...) lowers to a call into mochi_runtime.llm.dispatch which routes to a provider-pluggable backend (OpenAI, Anthropic, Mistral, Cohere, Google Gemini, local llama.cpp via HTTP) via the provider-dispatch table in 04-runtime §14. The dispatch table reads provider-specific API keys from process.env (Node, Bun), Deno.env.get (Deno), or rejects the call at codegen for browser builds (no API keys in browser source).
  • TLS verification: on by default. fetch's default behaviour is on; the emitter does not surface a verify=false opt-out (Mochi has no language flag for it).
  • HTTP/2 and HTTP/3: Node 22's undici-based fetch supports HTTP/2 (HTTP/3 is opt-in). Deno and Bun support HTTP/2 natively.

15. Source maps

Per 10-build-system §10:

  • tsc emits .js.map per .js (declared in tsconfig.base.json: "sourceMap": true, "declarationMap": true).
  • Source maps point back to the emitted .ts source, not to the Mochi source. A second mapping layer (Mochi to TS) is the v2 candidate; v1 ships the TS-to-JS layer only.
  • esbuild for --target=browser-bundle produces a combined source map that maps through all intermediate .ts files to the final bundle.
  • Source maps are published with the npm tarball (the dist/ directory includes them) so consumers debugging the published package see TS source.

16. Generated project layout

Per 10-build-system §2:

myapp/
package.json
package-lock.json # canonical lockfile
tsconfig.json # composite root
tsconfig.base.json # strict + ES2024 + isolatedModules
tsconfig.node.json
tsconfig.deno.json
tsconfig.bun.json
tsconfig.browser.json
jsr.json # JSR manifest (when --target=deno-jsr)
.eslintrc.json
.prettierrc.json
src/
index.ts # public entry
generated/ # Mochi-emitted
foo.ts
bar.ts
mod/
sub.ts
mochi_runtime/ # vendored when --target=typescript-source; npm dep otherwise
dist/ # produced by tsc --build (when --target=npm-package)
node/
index.js + index.d.ts + index.js.map
deno/
bun/
browser/
bundle/ # esbuild output (when --target=browser-bundle)
index.js
index.d.ts # shared declaration
test/
README.md
LICENSE
  • npm install in this directory bootstraps Node 22, Deno 2, Bun 1.1 conformance.
  • npm run build runs tsc --build.
  • npm pack produces the .tgz.

17. CLI surface

Per 10-build-system §1:

mochi build --target=<TGT> <input.mochi> [-o <output>] [--ts=5.6|5.7] [--es=2024|2025]

Five targets:

  • --target=typescript-source: emits .ts plus package.json plus tsconfig chain; does not run tsc.
  • --target=npm-package: emits source plus runs tsc --build plus runs npm pack; produces .tgz ready for npm publish.
  • --target=deno-jsr: emits source plus jsr.json; optionally runs deno publish --dry-run for validation.
  • --target=browser-bundle: emits source plus runs tsc --build plus runs esbuild; produces dist/bundle/index.js (single ESM file).
  • --target=deno-jupyter: produces a Deno kernelspec under ~/.local/share/jupyter/kernels/mochi-deno-<pkg>/kernel.json plus a small Mochi-wrapper driver that transpiles each cell to TS and forwards to Deno's --unstable-kernel mode.

The driver invokes npm for install and pack, tsc for build, esbuild for browser bundle, deno for JSR and Jupyter, and eslint plus prettier for the lint and format gates.

18. Gates

Per 11-testing-gates §1 to §9:

  1. vm3 byte-equal stdout (master gate).
  2. tsc --noEmit --strict --noUncheckedIndexedAccess --exactOptionalPropertyTypes --noImplicitOverride --noFallthroughCasesInSwitch --noPropertyAccessFromIndexSignature (zero diagnostics).
  3. eslint --max-warnings 0 with @typescript-eslint/recommended-type-checked.
  4. prettier --check fixed-point.
  5. Four-runtime execution: Node 22 (node dist/node/index.js), Deno 2 (deno run --allow-read dist/deno/index.js), Bun 1.1 (bun dist/bun/index.js), browser (Playwright + Chromium 130 + esbuild bundle, console.log captured).
  6. npm pack plus install from tarball into fresh directory plus execute.
  7. Reproducibility: byte-identical .tgz SHA256 across two CI hosts (SOURCE_DATE_EPOCH plus sorted tarball entries).
  8. JSR dry-run: deno publish --dry-run against a local JSR mirror.
  9. npm publish --dry-run --provenance against a verdaccio local registry (Sigstore signature verified locally).

Phase plan

Eighteen phases mirroring MEP-45 through MEP-51:

PhaseNameSurfaceTarget fixtures
1Hello worldconsole.log, const, number5
2Scalarsint via bigint/number; float; bool; string; bytes; comparisons30
3.1Listsreadonly T[] / T[], index, len, for-each, comprehensions25
3.2MapsMap<K, V>, get, set, has, delete, for-each, entries25
3.3SetsSet<T> with ES2024 methods (union, intersection, difference)15
3.4Lists of records(readonly Record)[], comprehensions over records20
4Recordsclass with readonly fields plus static of() factory35
5Sum typesdiscriminated union plus exhaustive switch (x.kind)40
6Closures and higher-orderarrow functions, nested functions, (args) => R30
7Query DSLiterator helpers (Iterator.from, .map, .filter, .take)40
8Datalogfacts, rules, recursion, semi-naive eval in @mochi/runtime/datalog20
9AgentsAsyncIterableQueue + AbortController, cast, call, supervision35
10StreamsAsyncIterable<T>, AsyncGenerator, for await25
11async coloring + MochiResultasync/await, AggregateError, MochiResult Ok/Err30
12FFINode N-API + Bun FFI + Deno FFI dispatch20
13LLMllm.generate (provider-pluggable)10
14fetchplatform fetch against local test server15
15npm package build via tsc + npm packtsc --build, install from tarball, execute on all four runtimescovered
16Reproducible buildSOURCE_DATE_EPOCH plus sorted tarball; byte-identical .tgzcovered
17Deno JSR + Jupyter + browser bundledeno publish --dry-run, deno jupyter, esbuild bundle25
18npm Trusted PublishingSigstore + GitHub OIDC + provenance dry-rungate-only

Per 11-testing-gates §18, a phase lands only when all gates are green: per-phase fixture corpus, tsc strict, eslint clean, prettier fixed-point, four-runtime execution, npm pack plus install, reproducibility for Phase 16, JSR plus Jupyter plus browser bundle for Phase 17, npm Trusted Publishing dry-run for Phase 18.

Rationale

The five load-bearing decisions (§Abstract) flow from a single observation: JavaScript and TypeScript span more deployment surfaces (npm registry, four tier-1 runtimes, the browser, the edge, the desktop, the Jupyter notebook) than any other Mochi target, and the typed-TypeScript tooling (tsc 5.6, eslint, prettier, esbuild, tsc-build-mode) has matured to the point where Mochi can emit fully-typed TypeScript that any production reviewer accepts.

The choice to ship both .ts source and pre-compiled .js dist (rather than picking one) follows from npm-ecosystem conventions. Library authors increasingly ship .ts source plus pre-compiled .js plus .d.ts: the .ts is for IDE go-to-definition (TypeScript's source map plus declaration map chain), and the .js is for runtime consumption by non-TS-aware tools. Shipping only .ts would force every consumer to run tsc (acceptable for Vite-style modern apps but not for legacy Node consumers). Shipping only .js plus .d.ts would lose source-level navigation. Both ship; the package.json "types" plus "exports" map routes consumers correctly.

The choice of the four-runtime matrix (Node + Deno + Bun + browser) rather than Node-only follows from the fragmentation of the JavaScript runtime landscape. By 2026 Q1, Deno Deploy and Cloudflare Workers (V8 isolate, not Node) carry significant edge-compute traffic; Bun has substantial production adoption for HTTP servers and CI; the browser is the only zero-install Mochi target. A Node-only Mochi build would close all three. Conditional exports (package.json "exports" with node, deno, bun, browser conditions) is the modern way to fan out a single TypeScript source into per-runtime bundles. The runtime fan-out cost is modest (four tsc invocations sharing project references), the testing cost is fixed (CI matrix), and the user benefit is "Mochi runs everywhere JavaScript runs".

The choice of AsyncIterableQueue plus AbortController over RxJS or Web Streams as the agent mailbox follows from dependency and ergonomic considerations. RxJS adds a 35KB minified dep that every Mochi user inherits; its Subject model is push-only and lacks native backpressure; its operator surface is huge and discoverable only through documentation. Web Streams (ReadableStream<T>, WritableStream<T>, TransformStream<T>) is built-in and standardised but its mailbox-shape (a ReadableByteStreamController plus a pull callback plus a cancel callback) is heavier than the agent needs, and ReadableStream typing is awkward under --exactOptionalPropertyTypes. A hand-rolled AsyncIterableQueue<T> is ~50 LOC, has the exact shape Mochi needs, mirrors asyncio.Queue (MEP-51) and Channel (MEP-50), and surfaces as AsyncIterable<T> to user code (so for await (const msg of mailbox) Just Works). Promise.withResolvers() (ES2024) is the load-bearing primitive that lets the call(req) reply future stay simple. AbortController is built-in on all four runtimes, is the platform-standard cancellation primitive (used by fetch, by setTimeout, by addEventListener), and propagates naturally through async stacks.

The choice of npm + tsc as canonical (rather than pnpm or Bun as primary) follows from ecosystem universality. npm is the default package manager on every Node install, on every CI image, in every npm-publish workflow tutorial, and on every npmjs.org documentation page. pnpm is faster on cold cache and uses content-addressable storage to reduce duplication but is not the default; users who prefer it can run pnpm install against the same package.json (the package-lock.json is read-compatible enough for pnpm to bootstrap). Bun is faster still but has its own install behaviour that diverges in edge cases (peer dep resolution, lifecycle scripts). npm is the safest universal default. tsc is the canonical type-checker and emitter; no Babel (slower for TS, separate type-check pass), no Rollup as primary (Rollup is a bundler, not a type-checker; esbuild covers the bundle case), no Webpack (heavy, slow, legacy). The result is a Mochi-emitted project that any npm-fluent developer recognises immediately.

The choice of class with readonly fields plus a static of() factory (rather than interface plus object literal, or readonly record types via TS-only constructs) follows from the goal of preserving Mochi's record identity at runtime. Object literals have no instanceof discriminator and no method dispatch surface; using them for records would lose the runtime tag that Mochi's reflection and serialisation need. A class with private constructor plus static of(args) factory gives instanceof discrimination, blocks accidental mutation (readonly fields enforced at the type level), and supports method dispatch (Mochi record methods lower to class methods). The cost is ~50 bytes of constructor overhead per record instance, which is acceptable at v1 and recoverable in v2 if profiling demands it (object-literal records behind a feature flag).

Backwards Compatibility

MEP-52 is purely additive. mochi run keeps the vm3 path; existing transpiler3 targets (C, BEAM, JVM, .NET, Swift, Kotlin, Python) are untouched. No Mochi language surface changes. The TypeScript target lands as a new transpiler3/typescript/ subtree and a new tests/transpiler3/typescript/ fixture corpus. Phase gates ensure no cross-target regression. Existing fixtures must produce byte-equal stdout via the TypeScript path on all four runtimes. New CLI flags: --target=typescript-source, --target=npm-package, --target=deno-jsr, --target=browser-bundle, --target=deno-jupyter. No existing flag changes meaning.

Reference Implementation

Implementation lives under:

  • transpiler3/typescript/aotir/program.go: shared aotir IR consumer (target-agnostic, shared with MEP-45 through MEP-51).
  • transpiler3/typescript/lower/lower.go: aotir to TypeScript syntax tree lowering pass.
  • transpiler3/typescript/emit/emit.go: TS tree printer, package.json emitter, tsconfig emitter, prettier invocation, eslint --fix invocation.
  • transpiler3/typescript/build/build.go: driver for tsc --build, esbuild, npm pack, npm install, npm publish, deno publish, jupyter kernelspec install.
  • runtime3/typescript/: vendored runtime source (@mochi/runtime package source tree).
  • tests/transpiler3/typescript/: fixture corpus plus gate tests.

The transpiler3/typescript codegen pass is approximately 5500 LOC of Go (a TypeScript-syntax-tree model plus the tree printer plus the prettier and eslint invocations). The runtime library @mochi/runtime is approximately 6500 LOC of TypeScript across approximately 35 modules with zero npm dependencies in the default build, plus optional ~1500 LOC of native bindings split across the Node N-API addon, the Bun FFI bindings, and the Deno FFI bindings (each a separate npm package the user opts into). The fixture corpus targets approximately 445 fixtures by Phase 18 completion. CI runs on x86_64-linux-gnu, aarch64-linux-gnu, aarch64-darwin, and x86_64-windows across Node 22.11.0 LTS, Deno 2.0.0, Bun 1.1.30, and Chromium 130 via Playwright. Initial implementation total: approximately 13,500 LOC.

Dependencies on other MEPs

  • MEP-4 (Type System), MEP-5 (Type Inference), MEP-13 (ADTs and Match): Mochi front-end.
  • MEP-45 (C transpiler): aotir IR plus shared passes.
  • MEP-46 (BEAM transpiler): shared IR confirmation, cross-target gates.
  • MEP-47 (JVM transpiler): shared IR confirmation, prior art for direct-bytecode (not used here, TS emits source).
  • MEP-48 (.NET transpiler): shared IR confirmation, prior art for typed-source emission via Roslyn-style syntax tree.
  • MEP-49 (Swift transpiler): shared IR confirmation, prior art for typed-source emission with strict checkers.
  • MEP-50 (Kotlin transpiler): shared IR confirmation, prior art for KotlinPoet-shaped emission (TS uses an equivalent Mochi-side tree).
  • MEP-51 (Python transpiler): shared IR confirmation, prior art for stdlib-AST emission and PyPI-style packaging.

External:

  • TypeScript 5.6+ (released September 2024; 5.7 forward-tracked).
  • Node.js 22 LTS (released April 2024; active LTS through 2027).
  • Deno 2.0+ (released October 2024).
  • Bun 1.1+ (released February 2024; 1.1.30+ current as of Q1 2026).
  • npm 10+ (shipped with Node 20.5, September 2023).
  • esbuild 0.24+ (for browser bundle target).
  • prettier 3.x (formatter; replaces hand layout).
  • eslint 9.x with typescript-eslint 8.x (linter, flat config).
  • Sigstore for npm provenance (GA April 2024).
  • JSR (jsr.io, GA September 2024).
  • Playwright 1.48+ (browser execution gate).

Runtime deps (in @mochi/runtime):

  • Zero npm dependencies in the default build.
  • Optional native: @mochi/runtime-native-node (Node N-API), @mochi/runtime-native-bun (Bun FFI), @mochi/runtime-native-deno (Deno FFI).
  • Temporal polyfill (@js-temporal/polyfill) until native Temporal ships (Stage 3 as of Q1 2026; see Q4).

Open Questions

Per 12-risks-and-alternatives §3:

  • Q1: When to default int to bigint versus number. bigint is arbitrary-precision and safe for the full Mochi integer range but is approximately 2x to 5x slower than number on arithmetic and cannot be mixed with number. The current decision is "default to bigint, specialise to number when monomorphisation proves the value fits in i53". The open question is whether the threshold should be more aggressive (default to number, fall back to bigint only when overflow is provably possible). Aggressive defaulting risks silent overflow on Mochi values the compiler cannot prove safe; conservative defaulting eats a perf cost the user may not need. Resolution: ship conservative (current), revisit in v1.5 based on user benchmark feedback.

  • Q2: Should JSR ship in v1 or v2. JSR (GA September 2024) is the Deno-native registry. Publishing there gives Deno users a first-class jsr:@mochi/[email protected] specifier (faster than npm:@mochi/foo because JSR transpiles TS on the server, no Deno-side npm shim). The cost is a second publish step in the release workflow plus a jsr.json manifest. v1 ships JSR as a --target=deno-jsr opt-in but does not gate releases on JSR availability. v2 may make JSR a default publish target if Deno adoption signals support it.

  • Q3: Cloudflare Workers as tier-1 or tier-2. Cloudflare Workers (V8 isolate, not Node) carries significant edge-compute traffic. By 2026 Workers is largely Node-compatible (nodejs_compat flag enables node:* imports) but is not bit-for-bit Node. The current decision is to ship Workers as a tier-2 deployment target (the same dist/browser/ build runs there in most cases because Workers' globals are mostly browser-compatible: fetch, Request, Response, URL). Promotion to tier-1 (separate dist/workers/ build plus separate test gate) is a v1.5 candidate.

  • Q4: Temporal polyfill until native ships. Temporal (TC39 proposal, Stage 3 as of Q1 2026) is the canonical replacement for Date. The polyfill (@js-temporal/polyfill) is approximately 60KB minified. v1 ships the polyfill as an opt-in dep of @mochi/runtime/temporal; emitted code uses Temporal.* types directly. When Temporal ships natively (likely Node 24 LTS, Deno 2.x, Bun 1.2.x, browsers Q3 2026 to Q1 2027 at earliest), the polyfill is dropped from the runtime's dependencies and surfaced only as a peerDependenciesMeta optional.

  • Q5: Should we ship CJS dual emit as a future v2 feature. ESM-only is the v1 choice. CommonJS (require, module.exports) is the legacy module system; it is still used by some npm packages and some legacy tooling (older Jest, older Webpack configs). A CJS dual emit (npm package with both dist/cjs/ and dist/esm/ plus a package.json "exports" map that routes per consumer module type) doubles the build cost and the test matrix. The current decision is to reject CJS at v1 (modern toolchains accept ESM; Mochi targets modern toolchains). v2 may revisit if user feedback demands CJS interop.

  • Q6: Source map second layer (Mochi to TS). v1 emits the TS-to-JS source map layer that tsc provides natively. The second layer (Mochi-to-TS) requires a Mochi-side source-position emitter that threads aotir node positions through the lowering pass and into the TS syntax tree. This is a meaningful engineering cost (approximately 2000 LOC of Go plus a source-map composition pass) and is deferred to v1.5.

  • Q7: Browser-side WASM acceleration for hot loops. The browser bundle is pure JavaScript by default. Hot loops (numerical, array, string) could benefit from a Mochi-emitted Wasm module loaded via WebAssembly.instantiate. The decision is to reject Wasm at v1 (it doubles the build matrix and adds complexity that the v1 corpus does not justify). v2 may add --target=browser-bundle-wasm for specific user-opt-in cases.

  • Q8: Tauri and Electron-specific build targets. Tauri 2.0 (October 2024) and Electron continue to consume web technologies for the UI. The current browser bundle target works for both. Specific targets (--target=tauri-bundle, --target=electron-renderer) that emit framework-specific bootstrapping (Tauri's tauri.conf.json, Electron's package.json main field) are v2 candidates.

Security

  • npm supply chain. npm publish --provenance (Sigstore + GitHub OIDC, GA April 2024) is the only publish path; no long-lived API tokens stored in CI. Sigstore signs the provenance statement; consumers verify via npm audit signatures. The provenance statement carries repository URL, commit SHA, workflow file, runner identity, and the SHA256 of the published tarball. npm audit runs at install time and reports known-vulnerable dependencies (the default Mochi runtime has zero npm deps, so the audit surface is minimal).
  • JSR supply chain. deno publish against jsr.io uses OIDC tokens; no long-lived tokens. JSR transpiles TS on the server; the source is published, not the binary, so consumers can audit.
  • Browser CSP. Emitted code is CSP-compatible: no eval, no new Function, no inline event handlers, no style="..." inline styles, no unsafe-inline script tags. The emitter rejects any IR construct that would require any of these. Strict CSP (default-src 'self'; script-src 'self'; style-src 'self') accepts Mochi browser bundles without modification.
  • Eval-free guarantee. Mochi-emitted code never uses eval, new Function, setTimeout(string, ...), setInterval(string, ...), or any dynamic-code-string evaluation path. All code is statically analysable by tsc. The codegen pass has a static rejection rule for IR constructs that would require dynamic code generation (none exist in v1).
  • Prototype pollution avoidance. Maps lower to Map<K, V>, never to plain Object. Where the codegen does emit object literals (record fields), the surrounding class has a null prototype option via Object.create(null) for the underlying field storage when the IR signals untrusted-key risk (e.g., user-supplied JSON keys). The runtime's JSON parser uses JSON.parse with a reviver that rejects __proto__, constructor, and prototype keys at deserialisation.
  • FFI safety. FFI declarations are explicit at the Mochi level; the runtime never auto-loads arbitrary shared libraries. Each FFI declaration carries a typed signature; the runtime validates argument types at the boundary.
  • LLM provider API keys. Read from process.env (Node, Bun), Deno.env.get (Deno), never logged, never persisted to disk, never serialised into the published tarball. The browser bundle rejects LLM calls at codegen unless the user explicitly opts into "browser-side LLM with user-supplied key" mode.
  • TLS verification. fetch's default behaviour is on; the emitter does not expose a verify=false opt-out.
  • Reproducibility. SOURCE_DATE_EPOCH pinned; tarball entries sorted; no __filename, __dirname, or import.meta.url leaks the build host's filesystem layout into emitted code.
  • Jupyter Deno kernel sandboxing. The Deno Jupyter kernel runs user-provided Mochi code in an in-process Deno isolate with the standard Deno permission model (--allow-read, --allow-net, etc. opt-in). Untrusted notebook execution requires the standard JupyterLab security policy (token-protected server, no auto-execute on open).

References

ECMAScript and TC39:

TypeScript:

Runtimes:

Tooling:

Sibling MEPs:

Research Notes

Twelve research notes elaborate the design:

  • 01-language-surface: Mochi sub-languages mapped onto TypeScript 5.6 lowering obligations.
  • 02-design-philosophy: Why TypeScript, why 5.6 floor, why both strict tsc and typed-eslint, why AsyncIterableQueue over RxJS, why npm canonical over pnpm or Bun.
  • 03-prior-art-transpilers: PureScript, ReScript (formerly BuckleScript), Elm, Kotlin/JS, Scala.js, Reason, Fable (F# to JS), and the Mochi-style typed-source competitors.
  • 04-runtime: @mochi/runtime layout, AsyncIterableQueue implementation, MochiResult, mochiStrLen and mochiStrIndex, datalog engine, LLM provider dispatch, Temporal polyfill wrapper.
  • 05-codegen-design: TypeScript syntax tree shape, tree printer, prettier and eslint invocations, aotir IR reuse.
  • 06-type-lowering: Type-by-type mapping to TypeScript 5.6 (bigint vs number, readonly arrays, class records, discriminated union sums, MochiResult).
  • 07-runtime-portability: Platform matrix (Node 22, Deno 2, Bun 1.1, Chromium 130, Firefox 130, Safari 18; Linux x86_64 and arm64, macOS arm64, Windows x86_64), Temporal availability, ESM availability.
  • 08-dataset-pipeline: Query DSL lowering via iterator helpers plus generator functions plus AsyncIterable, datalog engine in pure TS.
  • 09-agent-streams: Mochi agents as a class wrapping AsyncIterableQueue<Message> plus AbortController supervision, streams as AsyncIterable<T>, structured concurrency via parent-abort propagation.
  • 10-build-system: package.json plus tsconfig chain plus tsc --build plus esbuild plus npm pack plus npm publish --provenance plus JSR plus Deno Jupyter.
  • 11-testing-gates: Per-phase Go test gates, four-runtime execution matrix, tsc strict, eslint clean, prettier fixed-point, npm install plus execute, reproducibility, JSR dry-run, npm publish dry-run.
  • 12-risks-and-alternatives: Risk register; RxJS, Web Streams as mailbox, CJS dual emit, Cloudflare Workers as tier-1, Tauri/Electron-specific targets, browser-side Wasm acceleration rejected or deferred for v1.

The 18-phase delivery plan walks from hello-world through scalars, collections, records, sums, closures, queries, datalog, agents, streams, async coloring, FFI, LLM, fetch, then npm packaging, reproducibility, Deno JSR plus Jupyter plus browser bundle, and npm Trusted Publishing. Each phase is gated against vm3, tsc strict, eslint, prettier, four-runtime execution, npm install, and (per phase) reproducibility, JSR, browser bundle, or npm Trusted Publishing.