Skip to main content

pi/
pi_wasm.rs

1//! PiWasm: WebAssembly polyfill for QuickJS runtime.
2//!
3//! Provides `globalThis.WebAssembly` inside QuickJS, backed by wasmtime.
4//! Enables JS extensions to use WebAssembly modules (e.g., Emscripten-compiled
5//! code) even though QuickJS lacks native WebAssembly support.
6//!
7//! Architecture:
8//! - Native Rust functions (`__pi_wasm_*`) handle compile/instantiate/call
9//! - A JS polyfill wraps them into the standard `WebAssembly` namespace
10//! - Memory is synced as ArrayBuffer snapshots (wasmtime → JS) via a getter
11
12use std::cell::RefCell;
13use std::collections::HashMap;
14use std::rc::Rc;
15
16use rquickjs::function::Func;
17use rquickjs::{ArrayBuffer, Ctx, Value};
18use serde::Serialize;
19use tracing::debug;
20use wasmtime::{
21    Caller, Engine, ExternType, Instance as WasmInstance, Linker, Module as WasmModule, Store, Val,
22    ValType,
23};
24
25// ---------------------------------------------------------------------------
26// Bridge state
27// ---------------------------------------------------------------------------
28
29/// Host data stored in each wasmtime `Store`.
30struct WasmHostData {
31    /// Maximum memory pages allowed (enforced on grow).
32    max_memory_pages: u64,
33}
34
35/// Per-instance state: the wasmtime `Store` owns all WASM objects.
36struct InstanceState {
37    store: Store<WasmHostData>,
38    instance: WasmInstance,
39}
40
41#[derive(Serialize)]
42struct WasmExportEntry {
43    name: String,
44    kind: &'static str,
45}
46
47/// Per-JS-runtime WASM bridge state, shared via `Rc<RefCell<>>`.
48pub(crate) struct WasmBridgeState {
49    engine: Engine,
50    modules: HashMap<u32, WasmModule>,
51    instances: HashMap<u32, InstanceState>,
52    next_id: u32,
53    max_modules: usize,
54    max_instances: usize,
55}
56
57impl WasmBridgeState {
58    pub fn new() -> Self {
59        let engine = Engine::default();
60        Self {
61            engine,
62            modules: HashMap::new(),
63            instances: HashMap::new(),
64            next_id: 1,
65            max_modules: DEFAULT_MAX_MODULES,
66            max_instances: DEFAULT_MAX_INSTANCES,
67        }
68    }
69
70    fn alloc_id(&mut self) -> Result<u32, String> {
71        let start = match self.next_id {
72            0 => 1,
73            id if id > MAX_JS_WASM_ID => 1,
74            id => id,
75        };
76        let mut candidate = start;
77
78        loop {
79            if !self.modules.contains_key(&candidate) && !self.instances.contains_key(&candidate) {
80                self.next_id = candidate.wrapping_add(1);
81                if self.next_id == 0 || self.next_id > MAX_JS_WASM_ID {
82                    self.next_id = 1;
83                }
84                return Ok(candidate);
85            }
86
87            candidate = candidate.wrapping_add(1);
88            if candidate == 0 || candidate > MAX_JS_WASM_ID {
89                candidate = 1;
90            }
91            if candidate == start {
92                return Err("WASM instance/module id space exhausted".to_string());
93            }
94        }
95    }
96
97    #[cfg(test)]
98    fn set_limits_for_test(&mut self, max_modules: usize, max_instances: usize) {
99        self.max_modules = max_modules.max(1);
100        self.max_instances = max_instances.max(1);
101    }
102}
103
104// ---------------------------------------------------------------------------
105// Error helpers
106// ---------------------------------------------------------------------------
107
108fn throw_wasm(ctx: &Ctx<'_>, class: &str, msg: &str) -> rquickjs::Error {
109    let text = format!("{class}: {msg}");
110    if let Ok(js_text) = rquickjs::String::from_str(ctx.clone(), &text) {
111        let _ = ctx.throw(js_text.into_value());
112    }
113    rquickjs::Error::Exception
114}
115
116// ---------------------------------------------------------------------------
117// Value conversion: JS ↔ WASM
118// ---------------------------------------------------------------------------
119
120fn extract_bytes(ctx: &Ctx<'_>, value: &Value<'_>) -> rquickjs::Result<Vec<u8>> {
121    // Try ArrayBuffer
122    if let Some(obj) = value.as_object() {
123        if let Some(ab) = obj.as_array_buffer() {
124            return ab
125                .as_bytes()
126                .map(<[u8]>::to_vec)
127                .ok_or_else(|| throw_wasm(ctx, "TypeError", "Detached ArrayBuffer"));
128        }
129    }
130    // Try array of numbers
131    if let Some(arr) = value.as_array() {
132        let mut bytes = Vec::with_capacity(arr.len());
133        for i in 0..arr.len() {
134            let v: i32 = arr.get(i)?;
135            bytes.push(
136                u8::try_from(v)
137                    .map_err(|_| throw_wasm(ctx, "TypeError", "Byte value out of range"))?,
138            );
139        }
140        return Ok(bytes);
141    }
142    Err(throw_wasm(
143        ctx,
144        "TypeError",
145        "Expected ArrayBuffer or byte array",
146    ))
147}
148
149/// Convert a WASM `Val` to an f64 for returning to JS.
150/// Note: i64 is intentionally excluded to avoid silent precision loss.
151#[allow(clippy::cast_precision_loss)]
152fn val_to_f64(ctx: &Ctx<'_>, val: &Val) -> rquickjs::Result<f64> {
153    match val {
154        Val::I32(v) => Ok(f64::from(*v)),
155        Val::F32(bits) => Ok(f64::from(f32::from_bits(*bits))),
156        Val::F64(bits) => Ok(f64::from_bits(*bits)),
157        _ => Err(throw_wasm(
158            ctx,
159            "RuntimeError",
160            "Unsupported WASM return value type for PiJS bridge",
161        )),
162    }
163}
164
165/// Emulate JavaScript `ToInt32` semantics for number -> i32 coercion.
166#[allow(clippy::cast_possible_truncation)]
167fn js_to_i32(value: f64) -> i32 {
168    if !value.is_finite() || value == 0.0 {
169        return 0;
170    }
171
172    let mut wrapped = value.trunc() % TWO_POW_32;
173    if wrapped < 0.0 {
174        wrapped += TWO_POW_32;
175    }
176
177    if wrapped >= TWO_POW_31 {
178        (wrapped - TWO_POW_32) as i32
179    } else {
180        wrapped as i32
181    }
182}
183
184#[allow(clippy::cast_possible_truncation)]
185fn js_to_val(ctx: &Ctx<'_>, value: &Value<'_>, ty: &ValType) -> rquickjs::Result<Val> {
186    match ty {
187        ValType::I32 => {
188            let v: f64 = value
189                .as_number()
190                .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for i32"))?;
191            Ok(Val::I32(js_to_i32(v)))
192        }
193        ValType::I64 => Err(throw_wasm(
194            ctx,
195            "TypeError",
196            "i64 parameters are not supported by PiJS WebAssembly bridge",
197        )),
198        ValType::F32 => {
199            let v: f64 = value
200                .as_number()
201                .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for f32"))?;
202            #[expect(clippy::cast_possible_truncation)]
203            Ok(Val::F32((v as f32).to_bits()))
204        }
205        ValType::F64 => {
206            let v: f64 = value
207                .as_number()
208                .ok_or_else(|| throw_wasm(ctx, "TypeError", "Expected number for f64"))?;
209            Ok(Val::F64(v.to_bits()))
210        }
211        _ => Err(throw_wasm(ctx, "TypeError", "Unsupported WASM value type")),
212    }
213}
214
215fn validate_call_result_types(ctx: &Ctx<'_>, result_types: &[ValType]) -> rquickjs::Result<()> {
216    if result_types.len() > 1 {
217        return Err(throw_wasm(
218            ctx,
219            "RuntimeError",
220            "Multi-value WASM results are not supported by PiJS WebAssembly bridge",
221        ));
222    }
223
224    if let Some(ty) = result_types.first() {
225        return match ty {
226            ValType::I32 | ValType::F32 | ValType::F64 => Ok(()),
227            ValType::I64 => Err(throw_wasm(
228                ctx,
229                "RuntimeError",
230                "i64 results are not supported by PiJS WebAssembly bridge",
231            )),
232            _ => Err(throw_wasm(
233                ctx,
234                "RuntimeError",
235                "Unsupported WASM return type for PiJS WebAssembly bridge",
236            )),
237        };
238    }
239
240    Ok(())
241}
242
243// ---------------------------------------------------------------------------
244// Import stubs
245// ---------------------------------------------------------------------------
246
247/// Register no-op stub functions for every function import the module requires.
248/// Non-function imports are currently skipped (MVP behavior).
249fn register_stub_imports(
250    linker: &mut Linker<WasmHostData>,
251    module: &WasmModule,
252) -> Result<(), String> {
253    for import in module.imports() {
254        let mod_name = import.module();
255        let imp_name = import.name();
256        if let ExternType::Func(func_ty) = import.ty() {
257            let result_types: Vec<ValType> = func_ty.results().collect();
258            linker
259                .func_new(
260                    mod_name,
261                    imp_name,
262                    func_ty.clone(),
263                    move |_caller: Caller<'_, WasmHostData>,
264                          _params: &[Val],
265                          results: &mut [Val]| {
266                        for (i, ty) in result_types.iter().enumerate() {
267                            results[i] = Val::default_for_ty(ty).unwrap_or(Val::I32(0));
268                        }
269                        Ok(())
270                    },
271                )
272                .map_err(|e| format!("Failed to stub import {mod_name}.{imp_name}: {e}"))?;
273        } else {
274            // Non-function imports are currently skipped for MVP.
275        }
276    }
277    Ok(())
278}
279
280// ---------------------------------------------------------------------------
281// Public API: inject globalThis.WebAssembly
282// ---------------------------------------------------------------------------
283
284/// Maximum default memory pages (64 KiB per page → 64 MB).
285const DEFAULT_MAX_MEMORY_PAGES: u64 = 1024;
286/// Hard limit on compiled modules kept alive in one JS runtime.
287const DEFAULT_MAX_MODULES: usize = 256;
288/// Hard limit on instantiated modules kept alive in one JS runtime.
289const DEFAULT_MAX_INSTANCES: usize = 256;
290/// Keep IDs within QuickJS signed-int range for stable JS↔Rust roundtrips.
291const MAX_JS_WASM_ID: u32 = i32::MAX as u32;
292/// JS numeric coercion helpers.
293const TWO_POW_32: f64 = 4_294_967_296.0;
294const TWO_POW_31: f64 = 2_147_483_648.0;
295
296/// Inject `globalThis.WebAssembly` polyfill into the QuickJS context.
297#[allow(clippy::too_many_lines)]
298pub(crate) fn inject_wasm_globals(
299    ctx: &Ctx<'_>,
300    state: &Rc<RefCell<WasmBridgeState>>,
301) -> rquickjs::Result<()> {
302    let global = ctx.globals();
303
304    // ---- __pi_wasm_compile_native(bytes) → module_id ----
305    {
306        let st = Rc::clone(state);
307        global.set(
308            "__pi_wasm_compile_native",
309            Func::from(
310                move |ctx: Ctx<'_>, bytes_val: Value<'_>| -> rquickjs::Result<u32> {
311                    let bytes = extract_bytes(&ctx, &bytes_val)?;
312                    let mut bridge = st.borrow_mut();
313                    if bridge.modules.len() >= bridge.max_modules {
314                        return Err(throw_wasm(
315                            &ctx,
316                            "CompileError",
317                            &format!("Module limit reached ({})", bridge.max_modules),
318                        ));
319                    }
320                    let module = WasmModule::from_binary(&bridge.engine, &bytes)
321                        .map_err(|e| throw_wasm(&ctx, "CompileError", &e.to_string()))?;
322                    let id = bridge
323                        .alloc_id()
324                        .map_err(|e| throw_wasm(&ctx, "CompileError", &e))?;
325                    debug!(module_id = id, bytes_len = bytes.len(), "wasm: compiled");
326                    bridge.modules.insert(id, module);
327                    Ok(id)
328                },
329            ),
330        )?;
331    }
332
333    // ---- __pi_wasm_instantiate_native(module_id) → instance_id ----
334    {
335        let st = Rc::clone(state);
336        global.set(
337            "__pi_wasm_instantiate_native",
338            Func::from(
339                move |ctx: Ctx<'_>, module_id: u32| -> rquickjs::Result<u32> {
340                    let mut bridge = st.borrow_mut();
341                    if bridge.instances.len() >= bridge.max_instances {
342                        return Err(throw_wasm(
343                            &ctx,
344                            "RuntimeError",
345                            &format!("Instance limit reached ({})", bridge.max_instances),
346                        ));
347                    }
348                    let module = bridge
349                        .modules
350                        .get(&module_id)
351                        .ok_or_else(|| throw_wasm(&ctx, "LinkError", "Module not found"))?
352                        .clone();
353
354                    let mut linker = Linker::new(&bridge.engine);
355                    register_stub_imports(&mut linker, &module)
356                        .map_err(|e| throw_wasm(&ctx, "LinkError", &e))?;
357
358                    let mut store = Store::new(
359                        &bridge.engine,
360                        WasmHostData {
361                            max_memory_pages: DEFAULT_MAX_MEMORY_PAGES,
362                        },
363                    );
364                    let instance = linker
365                        .instantiate(&mut store, &module)
366                        .map_err(|e| throw_wasm(&ctx, "LinkError", &e.to_string()))?;
367
368                    let id = bridge
369                        .alloc_id()
370                        .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e))?;
371                    debug!(instance_id = id, module_id, "wasm: instantiated");
372                    bridge
373                        .instances
374                        .insert(id, InstanceState { store, instance });
375                    Ok(id)
376                },
377            ),
378        )?;
379    }
380
381    // ---- __pi_wasm_get_exports_native(instance_id) → JSON string [{name, kind}] ----
382    {
383        let st = Rc::clone(state);
384        global.set(
385            "__pi_wasm_get_exports_native",
386            Func::from(
387                move |ctx: Ctx<'_>, instance_id: u32| -> rquickjs::Result<String> {
388                    let mut bridge = st.borrow_mut();
389                    let inst = bridge
390                        .instances
391                        .get_mut(&instance_id)
392                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
393
394                    let mut entries: Vec<WasmExportEntry> = Vec::new();
395                    for export in inst.instance.exports(&mut inst.store) {
396                        let name = export.name().to_string();
397                        let kind = match export.into_extern() {
398                            wasmtime::Extern::Func(_) => "func",
399                            wasmtime::Extern::Memory(_) => "memory",
400                            wasmtime::Extern::Table(_) => "table",
401                            wasmtime::Extern::Global(_) => "global",
402                            wasmtime::Extern::SharedMemory(_) => "shared-memory",
403                            wasmtime::Extern::Tag(_) => "tag",
404                        };
405                        entries.push(WasmExportEntry { name, kind });
406                    }
407                    serde_json::to_string(&entries)
408                        .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))
409                },
410            ),
411        )?;
412    }
413
414    // ---- __pi_wasm_call_export_native(instance_id, name, args_array) → f64 result ----
415    {
416        let st = Rc::clone(state);
417        global.set(
418            "__pi_wasm_call_export_native",
419            Func::from(
420                move |ctx: Ctx<'_>,
421                      instance_id: u32,
422                      name: String,
423                      args_val: Value<'_>|
424                      -> rquickjs::Result<f64> {
425                    let mut bridge = st.borrow_mut();
426                    let inst = bridge
427                        .instances
428                        .get_mut(&instance_id)
429                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
430
431                    let func = inst
432                        .instance
433                        .get_func(&mut inst.store, &name)
434                        .ok_or_else(|| {
435                            throw_wasm(&ctx, "RuntimeError", &format!("Export '{name}' not found"))
436                        })?;
437
438                    let func_ty = func.ty(&inst.store);
439                    let param_types: Vec<ValType> = func_ty.params().collect();
440                    if param_types.iter().any(|ty| matches!(ty, ValType::I64)) {
441                        return Err(throw_wasm(
442                            &ctx,
443                            "TypeError",
444                            "i64 parameters are not supported by PiJS WebAssembly bridge",
445                        ));
446                    }
447
448                    // Convert JS args to WASM vals
449                    let args_arr = args_val
450                        .as_array()
451                        .ok_or_else(|| throw_wasm(&ctx, "TypeError", "args must be an array"))?;
452                    let mut params = Vec::with_capacity(param_types.len());
453                    for (i, ty) in param_types.iter().enumerate() {
454                        let js_val: Value<'_> = args_arr.get(i)?;
455                        params.push(js_to_val(&ctx, &js_val, ty)?);
456                    }
457
458                    // Allocate results
459                    let result_types: Vec<ValType> = func_ty.results().collect();
460                    validate_call_result_types(&ctx, &result_types)?;
461                    let mut results: Vec<Val> = result_types
462                        .iter()
463                        .map(|ty| Val::default_for_ty(ty).unwrap_or(Val::I32(0)))
464                        .collect();
465
466                    func.call(&mut inst.store, &params, &mut results)
467                        .map_err(|e| throw_wasm(&ctx, "RuntimeError", &e.to_string()))?;
468
469                    // Return first result as f64 (supports i32/f32/f64 only).
470                    results.first().map_or(Ok(0.0), |val| val_to_f64(&ctx, val))
471                },
472            ),
473        )?;
474    }
475
476    // ---- __pi_wasm_get_buffer_native(instance_id, mem_name) → stores ArrayBuffer in global ----
477    {
478        let st = Rc::clone(state);
479        global.set(
480            "__pi_wasm_get_buffer_native",
481            Func::from(
482                move |ctx: Ctx<'_>, instance_id: u32, mem_name: String| -> rquickjs::Result<i32> {
483                    let mut bridge = st.borrow_mut();
484                    let inst = bridge
485                        .instances
486                        .get_mut(&instance_id)
487                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
488                    let memory = inst
489                        .instance
490                        .get_memory(&mut inst.store, &mem_name)
491                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Memory not found"))?;
492                    let data = memory.data(&inst.store);
493                    let len = i32::try_from(data.len()).unwrap_or(i32::MAX);
494                    let buffer = ArrayBuffer::new_copy(ctx.clone(), data)?;
495                    ctx.globals().set("__pi_wasm_tmp_buf", buffer)?;
496                    Ok(len)
497                },
498            ),
499        )?;
500    }
501
502    // ---- __pi_wasm_memory_grow_native(instance_id, mem_name, delta) → prev_pages ----
503    {
504        let st = Rc::clone(state);
505        global.set(
506            "__pi_wasm_memory_grow_native",
507            Func::from(
508                move |ctx: Ctx<'_>,
509                      instance_id: u32,
510                      mem_name: String,
511                      delta: u32|
512                      -> rquickjs::Result<i32> {
513                    let mut bridge = st.borrow_mut();
514                    let inst = bridge
515                        .instances
516                        .get_mut(&instance_id)
517                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
518
519                    // Enforce policy limit
520                    let memory = inst
521                        .instance
522                        .get_memory(&mut inst.store, &mem_name)
523                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Memory not found"))?;
524                    let current = memory.size(&inst.store);
525                    let requested = current.saturating_add(u64::from(delta));
526                    if requested > inst.store.data().max_memory_pages {
527                        return Ok(-1); // growth denied by policy
528                    }
529
530                    Ok(memory
531                        .grow(&mut inst.store, u64::from(delta))
532                        .map_or(-1, |prev| i32::try_from(prev).unwrap_or(-1)))
533                },
534            ),
535        )?;
536    }
537
538    // ---- __pi_wasm_memory_size_native(instance_id, mem_name) → pages ----
539    {
540        let st = Rc::clone(state);
541        global.set(
542            "__pi_wasm_memory_size_native",
543            Func::from(
544                move |ctx: Ctx<'_>, instance_id: u32, mem_name: String| -> rquickjs::Result<u32> {
545                    let mut bridge = st.borrow_mut();
546                    let inst = bridge
547                        .instances
548                        .get_mut(&instance_id)
549                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Instance not found"))?;
550                    let memory = inst
551                        .instance
552                        .get_memory(&mut inst.store, &mem_name)
553                        .ok_or_else(|| throw_wasm(&ctx, "RuntimeError", "Memory not found"))?;
554                    Ok(u32::try_from(memory.size(&inst.store)).unwrap_or(u32::MAX))
555                },
556            ),
557        )?;
558    }
559
560    // ---- Inject the JS polyfill layer ----
561    ctx.eval::<(), _>(WASM_POLYFILL_JS)?;
562
563    debug!("wasm: globalThis.WebAssembly polyfill injected");
564    Ok(())
565}
566
567// ---------------------------------------------------------------------------
568// JS polyfill that wraps the native functions
569// ---------------------------------------------------------------------------
570
571const WASM_POLYFILL_JS: &str = r#"
572(function() {
573  "use strict";
574
575  class CompileError extends Error {
576    constructor(msg) { super(msg); this.name = "CompileError"; }
577  }
578  class LinkError extends Error {
579    constructor(msg) { super(msg); this.name = "LinkError"; }
580  }
581  class RuntimeError extends Error {
582    constructor(msg) { super(msg); this.name = "RuntimeError"; }
583  }
584
585  // Synchronous thenable: behaves like syncResolve() but executes
586  // .then() callbacks immediately. QuickJS doesn't auto-flush microtasks.
587  function syncResolve(value) {
588    return {
589      then: function(resolve, _reject) {
590        try {
591          var r = resolve(value);
592          return syncResolve(r);
593        } catch(e) { return syncReject(e); }
594      },
595      "catch": function() { return syncResolve(value); }
596    };
597  }
598  function syncReject(err) {
599    return {
600      then: function(_resolve, reject) {
601        if (reject) { reject(err); return syncResolve(undefined); }
602        return syncReject(err);
603      },
604      "catch": function(fn) { fn(err); return syncResolve(undefined); }
605    };
606  }
607
608  function normalizeBytes(source) {
609    if (source instanceof ArrayBuffer) {
610      return new Uint8Array(source);
611    }
612    if (ArrayBuffer.isView && ArrayBuffer.isView(source)) {
613      return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
614    }
615    if (Array.isArray(source)) {
616      return new Uint8Array(source);
617    }
618    throw new CompileError("Invalid source: expected ArrayBuffer, TypedArray, or byte array");
619  }
620
621  function buildExports(instanceId) {
622    var info = JSON.parse(__pi_wasm_get_exports_native(instanceId));
623    var exports = {};
624    for (var i = 0; i < info.length; i++) {
625      var exp = info[i];
626      if (exp.kind === "func") {
627        (function(name) {
628          exports[name] = function() {
629            var args = [];
630            for (var j = 0; j < arguments.length; j++) args.push(arguments[j]);
631            return __pi_wasm_call_export_native(instanceId, name, args);
632          };
633        })(exp.name);
634      } else if (exp.kind === "memory") {
635        (function(name) {
636          var memObj = {};
637          Object.defineProperty(memObj, "buffer", {
638            get: function() {
639              __pi_wasm_get_buffer_native(instanceId, name);
640              return globalThis.__pi_wasm_tmp_buf;
641            },
642            configurable: true
643          });
644          memObj.grow = function(delta) {
645            var prevPages = __pi_wasm_memory_grow_native(instanceId, name, delta);
646            if (prevPages < 0) {
647              throw new RangeError("WebAssembly.Memory.grow(): failed to grow memory");
648            }
649            return prevPages;
650          };
651          exports[name] = memObj;
652        })(exp.name);
653      }
654    }
655    return exports;
656  }
657
658  globalThis.WebAssembly = {
659    CompileError: CompileError,
660    LinkError: LinkError,
661    RuntimeError: RuntimeError,
662
663    compile: function(source) {
664      try {
665        var bytes = normalizeBytes(source);
666        var arr = [];
667        for (var i = 0; i < bytes.length; i++) arr.push(bytes[i]);
668        var moduleId = __pi_wasm_compile_native(arr);
669        var wasmMod = { __wasm_module_id: moduleId };
670        return syncResolve(wasmMod);
671      } catch (e) {
672        return syncReject(e);
673      }
674    },
675
676    instantiate: function(source, _imports) {
677      try {
678        var moduleId;
679        if (source && typeof source === "object" && source.__wasm_module_id !== undefined) {
680          moduleId = source.__wasm_module_id;
681        } else {
682          var bytes = normalizeBytes(source);
683          var arr = [];
684          for (var i = 0; i < bytes.length; i++) arr.push(bytes[i]);
685          moduleId = __pi_wasm_compile_native(arr);
686        }
687        var instanceId = __pi_wasm_instantiate_native(moduleId);
688        var exports = buildExports(instanceId);
689        var instance = { exports: exports };
690        var wasmMod = { __wasm_module_id: moduleId };
691
692        if (source && typeof source === "object" && source.__wasm_module_id !== undefined) {
693          return syncResolve(instance);
694        }
695        return syncResolve({ module: wasmMod, instance: instance });
696      } catch (e) {
697        return syncReject(e);
698      }
699    },
700
701    validate: function(_bytes) {
702      throw new Error("WebAssembly.validate not yet supported in PiJS");
703    },
704
705    instantiateStreaming: function() {
706      throw new Error("WebAssembly.instantiateStreaming not supported in PiJS");
707    },
708
709    compileStreaming: function() {
710      throw new Error("WebAssembly.compileStreaming not supported in PiJS");
711    },
712
713    Memory: function(descriptor) {
714      if (!(this instanceof WebAssembly.Memory)) {
715        throw new TypeError("WebAssembly.Memory must be called with new");
716      }
717      var initial = descriptor && descriptor.initial ? descriptor.initial : 0;
718      this._pages = initial;
719      this._buffer = new ArrayBuffer(initial * 65536);
720      Object.defineProperty(this, "buffer", {
721        get: function() { return this._buffer; },
722        configurable: true
723      });
724      this.grow = function(delta) {
725        var old = this._pages;
726        this._pages += delta;
727        this._buffer = new ArrayBuffer(this._pages * 65536);
728        return old;
729      };
730    },
731
732    Table: function() {
733      throw new Error("WebAssembly.Table not yet supported in PiJS");
734    },
735
736    Global: function() {
737      throw new Error("WebAssembly.Global not yet supported in PiJS");
738    }
739  };
740})();
741"#;
742
743// ---------------------------------------------------------------------------
744// Tests
745// ---------------------------------------------------------------------------
746
747#[cfg(test)]
748mod tests {
749    use super::*;
750
751    /// Helper: create a QuickJS runtime, inject WASM globals, and run a test.
752    fn run_wasm_test(f: impl FnOnce(&Ctx<'_>, Rc<RefCell<WasmBridgeState>>)) {
753        let rt = rquickjs::Runtime::new().expect("create runtime");
754        let ctx = rquickjs::Context::full(&rt).expect("create context");
755        ctx.with(|ctx| {
756            let state = Rc::new(RefCell::new(WasmBridgeState::new()));
757            inject_wasm_globals(&ctx, &state).expect("inject globals");
758            f(&ctx, state);
759        });
760    }
761
762    /// Get raw WASM binary bytes from WAT text.
763    fn wat_to_wasm(wat: &str) -> Vec<u8> {
764        wat::parse_str(wat).expect("parse WAT to WASM binary")
765    }
766
767    #[test]
768    fn js_to_i32_matches_javascript_wrapping_semantics() {
769        assert_eq!(js_to_i32(2_147_483_648.0), -2_147_483_648);
770        assert_eq!(js_to_i32(4_294_967_296.0), 0);
771        assert_eq!(js_to_i32(-2_147_483_649.0), 2_147_483_647);
772        assert_eq!(js_to_i32(-1.9), -1);
773        assert_eq!(js_to_i32(1.9), 1);
774        assert_eq!(js_to_i32(f64::NAN), 0);
775        assert_eq!(js_to_i32(f64::INFINITY), 0);
776        assert_eq!(js_to_i32(f64::NEG_INFINITY), 0);
777    }
778
779    #[test]
780    fn compile_and_instantiate_trivial_module() {
781        let wasm_bytes = wat_to_wasm(
782            r#"(module
783              (func (export "add") (param i32 i32) (result i32)
784                local.get 0 local.get 1 i32.add)
785              (memory (export "memory") 1)
786            )"#,
787        );
788        run_wasm_test(|ctx, _state| {
789            // Store bytes as a JS array
790            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
791            for (i, &b) in wasm_bytes.iter().enumerate() {
792                arr.set(i, i32::from(b)).unwrap();
793            }
794            ctx.globals().set("__test_bytes", arr).unwrap();
795
796            // Compile
797            let module_id: u32 = ctx
798                .eval("__pi_wasm_compile_native(__test_bytes)")
799                .expect("compile");
800            assert!(module_id > 0);
801
802            // Instantiate
803            let instance_id: u32 = ctx
804                .eval(format!("__pi_wasm_instantiate_native({module_id})"))
805                .expect("instantiate");
806            assert!(instance_id > 0);
807        });
808    }
809
810    #[test]
811    fn call_export_add() {
812        let wasm_bytes = wat_to_wasm(
813            r#"(module
814              (func (export "add") (param i32 i32) (result i32)
815                local.get 0 local.get 1 i32.add)
816            )"#,
817        );
818        run_wasm_test(|ctx, _state| {
819            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
820            for (i, &b) in wasm_bytes.iter().enumerate() {
821                arr.set(i, i32::from(b)).unwrap();
822            }
823            ctx.globals().set("__test_bytes", arr).unwrap();
824
825            let result: i32 = ctx
826                .eval(
827                    r#"
828                    var mid = __pi_wasm_compile_native(__test_bytes);
829                    var iid = __pi_wasm_instantiate_native(mid);
830                    __pi_wasm_call_export_native(iid, "add", [3, 4]);
831                "#,
832                )
833                .expect("call add");
834            assert_eq!(result, 7);
835        });
836    }
837
838    #[test]
839    fn call_export_multiply() {
840        let wasm_bytes = wat_to_wasm(
841            r#"(module
842              (func (export "mul") (param i32 i32) (result i32)
843                local.get 0 local.get 1 i32.mul)
844            )"#,
845        );
846        run_wasm_test(|ctx, _state| {
847            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
848            for (i, &b) in wasm_bytes.iter().enumerate() {
849                arr.set(i, i32::from(b)).unwrap();
850            }
851            ctx.globals().set("__test_bytes", arr).unwrap();
852
853            let result: i32 = ctx
854                .eval(
855                    r#"
856                    var mid = __pi_wasm_compile_native(__test_bytes);
857                    var iid = __pi_wasm_instantiate_native(mid);
858                    __pi_wasm_call_export_native(iid, "mul", [6, 7]);
859                "#,
860                )
861                .expect("call mul");
862            assert_eq!(result, 42);
863        });
864    }
865
866    #[test]
867    fn get_exports_lists_func_and_memory() {
868        let wasm_bytes = wat_to_wasm(
869            r#"(module
870              (func (export "f1") (result i32) i32.const 1)
871              (func (export "f2") (param i32) (result i32) local.get 0)
872              (memory (export "mem") 2)
873            )"#,
874        );
875        run_wasm_test(|ctx, _state| {
876            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
877            for (i, &b) in wasm_bytes.iter().enumerate() {
878                arr.set(i, i32::from(b)).unwrap();
879            }
880            ctx.globals().set("__test_bytes", arr).unwrap();
881
882            let count: i32 = ctx
883                .eval(
884                    r"
885                    var mid = __pi_wasm_compile_native(__test_bytes);
886                    var iid = __pi_wasm_instantiate_native(mid);
887                    var exps = JSON.parse(__pi_wasm_get_exports_native(iid));
888                    exps.length;
889                ",
890                )
891                .expect("get exports count");
892            assert_eq!(count, 3);
893        });
894    }
895
896    #[test]
897    fn get_exports_json_handles_escaped_names() {
898        let wasm_bytes = wat_to_wasm(
899            r#"(module
900              (func (export "name\"with_quote") (result i32) i32.const 1)
901            )"#,
902        );
903        run_wasm_test(|ctx, _state| {
904            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
905            for (i, &b) in wasm_bytes.iter().enumerate() {
906                arr.set(i, i32::from(b)).unwrap();
907            }
908            ctx.globals().set("__test_bytes", arr).unwrap();
909
910            let name: String = ctx
911                .eval(
912                    r"
913                    var mid = __pi_wasm_compile_native(__test_bytes);
914                    var iid = __pi_wasm_instantiate_native(mid);
915                    JSON.parse(__pi_wasm_get_exports_native(iid))[0].name;
916                ",
917                )
918                .expect("parse export JSON");
919            assert_eq!(name, "name\"with_quote");
920        });
921    }
922
923    #[test]
924    fn memory_buffer_returns_arraybuffer() {
925        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
926        run_wasm_test(|ctx, _state| {
927            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
928            for (i, &b) in wasm_bytes.iter().enumerate() {
929                arr.set(i, i32::from(b)).unwrap();
930            }
931            ctx.globals().set("__test_bytes", arr).unwrap();
932
933            let size: i32 = ctx
934                .eval(
935                    r#"
936                    var mid = __pi_wasm_compile_native(__test_bytes);
937                    var iid = __pi_wasm_instantiate_native(mid);
938                    var len = __pi_wasm_get_buffer_native(iid, "memory");
939                    len;
940                "#,
941                )
942                .expect("get buffer size");
943            // 1 page = 64 KiB = 65536 bytes
944            assert_eq!(size, 65536);
945
946            // Verify the ArrayBuffer was stored in the global
947            let buf_size: i32 = ctx
948                .eval("__pi_wasm_tmp_buf.byteLength")
949                .expect("tmp buffer size");
950            assert_eq!(buf_size, 65536);
951        });
952    }
953
954    #[test]
955    fn memory_grow_succeeds() {
956        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 10))"#);
957        run_wasm_test(|ctx, _state| {
958            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
959            for (i, &b) in wasm_bytes.iter().enumerate() {
960                arr.set(i, i32::from(b)).unwrap();
961            }
962            ctx.globals().set("__test_bytes", arr).unwrap();
963
964            let prev: i32 = ctx
965                .eval(
966                    r#"
967                    var mid = __pi_wasm_compile_native(__test_bytes);
968                    var iid = __pi_wasm_instantiate_native(mid);
969                    __pi_wasm_memory_grow_native(iid, "memory", 2);
970                "#,
971                )
972                .expect("grow memory");
973            // Previous size was 1 page
974            assert_eq!(prev, 1);
975
976            let new_size: i32 = ctx
977                .eval(r#"__pi_wasm_memory_size_native(iid, "memory")"#)
978                .expect("memory size");
979            assert_eq!(new_size, 3);
980        });
981    }
982
983    #[test]
984    fn memory_grow_denied_by_policy() {
985        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
986        run_wasm_test(|ctx, state| {
987            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
988            for (i, &b) in wasm_bytes.iter().enumerate() {
989                arr.set(i, i32::from(b)).unwrap();
990            }
991            ctx.globals().set("__test_bytes", arr).unwrap();
992
993            let instance_id: u32 = ctx
994                .eval(
995                    r"
996                    var mid = __pi_wasm_compile_native(__test_bytes);
997                    __pi_wasm_instantiate_native(mid);
998                ",
999                )
1000                .expect("instantiate");
1001
1002            // Reduce max pages to 2 in the instance's store
1003            {
1004                let mut bridge = state.borrow_mut();
1005                let inst = bridge.instances.get_mut(&instance_id).unwrap();
1006                inst.store.data_mut().max_memory_pages = 2;
1007            }
1008
1009            // Try to grow by 5 pages → should be denied (1 + 5 > 2)
1010            let result: i32 = ctx
1011                .eval(format!(
1012                    "__pi_wasm_memory_grow_native({instance_id}, 'memory', 5)"
1013                ))
1014                .expect("grow denied");
1015            assert_eq!(result, -1);
1016        });
1017    }
1018
1019    #[test]
1020    fn compile_invalid_bytes_fails() {
1021        run_wasm_test(|ctx, _state| {
1022            let result: rquickjs::Result<u32> = ctx.eval("__pi_wasm_compile_native([0, 1, 2, 3])");
1023            assert!(result.is_err());
1024        });
1025    }
1026
1027    #[test]
1028    fn instantiate_nonexistent_module_fails() {
1029        run_wasm_test(|ctx, _state| {
1030            let result: rquickjs::Result<u32> = ctx.eval("__pi_wasm_instantiate_native(99999)");
1031            assert!(result.is_err());
1032        });
1033    }
1034
1035    #[test]
1036    fn compile_rejects_when_module_limit_reached() {
1037        let wasm_bytes = wat_to_wasm(r"(module)");
1038        run_wasm_test(|ctx, state| {
1039            state.borrow_mut().set_limits_for_test(1, 8);
1040
1041            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1042            for (i, &b) in wasm_bytes.iter().enumerate() {
1043                arr.set(i, i32::from(b)).unwrap();
1044            }
1045            ctx.globals().set("__test_bytes", arr).unwrap();
1046
1047            let first: u32 = ctx
1048                .eval("__pi_wasm_compile_native(__test_bytes)")
1049                .expect("first compile");
1050            assert!(first > 0);
1051
1052            let second: rquickjs::Result<u32> = ctx.eval("__pi_wasm_compile_native(__test_bytes)");
1053            assert!(second.is_err());
1054        });
1055    }
1056
1057    #[test]
1058    fn instantiate_rejects_when_instance_limit_reached() {
1059        let wasm_bytes = wat_to_wasm(r"(module)");
1060        run_wasm_test(|ctx, state| {
1061            state.borrow_mut().set_limits_for_test(8, 1);
1062
1063            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1064            for (i, &b) in wasm_bytes.iter().enumerate() {
1065                arr.set(i, i32::from(b)).unwrap();
1066            }
1067            ctx.globals().set("__test_bytes", arr).unwrap();
1068
1069            let module_id: u32 = ctx
1070                .eval("__pi_wasm_compile_native(__test_bytes)")
1071                .expect("compile");
1072
1073            let first: u32 = ctx
1074                .eval(format!("__pi_wasm_instantiate_native({module_id})"))
1075                .expect("first instantiate");
1076            assert!(first > 0);
1077
1078            let second: rquickjs::Result<u32> =
1079                ctx.eval(format!("__pi_wasm_instantiate_native({module_id})"));
1080            assert!(second.is_err());
1081        });
1082    }
1083
1084    #[test]
1085    fn alloc_id_skips_zero_on_wrap() {
1086        let wasm_bytes = wat_to_wasm(r"(module)");
1087        run_wasm_test(|ctx, state| {
1088            {
1089                let mut bridge = state.borrow_mut();
1090                bridge.set_limits_for_test(8, 8);
1091                bridge.next_id = MAX_JS_WASM_ID;
1092            }
1093
1094            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1095            for (i, &b) in wasm_bytes.iter().enumerate() {
1096                arr.set(i, i32::from(b)).unwrap();
1097            }
1098            ctx.globals().set("__test_bytes", arr).unwrap();
1099
1100            let first: i32 = ctx
1101                .eval("__pi_wasm_compile_native(__test_bytes)")
1102                .expect("first compile");
1103            let second: i32 = ctx
1104                .eval("__pi_wasm_compile_native(__test_bytes)")
1105                .expect("second compile");
1106
1107            assert_eq!(first, i32::MAX);
1108            assert_eq!(second, 1);
1109        });
1110    }
1111
1112    #[test]
1113    fn call_nonexistent_export_fails() {
1114        let wasm_bytes = wat_to_wasm(r#"(module (func (export "f") (result i32) i32.const 1))"#);
1115        run_wasm_test(|ctx, _state| {
1116            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1117            for (i, &b) in wasm_bytes.iter().enumerate() {
1118                arr.set(i, i32::from(b)).unwrap();
1119            }
1120            ctx.globals().set("__test_bytes", arr).unwrap();
1121
1122            let result: rquickjs::Result<i32> = ctx.eval(
1123                r#"
1124                var mid = __pi_wasm_compile_native(__test_bytes);
1125                var iid = __pi_wasm_instantiate_native(mid);
1126                __pi_wasm_call_export_native(iid, "nonexistent", []);
1127            "#,
1128            );
1129            assert!(result.is_err());
1130        });
1131    }
1132
1133    #[test]
1134    fn call_export_i64_param_is_rejected() {
1135        let wasm_bytes = wat_to_wasm(
1136            r#"(module
1137              (func (export "id64") (param i64) (result i64)
1138                local.get 0)
1139            )"#,
1140        );
1141        run_wasm_test(|ctx, _state| {
1142            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1143            for (i, &b) in wasm_bytes.iter().enumerate() {
1144                arr.set(i, i32::from(b)).unwrap();
1145            }
1146            ctx.globals().set("__test_bytes", arr).unwrap();
1147
1148            let result: rquickjs::Result<i32> = ctx.eval(
1149                r#"
1150                var mid = __pi_wasm_compile_native(__test_bytes);
1151                var iid = __pi_wasm_instantiate_native(mid);
1152                __pi_wasm_call_export_native(iid, "id64", [1]);
1153            "#,
1154            );
1155            assert!(result.is_err());
1156        });
1157    }
1158
1159    #[test]
1160    fn call_export_i64_result_is_rejected() {
1161        let wasm_bytes = wat_to_wasm(
1162            r#"(module
1163              (func (export "ret64") (result i64)
1164                i64.const 42)
1165            )"#,
1166        );
1167        run_wasm_test(|ctx, _state| {
1168            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1169            for (i, &b) in wasm_bytes.iter().enumerate() {
1170                arr.set(i, i32::from(b)).unwrap();
1171            }
1172            ctx.globals().set("__test_bytes", arr).unwrap();
1173
1174            let result: rquickjs::Result<i32> = ctx.eval(
1175                r#"
1176                var mid = __pi_wasm_compile_native(__test_bytes);
1177                var iid = __pi_wasm_instantiate_native(mid);
1178                __pi_wasm_call_export_native(iid, "ret64", []);
1179            "#,
1180            );
1181            assert!(result.is_err());
1182        });
1183    }
1184
1185    #[test]
1186    fn call_export_multivalue_result_is_rejected() {
1187        let wasm_bytes = wat_to_wasm(
1188            r#"(module
1189              (func (export "pair") (result i32 i32)
1190                i32.const 1
1191                i32.const 2)
1192            )"#,
1193        );
1194        run_wasm_test(|ctx, _state| {
1195            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1196            for (i, &b) in wasm_bytes.iter().enumerate() {
1197                arr.set(i, i32::from(b)).unwrap();
1198            }
1199            ctx.globals().set("__test_bytes", arr).unwrap();
1200
1201            let result: rquickjs::Result<i32> = ctx.eval(
1202                r#"
1203                var mid = __pi_wasm_compile_native(__test_bytes);
1204                var iid = __pi_wasm_instantiate_native(mid);
1205                __pi_wasm_call_export_native(iid, "pair", []);
1206            "#,
1207            );
1208            assert!(result.is_err());
1209        });
1210    }
1211
1212    #[test]
1213    fn call_export_externref_result_is_rejected() {
1214        let wasm_bytes = wat_to_wasm(
1215            r#"(module
1216              (func (export "retref") (result externref)
1217                ref.null extern)
1218            )"#,
1219        );
1220        run_wasm_test(|ctx, _state| {
1221            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1222            for (i, &b) in wasm_bytes.iter().enumerate() {
1223                arr.set(i, i32::from(b)).unwrap();
1224            }
1225            ctx.globals().set("__test_bytes", arr).unwrap();
1226
1227            let result: rquickjs::Result<i32> = ctx.eval(
1228                r#"
1229                var mid = __pi_wasm_compile_native(__test_bytes);
1230                var iid = __pi_wasm_instantiate_native(mid);
1231                __pi_wasm_call_export_native(iid, "retref", []);
1232            "#,
1233            );
1234            assert!(result.is_err());
1235        });
1236    }
1237
1238    #[test]
1239    fn js_polyfill_webassembly_instantiate() {
1240        let wasm_bytes = wat_to_wasm(
1241            r#"(module
1242              (func (export "add") (param i32 i32) (result i32)
1243                local.get 0 local.get 1 i32.add)
1244            )"#,
1245        );
1246        run_wasm_test(|ctx, _state| {
1247            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1248            for (i, &b) in wasm_bytes.iter().enumerate() {
1249                arr.set(i, i32::from(b)).unwrap();
1250            }
1251            ctx.globals().set("__test_bytes", arr).unwrap();
1252
1253            // Use the full JS polyfill API (synchronous for QuickJS)
1254            let has_wa: bool = ctx
1255                .eval("typeof globalThis.WebAssembly !== 'undefined'")
1256                .expect("check WebAssembly");
1257            assert!(has_wa);
1258
1259            // WebAssembly.instantiate returns a Promise; in QuickJS we can
1260            // resolve it synchronously via .then()
1261            let result: i32 = ctx
1262                .eval(
1263                    r"
1264                    var __test_result = -1;
1265                    WebAssembly.instantiate(__test_bytes).then(function(r) {
1266                        __test_result = r.instance.exports.add(10, 20);
1267                    });
1268                    __test_result;
1269                ",
1270                )
1271                .expect("polyfill instantiate");
1272            assert_eq!(result, 30);
1273        });
1274    }
1275
1276    #[test]
1277    fn js_polyfill_memory_buffer_getter() {
1278        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1))"#);
1279        run_wasm_test(|ctx, _state| {
1280            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1281            for (i, &b) in wasm_bytes.iter().enumerate() {
1282                arr.set(i, i32::from(b)).unwrap();
1283            }
1284            ctx.globals().set("__test_bytes", arr).unwrap();
1285
1286            let size: i32 = ctx
1287                .eval(
1288                    r"
1289                    var __test_size = -1;
1290                    WebAssembly.instantiate(__test_bytes).then(function(r) {
1291                        __test_size = r.instance.exports.memory.buffer.byteLength;
1292                    });
1293                    __test_size;
1294                ",
1295                )
1296                .expect("polyfill memory buffer");
1297            assert_eq!(size, 65536);
1298        });
1299    }
1300
1301    #[test]
1302    fn js_polyfill_memory_grow_returns_previous_pages() {
1303        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 10))"#);
1304        run_wasm_test(|ctx, _state| {
1305            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1306            for (i, &b) in wasm_bytes.iter().enumerate() {
1307                arr.set(i, i32::from(b)).unwrap();
1308            }
1309            ctx.globals().set("__test_bytes", arr).unwrap();
1310
1311            let prev_pages: i32 = ctx
1312                .eval(
1313                    r"
1314                    var __test_prev = -1;
1315                    WebAssembly.instantiate(__test_bytes).then(function(r) {
1316                        __test_prev = r.instance.exports.memory.grow(2);
1317                    });
1318                    __test_prev;
1319                ",
1320                )
1321                .expect("polyfill memory grow");
1322            assert_eq!(prev_pages, 1);
1323
1324            let new_size: i32 = ctx
1325                .eval(
1326                    r"
1327                    var __test_size = -1;
1328                    WebAssembly.instantiate(__test_bytes).then(function(r) {
1329                        r.instance.exports.memory.grow(2);
1330                        __test_size = r.instance.exports.memory.buffer.byteLength;
1331                    });
1332                    __test_size;
1333                ",
1334                )
1335                .expect("polyfill memory size after grow");
1336            assert_eq!(new_size, 3 * 65536);
1337        });
1338    }
1339
1340    #[test]
1341    fn js_polyfill_memory_grow_failure_throws_range_error() {
1342        let wasm_bytes = wat_to_wasm(r#"(module (memory (export "memory") 1 1))"#);
1343        run_wasm_test(|ctx, _state| {
1344            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1345            for (i, &b) in wasm_bytes.iter().enumerate() {
1346                arr.set(i, i32::from(b)).unwrap();
1347            }
1348            ctx.globals().set("__test_bytes", arr).unwrap();
1349
1350            let threw_range_error: bool = ctx
1351                .eval(
1352                    r"
1353                    var __threw_range_error = false;
1354                    WebAssembly.instantiate(__test_bytes).then(function(r) {
1355                        try {
1356                            r.instance.exports.memory.grow(1);
1357                        } catch (e) {
1358                            __threw_range_error = e instanceof RangeError;
1359                        }
1360                    });
1361                    __threw_range_error;
1362                ",
1363                )
1364                .expect("polyfill memory grow failure");
1365            assert!(threw_range_error);
1366        });
1367    }
1368
1369    #[test]
1370    fn module_with_imports_instantiates_with_stubs() {
1371        let wasm_bytes = wat_to_wasm(
1372            r#"(module
1373              (import "env" "log" (func (param i32)))
1374              (func (export "run") (result i32)
1375                i32.const 42
1376                call 0
1377                i32.const 1)
1378            )"#,
1379        );
1380        run_wasm_test(|ctx, _state| {
1381            let arr = rquickjs::Array::new(ctx.clone()).unwrap();
1382            for (i, &b) in wasm_bytes.iter().enumerate() {
1383                arr.set(i, i32::from(b)).unwrap();
1384            }
1385            ctx.globals().set("__test_bytes", arr).unwrap();
1386
1387            let result: i32 = ctx
1388                .eval(
1389                    r#"
1390                    var mid = __pi_wasm_compile_native(__test_bytes);
1391                    var iid = __pi_wasm_instantiate_native(mid);
1392                    __pi_wasm_call_export_native(iid, "run", []);
1393                "#,
1394                )
1395                .expect("call with import stubs");
1396            assert_eq!(result, 1);
1397        });
1398    }
1399}