From 93e804eeaa880865fcb2e97b959004309c07d054 Mon Sep 17 00:00:00 2001 From: bakaq Date: Fri, 31 Jan 2025 09:24:39 -0300 Subject: [PATCH] Rework Wasm interface --- Cargo.lock | 58 ++++++- Cargo.toml | 1 + src/bin/scryer-prolog.rs | 6 +- src/lib.rs | 16 +- src/wasm.rs | 320 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 385 insertions(+), 16 deletions(-) create mode 100644 src/wasm.rs 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 0eb6b7a1..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" 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..d08e1860 --- /dev/null +++ b/src/wasm.rs @@ -0,0 +1,320 @@ +//! Wasm interface + +#![allow(missing_docs)] + +use std::mem; +use std::sync::mpsc; +use std::sync::mpsc::{Receiver, Sender}; + +use ouroboros::self_referencing; +use wasm_bindgen::prelude::*; + +use crate::*; + +#[wasm_bindgen(js_name = MachineBuilder)] +#[derive(Default)] +pub struct WasmMachineBuilder { + inner: MachineBuilder, +} + +#[wasm_bindgen(js_class = MachineBuilder)] +impl WasmMachineBuilder { + pub fn new() -> Self { + Default::default() + } + + pub fn build(&mut self) -> WasmMachine { + WasmMachine { + inner: Ok(std::mem::take(&mut self.inner).build()), + } + } +} + +#[wasm_bindgen(inline_js = " + export function self_iterable(obj) { + obj[Symbol.iterator] = function () { + return this + }; + } +")] +extern "C" { + fn self_iterable(obj: &JsValue); +} + +#[wasm_bindgen(js_name = Machine)] +pub struct WasmMachine { + inner: Result>, +} + +#[wasm_bindgen(js_class = Machine)] +impl WasmMachine { + #[wasm_bindgen(js_name = runQuery)] + pub fn run_query(&mut self, query: String) -> Result { + 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)); + } + + 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(); + + self_iterable(&query_state); + Ok(query_state) + } +} + +#[self_referencing] +struct WasmQueryStateInner { + machine: Machine, + drop_channel: Sender, + #[covariant] + #[borrows(mut machine)] + query_state: QueryState<'this>, +} + +#[wasm_bindgen(js_name = QueryState)] +pub struct WasmQueryState { + inner: Option, +} + +#[wasm_bindgen(js_class = QueryState)] +impl WasmQueryState { + #[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()) + } + + #[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(); + } +} + +#[wasm_bindgen(inline_js = r#" + export class LeafAnswer { + constructor(bindings) { + this.bindings = bindings; + } + } + + export class PrologInteger { + constructor(integerStr) { + this.type = "integer"; + this.integer = BigInt(integerStr); + } + } + + export class PrologRational { + constructor(numeratorStr, denominatorStr) { + this.type = "rational"; + this.numerator = BigInt(numeratorStr); + this.denominator = BigInt(denominatorStr); + } + } + + export class PrologFloat { + constructor(float) { + this.type = "float"; + this.float = float; + } + } + + export class PrologAtom { + constructor(atom) { + this.type = "atom"; + this.atom = atom; + } + } + + export class PrologString { + constructor(string) { + this.type = "string"; + this.string = string; + } + } + + export class PrologList { + constructor(list) { + this.type = "list"; + this.list = list; + } + } + + export class PrologCompound { + constructor(functor, args) { + this.type = "compound"; + this.functor = functor; + this.args = args; + } + } + + export class PrologVariable { + constructor(variable) { + this.type = "variable"; + this.variable = variable; + } + } + + export class PrologException { + constructor(exception) { + this.exception = exception; + } + } +"#)] +extern "C" { + #[wasm_bindgen(js_name = LeafAnswer)] + pub type JsLeafAnswer; + + #[wasm_bindgen(constructor, js_class = LeafAnswer)] + pub fn new(bindings: js_sys::Object) -> JsLeafAnswer; + + #[wasm_bindgen(js_name = PrologInteger)] + pub type JsPrologInteger; + + #[wasm_bindgen(constructor, js_class = PrologInteger)] + pub fn new(int_str: &str) -> JsPrologInteger; + + #[wasm_bindgen(js_name = PrologRational)] + pub type JsPrologRational; + + #[wasm_bindgen(constructor, js_class = PrologRational)] + pub fn new(numer_str: &str, denom_str: &str) -> JsPrologRational; + + #[wasm_bindgen(js_name = PrologFloat)] + pub type JsPrologFloat; + + #[wasm_bindgen(constructor, js_class = PrologFloat)] + pub fn new(float: f64) -> JsPrologFloat; + + #[wasm_bindgen(js_name = PrologAtom)] + pub type JsPrologAtom; + + #[wasm_bindgen(constructor, js_class = PrologAtom)] + pub fn new(atom: &str) -> JsPrologAtom; + + #[wasm_bindgen(js_name = PrologString)] + pub type JsPrologString; + + #[wasm_bindgen(constructor, js_class = PrologString)] + pub fn new(string: &str) -> JsPrologString; + + #[wasm_bindgen(js_name = PrologList)] + pub type JsPrologList; + + #[wasm_bindgen(constructor, js_class = PrologList)] + pub fn new(list: js_sys::Array) -> JsPrologList; + + #[wasm_bindgen(js_name = PrologCompound)] + pub type JsPrologCompound; + + #[wasm_bindgen(constructor, js_class = PrologCompound)] + pub fn new(functor: &str, args: js_sys::Array) -> JsPrologCompound; + + #[wasm_bindgen(js_name = PrologVariable)] + pub type JsPrologVariable; + + #[wasm_bindgen(constructor, js_class = PrologVariable)] + pub fn new(variable: &str) -> JsPrologVariable; + + #[wasm_bindgen(js_name = PrologException)] + pub type JsPrologException; + + #[wasm_bindgen(constructor, js_class = PrologException)] + pub fn new(exception: JsValue) -> JsPrologException; +} + +impl From for JsValue { + fn from(leaf_answer: LeafAnswer) -> JsValue { + match leaf_answer { + LeafAnswer::True => true.into(), + LeafAnswer::False => false.into(), + LeafAnswer::Exception(e) => JsPrologException::new(e.into()).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(); + } + JsLeafAnswer::new(bindings_obj).into() + } + } + } +} + +impl From for JsValue { + fn from(term: Term) -> JsValue { + match term { + Term::Integer(i) => JsPrologInteger::new(&i.to_string()).into(), + Term::Rational(r) => { + JsPrologRational::new(&r.numerator().to_string(), &r.denominator().to_string()) + .into() + } + Term::Float(f) => JsPrologFloat::new(f).into(), + Term::Atom(a) => JsPrologAtom::new(&a).into(), + Term::String(s) => JsPrologString::new(&s).into(), + Term::List(l) => JsPrologList::new(l.into_iter().map(JsValue::from).collect()).into(), + Term::Compound(functor, args) => { + JsPrologCompound::new(&functor, args.into_iter().map(JsValue::from).collect()) + .into() + } + Term::Var(v) => JsPrologVariable::new(&v).into(), + } + } +} -- 2.54.0