Skip to main content

Phase 14. Promise / async bridge

FieldValue
MEPMEP-72 §Phases
StatusNOT STARTED
Tracking issue(pending)
Tracking PR(pending)
Commit(pending)

Gate

TestPhase14PromiseAsync in package3/typescript/promise/phase14_test.go: subtests promise_to_async_fun, async_iterable_to_stream, iterable_to_list, abort_signal_extern, error_rejection_translation, microbench_50ns, golden_corpus. The first translates a representative TS Promise<string> return into a Mochi async fun(): string and asserts the emitted Mochi extern compiles. The second translates AsyncIterable<int> into stream<int> and asserts the emitted Mochi consumer loop uses for await. The third translates Iterable<int> into list<int> (eager) and asserts the for ... in consumer is correct. The fourth translates a function that accepts AbortSignal and asserts the Mochi-side parameter is an opaque extern. The fifth translates a function that rejects via throw new Error("...") and asserts the rejection routes through MEP-52 phase 11's try-catch desugar. The sixth microbenchmarks await Promise.resolve(0) and asserts the per-call cost is under 100ns on darwin-arm64. The seventh runs against the 24-package fixture corpus.

Lowering decisions

The Promise / async bridge is structurally smaller than MEP-73's Rust async bridge and MEP-74's Go goroutine bridge. The reason: the host JS runtime's event loop is intrinsic. Every runtime target (Node 22 LTS, Deno 2, Bun 1.1, browser, edge) ships its own event loop + microtask queue; the bridge adds zero code for runtime construction.

Translation table (from research note 08):

TS constructMochi constructLowering
Promise<T> (return)async fun(): Tdirect: await flows through
PromiseLike<T>async fun(): Tsame (structurally identical)
AsyncIterable<T> (return / for-await source)stream<T>for-await loop in Mochi → for await in emitted TS
AsyncIterator<T>stream<T>same
Iterable<T>list<T>eager materialise ([...iterable])
AbortSignal (parameter)opaque extern type AbortSignalpasses through
AbortController (constructor)global externexposed via globalThis.AbortController
rejection (throw new Error(...))Mochi exceptionMEP-52 phase 11 try-catch desugar

The phase ships:

  • An apply-promise.go pass that wraps each TS-side async function extern entry into a Mochi async fun declaration; the wrapping is purely declarative (no code generation; the call site uses await directly).
  • An apply-async-iterable.go pass that detects AsyncIterable<T> in ApiSurface and rewires the consumer's for ... in loop to emit a TS for await (...) loop.
  • A microbench/promise_bench_test.go benchmark suite that records the per-call cost on all three runtime targets (Node, Deno, Bun) and asserts the cost stays under 100ns.

No Runtime singleton, no tokio::block_on-equivalent, no handle pool. The bridge code in promise/ is ~200 LOC of Go (the translation table), vs MEP-73's package3/rust/async/ ~1500 LOC and MEP-74's package3/go/goroutine/ ~1200 LOC.

Files changed

FilePurpose
package3/typescript/promise/promise.goApplyPromiseToAsyncFun, ApplyAsyncIterableToStream, ApplyAbortSignalExtern
package3/typescript/promise/microbench/promise_bench_test.goper-runtime microbench
package3/typescript/promise/phase14_test.goTestPhase14PromiseAsync sentinel

Test set

7 subtests as listed in the Gate section.

Cross-references