rusty_racer 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +17 -1
- data/ext/rusty_racer/Cargo.toml +1 -1
- data/ext/rusty_racer/src/lib.rs +330 -98
- data/ext/rusty_racer/src/ops.rs +109 -95
- data/lib/rusty_racer/version.rb +1 -1
- data/lib/rusty_racer.rb +18 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 488967c6324cc118d0cf37974c23566725c7c6ce14ba48572330075d1b2dff66
|
|
4
|
+
data.tar.gz: 48f562d20d1217e91a9727d1fa348312c51c49b9f82607f3bd2803247e76976c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: faad4323b59c0eca2d69f66bb9698a41ab5837c0f417710b0f125ca39c0fad22e3d399f8fe730bd2c526a1e9cfe34ad9b8c8be5fdbc47b5c8a6e860eb23308f7
|
|
7
|
+
data.tar.gz: 304a7388700465fcfc86a8e7ffeba87c9bd95455075bce889277abe44193f8f407326a136cfe0be39738d53061603d249f153f031866c9570bddec424ac5b807
|
data/README.md
CHANGED
|
@@ -93,6 +93,8 @@ ran to completion. When the value is incidental, say so: `#eval_void`,
|
|
|
93
93
|
result. A statement list has a completion value whether you want one or not, so
|
|
94
94
|
plumbing an object into a global (`globalThis.win = someProxy`) is enough to
|
|
95
95
|
make a plain `#eval` fail after every one of its writes has landed.
|
|
96
|
+
`#eval_primitive` sits between the two: it returns the completion value only
|
|
97
|
+
where it is a primitive, and nil for an object or a symbol, which it never reads.
|
|
96
98
|
|
|
97
99
|
ES modules (the embedder owns the URL→module registry):
|
|
98
100
|
|
|
@@ -114,6 +116,12 @@ loop here (see below), a `true` graph whose `await` never settles returns from
|
|
|
114
116
|
`evaluate` with the module body still suspended, and `status` reports
|
|
115
117
|
`:evaluated` either way — so `graph_async?` is what distinguishes the two.
|
|
116
118
|
|
|
119
|
+
A rejected evaluation — a top-level throw, or a top-level `await` that rejects —
|
|
120
|
+
raises from `evaluate`. Pass `report:` the name of a function on the context's
|
|
121
|
+
global to have it called with the reason instead (HTML's "report the
|
|
122
|
+
exception"), at once for a throw and as it settles for an `await`:
|
|
123
|
+
`app.evaluate(report: "reportError")`.
|
|
124
|
+
|
|
117
125
|
Classic `<script>`s work the same way: `ctx.compile("1 + 1").run` # => 2.
|
|
118
126
|
|
|
119
127
|
### Resource limits
|
|
@@ -203,6 +211,12 @@ Also available:
|
|
|
203
211
|
`microtasks: :auto` also drains at the end of each outermost eval/call/evaluate;
|
|
204
212
|
`microtasks: :explicit` leaves it fully manual. There is no event loop or timers
|
|
205
213
|
either way.
|
|
214
|
+
- **`Isolate#pump_message_loop`** — runs the tasks V8 posts instead of running
|
|
215
|
+
inline, most visibly `FinalizationRegistry` cleanup callbacks: a microtask
|
|
216
|
+
checkpoint never runs them, so without a pump a collected target's callback
|
|
217
|
+
never fires. It drains every pending task without blocking, then a microtask
|
|
218
|
+
checkpoint; with nothing pending it is a no-op. The callbacks are ordinary JS,
|
|
219
|
+
so the pump is capped by `timeout_ms` like an `eval`.
|
|
206
220
|
- **`Isolate#terminate`**, **`Isolate#dynamic_import_resolver=`**,
|
|
207
221
|
**`Context#reset`** (below), and **`Platform.set_flags!`**.
|
|
208
222
|
|
|
@@ -218,7 +232,9 @@ warm isolate — a per-visit reset that avoids rebuilding the VM. Its contract:
|
|
|
218
232
|
- **Runtime mutations are dropped.** Anything set on the realm at runtime is gone.
|
|
219
233
|
- **Host fns are dropped.** Functions `attach`/`attach_many`'d into the realm are
|
|
220
234
|
released — the isolate stops holding them, so whatever they captured becomes
|
|
221
|
-
collectable; re-attach them after a reset.
|
|
235
|
+
collectable; re-attach them after a reset. One the embedder kept hold of (through
|
|
236
|
+
another realm, say) throws `unknown host function` when called; it never runs
|
|
237
|
+
what was attached after it.
|
|
222
238
|
- **Modules and classic scripts are dropped.** Handles compiled in the realm die
|
|
223
239
|
with the old context.
|
|
224
240
|
- **The realm id and the shared same-origin token are preserved** — the id keeps
|
data/ext/rusty_racer/Cargo.toml
CHANGED
data/ext/rusty_racer/src/lib.rs
CHANGED
|
@@ -59,7 +59,7 @@ use std::cell::RefCell;
|
|
|
59
59
|
use std::collections::HashMap;
|
|
60
60
|
use std::ffi::c_void;
|
|
61
61
|
use std::ptr::null_mut;
|
|
62
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
62
|
+
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
63
63
|
use std::sync::{Arc, Mutex, Once, Weak};
|
|
64
64
|
|
|
65
65
|
use magnus::block::Proc;
|
|
@@ -72,7 +72,7 @@ use magnus::{
|
|
|
72
72
|
mod marshal;
|
|
73
73
|
use marshal::{JsVal, js_to_jsval, jsval_to_js, jsval_to_ruby, ruby_to_jsval};
|
|
74
74
|
mod ops;
|
|
75
|
-
use ops::{Compiled, Request, VmReply, run_source, service_request};
|
|
75
|
+
use ops::{Compiled, Completion, Request, VmReply, run_source, service_request};
|
|
76
76
|
mod stack;
|
|
77
77
|
use stack::{STACK_DEBUG, StackScope, current_real_isolate, discover_scan_start_field};
|
|
78
78
|
mod watchdog;
|
|
@@ -146,23 +146,58 @@ macro_rules! istate {
|
|
|
146
146
|
pub(crate) use istate;
|
|
147
147
|
|
|
148
148
|
// One attach()'d host fn: the realm it was attached into — so resetting or
|
|
149
|
-
// disposing that realm can release it — whether it is still attached
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
149
|
+
// disposing that realm can release it — whether it is still attached, which
|
|
150
|
+
// occupant of the slot it is (`HostFnKey`), and the name it was attached under,
|
|
151
|
+
// kept only so a diagnostic can say which host function is involved (one String
|
|
152
|
+
// per attach, never per call). The PROC itself lives in the roots array at
|
|
153
|
+
// `proc_root_index(index)`, where the GC can see it without taking this lock.
|
|
154
154
|
struct ProcSlot {
|
|
155
155
|
context_id: i32,
|
|
156
156
|
live: bool,
|
|
157
|
+
generation: u32,
|
|
157
158
|
name: String,
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
//
|
|
161
|
+
// What a host function's V8 External carries: its slot, and the slot's generation
|
|
162
|
+
// when it was attached. A slot is reused once its realm is released, but a function
|
|
163
|
+
// of that realm can outlive the release — the embedder may still hold its document
|
|
164
|
+
// — so a call has to prove it is the slot's CURRENT occupant, or it would run
|
|
165
|
+
// whichever proc the slot was handed to next.
|
|
166
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
167
|
+
pub(crate) struct HostFnKey {
|
|
168
|
+
index: u32,
|
|
169
|
+
generation: u32,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
impl HostFnKey {
|
|
173
|
+
// (…both halves in one pointer-sized word: V8 builds for 64-bit targets alone.
|
|
174
|
+
// The word is stored as is only while V8's sandbox is off — the v8 crate's
|
|
175
|
+
// opt-in `v8_enable_sandbox`, which this crate leaves off; a sandboxed External
|
|
176
|
+
// goes through a pointer table that keeps tags in the high bits.)
|
|
177
|
+
pub(crate) fn to_external(self) -> *mut c_void {
|
|
178
|
+
const { assert!(usize::BITS >= 64) };
|
|
179
|
+
(((self.generation as u64) << 32) | self.index as u64) as usize as *mut c_void
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn from_external(value: *mut c_void) -> Self {
|
|
183
|
+
let value = value as usize as u64;
|
|
184
|
+
Self {
|
|
185
|
+
index: value as u32,
|
|
186
|
+
generation: (value >> 32) as u32,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fn index(self) -> usize {
|
|
191
|
+
self.index as usize
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The isolate's host-fn registry, indexed by HostFnKey::index. `free` lists slots
|
|
161
196
|
// emptied by release_context_procs so a later attach reuses them instead of
|
|
162
197
|
// growing `slots` forever — a long-lived process that re-navigates iframe
|
|
163
|
-
// realms would otherwise leak one slot per attach.
|
|
164
|
-
//
|
|
165
|
-
//
|
|
198
|
+
// realms would otherwise leak one slot per attach. A reused slot's generation
|
|
199
|
+
// moves on, so a function of the released realm that is still reachable calls
|
|
200
|
+
// a key the table no longer answers (`current`) instead of the new occupant.
|
|
166
201
|
#[derive(Default)]
|
|
167
202
|
struct ProcTable {
|
|
168
203
|
slots: Vec<ProcSlot>,
|
|
@@ -170,17 +205,39 @@ struct ProcTable {
|
|
|
170
205
|
}
|
|
171
206
|
|
|
172
207
|
impl ProcTable {
|
|
173
|
-
// Take a free slot if one exists,
|
|
174
|
-
fn alloc(&mut self,
|
|
175
|
-
|
|
176
|
-
self.slots[
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
208
|
+
// Take a free slot if one exists, its generation the next, else grow.
|
|
209
|
+
fn alloc(&mut self, context_id: i32, name: String) -> HostFnKey {
|
|
210
|
+
let (index, generation) = match self.free.pop() {
|
|
211
|
+
Some(index) => (index, self.slots[index].generation.wrapping_add(1)),
|
|
212
|
+
None => {
|
|
213
|
+
self.slots.push(ProcSlot {
|
|
214
|
+
context_id,
|
|
215
|
+
live: false,
|
|
216
|
+
generation: 0,
|
|
217
|
+
name: String::new(),
|
|
218
|
+
});
|
|
219
|
+
(self.slots.len() - 1, 0)
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
self.slots[index] = ProcSlot {
|
|
223
|
+
context_id,
|
|
224
|
+
live: true,
|
|
225
|
+
generation,
|
|
226
|
+
name,
|
|
227
|
+
};
|
|
228
|
+
HostFnKey {
|
|
229
|
+
index: index as u32,
|
|
230
|
+
generation,
|
|
181
231
|
}
|
|
182
232
|
}
|
|
183
233
|
|
|
234
|
+
// Whether `key` is its slot's live occupant.
|
|
235
|
+
fn current(&self, key: HostFnKey) -> bool {
|
|
236
|
+
self.slots
|
|
237
|
+
.get(key.index())
|
|
238
|
+
.is_some_and(|slot| slot.live && slot.generation == key.generation)
|
|
239
|
+
}
|
|
240
|
+
|
|
184
241
|
// Release every live proc attached into |context_id| (its realm is gone),
|
|
185
242
|
// returning their ids so the caller can clear their roots-array entries (a Ruby
|
|
186
243
|
// write, which this table never does: it is locked on paths a GC must not have to
|
|
@@ -202,11 +259,11 @@ impl ProcTable {
|
|
|
202
259
|
// long as a wrapper of it is reachable, in one place the GC can be shown without
|
|
203
260
|
// taking any Rust lock (a mark that had to lock `procs` could deadlock against
|
|
204
261
|
// the thread whose allocation triggered the GC while holding it). Slot 0 is the
|
|
205
|
-
// dynamic-import resolver; the attached host fns follow, one per
|
|
262
|
+
// dynamic-import resolver; the attached host fns follow, one per proc slot.
|
|
206
263
|
const ROOT_IMPORT_RESOLVER: isize = 0;
|
|
207
264
|
const ROOT_FIRST_PROC: isize = 1;
|
|
208
|
-
fn proc_root_index(
|
|
209
|
-
ROOT_FIRST_PROC +
|
|
265
|
+
fn proc_root_index(index: usize) -> isize {
|
|
266
|
+
ROOT_FIRST_PROC + index as isize
|
|
210
267
|
}
|
|
211
268
|
|
|
212
269
|
// Look up a RustyRacer::<name> exception class at raise time. The classes are
|
|
@@ -221,13 +278,18 @@ fn err_class(ruby: &Ruby, name: &str) -> ExceptionClass {
|
|
|
221
278
|
|
|
222
279
|
#[derive(Debug)]
|
|
223
280
|
enum VmError {
|
|
224
|
-
|
|
281
|
+
// compile-time failure -> RustyRacer::ParseError; the SyntaxError V8 made of
|
|
282
|
+
// it kept as for a JsError (`keep_thrown`)
|
|
283
|
+
Parse(String, Option<u64>),
|
|
225
284
|
Runtime(String), // internal failure (no JS stack) -> RustyRacer::RuntimeError
|
|
226
285
|
// A thrown JS exception: its message plus the JS stack frames, which become
|
|
227
|
-
// the Ruby exception's backtrace -> RustyRacer::RuntimeError
|
|
286
|
+
// the Ruby exception's backtrace -> RustyRacer::RuntimeError — and the value
|
|
287
|
+
// thrown, kept (`keep_thrown`), so a host fn the error propagates out of
|
|
288
|
+
// throws it on.
|
|
228
289
|
JsError {
|
|
229
290
|
message: String,
|
|
230
291
|
backtrace: Vec<String>,
|
|
292
|
+
thrown: Option<u64>,
|
|
231
293
|
},
|
|
232
294
|
Terminated, // watchdog/stop -> RustyRacer::ScriptTerminatedError
|
|
233
295
|
OutOfMemory, // memory_limit hit -> RustyRacer::V8OutOfMemoryError
|
|
@@ -330,9 +392,9 @@ where
|
|
|
330
392
|
// instead of saying "a host function". Rendered only on that path, so an
|
|
331
393
|
// ordinary callback pays for a pointer and nothing else.
|
|
332
394
|
enum RubyCall<'a> {
|
|
333
|
-
// An attach()'d host function, by
|
|
395
|
+
// An attach()'d host function, by its key — the name it was attached
|
|
334
396
|
// under lives in the isolate's proc table.
|
|
335
|
-
HostFn(
|
|
397
|
+
HostFn(HostFnKey),
|
|
336
398
|
// A module resolver (a static instantiate block, a dynamic auto-link, or
|
|
337
399
|
// import()), named by the specifier it was handed.
|
|
338
400
|
Resolver(&'a str),
|
|
@@ -341,15 +403,17 @@ enum RubyCall<'a> {
|
|
|
341
403
|
impl RubyCall<'_> {
|
|
342
404
|
fn describe(&self, core: &Core) -> String {
|
|
343
405
|
match self {
|
|
344
|
-
// try_lock, not lock: nothing holds this across a callback (
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
406
|
+
// try_lock, not lock: nothing holds this across a callback (`call_proc`
|
|
407
|
+
// holds it for one comparison, before the proc runs), but blocking here
|
|
408
|
+
// would turn a diagnostic into a hang. A slot handed on since names
|
|
409
|
+
// nobody: its name is another function's.
|
|
410
|
+
Self::HostFn(id) => match core.procs.try_lock().ok().and_then(|procs| {
|
|
411
|
+
procs
|
|
412
|
+
.slots
|
|
413
|
+
.get(id.index())
|
|
414
|
+
.filter(|slot| slot.generation == id.generation)
|
|
415
|
+
.map(|slot| slot.name.clone())
|
|
416
|
+
}) {
|
|
353
417
|
Some(name) => format!("The host function `{name}`"),
|
|
354
418
|
None => "A host function".to_string(),
|
|
355
419
|
},
|
|
@@ -546,8 +610,8 @@ fn host_fn_callback(
|
|
|
546
610
|
args: v8::FunctionCallbackArguments<'_>,
|
|
547
611
|
mut rv: v8::ReturnValue<'_, v8::Value>,
|
|
548
612
|
) {
|
|
549
|
-
let
|
|
550
|
-
Ok(e) => e.value()
|
|
613
|
+
let host_fn = match v8::Local::<v8::External>::try_from(args.data()) {
|
|
614
|
+
Ok(e) => HostFnKey::from_external(e.value()),
|
|
551
615
|
Err(_) => return,
|
|
552
616
|
};
|
|
553
617
|
let mut js_args = Vec::with_capacity(args.length() as usize);
|
|
@@ -574,20 +638,22 @@ fn host_fn_callback(
|
|
|
574
638
|
return;
|
|
575
639
|
}
|
|
576
640
|
let core = unsafe { &*core_ptr };
|
|
577
|
-
let result: Result<JsVal,
|
|
641
|
+
let result: Result<JsVal, HostFnFailure> = with_gvl(core, RubyCall::HostFn(host_fn), || {
|
|
578
642
|
let ruby = Ruby::get().unwrap();
|
|
579
643
|
// rb_protect so a Ruby exception raised during arg/return marshalling
|
|
580
644
|
// (ary_new_capa / jsval_to_ruby / ruby_to_jsval can raise on OOM etc.)
|
|
581
645
|
// is CAUGHT here instead of longjmp-ing through V8's C++ frames. The
|
|
582
646
|
// proc's own raise is already a magnus Err; this covers the rest.
|
|
583
647
|
use magnus::rb_sys::AsRawValue;
|
|
584
|
-
let mut out: Option<Result<JsVal,
|
|
648
|
+
let mut out: Option<Result<JsVal, HostFnFailure>> = None;
|
|
585
649
|
match magnus::rb_sys::protect(|| {
|
|
586
|
-
out = Some(core.call_proc(&ruby,
|
|
650
|
+
out = Some(core.call_proc(&ruby, host_fn, &js_args));
|
|
587
651
|
ruby.qnil().as_raw()
|
|
588
652
|
}) {
|
|
589
|
-
Ok(_) =>
|
|
590
|
-
|
|
653
|
+
Ok(_) => {
|
|
654
|
+
out.unwrap_or_else(|| Err("host function did not complete".to_string().into()))
|
|
655
|
+
}
|
|
656
|
+
Err(e) => Err(format!("{e}").into()),
|
|
591
657
|
}
|
|
592
658
|
});
|
|
593
659
|
match result {
|
|
@@ -595,8 +661,30 @@ fn host_fn_callback(
|
|
|
595
661
|
let v = jsval_to_js(scope, val);
|
|
596
662
|
rv.set(v);
|
|
597
663
|
}
|
|
598
|
-
// The proc raised (or marshalling failed): surface as a JS exception
|
|
599
|
-
|
|
664
|
+
// The proc raised (or marshalling failed): surface as a JS exception —
|
|
665
|
+
// the value JS threw, where the error is one a JS throw became.
|
|
666
|
+
Err(failure) => match failure.thrown.and_then(|serial| take_thrown(scope, serial)) {
|
|
667
|
+
Some(thrown) => {
|
|
668
|
+
scope.throw_exception(thrown);
|
|
669
|
+
}
|
|
670
|
+
None => throw_js_error(scope, &failure.message),
|
|
671
|
+
},
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// How a host fn failed: the Ruby error's text, and the serial of the JS value
|
|
676
|
+
// it carries when a JS throw is what it became (`keep_thrown`).
|
|
677
|
+
struct HostFnFailure {
|
|
678
|
+
message: String,
|
|
679
|
+
thrown: Option<u64>,
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
impl From<String> for HostFnFailure {
|
|
683
|
+
fn from(message: String) -> Self {
|
|
684
|
+
HostFnFailure {
|
|
685
|
+
message,
|
|
686
|
+
thrown: None,
|
|
687
|
+
}
|
|
600
688
|
}
|
|
601
689
|
}
|
|
602
690
|
|
|
@@ -696,7 +784,87 @@ fn capture_js_error(
|
|
|
696
784
|
let backtrace = stack_str
|
|
697
785
|
.map(|s| parse_js_stack(s.as_str()))
|
|
698
786
|
.unwrap_or_default();
|
|
699
|
-
|
|
787
|
+
let thrown = keep_thrown(scope, exception);
|
|
788
|
+
VmError::JsError {
|
|
789
|
+
message,
|
|
790
|
+
backtrace,
|
|
791
|
+
thrown,
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// A value JS failed with that no TryCatch caught — an errored module's
|
|
796
|
+
// exception, a rejected promise's reason — as a JsError: its text, no stack
|
|
797
|
+
// frames, and the value kept as a thrown one is.
|
|
798
|
+
fn rejected_with(scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>) -> VmError {
|
|
799
|
+
VmError::JsError {
|
|
800
|
+
message: value.to_rust_string_lossy(scope),
|
|
801
|
+
backtrace: vec![],
|
|
802
|
+
thrown: keep_thrown(scope, Some(value)),
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// A compile failure as a ParseError: V8's message, the location it recorded
|
|
807
|
+
// (always the file, the line when it has one), and the SyntaxError kept as a
|
|
808
|
+
// thrown value is.
|
|
809
|
+
fn parse_error(
|
|
810
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
811
|
+
exception: Option<v8::Local<v8::Value>>,
|
|
812
|
+
message: Option<v8::Local<v8::Message>>,
|
|
813
|
+
filename: &str,
|
|
814
|
+
fallback: &str,
|
|
815
|
+
) -> VmError {
|
|
816
|
+
let msg = exception
|
|
817
|
+
.map(|e| e.to_rust_string_lossy(scope))
|
|
818
|
+
.unwrap_or_else(|| fallback.to_string());
|
|
819
|
+
let res = message
|
|
820
|
+
.and_then(|m| m.get_script_resource_name(scope))
|
|
821
|
+
.filter(|v| v.is_string())
|
|
822
|
+
.map(|v| v.to_rust_string_lossy(scope))
|
|
823
|
+
.unwrap_or_else(|| filename.to_string());
|
|
824
|
+
let loc = match message.and_then(|m| m.get_line_number(scope)) {
|
|
825
|
+
Some(line) => format!(" at {res}:{line}"),
|
|
826
|
+
None => format!(" at {res}"),
|
|
827
|
+
};
|
|
828
|
+
VmError::Parse(format!("{msg}{loc}"), keep_thrown(scope, exception))
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Keep the value JS threw, so a host fn that lets the Ruby error it became
|
|
832
|
+
// propagate throws THAT value on — its identity, class and stack — rather than a
|
|
833
|
+
// fresh Error made of its text: a script a host fn ran for JS fails in JS as the
|
|
834
|
+
// script did. One slot per isolate, since only the latest thrown value can still
|
|
835
|
+
// be on its way back out, and only until the outermost op returns (`Core::run`),
|
|
836
|
+
// since only a host fn inside it can still ask — kept longer, it would pin the
|
|
837
|
+
// value, and its realm, for as long as nothing else threw. The serial (carried
|
|
838
|
+
// by the Ruby exception, see `carrying_thrown`) tells it from a stale one, and is
|
|
839
|
+
// drawn process-wide, so an error another isolate's op raised never matches this
|
|
840
|
+
// one's slot: its value cannot cross isolates. None on an isolate with no state
|
|
841
|
+
// (the snapshot creator's), where no host fn can ask.
|
|
842
|
+
fn keep_thrown(
|
|
843
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
844
|
+
exception: Option<v8::Local<v8::Value>>,
|
|
845
|
+
) -> Option<u64> {
|
|
846
|
+
static SERIAL: AtomicU64 = AtomicU64::new(0);
|
|
847
|
+
let exception = exception?;
|
|
848
|
+
let kept = v8::Global::new(scope, exception);
|
|
849
|
+
let state = scope.get_slot_mut::<IsolateState>()?;
|
|
850
|
+
let serial = SERIAL.fetch_add(1, Ordering::Relaxed) + 1;
|
|
851
|
+
state.thrown = Some((serial, kept));
|
|
852
|
+
Some(serial)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// The Ruby exception's kept thrown value (`keep_thrown`), when it still is the
|
|
856
|
+
// latest: taken, as it is thrown once.
|
|
857
|
+
fn take_thrown<'s>(
|
|
858
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
859
|
+
serial: u64,
|
|
860
|
+
) -> Option<v8::Local<'s, v8::Value>> {
|
|
861
|
+
let state = scope.get_slot_mut::<IsolateState>()?;
|
|
862
|
+
let (kept_serial, _) = state.thrown.as_ref()?;
|
|
863
|
+
if *kept_serial != serial {
|
|
864
|
+
return None;
|
|
865
|
+
}
|
|
866
|
+
let (_, kept) = state.thrown.take()?;
|
|
867
|
+
Some(v8::Local::new(scope, kept))
|
|
700
868
|
}
|
|
701
869
|
|
|
702
870
|
// ---------------------------------------------------------------------------
|
|
@@ -864,6 +1032,8 @@ struct IsolateState {
|
|
|
864
1032
|
// the ScriptTerminatedError can name what was running. Taken (cleared) when the
|
|
865
1033
|
// terminated op's error is built; None for any non-timeout outcome.
|
|
866
1034
|
timeout_backtrace: Option<Vec<String>>,
|
|
1035
|
+
// The latest value JS threw out of an op, with its serial (`keep_thrown`).
|
|
1036
|
+
thrown: Option<(u64, v8::Global<v8::Value>)>,
|
|
867
1037
|
}
|
|
868
1038
|
|
|
869
1039
|
impl IsolateState {
|
|
@@ -888,6 +1058,7 @@ impl IsolateState {
|
|
|
888
1058
|
oom_fired: false,
|
|
889
1059
|
oom_initial_limit: 0,
|
|
890
1060
|
timeout_backtrace: None,
|
|
1061
|
+
thrown: None,
|
|
891
1062
|
}
|
|
892
1063
|
}
|
|
893
1064
|
}
|
|
@@ -973,7 +1144,7 @@ fn transfer_registry() -> &'static Mutex<HashMap<u64, SendBackingStore>> {
|
|
|
973
1144
|
// Monotonic token source. Starts at 1 so 0 is free as the JS-side "not
|
|
974
1145
|
// transferable — fall back to a copy" sentinel. A token is f64-exact for ~2^53
|
|
975
1146
|
// transfers, which a process will never reach.
|
|
976
|
-
static NEXT_TRANSFER_TOKEN:
|
|
1147
|
+
static NEXT_TRANSFER_TOKEN: AtomicU64 = AtomicU64::new(1);
|
|
977
1148
|
|
|
978
1149
|
// Drain EVERY realm's microtask queue once. Each realm has its own queue (see
|
|
979
1150
|
// V8State::queues), so a single scope.perform_microtask_checkpoint() — which only
|
|
@@ -1534,7 +1705,7 @@ struct Core {
|
|
|
1534
1705
|
// layout. Read on the V8 side without the GVL (the import hooks), which is
|
|
1535
1706
|
// sound because the entries are pinned and nothing here allocates or raises.
|
|
1536
1707
|
roots: Opaque<RArray>,
|
|
1537
|
-
//
|
|
1708
|
+
// A HostFnKey indexes ProcTable.slots — bookkeeping only (the proc itself is in
|
|
1538
1709
|
// `roots`). Mutex (uncontended — single owner thread) so host_fn_callback can
|
|
1539
1710
|
// reach it through Core (via the slot's core_ptr) while a &Core method also
|
|
1540
1711
|
// holds it. reset/dispose releases the slots and recycles them.
|
|
@@ -2253,11 +2424,11 @@ fn build_snapshot(code: &str, base: Option<Vec<u8>>, warmup: bool) -> Result<Vec
|
|
|
2253
2424
|
// leaves behind); nothing reads its completion value, so
|
|
2254
2425
|
// don't marshal it — a bundle ending in an expression must
|
|
2255
2426
|
// not fail the snapshot over a value no one wants.
|
|
2256
|
-
|
|
2427
|
+
Completion::Void,
|
|
2257
2428
|
)
|
|
2258
2429
|
{
|
|
2259
2430
|
err = Some(match e {
|
|
2260
|
-
VmError::Parse(m) | VmError::Runtime(m) => m,
|
|
2431
|
+
VmError::Parse(m, _) | VmError::Runtime(m) => m,
|
|
2261
2432
|
VmError::JsError { message, .. } => message,
|
|
2262
2433
|
VmError::Terminated => "snapshot code was terminated".to_string(),
|
|
2263
2434
|
// Unreachable: the snapshot-creator is a separate isolate
|
|
@@ -2641,6 +2812,9 @@ impl Core {
|
|
|
2641
2812
|
// on an op.
|
|
2642
2813
|
if reply.is_none() {
|
|
2643
2814
|
self.shared.lock().unwrap().disposed = true;
|
|
2815
|
+
} else if depth == 0 {
|
|
2816
|
+
// No host fn is left to throw a kept value on (`keep_thrown`).
|
|
2817
|
+
istate!(unsafe { &mut *iso }).thrown = None;
|
|
2644
2818
|
}
|
|
2645
2819
|
self.depth.fetch_sub(1, Ordering::SeqCst);
|
|
2646
2820
|
reply
|
|
@@ -2741,15 +2915,24 @@ impl Core {
|
|
|
2741
2915
|
}
|
|
2742
2916
|
}
|
|
2743
2917
|
|
|
2744
|
-
fn call_proc(
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2918
|
+
fn call_proc(
|
|
2919
|
+
&self,
|
|
2920
|
+
ruby: &Ruby,
|
|
2921
|
+
host_fn: HostFnKey,
|
|
2922
|
+
args: &[JsVal],
|
|
2923
|
+
) -> Result<JsVal, HostFnFailure> {
|
|
2924
|
+
// A function of a released realm is refused here, before its slot's roots
|
|
2925
|
+
// entry is read: that entry may by now hold another realm's proc. (The lock
|
|
2926
|
+
// is uncontended and held for the comparison alone — nothing here allocates,
|
|
2927
|
+
// so no GC can need it meanwhile.)
|
|
2928
|
+
if !self.procs.lock().unwrap().current(host_fn) {
|
|
2929
|
+
return Err("unknown host function".to_string().into());
|
|
2930
|
+
}
|
|
2931
|
+
// `root_proc` and not `RArray::entry`, for the reason given there: a nil
|
|
2932
|
+
// slot must answer None, never dispatch `to_proc` and raise.
|
|
2750
2933
|
let proc = self
|
|
2751
|
-
.root_proc(proc_root_index(
|
|
2752
|
-
.
|
|
2934
|
+
.root_proc(proc_root_index(host_fn.index()))
|
|
2935
|
+
.ok_or_else(|| "unknown host function".to_string())?;
|
|
2753
2936
|
// Marshal into a Ruby Array, NOT a Vec<Value>: bare Values in a heap Vec
|
|
2754
2937
|
// are hidden from Ruby's GC mark phase (magnus's own RArray::to_vec doc
|
|
2755
2938
|
// spells this out). With several args, once arg N is parked in the Vec
|
|
@@ -2769,8 +2952,11 @@ impl Core {
|
|
|
2769
2952
|
// op the proc issues re-enters Core::run (depth > 0) directly — no nested
|
|
2770
2953
|
// frame bookkeeping is needed any more (the call stack IS the nesting).
|
|
2771
2954
|
let result: Result<Value, Error> = proc.call(unsafe { ruby_args.as_slice() });
|
|
2772
|
-
let value = result.map_err(|e|
|
|
2773
|
-
|
|
2955
|
+
let value = result.map_err(|e| HostFnFailure {
|
|
2956
|
+
message: e.to_string(),
|
|
2957
|
+
thrown: thrown_serial_of(&e),
|
|
2958
|
+
})?;
|
|
2959
|
+
ruby_to_jsval(value).map_err(|e| e.to_string().into())
|
|
2774
2960
|
}
|
|
2775
2961
|
|
|
2776
2962
|
// Context#call (and call_void). Resolves a dotted function path
|
|
@@ -2880,7 +3066,7 @@ impl Core {
|
|
|
2880
3066
|
source: String,
|
|
2881
3067
|
filename: String,
|
|
2882
3068
|
timeout_ms: u64,
|
|
2883
|
-
|
|
3069
|
+
completion: Completion,
|
|
2884
3070
|
) -> Result<Value, Error> {
|
|
2885
3071
|
let reply = self.run(
|
|
2886
3072
|
ruby,
|
|
@@ -2889,7 +3075,7 @@ impl Core {
|
|
|
2889
3075
|
source,
|
|
2890
3076
|
filename,
|
|
2891
3077
|
timeout_ms,
|
|
2892
|
-
|
|
3078
|
+
completion,
|
|
2893
3079
|
},
|
|
2894
3080
|
)?;
|
|
2895
3081
|
self.reply_value(ruby, reply)
|
|
@@ -2906,18 +3092,14 @@ impl Core {
|
|
|
2906
3092
|
// array's element storage, which the import hooks read with the GVL released —
|
|
2907
3093
|
// and a refused attach must not consume a slot or leave its proc rooted either.
|
|
2908
3094
|
self.ensure_owner_and_live(ruby)?;
|
|
2909
|
-
let
|
|
2910
|
-
|
|
2911
|
-
live: true,
|
|
2912
|
-
name: name.clone(),
|
|
2913
|
-
});
|
|
2914
|
-
self.root_store(ruby, proc_root_index(host_fn_id), proc)?;
|
|
3095
|
+
let host_fn = self.procs.lock().unwrap().alloc(context_id, name.clone());
|
|
3096
|
+
self.root_store(ruby, proc_root_index(host_fn.index()), proc)?;
|
|
2915
3097
|
let reply = self.run(
|
|
2916
3098
|
ruby,
|
|
2917
3099
|
Request::Attach {
|
|
2918
3100
|
context_id,
|
|
2919
3101
|
name,
|
|
2920
|
-
|
|
3102
|
+
host_fn,
|
|
2921
3103
|
timeout_ms: self.default_timeout_ms,
|
|
2922
3104
|
},
|
|
2923
3105
|
)?;
|
|
@@ -2926,7 +3108,7 @@ impl Core {
|
|
|
2926
3108
|
|
|
2927
3109
|
// attach_many: install several host fns in ONE round-trip to the V8 thread
|
|
2928
3110
|
// (a fresh realm needs ~dozens; one rendezvous instead of one per fn). Slots
|
|
2929
|
-
// are allocated up front so each carries a stable
|
|
3111
|
+
// are allocated up front so each carries a stable key; entries are
|
|
2930
3112
|
// applied IN ORDER and a build/attach failure aborts the batch with that
|
|
2931
3113
|
// (name-tagged) error WITHOUT rolling back earlier entries (see the
|
|
2932
3114
|
// AttachMany arm). On that error path the unused slots are reclaimed at the
|
|
@@ -2941,18 +3123,11 @@ impl Core {
|
|
|
2941
3123
|
return Ok(ruby.qnil().as_value()); // nothing to install, skip the round-trip
|
|
2942
3124
|
}
|
|
2943
3125
|
self.ensure_owner_and_live(ruby)?; // before the roots writes — see `attach`
|
|
2944
|
-
let allocated: Vec<(String,
|
|
3126
|
+
let allocated: Vec<(String, HostFnKey, Proc)> = {
|
|
2945
3127
|
let mut procs = self.procs.lock().unwrap();
|
|
2946
3128
|
entries
|
|
2947
3129
|
.into_iter()
|
|
2948
|
-
.map(|(name, proc)|
|
|
2949
|
-
let id = procs.alloc(ProcSlot {
|
|
2950
|
-
context_id,
|
|
2951
|
-
live: true,
|
|
2952
|
-
name: name.clone(),
|
|
2953
|
-
});
|
|
2954
|
-
(name, id, proc)
|
|
2955
|
-
})
|
|
3130
|
+
.map(|(name, proc)| (name.clone(), procs.alloc(context_id, name), proc))
|
|
2956
3131
|
.collect()
|
|
2957
3132
|
};
|
|
2958
3133
|
// Stored OUTSIDE the table's lock: storing into the array can allocate (the
|
|
@@ -2960,16 +3135,16 @@ impl Core {
|
|
|
2960
3135
|
// wrappers, a path that must never wait on `procs`. The Procs parked in the
|
|
2961
3136
|
// Vec meanwhile are invisible to the GC (see call_proc's note), but the caller's
|
|
2962
3137
|
// `table: RHash` argument is live in its own frame and marks every one of them.
|
|
2963
|
-
let mut
|
|
2964
|
-
for (name,
|
|
2965
|
-
self.root_store(ruby, proc_root_index(
|
|
2966
|
-
|
|
3138
|
+
let mut named_keys: Vec<(String, HostFnKey)> = Vec::with_capacity(allocated.len());
|
|
3139
|
+
for (name, key, proc) in allocated {
|
|
3140
|
+
self.root_store(ruby, proc_root_index(key.index()), proc)?;
|
|
3141
|
+
named_keys.push((name, key));
|
|
2967
3142
|
}
|
|
2968
3143
|
let reply = self.run(
|
|
2969
3144
|
ruby,
|
|
2970
3145
|
Request::AttachMany {
|
|
2971
3146
|
context_id,
|
|
2972
|
-
entries:
|
|
3147
|
+
entries: named_keys,
|
|
2973
3148
|
timeout_ms: self.default_timeout_ms,
|
|
2974
3149
|
},
|
|
2975
3150
|
)?;
|
|
@@ -2977,11 +3152,12 @@ impl Core {
|
|
|
2977
3152
|
}
|
|
2978
3153
|
|
|
2979
3154
|
// Drop the procs attached into |context_id| out of the roots array — its
|
|
2980
|
-
// realm is gone (reset or disposed),
|
|
2981
|
-
//
|
|
2982
|
-
//
|
|
2983
|
-
//
|
|
2984
|
-
// the
|
|
3155
|
+
// realm is gone (reset or disposed), and holding them would keep the embedder's
|
|
3156
|
+
// world alive for the rest of the isolate's life. A V8 function of that realm
|
|
3157
|
+
// may still be reachable (the embedder can hold the old global), but its key
|
|
3158
|
+
// stops answering (`ProcTable::current`), so it never calls a proc again.
|
|
3159
|
+
// Runs on a Ruby thread: it writes nil into the Array. The slots stay: the
|
|
3160
|
+
// keys of other realms are indices into the same Vec.
|
|
2985
3161
|
fn release_context_procs(&self, ruby: &Ruby, context_id: i32) {
|
|
2986
3162
|
// Nil the entries BEFORE the ids go back on the free list, so no attach can
|
|
2987
3163
|
// ever be handed an id whose old proc is still in the array.
|
|
@@ -3094,12 +3270,18 @@ impl Core {
|
|
|
3094
3270
|
self.reply_value(ruby, reply?)
|
|
3095
3271
|
}
|
|
3096
3272
|
|
|
3097
|
-
fn evaluate_module(
|
|
3273
|
+
fn evaluate_module(
|
|
3274
|
+
&self,
|
|
3275
|
+
ruby: &Ruby,
|
|
3276
|
+
module_id: i32,
|
|
3277
|
+
report: Option<String>,
|
|
3278
|
+
) -> Result<Value, Error> {
|
|
3098
3279
|
let reply = self.run(
|
|
3099
3280
|
ruby,
|
|
3100
3281
|
Request::EvaluateModule {
|
|
3101
3282
|
module_id,
|
|
3102
3283
|
timeout_ms: self.default_timeout_ms,
|
|
3284
|
+
report,
|
|
3103
3285
|
},
|
|
3104
3286
|
)?;
|
|
3105
3287
|
self.reply_value(ruby, reply)
|
|
@@ -3443,14 +3625,14 @@ impl Context {
|
|
|
3443
3625
|
Ok(())
|
|
3444
3626
|
}
|
|
3445
3627
|
// timeout_ms 0 = use the isolate's default; an explicit value overrides it.
|
|
3446
|
-
// |
|
|
3628
|
+
// |completion| 0 marshals the completion value, 1 discards it, 2 keeps a primitive alone (`Completion`).
|
|
3447
3629
|
fn eval(
|
|
3448
3630
|
ruby: &Ruby,
|
|
3449
3631
|
rb_self: &Self,
|
|
3450
3632
|
source: String,
|
|
3451
3633
|
timeout_ms: u64,
|
|
3452
3634
|
filename: String,
|
|
3453
|
-
|
|
3635
|
+
completion: u8,
|
|
3454
3636
|
) -> Result<Value, Error> {
|
|
3455
3637
|
rb_self.check_live(ruby)?;
|
|
3456
3638
|
let timeout = if timeout_ms == 0 {
|
|
@@ -3458,9 +3640,14 @@ impl Context {
|
|
|
3458
3640
|
} else {
|
|
3459
3641
|
timeout_ms
|
|
3460
3642
|
};
|
|
3643
|
+
let completion = match completion {
|
|
3644
|
+
1 => Completion::Void,
|
|
3645
|
+
2 => Completion::Primitive,
|
|
3646
|
+
_ => Completion::Value,
|
|
3647
|
+
};
|
|
3461
3648
|
rb_self
|
|
3462
3649
|
.core
|
|
3463
|
-
.eval_t(ruby, rb_self.id, source, filename, timeout,
|
|
3650
|
+
.eval_t(ruby, rb_self.id, source, filename, timeout, completion)
|
|
3464
3651
|
}
|
|
3465
3652
|
fn call(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
|
|
3466
3653
|
rb_self.check_live(ruby)?;
|
|
@@ -3649,9 +3836,12 @@ impl JsModule {
|
|
|
3649
3836
|
.core
|
|
3650
3837
|
.instantiate_module(ruby, rb_self.module_id, resolver)
|
|
3651
3838
|
}
|
|
3652
|
-
|
|
3839
|
+
// |report|: nil, or the name of the context global that reports the evaluation's rejection (`Request::EvaluateModule`).
|
|
3840
|
+
fn evaluate(ruby: &Ruby, rb_self: &Self, report: Option<String>) -> Result<Value, Error> {
|
|
3653
3841
|
rb_self.check_live(ruby)?;
|
|
3654
|
-
rb_self
|
|
3842
|
+
rb_self
|
|
3843
|
+
.core
|
|
3844
|
+
.evaluate_module(ruby, rb_self.module_id, report)
|
|
3655
3845
|
}
|
|
3656
3846
|
fn namespace(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
3657
3847
|
rb_self.check_live(ruby)?;
|
|
@@ -3808,9 +3998,15 @@ fn binary_bytes(
|
|
|
3808
3998
|
|
|
3809
3999
|
fn vm_err(ruby: &Ruby, e: VmError) -> Error {
|
|
3810
4000
|
match e {
|
|
3811
|
-
VmError::Parse(m) =>
|
|
4001
|
+
VmError::Parse(m, thrown) => {
|
|
4002
|
+
carrying_thrown(Error::new(err_class(ruby, "ParseError"), m), thrown)
|
|
4003
|
+
}
|
|
3812
4004
|
VmError::Runtime(m) => Error::new(err_class(ruby, "RuntimeError"), m),
|
|
3813
|
-
VmError::JsError {
|
|
4005
|
+
VmError::JsError {
|
|
4006
|
+
message,
|
|
4007
|
+
backtrace,
|
|
4008
|
+
thrown,
|
|
4009
|
+
} => carrying_thrown(js_runtime_error(ruby, message, backtrace), thrown),
|
|
3814
4010
|
VmError::Terminated => Error::new(
|
|
3815
4011
|
err_class(ruby, "ScriptTerminatedError"),
|
|
3816
4012
|
"JavaScript was terminated (timeout or stop)",
|
|
@@ -3852,6 +4048,42 @@ fn terminated_error(ruby: &Ruby, js_backtrace: Option<Vec<String>>) -> Error {
|
|
|
3852
4048
|
}
|
|
3853
4049
|
}
|
|
3854
4050
|
|
|
4051
|
+
// The ivar a Ruby exception carries its thrown value's serial in (`keep_thrown`),
|
|
4052
|
+
// beside the message it was raised with.
|
|
4053
|
+
const THROWN_IVAR: &str = "@__rusty_racer_thrown";
|
|
4054
|
+
|
|
4055
|
+
// |error| as an instance carrying |thrown|'s serial, for a host fn it propagates
|
|
4056
|
+
// out of to throw the kept value on (`take_thrown`).
|
|
4057
|
+
fn carrying_thrown(error: Error, thrown: Option<u64>) -> Error {
|
|
4058
|
+
let Some(serial) = thrown else {
|
|
4059
|
+
return error;
|
|
4060
|
+
};
|
|
4061
|
+
match error_to_exception(&error) {
|
|
4062
|
+
Some(exc) => {
|
|
4063
|
+
let Ok(message) = exc.funcall::<_, _, Value>("message", ()) else {
|
|
4064
|
+
return Error::from(exc);
|
|
4065
|
+
};
|
|
4066
|
+
let _ = exc
|
|
4067
|
+
.funcall::<_, _, Value>("instance_variable_set", (THROWN_IVAR, (serial, message)));
|
|
4068
|
+
Error::from(exc)
|
|
4069
|
+
}
|
|
4070
|
+
None => error,
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
// The serial of the thrown value a host fn's raised |error| carries, if any — not
|
|
4075
|
+
// when Ruby re-raised it with a message of its own (`raise e, 'other'` copies
|
|
4076
|
+
// the ivar): that error says something the value does not.
|
|
4077
|
+
fn thrown_serial_of(error: &Error) -> Option<u64> {
|
|
4078
|
+
let exc = error_to_exception(error)?;
|
|
4079
|
+
let (serial, message): (u64, Value) = exc
|
|
4080
|
+
.funcall::<_, _, Option<(u64, Value)>>("instance_variable_get", (THROWN_IVAR,))
|
|
4081
|
+
.ok()
|
|
4082
|
+
.flatten()?;
|
|
4083
|
+
let now: Value = exc.funcall("message", ()).ok()?;
|
|
4084
|
+
now.eql(message).ok()?.then_some(serial)
|
|
4085
|
+
}
|
|
4086
|
+
|
|
3855
4087
|
// Build a RustyRacer::RuntimeError carrying the JS stack as its Ruby backtrace.
|
|
3856
4088
|
// Constructs the exception instance so we can set_backtrace before raising;
|
|
3857
4089
|
// falls back to a plain Error if any of that fails.
|
|
@@ -3961,7 +4193,7 @@ pub fn install_classes(ruby: &Ruby) -> Result<(), Error> {
|
|
|
3961
4193
|
// A v8::Context (realm): eval/call/attach/compile_module.
|
|
3962
4194
|
let context = module.define_class("Context", ruby.class_object())?;
|
|
3963
4195
|
// Backs both keyword-arg wrappers in lib: Context#eval(source, timeout_ms:,
|
|
3964
|
-
// filename:) and #
|
|
4196
|
+
// filename:), #eval_void and #eval_primitive, which differ only in the 4th arg (what they keep of the completion value).
|
|
3965
4197
|
context.define_method("_eval", method!(Context::eval, 4))?;
|
|
3966
4198
|
context.define_method("call", method!(Context::call, -1))?;
|
|
3967
4199
|
context.define_method("call_void", method!(Context::call_void, -1))?;
|
|
@@ -3996,7 +4228,7 @@ pub fn install_classes(ruby: &Ruby) -> Result<(), Error> {
|
|
|
3996
4228
|
// Thin ES-module handle: Context#compile_module -> instantiate/evaluate.
|
|
3997
4229
|
let jsmodule = module.define_class("Module", ruby.class_object())?;
|
|
3998
4230
|
jsmodule.define_method("_instantiate", method!(JsModule::instantiate, 1))?;
|
|
3999
|
-
jsmodule.define_method("
|
|
4231
|
+
jsmodule.define_method("_evaluate", method!(JsModule::evaluate, 1))?;
|
|
4000
4232
|
jsmodule.define_method("namespace", method!(JsModule::namespace, 0))?;
|
|
4001
4233
|
jsmodule.define_method("_status", method!(JsModule::status, 0))?;
|
|
4002
4234
|
jsmodule.define_method("graph_async?", method!(JsModule::graph_async, 0))?;
|
data/ext/rusty_racer/src/ops.rs
CHANGED
|
@@ -41,6 +41,17 @@ macro_rules! marshalled {
|
|
|
41
41
|
}};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// What an eval hands back of its completion value: all of it, marshalled; nothing (`Void`: a statement list run for its
|
|
45
|
+
// EFFECT still has one — the last statement's value — and marshalling it can throw, an accessor or a Proxy trap, or walk
|
|
46
|
+
// a huge graph, for a value the caller discards); or a primitive alone (`Primitive`: an object's never walked, undefined
|
|
47
|
+
// in its place — for a caller that wants a string or a number of it, as a javascript: URL's evaluation does).
|
|
48
|
+
#[derive(Clone, Copy)]
|
|
49
|
+
pub(crate) enum Completion {
|
|
50
|
+
Value,
|
|
51
|
+
Void,
|
|
52
|
+
Primitive,
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
// One VM operation, built by a magnus method and run inline by Core::run ->
|
|
45
56
|
// service_request -> dispatch_one. |context_id| selects which realm the op runs
|
|
46
57
|
// in: 0 = the main realm (Context's own globalThis, swappable by reset_realm),
|
|
@@ -51,11 +62,7 @@ pub(crate) enum Request {
|
|
|
51
62
|
source: String,
|
|
52
63
|
filename: String,
|
|
53
64
|
timeout_ms: u64,
|
|
54
|
-
|
|
55
|
-
// statement-list eval run for its EFFECT still has one — the last
|
|
56
|
-
// statement's value — and marshalling it can throw (an accessor, a
|
|
57
|
-
// Proxy trap) or walk a huge graph, for a value the caller discards.
|
|
58
|
-
void: bool,
|
|
65
|
+
completion: Completion,
|
|
59
66
|
},
|
|
60
67
|
// Resolve a dotted function path on globalThis and invoke it with marshalled
|
|
61
68
|
// args (v8::Function::call), preserving the holder as `this`. Distinct from
|
|
@@ -76,14 +83,14 @@ pub(crate) enum Request {
|
|
|
76
83
|
Attach {
|
|
77
84
|
context_id: i32,
|
|
78
85
|
name: String,
|
|
79
|
-
|
|
86
|
+
host_fn: HostFnKey,
|
|
80
87
|
timeout_ms: u64,
|
|
81
88
|
},
|
|
82
|
-
// Batch attach: install many (name,
|
|
89
|
+
// Batch attach: install many (name, key) host fns in one round-trip
|
|
83
90
|
// (a fresh realm needs ~dozens). Same semantics as Attach, applied in order.
|
|
84
91
|
AttachMany {
|
|
85
92
|
context_id: i32,
|
|
86
|
-
entries: Vec<(String,
|
|
93
|
+
entries: Vec<(String, HostFnKey)>,
|
|
87
94
|
timeout_ms: u64,
|
|
88
95
|
},
|
|
89
96
|
// reset: swap globalThis for a fresh v8::Context, reusing the same warm
|
|
@@ -123,6 +130,10 @@ pub(crate) enum Request {
|
|
|
123
130
|
EvaluateModule {
|
|
124
131
|
module_id: i32,
|
|
125
132
|
timeout_ms: u64,
|
|
133
|
+
// The name of a function on the module's context's global that reports its evaluation's rejection — HTML "run
|
|
134
|
+
// a module script": upon rejection, report the exception — now or as a top-level-await module settles; none:
|
|
135
|
+
// the rejection is the op's error, as far as the drain gets.
|
|
136
|
+
report: Option<String>,
|
|
126
137
|
},
|
|
127
138
|
ModuleNamespace {
|
|
128
139
|
module_id: i32,
|
|
@@ -234,41 +245,30 @@ pub(crate) fn run_source(
|
|
|
234
245
|
scope: &mut v8::PinScope<'_, '_>,
|
|
235
246
|
source: &str,
|
|
236
247
|
filename: &str,
|
|
237
|
-
|
|
248
|
+
completion: Completion,
|
|
238
249
|
) -> Result<JsVal, VmError> {
|
|
239
250
|
v8::tc_scope!(let tc, scope);
|
|
240
251
|
// Compile and run as distinct phases so a compile failure maps to
|
|
241
252
|
// ParseError and a thrown exception to RuntimeError (csim rescues both).
|
|
242
253
|
let Some(code) = v8::String::new(tc, source) else {
|
|
243
|
-
return Err(VmError::Parse("source too large".into()));
|
|
254
|
+
return Err(VmError::Parse("source too large".into(), None));
|
|
244
255
|
};
|
|
245
256
|
let origin = script_origin(tc, filename);
|
|
246
257
|
let script = match v8::Script::compile(tc, code, Some(&origin)) {
|
|
247
258
|
Some(script) => script,
|
|
248
259
|
None if tc.has_terminated() => return Err(VmError::Terminated),
|
|
249
260
|
None => {
|
|
250
|
-
let
|
|
251
|
-
|
|
252
|
-
.map(|e| e.to_rust_string_lossy(tc))
|
|
253
|
-
.unwrap_or_else(|| "parse error".to_string());
|
|
254
|
-
// Append the location V8 recorded; always name the file, add the
|
|
255
|
-
// line when V8 reports one.
|
|
256
|
-
let message = tc.message();
|
|
257
|
-
let res = message
|
|
258
|
-
.and_then(|m| m.get_script_resource_name(tc))
|
|
259
|
-
.filter(|v| v.is_string())
|
|
260
|
-
.map(|v| v.to_rust_string_lossy(tc))
|
|
261
|
-
.unwrap_or_else(|| filename.to_string());
|
|
262
|
-
let loc = match message.and_then(|m| m.get_line_number(tc)) {
|
|
263
|
-
Some(line) => format!(" at {res}:{line}"),
|
|
264
|
-
None => format!(" at {res}"),
|
|
265
|
-
};
|
|
266
|
-
return Err(VmError::Parse(format!("{msg}{loc}")));
|
|
261
|
+
let (exc, message) = (tc.exception(), tc.message());
|
|
262
|
+
return Err(parse_error(tc, exc, message, filename, "parse error"));
|
|
267
263
|
}
|
|
268
264
|
};
|
|
269
265
|
match script.run(tc) {
|
|
270
|
-
Some(
|
|
271
|
-
|
|
266
|
+
Some(value) => match completion {
|
|
267
|
+
Completion::Value => marshalled!(tc, value),
|
|
268
|
+
Completion::Primitive if value.is_object() || value.is_symbol() => Ok(JsVal::Undefined),
|
|
269
|
+
Completion::Primitive => marshalled!(tc, value),
|
|
270
|
+
Completion::Void => Ok(JsVal::Undefined),
|
|
271
|
+
},
|
|
272
272
|
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
273
273
|
None => {
|
|
274
274
|
let exc = tc.exception();
|
|
@@ -465,9 +465,9 @@ fn dispatch_one(
|
|
|
465
465
|
source,
|
|
466
466
|
filename,
|
|
467
467
|
timeout_ms,
|
|
468
|
-
|
|
468
|
+
completion,
|
|
469
469
|
} => op_eval(
|
|
470
|
-
scope, context_id, source, filename, timeout_ms,
|
|
470
|
+
scope, context_id, source, filename, timeout_ms, completion, outermost,
|
|
471
471
|
),
|
|
472
472
|
Request::Call {
|
|
473
473
|
context_id,
|
|
@@ -480,9 +480,9 @@ fn dispatch_one(
|
|
|
480
480
|
Request::Attach {
|
|
481
481
|
context_id,
|
|
482
482
|
name,
|
|
483
|
-
|
|
483
|
+
host_fn,
|
|
484
484
|
timeout_ms,
|
|
485
|
-
} => op_attach(scope, context_id, name,
|
|
485
|
+
} => op_attach(scope, context_id, name, host_fn, timeout_ms, outermost),
|
|
486
486
|
Request::AttachMany {
|
|
487
487
|
context_id,
|
|
488
488
|
entries,
|
|
@@ -511,7 +511,8 @@ fn dispatch_one(
|
|
|
511
511
|
Request::EvaluateModule {
|
|
512
512
|
module_id,
|
|
513
513
|
timeout_ms,
|
|
514
|
-
|
|
514
|
+
report,
|
|
515
|
+
} => op_evaluate_module(scope, module_id, timeout_ms, report, outermost),
|
|
515
516
|
Request::ModuleNamespace { module_id } => op_module_namespace(scope, module_id),
|
|
516
517
|
Request::ModuleStatus { module_id } => op_module_status(scope, module_id),
|
|
517
518
|
Request::ModuleGraphAsync { module_id } => op_module_graph_async(scope, module_id),
|
|
@@ -619,7 +620,7 @@ fn op_eval(
|
|
|
619
620
|
source: String,
|
|
620
621
|
filename: String,
|
|
621
622
|
timeout_ms: u64,
|
|
622
|
-
|
|
623
|
+
completion: Completion,
|
|
623
624
|
outermost: bool,
|
|
624
625
|
) -> VmReply {
|
|
625
626
|
let outcome = run_js_bracketed(scope, outermost, timeout_ms, "eval", |scope, outermost| {
|
|
@@ -628,7 +629,7 @@ fn op_eval(
|
|
|
628
629
|
Some(ctx) => {
|
|
629
630
|
let context = v8::Local::new(scope, &ctx);
|
|
630
631
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
631
|
-
let out = run_source(scope, &source, &filename,
|
|
632
|
+
let out = run_source(scope, &source, &filename, completion);
|
|
632
633
|
auto_drain(scope, outermost);
|
|
633
634
|
(true, out)
|
|
634
635
|
}
|
|
@@ -699,7 +700,7 @@ fn op_attach(
|
|
|
699
700
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
700
701
|
context_id: i32,
|
|
701
702
|
name: String,
|
|
702
|
-
|
|
703
|
+
host_fn: HostFnKey,
|
|
703
704
|
timeout_ms: u64,
|
|
704
705
|
outermost: bool,
|
|
705
706
|
) -> VmReply {
|
|
@@ -719,7 +720,7 @@ fn op_attach(
|
|
|
719
720
|
Some(ctx) => {
|
|
720
721
|
let context = v8::Local::new(scope, &ctx);
|
|
721
722
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
722
|
-
let external = v8::External::new(scope,
|
|
723
|
+
let external = v8::External::new(scope, host_fn.to_external());
|
|
723
724
|
let out = match v8::Function::builder(host_fn_callback)
|
|
724
725
|
.data(external.into())
|
|
725
726
|
.build(scope)
|
|
@@ -747,7 +748,7 @@ fn op_attach(
|
|
|
747
748
|
fn op_attach_many(
|
|
748
749
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
749
750
|
context_id: i32,
|
|
750
|
-
entries: Vec<(String,
|
|
751
|
+
entries: Vec<(String, HostFnKey)>,
|
|
751
752
|
timeout_ms: u64,
|
|
752
753
|
outermost: bool,
|
|
753
754
|
) -> VmReply {
|
|
@@ -769,8 +770,8 @@ fn op_attach_many(
|
|
|
769
770
|
let context = v8::Local::new(scope, &ctx);
|
|
770
771
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
771
772
|
let mut out = Ok(JsVal::Undefined);
|
|
772
|
-
for (name,
|
|
773
|
-
let external = v8::External::new(scope,
|
|
773
|
+
for (name, host_fn) in &entries {
|
|
774
|
+
let external = v8::External::new(scope, host_fn.to_external());
|
|
774
775
|
out = match v8::Function::builder(host_fn_callback)
|
|
775
776
|
.data(external.into())
|
|
776
777
|
.build(scope)
|
|
@@ -957,21 +958,14 @@ fn op_compile_module(
|
|
|
957
958
|
// A module compile failure is a parse error
|
|
958
959
|
// (compile-time), not a thrown exception.
|
|
959
960
|
None => {
|
|
960
|
-
let
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
.map(|v| v.to_rust_string_lossy(tc))
|
|
969
|
-
.unwrap_or_else(|| filename.clone());
|
|
970
|
-
let loc = match message.and_then(|m| m.get_line_number(tc)) {
|
|
971
|
-
Some(line) => format!(" at {res}:{line}"),
|
|
972
|
-
None => format!(" at {res}"),
|
|
973
|
-
};
|
|
974
|
-
Err(VmError::Parse(format!("{msg}{loc}")))
|
|
961
|
+
let (exc, message) = (tc.exception(), tc.message());
|
|
962
|
+
Err(parse_error(
|
|
963
|
+
tc,
|
|
964
|
+
exc,
|
|
965
|
+
message,
|
|
966
|
+
&filename,
|
|
967
|
+
"module parse error",
|
|
968
|
+
))
|
|
975
969
|
}
|
|
976
970
|
}
|
|
977
971
|
}
|
|
@@ -1013,10 +1007,9 @@ fn op_instantiate_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -
|
|
|
1013
1007
|
| v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
|
|
1014
1008
|
// V8 CHECK-aborts on instantiating an errored
|
|
1015
1009
|
// module; surface its exception instead.
|
|
1016
|
-
v8::ModuleStatus::Errored =>
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
}),
|
|
1010
|
+
v8::ModuleStatus::Errored => {
|
|
1011
|
+
Err(rejected_with(scope, module.get_exception()))
|
|
1012
|
+
}
|
|
1020
1013
|
_ => {
|
|
1021
1014
|
v8::tc_scope!(let tc, scope);
|
|
1022
1015
|
match module.instantiate_module(tc, resolve_imported) {
|
|
@@ -1047,6 +1040,7 @@ fn op_evaluate_module(
|
|
|
1047
1040
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
1048
1041
|
module_id: i32,
|
|
1049
1042
|
timeout_ms: u64,
|
|
1043
|
+
report: Option<String>,
|
|
1050
1044
|
outermost: bool,
|
|
1051
1045
|
) -> VmReply {
|
|
1052
1046
|
// Top-level module code (and, under :auto, the microtasks its
|
|
@@ -1081,17 +1075,25 @@ fn op_evaluate_module(
|
|
|
1081
1075
|
// must not override their real outcome to Terminated.
|
|
1082
1076
|
let mut did_eval = false;
|
|
1083
1077
|
// V8 CHECK-aborts the process if evaluate runs on a
|
|
1084
|
-
// module that
|
|
1085
|
-
// status explicitly rather than
|
|
1078
|
+
// module that is not instantiated yet (Uninstantiated /
|
|
1079
|
+
// Instantiating), so guard status explicitly rather than
|
|
1080
|
+
// crash.
|
|
1086
1081
|
let out = match module.get_status() {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
}
|
|
1091
|
-
v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
|
|
1092
|
-
|
|
1082
|
+
// (…one evaluated already, with no reporter: nothing more to do, or its error again)
|
|
1083
|
+
v8::ModuleStatus::Errored if report.is_none() => {
|
|
1084
|
+
Err(rejected_with(scope, module.get_exception()))
|
|
1085
|
+
}
|
|
1086
|
+
v8::ModuleStatus::Evaluated if report.is_none() => Ok(JsVal::Undefined),
|
|
1087
|
+
// (…and with one, Evaluate again, as HTML runs every script element's record — V8 hands back
|
|
1088
|
+
// the same evaluation promise, rejected with its error or still pending its top-level
|
|
1089
|
+
// await — so each run reports)
|
|
1090
|
+
v8::ModuleStatus::Instantiated
|
|
1091
|
+
| v8::ModuleStatus::Evaluated
|
|
1092
|
+
| v8::ModuleStatus::Errored => {
|
|
1093
1093
|
did_eval = true;
|
|
1094
1094
|
v8::tc_scope!(let tc, scope);
|
|
1095
|
+
let reporter =
|
|
1096
|
+
report.as_deref().and_then(|name| global_function(tc, name));
|
|
1095
1097
|
match module.evaluate(tc) {
|
|
1096
1098
|
// A synchronous top-level throw yields a
|
|
1097
1099
|
// *rejected* promise (not None); a pending
|
|
@@ -1099,12 +1101,27 @@ fn op_evaluate_module(
|
|
|
1099
1101
|
// re-checked after the drain.
|
|
1100
1102
|
Some(value) => {
|
|
1101
1103
|
match v8::Local::<v8::Promise>::try_from(value) {
|
|
1104
|
+
// (…its rejection the reporter's: a synchronous throw's at once, before
|
|
1105
|
+
// whatever its script's runner does next — its element's `load`, Chrome —
|
|
1106
|
+
// the top-level await's as it settles, a microtask)
|
|
1107
|
+
Ok(p) if reporter.is_some() => {
|
|
1108
|
+
if let Some(reporter) = reporter {
|
|
1109
|
+
if p.state() == v8::PromiseState::Rejected {
|
|
1110
|
+
p.mark_as_handled();
|
|
1111
|
+
let (receiver, reason) = (
|
|
1112
|
+
v8::undefined(tc).into(),
|
|
1113
|
+
p.result(tc),
|
|
1114
|
+
);
|
|
1115
|
+
reporter.call(tc, receiver, &[reason]);
|
|
1116
|
+
} else {
|
|
1117
|
+
p.catch(tc, reporter);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
Ok(JsVal::Undefined)
|
|
1121
|
+
}
|
|
1102
1122
|
Ok(p) if p.state() == v8::PromiseState::Rejected => {
|
|
1103
1123
|
let reason = p.result(tc);
|
|
1104
|
-
Err(
|
|
1105
|
-
message: reason.to_rust_string_lossy(tc),
|
|
1106
|
-
backtrace: vec![],
|
|
1107
|
-
})
|
|
1124
|
+
Err(rejected_with(tc, reason))
|
|
1108
1125
|
}
|
|
1109
1126
|
Ok(p) => {
|
|
1110
1127
|
eval_promise = Some(v8::Global::new(tc, p));
|
|
@@ -1132,10 +1149,7 @@ fn op_evaluate_module(
|
|
|
1132
1149
|
let p = v8::Local::new(scope, &g);
|
|
1133
1150
|
if p.state() == v8::PromiseState::Rejected {
|
|
1134
1151
|
let reason = p.result(scope);
|
|
1135
|
-
Err(
|
|
1136
|
-
message: reason.to_rust_string_lossy(scope),
|
|
1137
|
-
backtrace: vec![],
|
|
1138
|
-
})
|
|
1152
|
+
Err(rejected_with(scope, reason))
|
|
1139
1153
|
} else {
|
|
1140
1154
|
out
|
|
1141
1155
|
}
|
|
@@ -1151,6 +1165,16 @@ fn op_evaluate_module(
|
|
|
1151
1165
|
VmReply::Done(outcome)
|
|
1152
1166
|
}
|
|
1153
1167
|
|
|
1168
|
+
// The function the current context's global has under `name`, or None.
|
|
1169
|
+
fn global_function<'s>(
|
|
1170
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
1171
|
+
name: &str,
|
|
1172
|
+
) -> Option<v8::Local<'s, v8::Function>> {
|
|
1173
|
+
let key = v8::String::new(scope, name)?;
|
|
1174
|
+
let global = scope.get_current_context().global(scope);
|
|
1175
|
+
v8::Local::<v8::Function>::try_from(global.get(scope, key.into())?).ok()
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1154
1178
|
// get_module_namespace and is_graph_async both CHECK-abort (a release check, so
|
|
1155
1179
|
// it kills the process) below Instantiated, so both ask this first. Everything
|
|
1156
1180
|
// from Instantiated up is admissible, Errored included: a module that fails to
|
|
@@ -1179,10 +1203,7 @@ fn op_module_namespace(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) ->
|
|
|
1179
1203
|
// never initialized — and the useful answer is WHY it errored,
|
|
1180
1204
|
// the same one instantiate and evaluate give.
|
|
1181
1205
|
if module.get_status() == v8::ModuleStatus::Errored {
|
|
1182
|
-
Err(
|
|
1183
|
-
message: module.get_exception().to_rust_string_lossy(scope),
|
|
1184
|
-
backtrace: vec![],
|
|
1185
|
-
})
|
|
1206
|
+
Err(rejected_with(scope, module.get_exception()))
|
|
1186
1207
|
} else {
|
|
1187
1208
|
match require_instantiated(module, "namespace") {
|
|
1188
1209
|
Err(e) => Err(e),
|
|
@@ -1306,21 +1327,14 @@ fn op_compile_script(
|
|
|
1306
1327
|
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
1307
1328
|
// Compile failure = a parse error (with location).
|
|
1308
1329
|
None => {
|
|
1309
|
-
let
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
.map(|v| v.to_rust_string_lossy(tc))
|
|
1318
|
-
.unwrap_or_else(|| filename.clone());
|
|
1319
|
-
let loc = match message.and_then(|m| m.get_line_number(tc)) {
|
|
1320
|
-
Some(line) => format!(" at {res}:{line}"),
|
|
1321
|
-
None => format!(" at {res}"),
|
|
1322
|
-
};
|
|
1323
|
-
Err(VmError::Parse(format!("{msg}{loc}")))
|
|
1330
|
+
let (exc, message) = (tc.exception(), tc.message());
|
|
1331
|
+
Err(parse_error(
|
|
1332
|
+
tc,
|
|
1333
|
+
exc,
|
|
1334
|
+
message,
|
|
1335
|
+
&filename,
|
|
1336
|
+
"script parse error",
|
|
1337
|
+
))
|
|
1324
1338
|
}
|
|
1325
1339
|
}
|
|
1326
1340
|
}
|
data/lib/rusty_racer/version.rb
CHANGED
data/lib/rusty_racer.rb
CHANGED
|
@@ -121,7 +121,7 @@ module RustyRacer
|
|
|
121
121
|
# `timeout_ms` (0 = the isolate default) caps this eval; `filename` names the
|
|
122
122
|
# script in stack traces and parse-error locations.
|
|
123
123
|
def eval(source, timeout_ms: 0, filename: '<eval>')
|
|
124
|
-
_eval(source, timeout_ms, filename,
|
|
124
|
+
_eval(source, timeout_ms, filename, 0)
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
# Run `source` for its EFFECT and return nil, without marshalling its
|
|
@@ -132,7 +132,14 @@ module RustyRacer
|
|
|
132
132
|
# whose writes all landed. `globalThis.win = someProxy` is the shape that
|
|
133
133
|
# bites.
|
|
134
134
|
def eval_void(source, timeout_ms: 0, filename: '<eval>')
|
|
135
|
-
_eval(source, timeout_ms, filename,
|
|
135
|
+
_eval(source, timeout_ms, filename, 1)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Run `source` and return its completion value only where it is a primitive — a string, a number, a boolean, a
|
|
139
|
+
# BigInt, null — and nil for an object or a symbol, which is never walked: for a caller that wants a string of it
|
|
140
|
+
# (a javascript: URL's evaluation), whatever object the source happens to end with.
|
|
141
|
+
def eval_primitive(source, timeout_ms: 0, filename: '<eval>')
|
|
142
|
+
_eval(source, timeout_ms, filename, 2)
|
|
136
143
|
end
|
|
137
144
|
|
|
138
145
|
# Compile a classic <script>; returns a RustyRacer::Script to #run.
|
|
@@ -171,6 +178,15 @@ module RustyRacer
|
|
|
171
178
|
self
|
|
172
179
|
end
|
|
173
180
|
|
|
181
|
+
# Evaluate the instantiated module. Its evaluation's rejection — a top-level throw, a top-level await's — raises
|
|
182
|
+
# (RustyRacer::RuntimeError, as far as the drain after it settles), or, `report:` naming a function of its context's
|
|
183
|
+
# global, is that function's to report: called with the rejection's reason — a top-level throw's before this
|
|
184
|
+
# returns, a top-level await's as it settles — and nothing raised (HTML "run a module script": upon rejection,
|
|
185
|
+
# report the exception).
|
|
186
|
+
def evaluate(report: nil)
|
|
187
|
+
_evaluate(report&.to_s)
|
|
188
|
+
end
|
|
189
|
+
|
|
174
190
|
# The V8 module status: :uninstantiated, :instantiating, :instantiated,
|
|
175
191
|
# :evaluating, :evaluated or :errored.
|
|
176
192
|
def status
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rusty_racer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Keita Urashima
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|