use std::cell::{Cell, RefCell, RefMut};
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use oxide_core::Handle;
use oxide_core::host::Host;
use oxide_core::vm::module::ModuleTables;
use oxide_core::{MemoryReport, NativeCx, RuntimeOptions};
use super::RuntimeError;
use super::context::Context;
use super::module::ModuleLoader;
use super::ops::View;
use super::property::{
CompleteOrdinaryPropertyDescriptor, OrdinaryPropertyDescriptor, PropertyKey, WellKnownSymbol,
};
use super::value::{CallableRef, ObjectRef, Slot, SymbolRef, Value};
use crate::engine::code::debug::DebugInfoMode;
use crate::engine::value::JsString;
#[derive(Clone, Debug)]
pub struct InterruptHandle {
flag: Arc<AtomicBool>,
}
impl InterruptHandle {
pub fn interrupt(&self) {
self.flag.store(true, Ordering::Relaxed);
}
pub fn clear(&self) {
self.flag.store(false, Ordering::Relaxed);
}
pub fn is_set(&self) -> bool {
self.flag.load(Ordering::Relaxed)
}
}
#[derive(Clone)]
pub struct Runtime {
pub(crate) shared: Rc<Shared>,
}
pub(crate) enum Preloaded {
Compiled {
function: ObjectRef,
tables: ModuleTables,
import_meta: Vec<(String, Value)>,
},
Synthetic(Vec<(String, Value)>),
}
pub(crate) struct Shared {
pub(crate) inner: RefCell<Inner>,
released: RefCell<Vec<Handle>>,
pub(crate) poisoned: Cell<bool>,
interrupt: Arc<AtomicBool>,
pub(crate) loader: RefCell<Option<Box<dyn ModuleLoader>>>,
pub(crate) modules: RefCell<HashMap<String, Preloaded>>,
pub(crate) debug_info: Cell<DebugInfoMode>,
turns: Cell<u32>,
}
impl Shared {
pub(crate) fn release(&self, handle: Handle) {
match self.inner.try_borrow_mut() {
Ok(mut inner) => inner.core.release(handle),
Err(_) => self.released.borrow_mut().push(handle),
}
}
pub(crate) fn drain_released(&self, core: &mut NativeCx<'_>) {
let released = std::mem::take(&mut *self.released.borrow_mut());
for handle in released {
core.release(handle);
}
}
}
pub(crate) struct Inner {
pub(crate) core: oxide_core::Runtime,
}
const NATIVE_RESERVE: usize = if cfg!(debug_assertions) {
256 * 1024
} else {
64 * 1024
};
thread_local! {
static CURRENT: RefCell<Vec<Rc<Shared>>> = const { RefCell::new(Vec::new()) };
}
pub(crate) fn current() -> Option<Rc<Shared>> {
CURRENT.with(|current| current.borrow().last().cloned())
}
pub(crate) fn current_debug_info() -> Option<DebugInfoMode> {
current().map(|shared| shared.debug_info.get())
}
pub(crate) struct Entry<'a> {
pub(crate) inner: RefMut<'a, Inner>,
pub(crate) shared: &'a Rc<Shared>,
}
impl Drop for Entry<'_> {
fn drop(&mut self) {
CURRENT.with(|current| current.borrow_mut().pop());
}
}
impl Entry<'_> {
pub(crate) fn view(&mut self) -> View<'_> {
View::new(self.inner.core.native_cx(), self.shared)
}
}
impl std::ops::Deref for Entry<'_> {
type Target = Inner;
fn deref(&self) -> &Inner {
&self.inner
}
}
impl std::ops::DerefMut for Entry<'_> {
fn deref_mut(&mut self) -> &mut Inner {
&mut self.inner
}
}
pub(crate) fn slot(shared: &Rc<Shared>, handle: Handle, value: oxide_core::Value) -> Rc<Slot> {
Rc::new(Slot {
shared: Rc::downgrade(shared),
handle,
value,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PendingJobOutcome {
NoJob,
Executed,
}
impl PendingJobOutcome {
pub const fn executed(self) -> bool {
matches!(self, Self::Executed)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PromiseState {
Pending,
Fulfilled,
Rejected,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PromiseSnapshot {
state: PromiseState,
result: Value,
}
impl PromiseSnapshot {
pub(crate) fn new(state: PromiseState, result: Value) -> Self {
PromiseSnapshot { state, result }
}
pub fn try_clone(&self) -> Result<Self, RuntimeError> {
Ok(self.clone())
}
pub const fn state(&self) -> PromiseState {
self.state
}
pub const fn result(&self) -> &Value {
&self.result
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum GcPolicy {
#[default]
Automatic,
Manual,
}
pub use oxide_core::heap::gc::GcStats;
pub use oxide_core::host::SystemHost;
impl Default for Runtime {
fn default() -> Self {
Self::new()
}
}
impl Runtime {
pub fn new() -> Self {
Self::with_options(RuntimeOptions::default())
}
pub fn with_options(options: RuntimeOptions) -> Self {
Self::with_host(SystemHost, options)
}
pub fn with_host(host: impl Host + 'static, options: RuntimeOptions) -> Self {
let mut core = oxide_core::Runtime::new(options)
.expect("the initial realm fits any heap cap the options allow");
core.set_host(Box::new(host));
core.set_compiler(super::compile::compile_for_core);
core.set_module_host(oxide_core::vm::module::ModuleHost {
normalize: super::module::normalize_shim,
load: super::module::load_shim,
});
let interrupt = core.native_cx().interrupt_flag();
Runtime {
shared: Rc::new(Shared {
interrupt,
inner: RefCell::new(Inner { core }),
released: RefCell::new(Vec::new()),
poisoned: Cell::new(false),
loader: RefCell::new(None),
modules: RefCell::new(HashMap::new()),
debug_info: Cell::new(DebugInfoMode::Full),
turns: Cell::new(0),
}),
}
}
pub fn new_with_host_services(host: impl Host + 'static) -> Self {
Self::with_host(host, RuntimeOptions::default())
}
pub(crate) fn enter(&self) -> Result<Entry<'_>, RuntimeError> {
if self.shared.poisoned.get() {
return Err(RuntimeError::Poisoned);
}
let mut inner = self
.shared
.inner
.try_borrow_mut()
.map_err(|_| RuntimeError::Reentered)?;
self.shared.drain_released(&mut inner.core.native_cx());
if CURRENT.with(|current| current.borrow().is_empty()) {
inner.core.native_cx().clear_interrupted();
let (available, declared) = crate::engine::compiler::stack_guard::available_stack();
let budget = available
.saturating_sub(available / 8)
.saturating_sub(NATIVE_RESERVE)
.max(available / 2);
let budget = if declared {
budget
} else {
budget.min(oxide_core::vm::NATIVE_STACK_BUDGET)
};
inner.core.set_native_stack_budget(budget);
}
CURRENT.with(|current| current.borrow_mut().push(Rc::clone(&self.shared)));
Ok(Entry {
inner,
shared: &self.shared,
})
}
pub fn context(&self) -> Context {
Context {
runtime: self.clone(),
}
}
pub fn new_context(&self) -> Result<Context, RuntimeError> {
Ok(self.context())
}
pub fn is_same_runtime(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.shared, &other.shared)
}
pub fn interrupt_handle(&self) -> InterruptHandle {
InterruptHandle {
flag: Arc::clone(&self.shared.interrupt),
}
}
pub fn set_can_block(&self, can_block: bool) -> Result<(), RuntimeError> {
self.enter()?.core.set_can_block(can_block);
Ok(())
}
pub fn set_debug_info_mode(&self, mode: DebugInfoMode) -> Result<(), RuntimeError> {
self.shared.debug_info.set(mode);
Ok(())
}
pub fn debug_info_mode(&self) -> Result<DebugInfoMode, RuntimeError> {
Ok(self.shared.debug_info.get())
}
pub fn set_module_loader(
&self,
loader: impl ModuleLoader + 'static,
) -> Result<(), RuntimeError> {
let mut slot = self
.shared
.loader
.try_borrow_mut()
.map_err(|_| RuntimeError::Reentered)?;
*slot = Some(Box::new(loader));
Ok(())
}
pub fn is_job_pending(&self) -> Result<bool, RuntimeError> {
Ok(self.enter()?.core.has_pending_jobs())
}
pub fn run_pending_job(&self) -> Result<PendingJobOutcome, RuntimeError> {
if self.shared.turns.get() > 0 {
return Err(RuntimeError::Invariant(
"a job was dispatched inside an execution turn",
));
}
let mut entry = self.enter()?;
let result = entry.inner.core.run_pending_job();
match entry.view().complete_with(result)? {
true => Ok(PendingJobOutcome::Executed),
false => Ok(PendingJobOutcome::NoJob),
}
}
pub fn execute_pending_job(&self) -> Result<PendingJobOutcome, RuntimeError> {
self.run_pending_job()
}
pub fn run_jobs(&self) -> Result<(), RuntimeError> {
while self.run_pending_job()?.executed() {}
Ok(())
}
pub fn with_execution_turn<T>(
&self,
body: impl FnOnce() -> Result<T, RuntimeError>,
) -> Result<T, RuntimeError> {
self.shared.turns.set(self.shared.turns.get() + 1);
let result = body();
let turns = self.shared.turns.get() - 1;
self.shared.turns.set(turns);
if turns == 0 {
self.enter()?.core.clear_kept_objects();
}
result
}
pub fn promise_snapshot(
&self,
promise: &ObjectRef,
) -> Result<Option<PromiseSnapshot>, RuntimeError> {
self.enter()?.view().promise_snapshot(promise)
}
pub fn set_track_rejections(&self, track: bool) -> Result<(), RuntimeError> {
self.enter()?.core.set_track_rejections(track);
Ok(())
}
pub fn take_unhandled_rejections(&self) -> Result<Vec<(ObjectRef, Value)>, RuntimeError> {
let mut entry = self.enter()?;
let promises = entry.inner.core.unhandled_rejections().to_vec();
let mut rejections = Vec::with_capacity(promises.len());
let mut view = entry.view();
for promise in promises {
let reason = view
.core
.promise_state(promise)
.map_or(oxide_core::Value::UNDEFINED, |(_, reason)| reason);
let promise = view.lift(promise)?;
let reason = view.lift(reason)?;
if let Value::Object(promise) = promise {
rejections.push((promise, reason));
}
}
drop(view);
entry.inner.core.clear_unhandled_rejections();
Ok(rejections)
}
pub fn run_gc(&self) -> Result<GcStats, RuntimeError> {
let mut entry = self.enter()?;
entry.core.run_gc();
Ok(entry.core.heap().gc().stats)
}
pub fn gc_stats(&self) -> Result<GcStats, RuntimeError> {
Ok(self.enter()?.core.heap().gc().stats)
}
pub fn memory_report(&self) -> Result<MemoryReport, RuntimeError> {
Ok(self.enter()?.core.memory_report())
}
pub fn set_gc_policy(&self, policy: GcPolicy) -> Result<(), RuntimeError> {
self.enter()?
.core
.heap_mut()
.set_gc_manual(policy == GcPolicy::Manual);
Ok(())
}
pub fn gc_policy(&self) -> Result<GcPolicy, RuntimeError> {
Ok(if self.enter()?.core.heap().gc().is_manual() {
GcPolicy::Manual
} else {
GcPolicy::Automatic
})
}
pub fn set_gc_stress(&self, interval: Option<u32>) -> Result<(), RuntimeError> {
let mut entry = self.enter()?;
#[cfg(feature = "gc-stress")]
{
let gc = entry.core.heap_mut().gc_mut();
gc.stress = interval.is_some_and(|interval| interval > 0);
gc.stress_interval = interval.unwrap_or(1).max(1);
Ok(())
}
#[cfg(not(feature = "gc-stress"))]
{
let _ = &mut entry;
match interval {
Some(_) => Err(RuntimeError::Unsupported(
"stress collections in this build",
)),
None => Ok(()),
}
}
}
pub const GC_STRESS_AVAILABLE: bool = cfg!(feature = "gc-stress");
pub const IC_STATS_AVAILABLE: bool = cfg!(feature = "ic-stats");
pub fn set_record_gc_pauses(&self, record: bool) -> Result<(), RuntimeError> {
self.enter()?.core.heap_mut().gc_mut().record_pauses = record;
Ok(())
}
pub fn gc_report(&self) -> Result<String, RuntimeError> {
let entry = self.enter()?;
let gc = entry.core.heap().gc();
let stats = gc.stats;
let mut pauses = gc.pauses.clone();
pauses.sort();
let percentile = |p: usize| {
pauses
.get((pauses.len() * p / 100).min(pauses.len().saturating_sub(1)))
.map_or(0.0, |pause| pause.as_secs_f64() * 1e3)
};
Ok(format!(
"gc: {} collections, total pause {:.1} ms, p50 {:.3} ms, p99 {:.3} ms, max {:.3} ms, \
live {} KiB, reserved {} KiB\n",
stats.collections,
stats.total_pause.as_secs_f64() * 1e3,
percentile(50),
percentile(99),
stats.max_pause.as_secs_f64() * 1e3,
stats.live_bytes / 1024,
entry.core.heap().bytes() / 1024,
))
}
pub fn ic_report(&self, worst: usize) -> Result<Option<String>, RuntimeError> {
Ok(self.enter()?.core.ic_report(worst))
}
pub fn disassemble(&self, source: &[u8], filename: &str) -> Result<String, RuntimeError> {
let mut entry = self.enter()?;
let debug_info = self.shared.debug_info.get();
let lowered = {
let mut compiler = entry.inner.core.compiler();
super::compile::compile_script(&mut compiler, source, filename, debug_info)
};
let lowered = match lowered {
Ok(lowered) => lowered,
Err(super::compile::ScriptCompileError::Front(error)) => {
return Err(RuntimeError::Compile(error));
}
Err(super::compile::ScriptCompileError::Lower(error)) => {
return Err(RuntimeError::Internal(error.to_string()));
}
};
let mut text = String::new();
for unit in entry.core.units().tree(lowered.root) {
text.push_str(&entry.core.disassemble(unit));
text.push('\n');
}
Ok(text)
}
pub fn intern_property_key(&self, text: &str) -> Result<PropertyKey, RuntimeError> {
Ok(PropertyKey::from_js_string(JsString::try_from_utf8(text)?))
}
pub fn intern_property_key_js_string(
&self,
text: &JsString,
) -> Result<PropertyKey, RuntimeError> {
Ok(PropertyKey::from_js_string(text.clone()))
}
pub fn property_key_to_js_string(&self, key: &PropertyKey) -> Result<JsString, RuntimeError> {
match key.as_symbol() {
Some(symbol) => Ok(self
.symbol_description(symbol)?
.unwrap_or_else(|| JsString::from_static(""))),
None => key
.to_js_string()
.ok_or(RuntimeError::Invariant("a string key")),
}
}
pub fn new_symbol(&self, description: Option<JsString>) -> Result<SymbolRef, RuntimeError> {
self.enter()?.view().new_symbol(description)
}
pub fn symbol_description(&self, symbol: &SymbolRef) -> Result<Option<JsString>, RuntimeError> {
self.enter()?.view().symbol_description(symbol)
}
pub fn well_known_symbol(&self, symbol: WellKnownSymbol) -> Result<SymbolRef, RuntimeError> {
self.enter()?.view().well_known_symbol(symbol)
}
pub fn get_prototype_of(&self, object: &ObjectRef) -> Result<Option<ObjectRef>, RuntimeError> {
self.enter()?.view().get_prototype_of(object)
}
pub fn set_prototype_of(
&self,
object: &ObjectRef,
prototype: Option<&ObjectRef>,
) -> Result<bool, RuntimeError> {
self.enter()?.view().set_prototype_of(object, prototype)
}
pub fn as_callable(&self, object: &ObjectRef) -> Result<Option<CallableRef>, RuntimeError> {
self.enter()?.view().as_callable(object)
}
pub fn is_constructor(&self, object: &ObjectRef) -> Result<bool, RuntimeError> {
self.enter()?.view().is_constructor(object)
}
pub fn is_array_object(&self, object: &ObjectRef) -> Result<bool, RuntimeError> {
self.enter()?.view().is_array_object(object)
}
pub fn is_error_object(&self, object: &ObjectRef) -> Result<bool, RuntimeError> {
self.enter()?.view().is_error_object(object)
}
pub fn define_own_property(
&self,
object: &ObjectRef,
key: &PropertyKey,
descriptor: &OrdinaryPropertyDescriptor,
) -> Result<bool, RuntimeError> {
self.context().define_own_property(object, key, descriptor)
}
pub fn get_own_property(
&self,
object: &ObjectRef,
key: &PropertyKey,
) -> Result<Option<CompleteOrdinaryPropertyDescriptor>, RuntimeError> {
self.context().get_own_property(object, key)
}
pub fn delete_property(
&self,
object: &ObjectRef,
key: &PropertyKey,
) -> Result<bool, RuntimeError> {
self.enter()?.view().delete_property(object, key)
}
pub fn has_property(
&self,
object: &ObjectRef,
key: &PropertyKey,
) -> Result<bool, RuntimeError> {
self.enter()?.view().has_property(object, key)
}
pub fn has_own_property(
&self,
object: &ObjectRef,
key: &PropertyKey,
) -> Result<bool, RuntimeError> {
self.enter()?.view().has_own_property(object, key)
}
pub fn own_property_keys(&self, object: &ObjectRef) -> Result<Vec<PropertyKey>, RuntimeError> {
self.enter()?.view().own_property_keys(object)
}
pub fn type_of(&self, value: &Value) -> Result<String, RuntimeError> {
self.enter()?.view().type_of(value)
}
pub fn print_value(&self, value: &Value) -> Result<String, RuntimeError> {
self.enter()?.view().print_value(value)
}
pub fn qjs_print_value_bytes(&self, value: &Value) -> Result<Vec<u8>, RuntimeError> {
self.enter()?.view().qjs_print_value_bytes(value)
}
}