Skip to main content

boa_engine/
script.rs

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