Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions runtime/src/shim/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export class Translator {
*/
readonly buildHash: string | null;

private constructor(instance: WebAssembly.Instance, buildHash: string | null) {
this.#exports = instance.exports as unknown as ShimExports;
private constructor(exports: object, buildHash: string | null) {
this.#exports = exports as unknown as ShimExports;
this.buildHash = buildHash;
for (const name of ["memory", "ts_alloc", "ts_dealloc", "ts_translate"]) {
if (!(name in this.#exports)) {
Expand All @@ -69,7 +69,33 @@ export class Translator {
).join("");
}
const instance = await WebAssembly.instantiate(module, {});
return new Translator(instance, buildHash);
return new Translator(instance.exports, buildHash);
}

/**
* Wrap an ALREADY-INSTANTIATED shim — the ESM wasm-module import path
* (issue #16 delivery design): `import * as shim from ".../translator_shim.wasm"`
* hands back an instantiated namespace (the shim imports nothing, so the
* ESM integration instantiates it trivially), and this wraps it with no
* further compile or copy.
*
* Sharing note: ESM gives ONE instance per realm, so every `fromExports`
* wrapper over the same namespace shares linear memory. That is safe by
* construction — `translate` is synchronous end-to-end (alloc → call →
* copy out → dealloc within one JS frame), so calls can never interleave —
* but treat the wrappers as equivalent, not independent.
*
* `buildHash` (hex sha-256 of the shim wasm bytes) cannot be recovered
* from an instance; pass it when known — a published package can ship the
* hash of the exact asset it carries — or leave it absent and the
* artifact cache politely refuses to key on translator identity
* (cache/core.ts).
*/
static fromExports(
exports: object,
opts: { buildHash?: string } = {},
): Translator {
return new Translator(exports, opts.buildHash ?? null);
}

/** Translate a component binary into plan v0 + adapter artifacts. */
Expand Down
90 changes: 90 additions & 0 deletions runtime/tests/translator_from_exports_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Translator.fromExports — the ESM wasm-module import path (issue #16
// delivery design note): Deno's native wasm imports hand back an
// instantiated namespace; `fromExports` wraps it with no compile and no
// copy. Pinned here: the wrapped namespace translates identically to a
// bytes-constructed Translator (envelope equality), the missing-export
// refusal, and the buildHash contract (null unless supplied).

import { assertEq } from "./support/asserts.ts";
import { Translator } from "../src/shim/mod.ts";

const shimUrl = new URL(
"../../target/wasm32-unknown-unknown/release/translator_shim.wasm",
import.meta.url,
);
const trivialUrl = new URL(
"../../crates/translator-shim/testdata/trivial.wasm",
import.meta.url,
);

async function maybeRead(url: URL): Promise<Uint8Array | null> {
try {
return await Deno.readFile(url);
} catch {
return null;
}
}

const shimBytes = await maybeRead(shimUrl);
const trivial = await maybeRead(trivialUrl);
const ready = shimBytes !== null && trivial !== null;

Deno.test({
name: "fromExports: a native wasm-module import translates identically to create(bytes)",
ignore: !ready,
fn: async () => {
// Deno's ESM wasm integration: dynamic import instantiates the
// zero-import shim and returns its exports as the module namespace.
const ns = await import(shimUrl.href);
const viaImport = Translator.fromExports(ns);
const viaBytes = await Translator.create(shimBytes!);
// Envelope equality is the strongest cheap equivalence: same plan JSON,
// same adapters, byte for byte.
assertEq(
viaImport.translateRaw(trivial!),
viaBytes.translateRaw(trivial!),
"envelopes must match",
);
},
});

Deno.test({
name: "fromExports: repeated translations on the shared instance stay stable",
ignore: !ready,
fn: async () => {
const ns = await import(shimUrl.href);
const t = Translator.fromExports(ns);
const first = t.translateRaw(trivial!);
for (let i = 0; i < 3; i++) {
assertEq(t.translateRaw(trivial!), first, `translation ${i} drifted`);
}
},
});

Deno.test("fromExports: a namespace missing shim exports is refused by name", () => {
let raised: unknown;
try {
Translator.fromExports({ memory: new WebAssembly.Memory({ initial: 1 }) });
} catch (e) {
raised = e;
}
assertEq(
String(raised).includes("ts_alloc"),
true,
`should name the missing export, got: ${raised}`,
);
});

Deno.test({
name: "fromExports: buildHash is null unless supplied",
ignore: !ready,
fn: async () => {
const ns = await import(shimUrl.href);
assertEq(Translator.fromExports(ns).buildHash, null, "default null");
assertEq(
Translator.fromExports(ns, { buildHash: "abc123" }).buildHash,
"abc123",
"supplied hash carried",
);
},
});
Loading