Repository navigation
Async-extern ABI: bind withProgress + LanguageClient.sendRequest #103
Description
Problem
The AffineScript extern-call ABI is synchronous: every extern fn is a Wasm import that returns a single primitive value to the caller in the same frame. This means Thenable-returning JS APIs (which are most of vscode's "interactive" surface) cannot be bound at all.
Two concrete APIs needed by rhodium-standard-repositories/satellites/rsr-certifier/extensions/vscode/src/extension.affine (ported in standards#46) are blocked by this:
// 1. withProgress: second arg is async () => Thenable<T>
vscode.window.withProgress(
{ location: vscode.ProgressLocation.Notification, title: '...' },
async () => { /* await client.sendRequest(...) */ }
);
// 2. LanguageClient.sendRequest: returns Thenable<T>
const result = await client.sendRequest('rsr/getCompliance');The current rsr-certifier port falls back to shelling out to the rsr CLI in a fresh terminal for all four commands (the same fallback path the original TS extension already had for the no-LSP case). The downstream effects of the unbound APIs are gone:
- Status-bar text mutation on every check result (now: static initial value)
- DiagnosticCollection population from check results (now: collection created and disposed, never populated)
- Webview report with per-check rendered HTML (now: constant placeholder shape)
onDidSaveTextDocumentfiltering by saved doc's basename (now: zero-arg handler, no-op to avoid spawning a terminal per save)
Why this matters
Every vscode extension that wants real-time in-process behaviour (not "open a terminal and run a CLI") needs one or both of these APIs. As the AffineScript-port footprint grows, this becomes load-bearing — withProgress alone is ubiquitous in extensions that show notifications, perform long-running work, or call out to language servers.
Design sketch
The simplest viable shape would surface JS Promise/Thenable handles to AffineScript via a small set of primitives, e.g.:
extern type Thenable;
// Resolve a wasm-table thunk against a Thenable. The thunk re-enters wasm
// when the Thenable settles. The result is registered in the handle table
// and accessible via the result-getter primitives below.
extern fn thenableThen(t: Thenable, handler: Int) -> Disposable;
// After the thunk fires, read the last-settled result for `t`.
extern fn thenableResultString(t: Thenable) -> String;
extern fn thenableResultInt(t: Thenable) -> Int;
extern fn thenableResultJson(t: Thenable) -> String;
withProgress then becomes:
extern fn withProgressNotification(title: String, work_thunk: Int) -> Thenable;
sendRequest becomes:
extern fn languageClientSendRequest(c: LanguageClient,
method: String,
params_json: String) -> Thenable;
This keeps the synchronous extern-call shape (each binding returns immediately) while modelling async by chaining wasm-table thunks. Compositionally it's awkward — you can't write let x = await foo() in AffineScript yet — but it's enough to express the rsr-certifier patterns.
A more invasive option is to grow the AffineScript language with proper async fn / await and have codegen emit JS shims that await on the host side. That's a much bigger lift.
Out of scope
- Browser-host webworker extension host. The current Node-CJS target binds to
require()+ Node Buffer. A webworker target would have to drop those. - Multi-promise composition (Promise.all etc.). The minimum viable design is single-thenable resolution.
Acceptance criteria
- Concrete binding surface for
vscode.window.withProgress(opts, task)andLanguageClient.sendRequest(method, params)instdlib/Vscode.affine+stdlib/VscodeLanguageClient.affine. - JS-side implementation in
packages/affine-vscode/mod.js. - A pilot consumer (most likely
hyperpolymath/standardsrsr-certifier) restores the lost in-process behaviour using these bindings.
Related
- Pilot port that documented the gap: hyperpolymath/standards#46
- Bindings expansion that touched everything but these two shapes: feat(vscode-bindings): expand surface for rsr-certifier port (#64) #102
- Parent: [Ecosystem] Port external VS Code extensions (my-lang, rsr-certifier) to .affine once #35 lands #64
Activity
feat: Deno-ESM target + grammar-v2 build-out (Refs #122) (#123)
feat(stdlib): async-extern binding surface — Thenable + withProgress/…
feat(affine-vscode): languageClientSendRequest host binding (Refs #103)…
feat(thenable): wasm-path thenable-resolution primitives (Closes #205) (
hyperpolymath commentedon May 18, 2026
Closing — async-extern ABI binding surface complete (STAGE-B).
The full host->guest async binding surface is shipped on
main:Combined with the callback ABI (#199), AffineScript can now bind withProgress + LanguageClient.sendRequest end to end. The cross-repo rsr-certifier pilot (was PR-5d-B) is decoupled to an independent standards-repo follow-up and no longer gates this issue.
Closed on owner approval (joint close, 2026-05-18).
feat(stdlib): add httpPostJson async-extern for out-of-process JSON P…
feat(rsr-certifier): BoJ-first 3-tier compliance in the VS Code exten…
hyperpolymath commentedon Jun 1, 2026
Status update — AS stdlib codegen pattern now established (2026-06-01) 📌
For ticket coordinators / anyone tracking this gap:
lib/codegen_deno.ml. Tests live intests/codegen-deno/*.What this means for the gap tracked in this ticket:
stdlib/*.affine→ wire indeno_builtinstable + prelude → smoke-test intests/codegen-deno/*.🤖 Cross-estate AS-status sweep on 2026-06-01.