tsgo-wasm 0.3.0

typescript-go (tsgo) compiled to WASI p1, with an optional wasmtime runtime
pub const TSGO_WASM_ZSTD: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/tsgo.wasm.zst"));
pub const TSGO_REV: &str = include_str!("../artifacts/tsgo.rev");

pub fn module_bytes() -> std::io::Result<Vec<u8>> {
    zstd::decode_all(TSGO_WASM_ZSTD)
}

#[cfg(feature = "runtime")]
pub use runtime::{TypeScript, TypeScriptConfig, build_cwasm, build_cwasm_zst, precompile};

#[cfg(feature = "runtime")]
mod api;
#[cfg(feature = "runtime")]
pub use api::{ApiSession, Diagnostic};

#[cfg(feature = "runtime")]
mod runtime {
    use std::path::{Path, PathBuf};
    use std::sync::Arc;
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::time::Duration;
    use wasmtime::{
        Config, Engine, InstancePre, Linker, Module, Store, StoreLimits, StoreLimitsBuilder,
    };
    use wasmtime_wasi::p1::{self, WasiP1Ctx};
    use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
    use wasmtime_wasi::{I32Exit, WasiCtxBuilder};

    #[derive(Clone, Debug)]
    pub struct TypeScriptConfig {
        pub signals_based_traps: bool,
        pub memory_limit: Option<usize>,
        pub epoch_tick: Duration,
        pub pipe_capacity: usize,
    }

    impl Default for TypeScriptConfig {
        fn default() -> Self {
            Self {
                signals_based_traps: true,
                memory_limit: None,
                epoch_tick: Duration::from_millis(100),
                pipe_capacity: 1 << 26,
            }
        }
    }

    impl TypeScriptConfig {
        fn wasmtime_config(&self) -> Config {
            let mut config = Config::new();
            config.epoch_interruption(true);
            config.signals_based_traps(self.signals_based_traps);
            config
        }

        fn engine(&self) -> anyhow::Result<Engine> {
            Ok(Engine::new(&self.wasmtime_config())?)
        }

        pub fn load(&self) -> anyhow::Result<TypeScript> {
            let engine = self.engine()?;
            let module = Module::new(&engine, crate::module_bytes()?)?;
            TypeScript::assemble(engine, module, self.clone())
        }

        pub fn load_cached(&self, cache: impl AsRef<Path>) -> anyhow::Result<TypeScript> {
            let cache = cache.as_ref();
            let engine = self.engine()?;
            let module = match unsafe { Module::deserialize_file(&engine, cache) } {
                Ok(module) => module,
                Err(_) => {
                    let module = Module::new(&engine, crate::module_bytes()?)?;
                    std::fs::write(cache, module.serialize()?)?;
                    module
                }
            };
            TypeScript::assemble(engine, module, self.clone())
        }

        pub unsafe fn from_cwasm(&self, cwasm: &[u8]) -> anyhow::Result<TypeScript> {
            let engine = self.engine()?;
            let decompressed;
            let cwasm = if cwasm.starts_with(&[0x28, 0xb5, 0x2f, 0xfd]) {
                decompressed = zstd::decode_all(cwasm)?;
                decompressed.as_slice()
            } else {
                cwasm
            };
            let module = unsafe { Module::deserialize(&engine, cwasm)? };
            TypeScript::assemble(engine, module, self.clone())
        }

        pub fn precompile(&self, target: Option<&str>) -> anyhow::Result<Vec<u8>> {
            let mut config = self.wasmtime_config();
            if let Some(target) = target {
                config.target(target)?;
            }
            let engine = Engine::new(&config)?;
            Ok(engine.precompile_module(&crate::module_bytes()?)?)
        }

        pub fn build_cwasm(&self) -> anyhow::Result<PathBuf> {
            let (out_dir, target) = build_script_env()?;
            let cwasm = self.precompile(target.as_deref())?;
            let path = out_dir.join("tsgo.cwasm");
            std::fs::write(&path, cwasm)?;
            Ok(path)
        }

        pub fn build_cwasm_zst(&self, level: i32) -> anyhow::Result<PathBuf> {
            let (out_dir, target) = build_script_env()?;
            let cwasm = self.precompile(target.as_deref())?;
            let path = out_dir.join("tsgo.cwasm.zst");
            std::fs::write(&path, zstd::encode_all(cwasm.as_slice(), level)?)?;
            Ok(path)
        }
    }

    fn build_script_env() -> anyhow::Result<(PathBuf, Option<String>)> {
        let out_dir = PathBuf::from(std::env::var("OUT_DIR")?);
        let target = std::env::var("TARGET")?;
        let host = std::env::var("HOST")?;
        Ok((out_dir, (target != host).then_some(target)))
    }

    pub fn precompile(target: Option<&str>) -> anyhow::Result<Vec<u8>> {
        TypeScriptConfig::default().precompile(target)
    }

    pub fn build_cwasm() -> anyhow::Result<PathBuf> {
        TypeScriptConfig::default().build_cwasm()
    }

    pub fn build_cwasm_zst(level: i32) -> anyhow::Result<PathBuf> {
        TypeScriptConfig::default().build_cwasm_zst(level)
    }

    pub(crate) struct State {
        pub(crate) wasi: WasiP1Ctx,
        pub(crate) limits: StoreLimits,
    }

    pub struct TypeScript {
        engine: Engine,
        instance_pre: InstancePre<State>,
        config: TypeScriptConfig,
        stop_ticker: Arc<AtomicBool>,
    }

    impl TypeScript {
        pub fn new() -> anyhow::Result<Self> {
            TypeScriptConfig::default().load()
        }

        pub fn with_cache(cache: impl AsRef<Path>) -> anyhow::Result<Self> {
            TypeScriptConfig::default().load_cached(cache)
        }

        pub unsafe fn from_cwasm(cwasm: &[u8]) -> anyhow::Result<Self> {
            unsafe { TypeScriptConfig::default().from_cwasm(cwasm) }
        }

        fn assemble(
            engine: Engine,
            module: Module,
            config: TypeScriptConfig,
        ) -> anyhow::Result<Self> {
            let mut linker: Linker<State> = Linker::new(&engine);
            p1::add_to_linker_sync(&mut linker, |state: &mut State| &mut state.wasi)?;
            let instance_pre = linker.instantiate_pre(&module)?;

            let stop_ticker = Arc::new(AtomicBool::new(false));
            let ticker_engine = engine.clone();
            let ticker_stop = stop_ticker.clone();
            let tick = config.epoch_tick;
            std::thread::spawn(move || {
                while !ticker_stop.load(Ordering::Relaxed) {
                    std::thread::sleep(tick);
                    ticker_engine.increment_epoch();
                }
            });

            Ok(Self {
                engine,
                instance_pre,
                config,
                stop_ticker,
            })
        }

        pub fn check(
            &self,
            files: &[(&str, &str)],
            timeout: Duration,
        ) -> anyhow::Result<Vec<crate::api::Diagnostic>> {
            self.api_session(files, timeout)?.diagnostics()
        }

        pub fn version(&self) -> anyhow::Result<String> {
            let stdout = MemoryOutputPipe::new(self.config.pipe_capacity);
            let mut builder = WasiCtxBuilder::new();
            builder.args(&["tsgo", "--version"]);
            builder.stdout(stdout.clone());
            let state = State {
                wasi: builder.build_p1(),
                limits: StoreLimitsBuilder::new().build(),
            };
            let mut store = Store::new(&self.engine, state);
            store.limiter(|state| &mut state.limits);
            store.set_epoch_deadline(u64::MAX / 2);
            let instance = self.instance_pre.instantiate(&mut store)?;
            let start = instance.get_typed_func::<(), ()>(&mut store, "_start")?;
            if let Err(error) = start.call(&mut store, ()) {
                if error.downcast_ref::<I32Exit>().is_none() {
                    return Err(error.into());
                }
            }
            drop(store);
            Ok(String::from_utf8_lossy(&stdout.contents())
                .trim()
                .to_string())
        }

        pub fn api_session(
            &self,
            files: &[(&str, &str)],
            timeout: Duration,
        ) -> anyhow::Result<crate::api::ApiSession> {
            crate::api::ApiSession::start(
                self.engine.clone(),
                self.instance_pre.clone(),
                self.config.clone(),
                files,
                timeout,
            )
        }
    }

    impl Drop for TypeScript {
        fn drop(&mut self) {
            self.stop_ticker.store(true, Ordering::Relaxed);
        }
    }
}