Skip to main content

oxide_core/vm/
compile.rs

1//! Code compiled at run time: `eval`, the `Function` constructors,
2//! `$262.evalScript` and `Function.prototype.toString`'s source text, through
3//! the compile host the embedder installs.
4//!
5//! The core has no front end. The embedder installs a [`CompileFn`] that
6//! compiles and lowers source into a [`Compiler`], the part of the runtime a
7//! lowering needs (the heap, the code store, the realm), which the natives
8//! can lend while the loop holds the code store: units compiled at run time
9//! are published into the heap, which frees them once unreachable.
10//!
11//! A compilation keeps what it makes alive until it ends: a unit published
12//! but not yet instantiated is pinned, and the atoms it interns (keys, names,
13//! string constants) are held. Before the program runs, its atoms are pinned
14//! for good instead.
15
16use super::native::CallSite;
17use super::{Cx, Throw, error, ops};
18use crate::ErrorKind;
19use crate::bytecode::{CodeStore, CodeUnit, CodeUnitId, Units, ValidateError, validate};
20use crate::heap::hash::FxHashMap;
21use crate::heap::{AtomMode, BytecodeTemplate, Heap, HeapError};
22use crate::runtime::{Intrinsic, Realm};
23use crate::value::{ObjectId, Value};
24
25/// The code store a compilation publishes into: owned before the program
26/// runs (units go in the static store), shared while it runs (units go in
27/// the heap).
28enum Store<'a> {
29    Owned(&'a mut CodeStore),
30    Shared(&'a CodeStore),
31}
32
33/// The runtime's parts a compilation uses. Dropping it ends the
34/// compilation: its pinned units and held atoms are released (a
35/// [`Compiler::reborrow`] ends nothing).
36pub struct Compiler<'a> {
37    heap: &'a mut Heap,
38    code: Store<'a>,
39    realm: &'a Realm,
40    /// The atom mode before the compilation, restored after it.
41    mode: AtomMode,
42    /// Where the compilation's pinned units and held atoms start.
43    pins: usize,
44    held: usize,
45    /// Whether dropping it ends the compilation (not a reborrow's).
46    ends: bool,
47}
48
49impl Drop for Compiler<'_> {
50    fn drop(&mut self) {
51        if !self.ends {
52            return;
53        }
54        self.heap.set_atom_mode(self.mode);
55        self.heap.code.unpin(self.pins);
56        self.heap.release_held_atoms(self.held);
57    }
58}
59
60impl<'a> Compiler<'a> {
61    fn with(heap: &'a mut Heap, code: Store<'a>, realm: &'a Realm, mode: AtomMode) -> Self {
62        let pins = heap.code.pin_mark();
63        let held = heap.held_atoms_mark();
64        let mode = heap.set_atom_mode(mode);
65        Compiler {
66            heap,
67            code,
68            realm,
69            mode,
70            pins,
71            held,
72            ends: true,
73        }
74    }
75
76    pub(crate) fn new(heap: &'a mut Heap, code: &'a CodeStore, realm: &'a Realm) -> Self {
77        Compiler::with(heap, Store::Shared(code), realm, AtomMode::Hold)
78    }
79
80    pub(crate) fn owned(heap: &'a mut Heap, code: &'a mut CodeStore, realm: &'a Realm) -> Self {
81        Compiler::with(heap, Store::Owned(code), realm, AtomMode::Pin)
82    }
83
84    /// The same parts for a shorter borrow (a host callback's view).
85    pub fn reborrow(&mut self) -> Compiler<'_> {
86        let code = match &mut self.code {
87            Store::Owned(code) => Store::Owned(code),
88            Store::Shared(code) => Store::Shared(code),
89        };
90        Compiler {
91            heap: self.heap,
92            code,
93            realm: self.realm,
94            mode: self.mode,
95            pins: self.pins,
96            held: self.held,
97            ends: false,
98        }
99    }
100
101    /// [`NativeCx::new_host_function`](crate::NativeCx::new_host_function).
102    pub fn new_host_function(
103        &mut self,
104        name: &str,
105        length: u32,
106        constructor: bool,
107        data: &[Value],
108        body: std::rc::Rc<super::native::HostFn>,
109    ) -> Result<ObjectId, HeapError> {
110        crate::embed::host_function(self.heap, self.realm, name, length, constructor, data, body)
111    }
112
113    /// [`NativeCx::new_host_object`](crate::NativeCx::new_host_object).
114    pub fn new_host_object(
115        &mut self,
116        prototype: Option<ObjectId>,
117        data: std::rc::Rc<dyn std::any::Any>,
118    ) -> Result<ObjectId, HeapError> {
119        crate::embed::host_object(self.heap, prototype, data)
120    }
121
122    /// Holds `value` for the embedder until released (the heap's handle
123    /// table).
124    pub fn hold(&mut self, value: Value) -> crate::Handle {
125        self.heap.handles().create(value)
126    }
127
128    /// A new error object of `kind` with `message` and an empty `stack`: an
129    /// error a host callback raises, with no JavaScript frame, as QuickJS's.
130    pub fn new_error(&mut self, kind: ErrorKind, message: &str) -> Result<Value, HeapError> {
131        let prototype = self.realm.intrinsic(kind.prototype());
132        let message = self.heap.alloc_string(message)?;
133        let mark = self.heap.roots().mark();
134        self.heap.roots().push(Value::string(message));
135        let result = (|| {
136            let object = self
137                .heap
138                .alloc_object(crate::ObjectKind::Ordinary, Some(prototype), 1)?;
139            self.heap
140                .set_object_flag(object, crate::heap::object::flags::ERROR);
141            let key = crate::runtime::Names::key(self.realm.names.message);
142            self.heap.add_property(
143                object,
144                key,
145                crate::PropertyFlags::data(true, false, true),
146                Value::string(message),
147            )?;
148            let empty = self.heap.intern("")?;
149            let stack = crate::runtime::Names::key(self.realm.names.stack);
150            self.heap.add_property(
151                object,
152                stack,
153                crate::PropertyFlags::data(true, false, true),
154                Value::string(empty),
155            )?;
156            Ok(Value::object(object))
157        })();
158        self.heap.roots().truncate(mark);
159        result
160    }
161
162    /// [`Compiler::new_error`] located at `file:line:column` (a module's
163    /// compile error): `fileName`, `lineNumber`, `columnNumber`, and a
164    /// `stack` of the location alone.
165    pub fn new_located_error(
166        &mut self,
167        kind: ErrorKind,
168        message: &str,
169        file: &str,
170        line: u32,
171        column: u32,
172    ) -> Result<Value, HeapError> {
173        let error = self.new_error(kind, message)?;
174        let object = error.as_object().expect("an error object");
175        let names = self.realm.names;
176        let mark = self.heap.roots().mark();
177        self.heap.roots().push(error);
178        let result = (|| {
179            let flags = crate::PropertyFlags::data(true, false, true);
180            let stack_key = crate::runtime::Names::key(names.stack);
181            // `fileName`, `lineNumber` and `columnNumber` precede `stack`,
182            // as QuickJS's `build_backtrace` defines them: the stack is
183            // re-added after them.
184            self.heap.delete_property(object, stack_key)?;
185            let file_text = self.heap.alloc_string(file)?;
186            self.heap.add_property(
187                object,
188                crate::runtime::Names::key(names.file_name),
189                flags,
190                Value::string(file_text),
191            )?;
192            self.heap.add_property(
193                object,
194                crate::runtime::Names::key(names.line_number),
195                flags,
196                Value::int32(line as i32),
197            )?;
198            self.heap.add_property(
199                object,
200                crate::runtime::Names::key(names.column_number),
201                flags,
202                Value::int32(column as i32),
203            )?;
204            let stack = self
205                .heap
206                .alloc_string(&format!("    at {file}:{line}:{column}\n"))?;
207            self.heap
208                .add_property(object, stack_key, flags, Value::string(stack))?;
209            Ok(error)
210        })();
211        self.heap.roots().truncate(mark);
212        result
213    }
214
215    pub fn heap(&self) -> &Heap {
216        self.heap
217    }
218
219    pub fn heap_mut(&mut self) -> &mut Heap {
220        self.heap
221    }
222
223    /// Every published unit, static and dynamic.
224    pub fn units(&self) -> Units<'_> {
225        let code = match &self.code {
226            Store::Owned(code) => &**code,
227            Store::Shared(code) => *code,
228        };
229        Units {
230            code,
231            dynamic: &self.heap.code,
232        }
233    }
234
235    pub fn intrinsic(&self, intrinsic: Intrinsic) -> ObjectId {
236        self.realm.intrinsic(intrinsic)
237    }
238
239    /// Validates `unit`, reserves its cache entries and publishes it: into
240    /// the code store before the program runs (its key atoms pinned), into
241    /// the heap (pinned until the compilation ends, its bytes charged to the
242    /// collection trigger) while it runs. Its children must be published
243    /// first.
244    pub fn publish(&mut self, mut unit: CodeUnit) -> Result<CodeUnitId, ValidateError> {
245        validate::validate(&unit, &|id| self.units().lookup(id))?;
246        self.heap.ics.reserve(&mut unit);
247        match &mut self.code {
248            // A static unit lives as long as the runtime, and so do its keys,
249            // wherever they were interned.
250            Store::Owned(code) => {
251                // Pinning is idempotent, so a key used twice needs no dedup.
252                for atom in unit.key_atom_uses() {
253                    self.heap.pin_atom(atom);
254                }
255                Ok(code.push(unit))
256            }
257            Store::Shared(_) => {
258                let (id, bytes) = self.heap.code.publish(unit);
259                self.heap.gc.allocated += bytes;
260                Ok(id)
261            }
262        }
263    }
264
265    /// A function object for the published unit `root`, with a bytecode
266    /// record for it and every unit below it. `constants` holds one table per
267    /// unit, in the pre-order of [`Units::tree`].
268    pub fn instantiate(
269        &mut self,
270        root: CodeUnitId,
271        constants: &[&[Value]],
272    ) -> Result<ObjectId, HeapError> {
273        let code = match &self.code {
274            Store::Owned(code) => &**code,
275            Store::Shared(code) => *code,
276        };
277        let code = Units {
278            code,
279            dynamic: &self.heap.code,
280        };
281        let order = code.tree(root);
282        assert_eq!(order.len(), constants.len(), "one constant table per unit");
283        let position: FxHashMap<CodeUnitId, u32> = order
284            .iter()
285            .enumerate()
286            .map(|(index, &unit)| (unit, index as u32))
287            .collect();
288        let children: Vec<Vec<u32>> = order
289            .iter()
290            .map(|&id| {
291                let unit = code.get(id);
292                unit.children.iter().map(|child| position[child]).collect()
293            })
294            .collect();
295        let tree: Vec<BytecodeTemplate<'_>> = order
296            .iter()
297            .zip(constants)
298            .zip(&children)
299            .map(|((&unit, &constants), children)| {
300                assert_eq!(
301                    constants.len(),
302                    code.get(unit).constant_count as usize,
303                    "constant table length"
304                );
305                BytecodeTemplate {
306                    unit,
307                    constants,
308                    children,
309                }
310            })
311            .collect();
312        let prototype = self.realm.intrinsic(Intrinsic::FunctionPrototype);
313        self.heap
314            .alloc_bytecode_function(Some(prototype), None, &tree)
315    }
316}
317
318impl Compiler<'_> {
319    /// A tagged template's frozen site object.
320    pub fn template_object(
321        &mut self,
322        cooked: &[Value],
323        raw: &[Value],
324    ) -> Result<ObjectId, HeapError> {
325        use crate::heap::object::flags;
326        let prototype = self.intrinsic(Intrinsic::ArrayPrototype);
327        let names = self.realm.names;
328        let heap = &mut *self.heap;
329        let mark = heap.roots().mark();
330        // An atom interned outside a compilation is weak.
331        for &value in cooked.iter().chain(raw) {
332            heap.roots().push(value);
333        }
334        let result = (|| {
335            let raw_array = heap.alloc_array(Some(prototype), raw.len() as u32)?;
336            heap.roots().push(Value::object(raw_array));
337            let array = heap.alloc_array(Some(prototype), cooked.len() as u32)?;
338            heap.roots().push(Value::object(array));
339            for (target, values) in [(raw_array, raw), (array, cooked)] {
340                for (index, &value) in values.iter().enumerate() {
341                    let key =
342                        crate::PropertyKey::index(index as u32).ok_or(HeapError::OutOfMemory)?;
343                    heap.add_property(
344                        target,
345                        key,
346                        crate::PropertyFlags::data(false, true, false),
347                        value,
348                    )?;
349                }
350                heap.set_array_length(target, values.len() as u32);
351            }
352            let raw_key = crate::runtime::Names::key(names.raw);
353            heap.add_property(
354                array,
355                raw_key,
356                crate::PropertyFlags::data(false, false, false),
357                Value::object(raw_array),
358            )?;
359            for target in [raw_array, array] {
360                heap.set_object_flag(target, flags::SPARSE | flags::LENGTH_READONLY);
361                heap.clear_object_flag(target, flags::EXTENSIBLE);
362            }
363            Ok(array)
364        })();
365        heap.roots().truncate(mark);
366        result
367    }
368}
369
370/// What to compile.
371#[derive(Clone, Copy, Debug, PartialEq, Eq)]
372pub enum CompileKind {
373    /// A script (`$262.evalScript`): its declarations are global.
374    Script,
375    /// Indirect eval code (`eval`, and the `Function` constructors' source):
376    /// the root returns the completion value.
377    IndirectEval,
378}
379
380/// A compilation request.
381#[derive(Clone, Copy, Debug)]
382pub struct CompileRequest<'s> {
383    pub source: &'s [u16],
384    pub kind: CompileKind,
385    pub filename: &'s str,
386}
387
388/// A failed compilation: the error to throw.
389#[derive(Clone, Debug)]
390pub struct CompileError {
391    pub kind: ErrorKind,
392    pub message: String,
393    /// A value the embedder threw instead (a module loader's own error
394    /// object): thrown as is, `kind` and `message` unused. It must stay
395    /// rooted until the core throws it (a handle the embedder releases
396    /// after the entry).
397    pub thrown: Option<Value>,
398    /// Where in which source the error is (a syntax error's file, line and
399    /// column, one-based): the error's `stack` starts with it, as
400    /// QuickJS's does.
401    pub location: Option<(Box<str>, u32, u32)>,
402}
403
404impl CompileError {
405    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
406        CompileError {
407            kind,
408            message: message.into(),
409            thrown: None,
410            location: None,
411        }
412    }
413
414    /// The error located at `line` and `column` of `file`.
415    pub fn at(mut self, file: &str, line: u32, column: u32) -> Self {
416        self.location = Some((file.into(), line, column));
417        self
418    }
419
420    pub fn thrown(value: Value) -> Self {
421        CompileError {
422            kind: ErrorKind::Internal,
423            message: String::new(),
424            thrown: Some(value),
425            location: None,
426        }
427    }
428}
429
430/// Throws a compile failure.
431pub(crate) fn throw_failure(cx: &mut Cx<'_>, failure: CompileError) -> Throw {
432    match (failure.thrown, failure.location) {
433        (Some(value), _) => Throw(value),
434        (None, Some((file, line, column))) => {
435            error::throw_located(cx, failure.kind, &failure.message, &file, line, column)
436        }
437        (None, None) => error::throw(cx, failure.kind, &failure.message),
438    }
439}
440
441/// The embedder's compiler: the root function of the compiled code, to be
442/// called with the global object as `this`.
443pub type CompileFn = fn(&mut Compiler<'_>, &CompileRequest<'_>) -> Result<ObjectId, CompileError>;
444
445/// Compiles `source` through the host, throwing its error.
446pub(crate) fn compile(
447    cx: &mut Cx<'_>,
448    source: &[u16],
449    kind: CompileKind,
450) -> Result<ObjectId, Throw> {
451    compile_named(cx, source, kind, "<input>")
452}
453
454/// [`compile`] with the filename the code's positions name.
455pub(crate) fn compile_named(
456    cx: &mut Cx<'_>,
457    source: &[u16],
458    kind: CompileKind,
459    filename: &str,
460) -> Result<ObjectId, Throw> {
461    let Some(compile) = cx.host.compile else {
462        return Err(super::object::unsupported(cx, "eval"));
463    };
464    let request = CompileRequest {
465        source,
466        kind,
467        filename,
468    };
469    let result = compile(
470        &mut Compiler::new(cx.heap, cx.static_code, cx.realm),
471        &request,
472    );
473    match result {
474        Ok(function) => Ok(function),
475        Err(failure) => Err(throw_failure(cx, failure)),
476    }
477}
478
479/// Compiles and runs `source` on the global object.
480fn run(cx: &mut Cx<'_>, source: &[u16], kind: CompileKind) -> Result<Value, Throw> {
481    let function = compile(cx, source, kind)?;
482    let mark = cx.heap.roots().mark();
483    cx.heap.roots().push(Value::object(function));
484    let global = Value::object(cx.realm.global());
485    let result = super::call(cx, Value::object(function), global, &[]);
486    cx.heap.roots().truncate(mark);
487    result
488}
489
490/// `%eval%(x)`: a direct eval runs as an indirect one.
491pub(crate) fn eval(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
492    let source = site.arg(cx, 0);
493    let Some(string) = source.as_string() else {
494        return Ok(source);
495    };
496    let units = ops::units(cx.heap, string);
497    run(cx, &units, CompileKind::IndirectEval)
498}
499
500/// Which `Function` constructor runs.
501#[derive(Clone, Copy, Debug, PartialEq, Eq)]
502pub(crate) enum FunctionKind {
503    Normal,
504    Generator,
505    Async,
506    AsyncGenerator,
507}
508
509/// `CreateDynamicFunction`, as QuickJS does it: the parameters and body in
510/// `(function anonymous(…\n) {\n…\n})`, evaluated as indirect eval code.
511pub(crate) fn function_constructor(
512    cx: &mut Cx<'_>,
513    site: CallSite,
514    kind: FunctionKind,
515) -> Result<Value, Throw> {
516    let mut source: Vec<u16> = Vec::new();
517    let text = |source: &mut Vec<u16>, text: &str| source.extend(text.encode_utf16());
518    text(&mut source, "(");
519    if matches!(kind, FunctionKind::Async | FunctionKind::AsyncGenerator) {
520        text(&mut source, "async ");
521    }
522    text(&mut source, "function");
523    if matches!(kind, FunctionKind::Generator | FunctionKind::AsyncGenerator) {
524        text(&mut source, "*");
525    }
526    text(&mut source, " anonymous(");
527    let parameters = site.argc.saturating_sub(1);
528    for index in 0..parameters {
529        if index > 0 {
530            text(&mut source, ",");
531        }
532        let parameter = site.arg(cx, index);
533        let parameter = ops::to_string(cx, parameter)?;
534        source.extend(ops::units(cx.heap, parameter.as_string().unwrap()));
535    }
536    text(&mut source, "\n) {\n");
537    if site.argc > 0 {
538        let body = site.arg(cx, site.argc - 1);
539        let body = ops::to_string(cx, body)?;
540        source.extend(ops::units(cx.heap, body.as_string().unwrap()));
541    }
542    text(&mut source, "\n})");
543    let function = run(cx, &source, CompileKind::IndirectEval)?;
544    let Some(object) = function.as_object() else {
545        return Err(error::type_error(cx, "not an object"));
546    };
547    let default = match kind {
548        FunctionKind::Normal => Intrinsic::FunctionPrototype,
549        FunctionKind::Generator => Intrinsic::GeneratorFunctionPrototype,
550        FunctionKind::Async => Intrinsic::AsyncFunctionPrototype,
551        FunctionKind::AsyncGenerator => Intrinsic::AsyncGeneratorFunctionPrototype,
552    };
553    if site.construct && !site.new_target.is_undefined() {
554        let mark = cx.heap.roots().mark();
555        cx.heap.roots().push(function);
556        let prototype = super::object::prototype_from_constructor(cx, site.new_target, default);
557        cx.heap.roots().truncate(mark);
558        let prototype = prototype?;
559        if !super::object::set_prototype_of(cx, object, Value::object(prototype))? {
560            return Err(error::type_error(cx, "prototype is immutable"));
561        }
562    }
563    Ok(function)
564}
565
566/// The unit flags of a bytecode function (its kind), or `None` for any
567/// other function.
568pub(crate) fn function_kind_flags(cx: &Cx<'_>, function: ObjectId) -> Option<u16> {
569    if cx.heap.object_kind(function) != crate::ObjectKind::Function {
570        return None;
571    }
572    let bytecode = cx.heap.function_bytecode(function)?;
573    let unit = cx.heap.bytecode_unit(bytecode);
574    Some(cx.unit(unit).flags)
575}
576
577/// The source text of a bytecode function, if it was recorded.
578pub(crate) fn function_source(cx: &Cx<'_>, function: ObjectId) -> Option<Vec<u16>> {
579    if cx.heap.object_kind(function) != crate::ObjectKind::Function {
580        return None;
581    }
582    let bytecode = cx.heap.function_bytecode(function)?;
583    let unit = cx.heap.bytecode_unit(bytecode);
584    let unit = cx.unit(unit);
585    Some(wtf8_units(unit.source.as_ref()?))
586}
587
588/// The UTF-16 units of source `bytes` as QuickJS's `JS_NewStringLen` reads
589/// them (`Function.prototype.toString` of a function's debug source): a
590/// surrogate's three-byte form gives the lone unit back, and an invalid
591/// sequence gives one U+FFFD, skipping its continuation bytes, the next
592/// byte and that byte's continuation bytes.
593fn wtf8_units(bytes: &[u8]) -> Vec<u16> {
594    let continuation = |byte: u8| (0x80..0xc0).contains(&byte);
595    let mut units = Vec::with_capacity(bytes.len());
596    let mut index = 0;
597    while index < bytes.len() {
598        let lead = bytes[index];
599        if lead < 0x80 {
600            units.push(u16::from(lead));
601            index += 1;
602            continue;
603        }
604        // QuickJS's `unicode_from_utf8`.
605        let (length, mask, minimum) = match lead {
606            0xc0..=0xdf => (1, 0x1f, 0x80),
607            0xe0..=0xef => (2, 0x0f, 0x800),
608            0xf0..=0xf7 => (3, 0x07, 0x1_0000),
609            0xf8..=0xfb => (4, 0x03, 0x20_0000),
610            0xfc..=0xfd => (5, 0x01, 0x400_0000),
611            _ => (0, 0, 0),
612        };
613        let decoded = (length > 0 && index + length < bytes.len())
614            .then(|| &bytes[index + 1..=index + length])
615            .filter(|tail| tail.iter().all(|&byte| continuation(byte)))
616            .map(|tail| {
617                tail.iter().fold(u32::from(lead & mask), |code, &byte| {
618                    code << 6 | u32::from(byte & 0x3f)
619                })
620            })
621            .filter(|&code| code >= minimum && code <= 0x10_ffff);
622        match decoded {
623            Some(code) if code >= 0x1_0000 => {
624                let code = code - 0x1_0000;
625                units.push(0xd800 | (code >> 10) as u16);
626                units.push(0xdc00 | (code & 0x3ff) as u16);
627                index += length + 1;
628            }
629            Some(code) => {
630                units.push(code as u16);
631                index += length + 1;
632            }
633            None => {
634                units.push(0xfffd);
635                while index < bytes.len() && continuation(bytes[index]) {
636                    index += 1;
637                }
638                if index < bytes.len() {
639                    index += 1;
640                    while index < bytes.len() && continuation(bytes[index]) {
641                        index += 1;
642                    }
643                }
644            }
645        }
646    }
647    units
648}
649
650/// `$262.evalScript(source)`: the source as a separate script. qjs's
651/// `load(path)` (`qjs --std`): the file, run as a script.
652pub(crate) fn load(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
653    load_script(cx, site).map(|_| Value::UNDEFINED)
654}
655
656/// qjs's `__loadScript(path)`: the file, run as a script; its completion
657/// value.
658pub(crate) fn load_script(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
659    let path = site.arg(cx, 0);
660    let path = ops::to_string(cx, path)?;
661    let path = cx.heap.string_text(path.as_string().unwrap());
662    let Some(text) = cx.host.hooks.read_file(&path) else {
663        let message = format!("could not load '{path}'");
664        return Err(error::throw(cx, ErrorKind::Reference, &message));
665    };
666    let units: Vec<u16> = text.encode_utf16().collect();
667    let function = compile_named(cx, &units, CompileKind::Script, &path)?;
668    let mark = cx.heap.roots().mark();
669    cx.heap.roots().push(Value::object(function));
670    let global = Value::object(cx.realm.global());
671    let result = super::call(cx, Value::object(function), global, &[]);
672    cx.heap.roots().truncate(mark);
673    result
674}
675
676/// qjs's `read(path)` (`qjs --std`): the file's text.
677pub(crate) fn read(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
678    let path = site.arg(cx, 0);
679    let path = ops::to_string(cx, path)?;
680    let path = cx.heap.string_text(path.as_string().unwrap());
681    let Some(text) = cx.host.hooks.read_file(&path) else {
682        let message = format!("could not read '{path}'");
683        return Err(error::throw(cx, ErrorKind::Error, &message));
684    };
685    crate::builtins::string_text(cx, &text)
686}
687
688pub(crate) fn eval_script(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
689    let source = site.arg(cx, 0);
690    let source = ops::to_string(cx, source)?;
691    let units = ops::units(cx.heap, source.as_string().unwrap());
692    run(cx, &units, CompileKind::Script)
693}
694
695#[cfg(test)]
696mod tests {
697    use crate::{Runtime, RuntimeOptions};
698
699    #[test]
700    fn a_compilation_holds_each_atom_once_until_it_ends() {
701        let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
702        let mut compiler = runtime.transient_compiler();
703        let mark = compiler.heap().held_atoms_mark();
704        let atom = compiler.heap_mut().intern("held once").unwrap();
705        for _ in 0..3 {
706            assert_eq!(compiler.heap_mut().intern("held once").unwrap(), atom);
707        }
708        assert_eq!(
709            compiler.heap_mut().intern_latin1(b"held once").unwrap(),
710            atom
711        );
712        assert_eq!(compiler.heap().held_atoms_mark(), mark + 1);
713        // A reborrow (a host callback's view) ends nothing when dropped.
714        {
715            let mut view = compiler.reborrow();
716            view.heap_mut().intern("seen by the view").unwrap();
717        }
718        compiler.heap_mut().collect();
719        assert_eq!(compiler.heap().string_text(atom), "held once");
720        assert_eq!(compiler.heap().held_atoms_mark(), mark + 2);
721        drop(compiler);
722        assert_eq!(runtime.heap().held_atoms_mark(), mark);
723    }
724}