Repository: RustPython/RustPython (language: rust)

Issue:
Make rustpython-vm compatible with non-js wasm32-unknown & add tests
Ideally the test crate shouldn't take long to compile, since it's the same target with only a few crates configured differently.



Discussion / hints:
Hmm, is there a reason we need to have wasm32-unknown-unknown checked in exotic_targets when we have the wasm job? Is that a holdover from before we had the wasm job?
I recently added it there without specific reason. Do you think it belongs to wasm job? That makes sense.
As far as I can tell compilation for the target is already tested fully on the wasm job, so it's redundant in exotic_targets.
I added the build to prevent the target's build error.

wasm build job was not fully covering the top-most crate rustpython build
See #3634
Oh, I see. But that's not supported anyway, right? You can't compile a cli application to wasm w/ js-interop, there's no syscalls or argv or anything.
there was wasm32-unknown use case without js interop.
the top crate is now more useful than vm because user must manually construct interpreter using vm and stdlib since we seprated stdlib crate. `rustpython` crate suggests a convenient way to construct one without knowing the details.


cc @lastmjs

Fix patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index bec7eafa32..0f39ed8343 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -216,13 +216,6 @@ jobs:
       - name: Check compilation for freebsd
         run: cargo check --target x86_64-unknown-freebsd
 
-      - uses: dtolnay/rust-toolchain@stable
-        with:
-          target: wasm32-unknown-unknown
-
-      - name: Check compilation for wasm32
-        run: cargo check --target wasm32-unknown-unknown --no-default-features
-
       - uses: dtolnay/rust-toolchain@stable
         with:
           target: x86_64-unknown-freebsd
@@ -380,6 +373,14 @@ jobs:
         env:
           NODE_OPTIONS: "--openssl-legacy-provider"
         working-directory: ./wasm/demo
+      - uses: mwilliamson/setup-wabt-action@v1
+        with: { wabt-version: "1.0.30" }
+      - name: check wasm32-unknown without js
+        run: |
+          cargo build --release --manifest-path wasm/wasm-unknown-test/Cargo.toml --target wasm32-unknown-unknown --verbose
+          if wasm-objdump -xj Import target/wasm32-unknown-unknown/release/wasm_unknown_test.wasm; then
+            echo "ERROR: wasm32-unknown module expects imports from the host environment" >2
+          fi
       - name: build notebook demo
         if: github.ref == 'refs/heads/release'
         run: |
diff --git a/Cargo.toml b/Cargo.toml
index eec5d84a8f..283698f52c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -96,7 +96,8 @@ x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md", dev-dependencies =
 resolver = "2"
 members = [
     "compiler", "compiler/core", "compiler/codegen",
-    ".", "common", "derive", "jit", "vm", "vm/sre_engine", "pylib", "stdlib", "wasm/lib", "derive-impl",
+    ".", "common", "derive", "jit", "vm", "vm/sre_engine", "pylib", "stdlib", "derive-impl",
+    "wasm/lib",
 ]
 
 [workspace.package]
@@ -146,6 +147,7 @@ cfg-if = "1.0"
 chrono = "0.4.37"
 crossbeam-utils = "0.8.19"
 flame = "0.2.2"
+getrandom = "0.2.12"
 glob = "0.3"
 hex = "0.4.3"
 indexmap = { version = "2.2.6", features = ["std"] }
@@ -178,6 +180,7 @@ thread_local = "1.1.4"
 unicode_names2 = "1.2.0"
 widestring = "1.1.0"
 windows-sys = "0.52.0"
+wasm-bindgen = "0.2.92"
 
 # Lints
 
diff --git a/stdlib/Cargo.toml b/stdlib/Cargo.toml
index 485855d646..1e327c4e9d 100644
--- a/stdlib/Cargo.toml
+++ b/stdlib/Cargo.toml
@@ -22,7 +22,7 @@ ssl-vendor = ["ssl", "openssl/vendored", "openssl-probe"]
 [dependencies]
 # rustpython crates
 rustpython-derive = { workspace = true }
-rustpython-vm = { workspace = true }
+rustpython-vm = { workspace = true, default-features = false }
 rustpython-common = { workspace = true }
 
 ahash = { workspace = true }
diff --git a/stdlib/src/dis.rs b/stdlib/src/dis.rs
index d9322d4871..12c2ea75df 100644
--- a/stdlib/src/dis.rs
+++ b/stdlib/src/dis.rs
@@ -13,18 +13,23 @@ mod decl {
         let co = if let Ok(co) = obj.get_attr("__code__", vm) {
             // Method or function:
             PyRef::try_from_object(vm, co)?
-        } else if let Ok(_co_str) = PyStrRef::try_from_object(vm, obj.clone()) {
+        } else if let Ok(co_str) = PyStrRef::try_from_object(vm, obj.clone()) {
             #[cfg(not(feature = "compiler"))]
-            return Err(vm.new_runtime_error(
-                "dis.dis() with str argument requires `compiler` feature".to_owned(),
-            ));
+            {
+                let _ = co_str;
+                return Err(vm.new_runtime_error(
+                    "dis.dis() with str argument requires `compiler` feature".to_owned(),
+                ));
+            }
             #[cfg(feature = "compiler")]
-            vm.compile(
-                _co_str.as_str(),
-                crate::vm::compiler::Mode::Exec,
-                "<dis>".to_owned(),
-            )
-            .map_err(|err| vm.new_syntax_error(&err, Some(_co_str.as_str())))?
+            {
+                vm.compile(
+                    co_str.as_str(),
+                    crate::vm::compiler::Mode::Exec,
+                    "<dis>".to_owned(),
+                )
+                .map_err(|err| vm.new_syntax_error(&err, Some(co_str.as_str())))?
+            }
         } else {
             PyRef::try_from_object(vm, obj)?
         };
diff --git a/vm/Cargo.toml b/vm/Cargo.toml
index 251d482ebe..78702bc8fd 100644
--- a/vm/Cargo.toml
+++ b/vm/Cargo.toml
@@ -10,7 +10,7 @@ repository.workspace = true
 license.workspace = true
 
 [features]
-default = ["compiler"]
+default = ["compiler", "wasmbind"]
 importlib = []
 encodings = ["importlib"]
 vm-tracing-logging = []
@@ -23,6 +23,7 @@ ast = ["rustpython-ast"]
 codegen = ["rustpython-codegen", "ast"]
 parser = ["rustpython-parser", "ast"]
 serde = ["dep:serde"]
+wasmbind = ["chrono/wasmbind", "getrandom/js", "wasm-bindgen"]
 
 [dependencies]
 rustpython-compiler = { workspace = true, optional = true }
@@ -48,6 +49,7 @@ cfg-if = { workspace = true }
 crossbeam-utils = { workspace = true }
 chrono = { workspace = true, features = ["wasmbind"] }
 flame = { workspace = true, optional = true }
+getrandom = { workspace = true }
 hex = { workspace = true }
 indexmap = { workspace = true }
 itertools = { workspace = true }
@@ -71,7 +73,6 @@ thread_local = { workspace = true }
 memchr = { workspace = true }
 
 caseless = "0.2.1"
-getrandom = { version = "0.2.12", features = ["js"] }
 flamer = { version = "0.4", optional = true }
 half = "1.8.2"
 memoffset = "0.9.1"
@@ -140,8 +141,9 @@ features = [
   "Win32_UI_WindowsAndMessaging",
 ]
 
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-wasm-bindgen = "0.2.92"
+[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
+wasm-bindgen = { workspace = true, optional = true }
+getrandom = { workspace = true, features = ["custom"] }
 
 [build-dependencies]
 glob = { workspace = true }
diff --git a/vm/src/stdlib/time.rs b/vm/src/stdlib/time.rs
index d026f2953e..8ef67bacec 100644
--- a/vm/src/stdlib/time.rs
+++ b/vm/src/stdlib/time.rs
@@ -97,12 +97,19 @@ mod decl {
         _time(vm)
     }
 
-    #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
+    #[cfg(not(all(
+        target_arch = "wasm32",
+        not(any(target_os = "emscripten", target_os = "wasi")),
+    )))]
     fn _time(vm: &VirtualMachine) -> PyResult<f64> {
         Ok(duration_since_system_now(vm)?.as_secs_f64())
     }
 
-    #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
+    #[cfg(all(
+        target_arch = "wasm32",
+        feature = "wasmbind",
+        not(any(target_os = "emscripten", target_os = "wasi"))
+    ))]
     fn _time(_vm: &VirtualMachine) -> PyResult<f64> {
         use wasm_bindgen::prelude::*;
         #[wasm_bindgen]
@@ -115,6 +122,15 @@ mod decl {
         Ok(Date::now() / 1000.0)
     }
 
+    #[cfg(all(
+        target_arch = "wasm32",
+        not(feature = "wasmbind"),
+        not(any(target_os = "emscripten", target_os = "wasi"))
+    ))]
+    fn _time(vm: &VirtualMachine) -> PyResult<f64> {
+        Err(vm.new_not_implemented_error("time.time".to_owned()))
+    }
+
     #[pyfunction]
     fn monotonic(vm: &VirtualMachine) -> PyResult<f64> {
         Ok(get_monotonic_time(vm)?.as_secs_f64())
diff --git a/vm/src/vm/vm_object.rs b/vm/src/vm/vm_object.rs
index e3d1436882..b687eea34c 100644
--- a/vm/src/vm/vm_object.rs
+++ b/vm/src/vm/vm_object.rs
@@ -13,13 +13,21 @@ impl VirtualMachine {
     #[track_caller]
     #[cold]
     fn _py_panic_failed(&self, exc: PyBaseExceptionRef, msg: &str) -> ! {
-        #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"))))]
+        #[cfg(not(all(
+            target_arch = "wasm32",
+            not(any(target_os = "emscripten", target_os = "wasi")),
+        )))]
         {
             self.print_exception(exc);
             self.flush_std();
             panic!("{msg}")
         }
-        #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
+        #[cfg(all(
+            target_arch = "wasm32",
+            feature = "wasmbind",
+            not(any(target_os = "emscripten", target_os = "wasi")),
+        ))]
+        #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), feature = "wasmbind"))]
         {
             use wasm_bindgen::prelude::*;
             #[wasm_bindgen]
@@ -32,6 +40,17 @@ impl VirtualMachine {
             error(&s);
             panic!("{}; exception backtrace above", msg)
         }
+        #[cfg(all(
+            target_arch = "wasm32",
+            not(feature = "wasmbind"),
+            not(any(target_os = "emscripten", target_os = "wasi")),
+        ))]
+        {
+            use crate::convert::ToPyObject;
+            let err_string: String = exc.to_pyobject(self).repr(self).unwrap().to_string();
+            eprintln!("{err_string}");
+            panic!("{}; python exception not available", msg)
+        }
     }
 
     pub(crate) fn flush_std(&self) {
diff --git a/wasm/lib/Cargo.toml b/wasm/lib/Cargo.toml
index 9b71bbce25..1e5c37f4ef 100644
--- a/wasm/lib/Cargo.toml
+++ b/wasm/lib/Cargo.toml
@@ -21,16 +21,16 @@ rustpython-common = { workspace = true }
 rustpython-pylib = { workspace = true, optional = true }
 rustpython-stdlib = { workspace = true, default-features = false, optional = true }
 # make sure no threading! otherwise wasm build will fail
-rustpython-vm = { workspace = true, features = ["compiler", "encodings", "serde"] }
+rustpython-vm = { workspace = true, features = ["compiler", "encodings", "serde", "wasmbind"] }
 
 rustpython-parser = { workspace = true }
 
 serde = { workspace = true }
+wasm-bindgen = { workspace = true }
 
 console_error_panic_hook = "0.1"
 js-sys = "0.3"
 serde-wasm-bindgen = "0.3.1"
-wasm-bindgen = "0.2.80"
 wasm-bindgen-futures = "0.4"
 web-sys = { version = "0.3", features = [
     "console",


Test patch:
diff --git a/wasm/wasm-unknown-test/Cargo.toml b/wasm/wasm-unknown-test/Cargo.toml
new file mode 100644
index 0000000000..f5e0b55786
--- /dev/null
+++ b/wasm/wasm-unknown-test/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "wasm-unknown-test"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+getrandom = { version = "0.2.12", features = ["custom"] }
+rustpython-vm = { path = "../../vm", default-features = false, features = ["compiler"] }
+
+[workspace]
diff --git a/wasm/wasm-unknown-test/README.md b/wasm/wasm-unknown-test/README.md
new file mode 100644
index 0000000000..3c4abd59cf
--- /dev/null
+++ b/wasm/wasm-unknown-test/README.md
@@ -0,0 +1,1 @@
+A test crate to ensure that `rustpython-vm` compiles on `wasm32-unknown-unknown` without a JS host.
diff --git a/wasm/wasm-unknown-test/src/lib.rs b/wasm/wasm-unknown-test/src/lib.rs
new file mode 100644
index 0000000000..fd043aea3a
--- /dev/null
+++ b/wasm/wasm-unknown-test/src/lib.rs
@@ -0,0 +1,16 @@
+use rustpython_vm::{eval, Interpreter};
+
+pub unsafe extern "C" fn eval(s: *const u8, l: usize) -> u32 {
+    let src = std::slice::from_raw_parts(s, l);
+    let src = std::str::from_utf8(src).unwrap();
+    Interpreter::without_stdlib(Default::default()).enter(|vm| {
+        let res = eval::eval(vm, src, vm.new_scope_with_builtins(), "<string>").unwrap();
+        res.try_into_value(vm).unwrap()
+    })
+}
+
+fn getrandom_always_fail(_buf: &mut [u8]) -> Result<(), getrandom::Error> {
+    Err(getrandom::Error::UNSUPPORTED)
+}
+
+getrandom::register_custom_getrandom!(getrandom_always_fail);