]> Repositorios git - scryer-prolog.git/commitdiff
Basic WebAssembly support with minimal Javascript API #615. Currently 6/12 crypto...
authorRujia Liu <[email protected]>
Fri, 8 Sep 2023 09:27:22 +0000 (17:27 +0800)
committerRujia Liu <[email protected]>
Fri, 8 Sep 2023 09:27:22 +0000 (17:27 +0800)
Cargo.toml
README.md
build/instructions_template.rs
src/atom_table.rs
src/lib.rs
src/machine/dispatch.rs
src/machine/mock_wam.rs
src/machine/mod.rs
src/machine/system_calls.rs

index 16f51100f4bd90cd59db8bac60cc07f2de00318e..c1e8ae2ef8ef22a556dd1708e42eeb41d2e5b3ac 100644 (file)
@@ -12,14 +12,18 @@ categories = ["command-line-utilities"]
 build = "build/main.rs"
 rust-version = "1.70"
 
+[lib]
+crate-type = ["cdylib", "rlib"]
+
 [features]
-default = ["ffi", "repl", "hostname", "tls", "http"]
+default = ["ffi", "repl", "hostname", "tls", "http", "crypto-full"]
 ffi = ["dep:libffi"]
 repl = ["dep:crossterm", "dep:ctrlc", "dep:rustyline"]
 hostname = ["dep:hostname"]
 tls = ["dep:native-tls"]
 http = ["dep:hyper", "dep:reqwest"]
 rust_beta_channel = []
+crypto-full = []
 
 [build-dependencies]
 indexmap = "1.0.2"
@@ -80,7 +84,19 @@ tokio = { version = "1.28.2", features = ["full"] }
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 getrandom = { version = "0.2.10", features = ["js"] }
-tokio = { version = "1.28.2", features = ["sync", "macros", "io-util", "rt", "time"] }
+tokio = { version = "1.28.2", features = ["sync", "macros", "io-util", "rt"] }
+
+[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
+console_error_panic_hook = "0.1"
+console_log = "1.0"
+wasm-bindgen = "0.2.87"
+wasm-bindgen-futures = "0.4"
+serde-wasm-bindgen = "0.5"
+web-sys = { version = "0.3", features = [
+    "Document",
+    "Window",
+    "Element",
+]}
 
 [target.'cfg(target_os = "wasi")'.dependencies]
 ring-wasi = { version = "0.16.25" }
index bd55c0500b842f22aa821dcd6c2576a770db3e46..4993405226390e84330120f7d973bb28f3ee99aa 100644 (file)
--- a/README.md
+++ b/README.md
@@ -152,6 +152,77 @@ It will generate a very basic MSI file which installs the main executable and a
 
 Scryer Prolog must be built with **Rust 1.63 and up**.
 
+### Building WebAssembly
+
+Scryer Prolog has basic WebAssembly support. You can follow `wasm-pack`'s [official instructions](https://rustwasm.github.io/docs/wasm-pack/quickstart.html) to install `wasm-pack` and build it in any way you like.
+
+However, none of the [default features](https://doc.rust-lang.org/cargo/reference/features.html#the-default-feature) are currently supported. The preferred way of disabling them is passing [extra options](https://rustwasm.github.io/wasm-pack/book/commands/build.html#extra-options) to `wasm-pack`.
+
+For example, if you want a minimal working package without using any bundler like `webpack`, you can do this:
+```
+wasm-pack build --target web -- --no-default-features
+```
+Then a `pkg` directory will be created, containing everything you need for a webapp. You can test whether the package is successfully built by creating an html file, adapted from `wasm-bindgen`'s [official example](https://rustwasm.github.io/wasm-bindgen/examples/without-a-bundler.html) like this:
+
+```html
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="UTF-8" />
+        <title>Scryer Prolog - Sudoku Solver Example</title>
+        <script type="module">
+        import init, { eval_code } from './pkg/scryer_prolog.js';
+
+        const run = async () => {
+            await init("./pkg/scryer_prolog_bg.wasm");
+            let code = `
+            :- use_module(library(format)).
+            :- use_module(library(clpz)).
+            :- use_module(library(lists)).
+            
+            sudoku(Rows) :-
+            length(Rows, 9), maplist(same_length(Rows), Rows),
+            append(Rows, Vs), Vs ins 1..9,
+            maplist(all_distinct, Rows),
+            transpose(Rows, Columns),
+            maplist(all_distinct, Columns),
+            Rows = [As,Bs,Cs,Ds,Es,Fs,Gs,Hs,Is],
+            blocks(As, Bs, Cs),
+            blocks(Ds, Es, Fs),
+            blocks(Gs, Hs, Is).
+            
+            blocks([], [], []).
+            blocks([N1,N2,N3|Ns1], [N4,N5,N6|Ns2], [N7,N8,N9|Ns3]) :-
+            all_distinct([N1,N2,N3,N4,N5,N6,N7,N8,N9]),
+            blocks(Ns1, Ns2, Ns3).
+            
+            problem(1, [[_,_,_,_,_,_,_,_,_],
+                        [_,_,_,_,_,3,_,8,5],
+                        [_,_,1,_,2,_,_,_,_],
+                        [_,_,_,5,_,7,_,_,_],
+                        [_,_,4,_,_,_,1,_,_],
+                        [_,9,_,_,_,_,_,_,_],
+                        [5,_,_,_,_,_,_,7,3],
+                        [_,_,2,_,1,_,_,_,_],
+                        [_,_,_,_,4,_,_,_,9]]).
+            
+            main :-
+            problem(1, Rows), sudoku(Rows), maplist(portray_clause, Rows).
+            
+            :- initialization(main).
+            `;
+            const result = eval_code(code);
+            document.write(`<p>Sudoku solver returns:</p><pre>${result}</pre>`);
+        }
+        run();
+        </script>    
+    </head>
+    <body></body>
+</html>
+```
+
+Then you can serve it with your favorite http server like `python -m http.server` or `npx serve`, and access the page with your browser.
+
 ### Docker Install
 
 First, install [Docker](https://docs.docker.com/get-docker/) on Linux,
index f626d781c0a7184dcabc58224bd74e410c1bb79d..4dc9c89328482970234eb62d63e159403d8ee76a 100644 (file)
@@ -496,22 +496,28 @@ enum SystemClauseType {
     CryptoDataHKDF,
     #[strum_discriminants(strum(props(Arity = "4", Name = "$crypto_password_hash")))]
     CryptoPasswordHash,
+    #[strum_discriminants(strum(props(Arity = "4", Name = "$crypto_curve_scalar_mult")))]
+    CryptoCurveScalarMult,
+    #[strum_discriminants(strum(props(Arity = "3", Name = "$curve25519_scalar_mult")))]
+    Curve25519ScalarMult,
+    #[cfg(feature = "crypto-full")]
     #[strum_discriminants(strum(props(Arity = "7", Name = "$crypto_data_encrypt")))]
     CryptoDataEncrypt,
+    #[cfg(feature = "crypto-full")]
     #[strum_discriminants(strum(props(Arity = "6", Name = "$crypto_data_decrypt")))]
     CryptoDataDecrypt,
-    #[strum_discriminants(strum(props(Arity = "4", Name = "$crypto_curve_scalar_mult")))]
-    CryptoCurveScalarMult,
+    #[cfg(feature = "crypto-full")]
     #[strum_discriminants(strum(props(Arity = "4", Name = "$ed25519_sign")))]
     Ed25519Sign,
+    #[cfg(feature = "crypto-full")]
     #[strum_discriminants(strum(props(Arity = "4", Name = "$ed25519_verify")))]
     Ed25519Verify,
+    #[cfg(feature = "crypto-full")]
     #[strum_discriminants(strum(props(Arity = "1", Name = "$ed25519_new_keypair")))]
     Ed25519NewKeyPair,
+    #[cfg(feature = "crypto-full")]
     #[strum_discriminants(strum(props(Arity = "2", Name = "$ed25519_keypair_public_key")))]
     Ed25519KeyPairPublicKey,
-    #[strum_discriminants(strum(props(Arity = "3", Name = "$curve25519_scalar_mult")))]
-    Curve25519ScalarMult,
     #[strum_discriminants(strum(props(Arity = "2", Name = "$first_non_octet")))]
     FirstNonOctet,
     #[strum_discriminants(strum(props(Arity = "3", Name = "$load_html")))]
@@ -1842,13 +1848,7 @@ fn generate_instruction_preface() -> TokenStream {
                     &Instruction::CallCryptoDataHash |
                     &Instruction::CallCryptoDataHKDF |
                     &Instruction::CallCryptoPasswordHash |
-                    &Instruction::CallCryptoDataEncrypt |
-                    &Instruction::CallCryptoDataDecrypt |
                     &Instruction::CallCryptoCurveScalarMult |
-                    &Instruction::CallEd25519Sign |
-                    &Instruction::CallEd25519Verify |
-                    &Instruction::CallEd25519NewKeyPair |
-                    &Instruction::CallEd25519KeyPairPublicKey |
                     &Instruction::CallCurve25519ScalarMult |
                     &Instruction::CallFirstNonOctet |
                     &Instruction::CallLoadHTML |
@@ -1905,6 +1905,17 @@ fn generate_instruction_preface() -> TokenStream {
                         functor!(atom!("call"), [atom(name), fixnum(arity)])
                     }
                     //
+                    #[cfg(feature = "crypto-full")]
+                    &Instruction::CallCryptoDataEncrypt |
+                    &Instruction::CallCryptoDataDecrypt |
+                    &Instruction::CallEd25519Sign |
+                    &Instruction::CallEd25519Verify |
+                    &Instruction::CallEd25519NewKeyPair |
+                    &Instruction::CallEd25519KeyPairPublicKey => {
+                        let (name, arity) = self.to_name_and_arity();
+                        functor!(atom!("call"), [atom(name), fixnum(arity)])
+                    }
+                    //
                     &Instruction::ExecuteAtomChars |
                     &Instruction::ExecuteAtomCodes |
                     &Instruction::ExecuteAtomLength |
@@ -2070,13 +2081,7 @@ fn generate_instruction_preface() -> TokenStream {
                     &Instruction::ExecuteCryptoDataHash |
                     &Instruction::ExecuteCryptoDataHKDF |
                     &Instruction::ExecuteCryptoPasswordHash |
-                    &Instruction::ExecuteCryptoDataEncrypt |
-                    &Instruction::ExecuteCryptoDataDecrypt |
                     &Instruction::ExecuteCryptoCurveScalarMult |
-                    &Instruction::ExecuteEd25519Sign |
-                    &Instruction::ExecuteEd25519Verify |
-                    &Instruction::ExecuteEd25519NewKeyPair |
-                    &Instruction::ExecuteEd25519KeyPairPublicKey |
                     &Instruction::ExecuteCurve25519ScalarMult |
                     &Instruction::ExecuteFirstNonOctet |
                     &Instruction::ExecuteLoadHTML |
@@ -2133,6 +2138,17 @@ fn generate_instruction_preface() -> TokenStream {
                         functor!(atom!("execute"), [atom(name), fixnum(arity)])
                     }
                     //
+                    #[cfg(feature = "crypto-full")]
+                    &Instruction::ExecuteCryptoDataEncrypt |
+                    &Instruction::ExecuteCryptoDataDecrypt |
+                    &Instruction::ExecuteEd25519Sign |
+                    &Instruction::ExecuteEd25519Verify |
+                    &Instruction::ExecuteEd25519NewKeyPair |
+                    &Instruction::ExecuteEd25519KeyPairPublicKey => {
+                        let (name, arity) = self.to_name_and_arity();
+                        functor!(atom!("execute"), [atom(name), fixnum(arity)])
+                    }
+                    //
                     &Instruction::Deallocate => {
                         functor!(atom!("deallocate"))
                     }
index ba5a80b4bd703caacf5d50ad7c684d5b677d2fe8..80ba54114909cbfcff2d9d334a8872d1ff8f1ed5 100644 (file)
@@ -159,6 +159,7 @@ impl std::ops::Deref for AtomString<'_> {
     }
 }
 
+#[cfg(feature = "repl")]
 impl rustyline::completion::Candidate for AtomString<'_> {
     fn display(&self) -> &str {
         self.deref()
index 63c818e98c3beab89281742ac85f872b000d4b8f..8817fa5fcfea6674b7bce0d03faa207005fb07e1 100644 (file)
@@ -40,3 +40,17 @@ pub mod types;
 use instructions::instr;
 
 mod rcu;
+
+#[cfg(target_arch = "wasm32")]
+use wasm_bindgen::prelude::*;
+
+#[cfg(target_arch = "wasm32")]
+#[wasm_bindgen]
+pub fn eval_code(s: &str) -> String {
+    use web_sys::console;
+    use machine::mock_wam::*;
+
+    let mut wam = Machine::with_test_streams();
+    let bytes = wam.test_load_string(s);
+    String::from_utf8_lossy(&bytes).to_string()
+}
index 35a73f581135e9087be12b3ac420e70a831e82c1..89174091710354360b07279997d3c2fb4b2d223c 100644 (file)
@@ -4742,18 +4742,22 @@ impl Machine {
                         self.crypto_password_hash();
                         step_or_fail!(self, self.machine_st.p = self.machine_st.cp);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::CallCryptoDataEncrypt => {
                         self.crypto_data_encrypt();
                         step_or_fail!(self, self.machine_st.p += 1);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::ExecuteCryptoDataEncrypt => {
                         self.crypto_data_encrypt();
                         step_or_fail!(self, self.machine_st.p = self.machine_st.cp);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::CallCryptoDataDecrypt => {
                         self.crypto_data_decrypt();
                         step_or_fail!(self, self.machine_st.p += 1);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::ExecuteCryptoDataDecrypt => {
                         self.crypto_data_decrypt();
                         step_or_fail!(self, self.machine_st.p = self.machine_st.cp);
@@ -4766,34 +4770,42 @@ impl Machine {
                         self.crypto_curve_scalar_mult();
                         step_or_fail!(self, self.machine_st.p = self.machine_st.cp);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::CallEd25519Sign => {
                         self.ed25519_sign();
                         step_or_fail!(self, self.machine_st.p += 1);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::ExecuteEd25519Sign => {
                         self.ed25519_sign();
                         step_or_fail!(self, self.machine_st.p = self.machine_st.cp);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::CallEd25519Verify => {
                         self.ed25519_verify();
                         step_or_fail!(self, self.machine_st.p += 1);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::ExecuteEd25519Verify => {
                         self.ed25519_verify();
                         step_or_fail!(self, self.machine_st.p = self.machine_st.cp);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::CallEd25519NewKeyPair => {
                         self.ed25519_new_key_pair();
                         step_or_fail!(self, self.machine_st.p += 1);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::ExecuteEd25519NewKeyPair => {
                         self.ed25519_new_key_pair();
                         step_or_fail!(self, self.machine_st.p = self.machine_st.cp);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::CallEd25519KeyPairPublicKey => {
                         self.ed25519_key_pair_public_key();
                         step_or_fail!(self, self.machine_st.p += 1);
                     }
+                    #[cfg(feature = "crypto-full")]
                     &Instruction::ExecuteEd25519KeyPairPublicKey => {
                         self.ed25519_key_pair_public_key();
                         step_or_fail!(self, self.machine_st.p = self.machine_st.cp);
index 7be95fbd0f4c18cc74c3f96a76e4429dcca037db..cd8f783675459dace13238023932698518d280ff 100644 (file)
@@ -332,6 +332,19 @@ impl Machine {
         self.load_file(file.into(), stream);
         self.user_output.bytes().map(|b| b.unwrap()).collect()
     }
+
+    pub fn test_load_string(&mut self, code: &str) -> Vec<u8> {
+        use std::io::Read;
+
+        let stream = Stream::from_owned_string(
+            code.to_owned(),
+            &mut self.machine_st.arena,
+        );
+
+        self.load_file("<stdin>".into(), stream);
+        self.user_output.bytes().map(|b| b.unwrap()).collect()
+    }
+
 }
 
 #[cfg(test)]
index a7d80e9f4027af8ae7096e0feb60541fb1a2c1e7..7e456d4ef318db22e4c8e5b9b6d7b368a5937b6d 100644 (file)
@@ -453,9 +453,9 @@ impl Machine {
         let user_output = Stream::stdout(&mut machine_st.arena);
         let user_error = Stream::stderr(&mut machine_st.arena);
 
-        #[cfg(not(target_os = "wasi"))]
+        #[cfg(not(target_arch = "wasm32"))]
         let runtime = tokio::runtime::Runtime::new().unwrap();
-        #[cfg(target_os = "wasi")]
+        #[cfg(target_arch = "wasm32")]
         let runtime = tokio::runtime::Builder::new_current_thread()
             .enable_all()
             .build()
index 7b017c175a96df65605cf372125def33ea2829e0..3e6a7090b78294274187bf9d83da5987bf5d831d 100644 (file)
@@ -60,7 +60,7 @@ use std::process;
 use std::str::FromStr;
 
 use chrono::{offset::Local, DateTime};
-#[cfg(not(target_os = "wasi"))]
+#[cfg(not(target_arch = "wasm32"))]
 use cpu_time::ProcessTime;
 use std::time::{Duration, SystemTime};
 
@@ -71,8 +71,11 @@ use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
 
 use blake2::{Blake2b, Blake2s};
 use ring::rand::{SecureRandom, SystemRandom};
+use ring::{digest, hkdf, pbkdf2};
+
+#[cfg(feature = "crypto-full")]
 use ring::{
-    aead, digest, hkdf, pbkdf2,
+    aead, 
     signature::{self, KeyPair},
 };
 use ripemd160::{Digest, Ripemd160};
@@ -4290,7 +4293,7 @@ impl Machine {
         self.machine_st.fail = result;
     }
 
-    #[cfg(not(target_os = "wasi"))]
+    #[cfg(not(target_arch = "wasm32"))]
     #[inline(always)]
     pub(crate) fn cpu_now(&mut self) {
         let secs = ProcessTime::now().as_duration().as_secs_f64();
@@ -4300,7 +4303,7 @@ impl Machine {
             .unify_f64(secs, self.machine_st.registers[1]);
     }
 
-    #[cfg(target_os = "wasi")]
+    #[cfg(target_arch = "wasm32")]
     #[inline(always)]
     pub(crate) fn cpu_now(&mut self) {
         // TODO
@@ -7382,6 +7385,7 @@ impl Machine {
         unify!(self.machine_st, self.machine_st.registers[4], ints_list);
     }
 
+    #[cfg(feature = "crypto-full")]
     #[inline(always)]
     pub(crate) fn crypto_data_encrypt(&mut self) {
         let encoding = cell_as_atom!(self.deref_register(3));
@@ -7430,6 +7434,7 @@ impl Machine {
         );
     }
 
+    #[cfg(feature = "crypto-full")]
     #[inline(always)]
     pub(crate) fn crypto_data_decrypt(&mut self) {
         let data = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet"));
@@ -7508,6 +7513,7 @@ impl Machine {
         unify!(self.machine_st, self.machine_st.registers[4], uncompressed);
     }
 
+    #[cfg(feature = "crypto-full")]
     #[inline(always)]
     pub(crate) fn ed25519_new_key_pair(&mut self) {
         let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(rng()).unwrap();
@@ -7520,6 +7526,7 @@ impl Machine {
         )
     }
 
+    #[cfg(feature = "crypto-full")]
     #[inline(always)]
     pub(crate) fn ed25519_key_pair_public_key(&mut self) {
         let bytes = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet"));
@@ -7541,6 +7548,7 @@ impl Machine {
         );
     }
 
+    #[cfg(feature = "crypto-full")]
     #[inline(always)]
     pub(crate) fn ed25519_sign(&mut self) {
         let key = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet"));
@@ -7567,6 +7575,7 @@ impl Machine {
         unify!(self.machine_st, self.machine_st.registers[4], sig_list);
     }
 
+    #[cfg(feature = "crypto-full")]
     #[inline(always)]
     pub(crate) fn ed25519_verify(&mut self) {
         let key = self.string_encoding_bytes(self.machine_st.registers[1], atom!("octet"));