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
47 changes: 47 additions & 0 deletions lib/codegen_deno.ml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,44 @@ const __as_regexMatch = (s, pat) => new RegExp(pat).test(String(s));
const __as_wasmInstance = (bytes) =>
new WebAssembly.Instance(new WebAssembly.Module(bytes)).exports;
const __as_wasmCall = (exports, name, args) => Number(exports[name](...(args || [])));
// ---- WasmValue (Deno.affine #455 — Tier 1 #5, Option B) ----
// Opaque tagged value crossing the AS/JS boundary as `{ kind, v }`.
// `kind` is one of "i32" | "i64" | "f32" | "f64". The `v` payload is
// `BigInt` for i64 (preserves precision beyond 2^53), `Number` otherwise.
const __as_wv_i32 = (n) => ({ kind: "i32", v: (Number(n) | 0) });
const __as_wv_i64 = (n) => ({ kind: "i64", v: BigInt(n) });
const __as_wv_f32 = (f) => ({ kind: "f32", v: Math.fround(Number(f)) });
const __as_wv_f64 = (f) => ({ kind: "f64", v: Number(f) });
const __as_wv_as_int = (v) => {
if (v == null) return 0;
if (typeof v.v === "bigint") {
// i64: truncate to safe-integer Number; caller's responsibility for
// precision-sensitive paths (use wv_kind to detect).
return Number(v.v);
}
// i32 / f32 / f64: truncate toward zero per AS Int semantics.
return (Number(v.v) | 0);
};
const __as_wv_as_float = (v) => {
if (v == null) return 0;
return typeof v.v === "bigint" ? Number(v.v) : Number(v.v);
};
const __as_wv_kind = (v) => (v && typeof v.kind === "string") ? v.kind : "";
const __as_wasm_export_call = (exports, name, args) => {
// Unmarshal AS-side [WasmValue] to raw JS scalars for the wasm call.
const rawArgs = (args || []).map((wv) => {
if (wv == null) return 0;
// i64 payload is BigInt; wasm i64 imports accept BigInt directly.
return wv.v;
});
const result = exports[name](...rawArgs);
// Wrap return as f64 (lossless for any numeric; callers expecting i32/i64
// can rebuild via wv_i32(wv_as_int(result)) or inspect wv_kind).
if (typeof result === "bigint") {
return { kind: "i64", v: result };
}
return { kind: "f64", v: Number(result) };
};
// ---- motion (bindings #4): consumer-provided import ----
// Host JS environment must expose globalThis.__as_motion (the motion
// library or a compatible mock). Tests set it in the harness before
Expand Down Expand Up @@ -431,6 +469,15 @@ let () =
b "regexMatch" (fun a -> Printf.sprintf "__as_regexMatch(%s, %s)" (arg 0 a) (arg 1 a));
b "wasmInstance" (fun a -> Printf.sprintf "__as_wasmInstance(%s)" (arg 0 a));
b "wasmCall" (fun a -> Printf.sprintf "__as_wasmCall(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
(* WasmValue constructors / accessors / typed export call — #455. *)
b "wv_i32" (fun a -> Printf.sprintf "__as_wv_i32(%s)" (arg 0 a));
b "wv_i64" (fun a -> Printf.sprintf "__as_wv_i64(%s)" (arg 0 a));
b "wv_f32" (fun a -> Printf.sprintf "__as_wv_f32(%s)" (arg 0 a));
b "wv_f64" (fun a -> Printf.sprintf "__as_wv_f64(%s)" (arg 0 a));
b "wv_as_int" (fun a -> Printf.sprintf "__as_wv_as_int(%s)" (arg 0 a));
b "wv_as_float" (fun a -> Printf.sprintf "__as_wv_as_float(%s)" (arg 0 a));
b "wv_kind" (fun a -> Printf.sprintf "__as_wv_kind(%s)" (arg 0 a));
b "wasm_export_call" (fun a -> Printf.sprintf "__as_wasm_export_call(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
(* ---- motion (bindings #4) ---- *)
b "motionAnimate" (fun a -> Printf.sprintf "__as_motionAnimate(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
b "motionAwait" (fun a -> Printf.sprintf "(await __as_motionAwait(%s))" (arg 0 a));
Expand Down
87 changes: 87 additions & 0 deletions stdlib/Deno.affine
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,93 @@ pub extern fn wasmInstance(bytes: Bytes) -> WasmExports;
/// };
pub extern fn wasmCall(exports: WasmExports, name: String, args: [Float]) -> Float;

// ── WebAssembly typed export call (#455 — Tier 1 #5, Option B) ────
//
// Generic `wasm_export_call` covering any wasm signature including i64,
// multi-typed args, future spec additions. Future-proof: no binding
// change required as wasm evolves. Tiny addition vs Option A's ~30
// per-signature variants. Typed wrappers can be layered on top of this
// generic as ergonomic helpers in a follow-up sub-issue.
//
// Trade-off: weaker static safety at the call site — user marshals
// manually via the `wv_*` constructors and reads via `wv_as_*`. Errors
// (wrong arity, missing export, type mismatch) deferred to runtime per
// owner's accepted trade-off in #455 comment.
//
// **Encoding decision (2026-05-30):** `WasmValue` lands as an OPAQUE
// extern type rather than a true AffineScript sum type. Rationale:
// the JS interop boundary needs a hand-written marshaller that pairs
// `wv_i32(42) -> { tag: "i32", v: 42 }` with the export-call dispatch
// `__as_wasm_export_call(exports, name, args)`. Mirrors the existing
// `WasmExports` opaque pattern. A true sum-type variant on top of this
// opaque base ships in a follow-up once `json.affine`-style tagged-
// variant codegen lands for the Deno-ESM backend.

/// Opaque wasm scalar value. Constructed via `wv_i32` / `wv_i64` /
/// `wv_f32` / `wv_f64`. Read via `wv_as_int` (i32/i64 → Int) or
/// `wv_as_float` (f32/f64 → Float). The kind tag is opaque to AS code
/// but inspectable host-side via `wv_kind` for diagnostics.
pub extern type WasmValue;

/// Wrap an `Int` as a wasm i32. Truncates to the low 32 bits at the
/// host boundary if `n` exceeds the i32 range.
pub extern fn wv_i32(n: Int) -> WasmValue;

/// Wrap an `Int` as a wasm i64. Crosses the boundary as a `BigInt`
/// host-side. Values outside the safe-integer range (>= 2^53) are
/// preserved as BigInt; arithmetic on the AS side that goes through
/// `wv_as_int` truncates to the safe-integer range.
pub extern fn wv_i64(n: Int) -> WasmValue;

/// Wrap a `Float` as a wasm f32. Rounded to f32 precision via
/// `Math.fround` at the host boundary.
pub extern fn wv_f32(f: Float) -> WasmValue;

/// Wrap a `Float` as a wasm f64. Preserved at full f64 precision.
pub extern fn wv_f64(f: Float) -> WasmValue;

/// Read a wasm scalar back as `Int`. Defined for both i32 and i64
/// variants. For f32/f64, truncates toward zero. Caller is responsible
/// for knowing the variant — there is no runtime check; reading the
/// wrong kind silently coerces.
pub extern fn wv_as_int(v: WasmValue) -> Int;

/// Read a wasm scalar back as `Float`. Defined for both f32 and f64
/// variants. For i32/i64, converts via JS `Number()` — i64 values
/// beyond 2^53 lose precision; caller can detect via `wv_kind`.
pub extern fn wv_as_float(v: WasmValue) -> Float;

/// Return the kind tag ("i32" / "i64" / "f32" / "f64") for runtime
/// dispatch when the AS-side caller doesn't statically know the
/// variant. Use sparingly — the typed `wv_as_*` accessors should be
/// the default path.
pub extern fn wv_kind(v: WasmValue) -> String;

/// `exports[name](...args)` with typed `WasmValue` marshalling.
/// Returns a `WasmValue` wrapping the export's return — kind is `f64`
/// by default (the lossless choice for any numeric return); callers
/// expecting i32/i64 should rebuild via `wv_i32(wv_as_int(result))`
/// or inspect `wv_kind` host-side. Multi-value returns are out of
/// scope at this binding — add a `wasm_export_call_multi` extern when
/// needed.
///
/// Example:
///
/// use Deno::{
/// Bytes, WasmExports, wasmInstance, wasm_export_call,
/// wv_i32, wv_as_int,
/// };
///
/// pub fn addI32ViaWasm(bytes: Bytes, a: Int, b: Int) -> Int {
/// let exports = wasmInstance(bytes);
/// let result = wasm_export_call(
/// exports, "add", [wv_i32(a), wv_i32(b)]);
/// wv_as_int(result)
/// }
pub extern fn wasm_export_call(
exports: WasmExports, name: String, args: [WasmValue]
) -> WasmValue;

// ── Array helper ───────────────────────────────────────────────────
//
// AffineScript has no mutable-array push primitive in this subset;
Expand Down
Loading