jstime_core 0.67.0

Another JS Runtime
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
mod buffered_random;
mod builtins;
mod error;
mod event_loop;
mod isolate_state;
mod js_loading;
mod module;
mod pool;
mod script;
mod sourcemap;

pub(crate) use isolate_state::IsolateState;

pub fn init(v8_flags: Option<Vec<String>>) {
    // Initialize ICU data before V8 initialization
    // This is required for locale-specific operations like toLocaleString()
    static ICU_INIT: std::sync::Once = std::sync::Once::new();
    ICU_INIT.call_once(|| {
        let icu_data =
            align_data::include_aligned!(align_data::Align16, "../third_party/icu/icudtl.dat");
        // Ignore errors - ICU data initialization is best-effort
        let _ = v8::icu::set_common_data_77(icu_data);
        // Set default locale to en_US
        v8::icu::set_default_locale("en_US");
    });

    let mut flags = v8_flags.unwrap_or_default();

    // Add performance-oriented V8 flags if not already present
    // Only add flags that don't conflict with user-provided flags
    let perf_flags = [
        "--turbofan", // Enable TurboFan optimizing compiler (usually on by default)
        "--opt",      // Enable optimizations
    ];

    for flag in &perf_flags {
        if !flags
            .iter()
            .any(|f| f.starts_with(flag) || f.starts_with(&format!("--no-{}", &flag[2..])))
        {
            flags.push(flag.to_string());
        }
    }

    flags.push("jstime".to_owned());
    flags.rotate_right(1);

    v8::V8::set_flags_from_command_line(flags);

    static V8_INIT: std::sync::Once = std::sync::Once::new();
    V8_INIT.call_once(|| {
        let platform = v8::new_default_platform(0, false).make_shared();
        v8::V8::initialize_platform(platform);
        v8::V8::initialize();
    });
}

/// Options for `JSTime::new`.
#[derive(Default)]
pub struct Options {
    pub snapshot: Option<&'static [u8]>,
    taking_snapshot: bool,
    pub process_argv: Vec<String>,
    /// Number of warmup iterations to run before actual execution.
    /// This allows V8's TurboFan JIT compiler to optimize the code.
    /// Default is 0 (no warmup).
    pub warmup_iterations: usize,
}

impl Options {
    pub fn new(snapshot: Option<&'static [u8]>) -> Options {
        Options {
            snapshot,
            taking_snapshot: false,
            process_argv: Vec::new(),
            warmup_iterations: 0,
        }
    }

    pub fn with_process_argv(mut self, argv: Vec<String>) -> Self {
        self.process_argv = argv;
        self
    }

    pub fn with_warmup(mut self, iterations: usize) -> Self {
        self.warmup_iterations = iterations;
        self
    }
}

/// JSTime Instance.
#[allow(clippy::all)]
pub struct JSTime {
    isolate: Option<v8::OwnedIsolate>,
    taking_snapshot: bool,
    warmup_iterations: usize,
}

impl JSTime {
    /// Create a new JSTime instance from `options`.
    pub fn new(options: Options) -> JSTime {
        let mut create_params = v8::Isolate::create_params()
            .external_references(builtins::get_external_references().into_vec().into())
            .heap_limits(0, 1024 * 1024 * 1024); // 1GB max heap size
        if let Some(snapshot) = options.snapshot {
            create_params = create_params.snapshot_blob(snapshot.into());
        }
        let isolate = v8::Isolate::new(create_params);
        JSTime::create(options, isolate)
    }

    pub fn create_snapshot(mut options: Options) -> Vec<u8> {
        assert!(
            options.snapshot.is_none(),
            "Cannot pass snapshot data while creating snapshot"
        );
        options.taking_snapshot = true;

        let external_refs = builtins::get_external_references();
        let external_refs_cow: std::borrow::Cow<'static, [v8::ExternalReference]> =
            std::borrow::Cow::Owned(external_refs.into_vec());
        let mut isolate = v8::Isolate::snapshot_creator(Some(external_refs_cow), None);

        // Set up import.meta callback before creating context
        isolate.set_host_initialize_import_meta_object_callback(
            module::host_initialize_import_meta_object_callback,
        );

        // Set up dynamic import callback
        isolate.set_host_import_module_dynamically_callback(
            module::host_import_module_dynamically_callback,
        );

        let global_context = {
            v8::scope!(let scope, &mut isolate);
            let context = v8::Context::new(scope, Default::default());
            let isolate_ref: &v8::Isolate = scope;
            v8::Global::new(isolate_ref, context)
        };

        isolate.set_slot(IsolateState::new(global_context, options.process_argv));

        // Create builtins in the snapshot context and set default context
        {
            let context = IsolateState::get(&mut isolate).borrow().context();
            v8::scope!(let scope, &mut isolate);
            let context_local = v8::Local::new(scope, context);
            let scope = &mut v8::ContextScope::new(scope, context_local);

            // Create builtins
            builtins::Builtins::create(scope);

            // Set the default context for the snapshot
            scope.set_default_context(context_local);
        }

        // Drop the context before creating the blob
        IsolateState::get(&mut isolate).borrow_mut().drop_context();

        match isolate.create_blob(v8::FunctionCodeHandling::Keep) {
            Some(data) => data.to_vec(),
            None => {
                panic!("Unable to create snapshot");
            }
        }
    }

    fn create(options: Options, mut isolate: v8::OwnedIsolate) -> JSTime {
        // Set up import.meta callback before creating context
        isolate.set_host_initialize_import_meta_object_callback(
            module::host_initialize_import_meta_object_callback,
        );

        // Set up dynamic import callback
        isolate.set_host_import_module_dynamically_callback(
            module::host_import_module_dynamically_callback,
        );

        let global_context = {
            v8::scope!(let scope, &mut isolate);
            let context = v8::Context::new(scope, Default::default());
            let isolate_ref: &v8::Isolate = scope;
            v8::Global::new(isolate_ref, context)
        };

        isolate.set_slot(IsolateState::new(global_context, options.process_argv));

        // If snapshot data was provided, the builtins already exist within it.
        if options.snapshot.is_none() {
            let context = IsolateState::get(&mut isolate).borrow().context();
            v8::scope!(let scope, &mut isolate);
            let context_local = v8::Local::new(scope, context);
            let mut scope = v8::ContextScope::new(scope, context_local);
            builtins::Builtins::create(&mut scope);
        }

        JSTime {
            isolate: Some(isolate),
            taking_snapshot: options.taking_snapshot,
            warmup_iterations: options.warmup_iterations,
        }
    }

    fn isolate(&mut self) -> &mut v8::Isolate {
        match self.isolate.as_mut() {
            Some(i) => i,
            None => unsafe {
                std::hint::unreachable_unchecked();
            },
        }
    }

    /// Import a module by filename.
    pub fn import(&mut self, filename: &str) -> Result<(), String> {
        // Perform JIT warmup if configured
        if self.warmup_iterations > 0 {
            self.warmup_import(filename)?;
        }

        let result = {
            let context = IsolateState::get(self.isolate()).borrow().context();
            v8::scope!(let scope, self.isolate());
            let context_local = v8::Local::new(scope, context);
            let mut scope = v8::ContextScope::new(scope, context_local);

            // Use a TryCatch scope to properly capture error details
            v8::tc_scope!(let tc, &mut scope);
            let loader = module::Loader::new();

            let mut cwd = std::env::current_dir().unwrap();
            cwd.push("jstime");
            let cwd = cwd.into_os_string().into_string().unwrap();
            match loader.import(tc, &cwd, filename) {
                Ok(_) => Ok(()),
                Err(exception) => {
                    // If we have caught exception details, format them properly
                    if tc.has_caught() {
                        Err(crate::error::format_exception(tc))
                    } else {
                        // Fallback: Format the exception value directly with enhanced formatting
                        Err(crate::error::format_exception_value(tc, exception))
                    }
                }
            }
        };

        // Run the event loop to process any pending timers
        self.run_event_loop();

        result
    }

    /// Warm up the JIT compiler by importing the module multiple times.
    /// This allows V8's TurboFan compiler to optimize the module code.
    fn warmup_import(&mut self, filename: &str) -> Result<(), String> {
        for _ in 0..self.warmup_iterations {
            let context = IsolateState::get(self.isolate()).borrow().context();
            v8::scope!(let scope, self.isolate());
            let context_local = v8::Local::new(scope, context);
            let mut scope = v8::ContextScope::new(scope, context_local);
            v8::tc_scope!(let tc, &mut scope);
            let loader = module::Loader::new();
            let mut cwd = std::env::current_dir().unwrap();
            cwd.push("jstime");
            let cwd = cwd.into_os_string().into_string().unwrap();
            // Import but ignore result during warmup
            match loader.import(tc, &cwd, filename) {
                Ok(_) => {}
                Err(exception) => {
                    if tc.has_caught() {
                        return Err(crate::error::format_exception(tc));
                    } else {
                        return Err(crate::error::format_exception_value(tc, exception));
                    }
                }
            }
        }
        Ok(())
    }

    /// Run a script and get a string representation of the result.
    /// This version runs the event loop after execution, which is suitable for file execution.
    /// For REPL usage, use `run_script_no_event_loop` instead.
    pub fn run_script(&mut self, source: &str, filename: &str) -> Result<String, String> {
        // Perform JIT warmup if configured
        if self.warmup_iterations > 0 {
            self.warmup_script(source, filename)?;
        }

        let result = self.run_script_no_event_loop(source, filename);

        // Run the event loop to process any pending timers
        self.run_event_loop();

        result
    }

    /// Warm up the JIT compiler by running the script multiple times.
    /// This allows V8's TurboFan compiler to optimize the code before the actual execution.
    fn warmup_script(&mut self, source: &str, filename: &str) -> Result<(), String> {
        for _ in 0..self.warmup_iterations {
            let context = IsolateState::get(self.isolate()).borrow().context();
            v8::scope!(let scope, self.isolate());
            let context_local = v8::Local::new(scope, context);
            let mut scope = v8::ContextScope::new(scope, context_local);
            // Run script but ignore the result during warmup
            script::run(&mut scope, source, filename)?;
        }
        Ok(())
    }

    /// Run a script and get a string representation of the result without running the event loop.
    /// This is suitable for REPL usage where the event loop should not block between commands.
    pub fn run_script_no_event_loop(
        &mut self,
        source: &str,
        filename: &str,
    ) -> Result<String, String> {
        let context = IsolateState::get(self.isolate()).borrow().context();
        v8::scope!(let scope, self.isolate());
        let context_local = v8::Local::new(scope, context);
        let mut scope = v8::ContextScope::new(scope, context_local);
        match script::run(&mut scope, source, filename) {
            Ok(v) => {
                let isolate: &v8::Isolate = &scope;
                Ok(v.to_string(&scope).unwrap().to_rust_string_lossy(isolate))
            }
            Err(e) => Err(e),
        }
    }

    /// Tick the event loop to execute ready timers without blocking.
    /// This is suitable for REPL usage to allow timers to execute in the background.
    pub fn tick_event_loop(&mut self) {
        let context = IsolateState::get(self.isolate()).borrow().context();
        v8::scope!(let scope, self.isolate());
        let context_local = v8::Local::new(scope, context);
        let mut scope = v8::ContextScope::new(scope, context_local);
        let event_loop = event_loop::get_event_loop(&mut scope);
        event_loop.borrow_mut().tick(&mut scope);
    }

    /// Run the event loop until all pending operations are complete
    fn run_event_loop(&mut self) {
        let context = IsolateState::get(self.isolate()).borrow().context();
        v8::scope!(let scope, self.isolate());
        let context_local = v8::Local::new(scope, context);
        let mut scope = v8::ContextScope::new(scope, context_local);
        let event_loop = event_loop::get_event_loop(&mut scope);
        event_loop.borrow_mut().run(&mut scope);
    }

    /// Get all global property names for REPL autocomplete.
    /// Returns a vector of property names on the global object (globalThis).
    pub fn get_global_names(&mut self) -> Vec<String> {
        let script = r#"
            (function() {
                const names = [];
                // Get own properties of globalThis
                names.push(...Object.getOwnPropertyNames(globalThis));
                // Sort and deduplicate
                return [...new Set(names)].sort().join('\n');
            })()
        "#;

        match self.run_script_no_event_loop(script, "__repl_get_globals__") {
            Ok(result) => result
                .lines()
                .filter(|s| !s.is_empty())
                .map(String::from)
                .collect(),
            Err(_) => Vec::new(),
        }
    }

    /// Get property names of an object for REPL autocomplete.
    ///
    /// The `obj_expr` parameter should be a valid JavaScript expression that evaluates to an object
    /// (e.g., "Math", "console", "crypto.subtle").
    ///
    /// Returns a vector of property names on the object, including inherited properties.
    ///
    /// # Safety
    /// This method validates input to only allow safe property access expressions
    /// (alphanumeric characters, underscores, dollar signs, and dots). Expressions
    /// with special characters are rejected and return an empty vector.
    pub fn get_property_names(&mut self, obj_expr: &str) -> Vec<String> {
        // Validate the expression contains only safe characters for property access
        // This prevents injection of arbitrary code through the expression
        // Allowed: alphanumeric, underscore, dollar sign, dot
        // (for property chains like crypto.subtle)
        let is_safe_expr = obj_expr
            .chars()
            .all(|c| c.is_alphanumeric() || c == '_' || c == '$' || c == '.');

        if !is_safe_expr || obj_expr.is_empty() {
            return Vec::new();
        }

        let script = format!(
            r#"
            (function() {{
                try {{
                    const obj = {obj_expr};
                    if (obj === null || obj === undefined) {{
                        return '';
                    }}
                    const names = new Set();
                    // Walk the prototype chain to get all properties
                    let current = obj;
                    while (current !== null) {{
                        Object.getOwnPropertyNames(current).forEach(n => names.add(n));
                        current = Object.getPrototypeOf(current);
                    }}
                    // Also get symbol properties converted to strings (like [Symbol.iterator])
                    return [...names].sort().join('\n');
                }} catch (e) {{
                    return '';
                }}
            }})()
        "#
        );

        match self.run_script_no_event_loop(&script, "__repl_get_props__") {
            Ok(result) => result
                .lines()
                .filter(|s| !s.is_empty())
                .map(String::from)
                .collect(),
            Err(_) => Vec::new(),
        }
    }
}

impl Drop for JSTime {
    fn drop(&mut self) {
        if self.taking_snapshot {
            // The isolate is not actually owned by JSTime if we're
            // snapshotting, it's owned by the SnapshotCreator.
            std::mem::forget(self.isolate.take().unwrap())
        }
    }
}