diff --git a/Cargo.lock b/Cargo.lock index 623d5cf7..8be9620b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -1129,6 +1135,12 @@ dependencies = [ "http 0.2.12", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1844,6 +1856,30 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.72", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -2132,6 +2168,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", + "version_check", + "yansi", +] + [[package]] name = "quick-xml" version = "0.26.0" @@ -2580,6 +2629,7 @@ dependencies = [ "native-tls", "num-order", "ordered-float", + "ouroboros", "phf 0.11.2", "pprof", "predicates-core", @@ -2966,7 +3016,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", @@ -3780,6 +3830,12 @@ dependencies = [ "tap", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index a299e690..4d89a016 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,6 +116,7 @@ web-sys = { version = "0.3", features = [ "Performance", ] } js-sys = "0.3" +ouroboros = "0.18" [dev-dependencies] maplit = "1.0.2" @@ -139,6 +140,17 @@ opt-level = 3 lto = true opt-level = 3 +[profile.wasm-dev] +inherits = "dev" +opt-level = 1 +lto = "off" + +[profile.wasm-release] +inherits = "release" +lto = "off" +panic = "abort" +codegen-units = 256 + [[bench]] name = "run_criterion" harness = false diff --git a/flake.nix b/flake.nix index 852aa4af..14e38943 100644 --- a/flake.nix +++ b/flake.nix @@ -20,6 +20,10 @@ rustToolchainDev = super.rust-bin.stable.latest.default.override { extensions = [ "rust-src" "rust-analyzer" ]; }; + rustToolchainDevWasm = super.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" ]; + targets = [ "wasm32-unknown-unknown" ]; + }; rustToolchainNightly = super.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override { extensions = [ "rust-src" "rust-analyzer" "miri" ]; @@ -38,12 +42,21 @@ in { devShells = { - default = pkgs.mkShell { + default = pkgs.mkShell.override { stdenv = pkgs.clangMultiStdenv; } { nativeBuildInputs = nativeBuildInputs; buildInputs = buildInputs ++ (with pkgs; [ rustToolchainDev ]); }; + wasm-js = pkgs.mkShell.override { stdenv = pkgs.clangMultiStdenv; } { + nativeBuildInputs = nativeBuildInputs; + buildInputs = buildInputs ++ (with pkgs; [ + wasm-pack + rustToolchainDevWasm + ]); + TARGET_CC = "${pkgs.clangMultiStdenv.cc}/bin/clang"; + hardeningDisable = [ "all" ]; + }; # For use with Miri and stuff like it nightly = pkgs.mkShell { nativeBuildInputs = nativeBuildInputs; diff --git a/src/bin/scryer-prolog.rs b/src/bin/scryer-prolog.rs index 763314ec..4a9dd2cd 100644 --- a/src/bin/scryer-prolog.rs +++ b/src/bin/scryer-prolog.rs @@ -1,3 +1,7 @@ fn main() -> std::process::ExitCode { - scryer_prolog::run_binary() + #[cfg(target_arch = "wasm32")] + return std::process::ExitCode::SUCCESS; + + #[cfg(not(target_arch = "wasm32"))] + return scryer_prolog::run_binary(); } diff --git a/src/lib.rs b/src/lib.rs index 85ee726a..fcb65ff0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,27 +39,15 @@ mod repl_helper; mod targets; pub(crate) mod types; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; - // Re-exports pub use machine::config::*; pub use machine::lib_machine::*; pub use machine::Machine; -/// Eval a source file in Wasm. #[cfg(target_arch = "wasm32")] -#[wasm_bindgen] -pub fn eval_code(s: &str) -> String { - use machine::mock_wam::*; - - console_error_panic_hook::set_once(); - - let mut wam = MachineBuilder::default().build(); - let bytes = wam.test_load_string(s); - String::from_utf8_lossy(&bytes).to_string() -} +pub mod wasm; +#[cfg(not(target_arch = "wasm32"))] /// The entry point for the Scryer Prolog CLI. pub fn run_binary() -> std::process::ExitCode { use crate::atom_table::Atom; diff --git a/src/wasm.rs b/src/wasm.rs new file mode 100644 index 00000000..10430677 --- /dev/null +++ b/src/wasm.rs @@ -0,0 +1,289 @@ +//! Wasm interface + +use std::mem; +use std::sync::mpsc; +use std::sync::mpsc::{Receiver, Sender}; + +use ouroboros::self_referencing; +use wasm_bindgen::prelude::*; + +use crate::*; + +/// A builder for a `Machine`. +#[wasm_bindgen(js_name = MachineBuilder)] +#[derive(Default)] +pub struct WasmMachineBuilder { + inner: MachineBuilder, +} + +#[wasm_bindgen(js_class = MachineBuilder)] +impl WasmMachineBuilder { + /// Creates a new `MachineBuilder` with the default configuration. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Default::default() + } + + /// Creates a new `Machine`. + pub fn build(&mut self) -> WasmMachine { + WasmMachine { + inner: Ok(std::mem::take(&mut self.inner).build()), + } + } +} + +/// The Scryer Prolog `Machine`. +#[wasm_bindgen(js_name = Machine)] +pub struct WasmMachine { + inner: Result>, +} + +#[wasm_bindgen(js_class = Machine)] +impl WasmMachine { + fn ensure_machine_ownership(&mut self) -> Result<(), JsValue> { + if self.inner.is_err() { + // We have a receiver, try to get the Machine + let machine = self + .inner + .as_mut() + .unwrap_err() + .try_recv() + .map_err(|_| js_sys::Error::new("Another query is still active"))?; + let _ = mem::replace(&mut self.inner, Ok(machine)); + } + Ok(()) + } + + /// Runs a query. + /// + /// You can only have one query at a time. If you try to do anything with this machine while + /// doing a query an error will be thrown. + #[wasm_bindgen(js_name = runQuery)] + pub fn run_query(&mut self, query: String) -> Result { + self.ensure_machine_ownership()?; + assert!(self.inner.is_ok()); + + // Installs a receiver and gets the machine + let (sender, receiver) = mpsc::channel(); + let machine = mem::replace(&mut self.inner, Err(receiver)).unwrap(); + + let query_state: JsValue = WasmQueryState { + inner: Some( + WasmQueryStateInnerBuilder { + machine, + drop_channel: sender, + query_state_builder: move |m: &mut Machine| m.run_query(query), + } + .build(), + ), + } + .into(); + + Ok(query_state) + } + + /// Consults a module. + #[wasm_bindgen(js_name = consultModuleString)] + pub fn consult_module_string( + &mut self, + module: String, + program: String, + ) -> Result<(), JsValue> { + self.ensure_machine_ownership()?; + assert!(self.inner.is_ok()); + + let inner = self.inner.as_mut().unwrap(); + inner.consult_module_string(&module, program); + + Ok(()) + } +} + +#[self_referencing] +struct WasmQueryStateInner { + machine: Machine, + drop_channel: Sender, + #[covariant] + #[borrows(mut machine)] + query_state: QueryState<'this>, +} + +/// The state of a running query. +#[wasm_bindgen(js_name = QueryState)] +pub struct WasmQueryState { + inner: Option, +} + +#[wasm_bindgen(js_class = QueryState)] +impl WasmQueryState { + /// Gets the next leaf answer. + /// + /// This follows the Javascript iterator protocol, so it returns an object that + /// contains a `done` field and a `value` field. If `done` is `false`, then the query ended + /// and control of the `Machine` will be given back to the `Machine` that created this query. + /// Any call after that will result in an error. + #[wasm_bindgen(js_name = next)] + pub fn next_answer(&mut self) -> Result { + let ret = js_sys::Object::new(); + let mut error = None; + let mut to_drop = false; + match &mut self.inner { + Some(ref mut inner) => { + inner.with_query_state_mut(|query_state| match query_state.next() { + Some(Ok(leaf_answer)) => { + js_sys::Reflect::set(&ret, &"value".into(), &leaf_answer.into()).unwrap(); + js_sys::Reflect::set(&ret, &"done".into(), &false.into()).unwrap(); + } + Some(Err(error_term)) => { + let js_error = js_sys::Error::new("Prolog error"); + js_error.set_cause(&error_term.into()); + error = Some(js_error); + } + None => { + js_sys::Reflect::set(&ret, &"done".into(), &true.into()).unwrap(); + to_drop = true; + } + }) + } + None => return Err(js_sys::Error::new("This query was already dropped").into()), + } + + if let Some(e) = error { + self.drop_inner(); + return Err(JsValue::from(e)); + } + + if to_drop { + self.drop_inner(); + } + + Ok(ret.into()) + } + + /// Drops the query. + /// + /// This is useful to end a query early. Like finishing a query, control will be given back + /// to the `Machine` and any call to `next` after that will result in an error. + #[wasm_bindgen(js_name = drop)] + pub fn drop_inner(&mut self) { + let ouroboros_impl_wasm_query_state_inner::Heads { + machine, + drop_channel, + } = self.inner.take().unwrap().into_heads(); + drop_channel.send(machine).unwrap(); + } +} + +impl From for JsValue { + fn from(leaf_answer: LeafAnswer) -> JsValue { + match leaf_answer { + LeafAnswer::True => true.into(), + LeafAnswer::False => false.into(), + LeafAnswer::Exception(e) => { + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"type".into(), &"exception".into()).unwrap(); + js_sys::Reflect::set(&obj, &"exception".into(), &e.into()).unwrap(); + obj.into() + } + LeafAnswer::LeafAnswer { bindings } => { + let bindings_obj = js_sys::Object::new(); + for (var, term) in bindings.into_iter() { + js_sys::Reflect::set(&bindings_obj, &var.into(), &term.into()).unwrap(); + } + + let leaf_answer_obj = js_sys::Object::new(); + js_sys::Reflect::set(&leaf_answer_obj, &"type".into(), &"leafAnswer".into()) + .unwrap(); + js_sys::Reflect::set(&leaf_answer_obj, &"bindings".into(), &bindings_obj.into()) + .unwrap(); + leaf_answer_obj.into() + } + } + } +} + +impl From for JsValue { + fn from(term: Term) -> JsValue { + match term { + Term::Integer(i) => { + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"type".into(), &"integer".into()).unwrap(); + js_sys::Reflect::set( + &obj, + &"integer".into(), + &js_sys::BigInt::new(&i.to_string().into()).unwrap().into(), + ) + .unwrap(); + obj.into() + } + Term::Rational(r) => { + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"type".into(), &"rational".into()).unwrap(); + js_sys::Reflect::set( + &obj, + &"numerator".into(), + &js_sys::BigInt::new(&r.numerator().to_string().into()) + .unwrap() + .into(), + ) + .unwrap(); + js_sys::Reflect::set( + &obj, + &"denominator".into(), + &js_sys::BigInt::new(&r.denominator().to_string().into()) + .unwrap() + .into(), + ) + .unwrap(); + obj.into() + } + Term::Float(f) => { + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"type".into(), &"float".into()).unwrap(); + js_sys::Reflect::set(&obj, &"float".into(), &f.into()).unwrap(); + obj.into() + } + Term::Atom(a) => { + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"type".into(), &"atom".into()).unwrap(); + js_sys::Reflect::set(&obj, &"atom".into(), &a.into()).unwrap(); + obj.into() + } + Term::String(s) => { + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"type".into(), &"string".into()).unwrap(); + js_sys::Reflect::set(&obj, &"string".into(), &s.into()).unwrap(); + obj.into() + } + Term::List(l) => { + let list = js_sys::Array::new(); + for term in l { + list.push(&term.into()); + } + + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"type".into(), &"list".into()).unwrap(); + js_sys::Reflect::set(&obj, &"list".into(), &list.into()).unwrap(); + obj.into() + } + Term::Compound(functor, args) => { + let args_list = js_sys::Array::new(); + for term in args { + args_list.push(&term.into()); + } + + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"type".into(), &"compound".into()).unwrap(); + js_sys::Reflect::set(&obj, &"functor".into(), &functor.into()).unwrap(); + js_sys::Reflect::set(&obj, &"args".into(), &args_list.into()).unwrap(); + obj.into() + } + Term::Var(v) => { + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"type".into(), &"variable".into()).unwrap(); + js_sys::Reflect::set(&obj, &"variable".into(), &v.into()).unwrap(); + obj.into() + } + } + } +}