diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 3576d66ac59..71937578a43 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -183,7 +183,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 # on a successful run, runs in 8 minutes container: - image: rust:1.83.0 + image: rust:1.87.0 options: --privileged # filter for a comment containing 'benchmarks please' if: ${{ github.event_name != 'issue_comment' || (github.event.issue.pull_request && contains(github.event.comment.body, 'benchmarks please')) }} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000000..648391fd827 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "arrowParens": "avoid", + "jsxSingleQuote": false, + "trailingComma": "es5", + "endOfLine": "auto", + "printWidth": 100 +} diff --git a/Cargo.lock b/Cargo.lock index 94019cbd817..17f46bcddee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,6 +429,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.101", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -712,6 +732,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -776,6 +805,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "3.2.23" @@ -1875,6 +1915,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -2058,6 +2108,15 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "gzip-header" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" +dependencies = [ + "crc32fast", +] + [[package]] name = "h2" version = "0.3.26" @@ -2477,7 +2536,7 @@ dependencies = [ "shlex", "tempfile", "version-compare", - "which", + "which 4.4.2", ] [[package]] @@ -2942,6 +3001,16 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libloading" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" +dependencies = [ + "cfg-if", + "windows-targets 0.53.0", +] + [[package]] name = "libm" version = "0.2.15" @@ -3157,6 +3226,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.8" @@ -3284,6 +3359,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3878,6 +3963,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +dependencies = [ + "proc-macro2", + "syn 2.0.101", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -5465,6 +5560,7 @@ dependencies = [ "lazy_static", "log", "memchr", + "num-traits", "once_cell", "openssl", "parking_lot 0.12.3", @@ -5537,6 +5633,7 @@ dependencies = [ "url", "urlencoding", "uuid", + "v8", "wasmtime", ] @@ -7143,6 +7240,22 @@ dependencies = [ "getrandom 0.3.2", ] +[[package]] +name = "v8" +version = "137.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be127b878f582d3b7602bd2e26a39f90a95b388deb940da7f6554617aedcdf40" +dependencies = [ + "bindgen", + "bitflags 2.9.0", + "fslock", + "gzip-header", + "home", + "miniz_oxide", + "paste", + "which 6.0.3", +] + [[package]] name = "valuable" version = "0.1.1" @@ -7617,6 +7730,18 @@ dependencies = [ "rustix 0.38.44", ] +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix 0.38.44", + "winsafe", +] + [[package]] name = "whoami" version = "1.6.0" @@ -8099,6 +8224,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index 3555d69d517..56e1ff6f402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,7 +90,7 @@ debug = true version = "1.2.0" edition = "2021" # update rust-toolchain.toml too! -rust-version = "1.84.0" +rust-version = "1.87.0" [workspace.dependencies] spacetimedb = { path = "crates/bindings", version = "1.2.0" } @@ -258,7 +258,6 @@ termcolor = "1.2.0" thin-vec = "0.2.13" thiserror = "1.0.37" tokio = { version = "1.37", features = ["full"] } -tokio_metrics = { version = "0.4.0" } tokio-postgres = { version = "0.7.8", features = ["with-chrono-0_4"] } tokio-stream = "0.1.17" tokio-tungstenite = { version = "0.26.2", features = ["native-tls"] } @@ -322,3 +321,10 @@ features = [ "broadcast", "ondemand", ] + +[workspace.lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] } + +[workspace.lints.clippy] +# FIXME: we should work on this lint incrementally +result_large_err = "allow" diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml index 775830ebbfb..a5592c08f5f 100644 --- a/crates/auth/Cargo.toml +++ b/crates/auth/Cargo.toml @@ -16,3 +16,6 @@ jsonwebtoken.workspace = true [dev-dependencies] serde_json.workspace = true + +[lints] +workspace = true diff --git a/crates/bench/Cargo.toml b/crates/bench/Cargo.toml index ce6a138c2c4..ee3ff580e29 100644 --- a/crates/bench/Cargo.toml +++ b/crates/bench/Cargo.toml @@ -89,3 +89,6 @@ tikv-jemalloc-ctl = { workspace = true } iai-callgrind = { git = "https://github.com/clockworklabs/iai-callgrind.git", branch = "main" } iai-callgrind-runner = { git = "https://github.com/clockworklabs/iai-callgrind.git", branch = "main" } iai-callgrind-macros = { git = "https://github.com/clockworklabs/iai-callgrind.git", branch = "main" } + +[lints] +workspace = true diff --git a/crates/bench/Dockerfile b/crates/bench/Dockerfile index 061cb337815..77390540b10 100644 --- a/crates/bench/Dockerfile +++ b/crates/bench/Dockerfile @@ -3,7 +3,7 @@ # See the README for commands to run. # sync with: ../../rust-toolchain.toml -FROM rust:1.84.0 +FROM rust:1.87.0 RUN apt-get update && \ apt-get install -y valgrind bash && \ diff --git a/crates/bindings-macro/Cargo.toml b/crates/bindings-macro/Cargo.toml index 4fc2acbccec..9e0094df23a 100644 --- a/crates/bindings-macro/Cargo.toml +++ b/crates/bindings-macro/Cargo.toml @@ -19,3 +19,6 @@ proc-macro2.workspace = true quote.workspace = true syn.workspace = true heck.workspace = true + +[lints] +workspace = true diff --git a/crates/bindings-macro/src/sats.rs b/crates/bindings-macro/src/sats.rs index 98c608306b3..3c540838476 100644 --- a/crates/bindings-macro/src/sats.rs +++ b/crates/bindings-macro/src/sats.rs @@ -347,7 +347,7 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream { de_generics.params.insert(0, de_lt_param.into()); let (de_impl_generics, _, de_where_clause) = de_generics.split_for_impl(); - let (iter_n, iter_n2, iter_n3) = (0usize.., 0usize.., 0usize..); + let (iter_n, iter_n2, iter_n3, iter_n4) = (0usize.., 0usize.., 0usize.., 0usize..); match &ty.data { SatsTypeData::Product(fields) => { @@ -447,12 +447,23 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream { names.extend::<&[&str]>(&[#(#field_strings),*]) } + fn nth_name(&self, i: usize) -> Option<&str> { + [#(#field_strings),*].get(i).copied() + } + fn visit<__E: #spacetimedb_lib::de::Error>(self, name: &str) -> Result { match name { #(#field_strings => Ok(__ProductFieldIdent::#field_names),)* _ => Err(#spacetimedb_lib::de::Error::unknown_field_name(name, &self)), } } + + fn visit_seq<__E: #spacetimedb_lib::de::Error>(self, i: usize) -> Result { + match i { + #(#iter_n4 => Ok(__ProductFieldIdent::#field_names),)* + _ => Err(#spacetimedb_lib::de::Error::invalid_product_length(i.saturating_add(1), &self)), + } + } } #[allow(non_camel_case_types)] diff --git a/crates/bindings-sys/Cargo.toml b/crates/bindings-sys/Cargo.toml index 23758e89df7..ee8fb474b6d 100644 --- a/crates/bindings-sys/Cargo.toml +++ b/crates/bindings-sys/Cargo.toml @@ -15,3 +15,6 @@ unstable = [] [dependencies] spacetimedb-primitives.workspace = true + +[lints] +workspace = true diff --git a/crates/bindings-sys/src/lib.rs b/crates/bindings-sys/src/lib.rs index a9cc8c968e5..b901de7e5d7 100644 --- a/crates/bindings-sys/src/lib.rs +++ b/crates/bindings-sys/src/lib.rs @@ -148,7 +148,7 @@ pub mod raw { /// /// Traps if: /// - `prefix_elems > 0` - /// and (`prefix_ptr` is NULL or `prefix` is not in bounds of WASM memory). + /// and (`prefix_ptr` is NULL or `prefix` is not in bounds of WASM memory). /// - `rstart` is NULL or `rstart` is not in bounds of WASM memory. /// - `rend` is NULL or `rend` is not in bounds of WASM memory. /// - `out` is NULL or `out[..size_of::()]` is not in bounds of WASM memory. @@ -161,11 +161,11 @@ pub mod raw { /// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index. /// - `WRONG_INDEX_ALGO` if the index is not a range-compatible index. /// - `BSATN_DECODE_ERROR`, when `prefix` cannot be decoded to - /// a `prefix_elems` number of `AlgebraicValue` - /// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. - /// Or when `rstart` or `rend` cannot be decoded to an `Bound` - /// where the inner `AlgebraicValue`s are - /// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. + /// a `prefix_elems` number of `AlgebraicValue` + /// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. + /// Or when `rstart` or `rend` cannot be decoded to an `Bound` + /// where the inner `AlgebraicValue`s are + /// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. pub fn datastore_index_scan_range_bsatn( index_id: IndexId, prefix_ptr: *const u8, @@ -212,7 +212,7 @@ pub mod raw { /// /// Traps if: /// - `prefix_elems > 0` - /// and (`prefix_ptr` is NULL or `prefix` is not in bounds of WASM memory). + /// and (`prefix_ptr` is NULL or `prefix` is not in bounds of WASM memory). /// - `rstart` is NULL or `rstart` is not in bounds of WASM memory. /// - `rend` is NULL or `rend` is not in bounds of WASM memory. /// - `out` is NULL or `out[..size_of::()]` is not in bounds of WASM memory. @@ -225,11 +225,11 @@ pub mod raw { /// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index. /// - `WRONG_INDEX_ALGO` if the index is not a range-compatible index. /// - `BSATN_DECODE_ERROR`, when `prefix` cannot be decoded to - /// a `prefix_elems` number of `AlgebraicValue` - /// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. - /// Or when `rstart` or `rend` cannot be decoded to an `Bound` - /// where the inner `AlgebraicValue`s are - /// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. + /// a `prefix_elems` number of `AlgebraicValue` + /// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. + /// Or when `rstart` or `rend` cannot be decoded to an `Bound` + /// where the inner `AlgebraicValue`s are + /// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. pub fn datastore_delete_by_index_scan_range_bsatn( index_id: IndexId, prefix_ptr: *const u8, @@ -364,7 +364,7 @@ pub mod raw { /// - `NOT_IN_TRANSACTION`, when called outside of a transaction. /// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table. /// - `BSATN_DECODE_ERROR`, when `row` cannot be decoded to a `ProductValue`. - /// typed at the `ProductType` the table's schema specifies. + /// typed at the `ProductType` the table's schema specifies. /// - `UNIQUE_ALREADY_EXISTS`, when inserting `row` would violate a unique constraint. /// - `SCHEDULE_AT_DELAY_TOO_LONG`, when the delay specified in the row was too long. pub fn datastore_insert_bsatn(table_id: TableId, row_ptr: *mut u8, row_len_ptr: *mut usize) -> u16; @@ -406,8 +406,8 @@ pub mod raw { /// - `INDEX_NOT_UNIQUE`, when the index was not unique. /// - `NO_SUCH_ROW`, when the row was not found in the unique index. /// - `BSATN_DECODE_ERROR`, when `row` cannot be decoded to a `ProductValue` - /// typed at the `ProductType` the table's schema specifies - /// or when it cannot be projected to the index identified by `index_id`. + /// typed at the `ProductType` the table's schema specifies + /// or when it cannot be projected to the index identified by `index_id`. /// - `UNIQUE_ALREADY_EXISTS`, when inserting `row` would violate a unique constraint. /// - `SCHEDULE_AT_DELAY_TOO_LONG`, when the delay specified in the row was too long. pub fn datastore_update_bsatn( @@ -942,11 +942,11 @@ pub fn datastore_table_scan_bsatn(table_id: TableId) -> Result { /// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index. /// - `WRONG_INDEX_ALGO` if the index is not a range-compatible index. /// - `BSATN_DECODE_ERROR`, when `prefix` cannot be decoded to -/// a `prefix_elems` number of `AlgebraicValue` -/// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. -/// Or when `rstart` or `rend` cannot be decoded to an `Bound` -/// where the inner `AlgebraicValue`s are -/// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. +/// a `prefix_elems` number of `AlgebraicValue` +/// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. +/// Or when `rstart` or `rend` cannot be decoded to an `Bound` +/// where the inner `AlgebraicValue`s are +/// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. pub fn datastore_index_scan_range_bsatn( index_id: IndexId, prefix: &[u8], @@ -990,11 +990,11 @@ pub fn datastore_index_scan_range_bsatn( /// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index. /// - `WRONG_INDEX_ALGO` if the index is not a range-compatible index. /// - `BSATN_DECODE_ERROR`, when `prefix` cannot be decoded to -/// a `prefix_elems` number of `AlgebraicValue` -/// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. -/// Or when `rstart` or `rend` cannot be decoded to an `Bound` -/// where the inner `AlgebraicValue`s are -/// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. +/// a `prefix_elems` number of `AlgebraicValue` +/// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. +/// Or when `rstart` or `rend` cannot be decoded to an `Bound` +/// where the inner `AlgebraicValue`s are +/// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. pub fn datastore_delete_by_index_scan_range_bsatn( index_id: IndexId, prefix: &[u8], diff --git a/crates/bindings/Cargo.toml b/crates/bindings/Cargo.toml index 19cea313fac..8e75cc3fce3 100644 --- a/crates/bindings/Cargo.toml +++ b/crates/bindings/Cargo.toml @@ -39,3 +39,6 @@ getrandom02 = { workspace = true, optional = true, features = ["custom"] } [dev-dependencies] insta.workspace = true trybuild.workspace = true + +[lints] +workspace = true diff --git a/crates/bindings/tests/deps.rs b/crates/bindings/tests/deps.rs index b070f3ba372..2383fdc7a1e 100644 --- a/crates/bindings/tests/deps.rs +++ b/crates/bindings/tests/deps.rs @@ -2,45 +2,12 @@ //! to make sure we don't unknowingly add a bunch of dependencies here, //! slowing down compilation for every spacetime module. -// We need to remove the `cpufeatures` and `libc` dependencies from the output, it added on `arm` architecture: -// https://github.com/RustCrypto/sponges/blob/master/keccak/Cargo.toml#L24-L25, breaking local testing. -#[cfg(target_arch = "aarch64")] -fn hack_keccack(cmd: String) -> String { - let mut found = false; - let mut lines = cmd.lines().peekable(); - let mut output = String::new(); - - while let Some(line) = lines.next() { - // Check we only match keccak/cpufeatures/libc - if line.contains("keccak") { - found = true; - } - if found && line.contains("cpufeatures") { - if let Some(next_line) = lines.peek() { - if next_line.contains("libc") { - found = false; - // Skip libc - lines.next(); - continue; - } - } - } - output.push_str(line); - output.push('\n'); - } - - output -} -#[cfg(not(target_arch = "aarch64"))] -fn hack_keccack(cmd: String) -> String { - cmd -} - #[test] fn deptree_snapshot() -> std::io::Result<()> { - let cmd = "cargo tree -p spacetimedb -f {lib} -e no-dev"; - let deps_tree = hack_keccack(run_cmd(cmd)); - let all_deps = hack_keccack(run_cmd("cargo tree -p spacetimedb -e no-dev --prefix none --no-dedupe")); + let cmd_common = "cargo tree -p spacetimedb -e no-dev --color never --target wasm32-unknown-unknown"; + let cmd = &format!("{cmd_common} -f {{lib}}"); + let deps_tree = run_cmd(cmd); + let all_deps = run_cmd(&format!("{cmd_common} --prefix none --no-dedupe")); let mut all_deps = all_deps.lines().collect::>(); all_deps.sort(); all_deps.dedup(); @@ -52,7 +19,7 @@ fn deptree_snapshot() -> std::io::Result<()> { cmd ); - let cmd = "cargo tree -p spacetimedb -e no-dev -d --depth 0"; + let cmd = &format!("{cmd_common} -d --depth 0"); insta::assert_snapshot!("duplicate_deps", run_cmd(cmd), cmd); Ok(()) diff --git a/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap b/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap index 3162fc6564e..4828a6ef1f1 100644 --- a/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap +++ b/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap @@ -1,8 +1,8 @@ --- source: crates/bindings/tests/deps.rs -expression: "cargo tree -p spacetimedb -f {lib} -e no-dev" +expression: "cargo tree -p spacetimedb -e no-dev --color never --target wasm32-unknown-unknown -f {lib}" --- -total crates: 66 +total crates: 65 spacetimedb ├── bytemuck ├── derive_more @@ -19,11 +19,9 @@ spacetimedb │ └── rustc_version │ └── semver ├── getrandom -│ ├── cfg_if -│ └── libc +│ └── cfg_if ├── log ├── rand -│ ├── libc │ ├── rand_chacha │ │ ├── ppv_lite86 │ │ │ └── zerocopy @@ -44,7 +42,12 @@ spacetimedb │ │ └── nohash_hasher │ └── syn (*) ├── spacetimedb_bindings_sys -│ └── spacetimedb_primitives (*) +│ └── spacetimedb_primitives +│ ├── bitflags +│ ├── either +│ ├── itertools +│ │ └── either +│ └── nohash_hasher ├── spacetimedb_lib │ ├── anyhow │ ├── bitflags diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 6f9d290206f..1ec691324ad 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -81,3 +81,6 @@ tikv-jemalloc-ctl = { workspace = true } [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = ["Win32_System_Console"] } + +[lints] +workspace = true diff --git a/crates/cli/src/config.rs b/crates/cli/src/config.rs index 1e709b892f6..233a1461dfc 100644 --- a/crates/cli/src/config.rs +++ b/crates/cli/src/config.rs @@ -395,7 +395,7 @@ Fetch the server's fingerprint with: let cfg = self.find_server_mut(server)?; let old_nickname = if let Some(new_nickname) = new_nickname { - std::mem::replace(&mut cfg.nickname, Some(new_nickname.to_string())) + cfg.nickname.replace(new_nickname.to_string()) } else { None }; diff --git a/crates/cli/src/subcommands/logs.rs b/crates/cli/src/subcommands/logs.rs index cdfa2651b2a..f3abb302ad5 100644 --- a/crates/cli/src/subcommands/logs.rs +++ b/crates/cli/src/subcommands/logs.rs @@ -156,10 +156,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E let out = termcolor::StandardStream::stdout(term_color); let mut out = out.lock(); - let mut rdr = res - .bytes_stream() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) - .into_async_read(); + let mut rdr = res.bytes_stream().map_err(io::Error::other).into_async_read(); let mut line = String::new(); while rdr.read_line(&mut line).await? != 0 { let record = serde_json::from_str::>(&line)?; diff --git a/crates/client-api-messages/Cargo.toml b/crates/client-api-messages/Cargo.toml index 90f767ee364..83c5b2ade97 100644 --- a/crates/client-api-messages/Cargo.toml +++ b/crates/client-api-messages/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license-file = "LICENSE" description = "Types for the SpacetimeDB client API messages" +rust-version.workspace = true [dependencies] spacetimedb-lib = { workspace = true, features = ["serde"] } @@ -29,3 +30,6 @@ hex.workspace = true itertools.workspace = true proptest.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/crates/client-api/Cargo.toml b/crates/client-api/Cargo.toml index bd30f31da8d..ff3477c1882 100644 --- a/crates/client-api/Cargo.toml +++ b/crates/client-api/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license-file = "LICENSE" description = "The HTTP API for SpacetimeDB" +rust-version.workspace = true [dependencies] spacetimedb-client-api-messages.workspace = true @@ -54,3 +55,6 @@ jemalloc_pprof.workspace = true [dev-dependencies] jsonwebtoken.workspace = true + +[lints] +workspace = true diff --git a/crates/codegen/Cargo.toml b/crates/codegen/Cargo.toml index b18d54209d2..54e0f589669 100644 --- a/crates/codegen/Cargo.toml +++ b/crates/codegen/Cargo.toml @@ -21,3 +21,6 @@ fs-err.workspace = true insta.workspace = true regex.workspace = true spacetimedb-testing = { path = "../testing" } + +[lints] +workspace = true diff --git a/crates/commitlog/Cargo.toml b/crates/commitlog/Cargo.toml index 99c4fb2a42f..b3fbe077d10 100644 --- a/crates/commitlog/Cargo.toml +++ b/crates/commitlog/Cargo.toml @@ -50,3 +50,6 @@ proptest.workspace = true rand.workspace = true tempfile.workspace = true tokio-stream = { version = "0.1.17", features = ["fs"] } + +[lints] +workspace = true diff --git a/crates/commitlog/src/index/indexfile.rs b/crates/commitlog/src/index/indexfile.rs index 984318bce48..98e0198b5a3 100644 --- a/crates/commitlog/src/index/indexfile.rs +++ b/crates/commitlog/src/index/indexfile.rs @@ -61,13 +61,13 @@ impl + From> IndexFileMut { num_entries: 0, _marker: PhantomData, }; - me.num_entries = me.num_entries().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + me.num_entries = me.num_entries().map_err(io::Error::other)?; Ok(me) } pub fn delete_index_file(path: &OffsetIndexFile) -> io::Result<()> { - fs::remove_file(path).map_err(Into::into) + fs::remove_file(path) } // Searches for first 0-key, to count number of entries @@ -267,9 +267,7 @@ impl + From> IndexFile { num_entries: 0, _marker: PhantomData, }; - inner.num_entries = inner - .num_entries() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + inner.num_entries = inner.num_entries().map_err(io::Error::other)?; Ok(Self { inner }) } diff --git a/crates/commitlog/src/repo/mem.rs b/crates/commitlog/src/repo/mem.rs index b0bfd4061c0..9a301e750b8 100644 --- a/crates/commitlog/src/repo/mem.rs +++ b/crates/commitlog/src/repo/mem.rs @@ -223,7 +223,7 @@ impl Repo for Memory { btree_map::Entry::Occupied(entry) => { let entry = entry.get(); let read_guard = entry.read().unwrap(); - if read_guard.len() == 0 { + if read_guard.is_empty() { Ok(Segment::from(Arc::clone(entry))) } else { Err(io::Error::new( diff --git a/crates/commitlog/src/repo/mod.rs b/crates/commitlog/src/repo/mod.rs index 7669421d356..7129b3be314 100644 --- a/crates/commitlog/src/repo/mod.rs +++ b/crates/commitlog/src/repo/mod.rs @@ -106,17 +106,17 @@ pub trait Repo: Clone { /// Create [`TxOffsetIndexMut`] for the given `offset` or open it if already exist. /// The `cap` parameter is the maximum number of entries in the index. fn create_offset_index(&self, _offset: TxOffset, _cap: u64) -> io::Result { - Err(io::Error::new(io::ErrorKind::Other, "not implemented")) + Err(io::Error::other("not implemented")) } /// Remove [`TxOffsetIndexMut`] named with `offset`. fn remove_offset_index(&self, _offset: TxOffset) -> io::Result<()> { - Err(io::Error::new(io::ErrorKind::Other, "not implemented")) + Err(io::Error::other("not implemented")) } /// Get [`TxOffsetIndex`] for the given `offset`. fn get_offset_index(&self, _offset: TxOffset) -> io::Result { - Err(io::Error::new(io::ErrorKind::Other, "not implemented")) + Err(io::Error::other("not implemented")) } } diff --git a/crates/commitlog/src/segment.rs b/crates/commitlog/src/segment.rs index 88faef9c4b6..d2b14e1ed23 100644 --- a/crates/commitlog/src/segment.rs +++ b/crates/commitlog/src/segment.rs @@ -348,12 +348,8 @@ impl FileLike for IndexFileMut { } fn ftruncate(&mut self, tx_offset: u64, _size: u64) -> io::Result<()> { - self.truncate(tx_offset).map_err(|e| { - io::Error::new( - ErrorKind::Other, - format!("failed to truncate offset index at {tx_offset}: {e:?}"), - ) - }) + self.truncate(tx_offset) + .map_err(|e| io::Error::other(format!("failed to truncate offset index at {tx_offset}: {e:?}"))) } } @@ -621,7 +617,7 @@ impl Metadata { /// /// Returns /// * `Ok((Metadata)` - If a valid commit is found containing the commit, It adds a default - /// header, which should be replaced with the actual header. + /// header, which should be replaced with the actual header. /// * `Err` - If no valid commit is found or if the index is empty fn find_valid_indexed_commit( min_tx_offset: u64, diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 0c473b72f7f..6758133e56d 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -114,6 +114,11 @@ jwks.workspace = true async_cache = "0.3.1" faststr = "0.2.23" +v8 = "137" +num-traits = "0.2" + +# sourcemap = "9" + [target.'cfg(not(target_env = "msvc"))'.dependencies] tikv-jemallocator = {workspace = true} tikv-jemalloc-ctl = {workspace = true} @@ -145,5 +150,5 @@ jsonwebtoken.workspace = true axum.workspace = true reqwest.workspace = true -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] } +[lints] +workspace = true diff --git a/crates/core/src/db/datastore/locking_tx_datastore/mut_tx.rs b/crates/core/src/db/datastore/locking_tx_datastore/mut_tx.rs index 52eee235d98..bcfbc8fb708 100644 --- a/crates/core/src/db/datastore/locking_tx_datastore/mut_tx.rs +++ b/crates/core/src/db/datastore/locking_tx_datastore/mut_tx.rs @@ -436,7 +436,7 @@ impl MutTxId { /// - `index.index_id == IndexId::SENTINEL` /// - `index.table_id != TableId::SENTINEL` /// - `is_unique` must be `true` if and only if a unique constraint will exist on - /// `ColSet::from(&index.index_algorithm.columns())` after this transaction is committed. + /// `ColSet::from(&index.index_algorithm.columns())` after this transaction is committed. /// /// Ensures: /// - The index metadata is inserted into the system tables (and other data structures reflecting them). @@ -698,7 +698,7 @@ impl MutTxId { let mut vals: Vec<_> = prefix.elements.into(); vals.reserve(1 + suffix_len); vals.push(val); - vals.extend(iter::repeat(fill).take(suffix_len)); + vals.extend(iter::repeat_n(fill, suffix_len)); AlgebraicValue::product(vals) }; // The start endpoint needs `Min` as the suffix-filling element, @@ -899,7 +899,7 @@ impl MutTxId { /// - `constraint.constraint_id == ConstraintId::SENTINEL` /// - `constraint.table_id != TableId::SENTINEL` /// - `is_unique` must be `true` if and only if a unique constraint will exist on - /// `ColSet::from(&constraint.constraint_algorithm.columns())` after this transaction is committed. + /// `ColSet::from(&constraint.constraint_algorithm.columns())` after this transaction is committed. /// /// Ensures: /// - The constraint metadata is inserted into the system tables (and other data structures reflecting them). diff --git a/crates/core/src/db/datastore/system_tables.rs b/crates/core/src/db/datastore/system_tables.rs index f3760732884..a0a33fa518f 100644 --- a/crates/core/src/db/datastore/system_tables.rs +++ b/crates/core/src/db/datastore/system_tables.rs @@ -813,10 +813,21 @@ impl From for RowLevelSecuritySchema { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ModuleKind(u8); -/// The [`ModuleKind`] of WASM-based modules. -/// -/// This is currently the only known kind. -pub const WASM_MODULE: ModuleKind = ModuleKind(0); +impl ModuleKind { + /// The [`ModuleKind`] of WASM-based modules. + pub const WASM: ModuleKind = ModuleKind(0); + /// The [`ModuleKind`] of JS modules. + pub const JS: ModuleKind = ModuleKind(1); +} + +impl From for ModuleKind { + fn from(host_type: crate::messages::control_db::HostType) -> Self { + match host_type { + crate::messages::control_db::HostType::Wasm => Self::WASM, + crate::messages::control_db::HostType::Js => Self::JS, + } + } +} impl_serialize!([] ModuleKind, (self, ser) => self.0.serialize(ser)); impl_deserialize!([] ModuleKind, de => u8::deserialize(de).map(Self)); diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index 0c16db7fb82..136dfad0e7a 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -17,7 +17,7 @@ use super::datastore::{ traits::TxData, }; use super::db_metrics::DB_METRICS; -use crate::db::datastore::system_tables::{StModuleRow, WASM_MODULE}; +use crate::db::datastore::system_tables::StModuleRow; use crate::error::{DBError, DatabaseError, RestoreSnapshotError}; use crate::execution_context::{ReducerContext, Workload, WorkloadType}; use crate::messages::control_db::HostType; @@ -427,9 +427,7 @@ impl RelationalDB { database_identity: self.database_identity.into(), owner_identity: self.owner_identity.into(), - program_kind: match host_type { - HostType::Wasm => WASM_MODULE, - }, + program_kind: host_type.into(), program_hash: program.hash, program_bytes: program.bytes, module_version: ONLY_MODULE_VERSION.into(), @@ -473,10 +471,7 @@ impl RelationalDB { /// - the `__init__` reducer contained in the module has been executed /// within the transactional context `tx`. pub fn update_program(&self, tx: &mut MutTx, host_type: HostType, program: Program) -> Result<(), DBError> { - let program_kind = match host_type { - HostType::Wasm => WASM_MODULE, - }; - Ok(self.inner.update_program(tx, program_kind, program)?) + Ok(self.inner.update_program(tx, host_type.into(), program)?) } fn restore_from_snapshot_or_bootstrap( diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 211a37f6c32..77831b578e7 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -82,6 +82,8 @@ pub enum DatabaseError { DatabasedOpened(PathBuf, anyhow::Error), } +// FIXME: reduce type size +#[expect(clippy::large_enum_variant)] #[derive(Error, Debug, EnumAsInner)] pub enum DBError { #[error("LibError: {0}")] diff --git a/crates/core/src/host/host_controller.rs b/crates/core/src/host/host_controller.rs index bb2c47cfc75..aafbeac8391 100644 --- a/crates/core/src/host/host_controller.rs +++ b/crates/core/src/host/host_controller.rs @@ -1,5 +1,6 @@ use super::module_host::{EventStatus, ModuleHost, ModuleInfo, NoSuchModule}; use super::scheduler::SchedulerStarter; +use super::v8::V8Runtime; use super::wasmtime::WasmtimeRuntime; use super::{Scheduler, UpdateDatabaseResult}; use crate::database_logger::DatabaseLogger; @@ -99,12 +100,14 @@ pub struct HostController { struct HostRuntimes { wasmtime: WasmtimeRuntime, + v8: V8Runtime, } impl HostRuntimes { fn new(data_dir: Option<&ServerDataDir>) -> Arc { let wasmtime = WasmtimeRuntime::new(data_dir); - Arc::new(Self { wasmtime }) + let v8 = V8Runtime::new(); + Arc::new(Self { wasmtime, v8 }) } } @@ -566,19 +569,23 @@ async fn make_module_host( unregister: impl Fn() + Send + Sync + 'static, ) -> anyhow::Result<(Program, ModuleHost)> { spawn_rayon(move || { + let mcc = ModuleCreationContext { + replica_ctx, + scheduler, + program: &program, + energy_monitor, + }; let module_host = match host_type { HostType::Wasm => { - let mcc = ModuleCreationContext { - replica_ctx, - scheduler, - program: &program, - energy_monitor, - }; let start = Instant::now(); let actor = runtimes.wasmtime.make_actor(mcc)?; trace!("wasmtime::make_actor blocked for {:?}", start.elapsed()); ModuleHost::new(actor, unregister) } + HostType::Js => { + let actor = runtimes.v8.make_actor(mcc)?; + ModuleHost::new(actor, unregister) + } }; Ok((program, module_host)) }) diff --git a/crates/core/src/host/mod.rs b/crates/core/src/host/mod.rs index 151e6d233a8..3598ae9e252 100644 --- a/crates/core/src/host/mod.rs +++ b/crates/core/src/host/mod.rs @@ -18,6 +18,7 @@ pub mod scheduler; pub mod wasmtime; // Visible for integration testing. pub mod instance_env; +pub mod v8; // only pub for testing mod wasm_common; pub use disk_storage::DiskStorage; diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index fa5dad0be41..d685fb87ed9 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -583,6 +583,8 @@ pub enum InitDatabaseError { Other(anyhow::Error), } +// FIXME: reduce type size +#[expect(clippy::large_enum_variant)] #[derive(thiserror::Error, Debug)] pub enum ClientConnectedError { #[error(transparent)] @@ -749,7 +751,6 @@ impl ModuleHost { .inspect_err(|e| { log::error!("`call_identity_connected`: fallback transaction to insert into `st_client` failed: {e:#?}") }) - .map_err(DBError::from) .map_err(Into::into) } } @@ -990,7 +991,6 @@ impl ModuleHost { }) .await .unwrap_or_else(|e| Err(e.into())) - .map_err(Into::into) } pub fn subscribe_to_logs(&self) -> anyhow::Result> { @@ -1016,7 +1016,6 @@ impl ModuleHost { inst.update_database(program, old_module_info) }) .await? - .map_err(Into::into) } pub async fn exit(&self) { diff --git a/crates/core/src/host/v8/.gitignore b/crates/core/src/host/v8/.gitignore new file mode 100644 index 00000000000..a6c7c2852d0 --- /dev/null +++ b/crates/core/src/host/v8/.gitignore @@ -0,0 +1 @@ +*.js diff --git a/crates/core/src/host/v8/convert.rs b/crates/core/src/host/v8/convert.rs new file mode 100644 index 00000000000..a5da4de24e9 --- /dev/null +++ b/crates/core/src/host/v8/convert.rs @@ -0,0 +1,152 @@ +use super::util::{IntoException, IntoExceptionResultExt, TypeError, ValueResult}; +use num_traits::ToPrimitive; +use spacetimedb_sats::{i256, u256}; + +pub(super) trait FromValue: Sized { + fn from_value<'s>(scope: &mut v8::HandleScope<'s>, val: v8::Local<'_, v8::Value>) -> ValueResult<'s, Self>; +} + +impl FromValue for bool { + fn from_value<'s>(scope: &mut v8::HandleScope<'s>, val: v8::Local<'_, v8::Value>) -> ValueResult<'s, Self> { + let b = cast!(val, v8::Boolean, "boolean").map_err(|e| e.into_exception(scope))?; + Ok(b.is_true()) + } +} + +macro_rules! cast { + ($val:expr, $t:ty, $expected:literal $(, $args:expr)* $(,)?) => {{ + let val = $val; + val.try_cast::<$t>() + .map_err(|_| $crate::host::v8::util::TypeError(format!( + concat!("Expected ", $expected, ", got {__got}"), + $($args,)* + __got = val.type_repr() + ))) + }}; +} +pub(super) use cast; + +macro_rules! num_from_value { + ($($t:ident: $to:ident),*) => { + $(impl FromValue for $t { + fn from_value<'s>(scope: &mut v8::HandleScope<'s>, val: v8::Local<'_, v8::Value>) -> ValueResult<'s, Self> { + let num = cast!(val, v8::Number, "number for {}", stringify!($t)).map_err_exc(scope)?; + num.value() + .$to() + .ok_or_else(|| TypeError(format!("Value overflowed {}", stringify!($t)))) + .map_err_exc(scope) + } + })* + }; + (64bit $($t:ident: $value_method:ident),*) => { + $(impl FromValue for $t { + fn from_value<'s>(scope: &mut v8::HandleScope<'s>, val: v8::Local<'_, v8::Value>) -> ValueResult<'s, Self> { + let int = cast!(val, v8::BigInt, "bigint for {}", stringify!($t)).map_err_exc(scope)?; + let (val, ok) = int.$value_method(); + ok.then_some(val) + .ok_or_else(|| TypeError(format!("Value overflowed {}", stringify!($t)))) + .map_err_exc(scope) + } + })* + }; + (float $($t:ident),*) => { + $(impl FromValue for $t { + fn from_value<'s>(scope: &mut v8::HandleScope<'s>, val: v8::Local<'_, v8::Value>) -> ValueResult<'s, Self> { + let num = cast!(val, v8::Number, "number for {}", stringify!($t)).map_err_exc(scope)?; + Ok(num.value() as _) + } + })* + }; + (large $($t:ident: $value64:ident),*) => { + $(impl FromValue for $t { + fn from_value<'s>(scope: &mut v8::HandleScope<'s>, val: v8::Local<'_, v8::Value>) -> ValueResult<'s, Self> { + let int = cast!(val, v8::BigInt, "bigint for {}", stringify!($t)).map_err_exc(scope)?; + if let (val, true) = int.u64_value() { + return Ok(val.into()); + } + const WORDS: usize = size_of::<$t>() / size_of::(); + let mut err = || TypeError(format!("Value overflowed {}", stringify!($t))).into_exception(scope); + if int.word_count() > WORDS { + #[allow(unused_comparisons)] + if $t::MIN < 0 && int.word_count() == WORDS + 1 { + let mut words = [0u64; WORDS + 1]; + let (sign, _) = int.to_words_array(&mut words); + let [prev @ .., last] = words; + if sign && prev == [0; WORDS] && last == (1 << 63) { + return Ok($t::MIN) + } + } + return Err(err()); + } + let mut words = [0u64; WORDS]; + let (sign, _) = int.to_words_array(&mut words); + let bytes = bytemuck::must_cast(words.map(|w| w.to_le_bytes())); + let x = Self::from_le_bytes(bytes); + if sign { + x.checked_neg().ok_or_else(err) + } else { + Ok(x) + } + } + })* + }; +} + +num_from_value!(u8: to_u8, i8: to_i8, u16: to_u16, i16: to_i16, u32: to_u32, i32: to_i32); + +num_from_value!(64bit u64: u64_value, i64: i64_value); + +num_from_value!(float f32, f64); + +num_from_value!(large u128: u64_value, i128: i64_value, u256: u64_value, i256: i64_value); + +pub(super) trait ToValue { + fn to_value<'s>(&self, scope: &mut v8::HandleScope<'s>) -> ValueResult<'s, v8::Local<'s, v8::Value>>; +} + +impl ToValue for bool { + fn to_value<'s>(&self, scope: &mut v8::HandleScope<'s>) -> ValueResult<'s, v8::Local<'s, v8::Value>> { + Ok(v8::Boolean::new(scope, *self).into()) + } +} + +macro_rules! num_to_value { + ($($t:ident),*) => { + $(impl ToValue for $t { + fn to_value<'s>(&self, scope: &mut v8::HandleScope<'s>) -> ValueResult<'s, v8::Local<'s, v8::Value>> { + Ok(v8::Number::new(scope, *self as f64).into()) + } + })* + }; + (64bit $($t:ident: $new_from:ident),*) => { + $(impl ToValue for $t { + fn to_value<'s>(&self, scope: &mut v8::HandleScope<'s>) -> ValueResult<'s, v8::Local<'s, v8::Value>> { + Ok(v8::BigInt::$new_from(scope, *self).into()) + } + })* + }; + (large $($t:ident),*) => { + $(impl ToValue for $t { + fn to_value<'s>(&self, scope: &mut v8::HandleScope<'s>) -> ValueResult<'s, v8::Local<'s, v8::Value>> { + const WORDS: usize = size_of::<$t>() / size_of::(); + #[allow(unused_comparisons)] + let sign = *self < 0; + let Some(magnitude) = (if sign { self.checked_neg() } else { Some(*self) }) else { + let mut words = [0u64; WORDS + 1]; + let [.., last] = &mut words; + *last = 1 << 63; + return Ok(v8::BigInt::new_from_words(scope, true, &words).unwrap().into()); + }; + let bytes = magnitude.to_le_bytes(); + let words = bytemuck::must_cast::<_, [u64; WORDS]>(bytes).map(u64::from_le); + Ok(v8::BigInt::new_from_words(scope, sign, &words).unwrap().into()) + } + })* + }; +} + +num_to_value!(u8, i8, u16, i16, u32, i32, f32, f64); + +num_to_value!(64bit u64: new_from_u64, i64: new_from_i64); + +num_to_value!(large u128, i128, u256, i256); diff --git a/crates/core/src/host/v8/de.rs b/crates/core/src/host/v8/de.rs new file mode 100644 index 00000000000..4c5422f6a1d --- /dev/null +++ b/crates/core/src/host/v8/de.rs @@ -0,0 +1,353 @@ +use std::borrow::Cow; + +use spacetimedb_sats::{de, i256, u256}; + +use super::convert::{cast, FromValue}; +use super::util::{scratch_buf, ExceptionOptionExt, ExceptionThrown, IntoExceptionResultExt, Throwable, TypeError}; + +pub(super) struct Deserializer<'a, 's> { + common: DeserializerCommon<'a, 's>, + input: v8::Local<'s, v8::Value>, +} + +impl<'a, 's> Deserializer<'a, 's> { + pub fn new( + scope: &'a mut v8::HandleScope<'s>, + input: v8::Local<'_, v8::Value>, + key_cache: &'a mut KeyCache, + ) -> Self { + let input = v8::Local::new(scope, input); + let common = DeserializerCommon { scope, key_cache }; + Deserializer { input, common } + } +} + +struct DeserializerCommon<'a, 's> { + scope: &'a mut v8::HandleScope<'s>, + key_cache: &'a mut KeyCache, +} + +impl<'a, 's> DeserializerCommon<'a, 's> { + fn reborrow(&mut self) -> DeserializerCommon<'_, 's> { + DeserializerCommon { + scope: self.scope, + key_cache: self.key_cache, + } + } +} + +macro_rules! def_key_cache { + ($($key:ident$(: $string:expr)?),* $(,)?) => { + #[derive(Default)] + pub(super) struct KeyCache { + $($key: Option>,)* + } + impl KeyCache { + $(pub(super) fn $key<'s>(&mut self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::String> { + get_or_create_key(scope, &mut self.$key, ($($string,)? stringify!($key),).0) + })* + } + }; +} + +fn get_or_create_key<'s>( + scope: &mut v8::HandleScope<'s>, + key: &mut Option>, + string: &str, +) -> v8::Local<'s, v8::String> { + if let Some(s) = &*key { + v8::Local::new(scope, s) + } else { + let s = v8_struct_key(scope, string); + *key = Some(v8::Global::new(scope, s)); + s + } +} + +def_key_cache!(tag, value, some); + +// creates an optimized v8::String for a struct field +pub fn v8_struct_key<'s>(scope: &mut v8::HandleScope<'s>, field: &str) -> v8::Local<'s, v8::String> { + // Internalized v8 strings are significantly faster than "normal" v8 strings + // since v8 deduplicates re-used strings minimizing new allocations + // see: https://github.com/v8/v8/blob/14ac92e02cc3db38131a57e75e2392529f405f2f/include/v8.h#L3165-L3171 + v8::String::new_from_utf8(scope, field.as_ref(), v8::NewStringType::Internalized).unwrap() +} + +pub(super) enum Error<'s> { + Value(v8::Local<'s, v8::Value>), + Exception(ExceptionThrown), + String(String), +} + +impl<'s> Throwable for Error<'s> { + fn throw(self, scope: &mut v8::HandleScope<'_>) -> ExceptionThrown { + match self { + Error::Value(exc) => exc.throw(scope), + Error::Exception(thrown) => thrown, + Error::String(s) => TypeError(s).throw(scope), + } + } +} + +impl<'s> From for Error<'s> { + fn from(v: ExceptionThrown) -> Self { + Self::Exception(v) + } +} + +impl<'s> From> for Error<'s> { + fn from(v: v8::Local<'s, v8::Value>) -> Self { + Self::Value(v) + } +} + +impl de::Error for Error<'_> { + fn custom(msg: impl core::fmt::Display) -> Self { + Self::String(msg.to_string()) + } +} + +fn extend_local<'s, T>(local: v8::Local<'s, T>) -> &'s T { + unsafe { std::mem::transmute::<&T, &'s T>(&local) } +} + +macro_rules! deserialize_primitive { + ($dmethod:ident, $t:ty) => { + fn $dmethod(self) -> Result<$t, Self::Error> { + FromValue::from_value(self.common.scope, self.input).map_err(Error::Value) + } + }; +} + +impl<'de, 'a, 's: 'de, 'x> de::Deserializer<'de> for Deserializer<'a, 's> { + type Error = Error<'s>; + + fn deserialize_product>(self, visitor: V) -> Result { + let obj = cast!( + self.input, + v8::Object, + "object for product type {}", + visitor.product_name().unwrap_or("") + ) + .map_err_exc(self.common.scope)?; + + visitor.visit_named_product(ProductAccess { + common: self.common, + obj, + next_value: None, + n: 0, + }) + } + + fn deserialize_sum>(self, visitor: V) -> Result { + let scope = &mut *self.common.scope; + let val = if visitor.is_option() { + if self.input.is_null_or_undefined() { + return visitor.visit_sum(de::NoneAccess::new()); + } + let val = cast!(self.input, v8::Object, "nullish or {{some:_}} for option").map_err_exc(scope)?; + let some_field = self.common.key_cache.some(scope); + if val.has_own_property(scope, some_field.into()).err()? { + let value = val.get(scope, some_field.into()).err()?; + return visitor.visit_sum(SumAccess { + common: self.common, + tag: some_field, + value, + }); + } + val + } else { + cast!( + self.input, + v8::Object, + "object for sum type {}", + visitor.sum_name().unwrap_or("") + ) + .map_err_exc(scope)? + }; + + let tag_field = self.common.key_cache.tag(scope); + let tag = val.get(scope, tag_field.into()).err()?; + let tag = cast!( + tag, + v8::String, + "string for sum tag of {}", + visitor.sum_name().unwrap_or("") + ) + .map_err_exc(scope)?; + + let value_field = self.common.key_cache.value(scope); + let value = val.get(scope, value_field.into()).err()?; + + visitor.visit_sum(SumAccess { + common: self.common, + tag, + value, + }) + } + + deserialize_primitive!(deserialize_bool, bool); + + deserialize_primitive!(deserialize_u8, u8); + deserialize_primitive!(deserialize_u16, u16); + deserialize_primitive!(deserialize_u32, u32); + deserialize_primitive!(deserialize_u64, u64); + deserialize_primitive!(deserialize_u128, u128); + deserialize_primitive!(deserialize_u256, u256); + + deserialize_primitive!(deserialize_i8, i8); + deserialize_primitive!(deserialize_i16, i16); + deserialize_primitive!(deserialize_i32, i32); + deserialize_primitive!(deserialize_i64, i64); + deserialize_primitive!(deserialize_i128, i128); + deserialize_primitive!(deserialize_i256, i256); + + deserialize_primitive!(deserialize_f64, f64); + deserialize_primitive!(deserialize_f32, f32); + + fn deserialize_str>(self, visitor: V) -> Result { + let scope = self.common.scope; + let val = cast!(self.input, v8::String, "string").map_err_exc(scope)?; + let mut buf = scratch_buf::<64>(); + match val.to_rust_cow_lossy(scope, &mut buf) { + Cow::Borrowed(s) => visitor.visit(s), + Cow::Owned(string) => visitor.visit_owned(string), + } + } + + fn deserialize_bytes>(self, visitor: V) -> Result { + let scope = self.common.scope; + let arr = cast!(self.input, v8::Uint8Array, "Uint8Array for bytes").map_err_exc(scope)?; + let storage: &'static mut [u8] = &mut [0; v8::TYPED_ARRAY_MAX_SIZE_IN_HEAP]; + let bytes = extend_local(arr).get_contents(storage); + visitor.visit_borrowed(bytes) + } + + fn deserialize_array_seed, T: de::DeserializeSeed<'de> + Clone>( + self, + visitor: V, + seed: T, + ) -> Result { + let arr = cast!(self.input, v8::Array, "Array").map_err_exc(self.common.scope)?; + visitor.visit(ArrayAccess::new(arr, self.common, seed)) + } +} + +struct ProductAccess<'a, 's> { + common: DeserializerCommon<'a, 's>, + obj: v8::Local<'s, v8::Object>, + next_value: Option>, + n: usize, +} + +impl<'de, 's: 'de> de::NamedProductAccess<'de> for ProductAccess<'_, 's> { + type Error = Error<'s>; + + fn get_field_ident>(&mut self, visitor: V) -> Result, Self::Error> { + let scope = &mut *self.common.scope; + while let Some(field) = visitor.nth_name(self.n) { + let i = self.n; + self.n += 1; + let key = v8_struct_key(scope, field); + if !self.obj.has_own_property(scope, key.into()).err()? { + continue; + } + let val = self.obj.get(scope, key.into()).err()?; + self.next_value = Some(val); + return visitor.visit_seq(i).map(Some); + } + Ok(None) + } + + fn get_field_value_seed>(&mut self, seed: T) -> Result { + let val = self + .next_value + .take() + .expect("Call next_key_seed before next_value_seed"); + seed.deserialize(Deserializer { + common: self.common.reborrow(), + input: val, + }) + } +} + +struct SumAccess<'a, 's> { + common: DeserializerCommon<'a, 's>, + tag: v8::Local<'s, v8::String>, + value: v8::Local<'s, v8::Value>, + // p1: std::marker::PhantomData<&'x ()>, +} + +impl<'de, 'a, 's: 'de> de::SumAccess<'de> for SumAccess<'a, 's> { + type Error = Error<'s>; + type Variant = Deserializer<'a, 's>; + + fn variant(self, visitor: V) -> Result<(V::Output, Self::Variant), Self::Error> { + let mut buf = scratch_buf::<32>(); + let name = self.tag.to_rust_cow_lossy(self.common.scope, &mut buf); + let variant = visitor.visit_name::(&name)?; + let dpayload = Deserializer { + common: self.common, + input: self.value, + }; + + Ok((variant, dpayload)) + } +} + +impl<'de, 'a, 's: 'de> de::VariantAccess<'de> for Deserializer<'a, 's> { + type Error = Error<'s>; + + fn deserialize_seed>(self, seed: T) -> Result { + seed.deserialize(self) + } +} + +struct ArrayAccess<'a, 's, T> { + common: DeserializerCommon<'a, 's>, + arr: v8::Local<'s, v8::Array>, + seeds: std::iter::RepeatN, + len: u32, +} + +impl<'de, 'a, 's, T> ArrayAccess<'a, 's, T> +where + T: de::DeserializeSeed<'de> + Clone, +{ + fn new(arr: v8::Local<'s, v8::Array>, common: DeserializerCommon<'a, 's>, seed: T) -> Self { + let len = arr.length(); + Self { + arr, + common, + seeds: std::iter::repeat_n(seed, len as usize), + len, + } + } +} + +impl<'de, 's: 'de, T> de::ArrayAccess<'de> for ArrayAccess<'_, 's, T> +where + T: de::DeserializeSeed<'de> + Clone, +{ + type Element = T::Output; + type Error = Error<'s>; + + fn next_element(&mut self) -> Result, Self::Error> { + let i = self.len - self.seeds.len() as u32; + if let Some(seed) = self.seeds.next() { + let val = self.arr.get_index(self.common.scope, i).err()?; + let val = seed.deserialize(Deserializer { + common: self.common.reborrow(), + input: val, + })?; + Ok(Some(val)) + } else { + Ok(None) + } + } + + fn size_hint(&self) -> Option { + Some(self.seeds.len()) + } +} diff --git a/crates/core/src/host/v8/mod.rs b/crates/core/src/host/v8/mod.rs new file mode 100644 index 00000000000..63c6a65359e --- /dev/null +++ b/crates/core/src/host/v8/mod.rs @@ -0,0 +1,624 @@ +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::{Arc, Once}; + +use crate::database_logger::{BacktraceProvider, LogLevel, Record}; +use crate::db::datastore::locking_tx_datastore::MutTxId; +use crate::db::datastore::traits::{IsolationLevel, Program}; +use crate::energy::EnergyMonitor; +use crate::execution_context::{ReducerContext, Workload}; +use crate::module_host_context::ModuleCreationContext; +use crate::replica_context::ReplicaContext; + +use super::instance_env::InstanceEnv; +use super::module_host::{CallReducerParams, Module, ModuleInfo, ModuleInstance}; +use super::{ReducerCallResult, Scheduler, UpdateDatabaseResult}; + +mod convert; +mod de; +mod ser; +mod util; + +use indexmap::IndexMap; +use spacetimedb_client_api_messages::energy::EnergyQuanta; +use spacetimedb_lib::db::raw_def::v9::{sats_name_to_scoped_name, RawModuleDefV9, RawReducerDefV9, RawTypeDefV9}; +use spacetimedb_lib::sats; +use util::{ + ascii_str, module, scratch_buf, strings, throw, ErrorOrException, ExcResult, ExceptionOptionExt, ExceptionThrown, + ThrowableResultExt, TypeError, +}; + +pub struct V8Runtime { + _priv: (), +} + +impl V8Runtime { + pub fn new() -> Self { + static V8_INIT: Once = Once::new(); + V8_INIT.call_once(|| { + let platform = v8::new_default_platform(0, false).make_shared(); + v8::V8::initialize_platform(platform); + v8::V8::initialize(); + }); + Self { _priv: () } + } + + pub fn make_actor(&self, mcc: ModuleCreationContext<'_>) -> anyhow::Result { + let program = std::str::from_utf8(&mcc.program.bytes)?; + let (snapshot, mut module_builder) = compile(program, Arc::new(Logger))?; + let info = ModuleInfo::new( + module_builder.inner.try_into()?, + mcc.replica_ctx.owner_identity, + mcc.replica_ctx.database_identity, + mcc.program.hash, + // TODO: + tokio::sync::broadcast::channel(0).0, + mcc.replica_ctx.subscriptions.clone(), + ); + module_builder.reducers.shrink_to_fit(); + Ok(JsModule { + replica_context: mcc.replica_ctx, + scheduler: mcc.scheduler, + info, + _energy_monitor: mcc.energy_monitor, + snapshot, + reducers: Arc::new(module_builder.reducers), + }) + } +} + +#[derive(Clone)] +pub struct JsModule { + replica_context: Arc, + scheduler: Scheduler, + info: Arc, + _energy_monitor: Arc, + snapshot: Arc<[u8]>, + reducers: Arc>, +} + +#[derive(thiserror::Error, Debug)] +#[error("js error: {msg:?}")] +struct JsError { + msg: String, +} + +impl JsError { + fn from_caught(scope: &mut v8::TryCatch<'_, v8::HandleScope<'_>>) -> Self { + match scope.message() { + Some(msg) => Self { + msg: msg.get(scope).to_rust_string_lossy(scope), + }, + None => Self { + msg: "unknown error".to_owned(), + }, + } + } +} + +fn catch_exception<'s, T>( + scope: &mut v8::HandleScope<'s>, + f: impl FnOnce(&mut v8::HandleScope<'s>) -> Result, +) -> Result> { + let scope = &mut v8::TryCatch::new(scope); + f(scope).map_err(|e| match e { + ErrorOrException::Err(e) => ErrorOrException::Err(e), + ErrorOrException::Exception(ExceptionThrown) => ErrorOrException::Exception(JsError::from_caught(scope)), + }) +} + +struct Logger; +impl Logger { + fn write(&self, level: LogLevel, record: &Record<'_>, _bt: &dyn BacktraceProvider) { + eprintln!( + "{level:?} [{}] [{}:{}] {}", + record.ts, + record.filename.unwrap_or(""), + record.line_number.unwrap_or(0), + record.message, + ) + } +} + +#[derive(Default, Debug)] +struct ModuleBuilder { + /// The module definition. + inner: RawModuleDefV9, + /// The reducers of the module. + reducers: IndexMap, +} + +#[repr(usize)] +enum GlobalInternalField { + WrapperModule, + Last, +} + +// fn builtins_snapshot() -> anyhow::Result> { +fn builtins_snapshot() -> anyhow::Result<(v8::OwnedIsolate, v8::Global)> { + let isolate = v8::Isolate::snapshot_creator(Some(extern_refs().into()), None); + let mut isolate = scopeguard::guard(isolate, |isolate| { + // rusty_v8 panics if we don't call this when dropping isolate + isolate.create_blob(v8::FunctionCodeHandling::Clear); + }); + let context = { + let isolate = &mut *isolate; + let handle_scope = &mut v8::HandleScope::new(isolate); + + let global_template = v8::ObjectTemplate::new(handle_scope); + global_template.set_internal_field_count(GlobalInternalField::Last as usize); + let context = v8::Context::new( + handle_scope, + v8::ContextOptions { + global_template: Some(global_template), + ..Default::default() + }, + ); + + let scope = &mut v8::ContextScope::new(handle_scope, context); + scope.set_default_context(context); + assert_eq!(scope.add_context(context), 0); + let global = context.global(scope); + // scope.add_context_data(context, global); + // scope.add_context_ + // scope.get_current_context().set_slot(logger); + catch_exception(scope, |scope| { + let name = ascii_str!("spacetime:wrapper").string(scope).into(); + let module = init_module(scope, name, 0, include_str!("./wrapper.js"), resolve_internal_module)?; + + // this is hacky + global.set_internal_field(GlobalInternalField::WrapperModule as usize, module.into()); + + Ok(()) + })?; + v8::Global::new(scope, context) + }; + + // let snapshot = scopeguard::ScopeGuard::into_inner(isolate) + // .create_blob(v8::FunctionCodeHandling::Clear) + // .unwrap(); + + // Ok((*snapshot).into()) + + Ok((scopeguard::ScopeGuard::into_inner(isolate), context)) +} + +fn compile(program: &str, logger: Arc) -> anyhow::Result<(Arc<[u8]>, ModuleBuilder)> { + // let builtins = builtins_snapshot()?; + // let isolate = v8::Isolate::snapshot_creator_from_existing_snapshot(builtins, Some(&EXTERN_REFS), None); + let (isolate, context) = builtins_snapshot()?; + let mut isolate = scopeguard::guard(isolate, |isolate| { + // rusty_v8 panics if we don't call this when dropping isolate + isolate.create_blob(v8::FunctionCodeHandling::Keep); + }); + isolate.set_slot(ModuleBuilder::default()); + isolate.set_slot(logger.clone()); + + { + let isolate = &mut *isolate; + let handle_scope = &mut v8::HandleScope::new(isolate); + // let context = v8::Context::from_snapshot(handle_scope, 0, Default::default()).unwrap(); + let context = v8::Local::new(handle_scope, context); + let scope = &mut v8::ContextScope::new(handle_scope, context); + + // scope.set_prepare_stack_trace_callback(prepare_stack_trace); + + // scope.set_default_context(context); + + catch_exception(scope, |scope| { + let name = ascii_str!("spacetime:module").string(scope).into(); + init_module(scope, name, 1, program, resolve_wrapper_module)?; + Ok(()) + })?; + } + + let module_builder = isolate.remove_slot::().unwrap(); + + let snapshot = scopeguard::ScopeGuard::into_inner(isolate) + .create_blob(v8::FunctionCodeHandling::Keep) + .unwrap(); + // d923b61bd4a4a000589af55b9ac5f046e97c4c756c96427fbc24d1253e7c9c77 + // dbg!(spacetimedb_lib::hash_bytes(&snapshot)); + let snapshot = >::from(&*snapshot); + + Ok((snapshot, module_builder)) +} + +// fn prepare_stack_trace<'s>( +// scope: &mut v8::HandleScope<'s>, +// error: v8::Local<'_, v8::Value>, +// sites: v8::Local<'_, v8::Array>, +// ) -> v8::Local<'s, v8::Value> { +// error. +// todo!(); +// } + +fn find_source_map(program: &str) -> Option<&str> { + let sm_ref = "//# sourceMappingURL="; + program.match_indices(sm_ref).find_map(|(i, _)| { + let (before, after) = program.split_at(i); + (before.is_empty() || before.ends_with(['\r', '\n'])) + .then(|| &after.lines().next().unwrap_or(after)[sm_ref.len()..]) + }) +} + +fn init_module<'s>( + scope: &mut v8::HandleScope<'s>, + resource_name: v8::Local<'s, v8::Value>, + script_id: i32, + program: &str, + resolve_module: impl v8::MapFnTo>, +) -> Result, ErrorOrException> { + let source = v8::String::new(scope, program).err()?; + let source_map_url = find_source_map(program).map(|r| v8::String::new(scope, r).unwrap().into()); + let origin = v8::ScriptOrigin::new( + scope, + resource_name, + 0, + 0, + false, + script_id, + source_map_url, + false, + false, + true, + None, + ); + let source = &mut v8::script_compiler::Source::new(source, Some(&origin)); + let module = v8::script_compiler::compile_module(scope, source).err()?; + + module.instantiate_module(scope, resolve_module).err()?; + + module.evaluate(scope).err()?; + + if module.get_status() == v8::ModuleStatus::Errored { + let exc = v8::Local::new(scope, module.get_exception()); + throw(scope, exc)?; + } + + Ok(module) +} + +fn resolve_internal_module<'s>( + context: v8::Local<'s, v8::Context>, + spec: v8::Local<'s, v8::String>, + _attrs: v8::Local<'s, v8::FixedArray>, + _referrer: v8::Local<'s, v8::Module>, +) -> Option> { + let scope = &mut *unsafe { v8::CallbackScope::new(context) }; + if spec == spacetime_sys_10_0::SPEC_STRING.string(scope) { + Some(spacetime_sys_10_0::make(scope)) + } else { + let exc = module_exception(scope, spec); + throw(scope, exc).ok() + } +} + +strings!(SPACETIME_MODULE = "spacetimedb"); + +fn resolve_wrapper_module<'s>( + context: v8::Local<'s, v8::Context>, + spec: v8::Local<'s, v8::String>, + _attrs: v8::Local<'s, v8::FixedArray>, + _referrer: v8::Local<'s, v8::Module>, +) -> Option> { + let scope = &mut *unsafe { v8::CallbackScope::new(context) }; + if spec == SPACETIME_MODULE.string(scope) { + let module = context + .global(scope) + .get_internal_field(scope, GlobalInternalField::WrapperModule as usize) + .unwrap() + .cast::(); + Some(module) + } else { + let exc = module_exception(scope, spec); + throw(scope, exc).ok() + } +} + +fn module_exception(scope: &mut v8::HandleScope<'_>, spec: v8::Local<'_, v8::String>) -> TypeError { + let mut buf = scratch_buf::<32>(); + let spec = spec.to_rust_cow_lossy(scope, &mut buf); + TypeError(format!("Could not find module {spec:?}")) +} + +module!( + spacetime_sys_10_0 = "spacetime:sys/v10.0", + function(console_log), + symbol(console_level_error = "console.Level.Error"), + symbol(console_level_warn = "console.Level.Warn"), + symbol(console_level_info = "console.Level.Info"), + symbol(console_level_debug = "console.Level.Debug"), + symbol(console_level_trace = "console.Level.Trace"), + symbol(console_level_panic = "console.Level.Panic"), + function(register_reducer), + function(register_type), +); + +fn extern_refs() -> Vec { + spacetime_sys_10_0::external_refs() + .chain(Some(v8::ExternalReference { + pointer: std::ptr::null_mut(), + })) + .collect() +} + +fn console_log(scope: &mut v8::HandleScope<'_>, args: v8::FunctionCallbackArguments<'_>) -> ExcResult<()> { + let logger = scope.get_slot::>().unwrap().clone(); + let level = args.get(0); + let level = if level == spacetime_sys_10_0::console_level_error(scope) { + LogLevel::Error + } else if level == spacetime_sys_10_0::console_level_warn(scope) { + LogLevel::Warn + } else if level == spacetime_sys_10_0::console_level_info(scope) { + LogLevel::Info + } else if level == spacetime_sys_10_0::console_level_debug(scope) { + LogLevel::Debug + } else if level == spacetime_sys_10_0::console_level_trace(scope) { + LogLevel::Trace + } else if level == spacetime_sys_10_0::console_level_panic(scope) { + LogLevel::Panic + } else { + throw(scope, TypeError(ascii_str!("Invalid log level")))? + }; + let msg = args.get(1).cast::(); + let mut buf = scratch_buf::<128>(); + let msg = msg.to_rust_cow_lossy(scope, &mut buf); + let frame = v8::StackTrace::current_stack_trace(scope, 2) + .err()? + .get_frame(scope, 1) + .err()?; + let mut buf = scratch_buf::<32>(); + let filename = frame + .get_script_name(scope) + .map(|s| s.to_rust_cow_lossy(scope, &mut buf)); + let record = Record { + // TODO: figure out whether to use walltime now or logical reducer now (env.reducer_start) + ts: chrono::Utc::now(), + target: None, + filename: filename.as_deref(), + line_number: Some(frame.get_line_number() as u32), + message: &msg, + }; + logger.write(level, &record, &()); + Ok(()) +} + +fn get_or_create_key_cache(scope: &mut v8::HandleScope<'_>) -> Rc> { + let context = scope.get_current_context(); + context.get_slot::>().unwrap_or_else(|| { + let cache = Rc::default(); + context.set_slot(Rc::clone(&cache)); + cache + }) +} + +fn deserialize_js_seed<'de, T: sats::de::DeserializeSeed<'de>>( + scope: &mut v8::HandleScope<'de>, + val: v8::Local<'_, v8::Value>, + seed: T, +) -> ExcResult { + let key_cache = get_or_create_key_cache(scope); + let key_cache = &mut *key_cache.borrow_mut(); + let de = de::Deserializer::new(scope, val, key_cache); + seed.deserialize(de).throw(scope) +} + +fn deserialize_js<'de, T: sats::Deserialize<'de>>( + scope: &mut v8::HandleScope<'de>, + val: v8::Local<'_, v8::Value>, +) -> ExcResult { + deserialize_js_seed(scope, val, std::marker::PhantomData) +} + +fn serialize_to_js<'s, T: sats::Serialize>( + scope: &mut v8::HandleScope<'s>, + value: &T, +) -> ExcResult> { + let key_cache = get_or_create_key_cache(scope); + let key_cache = &mut *key_cache.borrow_mut(); + value.serialize(ser::Serializer::new(scope, key_cache)).throw(scope) +} + +fn register_reducer(scope: &mut v8::HandleScope<'_>, args: v8::FunctionCallbackArguments<'_>) -> ExcResult<()> { + if scope.get_slot::().is_none() { + throw(scope, TypeError(ascii_str!("You cannot dynamically register reducers")))?; + } + + let name = args.get(0).cast::(); + let params = args.get(1); + + let params: sats::ProductType = deserialize_js(scope, params)?; + + let function = args + .get(2) + .try_cast::() + .map_err(|_| TypeError(ascii_str!("Third argument to register_reducer must be function"))) + .throw(scope)?; + + function.set_name(name); + + let name = name.to_rust_string_lossy(scope); + + let context = scope.get_current_context(); + let function_idx = scope.add_context_data(context, function); + + let module = scope.get_slot_mut::().unwrap(); + module.inner.reducers.push(RawReducerDefV9 { + name: (&*name).into(), + params, + lifecycle: None, + }); + match module.reducers.entry(name) { + indexmap::map::Entry::Vacant(v) => { + v.insert(function_idx); + } + indexmap::map::Entry::Occupied(o) => { + let msg = format!("Reducer {:?} already registered", o.key()); + throw(scope, TypeError(msg))?; + } + } + + Ok(()) +} + +fn register_type(scope: &mut v8::HandleScope<'_>, args: v8::FunctionCallbackArguments<'_>) -> ExcResult { + if scope.get_slot::().is_none() { + throw(scope, TypeError(ascii_str!("You cannot dynamically register reducers")))?; + } + + let name = args.get(0).cast::(); + let ty = args.get(1); + + let mut buf = scratch_buf::<32>(); + let name = name.to_rust_cow_lossy(scope, &mut buf); + let name = sats_name_to_scoped_name(&name); + + let ty: sats::AlgebraicType = deserialize_js(scope, ty)?; + + let module = scope.get_slot_mut::().unwrap(); + let r = module.inner.typespace.add(ty); + module.inner.types.push(RawTypeDefV9 { + name, + ty: r, + custom_ordering: false, + }); + + Ok(r.0) +} + +#[test] +fn v8_compile_test() { + let program = include_str!("./test_code.js"); + let (_snapshot, module) = compile(program, Arc::new(Logger)).unwrap(); + dbg!(module); + // dbg!(module_idx, bytes::Bytes::copy_from_slice(&snapshot)); + // panic!(); +} + +impl Module for JsModule { + type Instance = JsInstance; + + type InitialInstances<'a> = std::iter::Empty; + + fn initial_instances(&mut self) -> Self::InitialInstances<'_> { + std::iter::empty() + } + + fn info(&self) -> Arc { + self.info.clone() + } + + fn create_instance(&self) -> Self::Instance { + todo!() + } + + fn replica_ctx(&self) -> &ReplicaContext { + &self.replica_context + } + + fn scheduler(&self) -> &Scheduler { + &self.scheduler + } +} + +pub struct JsInstance { + module: JsModule, +} + +#[allow(unused)] +impl ModuleInstance for JsInstance { + fn trapped(&self) -> bool { + false + } + + fn update_database( + &mut self, + program: Program, + old_module_info: Arc, + ) -> anyhow::Result { + todo!() + } + + fn call_reducer(&mut self, tx: Option, params: CallReducerParams) -> ReducerCallResult { + let stdb = self.module.replica_context.relational_db.clone(); + let module_def = &self.module.info.module_def; + let reducer_def = module_def.reducer_by_id(params.reducer_id); + + let tx = tx.unwrap_or_else(|| { + stdb.begin_mut_tx( + IsolationLevel::Serializable, + Workload::Reducer(ReducerContext { + name: (&*reducer_def.name).into(), + caller_identity: params.caller_identity, + caller_connection_id: params.caller_connection_id, + timestamp: params.timestamp, + arg_bsatn: params.args.get_bsatn().clone(), + }), + ) + }); + + let mut instance_env = InstanceEnv::new(self.module.replica_context.clone(), self.module.scheduler.clone()); + instance_env.start_reducer(params.timestamp); + let mut tx_slot = instance_env.tx.clone(); + + let mut isolate = v8::Isolate::new( + v8::CreateParams::default() + .external_references(extern_refs().into()) + // have to reallocate :( + .snapshot_blob(self.module.snapshot.to_vec().into()), + ); + let reducer_function_idx = self.module.reducers[params.reducer_id.idx()]; + let start = std::time::Instant::now(); + let (tx, result) = tx_slot.set(tx, || { + let mut scope = &mut v8::HandleScope::new(&mut isolate); + let mut context = v8::Context::from_snapshot(scope, 0, v8::ContextOptions::default()).unwrap(); + let mut scope = &mut v8::ContextScope::new(scope, context); + let func = scope + .get_context_data_from_snapshot_once::(reducer_function_idx) + .unwrap(); + let args = module_def + .typespace() + .with_type(&reducer_def.params) + .with_value(¶ms.args.tuple); + catch_exception(scope, |scope| { + let args = args + .elements() + .map(|x| serialize_to_js(scope, &x)) + .collect::, _>>()?; + let recv = v8::undefined(scope).into(); + func.call(scope, recv, &args).err()?; + Ok(()) + }) + }); + let execution_duration = start.elapsed(); + let outcome = match result { + Ok(()) => super::ReducerOutcome::Committed, + Err(e) => super::ReducerOutcome::Failed(anyhow::Error::from(e).to_string()), + }; + ReducerCallResult { + outcome, + energy_used: EnergyQuanta::ZERO, + execution_duration, + } + } +} + +fn _request_interrupt(handle: &v8::IsolateHandle, f: F) -> bool +where + F: FnOnce(&mut v8::Isolate), +{ + unsafe extern "C" fn cb(isolate: &mut v8::Isolate, data: *mut std::ffi::c_void) + where + F: FnOnce(&mut v8::Isolate), + { + let f = unsafe { Box::::from_raw(data.cast()) }; + f(isolate) + } + let data = Box::into_raw(Box::new(f)); + let already_destroyed = handle.request_interrupt(cb::, data.cast()); + if already_destroyed { + drop(unsafe { Box::from_raw(data) }); + } + already_destroyed +} diff --git a/crates/core/src/host/v8/ser.rs b/crates/core/src/host/v8/ser.rs new file mode 100644 index 00000000000..81f5ef649b1 --- /dev/null +++ b/crates/core/src/host/v8/ser.rs @@ -0,0 +1,166 @@ +use spacetimedb_sats::{i256, ser, u256}; + +use super::convert::ToValue; +use super::de::{v8_struct_key, Error, KeyCache}; +use super::ExceptionOptionExt; + +pub(super) struct Serializer<'a, 's> { + scope: &'a mut v8::HandleScope<'s>, + key_cache: &'a mut KeyCache, +} + +impl<'a, 's> Serializer<'a, 's> { + pub fn new(scope: &'a mut v8::HandleScope<'s>, key_cache: &'a mut KeyCache) -> Self { + Self { scope, key_cache } + } + + fn reborrow(&mut self) -> Serializer<'_, 's> { + Serializer { + scope: self.scope, + key_cache: self.key_cache, + } + } +} + +impl ser::Error for Error<'_> { + fn custom(msg: T) -> Self { + Self::String(msg.to_string()) + } +} + +macro_rules! serialize_primitive { + ($smethod:ident, $t:ty) => { + fn $smethod(self, v: $t) -> Result { + Ok(ToValue::to_value(&v, self.scope)?) + } + }; +} + +impl<'a, 's> ser::Serializer for Serializer<'a, 's> { + type Ok = v8::Local<'s, v8::Value>; + type Error = Error<'s>; + + type SerializeArray = SerializeArray<'a, 's>; + type SerializeSeqProduct = ser::Impossible; + type SerializeNamedProduct = SerializeNamedProduct<'a, 's>; + + serialize_primitive!(serialize_bool, bool); + + serialize_primitive!(serialize_u8, u8); + serialize_primitive!(serialize_u16, u16); + serialize_primitive!(serialize_u32, u32); + serialize_primitive!(serialize_u64, u64); + serialize_primitive!(serialize_u128, u128); + serialize_primitive!(serialize_u256, u256); + + serialize_primitive!(serialize_i8, i8); + serialize_primitive!(serialize_i16, i16); + serialize_primitive!(serialize_i32, i32); + serialize_primitive!(serialize_i64, i64); + serialize_primitive!(serialize_i128, i128); + serialize_primitive!(serialize_i256, i256); + + serialize_primitive!(serialize_f64, f64); + serialize_primitive!(serialize_f32, f32); + + fn serialize_str(self, v: &str) -> Result { + v8::String::new(self.scope, v) + .map(Into::into) + .ok_or_else(|| ser::Error::custom("string too large")) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + let store = v8::ArrayBuffer::new_backing_store_from_boxed_slice(v.into()).make_shared(); + let buf = v8::ArrayBuffer::with_backing_store(self.scope, &store); + Ok(v8::Uint8Array::new(self.scope, buf, 0, v.len()).unwrap().into()) + } + + fn serialize_array(self, len: usize) -> Result { + Ok(SerializeArray { + arr: v8::Array::new(self.scope, len as _), + inner: self, + next: 0, + }) + } + + fn serialize_seq_product(self, _len: usize) -> Result { + Err(ser::Error::custom("Can't serialize seqproduct for JS")) + } + + fn serialize_named_product(self, _len: usize) -> Result { + // TODO: this can be more efficient if we tell it the names ahead of time + let obj = v8::Object::new(self.scope); + Ok(SerializeNamedProduct { inner: self, obj }) + } + + fn serialize_variant( + mut self, + _tag: u8, + name: Option<&str>, + value: &T, + ) -> Result { + let names = [ + self.key_cache.tag(self.scope).into(), + self.key_cache.value(self.scope).into(), + ]; + + let values = [ + v8_struct_key(self.scope, name.unwrap()).into(), + value.serialize(self.reborrow())?, + ]; + + let null = v8::null(self.scope); + Ok(v8::Object::with_prototype_and_properties(self.scope, null.into(), &names, &values).into()) + } +} + +pub(super) struct SerializeArray<'a, 's> { + inner: Serializer<'a, 's>, + arr: v8::Local<'s, v8::Array>, + next: u32, +} + +impl<'a, 's> ser::SerializeArray for SerializeArray<'a, 's> { + type Ok = v8::Local<'s, v8::Value>; + type Error = Error<'s>; + + fn serialize_element(&mut self, element: &T) -> Result<(), Self::Error> { + let i = self.next; + let value = element.serialize(self.inner.reborrow())?; + self.arr.set_index(self.inner.scope, i, value).err()?; + self.next += 1; + Ok(()) + } + + fn end(self) -> Result { + Ok(self.arr.into()) + } +} + +pub(super) struct SerializeNamedProduct<'a, 's> { + inner: Serializer<'a, 's>, + obj: v8::Local<'s, v8::Object>, +} + +impl<'a, 's> ser::SerializeNamedProduct for SerializeNamedProduct<'a, 's> { + type Ok = v8::Local<'s, v8::Value>; + type Error = Error<'s>; + + fn serialize_element( + &mut self, + name: Option<&str>, + elem: &T, + ) -> Result<(), Self::Error> { + let key = v8_struct_key(self.inner.scope, name.unwrap()); + let value = elem.serialize(self.inner.reborrow())?; + self.obj.set(self.inner.scope, key.into(), value).err()?; + Ok(()) + } + + fn end(self) -> Result { + self.obj + .set_integrity_level(self.inner.scope, v8::IntegrityLevel::Sealed) + .err()?; + Ok(self.obj.into()) + } +} diff --git a/crates/core/src/host/v8/test_code.ts b/crates/core/src/host/v8/test_code.ts new file mode 100644 index 00000000000..54c22574e94 --- /dev/null +++ b/crates/core/src/host/v8/test_code.ts @@ -0,0 +1,25 @@ +import { registerReducer, registerType, type } from 'spacetimedb'; + +const Foo = registerType( + 'Foo', + type.product({ + bar: type.f32, + baz: type.string, + }) +); + +console.log('hello there', new Error().stack); +try { + function x() { + throw new Error('hello'); + } + x(); +} catch (e) { + // Error.captureStackTrace(e, ) + console.log('woww', e); +} +registerReducer('beepboop', [type.array(type.f32), type.bool, Foo], (x, y, z) => { + // z.bar; +}); +// console.log(registerReducer); +// registerReducer(1, [], () => {}); diff --git a/crates/core/src/host/v8/tsconfig.json b/crates/core/src/host/v8/tsconfig.json new file mode 100644 index 00000000000..a6f87031a61 --- /dev/null +++ b/crates/core/src/host/v8/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "ESNext", + "paths": { + "spacetimedb": ["./wrapper"] + }, + "inlineSourceMap": true + } +} diff --git a/crates/core/src/host/v8/types.d.ts b/crates/core/src/host/v8/types.d.ts new file mode 100644 index 00000000000..a9cb7080b19 --- /dev/null +++ b/crates/core/src/host/v8/types.d.ts @@ -0,0 +1,57 @@ +declare module 'spacetime:sys/v10.0' { + export const console_level_error: unique symbol; + export const console_level_warn: unique symbol; + export const console_level_info: unique symbol; + export const console_level_debug: unique symbol; + export const console_level_trace: unique symbol; + export const console_level_panic: unique symbol; + type ConsoleLevel = + | typeof console_level_error + | typeof console_level_warn + | typeof console_level_info + | typeof console_level_debug + | typeof console_level_trace + | typeof console_level_panic; + + export function console_log(level: ConsoleLevel, msg: string): void; + + export function register_reducer(name: string, product_type: ProductType, func: Function): void; + + export function register_type(name: string, type: AlgebraicType): number; + + type Variant = Readonly<{ tag: Tag; value: Value }>; + + export type option = Readonly<{ some: T }> | null; + + export type AlgebraicType = + | Variant<'Ref', number> + | Variant<'Product', ProductType> + | ArrayVariant + | PrimitiveType; + export type ProductType = Readonly<{ + elements: readonly ProductTypeElement[]; + }>; + export type ProductTypeElement = Readonly<{ + name?: option; + algebraic_type: AlgebraicType; + }>; + type ArrayVariant = Readonly<{ tag: 'Array'; value: AlgebraicType }>; + export type Unit = Readonly<{}>; + export type PrimitiveType = + | Variant<'String', Unit> + | Variant<'Bool', Unit> + | Variant<'I8', Unit> + | Variant<'U8', Unit> + | Variant<'I16', Unit> + | Variant<'U16', Unit> + | Variant<'I32', Unit> + | Variant<'U32', Unit> + | Variant<'I64', Unit> + | Variant<'U64', Unit> + | Variant<'I128', Unit> + | Variant<'U128', Unit> + | Variant<'I256', Unit> + | Variant<'U256', Unit> + | Variant<'F32', Unit> + | Variant<'F64', Unit>; +} diff --git a/crates/core/src/host/v8/util.rs b/crates/core/src/host/v8/util.rs new file mode 100644 index 00000000000..1242ae12f09 --- /dev/null +++ b/crates/core/src/host/v8/util.rs @@ -0,0 +1,270 @@ +use std::mem::MaybeUninit; + +pub(super) struct StringConst(v8::OneByteConst); + +impl StringConst { + pub(super) const fn new(s: &'static str) -> Self { + Self(v8::String::create_external_onebyte_const(s.as_bytes())) + } + pub(super) fn string<'s>(&'static self, scope: &mut v8::HandleScope<'s, ()>) -> v8::Local<'s, v8::String> { + // unwrap() b/c create_external_onebyte_const asserts new_from_onebyte_const's + // preconditions (str < kMaxLength) + v8::String::new_from_onebyte_const(scope, &self.0).unwrap() + } +} + +pub(super) fn scratch_buf() -> [MaybeUninit; N] { + [const { MaybeUninit::uninit() }; N] +} + +pub(super) type ValueResult<'s, T> = Result>; + +#[derive(Debug)] +pub(super) struct ExceptionThrown; + +#[derive(Debug)] +pub(super) enum ErrorOrException { + Err(anyhow::Error), + Exception(Exc), +} + +impl From for ErrorOrException { + fn from(err: anyhow::Error) -> Self { + Self::Err(err) + } +} + +impl From for ErrorOrException { + fn from(err: ExceptionThrown) -> Self { + Self::Exception(err) + } +} + +impl From> for anyhow::Error { + fn from(err: ErrorOrException) -> Self { + match err { + ErrorOrException::Err(e) => e, + ErrorOrException::Exception(e) => e.into(), + } + } +} + +pub(super) trait ExceptionOptionExt { + type T; + fn err(self) -> Result; +} +impl ExceptionOptionExt for Option { + type T = T; + fn err(self) -> Result { + self.ok_or(ExceptionThrown) + } +} + +pub(super) fn throw(scope: &mut v8::HandleScope<'_>, err: E) -> Result +where + E: Throwable, +{ + Err(err.throw(scope)) +} + +pub(super) trait ThrowableResultExt { + type T; + fn throw(self, scope: &mut v8::HandleScope<'_>) -> Result; +} + +pub(super) trait IntoExceptionResultExt { + type T; + fn map_err_exc<'s>(self, scope: &mut v8::HandleScope<'s>) -> ValueResult<'s, Self::T>; +} + +impl ThrowableResultExt for Result { + type T = T; + fn throw(self, scope: &mut v8::HandleScope<'_>) -> Result { + self.map_err(|err| err.throw(scope)) + } +} + +impl IntoExceptionResultExt for Result { + type T = T; + fn map_err_exc<'s>(self, scope: &mut v8::HandleScope<'s>) -> ValueResult<'s, Self::T> { + self.map_err(|e| e.into_exception(scope)) + } +} + +pub(super) trait IntoException { + fn into_exception<'s>(self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::Value>; +} + +impl IntoException for v8::Local<'_, v8::Value> { + fn into_exception<'s>(self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::Value> { + v8::Local::new(scope, self) + } +} + +pub(super) trait Throwable { + fn throw(self, scope: &mut v8::HandleScope<'_>) -> ExceptionThrown; +} + +impl Throwable for T { + fn throw(self, scope: &mut v8::HandleScope<'_>) -> ExceptionThrown { + let exception = self.into_exception(scope); + scope.throw_exception(exception); + ExceptionThrown + } +} + +#[derive(Copy, Clone)] +pub struct TypeError(pub M); + +impl IntoException for TypeError { + fn into_exception<'s>(self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::Value> { + let msg = self.0.into_string(scope); + v8::Exception::type_error(scope, msg) + } +} + +pub(super) trait IntoJsString { + fn into_string<'s>(self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::String>; +} +impl IntoJsString for v8::Local<'_, v8::String> { + fn into_string<'s>(self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::String> { + v8::Local::new(scope, self) + } +} +impl IntoJsString for String { + fn into_string<'s>(self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::String> { + v8::String::new(scope, &self).unwrap() + } +} +impl IntoJsString for &'static StringConst { + fn into_string<'s>(self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::String> { + self.string(scope) + } +} + +pub(super) fn nicer_callback(f: F) -> v8::FunctionCallback +where + F: Fn(&mut v8::HandleScope<'_>, v8::FunctionCallbackArguments<'_>) -> ExcResult + Copy, + R: ReturnValue, +{ + let cb = move |scope: &mut v8::HandleScope<'_>, + args: v8::FunctionCallbackArguments<'_>, + rv: v8::ReturnValue<'_>| { + match f(scope, args) { + Ok(value) => value.set_return_value(rv), + Err(ExceptionThrown) => {} + } + }; + v8::MapFnTo::map_fn_to(cb) +} + +pub(super) type ExcResult = Result; + +pub(super) trait ReturnValue { + fn set_return_value(self, rv: v8::ReturnValue<'_>); +} + +macro_rules! impl_return_value { + ($t:ty, $self:ident, $func:ident($($args:tt)*)) => { + impl ReturnValue for $t { + fn set_return_value($self, mut rv: v8::ReturnValue<'_>) { + rv.$func($($args)*); + } + } + }; + ($t:ty, $func:ident) => { + impl_return_value!($t, self, $func(self)); + }; +} + +impl_return_value!(v8::Local<'_, v8::Value>, set); +impl_return_value!(bool, set_bool); +impl_return_value!(i32, set_int32); +impl_return_value!(u32, set_uint32); +impl_return_value!(f64, set_double); +impl_return_value!((), self, set_undefined()); + +pub(super) fn external_synthetic_steps(f: F) -> v8::ExternalReference +where + for<'a> F: v8::MapFnTo>, +{ + let pointer = f.map_fn_to() as _; + v8::ExternalReference { pointer } +} + +macro_rules! ascii_str { + ($str:expr) => { + const { &$crate::host::v8::util::StringConst::new($str) } + }; +} +pub(super) use ascii_str; + +macro_rules! strings { + ($vis:vis $($name:ident = $val:expr),*$(,)?) => { + $($vis static $name: $crate::host::v8::util::StringConst = $crate::host::v8::util::StringConst::new($val);)* + }; +} +pub(super) use strings; + +macro_rules! module { + ($name:ident = $module_name:expr, $($export_kind:ident($export_name:ident $($export:tt)*)),*$(,)?) => { + mod $name { + pub const SPEC: &str = $module_name; + $crate::host::v8::util::strings!(pub SPEC_STRING = SPEC); + + #[allow(non_snake_case, non_upper_case_globals)] + mod names { + $crate::host::v8::util::strings!(pub(super) $($export_name = stringify!($export_name),)*); + } + + pub fn make<'s>(scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::Module> { + let export_names = [$(names::$export_name.string(scope),)*]; + let spec = SPEC_STRING.string(scope); + v8::Module::create_synthetic_module(scope, spec, &export_names, evaluation_steps) + } + + fn evaluation_steps<'s>(context: v8::Local<'s, v8::Context>, module: v8::Local<'s, v8::Module>) -> Option> { + let scope = &mut *unsafe { v8::CallbackScope::new(context) }; + $({ + let name = names::$export_name.string(scope); + let val = + $crate::host::v8::util::module!(@export scope, name, $export_kind($export_name $($export)*)); + module.set_synthetic_module_export(scope, name, val)?; + })* + Some(v8::undefined(scope).into()) + } + + pub fn external_refs<'s>() -> impl Iterator { + [ + $crate::host::v8::util::external_synthetic_steps(evaluation_steps), + $($crate::host::v8::util::module!(@export_ref $export_kind($export_name $($export)*)),)* + ].into_iter() + } + + $($crate::host::v8::util::module!(@export_rust $export_kind($export_name $($export)*));)* + } + }; + (@export $scope:ident, $name:ident, function($export_name:ident)) => {{ + let func = v8::Function::new_raw($scope, $crate::host::v8::util::nicer_callback(super::$export_name)).unwrap(); + func.set_name($name); + func.into() + }}; + (@export_ref function($export_name:ident)) => { + v8::ExternalReference { function: $crate::host::v8::util::nicer_callback(super::$export_name) } + }; + (@export_rust function($($t:tt)*)) => {}; + (@export $scope:ident, $name:ident, symbol($export_name:ident = $symbol:expr)) => {{ + $export_name($scope).into() + }}; + (@export_ref symbol($($t:tt)*)) => { + #[cfg(any())] () + }; + (@export_rust symbol($export_name:ident = $symbol:expr)) => { + pub fn $export_name<'s>(scope: &mut v8::HandleScope<'s, ()>) -> v8::Local<'s, v8::Symbol> { + $crate::host::v8::util::strings!(STRING = $symbol); + let string = STRING.string(scope); + v8::Symbol::for_api(scope, string) + } + }; +} +pub(super) use module; diff --git a/crates/core/src/host/v8/wrapper.ts b/crates/core/src/host/v8/wrapper.ts new file mode 100644 index 00000000000..b7ab8b2a2ec --- /dev/null +++ b/crates/core/src/host/v8/wrapper.ts @@ -0,0 +1,304 @@ +import { + console_log, + console_level_error, + console_level_warn, + console_level_info, + console_level_debug, + console_level_trace, + console_level_panic, + register_reducer, + register_type, +} from 'spacetime:sys/v10.0'; + +function fmtLog(...data: unknown[]) { + return data.join(' '); +} + +const console = { + __proto__: {}, + + [Symbol.toStringTag]: 'console', + + assert: (condition = false, ...data: any) => { + if (!condition) { + console_log(console_level_error, fmtLog(...data)); + } + }, + clear: () => {}, + debug: (...data: any) => { + console_log(console_level_debug, fmtLog(...data)); + }, + error: (...data: any) => { + console_log(console_level_error, fmtLog(...data)); + }, + info: (...data: any) => { + console_log(console_level_info, fmtLog(...data)); + }, + log: (...data: any) => { + console_log(console_level_info, fmtLog(...data)); + }, + table: (tabularData: unknown, properties: any) => { + console_log(console_level_info, fmtLog(tabularData)); + }, + trace: (...data: any) => { + console_log(console_level_trace, fmtLog(...data)); + }, + warn: (...data: any) => { + console_log(console_level_warn, fmtLog(...data)); + }, + dir: (item: any, options: any) => {}, + dirxml: (...data: any) => {}, + + // Counting + count: (label = 'default') => {}, + countReset: (label = 'default') => {}, + + // Grouping + group: (...data: any) => {}, + groupCollapsed: (...data: any) => {}, + groupEnd: () => {}, + + // Timing + time: (label = 'default') => {}, + timeLog: (label = 'default', ...data: any) => {}, + timeEnd: (label = 'default') => {}, +}; +// @ts-ignore +globalThis.console = console; + +const { freeze } = Object; + +const stringType = Symbol('spacetimedb.type.string'); +const boolType = Symbol('spacetimedb.type.bool'); +const i8Type = Symbol('spacetimedb.type.i8'); +const u8Type = Symbol('spacetimedb.type.u8'); +const i16Type = Symbol('spacetimedb.type.i16'); +const u16Type = Symbol('spacetimedb.type.u16'); +const i32Type = Symbol('spacetimedb.type.i32'); +const u32Type = Symbol('spacetimedb.type.u32'); +const i64Type = Symbol('spacetimedb.type.i64'); +const u64Type = Symbol('spacetimedb.type.u64'); +const i128Type = Symbol('spacetimedb.type.i128'); +const u128Type = Symbol('spacetimedb.type.u128'); +const i256Type = Symbol('spacetimedb.type.i256'); +const u256Type = Symbol('spacetimedb.type.u256'); +const f32Type = Symbol('spacetimedb.type.f32'); +const f64Type = Symbol('spacetimedb.type.f64'); + +export const type = freeze({ + string: stringType, + bool: boolType, + i8: i8Type, + u8: u8Type, + i16: i16Type, + u16: u16Type, + i32: i32Type, + u32: u32Type, + i64: i64Type, + u64: u64Type, + i128: i128Type, + u128: u128Type, + i256: i256Type, + u256: u256Type, + f32: f32Type, + f64: f64Type, + array(elem: Elem) { + return new ArrayType(elem); + }, + product(map: Map) { + return new ProductType(map); + }, +}); + +const toInternalType = Symbol('spacetimedb.toInternalType'); + +class ArrayType { + #inner: Extract; + constructor(inner: Elem) { + this.#inner = freeze({ tag: 'Array', value: convertType(inner) }); + } + get [toInternalType]() { + return this.#inner; + } +} + +type ProductMap = { [s: string]: AlgebraicType }; +class ProductType { + #inner: Extract; + constructor(map: Map) { + const elements = freeze( + Object.entries(map).map(([k, v]) => + freeze({ name: freeze({ some: k }), algebraic_type: convertType(v) }) + ) + ); + this.#inner = freeze({ tag: 'Product', value: freeze({ elements }) }); + } + get [toInternalType]() { + return this.#inner; + } +} + +class TypeRef { + #inner: Extract; + constructor(ref: number) { + this.#inner = freeze({ tag: 'Ref', value: ref }); + } + get [toInternalType]() { + return this.#inner; + } +} + +export const unit = freeze({}); + +const primitives = freeze({ + string: freeze({ tag: 'String', value: unit }), + bool: freeze({ tag: 'Bool', value: unit }), + i8: freeze({ tag: 'I8', value: unit }), + u8: freeze({ tag: 'U8', value: unit }), + i16: freeze({ tag: 'I16', value: unit }), + u16: freeze({ tag: 'U16', value: unit }), + i32: freeze({ tag: 'I32', value: unit }), + u32: freeze({ tag: 'U32', value: unit }), + i64: freeze({ tag: 'I64', value: unit }), + u64: freeze({ tag: 'U64', value: unit }), + i128: freeze({ tag: 'I128', value: unit }), + u128: freeze({ tag: 'U128', value: unit }), + i256: freeze({ tag: 'I256', value: unit }), + u256: freeze({ tag: 'U256', value: unit }), + f32: freeze({ tag: 'F32', value: unit }), + f64: freeze({ tag: 'F64', value: unit }), +}); + +function convertType(ty: AlgebraicType): import('spacetime:sys/v10.0').AlgebraicType { + if (typeof ty === 'symbol') { + switch (ty) { + case type.string: + return primitives.string; + case type.bool: + return primitives.bool; + case type.i8: + return primitives.i8; + case type.u8: + return primitives.u8; + case type.i16: + return primitives.i16; + case type.u16: + return primitives.u16; + case type.i32: + return primitives.i32; + case type.u32: + return primitives.u32; + case type.i64: + return primitives.i64; + case type.u64: + return primitives.u64; + case type.i128: + return primitives.i128; + case type.u128: + return primitives.u128; + case type.i256: + return primitives.i256; + case type.u256: + return primitives.u256; + case type.f32: + return primitives.f32; + case type.f64: + return primitives.f64; + default: + let {}: never = ty; + } + } else if (toInternalType in ty) { + return ty[toInternalType]; + } + throw new TypeError('Expected Spacetime type, got ' + ty); +} + +type PrimitiveType = Extract<(typeof type)[keyof typeof type], symbol>; + +type AlgebraicType = TypeRef | ProductType | ArrayType | PrimitiveType; + +export type I8 = number; +export type U8 = number; +export type I16 = number; +export type U16 = number; +export type I32 = number; +export type U32 = number; +export type I64 = bigint; +export type U64 = bigint; +export type I128 = bigint; +export type U128 = bigint; +export type I256 = bigint; +export type U256 = bigint; + +type PrimitiveTypeToType = T extends typeof stringType + ? string + : T extends typeof boolType + ? boolean + : T extends typeof i8Type + ? I8 + : T extends typeof u8Type + ? U8 + : T extends typeof i16Type + ? I16 + : T extends typeof u16Type + ? U16 + : T extends typeof i32Type + ? I32 + : T extends typeof u32Type + ? U32 + : T extends typeof i64Type + ? I64 + : T extends typeof u64Type + ? U64 + : T extends typeof i128Type + ? I128 + : T extends typeof u128Type + ? U128 + : T extends typeof i256Type + ? I256 + : T extends typeof u256Type + ? U256 + : T extends typeof f32Type + ? number + : T extends typeof f64Type + ? number + : never; + +type AlgebraicTypeToType = [T] extends [TypeRef] + ? AlgebraicTypeToType + : [T] extends [ProductType] + ? { [k in keyof U]: AlgebraicTypeToType } + : [T] extends [ArrayType] + ? AlgebraicTypeToType[] + : [T] extends [PrimitiveType] + ? PrimitiveTypeToType + : never; + +type ArgsToType = { + [i in keyof Args]: AlgebraicTypeToType; +}; + +export function registerReducer( + name: string, + params: Args, + func: (...args: ArgsToType) => void +) { + if (typeof name !== 'string') { + throw new TypeError('First argument to registerReducer must be string'); + } + if (!Array.isArray(params)) { + throw new TypeError('Second argument to registerReducer must be array'); + } + const elements = freeze( + params.map(ty => freeze({ name: null, algebraic_type: convertType(ty) })) + ); + register_reducer(name, freeze({ elements }), func); +} + +export function registerType(name: string, type: Type): TypeRef { + if (typeof name !== 'string') { + throw new TypeError('First argument to registerType must be string'); + } + const ref = register_type(name, convertType(type)); + return new TypeRef(ref); +} diff --git a/crates/core/src/host/wasmtime/wasm_instance_env.rs b/crates/core/src/host/wasmtime/wasm_instance_env.rs index cd45950f6e9..f4605274986 100644 --- a/crates/core/src/host/wasmtime/wasm_instance_env.rs +++ b/crates/core/src/host/wasmtime/wasm_instance_env.rs @@ -466,7 +466,7 @@ impl WasmInstanceEnv { /// /// Traps if: /// - `prefix_elems > 0` - /// and (`prefix_ptr` is NULL or `prefix` is not in bounds of WASM memory). + /// and (`prefix_ptr` is NULL or `prefix` is not in bounds of WASM memory). /// - `rstart` is NULL or `rstart` is not in bounds of WASM memory. /// - `rend` is NULL or `rend` is not in bounds of WASM memory. /// - `out` is NULL or `out[..size_of::()]` is not in bounds of WASM memory. @@ -479,11 +479,11 @@ impl WasmInstanceEnv { /// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index. /// - `WRONG_INDEX_ALGO` if the index is not a range-scan compatible index. /// - `BSATN_DECODE_ERROR`, when `prefix` cannot be decoded to - /// a `prefix_elems` number of `AlgebraicValue` - /// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. - /// Or when `rstart` or `rend` cannot be decoded to an `Bound` - /// where the inner `AlgebraicValue`s are - /// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. + /// a `prefix_elems` number of `AlgebraicValue` + /// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. + /// Or when `rstart` or `rend` cannot be decoded to an `Bound` + /// where the inner `AlgebraicValue`s are + /// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. pub fn datastore_index_scan_range_bsatn( caller: Caller<'_, Self>, index_id: u32, @@ -805,7 +805,7 @@ impl WasmInstanceEnv { /// /// Traps if: /// - `prefix_elems > 0` - /// and (`prefix_ptr` is NULL or `prefix` is not in bounds of WASM memory). + /// and (`prefix_ptr` is NULL or `prefix` is not in bounds of WASM memory). /// - `rstart` is NULL or `rstart` is not in bounds of WASM memory. /// - `rend` is NULL or `rend` is not in bounds of WASM memory. /// - `out` is NULL or `out[..size_of::()]` is not in bounds of WASM memory. @@ -818,11 +818,11 @@ impl WasmInstanceEnv { /// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index. /// - `WRONG_INDEX_ALGO` if the index is not a range-compatible index. /// - `BSATN_DECODE_ERROR`, when `prefix` cannot be decoded to - /// a `prefix_elems` number of `AlgebraicValue` - /// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. - /// Or when `rstart` or `rend` cannot be decoded to an `Bound` - /// where the inner `AlgebraicValue`s are - /// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. + /// a `prefix_elems` number of `AlgebraicValue` + /// typed at the initial `prefix_elems` `AlgebraicType`s of the index's key type. + /// Or when `rstart` or `rend` cannot be decoded to an `Bound` + /// where the inner `AlgebraicValue`s are + /// typed at the `prefix_elems + 1` `AlgebraicType` of the index's key type. pub fn datastore_delete_by_index_scan_range_bsatn( caller: Caller<'_, Self>, index_id: u32, @@ -1075,7 +1075,7 @@ impl WasmInstanceEnv { /// /// - `NO_SUCH_BYTES`, when `sink` is not a valid bytes sink. /// - `NO_SPACE`, when there is no room for more bytes in `sink`. - /// (Doesn't currently happen.) + /// (Doesn't currently happen.) pub fn bytes_sink_write( caller: Caller<'_, Self>, sink: u32, diff --git a/crates/core/src/messages/control_db.rs b/crates/core/src/messages/control_db.rs index f3c52cd22da..bdd9463b240 100644 --- a/crates/core/src/messages/control_db.rs +++ b/crates/core/src/messages/control_db.rs @@ -74,4 +74,5 @@ pub struct NodeStatus { #[repr(i32)] pub enum HostType { Wasm = 0, + Js = 1, } diff --git a/crates/core/src/startup.rs b/crates/core/src/startup.rs index e7f2b240981..cb2f7bb0cbc 100644 --- a/crates/core/src/startup.rs +++ b/crates/core/src/startup.rs @@ -161,7 +161,7 @@ fn reload_config(conf_file: &ConfigToml, reload_handle: &reload::Handle prev) { + if prev_time.is_none_or(|prev| modified > prev) { log::info!("reloading log config..."); prev_time = Some(modified); if reload_handle.reload(parse_from_file(conf_file)).is_err() { diff --git a/crates/core/src/vm.rs b/crates/core/src/vm.rs index 07b67b20863..fdc846d6bdd 100644 --- a/crates/core/src/vm.rs +++ b/crates/core/src/vm.rs @@ -284,7 +284,7 @@ where Rhs: RelOps<'a>, { fn filter(&self, index_row: &RelValue<'_>) -> bool { - self.index_select.as_ref().map_or(true, |op| op.eval_bool(index_row)) + self.index_select.as_ref().is_none_or(|op| op.eval_bool(index_row)) } } @@ -383,7 +383,7 @@ where IndexIter: Iterator>, { fn filter(&self, index_row: &RelValue<'_>) -> bool { - self.index_select.as_ref().map_or(true, |op| op.eval_bool(index_row)) + self.index_select.as_ref().is_none_or(|op| op.eval_bool(index_row)) } } diff --git a/crates/data-structures/Cargo.toml b/crates/data-structures/Cargo.toml index d5e52a44e26..1e943350924 100644 --- a/crates/data-structures/Cargo.toml +++ b/crates/data-structures/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license-file = "LICENSE" description = "Assorted data structures used in spacetimedb" +rust-version.workspace = true [features] serde = ["dep:serde", "hashbrown/serde"] @@ -15,3 +16,6 @@ nohash-hasher.workspace = true serde = { workspace = true, optional = true } thiserror.workspace = true smallvec.workspace = true + +[lints] +workspace = true diff --git a/crates/data-structures/src/error_stream.rs b/crates/data-structures/src/error_stream.rs index 537c1919dea..bf727c2f64a 100644 --- a/crates/data-structures/src/error_stream.rs +++ b/crates/data-structures/src/error_stream.rs @@ -407,8 +407,8 @@ impl>>> CollectAllErrors for I /// - `$result` must be a `Result<_, ErrorStream>`. /// - `$expected` is a pattern to match against the error. /// - `$cond` is an optional expression that should evaluate to `true` if the error matches. -/// Variables from `$expected` are bound in `$cond` behind references. -/// Do not use any asserts in `$cond` as it may be called against multiple errors. +/// Variables from `$expected` are bound in `$cond` behind references. +/// Do not use any asserts in `$cond` as it may be called against multiple errors. /// /// ``` /// use spacetimedb_data_structures::error_stream::{ diff --git a/crates/data-structures/src/slim_slice.rs b/crates/data-structures/src/slim_slice.rs index 38c86952b79..6d4d98b6288 100644 --- a/crates/data-structures/src/slim_slice.rs +++ b/crates/data-structures/src/slim_slice.rs @@ -160,7 +160,7 @@ pub unsafe trait SafelyExchangeable {} /// Implementation detail of the other types. /// Provides some convenience but users of the type are responsible /// for safety, invariants and variance. -#[repr(packed)] +#[repr(Rust, packed)] struct SlimRawSlice { /// A valid pointer to the slice data. ptr: NonNull, @@ -201,10 +201,10 @@ impl SlimRawSlice { /// must satisfy [`std::slice::from_raw_parts`]'s requirements. /// That is, /// * `self.ptr` must be valid for reads - /// for `self.len * size_of::` many bytes and must be aligned. + /// for `self.len * size_of::` many bytes and must be aligned. /// /// * `self.ptr` must point to `self.len` - /// consecutive properly initialized values of type `T`. + /// consecutive properly initialized values of type `T`. /// /// * The memory referenced by the returned slice /// must not be mutated for the duration of lifetime `'a`, @@ -228,7 +228,7 @@ impl SlimRawSlice { /// must satisfy [`std::slice::from_raw_parts_mut`]'s requirements. /// That is, /// * `self.ptr` must be [valid] for both reads and writes - /// for `self.len * mem::size_of::()` many bytes, + /// for `self.len * mem::size_of::()` many bytes, /// and it must be properly aligned. /// /// * `self.ptr` must point to `self.len` diff --git a/crates/durability/Cargo.toml b/crates/durability/Cargo.toml index 2616ff4d89f..34816a05437 100644 --- a/crates/durability/Cargo.toml +++ b/crates/durability/Cargo.toml @@ -16,3 +16,6 @@ spacetimedb-paths.workspace = true spacetimedb-sats.workspace = true tokio.workspace = true tracing.workspace = true + +[lints] +workspace = true diff --git a/crates/execution/Cargo.toml b/crates/execution/Cargo.toml index 515ab19c7c3..9eff876d333 100644 --- a/crates/execution/Cargo.toml +++ b/crates/execution/Cargo.toml @@ -16,3 +16,6 @@ spacetimedb-physical-plan.workspace = true spacetimedb-primitives.workspace = true spacetimedb-sql-parser.workspace = true spacetimedb-table.workspace = true + +[lints] +workspace = true diff --git a/crates/expr/Cargo.toml b/crates/expr/Cargo.toml index 489beb3b0a1..afc5fb9f401 100644 --- a/crates/expr/Cargo.toml +++ b/crates/expr/Cargo.toml @@ -22,3 +22,6 @@ spacetimedb-sql-parser.workspace = true pretty_assertions.workspace = true spacetimedb = { path = "../bindings", features = ["unstable"] } spacetimedb-lib = { path = "../lib" } + +[lints] +workspace = true diff --git a/crates/expr/src/errors.rs b/crates/expr/src/errors.rs index c523fb9452e..d611ec945d2 100644 --- a/crates/expr/src/errors.rs +++ b/crates/expr/src/errors.rs @@ -122,6 +122,8 @@ pub struct DuplicateName(pub String); #[error("`filter!` does not support column projections; Must return table rows")] pub struct FilterReturnType; +// FIXME: reduce type size +#[expect(clippy::large_enum_variant)] #[derive(Error, Debug)] pub enum TypingError { #[error(transparent)] diff --git a/crates/fs-utils/Cargo.toml b/crates/fs-utils/Cargo.toml index 6065e4cfed6..3846f579426 100644 --- a/crates/fs-utils/Cargo.toml +++ b/crates/fs-utils/Cargo.toml @@ -16,3 +16,6 @@ zstd-framed.workspace = true [dev-dependencies] tempdir.workspace = true + +[lints] +workspace = true diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 1057d8fb840..3e8cba9e8d1 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -57,3 +57,6 @@ ron.workspace = true # Also as dev-dependencies for use in _this_ crate's tests. proptest.workspace = true proptest-derive.workspace = true + +[lints] +workspace = true diff --git a/crates/metrics/Cargo.toml b/crates/metrics/Cargo.toml index 3267d875412..55468b823d3 100644 --- a/crates/metrics/Cargo.toml +++ b/crates/metrics/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license-file = "LICENSE" description = "Prometheus utilities for SpacetimeDB" +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -12,3 +13,6 @@ itertools.workspace = true paste.workspace = true prometheus.workspace = true arrayvec.workspace = true + +[lints] +workspace = true diff --git a/crates/paths/Cargo.toml b/crates/paths/Cargo.toml index fdc75d472e7..c293807a35e 100644 --- a/crates/paths/Cargo.toml +++ b/crates/paths/Cargo.toml @@ -23,3 +23,6 @@ xdg.workspace = true [dev-dependencies] tempfile.workspace = true + +[lints] +workspace = true diff --git a/crates/paths/src/lib.rs b/crates/paths/src/lib.rs index 6baafb408d7..bd41c0bc629 100644 --- a/crates/paths/src/lib.rs +++ b/crates/paths/src/lib.rs @@ -5,20 +5,20 @@ //! [`SpacetimePaths`] holds the paths to the various directories used by the CLI & database. //! //! * **cli-bin-dir**: a directory under which all versions of all -//! SpacetimeDB binaries is be stored. Each binary is stored in a -//! directory named with version number of the binary in this directory. If a -//! binary has any related files required by that binary which are specific to -//! that version, for example, template configuration files, these files will be -//! installed in this folder as well. +//! SpacetimeDB binaries is be stored. Each binary is stored in a +//! directory named with version number of the binary in this directory. If a +//! binary has any related files required by that binary which are specific to +//! that version, for example, template configuration files, these files will be +//! installed in this folder as well. //! //! * **cli-config-dir**: a directory where configuration and state for the CLI, -//! as well as the keyfiles used by the server, are stored. +//! as well as the keyfiles used by the server, are stored. //! //! * **cli-bin-file**: the location of the default spacetime CLI executable, which -//! is a symlink to the actual `spacetime` binary in the cli-bin-dir. +//! is a symlink to the actual `spacetime` binary in the cli-bin-dir. //! //! * **data-dir**: the directory where all persistent server & database files -//! are stored. +//! are stored. //! //! ## Unix Directory Structure //! diff --git a/crates/physical-plan/Cargo.toml b/crates/physical-plan/Cargo.toml index a8a8a133894..3e92447137d 100644 --- a/crates/physical-plan/Cargo.toml +++ b/crates/physical-plan/Cargo.toml @@ -19,3 +19,6 @@ spacetimedb-table.workspace = true [dev-dependencies] pretty_assertions.workspace = true + +[lints] +workspace = true diff --git a/crates/physical-plan/src/rules.rs b/crates/physical-plan/src/rules.rs index dfcca1142e8..4a45257cabd 100644 --- a/crates/physical-plan/src/rules.rs +++ b/crates/physical-plan/src/rules.rs @@ -3,29 +3,29 @@ //! These include: //! //! * [PushConstEq] -//! Push down predicates of the form `x=1` +//! Push down predicates of the form `x=1` //! * [PushConstAnd] -//! Push down predicates of the form `x=1 and y=2` +//! Push down predicates of the form `x=1 and y=2` //! * [IxScanEq] -//! Generate 1-column index scan for `x=1` +//! Generate 1-column index scan for `x=1` //! * [IxScanAnd] -//! Generate 1-column index scan for `x=1 and y=2` +//! Generate 1-column index scan for `x=1 and y=2` //! * [IxScanEq2Col] -//! Generate 2-column index scan +//! Generate 2-column index scan //! * [IxScanEq3Col] -//! Generate 3-column index scan +//! Generate 3-column index scan //! * [ReorderHashJoin] -//! Reorder the sides of a hash join +//! Reorder the sides of a hash join //! * [ReorderDeltaJoinRhs] -//! Reorder the sides of a hash join with delta tables +//! Reorder the sides of a hash join with delta tables //! * [PullFilterAboveHashJoin] -//! Pull a filter above a hash join with delta tables +//! Pull a filter above a hash join with delta tables //! * [HashToIxJoin] -//! Convert hash join to index join +//! Convert hash join to index join //! * [UniqueIxJoinRule] -//! Mark index join as unique +//! Mark index join as unique //! * [UniqueHashJoinRule] -//! Mark hash join as unique +//! Mark hash join as unique use anyhow::{bail, Result}; use spacetimedb_primitives::{ColId, ColSet, IndexId}; use spacetimedb_schema::schema::IndexSchema; diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 2268fb00235..02ec2571df8 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license-file = "LICENSE" description = "Primitives such as TableId and ColumnIndexAttribute" +rust-version.workspace = true [dependencies] bitflags.workspace = true @@ -13,3 +14,6 @@ itertools.workspace = true [dev-dependencies] proptest.workspace = true + +[lints] +workspace = true diff --git a/crates/primitives/src/col_list.rs b/crates/primitives/src/col_list.rs index 380f7d3bec2..fda0ecc2431 100644 --- a/crates/primitives/src/col_list.rs +++ b/crates/primitives/src/col_list.rs @@ -197,7 +197,7 @@ impl ColList { /// /// If `col >= 63` or `col <= last_col`, the list will become heap allocated if not already. pub fn push(&mut self, col: ColId) { - self.push_inner(col, self.last().map_or(true, |l| l < col)); + self.push_inner(col, self.last().is_none_or(|l| l < col)); } /// Sort and deduplicate the list. diff --git a/crates/query/Cargo.toml b/crates/query/Cargo.toml index 6557044479b..87a465ba0fb 100644 --- a/crates/query/Cargo.toml +++ b/crates/query/Cargo.toml @@ -18,3 +18,6 @@ spacetimedb-primitives.workspace = true spacetimedb-physical-plan.workspace = true spacetimedb-sql-parser.workspace = true spacetimedb-table.workspace = true + +[lints] +workspace = true diff --git a/crates/sats/Cargo.toml b/crates/sats/Cargo.toml index ff57af8b759..659f54e0429 100644 --- a/crates/sats/Cargo.toml +++ b/crates/sats/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license-file = "LICENSE" description = "Spacetime Algebraic Type Notation" +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -62,3 +63,6 @@ rand.workspace = true # Also as dev-dependencies for use in _this_ crate's tests. proptest.workspace = true proptest-derive.workspace = true + +[lints] +workspace = true diff --git a/crates/sats/src/algebraic_value.rs b/crates/sats/src/algebraic_value.rs index 2666765c6d2..f5316018502 100644 --- a/crates/sats/src/algebraic_value.rs +++ b/crates/sats/src/algebraic_value.rs @@ -114,7 +114,7 @@ pub enum AlgebraicValue { /// Wraps `T` making the outer type packed with alignment 1. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[repr(packed)] +#[repr(Rust, packed)] pub struct Packed(pub T); impl From for Packed { diff --git a/crates/sats/src/de.rs b/crates/sats/src/de.rs index 757faa0a243..cb8ecae13f1 100644 --- a/crates/sats/src/de.rs +++ b/crates/sats/src/de.rs @@ -361,7 +361,14 @@ pub trait FieldNameVisitor<'de> { /// Provides the visitor the chance to add valid names into `names`. fn field_names(&self, names: &mut dyn ValidNames); + fn nth_name(&self, i: usize) -> Option<&str> { + let _ = i; + None + } + fn visit(self, name: &str) -> Result; + + fn visit_seq(self, i: usize) -> Result; } /// A trait for types storing a set of valid names. @@ -735,3 +742,38 @@ fn one_of_names(names: impl Fn(&mut dyn ValidNames)) -> Option(PhantomData); + +impl NoneAccess { + pub fn new() -> Self { + NoneAccess(PhantomData) + } +} + +impl Default for NoneAccess { + fn default() -> Self { + Self::new() + } +} + +impl SumAccess<'_> for NoneAccess { + type Error = E; + type Variant = Self; + + fn variant(self, visitor: V) -> Result<(V::Output, Self::Variant), Self::Error> { + visitor.visit_name("none").map(|var| (var, self)) + } +} +impl<'de, E: Error> VariantAccess<'de> for NoneAccess { + type Error = E; + fn deserialize_seed>(self, seed: T) -> Result { + use crate::algebraic_value::de::*; + seed.deserialize(ValueDeserializer::new(crate::AlgebraicValue::unit())) + .map_err(|err| match err { + ValueDeserializeError::MismatchedType => E::custom("mismatched type"), + ValueDeserializeError::Custom(err) => E::custom(err), + }) + } +} diff --git a/crates/sats/src/de/impls.rs b/crates/sats/src/de/impls.rs index 4cc448f0a04..7a3b3c33a16 100644 --- a/crates/sats/src/de/impls.rs +++ b/crates/sats/src/de/impls.rs @@ -647,6 +647,10 @@ impl FieldNameVisitor<'_> for TupleNameVisitor<'_> { names.extend(self.elems.iter().filter_map(|f| f.name())) } + fn nth_name(&self, i: usize) -> Option<&str> { + self.elems.get(i).and_then(|f| f.name()) + } + fn kind(&self) -> ProductKind { self.kind } @@ -658,6 +662,10 @@ impl FieldNameVisitor<'_> for TupleNameVisitor<'_> { .position(|f| f.has_name(name)) .ok_or_else(|| Error::unknown_field_name(name, &self)) } + + fn visit_seq(self, i: usize) -> Result { + Ok(i) + } } impl_deserialize!([] spacetimedb_primitives::TableId, de => u32::deserialize(de).map(Self)); diff --git a/crates/sats/src/de/serde.rs b/crates/sats/src/de/serde.rs index bf56fb57c41..88adb03502f 100644 --- a/crates/sats/src/de/serde.rs +++ b/crates/sats/src/de/serde.rs @@ -2,7 +2,6 @@ use super::Deserializer; use crate::serde::{SerdeError, SerdeWrapper}; use crate::{i256, u256}; use core::fmt; -use core::marker::PhantomData; use serde::de as serde; /// Converts any [`serde::Deserializer`] to a SATS [`Deserializer`] @@ -235,28 +234,6 @@ impl<'de, A: serde::SeqAccess<'de>> super::SeqProductAccess<'de> for SeqTupleAcc } } -/// Deserializes `none` variant of an optional value. -struct NoneAccess(PhantomData); -impl super::SumAccess<'_> for NoneAccess { - type Error = E; - type Variant = Self; - - fn variant(self, visitor: V) -> Result<(V::Output, Self::Variant), Self::Error> { - visitor.visit_name("none").map(|var| (var, self)) - } -} -impl<'de, E: super::Error> super::VariantAccess<'de> for NoneAccess { - type Error = E; - fn deserialize_seed>(self, seed: T) -> Result { - use crate::algebraic_value::de::*; - seed.deserialize(ValueDeserializer::new(crate::AlgebraicValue::unit())) - .map_err(|err| match err { - ValueDeserializeError::MismatchedType => E::custom("mismatched type"), - ValueDeserializeError::Custom(err) => E::custom(err), - }) - } -} - /// Converts a SATS `SumVisitor` to `serde::Visitor`. struct EnumVisitor { /// The `SumVisitor`. @@ -289,7 +266,7 @@ impl<'de, V: super::SumVisitor<'de>> serde::Visitor<'de> for EnumVisitor { fn visit_unit(self) -> Result { if self.visitor.is_option() { - self.visitor.visit_sum(NoneAccess(PhantomData)).map_err(unwrap_error) + self.visitor.visit_sum(super::NoneAccess::new()).map_err(unwrap_error) } else { Err(E::invalid_type(serde::Unexpected::Unit, &self)) } diff --git a/crates/sats/src/ser.rs b/crates/sats/src/ser.rs index bfbe392a219..98be558a03b 100644 --- a/crates/sats/src/ser.rs +++ b/crates/sats/src/ser.rs @@ -6,6 +6,7 @@ mod impls; pub mod serde; use core::fmt; +use std::marker::PhantomData; /// A data format that can deserialize any data structure supported by SATs. /// @@ -128,7 +129,18 @@ pub trait Serializer: Sized { /// /// - `AlgebraicValue::decode(ty, &mut bsatn).is_ok()`. /// That is, `bsatn` encodes a valid element of `ty`. - unsafe fn serialize_bsatn(self, ty: &AlgebraicType, bsatn: &[u8]) -> Result; + unsafe fn serialize_bsatn(self, ty: &AlgebraicType, bsatn: &[u8]) -> Result { + // TODO(Centril): Consider instead deserializing the `bsatn` through a + // deserializer that serializes into `self` directly. + + // First convert the BSATN to an `AlgebraicValue`. + // SAFETY: Forward caller requirements of this method to that we are calling. + let res = unsafe { ValueSerializer.serialize_bsatn(ty, bsatn) }; + let value = res.unwrap_or_else(|x| match x {}); + + // Then serialize that. + value.serialize(self) + } /// Serialize the given `bsatn` encoded data of type `ty`. /// @@ -160,7 +172,19 @@ pub trait Serializer: Sized { ty: &AlgebraicType, total_bsatn_len: usize, bsatn: I, - ) -> Result; + ) -> Result { + // TODO(Centril): Unlike above, in this case we must at minimum concatenate `bsatn` + // before we can do the piping mentioned above, but that's better than + // serializing to `AlgebraicValue` first, so consider that. + + // First convert the BSATN to an `AlgebraicValue`. + // SAFETY: Forward caller requirements of this method to that we are calling. + let res = unsafe { ValueSerializer.serialize_bsatn_in_chunks(ty, total_bsatn_len, bsatn) }; + let value = res.unwrap_or_else(|x| match x {}); + + // Then serialize that. + value.serialize(self) + } /// Serialize the given `string`. /// @@ -194,12 +218,22 @@ pub trait Serializer: Sized { self, total_len: usize, string: I, - ) -> Result; + ) -> Result { + // First convert the `string` to an `AlgebraicValue`. + // SAFETY: Forward caller requirements of this method to that we are calling. + let res = unsafe { ValueSerializer.serialize_str_in_chunks(total_len, string) }; + let value = res.unwrap_or_else(|x| match x {}); + + // Then serialize that. + // This incurs a very minor cost of branching on `AlgebraicValue::String`. + value.serialize(self) + } } use ethnum::{i256, u256}; pub use spacetimedb_bindings_macro::Serialize; +use crate::algebraic_value::ser::ValueSerializer; use crate::{bsatn, buffer::BufWriter, AlgebraicType}; /// A **data structure** that can be serialized into any data format supported by @@ -355,3 +389,49 @@ impl SerializeNamedProduct for ForwardNamedToSeqProduct< self.tup.end() } } + +enum Void {} + +pub struct Impossible { + void: Void, + marker: PhantomData<(Ok, Error)>, +} + +impl SerializeArray for Impossible { + type Ok = Ok; + type Error = Error; + + fn serialize_element(&mut self, _element: &T) -> Result<(), Self::Error> { + match self.void {} + } + + fn end(self) -> Result { + match self.void {} + } +} + +impl SerializeSeqProduct for Impossible { + type Ok = Ok; + type Error = Error; + + fn serialize_element(&mut self, _element: &T) -> Result<(), Self::Error> { + match self.void {} + } + + fn end(self) -> Result { + match self.void {} + } +} + +impl SerializeNamedProduct for Impossible { + type Ok = Ok; + type Error = Error; + + fn serialize_element(&mut self, _name: Option<&str>, _elem: &T) -> Result<(), Self::Error> { + match self.void {} + } + + fn end(self) -> Result { + match self.void {} + } +} diff --git a/crates/sats/src/ser/serde.rs b/crates/sats/src/ser/serde.rs index 53a7a381adc..218472c487e 100644 --- a/crates/sats/src/ser/serde.rs +++ b/crates/sats/src/ser/serde.rs @@ -1,10 +1,8 @@ -use super::Serialize as _; +use crate::{i256, u256}; use crate::{ - algebraic_value::ser::ValueSerializer, ser::{self, Serializer}, serde::{SerdeError, SerdeWrapper}, }; -use crate::{i256, u256}; use core::fmt; use serde::ser as serde; @@ -123,53 +121,6 @@ impl Serializer for SerdeSerializer { seq.end().map_err(SerdeError) } } - - unsafe fn serialize_bsatn(self, ty: &crate::AlgebraicType, bsatn: &[u8]) -> Result { - // TODO(Centril): Consider instead deserializing the `bsatn` through a - // deserializer that serializes into `self` directly. - - // First convert the BSATN to an `AlgebraicValue`. - // SAFETY: Forward caller requirements of this method to that we are calling. - let res = unsafe { ValueSerializer.serialize_bsatn(ty, bsatn) }; - let value = res.unwrap_or_else(|x| match x {}); - - // Then serialize that. - value.serialize(self) - } - - unsafe fn serialize_bsatn_in_chunks<'a, I: Clone + Iterator>( - self, - ty: &crate::AlgebraicType, - total_bsatn_len: usize, - bsatn: I, - ) -> Result { - // TODO(Centril): Unlike above, in this case we must at minimum concatenate `bsatn` - // before we can do the piping mentioned above, but that's better than - // serializing to `AlgebraicValue` first, so consider that. - - // First convert the BSATN to an `AlgebraicValue`. - // SAFETY: Forward caller requirements of this method to that we are calling. - let res = unsafe { ValueSerializer.serialize_bsatn_in_chunks(ty, total_bsatn_len, bsatn) }; - let value = res.unwrap_or_else(|x| match x {}); - - // Then serialize that. - value.serialize(self) - } - - unsafe fn serialize_str_in_chunks<'a, I: Clone + Iterator>( - self, - total_len: usize, - string: I, - ) -> Result { - // First convert the `string` to an `AlgebraicValue`. - // SAFETY: Forward caller requirements of this method to that we are calling. - let res = unsafe { ValueSerializer.serialize_str_in_chunks(total_len, string) }; - let value = res.unwrap_or_else(|x| match x {}); - - // Then serialize that. - // This incurs a very minor cost of branching on `AlgebraicValue::String`. - value.serialize(self) - } } /// Serializes array elements by forwarding to `S: serde::SerializeSeq`. diff --git a/crates/schema/Cargo.toml b/crates/schema/Cargo.toml index 5dd3449c303..c8d55b9e52f 100644 --- a/crates/schema/Cargo.toml +++ b/crates/schema/Cargo.toml @@ -37,3 +37,6 @@ spacetimedb-testing = { path = "../testing" } proptest.workspace = true serial_test.workspace = true + +[lints] +workspace = true diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 9daa2a983eb..ec184e73536 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license-file = "LICENSE" description = "A Rust SDK for clients to interface with SpacetimeDB" +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -39,3 +40,6 @@ futures-channel.workspace = true # for tests spacetimedb-testing = { path = "../testing" } + +[lints] +workspace = true diff --git a/crates/sdk/tests/connect_disconnect_client/Cargo.toml b/crates/sdk/tests/connect_disconnect_client/Cargo.toml index ac15d05e882..126d8568ac5 100644 --- a/crates/sdk/tests/connect_disconnect_client/Cargo.toml +++ b/crates/sdk/tests/connect_disconnect_client/Cargo.toml @@ -10,3 +10,6 @@ license-file = "LICENSE" spacetimedb-sdk = { path = "../.." } test-counter = { path = "../test-counter" } anyhow.workspace = true + +[lints] +workspace = true diff --git a/crates/sdk/tests/test-client/Cargo.toml b/crates/sdk/tests/test-client/Cargo.toml index 77e8cdd891c..7a7167234a5 100644 --- a/crates/sdk/tests/test-client/Cargo.toml +++ b/crates/sdk/tests/test-client/Cargo.toml @@ -13,3 +13,6 @@ tokio.workspace = true anyhow.workspace = true env_logger.workspace = true rand.workspace = true + +[lints] +workspace = true diff --git a/crates/sdk/tests/test-counter/Cargo.toml b/crates/sdk/tests/test-counter/Cargo.toml index 741ffccb773..969dc417cf6 100644 --- a/crates/sdk/tests/test-counter/Cargo.toml +++ b/crates/sdk/tests/test-counter/Cargo.toml @@ -9,3 +9,6 @@ license-file = "LICENSE" [dependencies] spacetimedb-data-structures.workspace = true anyhow.workspace = true + +[lints] +workspace = true diff --git a/crates/snapshot/Cargo.toml b/crates/snapshot/Cargo.toml index fda2af1ebcf..461d8fe9969 100644 --- a/crates/snapshot/Cargo.toml +++ b/crates/snapshot/Cargo.toml @@ -37,3 +37,6 @@ anyhow.workspace = true env_logger.workspace = true pretty_assertions = { workspace = true, features = ["unstable"] } rand.workspace = true + +[lints] +workspace = true diff --git a/crates/sql-parser/Cargo.toml b/crates/sql-parser/Cargo.toml index 15cee91fa23..5ed77a05bdc 100644 --- a/crates/sql-parser/Cargo.toml +++ b/crates/sql-parser/Cargo.toml @@ -11,3 +11,6 @@ derive_more.workspace = true sqlparser.workspace = true thiserror.workspace = true spacetimedb-lib.workspace = true + +[lints] +workspace = true diff --git a/crates/sql-parser/src/parser/errors.rs b/crates/sql-parser/src/parser/errors.rs index 0dc7b773a08..510a5747414 100644 --- a/crates/sql-parser/src/parser/errors.rs +++ b/crates/sql-parser/src/parser/errors.rs @@ -9,6 +9,8 @@ use sqlparser::{ }; use thiserror::Error; +// FIXME: reduce type size +#[expect(clippy::large_enum_variant)] #[derive(Error, Debug)] pub enum SubscriptionUnsupported { #[error("Unsupported SELECT: {0}")] @@ -25,6 +27,8 @@ impl SubscriptionUnsupported { } } +// FIXME: reduce type size +#[expect(clippy::large_enum_variant)] #[derive(Error, Debug)] pub enum SqlUnsupported { #[error("Unsupported literal expression: {0}")] diff --git a/crates/sqltest/Cargo.toml b/crates/sqltest/Cargo.toml index 648c68c2124..dffde7d2564 100644 --- a/crates/sqltest/Cargo.toml +++ b/crates/sqltest/Cargo.toml @@ -32,3 +32,6 @@ sqllogictest.workspace = true tempfile.workspace = true tokio.workspace = true tokio-postgres.workspace = true + +[lints] +workspace = true diff --git a/crates/sqltest/src/db.rs b/crates/sqltest/src/db.rs index e10805aa981..05b4c4f0bab 100644 --- a/crates/sqltest/src/db.rs +++ b/crates/sqltest/src/db.rs @@ -5,6 +5,7 @@ use async_trait::async_trait; use spacetimedb::error::DBError; use sqllogictest::{AsyncDB, DBOutput}; +#[expect(clippy::large_enum_variant)] pub enum DBRunner { Sqlite(Sqlite), Space(SpaceDb), diff --git a/crates/standalone/Cargo.toml b/crates/standalone/Cargo.toml index a1583855c37..4ff0d51f787 100644 --- a/crates/standalone/Cargo.toml +++ b/crates/standalone/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license-file = "LICENSE" description = "An executable for running a single SpacetimeDB standalone instance" +rust-version.workspace = true [[bin]] name = "spacetimedb-standalone" # The name of the target. @@ -56,3 +57,6 @@ tikv-jemalloc-ctl = {workspace = true} [dev-dependencies] once_cell.workspace = true tempfile.workspace = true + +[lints] +workspace = true diff --git a/crates/standalone/Dockerfile b/crates/standalone/Dockerfile index 59b85b50ed5..db2f595c026 100644 --- a/crates/standalone/Dockerfile +++ b/crates/standalone/Dockerfile @@ -1,7 +1,7 @@ ARG CARGO_PROFILE=release -FROM rust:1.84.0 AS chef +FROM rust:1.87.0 AS chef RUN rust_target=$(rustc -vV | awk '/^host:/{ print $2 }') && \ curl https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-$rust_target.tgz -fL | tar xz -C $CARGO_HOME/bin RUN cargo binstall -y cargo-chef@0.1.70 diff --git a/crates/standalone/src/subcommands/extract_schema.rs b/crates/standalone/src/subcommands/extract_schema.rs index 9b2e5379f06..4e7b7a831d9 100644 --- a/crates/standalone/src/subcommands/extract_schema.rs +++ b/crates/standalone/src/subcommands/extract_schema.rs @@ -22,12 +22,14 @@ struct Args { #[derive(clap::ValueEnum, Copy, Clone)] enum HostType { Wasm, + Js, } impl From for control_db::HostType { fn from(x: HostType) -> Self { match x { HostType::Wasm => control_db::HostType::Wasm, + HostType::Js => control_db::HostType::Js, } } } diff --git a/crates/subscription/Cargo.toml b/crates/subscription/Cargo.toml index 17b88269dbc..54f29e864ae 100644 --- a/crates/subscription/Cargo.toml +++ b/crates/subscription/Cargo.toml @@ -13,4 +13,6 @@ spacetimedb-expr.workspace = true spacetimedb-lib.workspace = true spacetimedb-primitives.workspace = true spacetimedb-physical-plan.workspace = true -spacetimedb-query.workspace = true \ No newline at end of file +spacetimedb-query.workspace = true +[lints] +workspace = true diff --git a/crates/table/Cargo.toml b/crates/table/Cargo.toml index 5a4a7cf5341..e2f9557701e 100644 --- a/crates/table/Cargo.toml +++ b/crates/table/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license-file = "LICENSE" description = "A database Table implementation and friends" +rust-version.workspace = true [[bench]] name = "pointer_map" @@ -57,3 +58,6 @@ criterion.workspace = true proptest.workspace = true proptest-derive.workspace = true rand.workspace = true + +[lints] +workspace = true diff --git a/crates/table/benches/page_manager.rs b/crates/table/benches/page_manager.rs index 6391bf15bff..b8d2a2e4cb8 100644 --- a/crates/table/benches/page_manager.rs +++ b/crates/table/benches/page_manager.rs @@ -782,8 +782,7 @@ fn insert_num_same( mut make_row: impl FnMut() -> R, num_same: usize, ) -> Option { - iter::repeat(make_row().to_product()) - .take(num_same) + iter::repeat_n(make_row().to_product(), num_same) .zip(0u32..) .map(|(mut row, n)| { if let Some(slot) = row.elements.get_mut(1) { diff --git a/crates/table/src/page.rs b/crates/table/src/page.rs index c6eeb2e6859..39f43abf78d 100644 --- a/crates/table/src/page.rs +++ b/crates/table/src/page.rs @@ -12,25 +12,25 @@ //! Technical terms: //! //! - `valid` refers to, when referring to a type, granule, or row, -//! depending on the context, a memory location that holds a *safe* object. -//! When "valid for writes" is used, the location must be properly aligned -//! and none of its bytes may be uninit, -//! but the value need not be valid at the type in question. -//! "Valid for writes" is equivalent to valid-unconstrained. +//! depending on the context, a memory location that holds a *safe* object. +//! When "valid for writes" is used, the location must be properly aligned +//! and none of its bytes may be uninit, +//! but the value need not be valid at the type in question. +//! "Valid for writes" is equivalent to valid-unconstrained. //! //! - `valid-unconstrained`, when referring to a memory location with a given type, -//! that the location stores a byte pattern which Rust/LLVM's memory model recognizes as valid, -//! and therefore must not contain any uninit, -//! but the value is not required to be logically meaningful, -//! and no code may depend on the data within it to uphold any invariants. -//! E.g. an unallocated [`VarLenGranule`] within a page stores valid-unconstrained bytes, -//! because the bytes are either 0 from the initial [`alloc_zeroed`] of the page, -//! or contain stale data from a previously freed [`VarLenGranule`]. +//! that the location stores a byte pattern which Rust/LLVM's memory model recognizes as valid, +//! and therefore must not contain any uninit, +//! but the value is not required to be logically meaningful, +//! and no code may depend on the data within it to uphold any invariants. +//! E.g. an unallocated [`VarLenGranule`] within a page stores valid-unconstrained bytes, +//! because the bytes are either 0 from the initial [`alloc_zeroed`] of the page, +//! or contain stale data from a previously freed [`VarLenGranule`]. //! //! - `unused` means that it is safe to overwrite a block of memory without cleaning up its previous value. //! -//! See the post [Two Kinds of Invariants: Safety and Validity][ralf_safe_valid] -//! for a discussion on safety and validity invariants. +//! See the post [Two Kinds of Invariants: Safety and Validity][ralf_safe_valid] +//! for a discussion on safety and validity invariants. use super::{ blob_store::BlobStore, @@ -660,6 +660,7 @@ impl<'page> VarView<'page> { /// b. For each `(_, len) ∈ cs`, caller must ensure that /// the relevant granule is initialized with data for at least `len` /// before the granule's data is read from / assumed to be initialized. + #[expect(clippy::doc_overindented_list_items)] fn alloc_for_obj_common<'chunk, Cs: Iterator>( &mut self, obj_len: usize, diff --git a/crates/table/src/pages.rs b/crates/table/src/pages.rs index f87ab3cbacb..d0e624d59de 100644 --- a/crates/table/src/pages.rs +++ b/crates/table/src/pages.rs @@ -175,8 +175,8 @@ impl Pages { /// - `var_len_visitor` must be suitable for visiting var-len refs in `fixed_row`. /// - `fixed_row.len()` matches the row type size exactly. /// - `fixed_row.len()` is consistent - /// with what has been passed to the manager in all other ops - /// and must be consistent with the `var_len_visitor` the manager was made with. + /// with what has been passed to the manager in all other ops + /// and must be consistent with the `var_len_visitor` the manager was made with. // TODO(bikeshedding): rename to make purpose as bench interface clear? pub unsafe fn insert_row( &mut self, diff --git a/crates/testing/Cargo.toml b/crates/testing/Cargo.toml index 50dbd287378..ff9d70e306e 100644 --- a/crates/testing/Cargo.toml +++ b/crates/testing/Cargo.toml @@ -30,3 +30,6 @@ serde.workspace = true [dev-dependencies] serial_test.workspace = true + +[lints] +workspace = true diff --git a/crates/update/Cargo.toml b/crates/update/Cargo.toml index a698251f37c..385c5c0c02a 100644 --- a/crates/update/Cargo.toml +++ b/crates/update/Cargo.toml @@ -34,3 +34,6 @@ zip = "2.3" [target.'cfg(windows)'.dependencies] windows-sys = { workspace = true, features = ["Win32_System_Console"] } + +[lints] +workspace = true diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 5752dde0ca9..cb527c0d8f5 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true license-file = "LICENSE" description = "A VM for SpacetimeDB" +rust-version.workspace = true [dependencies] spacetimedb-data-structures.workspace = true @@ -26,3 +27,6 @@ tracing.workspace = true [dev-dependencies] tempfile.workspace = true typed-arena.workspace = true + +[lints] +workspace = true diff --git a/rust-toolchain.toml b/rust-toolchain.toml index c1e42e58bc4..3d400b15420 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,7 @@ [toolchain] -# change crates/{standalone,bench}/Dockerfile and rust-version in Cargo.toml too! -# also the docker image tag in .github/workflows/benchmarks.yml:jobs/callgrind_benchmark/container/image -channel = "1.84.0" +# change crates/{standalone,bench}/Dockerfile, and the docker image tag in +# .github/workflows/benchmarks.yml:jobs/callgrind_benchmark/container/image +# maybe also the rust-version in Cargo.toml +channel = "1.87.0" profile = "default" targets = ["wasm32-unknown-unknown"]