diff --git a/Cargo.lock b/Cargo.lock index ea0b6e71..e993fb34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -452,6 +452,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -516,6 +526,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "cozo-lib-wasm" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "cozo", + "wasm-bindgen", + "wasm-bindgen-test", + "wee_alloc", +] + [[package]] name = "cozo_c" version = "0.1.3" @@ -1502,6 +1523,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + [[package]] name = "miette" version = "5.3.0" @@ -2558,6 +2585,12 @@ dependencies = [ "windows-sys 0.36.1", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.1.0" @@ -3408,6 +3441,30 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +[[package]] +name = "wasm-bindgen-test" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d2fff962180c3fadf677438054b1db62bee4aa32af26a45388af07d1287e1d" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683da3dfc016f704c9f82cf401520c4f1cb3ee440f7f52b3d6ac29506a49ca7" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "web-sys" version = "0.3.60" @@ -3437,6 +3494,18 @@ dependencies = [ "webpki", ] +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + [[package]] name = "which" version = "4.3.0" diff --git a/Cargo.toml b/Cargo.toml index b915c6cd..149db5bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,4 @@ lto = true [workspace] -members = ["cozorocks", "cozo-lib-c", "cozo-lib-java", "cozo-core", "cozoserver"] +members = ["cozorocks", "cozo-lib-c", "cozo-lib-java", "cozo-core", "cozoserver", "cozo-lib-wasm"] diff --git a/cozo-core/src/data/functions.rs b/cozo-core/src/data/functions.rs index 0c0bfd70..f65152e1 100644 --- a/cozo-core/src/data/functions.rs +++ b/cozo-core/src/data/functions.rs @@ -1432,6 +1432,11 @@ pub(crate) fn op_to_uuid(args: &[DataValue]) -> Result { } define_op!(OP_NOW, 0, false); +#[cfg(feature = "wasm")] +pub(crate) fn op_now(_args: &[DataValue]) -> Result { + bail!("`op_now` is not supported under WASM runtime") +} +#[cfg(not(feature = "wasm"))] pub(crate) fn op_now(_args: &[DataValue]) -> Result { let now = SystemTime::now(); Ok(DataValue::from( @@ -1440,6 +1445,11 @@ pub(crate) fn op_now(_args: &[DataValue]) -> Result { } define_op!(OP_FORMAT_TIMESTAMP, 1, true); +#[cfg(feature = "wasm")] +pub(crate) fn op_format_timestamp(_args: &[DataValue]) -> Result { + bail!("`format_timestamp` is not supported under WASM") +} +#[cfg(not(feature = "wasm"))] pub(crate) fn op_format_timestamp(args: &[DataValue]) -> Result { let f = args[0] .get_float() @@ -1468,6 +1478,11 @@ pub(crate) fn op_format_timestamp(args: &[DataValue]) -> Result { } define_op!(OP_PARSE_TIMESTAMP, 1, false); +#[cfg(feature = "wasm")] +pub(crate) fn op_parse_timestamp(_args: &[DataValue]) -> Result { + bail!("`parse_timestamp` is not supported under WASM") +} +#[cfg(not(feature = "wasm"))] pub(crate) fn op_parse_timestamp(args: &[DataValue]) -> Result { let s = args[0] .get_string() @@ -1480,6 +1495,11 @@ pub(crate) fn op_parse_timestamp(args: &[DataValue]) -> Result { } define_op!(OP_RAND_UUID_V1, 0, false); +#[cfg(feature = "wasm")] +pub(crate) fn op_rand_uuid_v1(_args: &[DataValue]) -> Result { + bail!("`rand_uuid_v1` is not supported under WASM") +} +#[cfg(not(feature = "wasm"))] pub(crate) fn op_rand_uuid_v1(_args: &[DataValue]) -> Result { let mut rng = rand::thread_rng(); let uuid_ctx = uuid::v1::Context::new(rng.gen()); diff --git a/cozo-core/src/parse/query.rs b/cozo-core/src/parse/query.rs index 72a5a690..128dbb96 100644 --- a/cozo-core/src/parse/query.rs +++ b/cozo-core/src/parse/query.rs @@ -234,15 +234,19 @@ pub(crate) fn parse_query( out_opts.timeout = Some(timeout); } Rule::sleep_option => { - let pair = pair.into_inner().next().unwrap(); - let span = pair.extract_span(); - let sleep = build_expr(pair, param_pool)? - .eval_to_const() - .map_err(|err| OptionNotConstantError("sleep", span, [err]))? - .get_float() - .ok_or(OptionNotNonNegIntError("sleep", span))?; - ensure!(sleep > 0., OptionNotPosIntError("sleep", span)); - out_opts.sleep = Some(sleep); + #[cfg(feature = "wasm")] + bail!(":sleep is not supported under WASM"); + #[cfg(not(feature = "wasm"))] { + let pair = pair.into_inner().next().unwrap(); + let span = pair.extract_span(); + let sleep = build_expr(pair, param_pool)? + .eval_to_const() + .map_err(|err| OptionNotConstantError("sleep", span, [err]))? + .get_float() + .ok_or(OptionNotNonNegIntError("sleep", span))?; + ensure!(sleep > 0., OptionNotPosIntError("sleep", span)); + out_opts.sleep = Some(sleep); + } } Rule::limit_option => { let pair = pair.into_inner().next().unwrap(); diff --git a/cozo-core/src/runtime/db.rs b/cozo-core/src/runtime/db.rs index 0dc66fd2..13892f1a 100644 --- a/cozo-core/src/runtime/db.rs +++ b/cozo-core/src/runtime/db.rs @@ -8,8 +8,8 @@ use std::collections::BTreeMap; use std::fmt::{Debug, Formatter}; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -17,7 +17,7 @@ use either::{Left, Right}; use itertools::Itertools; use lazy_static::lazy_static; use miette::{ - bail, ensure, Diagnostic, GraphicalReportHandler, GraphicalTheme, IntoDiagnostic, + bail, Diagnostic, ensure, GraphicalReportHandler, GraphicalTheme, IntoDiagnostic, JSONReportHandler, Result, WrapErr, }; use serde_json::{json, Map}; @@ -28,8 +28,8 @@ use crate::data::json::JsonValue; use crate::data::program::{InputProgram, QueryAssertion, RelationOp}; use crate::data::tuple::Tuple; use crate::data::value::{DataValue, LARGEST_UTF_CHAR}; +use crate::parse::{CozoScript, parse_script, SourceSpan}; use crate::parse::sys::SysOp; -use crate::parse::{parse_script, CozoScript, SourceSpan}; use crate::query::compile::{CompiledProgram, CompiledRule, CompiledRuleSet}; use crate::query::relation::{ FilteredRA, InMemRelationRA, InnerJoin, NegJoin, RelAlgebra, ReorderRA, StoredRA, UnificationRA, @@ -144,13 +144,18 @@ impl<'s, S: Storage<'s>> Db { payload: &str, params: &Map, ) -> Result { + #[cfg(not(feature = "wasm"))] let start = Instant::now(); match self.do_run_script(payload, params) { Ok(mut json) => { - let took = start.elapsed().as_secs_f64(); - let map = json.as_object_mut().unwrap(); - map.insert("ok".to_string(), json!(true)); - map.insert("took".to_string(), json!(took)); + { + #[cfg(not(feature = "wasm"))] + let took = start.elapsed().as_secs_f64(); + let map = json.as_object_mut().unwrap(); + map.insert("ok".to_string(), json!(true)); + #[cfg(not(feature = "wasm"))] + map.insert("took".to_string(), json!(took)); + } Ok(json) } err => err, @@ -231,6 +236,7 @@ impl<'s, S: Storage<'s>> Db { let (q_res, q_cleanups) = self.run_query(&mut tx, p)?; res = q_res; cleanups.extend(q_cleanups); + #[cfg(not(feature = "wasm"))] if let Some(secs) = sleep_opt { thread::sleep(Duration::from_micros((secs * 1000000.) as u64)); } @@ -558,12 +564,17 @@ impl<'s, S: Storage<'s>> Db { } let id = self.queries_count.fetch_add(1, Ordering::AcqRel); + #[cfg(not(feature = "wasm"))] let now = SystemTime::now(); + #[cfg(not(feature = "wasm"))] let since_the_epoch = now .duration_since(UNIX_EPOCH) .into_diagnostic()? .as_secs_f64(); + #[cfg(feature = "wasm")] + let since_the_epoch = 0.; + let handle = RunningQueryHandle { started_at: since_the_epoch, poison: poison.clone(), diff --git a/cozo-lib-wasm/.appveyor.yml b/cozo-lib-wasm/.appveyor.yml new file mode 100644 index 00000000..50910bd6 --- /dev/null +++ b/cozo-lib-wasm/.appveyor.yml @@ -0,0 +1,11 @@ +install: + - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe + - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly + - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin + - rustc -V + - cargo -V + +build: false + +test_script: + - cargo test --locked diff --git a/cozo-lib-wasm/.gitignore b/cozo-lib-wasm/.gitignore new file mode 100644 index 00000000..4e301317 --- /dev/null +++ b/cozo-lib-wasm/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log diff --git a/cozo-lib-wasm/.travis.yml b/cozo-lib-wasm/.travis.yml new file mode 100644 index 00000000..7a913256 --- /dev/null +++ b/cozo-lib-wasm/.travis.yml @@ -0,0 +1,69 @@ +language: rust +sudo: false + +cache: cargo + +matrix: + include: + + # Builds with wasm-pack. + - rust: beta + env: RUST_BACKTRACE=1 + addons: + firefox: latest + chrome: stable + before_script: + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) + - cargo install-update -a + - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f + script: + - cargo generate --git . --name testing + # Having a broken Cargo.toml (in that it has curlies in fields) anywhere + # in any of our parent dirs is problematic. + - mv Cargo.toml Cargo.toml.tmpl + - cd testing + - wasm-pack build + - wasm-pack test --chrome --firefox --headless + + # Builds on nightly. + - rust: nightly + env: RUST_BACKTRACE=1 + before_script: + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) + - cargo install-update -a + - rustup target add wasm32-unknown-unknown + script: + - cargo generate --git . --name testing + - mv Cargo.toml Cargo.toml.tmpl + - cd testing + - cargo check + - cargo check --target wasm32-unknown-unknown + - cargo check --no-default-features + - cargo check --target wasm32-unknown-unknown --no-default-features + - cargo check --no-default-features --features console_error_panic_hook + - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook + - cargo check --no-default-features --features "console_error_panic_hook wee_alloc" + - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc" + + # Builds on beta. + - rust: beta + env: RUST_BACKTRACE=1 + before_script: + - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) + - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) + - cargo install-update -a + - rustup target add wasm32-unknown-unknown + script: + - cargo generate --git . --name testing + - mv Cargo.toml Cargo.toml.tmpl + - cd testing + - cargo check + - cargo check --target wasm32-unknown-unknown + - cargo check --no-default-features + - cargo check --target wasm32-unknown-unknown --no-default-features + - cargo check --no-default-features --features console_error_panic_hook + - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook + # Note: no enabling the `wee_alloc` feature here because it requires + # nightly for now. diff --git a/cozo-lib-wasm/Cargo.toml b/cozo-lib-wasm/Cargo.toml new file mode 100644 index 00000000..63f7f0ac --- /dev/null +++ b/cozo-lib-wasm/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "cozo-lib-wasm" +version = "0.1.0" +authors = ["Ziyang Hu "] +edition = "2021" +license = "MPL-2.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2.63" +cozo = { version = "0.1.7", path = "../cozo-core", default-features = false, features = ["wasm", "graph-algo", "nothread"] } + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.6", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +wee_alloc = { version = "0.4.5", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3.13" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/cozo-lib-wasm/src/lib.rs b/cozo-lib-wasm/src/lib.rs new file mode 100644 index 00000000..c8dc8705 --- /dev/null +++ b/cozo-lib-wasm/src/lib.rs @@ -0,0 +1,41 @@ +/* + * Copyright 2022, The Cozo Project Authors. + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +use wasm_bindgen::prelude::*; + +use cozo::*; + +mod utils; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[wasm_bindgen] +extern "C" { + fn alert(s: &str); +} + +#[wasm_bindgen] +pub struct CozoDb { + db: Db, +} + +#[wasm_bindgen] +impl CozoDb { + pub fn new() -> Self { + utils::set_panic_hook(); + let db = new_cozo_mem().unwrap(); + Self { db } + } + pub fn run(&self, script: &str, params: &str) -> String { + self.db.run_script_str(script, params) + } +} diff --git a/cozo-lib-wasm/src/utils.rs b/cozo-lib-wasm/src/utils.rs new file mode 100644 index 00000000..5a0536ba --- /dev/null +++ b/cozo-lib-wasm/src/utils.rs @@ -0,0 +1,18 @@ +/* + * Copyright 2022, The Cozo Project Authors. + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/cozo-lib-wasm/tests/web.rs b/cozo-lib-wasm/tests/web.rs new file mode 100644 index 00000000..7efda3f8 --- /dev/null +++ b/cozo-lib-wasm/tests/web.rs @@ -0,0 +1,21 @@ +/* + * Copyright 2022, The Cozo Project Authors. + * + * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. + * If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +//! Test suite for the Web and headless browsers. + +#![cfg(target_arch = "wasm32")] + +extern crate wasm_bindgen_test; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn pass() { + assert_eq!(1 + 1, 2); +}