From 56585bfca5abb56304a49f79dedd09ddd9a2c8e7 Mon Sep 17 00:00:00 2001 From: Noa Date: Tue, 29 Apr 2025 12:08:51 -0500 Subject: [PATCH] wip --- .prettierrc | 11 + Cargo.lock | 164 +++++++- crates/core/Cargo.toml | 2 + crates/core/src/host/mod.rs | 1 + crates/core/src/host/v8/mod.rs | 577 ++++++++++++++++++++++++++ crates/core/src/host/v8/test_code.ts | 25 ++ crates/core/src/host/v8/tsconfig.json | 9 + crates/core/src/host/v8/types.d.ts | 57 +++ crates/core/src/host/v8/util.rs | 279 +++++++++++++ crates/core/src/host/v8/wrapper.ts | 301 ++++++++++++++ 10 files changed, 1420 insertions(+), 6 deletions(-) create mode 100644 .prettierrc create mode 100644 crates/core/src/host/v8/mod.rs create mode 100644 crates/core/src/host/v8/test_code.ts create mode 100644 crates/core/src/host/v8/tsconfig.json create mode 100644 crates/core/src/host/v8/types.d.ts create mode 100644 crates/core/src/host/v8/util.rs create mode 100644 crates/core/src/host/v8/wrapper.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..648391fd82 --- /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 6c3e66c3aa..1a97b306f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,12 @@ dependencies = [ "gimli 0.31.1", ] +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler2" version = "2.0.0" @@ -381,7 +387,7 @@ dependencies = [ "addr2line 0.24.2", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.8", "object", "rustc-demangle", "windows-targets 0.52.6", @@ -429,6 +435,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.101", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -712,6 +738,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 +811,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" @@ -1043,7 +1089,7 @@ dependencies = [ "hashbrown 0.14.5", "log", "regalloc2", - "rustc-hash", + "rustc-hash 2.1.1", "smallvec", "target-lexicon", ] @@ -1828,7 +1874,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.8", ] [[package]] @@ -1886,6 +1932,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" @@ -2069,6 +2125,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" @@ -2488,7 +2553,7 @@ dependencies = [ "shlex", "tempfile", "version-compare", - "which", + "which 4.4.2", ] [[package]] @@ -2953,6 +3018,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" @@ -3168,6 +3243,21 @@ 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.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.8.8" @@ -3307,6 +3397,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" @@ -3901,6 +4001,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" @@ -4333,7 +4443,7 @@ checksum = "12908dbeb234370af84d0579b9f68258a0f67e201412dd9a2814e6f45b2fc0f0" dependencies = [ "hashbrown 0.14.5", "log", - "rustc-hash", + "rustc-hash 2.1.1", "slice-group-by", "smallvec", ] @@ -4622,6 +4732,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -5506,7 +5622,7 @@ dependencies = [ "regex", "reqwest 0.12.15", "rustc-demangle", - "rustc-hash", + "rustc-hash 2.1.1", "scopeguard", "semver", "serde", @@ -5564,6 +5680,7 @@ dependencies = [ "url", "urlencoding", "uuid", + "v8", "wasmtime", ] @@ -7187,6 +7304,23 @@ dependencies = [ "getrandom 0.3.2", ] +[[package]] +name = "v8" +version = "134.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21c7a224a7eaf3f98c1bad772fbaee56394dce185ef7b19a2e0ca5e3d274165d" +dependencies = [ + "bindgen", + "bitflags 2.9.0", + "fslock", + "gzip-header", + "home", + "miniz_oxide 0.7.4", + "once_cell", + "paste", + "which 6.0.3", +] + [[package]] name = "valuable" version = "0.1.1" @@ -7661,6 +7795,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" @@ -8143,6 +8289,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/crates/core/Cargo.toml b/crates/core/Cargo.toml index 5aa122f156..ed87821b9c 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -116,6 +116,8 @@ jwks.workspace = true async_cache = "0.3.1" faststr = "0.2.23" core_affinity = "0.8" +v8 = "134" +# sourcemap = "9" [target.'cfg(not(target_env = "msvc"))'.dependencies] tikv-jemallocator = {workspace = true} diff --git a/crates/core/src/host/mod.rs b/crates/core/src/host/mod.rs index 151e6d233a..3598ae9e25 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/v8/mod.rs b/crates/core/src/host/v8/mod.rs new file mode 100644 index 0000000000..dd87fda2e1 --- /dev/null +++ b/crates/core/src/host/v8/mod.rs @@ -0,0 +1,577 @@ +use std::sync::{Arc, LazyLock}; + +use crate::database_logger::{BacktraceProvider, LogLevel, Record}; +use crate::db::datastore::locking_tx_datastore::MutTxId; +use crate::db::datastore::traits::Program; +use crate::energy::EnergyMonitor; +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 util; + +use indexmap::IndexMap; +use itertools::Itertools; +use spacetimedb_lib::db::raw_def::v9::{sats_name_to_scoped_name, RawModuleDefV9, RawReducerDefV9, RawTypeDefV9}; +use spacetimedb_lib::sats; +use util::{ + ascii_str, iter_array, module, scratch_buf, strings, throw, ErrorOrException, ExcResult, ExceptionOptionExt, + ExceptionThrown, ObjectExt, ThrowExceptionResultExt, TypeError, +}; + +#[allow(unused)] +struct V8InstanceEnv { + instance_env: InstanceEnv, +} + +#[allow(unused)] +pub struct JsModule { + replica_context: Arc, + scheduler: Scheduler, + info: Arc, + energy_monitor: Arc, + snapshot: Arc<[u8]>, + module_builder: ModuleBuilder, +} + +#[allow(unused)] +pub fn compile_real(mcc: ModuleCreationContext) -> anyhow::Result { + let program = std::str::from_utf8(&mcc.program.bytes)?; + let (snapshot, module_builder) = compile(program, Arc::new(Logger))?; + Ok(JsModule { + replica_context: mcc.replica_ctx, + scheduler: mcc.scheduler, + info: todo!(), + energy_monitor: mcc.energy_monitor, + snapshot, + module_builder, + }) +} + +#[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), 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), +); + +static EXTERN_REFS: LazyLock = + LazyLock::new(|| v8::ExternalReferences::new(&spacetime_sys_10_0::external_refs().collect_vec())); + +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 js_to_type(scope: &mut v8::HandleScope<'_>, val: v8::Local<'_, v8::Value>) -> ExcResult { + strings!( + REF = "ref", + TYPE = "type", + SUM = "sum", + PRODUCT = "product", + ARRAY = "array", + STRING = "string", + BOOL = "bool", + I8 = "i8", + U8 = "u8", + I16 = "i16", + U16 = "u16", + I32 = "i32", + U32 = "u32", + I64 = "i64", + U64 = "u64", + I128 = "i128", + U128 = "u128", + I256 = "i256", + U256 = "u256", + F32 = "f32", + F64 = "f64", + ); + + let val = val.cast::(); + let ty = val.get_str(scope, &TYPE).err()?; + + if ty == REF.string(scope) { + let r = val.get_str(scope, &REF).err()?.cast::().value() as u32; + Ok(sats::AlgebraicType::Ref(sats::AlgebraicTypeRef(r))) + } else if ty == PRODUCT.string(scope) { + let elements = val.get_str(scope, ascii_str!("elements")).err()?.cast(); + let elements = iter_array(scope, elements, |scope, elem| { + let elem = elem.cast::(); + + let name = elem.get_str(scope, ascii_str!("name")).err()?; + let name = if name.is_null_or_undefined() { + None + } else { + Some(name.cast::().to_rust_string_lossy(scope).into()) + }; + + let ty = elem.get_str(scope, ascii_str!("algebraic_type")).err()?; + let ty = js_to_type(scope, ty)?; + + Ok(sats::ProductTypeElement::new(ty, name)) + }) + .collect::, _>>()?; + + Ok(sats::AlgebraicType::product(elements)) + } else if ty == ARRAY.string(scope) { + let elem_ty = val.get_str(scope, ascii_str!("elem_ty")).err()?; + let elem_ty = js_to_type(scope, elem_ty)?; + Ok(sats::AlgebraicType::array(elem_ty)) + } else if ty == STRING.string(scope) { + Ok(sats::AlgebraicType::String) + } else if ty == BOOL.string(scope) { + Ok(sats::AlgebraicType::Bool) + } else if ty == I8.string(scope) { + Ok(sats::AlgebraicType::I8) + } else if ty == U8.string(scope) { + Ok(sats::AlgebraicType::U8) + } else if ty == I16.string(scope) { + Ok(sats::AlgebraicType::I16) + } else if ty == U16.string(scope) { + Ok(sats::AlgebraicType::U16) + } else if ty == I32.string(scope) { + Ok(sats::AlgebraicType::I32) + } else if ty == U32.string(scope) { + Ok(sats::AlgebraicType::U32) + } else if ty == I64.string(scope) { + Ok(sats::AlgebraicType::I64) + } else if ty == U64.string(scope) { + Ok(sats::AlgebraicType::U64) + } else if ty == I128.string(scope) { + Ok(sats::AlgebraicType::I128) + } else if ty == U128.string(scope) { + Ok(sats::AlgebraicType::U128) + } else if ty == I256.string(scope) { + Ok(sats::AlgebraicType::I256) + } else if ty == U256.string(scope) { + Ok(sats::AlgebraicType::U256) + } else if ty == F32.string(scope) { + Ok(sats::AlgebraicType::F32) + } else if ty == F64.string(scope) { + Ok(sats::AlgebraicType::F64) + } else { + throw(scope, TypeError(ascii_str!("Unknown type"))) + } +} + +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 = js_to_type(scope, params)?.into_product().unwrap(); + + 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 = js_to_type(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 platform = v8::new_default_platform(0, false).make_shared(); + v8::V8::initialize_platform(platform); + v8::V8::initialize(); + 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!(); +} + +#[allow(unused)] +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 {} + +#[allow(unused)] +impl ModuleInstance for JsInstance { + fn trapped(&self) -> bool { + false + } + + fn init_database(&mut self, program: Program) -> anyhow::Result> { + todo!() + } + + fn update_database( + &mut self, + program: Program, + old_module_info: Arc, + ) -> anyhow::Result { + todo!() + } + + fn call_reducer(&mut self, tx: Option, params: CallReducerParams) -> ReducerCallResult { + todo!() + } +} 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 0000000000..54c22574e9 --- /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 0000000000..a6f87031a6 --- /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 0000000000..5a0f6b6734 --- /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; + + export type AlgebraicType = TypeRef | ProductType | ArrayType | PrimitiveType; + export type TypeRef = Readonly<{ + type: 'ref'; + ref: number; + }>; + export type ProductType = Readonly<{ + type: 'product'; + elements: readonly ProductTypeElement[]; + }>; + export type ProductTypeElement = Readonly<{ + name: string | null; + algebraic_type: AlgebraicType; + }>; + export type ArrayType = Readonly<{ + type: 'array'; + elem_ty: AlgebraicType; + }>; + export type PrimitiveType = Readonly< + | { type: 'string' } + | { type: 'bool' } + | { type: 'i8' } + | { type: 'u8' } + | { type: 'i16' } + | { type: 'u16' } + | { type: 'i32' } + | { type: 'u32' } + | { type: 'i64' } + | { type: 'u64' } + | { type: 'i128' } + | { type: 'u128' } + | { type: 'i256' } + | { type: 'u256' } + | { type: 'f32' } + | { type: 'f64' } + >; +} diff --git a/crates/core/src/host/v8/util.rs b/crates/core/src/host/v8/util.rs new file mode 100644 index 0000000000..ed44b604bf --- /dev/null +++ b/crates/core/src/host/v8/util.rs @@ -0,0 +1,279 @@ +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) trait ObjectExt { + fn get_str<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + key: &'static StringConst, + ) -> Option>; +} + +impl ObjectExt for v8::Object { + fn get_str<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + key: &'static StringConst, + ) -> Option> { + let key = key.string(scope); + self.get(scope, key.into()) + } +} + +pub(super) fn iter_array<'a, 'b, 's, T, F>( + scope: &'a mut v8::HandleScope<'s>, + array: v8::Local<'b, v8::Array>, + mut map: F, +) -> impl Iterator> + use<'a, 'b, 's, T, F> +where + F: FnMut(&mut v8::HandleScope<'s>, v8::Local<'s, v8::Value>) -> ExcResult + 'a, +{ + (0..array.length()).map(move |i| { + let val = array.get_index(scope, i).err()?; + map(scope, val) + }) +} + +#[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<'s, T, E>(scope: &mut v8::HandleScope<'s>, err: E) -> Result +where + E: IntoException<'s>, +{ + let exc = err.into_exception(scope); + scope.throw_exception(exc); + Err(ExceptionThrown) +} + +pub(super) trait ThrowExceptionResultExt<'s> { + type T; + fn throw(self, scope: &mut v8::HandleScope<'s>) -> Result; +} + +impl<'s, T, E: IntoException<'s>> ThrowExceptionResultExt<'s> for Result { + type T = T; + fn throw(self, scope: &mut v8::HandleScope<'s>) -> Result { + self.or_else(|err| throw(scope, err)) + } +} + +pub(super) trait IntoException<'s> { + fn into_exception(self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::Value>; +} + +impl<'s> IntoException<'s> for v8::Local<'s, v8::Value> { + fn into_exception(self, _scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::Value> { + self + } +} + +#[derive(Copy, Clone)] +pub struct TypeError(pub M); + +impl<'s, M: IntoJsString<'s>> IntoException<'s> for TypeError { + fn into_exception(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<'s> { + fn into_string(self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::String>; +} +impl<'s> IntoJsString<'s> for v8::Local<'s, v8::String> { + fn into_string(self, _scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::String> { + self + } +} +impl<'s> IntoJsString<'s> for String { + fn into_string(self, scope: &mut v8::HandleScope<'s>) -> v8::Local<'s, v8::String> { + v8::String::new(scope, &self).unwrap() + } +} +impl<'s> IntoJsString<'s> for &'static StringConst { + fn into_string(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<'s, F>(f: F) -> v8::ExternalReference<'s> +where + 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 0000000000..d452fbd9d9 --- /dev/null +++ b/crates/core/src/host/v8/wrapper.ts @@ -0,0 +1,301 @@ +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: import('spacetime:sys/v10.0').ArrayType; + constructor(inner: Elem) { + this.#inner = freeze({ type: 'array', elem_ty: convertType(inner) }); + } + get [toInternalType]() { + return this.#inner; + } +} + +type ProductMap = { [s: string]: AlgebraicType }; +class ProductType { + #inner: import('spacetime:sys/v10.0').ProductType; + constructor(map: Map) { + const elements = freeze( + Object.entries(map).map(([k, v]) => freeze({ name: k, algebraic_type: convertType(v) })) + ); + this.#inner = freeze({ type: 'product', elements }); + } + get [toInternalType]() { + return this.#inner; + } +} + +class TypeRef { + #inner: import('spacetime:sys/v10.0').TypeRef; + constructor(ref: number) { + this.#inner = freeze({ type: 'ref', ref }); + } + get [toInternalType]() { + return this.#inner; + } +} + +const primitives = freeze({ + string: freeze({ type: 'string' }), + bool: freeze({ type: 'bool' }), + i8: freeze({ type: 'i8' }), + u8: freeze({ type: 'u8' }), + i16: freeze({ type: 'i16' }), + u16: freeze({ type: 'u16' }), + i32: freeze({ type: 'i32' }), + u32: freeze({ type: 'u32' }), + i64: freeze({ type: 'i64' }), + u64: freeze({ type: 'u64' }), + i128: freeze({ type: 'i128' }), + u128: freeze({ type: 'u128' }), + i256: freeze({ type: 'i256' }), + u256: freeze({ type: 'u256' }), + f32: freeze({ type: 'f32' }), + f64: freeze({ type: 'f64' }), +}); + +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; + } + } else if (ty != null) { + const x = ty[toInternalType]; + if (x) return x; + } + 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 MakeArray = T extends Array ? T : 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({ type: 'product', 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); +}