quickjs-oxide-core 0.4.0

The new quickjs-oxide core: 8-byte values, typed arenas and a mark-sweep heap
Documentation
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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
//! Code compiled at run time: `eval`, the `Function` constructors,
//! `$262.evalScript` and `Function.prototype.toString`'s source text, through
//! the compile host the embedder installs.
//!
//! The core has no front end. The embedder installs a [`CompileFn`] that
//! compiles and lowers source into a [`Compiler`], the part of the runtime a
//! lowering needs (the heap, the code store, the realm), which the natives
//! can lend while the loop holds the code store: units compiled at run time
//! are published into the heap, which frees them once unreachable.
//!
//! A compilation keeps what it makes alive until it ends: a unit published
//! but not yet instantiated is pinned, and the atoms it interns (keys, names,
//! string constants) are held. Before the program runs, its atoms are pinned
//! for good instead.

use super::native::CallSite;
use super::{Cx, Throw, error, ops};
use crate::ErrorKind;
use crate::bytecode::{CodeStore, CodeUnit, CodeUnitId, Units, ValidateError, validate};
use crate::heap::hash::FxHashMap;
use crate::heap::{AtomMode, BytecodeTemplate, Heap, HeapError};
use crate::runtime::{Intrinsic, Realm};
use crate::value::{ObjectId, Value};

/// The code store a compilation publishes into: owned before the program
/// runs (units go in the static store), shared while it runs (units go in
/// the heap).
enum Store<'a> {
    Owned(&'a mut CodeStore),
    Shared(&'a CodeStore),
}

/// The runtime's parts a compilation uses. Dropping it ends the
/// compilation: its pinned units and held atoms are released (a
/// [`Compiler::reborrow`] ends nothing).
pub struct Compiler<'a> {
    heap: &'a mut Heap,
    code: Store<'a>,
    realm: &'a Realm,
    /// The atom mode before the compilation, restored after it.
    mode: AtomMode,
    /// Where the compilation's pinned units and held atoms start.
    pins: usize,
    held: usize,
    /// Whether dropping it ends the compilation (not a reborrow's).
    ends: bool,
}

impl Drop for Compiler<'_> {
    fn drop(&mut self) {
        if !self.ends {
            return;
        }
        self.heap.set_atom_mode(self.mode);
        self.heap.code.unpin(self.pins);
        self.heap.release_held_atoms(self.held);
    }
}

impl<'a> Compiler<'a> {
    fn with(heap: &'a mut Heap, code: Store<'a>, realm: &'a Realm, mode: AtomMode) -> Self {
        let pins = heap.code.pin_mark();
        let held = heap.held_atoms_mark();
        let mode = heap.set_atom_mode(mode);
        Compiler {
            heap,
            code,
            realm,
            mode,
            pins,
            held,
            ends: true,
        }
    }

    pub(crate) fn new(heap: &'a mut Heap, code: &'a CodeStore, realm: &'a Realm) -> Self {
        Compiler::with(heap, Store::Shared(code), realm, AtomMode::Hold)
    }

    pub(crate) fn owned(heap: &'a mut Heap, code: &'a mut CodeStore, realm: &'a Realm) -> Self {
        Compiler::with(heap, Store::Owned(code), realm, AtomMode::Pin)
    }

    /// The same parts for a shorter borrow (a host callback's view).
    pub fn reborrow(&mut self) -> Compiler<'_> {
        let code = match &mut self.code {
            Store::Owned(code) => Store::Owned(code),
            Store::Shared(code) => Store::Shared(code),
        };
        Compiler {
            heap: self.heap,
            code,
            realm: self.realm,
            mode: self.mode,
            pins: self.pins,
            held: self.held,
            ends: false,
        }
    }

    /// [`NativeCx::new_host_function`](crate::NativeCx::new_host_function).
    pub fn new_host_function(
        &mut self,
        name: &str,
        length: u32,
        constructor: bool,
        data: &[Value],
        body: std::rc::Rc<super::native::HostFn>,
    ) -> Result<ObjectId, HeapError> {
        crate::embed::host_function(self.heap, self.realm, name, length, constructor, data, body)
    }

    /// [`NativeCx::new_host_object`](crate::NativeCx::new_host_object).
    pub fn new_host_object(
        &mut self,
        prototype: Option<ObjectId>,
        data: std::rc::Rc<dyn std::any::Any>,
    ) -> Result<ObjectId, HeapError> {
        crate::embed::host_object(self.heap, prototype, data)
    }

    /// Holds `value` for the embedder until released (the heap's handle
    /// table).
    pub fn hold(&mut self, value: Value) -> crate::Handle {
        self.heap.handles().create(value)
    }

    /// A new error object of `kind` with `message` and an empty `stack`: an
    /// error a host callback raises, with no JavaScript frame, as QuickJS's.
    pub fn new_error(&mut self, kind: ErrorKind, message: &str) -> Result<Value, HeapError> {
        let prototype = self.realm.intrinsic(kind.prototype());
        let message = self.heap.alloc_string(message)?;
        let mark = self.heap.roots().mark();
        self.heap.roots().push(Value::string(message));
        let result = (|| {
            let object = self
                .heap
                .alloc_object(crate::ObjectKind::Ordinary, Some(prototype), 1)?;
            self.heap
                .set_object_flag(object, crate::heap::object::flags::ERROR);
            let key = crate::runtime::Names::key(self.realm.names.message);
            self.heap.add_property(
                object,
                key,
                crate::PropertyFlags::data(true, false, true),
                Value::string(message),
            )?;
            let empty = self.heap.intern("")?;
            let stack = crate::runtime::Names::key(self.realm.names.stack);
            self.heap.add_property(
                object,
                stack,
                crate::PropertyFlags::data(true, false, true),
                Value::string(empty),
            )?;
            Ok(Value::object(object))
        })();
        self.heap.roots().truncate(mark);
        result
    }

    /// [`Compiler::new_error`] located at `file:line:column` (a module's
    /// compile error): `fileName`, `lineNumber`, `columnNumber`, and a
    /// `stack` of the location alone.
    pub fn new_located_error(
        &mut self,
        kind: ErrorKind,
        message: &str,
        file: &str,
        line: u32,
        column: u32,
    ) -> Result<Value, HeapError> {
        let error = self.new_error(kind, message)?;
        let object = error.as_object().expect("an error object");
        let names = self.realm.names;
        let mark = self.heap.roots().mark();
        self.heap.roots().push(error);
        let result = (|| {
            let flags = crate::PropertyFlags::data(true, false, true);
            let stack_key = crate::runtime::Names::key(names.stack);
            // `fileName`, `lineNumber` and `columnNumber` precede `stack`,
            // as QuickJS's `build_backtrace` defines them: the stack is
            // re-added after them.
            self.heap.delete_property(object, stack_key)?;
            let file_text = self.heap.alloc_string(file)?;
            self.heap.add_property(
                object,
                crate::runtime::Names::key(names.file_name),
                flags,
                Value::string(file_text),
            )?;
            self.heap.add_property(
                object,
                crate::runtime::Names::key(names.line_number),
                flags,
                Value::int32(line as i32),
            )?;
            self.heap.add_property(
                object,
                crate::runtime::Names::key(names.column_number),
                flags,
                Value::int32(column as i32),
            )?;
            let stack = self
                .heap
                .alloc_string(&format!("    at {file}:{line}:{column}\n"))?;
            self.heap
                .add_property(object, stack_key, flags, Value::string(stack))?;
            Ok(error)
        })();
        self.heap.roots().truncate(mark);
        result
    }

    pub fn heap(&self) -> &Heap {
        self.heap
    }

    pub fn heap_mut(&mut self) -> &mut Heap {
        self.heap
    }

    /// Every published unit, static and dynamic.
    pub fn units(&self) -> Units<'_> {
        let code = match &self.code {
            Store::Owned(code) => &**code,
            Store::Shared(code) => *code,
        };
        Units {
            code,
            dynamic: &self.heap.code,
        }
    }

    pub fn intrinsic(&self, intrinsic: Intrinsic) -> ObjectId {
        self.realm.intrinsic(intrinsic)
    }

    /// Validates `unit`, reserves its cache entries and publishes it: into
    /// the code store before the program runs (its key atoms pinned), into
    /// the heap (pinned until the compilation ends, its bytes charged to the
    /// collection trigger) while it runs. Its children must be published
    /// first.
    pub fn publish(&mut self, mut unit: CodeUnit) -> Result<CodeUnitId, ValidateError> {
        validate::validate(&unit, &|id| self.units().lookup(id))?;
        self.heap.ics.reserve(&mut unit);
        match &mut self.code {
            // A static unit lives as long as the runtime, and so do its keys,
            // wherever they were interned.
            Store::Owned(code) => {
                for &atom in &unit.key_atoms() {
                    self.heap.pin_atom(atom);
                }
                Ok(code.push(unit))
            }
            Store::Shared(_) => {
                let (id, bytes) = self.heap.code.publish(unit);
                self.heap.gc.allocated += bytes;
                Ok(id)
            }
        }
    }

    /// A function object for the published unit `root`, with a bytecode
    /// record for it and every unit below it. `constants` holds one table per
    /// unit, in the pre-order of [`Units::tree`].
    pub fn instantiate(
        &mut self,
        root: CodeUnitId,
        constants: &[&[Value]],
    ) -> Result<ObjectId, HeapError> {
        let code = match &self.code {
            Store::Owned(code) => &**code,
            Store::Shared(code) => *code,
        };
        let code = Units {
            code,
            dynamic: &self.heap.code,
        };
        let order = code.tree(root);
        assert_eq!(order.len(), constants.len(), "one constant table per unit");
        let position: FxHashMap<CodeUnitId, u32> = order
            .iter()
            .enumerate()
            .map(|(index, &unit)| (unit, index as u32))
            .collect();
        let children: Vec<Vec<u32>> = order
            .iter()
            .map(|&id| {
                let unit = code.get(id);
                unit.children.iter().map(|child| position[child]).collect()
            })
            .collect();
        let tree: Vec<BytecodeTemplate<'_>> = order
            .iter()
            .zip(constants)
            .zip(&children)
            .map(|((&unit, &constants), children)| {
                assert_eq!(
                    constants.len(),
                    code.get(unit).constant_count as usize,
                    "constant table length"
                );
                BytecodeTemplate {
                    unit,
                    constants,
                    children,
                }
            })
            .collect();
        let prototype = self.realm.intrinsic(Intrinsic::FunctionPrototype);
        self.heap
            .alloc_bytecode_function(Some(prototype), None, &tree)
    }
}

impl Compiler<'_> {
    /// A tagged template's frozen site object.
    pub fn template_object(
        &mut self,
        cooked: &[Value],
        raw: &[Value],
    ) -> Result<ObjectId, HeapError> {
        use crate::heap::object::flags;
        let prototype = self.intrinsic(Intrinsic::ArrayPrototype);
        let names = self.realm.names;
        let heap = &mut *self.heap;
        let mark = heap.roots().mark();
        // An atom interned outside a compilation is weak.
        for &value in cooked.iter().chain(raw) {
            heap.roots().push(value);
        }
        let result = (|| {
            let raw_array = heap.alloc_array(Some(prototype), raw.len() as u32)?;
            heap.roots().push(Value::object(raw_array));
            let array = heap.alloc_array(Some(prototype), cooked.len() as u32)?;
            heap.roots().push(Value::object(array));
            for (target, values) in [(raw_array, raw), (array, cooked)] {
                for (index, &value) in values.iter().enumerate() {
                    let key =
                        crate::PropertyKey::index(index as u32).ok_or(HeapError::OutOfMemory)?;
                    heap.add_property(
                        target,
                        key,
                        crate::PropertyFlags::data(false, true, false),
                        value,
                    )?;
                }
                heap.set_array_length(target, values.len() as u32);
            }
            let raw_key = crate::runtime::Names::key(names.raw);
            heap.add_property(
                array,
                raw_key,
                crate::PropertyFlags::data(false, false, false),
                Value::object(raw_array),
            )?;
            for target in [raw_array, array] {
                heap.set_object_flag(target, flags::SPARSE | flags::LENGTH_READONLY);
                heap.clear_object_flag(target, flags::EXTENSIBLE);
            }
            Ok(array)
        })();
        heap.roots().truncate(mark);
        result
    }
}

/// What to compile.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CompileKind {
    /// A script (`$262.evalScript`): its declarations are global.
    Script,
    /// Indirect eval code (`eval`, and the `Function` constructors' source):
    /// the root returns the completion value.
    IndirectEval,
}

/// A compilation request.
#[derive(Clone, Copy, Debug)]
pub struct CompileRequest<'s> {
    pub source: &'s [u16],
    pub kind: CompileKind,
    pub filename: &'s str,
}

/// A failed compilation: the error to throw.
#[derive(Clone, Debug)]
pub struct CompileError {
    pub kind: ErrorKind,
    pub message: String,
    /// A value the embedder threw instead (a module loader's own error
    /// object): thrown as is, `kind` and `message` unused. It must stay
    /// rooted until the core throws it (a handle the embedder releases
    /// after the entry).
    pub thrown: Option<Value>,
    /// Where in which source the error is (a syntax error's file, line and
    /// column, one-based): the error's `stack` starts with it, as
    /// QuickJS's does.
    pub location: Option<(Box<str>, u32, u32)>,
}

impl CompileError {
    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
        CompileError {
            kind,
            message: message.into(),
            thrown: None,
            location: None,
        }
    }

    /// The error located at `line` and `column` of `file`.
    pub fn at(mut self, file: &str, line: u32, column: u32) -> Self {
        self.location = Some((file.into(), line, column));
        self
    }

    pub fn thrown(value: Value) -> Self {
        CompileError {
            kind: ErrorKind::Internal,
            message: String::new(),
            thrown: Some(value),
            location: None,
        }
    }
}

/// Throws a compile failure.
pub(crate) fn throw_failure(cx: &mut Cx<'_>, failure: CompileError) -> Throw {
    match (failure.thrown, failure.location) {
        (Some(value), _) => Throw(value),
        (None, Some((file, line, column))) => {
            error::throw_located(cx, failure.kind, &failure.message, &file, line, column)
        }
        (None, None) => error::throw(cx, failure.kind, &failure.message),
    }
}

/// The embedder's compiler: the root function of the compiled code, to be
/// called with the global object as `this`.
pub type CompileFn = fn(&mut Compiler<'_>, &CompileRequest<'_>) -> Result<ObjectId, CompileError>;

/// Compiles `source` through the host, throwing its error.
pub(crate) fn compile(
    cx: &mut Cx<'_>,
    source: &[u16],
    kind: CompileKind,
) -> Result<ObjectId, Throw> {
    compile_named(cx, source, kind, "<input>")
}

/// [`compile`] with the filename the code's positions name.
pub(crate) fn compile_named(
    cx: &mut Cx<'_>,
    source: &[u16],
    kind: CompileKind,
    filename: &str,
) -> Result<ObjectId, Throw> {
    let Some(compile) = cx.host.compile else {
        return Err(super::object::unsupported(cx, "eval"));
    };
    let request = CompileRequest {
        source,
        kind,
        filename,
    };
    let result = compile(
        &mut Compiler::new(cx.heap, cx.static_code, cx.realm),
        &request,
    );
    match result {
        Ok(function) => Ok(function),
        Err(failure) => Err(throw_failure(cx, failure)),
    }
}

/// Compiles and runs `source` on the global object.
fn run(cx: &mut Cx<'_>, source: &[u16], kind: CompileKind) -> Result<Value, Throw> {
    let function = compile(cx, source, kind)?;
    let mark = cx.heap.roots().mark();
    cx.heap.roots().push(Value::object(function));
    let global = Value::object(cx.realm.global());
    let result = super::call(cx, Value::object(function), global, &[]);
    cx.heap.roots().truncate(mark);
    result
}

/// `%eval%(x)`: a direct eval runs as an indirect one.
pub(crate) fn eval(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
    let source = site.arg(cx, 0);
    let Some(string) = source.as_string() else {
        return Ok(source);
    };
    let units = ops::units(cx.heap, string);
    run(cx, &units, CompileKind::IndirectEval)
}

/// Which `Function` constructor runs.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FunctionKind {
    Normal,
    Generator,
    Async,
    AsyncGenerator,
}

/// `CreateDynamicFunction`, as QuickJS does it: the parameters and body in
/// `(function anonymous(…\n) {\n…\n})`, evaluated as indirect eval code.
pub(crate) fn function_constructor(
    cx: &mut Cx<'_>,
    site: CallSite,
    kind: FunctionKind,
) -> Result<Value, Throw> {
    let mut source: Vec<u16> = Vec::new();
    let text = |source: &mut Vec<u16>, text: &str| source.extend(text.encode_utf16());
    text(&mut source, "(");
    if matches!(kind, FunctionKind::Async | FunctionKind::AsyncGenerator) {
        text(&mut source, "async ");
    }
    text(&mut source, "function");
    if matches!(kind, FunctionKind::Generator | FunctionKind::AsyncGenerator) {
        text(&mut source, "*");
    }
    text(&mut source, " anonymous(");
    let parameters = site.argc.saturating_sub(1);
    for index in 0..parameters {
        if index > 0 {
            text(&mut source, ",");
        }
        let parameter = site.arg(cx, index);
        let parameter = ops::to_string(cx, parameter)?;
        source.extend(ops::units(cx.heap, parameter.as_string().unwrap()));
    }
    text(&mut source, "\n) {\n");
    if site.argc > 0 {
        let body = site.arg(cx, site.argc - 1);
        let body = ops::to_string(cx, body)?;
        source.extend(ops::units(cx.heap, body.as_string().unwrap()));
    }
    text(&mut source, "\n})");
    let function = run(cx, &source, CompileKind::IndirectEval)?;
    let Some(object) = function.as_object() else {
        return Err(error::type_error(cx, "not an object"));
    };
    let default = match kind {
        FunctionKind::Normal => Intrinsic::FunctionPrototype,
        FunctionKind::Generator => Intrinsic::GeneratorFunctionPrototype,
        FunctionKind::Async => Intrinsic::AsyncFunctionPrototype,
        FunctionKind::AsyncGenerator => Intrinsic::AsyncGeneratorFunctionPrototype,
    };
    if site.construct && !site.new_target.is_undefined() {
        let mark = cx.heap.roots().mark();
        cx.heap.roots().push(function);
        let prototype = super::object::prototype_from_constructor(cx, site.new_target, default);
        cx.heap.roots().truncate(mark);
        let prototype = prototype?;
        if !super::object::set_prototype_of(cx, object, Value::object(prototype))? {
            return Err(error::type_error(cx, "prototype is immutable"));
        }
    }
    Ok(function)
}

/// The unit flags of a bytecode function (its kind), or `None` for any
/// other function.
pub(crate) fn function_kind_flags(cx: &Cx<'_>, function: ObjectId) -> Option<u16> {
    if cx.heap.object_kind(function) != crate::ObjectKind::Function {
        return None;
    }
    let bytecode = cx.heap.function_bytecode(function)?;
    let unit = cx.heap.bytecode_unit(bytecode);
    Some(cx.unit(unit).flags)
}

/// The source text of a bytecode function, if it was recorded.
pub(crate) fn function_source(cx: &Cx<'_>, function: ObjectId) -> Option<Vec<u16>> {
    if cx.heap.object_kind(function) != crate::ObjectKind::Function {
        return None;
    }
    let bytecode = cx.heap.function_bytecode(function)?;
    let unit = cx.heap.bytecode_unit(bytecode);
    let unit = cx.unit(unit);
    Some(wtf8_units(unit.source.as_ref()?))
}

/// The UTF-16 units of source `bytes` as QuickJS's `JS_NewStringLen` reads
/// them (`Function.prototype.toString` of a function's debug source): a
/// surrogate's three-byte form gives the lone unit back, and an invalid
/// sequence gives one U+FFFD, skipping its continuation bytes, the next
/// byte and that byte's continuation bytes.
fn wtf8_units(bytes: &[u8]) -> Vec<u16> {
    let continuation = |byte: u8| (0x80..0xc0).contains(&byte);
    let mut units = Vec::with_capacity(bytes.len());
    let mut index = 0;
    while index < bytes.len() {
        let lead = bytes[index];
        if lead < 0x80 {
            units.push(u16::from(lead));
            index += 1;
            continue;
        }
        // QuickJS's `unicode_from_utf8`.
        let (length, mask, minimum) = match lead {
            0xc0..=0xdf => (1, 0x1f, 0x80),
            0xe0..=0xef => (2, 0x0f, 0x800),
            0xf0..=0xf7 => (3, 0x07, 0x1_0000),
            0xf8..=0xfb => (4, 0x03, 0x20_0000),
            0xfc..=0xfd => (5, 0x01, 0x400_0000),
            _ => (0, 0, 0),
        };
        let decoded = (length > 0 && index + length < bytes.len())
            .then(|| &bytes[index + 1..=index + length])
            .filter(|tail| tail.iter().all(|&byte| continuation(byte)))
            .map(|tail| {
                tail.iter().fold(u32::from(lead & mask), |code, &byte| {
                    code << 6 | u32::from(byte & 0x3f)
                })
            })
            .filter(|&code| code >= minimum && code <= 0x10_ffff);
        match decoded {
            Some(code) if code >= 0x1_0000 => {
                let code = code - 0x1_0000;
                units.push(0xd800 | (code >> 10) as u16);
                units.push(0xdc00 | (code & 0x3ff) as u16);
                index += length + 1;
            }
            Some(code) => {
                units.push(code as u16);
                index += length + 1;
            }
            None => {
                units.push(0xfffd);
                while index < bytes.len() && continuation(bytes[index]) {
                    index += 1;
                }
                if index < bytes.len() {
                    index += 1;
                    while index < bytes.len() && continuation(bytes[index]) {
                        index += 1;
                    }
                }
            }
        }
    }
    units
}

/// `$262.evalScript(source)`: the source as a separate script. qjs's
/// `load(path)` (`qjs --std`): the file, run as a script.
pub(crate) fn load(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
    load_script(cx, site).map(|_| Value::UNDEFINED)
}

/// qjs's `__loadScript(path)`: the file, run as a script; its completion
/// value.
pub(crate) fn load_script(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
    let path = site.arg(cx, 0);
    let path = ops::to_string(cx, path)?;
    let path = cx.heap.string_text(path.as_string().unwrap());
    let Some(text) = cx.host.hooks.read_file(&path) else {
        let message = format!("could not load '{path}'");
        return Err(error::throw(cx, ErrorKind::Reference, &message));
    };
    let units: Vec<u16> = text.encode_utf16().collect();
    let function = compile_named(cx, &units, CompileKind::Script, &path)?;
    let mark = cx.heap.roots().mark();
    cx.heap.roots().push(Value::object(function));
    let global = Value::object(cx.realm.global());
    let result = super::call(cx, Value::object(function), global, &[]);
    cx.heap.roots().truncate(mark);
    result
}

/// qjs's `read(path)` (`qjs --std`): the file's text.
pub(crate) fn read(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
    let path = site.arg(cx, 0);
    let path = ops::to_string(cx, path)?;
    let path = cx.heap.string_text(path.as_string().unwrap());
    let Some(text) = cx.host.hooks.read_file(&path) else {
        let message = format!("could not read '{path}'");
        return Err(error::throw(cx, ErrorKind::Error, &message));
    };
    crate::builtins::string_text(cx, &text)
}

pub(crate) fn eval_script(cx: &mut Cx<'_>, site: CallSite) -> Result<Value, Throw> {
    let source = site.arg(cx, 0);
    let source = ops::to_string(cx, source)?;
    let units = ops::units(cx.heap, source.as_string().unwrap());
    run(cx, &units, CompileKind::Script)
}

#[cfg(test)]
mod tests {
    use crate::{Runtime, RuntimeOptions};

    #[test]
    fn a_compilation_holds_each_atom_once_until_it_ends() {
        let mut runtime = Runtime::new(RuntimeOptions::default()).unwrap();
        let mut compiler = runtime.transient_compiler();
        let mark = compiler.heap().held_atoms_mark();
        let atom = compiler.heap_mut().intern("held once").unwrap();
        for _ in 0..3 {
            assert_eq!(compiler.heap_mut().intern("held once").unwrap(), atom);
        }
        assert_eq!(
            compiler.heap_mut().intern_latin1(b"held once").unwrap(),
            atom
        );
        assert_eq!(compiler.heap().held_atoms_mark(), mark + 1);
        // A reborrow (a host callback's view) ends nothing when dropped.
        {
            let mut view = compiler.reborrow();
            view.heap_mut().intern("seen by the view").unwrap();
        }
        compiler.heap_mut().collect();
        assert_eq!(compiler.heap().string_text(atom), "held once");
        assert_eq!(compiler.heap().held_atoms_mark(), mark + 2);
        drop(compiler);
        assert_eq!(runtime.heap().held_atoms_mark(), mark);
    }
}