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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/target/
# @deltic/translator packaged asset (copied by `just shim`)
translator/translator_shim.wasm
harness/generated/
examples/guests/build/
node_modules/
Expand All @@ -17,3 +19,5 @@ bench/boundary/deltic-embedder.local.mjs
bench/boundary/generated/
bench/boundary/node_modules/
bench/boundary/guest/target/
# @deltic/translator packaged asset (copied by `just shim`)
translator/translator_shim.wasm
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Pre-1.0, but densely gated:
| `runtime/` | TS core: plan executor, canonical ABI, 0.3 task scheduler, JSPI bridge, embedder API (`runtime/src/embedder`) |
| `crates/bindgen` | WIT → TypeScript types for the embedder conventions |
| `examples/` | **start here to embed**: hello-world + kitchen-sink (WIT + Rust guest + TS host, self-checking), plus the guest fixture corpus |
| `translator/` | `@deltic/translator`: the packaged translator asset + `defaultTranslator()` per-platform loader (build-time alternative: `tools/translate`) |
| `wasi-shims/` | minimal WASI providers (p2 baseline + p3 clocks), one per semver track |
| `ct-runner/` | conformance-suite runner for the polymorph-test L1 contract |
| `harness/` + `tools/browser` | official-suite harness; Deno lane + Chromium/Firefox/WebKit lanes |
Expand Down
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"./harness",
"./wasi-shims",
"./ct-runner",
"./examples"
"./examples",
"./translator"
]
}
26 changes: 9 additions & 17 deletions examples/hello-world/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,20 @@
// call: give `instantiate` the component bytes and the translator, get
// typed-shaped exports back.
//
// The two wasm files are read with `Deno.readFile`, so this script runs
// with a scoped read permission (run.sh passes it):
// The translator comes from @deltic/translator — on Deno it arrives via a
// native wasm-module import (permission-free); the only permission this
// script needs is reading the component it runs:
//
// deno run --allow-read=..,../../target host.ts
// deno run --allow-read=build host.ts
//
// (Deno's `import ... with { type: "bytes" }` will make this flag-free
// once it stabilizes — it is behind --unstable-raw-imports as of Deno
// 2.9, and `type: "text"` is not an option for binaries: lossy UTF-8
// decoding corrupts them.)
//
// Inside this repository `@deltic/runtime` resolves through the Deno
// workspace; a published consumer uses the same specifier via JSR/npm or
// the `deltic-embedder.mjs` release bundle (deltic#16 tracks packaging).
// Inside this repository `@deltic/runtime` and `@deltic/translator`
// resolve through the Deno workspace; a published consumer uses the same
// specifiers via JSR/npm (deltic#16 tracks packaging).

import { instantiate } from "@deltic/runtime/embedder";
import { defaultTranslator } from "@deltic/translator";

const translator = await Deno.readFile(
new URL(
"../../target/wasm32-unknown-unknown/release/translator_shim.wasm",
import.meta.url,
),
);
const translator = await defaultTranslator();
const componentBytes = await Deno.readFile(
new URL("build/hello.component.wasm", import.meta.url),
);
Expand Down
2 changes: 1 addition & 1 deletion examples/hello-world/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ wasm-tools component new \
wasm-tools validate --features component-model build/hello.component.wasm

deno check host.ts
deno run --allow-read=..,../../target host.ts
deno run --allow-read=build host.ts
17 changes: 7 additions & 10 deletions examples/kitchen-sink/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
suspending,
WitError,
} from "@deltic/runtime/embedder";
import { defaultTranslator } from "@deltic/translator";

// Tiny self-checks so the example fails loudly if the API drifts.
// (`undefined` is meaningful in the conventions — the outermost-option
Expand Down Expand Up @@ -117,20 +118,16 @@ const imports = {

// --- §1: translate + instantiate --------------------------------------------

const translator = await Deno.readFile(
new URL(
"../../target/wasm32-unknown-unknown/release/translator_shim.wasm",
import.meta.url,
),
);
const translator = await defaultTranslator();
const componentBytes = await Deno.readFile(
new URL("build/kitchen-sink.component.wasm", import.meta.url),
);

// `{ componentBytes, translator }` translates internally (A3). When
// instantiating several components, create one `Translator` explicitly
// (`Translator.create(bytes)` from @deltic/runtime/shim) and pass it here
// instead — the wasm compile is the cost worth sharing.
// `{ componentBytes, translator }` translates internally (A3);
// `defaultTranslator()` is @deltic/translator's packaged, per-realm-cached
// loader (on Deno: a native wasm-module import — no permissions). Apps
// that know their components at build time can skip the translator
// entirely: see tools/translate (embedder-api A4).
//
// A marked import is auto-detection evidence: this instantiation selects
// JSPI mode by itself. (`jspi: false` would force plain mode, where a
Expand Down
2 changes: 1 addition & 1 deletion examples/kitchen-sink/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ wasm-tools component new \
wasm-tools validate --features component-model,cm-async build/kitchen-sink.component.wasm

deno check host.ts
deno run --allow-read=..,../../target host.ts
deno run --allow-read=build host.ts
2 changes: 2 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ shim:
CARGO_PROFILE_RELEASE_PANIC=abort \
CARGO_PROFILE_RELEASE_STRIP=symbols \
cargo build -p translator-shim --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/translator_shim.wasm translator/translator_shim.wasm

# wasmtime CLI is optional in build.sh (smoke run only when present).
# Guest fixture components (examples/guests/build/, gitignored): the
Expand All @@ -62,6 +63,7 @@ examples: shim
# mismatched-pair refusal.
test-translate: shim
deno test --allow-read --allow-write=/tmp --allow-run tools/translate/translate_test.ts
cd translator && deno task check && deno task test

# Rehearsal finding: 20 runtime e2e tests self-skip when it is absent —
# generation must precede the runtime suite (318/0/3 with; 298/0/23 without).
Expand Down
9 changes: 9 additions & 0 deletions translator/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@deltic/translator",
"version": "0.1.0",
"exports": { ".": "./mod.ts" },
"tasks": {
"test": "deno test --allow-read tests/",
"check": "deno check mod.ts tests/"
}
}
74 changes: 74 additions & 0 deletions translator/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// @deltic/translator — the packaged translator wasm plus its per-platform
// loader (issue #16 delivery design note, item 3).
//
// Why a separate package: the translator is a versioned peer of the
// runtime (plan-format coupling), so it ships inside the same release —
// but embedders that translate at BUILD time (tools/translate,
// embedder-api A4) deploy no translator at all, and keeping the ~1.85 MB
// asset out of @deltic/runtime keeps their production graphs clean.
//
// The asset (`translator_shim.wasm`, sibling to this module) is copied
// from the cargo build by `just shim` and is gitignored — run `just shim`
// once in a fresh checkout. Publish tooling will pin the exact asset (and
// its digest) into the released package when #16's packaging lands.

import { Translator } from "@deltic/runtime/shim";

let singleton: Promise<Translator> | undefined;

/**
* The packaged translator, loaded lazily and cached for the realm.
*
* Platform paths, in order of preference:
*
* * **Deno** — native wasm-module import: stable, permission-free, and
* delivery/caching ride the module cache. The shim imports nothing,
* so the ESM integration instantiates it trivially and
* `Translator.fromExports` wraps the namespace with no compile and no
* copy. (`buildHash` is unrecoverable from an instance, so the
* artifact cache keys without translator identity on this path —
* see Translator.buildHash.)
* * **Node** — `node:fs` read of the packaged asset (Node's wasm-module
* imports are still experimental; don't build on them).
* * **Browser / workers** — `fetch` of the packaged asset URL (bundlers
* understand the `new URL(…, import.meta.url)` pattern and carry the
* asset).
*
* Pass the result to `instantiate({ componentBytes, translator })`
* (embedder-api A3), or call `.translate()` directly.
*/
export function defaultTranslator(): Promise<Translator> {
return singleton ??= load();
}

async function load(): Promise<Translator> {
const url = new URL("./translator_shim.wasm", import.meta.url);
try {
if (typeof Deno !== "undefined") {
// String-literal dynamic import: statically analyzable, so the wasm
// rides the module graph permission-free; still lazy, and non-Deno
// platforms never evaluate the Deno-only module (see its header).
const { ns } = await import("./shim_asset_deno.ts");
return Translator.fromExports(ns);
}
const proc = (globalThis as { process?: { versions?: { node?: string } } })
.process;
if (proc?.versions?.node) {
const { readFile } = await import("node:fs/promises");
return await Translator.create(new Uint8Array(await readFile(url)));
}
const res = await fetch(url);
if (!res.ok) {
throw new Error(`fetching the translator asset failed: ${res.status}`);
}
return await Translator.create(new Uint8Array(await res.arrayBuffer()));
} catch (e) {
// The overwhelmingly likely in-repo cause is the missing gitignored
// asset; say so instead of leaking a bare module-resolution error.
throw new Error(
`@deltic/translator: could not load ${url}: ${e}\n` +
`(in a repo checkout, run \`just shim\` to build and place the asset)`,
{ cause: e },
);
}
}
12 changes: 12 additions & 0 deletions translator/shim_asset_deno.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// The Deno arm of @deltic/translator, reached from mod.ts via a
// STRING-LITERAL dynamic import: the literal specifier puts this module —
// and the wasm it statically imports — into the statically-analyzable
// module graph, so no read permission is needed (unlike a computed
// `import(url)`, which Deno gates); the dynamic edge keeps it lazy, and
// non-Deno platforms never evaluate it. The static wasm import instantiates
// the zero-import shim under Deno's ESM integration when this module first
// evaluates.

import * as ns from "./translator_shim.wasm";

export { ns };
52 changes: 52 additions & 0 deletions translator/tests/mod_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// defaultTranslator: the packaged loader works (Deno path = native wasm
// import via Translator.fromExports), caches per realm, and translates
// identically to a bytes-built Translator.

import { defaultTranslator } from "../mod.ts";
import { Translator } from "@deltic/runtime/shim";

function assertEq(got: unknown, want: unknown, what: string) {
if (got !== want) throw new Error(`${what}: expected ${want}, got ${got}`);
}

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

const trivial = await maybeRead(
new URL("../../crates/translator-shim/testdata/trivial.wasm", import.meta.url),
);
const asset = await maybeRead(
new URL("../translator_shim.wasm", import.meta.url),
);
const ready = trivial !== null && asset !== null;

Deno.test({
name: "defaultTranslator: loads, translates, and matches a bytes-built Translator",
ignore: !ready,
fn: async () => {
const t = await defaultTranslator();
const reference = await Translator.create(asset!);
assertEq(
t.translateRaw(trivial!),
reference.translateRaw(trivial!),
"envelope equality",
);
},
});

Deno.test({
name: "defaultTranslator: one instance per realm",
ignore: !ready,
fn: async () => {
assertEq(
await defaultTranslator() === await defaultTranslator(),
true,
"singleton identity",
);
},
});
Loading