tsrun 0.1.23

A TypeScript interpreter designed for embedding in applications
Documentation
//! Raw WASM exports for non-browser runtimes (wazero, wasmer, wasmtime).
//!
//! This module provides a WASM-friendly API that exposes the C FFI functions
//! without wasm-bindgen dependencies. The host runtime (Go, etc.) provides
//! platform services through imported functions.
//!
//! # Host Import Contract
//!
//! The host must provide these functions in the `tsrun_host` module:
//!
//! - `host_time_now() -> i64` - Current Unix timestamp in milliseconds
//! - `host_time_start_timer() -> u64` - Start a performance timer
//! - `host_time_elapsed(start: u64) -> u64` - Elapsed milliseconds since timer start
//! - `host_random() -> f64` - Random float in [0, 1)
//! - `host_console_write(level: u32, ptr: u32, len: u32)` - Write console message
//! - `host_console_clear()` - Clear console
//!
//! Console levels: 0=log, 1=info, 2=debug, 3=warn, 4=error
//!
//! # Memory Management
//!
//! Strings are passed by pointer + length. The host can allocate WASM memory via:
//! - `tsrun_alloc(size: u32) -> u32` - Allocate memory
//! - `tsrun_dealloc(ptr: u32, size: u32)` - Free memory
//!
//! # Example Usage (Go with wazero)
//!
//! ```go
//! // Load WASM module
//! module, _ := runtime.Instantiate(ctx, wasmBytes)
//!
//! // Allocate string in WASM memory
//! code := "console.log('hello')"
//! ptr := module.ExportedFunction("tsrun_alloc").Call(ctx, len(code))
//! memory.Write(ptr, []byte(code))
//!
//! // Create context and execute
//! ctxPtr := module.ExportedFunction("tsrun_wasm_new").Call(ctx)
//! module.ExportedFunction("tsrun_prepare").Call(ctx, ctxPtr, ptr, 0)
//! result := module.ExportedFunction("tsrun_run").Call(ctx, ctxPtr)
//! ```

extern crate alloc;

use alloc::boxed::Box;
use core::alloc::Layout;
use core::ffi::c_void;

use crate::platform::{ConsoleLevel, ConsoleProvider, RandomProvider, TimeProvider};
use crate::prelude::Rc;

use crate::ffi::{TsRunContext, console::FfiConsoleProvider};

mod regexp;
pub use regexp::WasmRegExpProvider;

// ============================================================================
// Global Allocator and Panic Handler
// ============================================================================

// Use dlmalloc as the global allocator for WASM builds.
use dlmalloc::GlobalDlmalloc;

#[global_allocator]
static ALLOCATOR: GlobalDlmalloc = GlobalDlmalloc;

/// Panic handler for no_std WASM builds.
/// Aborts execution - the host can detect this via wasm trap.
/// Excluded during tests since the test harness provides its own panic handler.
#[cfg(all(not(feature = "std"), not(test)))]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    core::arch::wasm32::unreachable()
}

// ============================================================================
// Host Imports
// ============================================================================

#[link(wasm_import_module = "tsrun_host")]
unsafe extern "C" {
    /// Get current time as milliseconds since Unix epoch.
    fn host_time_now() -> i64;

    /// Start a timer and return an opaque handle.
    fn host_time_start_timer() -> u64;

    /// Get elapsed milliseconds since the timer was started.
    fn host_time_elapsed(start: u64) -> u64;

    /// Generate a random f64 in the range [0, 1).
    fn host_random() -> f64;

    /// Write a console message.
    /// level: 0=log, 1=info, 2=debug, 3=warn, 4=error
    fn host_console_write(level: u32, ptr: *const u8, len: u32);

    /// Clear the console.
    fn host_console_clear();

    // ========================================================================
    // RegExp Host Imports
    // ========================================================================

    /// Compile a regex pattern with flags.
    /// Returns a handle (>0) on success, 0 on error.
    /// On error, error message pointer/length are written to out params.
    pub fn host_regex_compile(
        pattern_ptr: *const u8,
        pattern_len: u32,
        flags_ptr: *const u8,
        flags_len: u32,
        error_ptr_out: *mut u32,
        error_len_out: *mut u32,
    ) -> u32;

    /// Free a compiled regex handle.
    pub fn host_regex_free(handle: u32);

    /// Test if regex matches input string.
    /// Returns: 1 = match, 0 = no match
    pub fn host_regex_test(handle: u32, input_ptr: *const u8, input_len: u32) -> i32;

    /// Find a match starting at start_pos (byte offset).
    /// Returns: 1 = found, 0 = not found
    /// Out params filled on success:
    /// - match_start, match_end: byte offsets of full match
    /// - captures_ptr: pointer to array of i32 pairs (start, end), -1 = non-participating
    /// - captures_count: number of capture groups (including group 0)
    /// Caller must free captures via host_free_captures.
    pub fn host_regex_exec(
        handle: u32,
        input_ptr: *const u8,
        input_len: u32,
        start_pos: u32,
        match_start_out: *mut u32,
        match_end_out: *mut u32,
        captures_ptr_out: *mut u32,
        captures_count_out: *mut u32,
    ) -> i32;

    /// Free captures array allocated by host_regex_exec.
    pub fn host_free_captures(ptr: u32, count: u32);

    /// Replace matches with replacement string.
    /// global: 0 = first match only, 1 = all matches
    /// Returns result string via out params (ptr, len).
    /// Returns: 1 = success, 0 = error
    /// Caller must free result via host_free_string.
    pub fn host_regex_replace(
        handle: u32,
        input_ptr: *const u8,
        input_len: u32,
        replacement_ptr: *const u8,
        replacement_len: u32,
        global: u32,
        result_ptr_out: *mut u32,
        result_len_out: *mut u32,
    ) -> i32;

    /// Split input by regex matches.
    /// Returns array of string parts via out params:
    /// - parts_ptr: pointer to array of (ptr: u32, len: u32) pairs
    /// - parts_count: number of parts
    /// Returns: 1 = success, 0 = error
    /// Caller must free each part string and the array via host_free_split_result.
    pub fn host_regex_split(
        handle: u32,
        input_ptr: *const u8,
        input_len: u32,
        parts_ptr_out: *mut u32,
        parts_count_out: *mut u32,
    ) -> i32;

    /// Free split result: the parts array and all strings within it.
    pub fn host_free_split_result(parts_ptr: u32, parts_count: u32);

    /// Free a host-allocated string.
    pub fn host_free_string(ptr: u32, len: u32);
}

// ============================================================================
// Platform Providers
// ============================================================================

/// Time provider that delegates to host imports.
pub struct WasmRawTimeProvider;

impl TimeProvider for WasmRawTimeProvider {
    fn now_millis(&self) -> i64 {
        unsafe { host_time_now() }
    }

    fn start_timer(&self) -> u64 {
        unsafe { host_time_start_timer() }
    }

    fn elapsed_millis(&self, start: u64) -> u64 {
        unsafe { host_time_elapsed(start) }
    }
}

/// Random provider that delegates to host imports.
pub struct WasmRawRandomProvider;

impl RandomProvider for WasmRawRandomProvider {
    fn random(&mut self) -> f64 {
        unsafe { host_random() }
    }
}

/// Console provider that delegates to host imports.
pub struct WasmRawConsoleProvider;

impl ConsoleProvider for WasmRawConsoleProvider {
    fn write(&self, level: ConsoleLevel, message: &str) {
        let level_num = match level {
            ConsoleLevel::Log => 0,
            ConsoleLevel::Info => 1,
            ConsoleLevel::Debug => 2,
            ConsoleLevel::Warn => 3,
            ConsoleLevel::Error => 4,
        };
        unsafe {
            host_console_write(level_num, message.as_ptr(), message.len() as u32);
        }
    }

    fn clear(&self) {
        unsafe {
            host_console_clear();
        }
    }
}

// ============================================================================
// Memory Allocator Exports
// ============================================================================

/// Allocate memory in WASM linear memory.
///
/// Returns a pointer to the allocated memory, or 0 on failure.
/// The caller is responsible for calling `tsrun_dealloc` to free the memory.
#[unsafe(no_mangle)]
pub extern "C" fn tsrun_alloc(size: u32) -> u32 {
    if size == 0 {
        return 0;
    }
    // Use 8-byte alignment for struct safety (u64 fields need this)
    let Ok(layout) = Layout::from_size_align(size as usize, 8) else {
        return 0;
    };
    let ptr = unsafe { alloc::alloc::alloc(layout) };
    if ptr.is_null() { 0 } else { ptr as u32 }
}

/// Free memory allocated by `tsrun_alloc`.
#[unsafe(no_mangle)]
pub extern "C" fn tsrun_dealloc(ptr: u32, size: u32) {
    if ptr == 0 || size == 0 {
        return;
    }
    // Must match the alignment used in tsrun_alloc
    let Ok(layout) = Layout::from_size_align(size as usize, 8) else {
        return;
    };
    unsafe {
        alloc::alloc::dealloc(ptr as *mut u8, layout);
    }
}

// ============================================================================
// WASM-Specific Context Creation
// ============================================================================

/// Create a new interpreter context with WASM host platform providers.
///
/// This is similar to `tsrun_new()` but installs providers that delegate
/// to host-provided import functions instead of std library functions.
///
/// Returns a pointer to the context, or NULL on failure.
#[unsafe(no_mangle)]
pub extern "C" fn tsrun_wasm_new() -> *mut TsRunContext {
    let ctx = Box::new(TsRunContext::new());
    let ctx_ptr = Box::into_raw(ctx);

    // Get a mutable reference to set up providers
    let ctx_ref = unsafe { &mut *ctx_ptr };

    // Install WASM platform providers
    ctx_ref
        .interp
        .set_time_provider(Box::new(WasmRawTimeProvider));
    ctx_ref
        .interp
        .set_random_provider(Box::new(WasmRawRandomProvider));

    // For console, we can either use the raw provider or the FFI callback system.
    // Using the raw provider directly for simplicity in WASM context.
    ctx_ref.interp.set_console(Box::new(WasmRawConsoleProvider));

    // Install WASM regex provider that delegates to host's native RegExp
    ctx_ref
        .interp
        .set_regexp_provider(Rc::new(WasmRegExpProvider));

    ctx_ptr
}

/// Alternative: Create context that uses FFI console callback system.
///
/// This allows the host to set a console callback via `tsrun_set_console_callback`,
/// which may be preferred for some use cases.
#[unsafe(no_mangle)]
pub extern "C" fn tsrun_wasm_new_with_callback_console() -> *mut TsRunContext {
    let ctx = Box::new(TsRunContext::new());
    let ctx_ptr = Box::into_raw(ctx);

    let ctx_ref = unsafe { &mut *ctx_ptr };

    // Install WASM platform providers for time and random
    ctx_ref
        .interp
        .set_time_provider(Box::new(WasmRawTimeProvider));
    ctx_ref
        .interp
        .set_random_provider(Box::new(WasmRawRandomProvider));

    // Use FFI console provider that looks up callback from context
    let provider = FfiConsoleProvider::new(ctx_ptr as *mut c_void);
    ctx_ref.interp.set_console(Box::new(provider));

    ctx_ptr
}