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)
onDidSaveTextDocument filtering 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
Related
Problem
The AffineScript extern-call ABI is synchronous: every
extern fnis 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:The current rsr-certifier port falls back to shelling out to the
rsrCLI 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: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 —
withProgressalone 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.:
withProgressthen becomes:sendRequestbecomes: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/awaitand have codegen emit JS shims that await on the host side. That's a much bigger lift.Out of scope
require()+ Node Buffer. A webworker target would have to drop those.Acceptance criteria
vscode.window.withProgress(opts, task)andLanguageClient.sendRequest(method, params)instdlib/Vscode.affine+stdlib/VscodeLanguageClient.affine.packages/affine-vscode/mod.js.hyperpolymath/standardsrsr-certifier) restores the lost in-process behaviour using these bindings.Related