diff --git a/lib/codegen_deno.ml b/lib/codegen_deno.ml index eb3c779..9d6de7e 100644 --- a/lib/codegen_deno.ml +++ b/lib/codegen_deno.ml @@ -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 @@ -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)); diff --git a/stdlib/Deno.affine b/stdlib/Deno.affine index d0711ca..a69c0b8 100644 --- a/stdlib/Deno.affine +++ b/stdlib/Deno.affine @@ -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;