From b3a226f5fec4ba6b1ec4f0773cc2ef86907ef323 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 18 May 2026 09:42:22 +0100 Subject: [PATCH] feat(thenable): wasm-path thenable-resolution primitives (Closes #205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wasm/Node backend cannot await a host Thenable, so a languageClientSendRequest result was unconsumable by a wasm guest (blocking the #103 rsr-certifier pilot data path). The #199 function-value ABI now makes the host→guest re-entry expressible, so the minimal #103 design sketch is finally buildable. - stdlib/Vscode.affine: `thenableThen(t, on_settle: fn(Unit) -> Int) -> Disposable / Async` and `thenableResultJson(t) -> String`. - packages/affine-vscode/mod.js: thenableThen wraps the guest closure via the PR-5c closure-pointer marshalling (wrapHandler) and stores the settled value keyed by the Thenable handle; thenableResultJson returns it JSON-encoded via the established reg(string) convention used by every other `-> String` extern. Per-process __thenableResults map. Rejection path stored as { __error }. Verified: Vscode.affine typechecks; dune test --force 253/253, zero regression; mod.js node --check clean; a consumer using both primitives typechecks and lowers (`thenableThen(t, ((u) => 0))`). Unblocks the rsr-certifier pilot (PR-5d-B): sendRequest rsr/getCompliance → thenableThen/thenableResultJson → status-bar/diagnostics/webview. Closes #205 Refs #103, #199 Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/affine-vscode/mod.js | 30 ++++++++++++++++++++++++++++++ stdlib/Vscode.affine | 17 +++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/affine-vscode/mod.js b/packages/affine-vscode/mod.js index 7310a0f5..0b97778a 100644 --- a/packages/affine-vscode/mod.js +++ b/packages/affine-vscode/mod.js @@ -33,6 +33,8 @@ module.exports = function makeVscodeBindings(vscode, lcModule, hostShim) { const reg = (obj) => hostShim._registerHandle(obj); const get = (h) => hostShim._getHandle(h); const getInstance = () => hostShim._instance; + // Settled host-Thenable values, keyed by Thenable handle (issue #205). + const __thenableResults = new Map(); // ── String marshalling ───────────────────────────────────────────── // AffineScript's WASM 1.0 codegen stores string literals at the offset @@ -333,6 +335,34 @@ module.exports = function makeVscodeBindings(vscode, lcModule, hostShim) { const ctx = get(ctxHandle); return reg(ctx ? ctx.asAbsolutePath(readString(relPtr)) : ""); }, + + // ── Thenable resolution (issue #205) ─────────────────────────── + // The wasm guest cannot await; these let it observe a settled host + // Thenable. thenableThen registers the guest closure (reusing the + // #199 closure-pointer marshalling via wrapHandler) and stores the + // settled value keyed by the Thenable handle; thenableResultJson + // returns it JSON-encoded (same reg(string) return convention as + // every other `-> String` extern). + thenableThen: (tHandle, onSettlePtr) => { + const thenable = get(tHandle); + const cb = wrapHandler(onSettlePtr); + if (!thenable || typeof thenable.then !== "function") { + return reg({ dispose() {} }); + } + Promise.resolve(thenable).then( + (val) => { __thenableResults.set(tHandle, val); try { cb(); } catch (_e) {} }, + (err) => { + __thenableResults.set(tHandle, { __error: String(err) }); + try { cb(); } catch (_e) {} + } + ); + return reg({ dispose() {} }); + }, + thenableResultJson: (tHandle) => { + if (!__thenableResults.has(tHandle)) return reg(""); + try { return reg(JSON.stringify(__thenableResults.get(tHandle))); } + catch (_e) { return reg(""); } + }, }; const VscodeLanguageClient = { diff --git a/stdlib/Vscode.affine b/stdlib/Vscode.affine index df0d5b50..beccf7c9 100644 --- a/stdlib/Vscode.affine +++ b/stdlib/Vscode.affine @@ -305,3 +305,20 @@ pub extern fn extensionAbsolutePath(ctx: ExtensionContext, rel_path: String) -> /// explicit `Unit` param avoids the zero-param-fn return-type collapse). /// Returns the progress Thenable. pub extern fn withProgressNotification(title: String, work: fn(Unit) -> Thenable) -> Thenable / Async; + +// ── Thenable resolution (issue #205) ───────────────────────────────── +// +// The source-to-source backend can `await` a Thenable directly; the +// wasm/Node backend cannot, so these primitives let a wasm guest +// observe a settled host Thenable. `thenableThen` registers a guest +// callback (the #199 function-value ABI — the host re-enters it when +// the Thenable settles); `thenableResultJson` reads the settled payload +// JSON-encoded once the callback has fired. + +/// Run `on_settle` when `t` resolves. `on_settle` is a function value +/// (#199); the host invokes it after settlement. Returns a Disposable. +pub extern fn thenableThen(t: Thenable, on_settle: fn(Unit) -> Int) -> Disposable / Async; + +/// The resolved value of `t`, JSON-encoded. Empty string until `t` has +/// settled (i.e. until the `thenableThen` callback has fired). +pub extern fn thenableResultJson(t: Thenable) -> String;