1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
//! Boa's implementation of ECMAScript's Scripts.
//!
//! This module contains the [`Script`] type, which represents a [**Script Record**][script].
//!
//! More information:
//! - [ECMAScript reference][spec]
//!
//! [spec]: https://tc39.es/ecma262/#sec-scripts
//! [script]: https://tc39.es/ecma262/#sec-script-records
use boa_gc::{Finalize, Gc, GcRefCell, Trace};
use boa_parser::{source::ReadChar, Parser, Source};
use boa_profiler::Profiler;
use rustc_hash::FxHashMap;
use crate::{
bytecompiler::ByteCompiler,
js_string,
realm::Realm,
vm::{ActiveRunnable, CallFrame, CallFrameFlags, CodeBlock},
Context, HostDefined, JsResult, JsString, JsValue, Module,
};
/// ECMAScript's [**Script Record**][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-script-records
#[derive(Clone, Trace, Finalize)]
pub struct Script {
inner: Gc<Inner>,
}
impl std::fmt::Debug for Script {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Script")
.field("realm", &self.inner.realm.addr())
.field("code", &self.inner.source)
.field("loaded_modules", &self.inner.loaded_modules)
.finish()
}
}
#[derive(Trace, Finalize)]
struct Inner {
realm: Realm,
#[unsafe_ignore_trace]
source: boa_ast::Script,
codeblock: GcRefCell<Option<Gc<CodeBlock>>>,
loaded_modules: GcRefCell<FxHashMap<JsString, Module>>,
host_defined: HostDefined,
}
impl Script {
/// Gets the realm of this script.
#[must_use]
pub fn realm(&self) -> &Realm {
&self.inner.realm
}
/// Returns the [`ECMAScript specification`][spec] defined [`\[\[HostDefined\]\]`][`HostDefined`] field of the [`Module`].
///
/// [spec]: https://tc39.es/ecma262/#script-record
#[must_use]
pub fn host_defined(&self) -> &HostDefined {
&self.inner.host_defined
}
/// Gets the loaded modules of this script.
pub(crate) fn loaded_modules(&self) -> &GcRefCell<FxHashMap<JsString, Module>> {
&self.inner.loaded_modules
}
/// Abstract operation [`ParseScript ( sourceText, realm, hostDefined )`][spec].
///
/// Parses the provided `src` as an ECMAScript script, returning an error if parsing fails.
///
/// [spec]: https://tc39.es/ecma262/#sec-parse-script
pub fn parse<R: ReadChar>(
src: Source<'_, R>,
realm: Option<Realm>,
context: &mut Context,
) -> JsResult<Self> {
let _timer = Profiler::global().start_event("Script parsing", "Main");
let mut parser = Parser::new(src);
parser.set_identifier(context.next_parser_identifier());
if context.is_strict() {
parser.set_strict();
}
let mut code = parser.parse_script(context.interner_mut())?;
if !context.optimizer_options().is_empty() {
context.optimize_statement_list(code.statements_mut());
}
Ok(Self {
inner: Gc::new(Inner {
realm: realm.unwrap_or_else(|| context.realm().clone()),
source: code,
codeblock: GcRefCell::default(),
loaded_modules: GcRefCell::default(),
host_defined: HostDefined::default(),
}),
})
}
/// Compiles the codeblock of this script.
///
/// This is a no-op if this has been called previously.
pub fn codeblock(&self, context: &mut Context) -> JsResult<Gc<CodeBlock>> {
let mut codeblock = self.inner.codeblock.borrow_mut();
if let Some(codeblock) = &*codeblock {
return Ok(codeblock.clone());
};
let _timer = Profiler::global().start_event("Script compilation", "Main");
let mut compiler = ByteCompiler::new(
js_string!("<main>"),
self.inner.source.strict(),
false,
self.inner.realm.environment().compile_env(),
self.inner.realm.environment().compile_env(),
context,
);
// TODO: move to `Script::evaluate` to make this operation infallible.
compiler.global_declaration_instantiation(
&self.inner.source,
&self.inner.realm.environment().compile_env(),
)?;
compiler.compile_statement_list(self.inner.source.statements(), true, false);
let cb = Gc::new(compiler.finish());
*codeblock = Some(cb.clone());
Ok(cb)
}
/// Evaluates this script and returns its result.
///
/// Note that this won't run any scheduled promise jobs; you need to call [`Context::run_jobs`]
/// on the context or [`JobQueue::run_jobs`] on the provided queue to run them.
///
/// [`JobQueue::run_jobs`]: crate::job::JobQueue::run_jobs
pub fn evaluate(&self, context: &mut Context) -> JsResult<JsValue> {
let _timer = Profiler::global().start_event("Execution", "Main");
self.prepare_run(context)?;
let record = context.run();
context.vm.pop_frame();
context.clear_kept_objects();
record.consume()
}
/// Evaluates this script and returns its result, periodically yielding to the executor
/// in order to avoid blocking the current thread.
///
/// This uses an implementation defined amount of "clock cycles" that need to pass before
/// execution is suspended. See [`Script::evaluate_async_with_budget`] if you want to also
/// customize this parameter.
#[allow(clippy::future_not_send)]
pub async fn evaluate_async(&self, context: &mut Context) -> JsResult<JsValue> {
self.evaluate_async_with_budget(context, 256).await
}
/// Evaluates this script and returns its result, yielding to the executor each time `budget`
/// number of "clock cycles" pass.
///
/// Note that "clock cycle" is in quotation marks because we can't determine exactly how many
/// CPU clock cycles a VM instruction will take, but all instructions have a "cost" associated
/// with them that depends on their individual complexity. We'd recommend benchmarking with
/// different budget sizes in order to find the ideal yielding time for your application.
#[allow(clippy::future_not_send)]
pub async fn evaluate_async_with_budget(
&self,
context: &mut Context,
budget: u32,
) -> JsResult<JsValue> {
let _timer = Profiler::global().start_event("Async Execution", "Main");
self.prepare_run(context)?;
let record = context.run_async_with_budget(budget).await;
context.vm.pop_frame();
context.clear_kept_objects();
record.consume()
}
fn prepare_run(&self, context: &mut Context) -> JsResult<()> {
let codeblock = self.codeblock(context)?;
let env_fp = context.vm.environments.len() as u32;
context.vm.push_frame_with_stack(
CallFrame::new(
codeblock,
Some(ActiveRunnable::Script(self.clone())),
context.vm.environments.clone(),
self.inner.realm.clone(),
)
.with_env_fp(env_fp)
.with_flags(CallFrameFlags::EXIT_EARLY),
JsValue::undefined(),
JsValue::null(),
);
// TODO: Here should be https://tc39.es/ecma262/#sec-globaldeclarationinstantiation
self.realm().resize_global_env();
Ok(())
}
}