rusty_racer 0.2.4 → 0.2.6
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/CHANGELOG.md +60 -0
- data/README.md +79 -1
- data/ext/rusty_racer/Cargo.toml +6 -1
- data/ext/rusty_racer/src/lib.rs +596 -174
- data/ext/rusty_racer/src/marshal.rs +356 -117
- data/ext/rusty_racer/src/ops.rs +353 -184
- data/lib/rusty_racer/version.rb +1 -1
- data/lib/rusty_racer.rb +18 -2
- metadata +3 -2
data/ext/rusty_racer/src/ops.rs
CHANGED
|
@@ -29,18 +29,40 @@ macro_rules! marshalled {
|
|
|
29
29
|
($tc:expr, $value:expr) => {{
|
|
30
30
|
let tc = $tc;
|
|
31
31
|
let out = js_to_jsval(tc, $value);
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
if tc.has_caught() {
|
|
33
|
+
Err(caught!(tc))
|
|
34
|
+
} else {
|
|
34
35
|
Ok(out)
|
|
35
|
-
}
|
|
36
|
-
|
|
36
|
+
}
|
|
37
|
+
}};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The failure a TryCatch holds once a JS step came back empty: a termination, or
|
|
41
|
+
// the exception with its stack (and the thrown value kept — `keep_thrown`).
|
|
42
|
+
macro_rules! caught {
|
|
43
|
+
($tc:expr) => {{
|
|
44
|
+
let tc = $tc;
|
|
45
|
+
if tc.has_terminated() {
|
|
46
|
+
VmError::Terminated
|
|
37
47
|
} else {
|
|
48
|
+
let exc = tc.exception();
|
|
38
49
|
let stack = tc.stack_trace();
|
|
39
|
-
|
|
50
|
+
capture_js_error(tc, exc, stack)
|
|
40
51
|
}
|
|
41
52
|
}};
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
// What an eval hands back of its completion value: all of it, marshalled; nothing (`Void`: a statement list run for its
|
|
56
|
+
// EFFECT still has one — the last statement's value — and marshalling it can throw, an accessor or a Proxy trap, or walk
|
|
57
|
+
// a huge graph, for a value the caller discards); or a primitive alone (`Primitive`: an object's never walked, undefined
|
|
58
|
+
// in its place — for a caller that wants a string or a number of it, as a javascript: URL's evaluation does).
|
|
59
|
+
#[derive(Clone, Copy)]
|
|
60
|
+
pub(crate) enum Completion {
|
|
61
|
+
Value,
|
|
62
|
+
Void,
|
|
63
|
+
Primitive,
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
// One VM operation, built by a magnus method and run inline by Core::run ->
|
|
45
67
|
// service_request -> dispatch_one. |context_id| selects which realm the op runs
|
|
46
68
|
// in: 0 = the main realm (Context's own globalThis, swappable by reset_realm),
|
|
@@ -51,11 +73,7 @@ pub(crate) enum Request {
|
|
|
51
73
|
source: String,
|
|
52
74
|
filename: String,
|
|
53
75
|
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,
|
|
76
|
+
completion: Completion,
|
|
59
77
|
},
|
|
60
78
|
// Resolve a dotted function path on globalThis and invoke it with marshalled
|
|
61
79
|
// args (v8::Function::call), preserving the holder as `this`. Distinct from
|
|
@@ -69,6 +87,22 @@ pub(crate) enum Request {
|
|
|
69
87
|
void: bool,
|
|
70
88
|
timeout_ms: u64,
|
|
71
89
|
},
|
|
90
|
+
// Context#function: resolve the path as Call does, once, and keep the
|
|
91
|
+
// function and its holder (FunctionReg); replies with the handle's id.
|
|
92
|
+
ResolveFunction {
|
|
93
|
+
context_id: i32,
|
|
94
|
+
path: String,
|
|
95
|
+
timeout_ms: u64,
|
|
96
|
+
},
|
|
97
|
+
// Function#call: invoke a resolved handle as Call invokes what it resolves,
|
|
98
|
+
// with no path walk and no property name made.
|
|
99
|
+
CallFunction {
|
|
100
|
+
context_id: i32,
|
|
101
|
+
function_id: u64,
|
|
102
|
+
args: Vec<JsVal>,
|
|
103
|
+
void: bool,
|
|
104
|
+
timeout_ms: u64,
|
|
105
|
+
},
|
|
72
106
|
// Drain the isolate's microtask queue once (no auto event loop).
|
|
73
107
|
DrainMicrotasks {
|
|
74
108
|
timeout_ms: u64,
|
|
@@ -76,14 +110,14 @@ pub(crate) enum Request {
|
|
|
76
110
|
Attach {
|
|
77
111
|
context_id: i32,
|
|
78
112
|
name: String,
|
|
79
|
-
|
|
113
|
+
host_fn: HostFnKey,
|
|
80
114
|
timeout_ms: u64,
|
|
81
115
|
},
|
|
82
|
-
// Batch attach: install many (name,
|
|
116
|
+
// Batch attach: install many (name, key) host fns in one round-trip
|
|
83
117
|
// (a fresh realm needs ~dozens). Same semantics as Attach, applied in order.
|
|
84
118
|
AttachMany {
|
|
85
119
|
context_id: i32,
|
|
86
|
-
entries: Vec<(String,
|
|
120
|
+
entries: Vec<(String, HostFnKey)>,
|
|
87
121
|
timeout_ms: u64,
|
|
88
122
|
},
|
|
89
123
|
// reset: swap globalThis for a fresh v8::Context, reusing the same warm
|
|
@@ -123,6 +157,10 @@ pub(crate) enum Request {
|
|
|
123
157
|
EvaluateModule {
|
|
124
158
|
module_id: i32,
|
|
125
159
|
timeout_ms: u64,
|
|
160
|
+
// The name of a function on the module's context's global that reports its evaluation's rejection — HTML "run
|
|
161
|
+
// a module script": upon rejection, report the exception — now or as a top-level-await module settles; none:
|
|
162
|
+
// the rejection is the op's error, as far as the drain gets.
|
|
163
|
+
report: Option<String>,
|
|
126
164
|
},
|
|
127
165
|
ModuleNamespace {
|
|
128
166
|
module_id: i32,
|
|
@@ -137,9 +175,6 @@ pub(crate) enum Request {
|
|
|
137
175
|
ModuleGraphAsync {
|
|
138
176
|
module_id: i32,
|
|
139
177
|
},
|
|
140
|
-
DisposeModule {
|
|
141
|
-
module_id: i32,
|
|
142
|
-
},
|
|
143
178
|
// Classic <script> primitives (V8 ScriptCompiler::CompileUnboundScript): an
|
|
144
179
|
// unbound script, compiled in a context, runnable repeatedly, with the same
|
|
145
180
|
// bytecode-cache options as modules. Addressed by id like a module.
|
|
@@ -158,9 +193,6 @@ pub(crate) enum Request {
|
|
|
158
193
|
timeout_ms: u64,
|
|
159
194
|
void: bool,
|
|
160
195
|
},
|
|
161
|
-
DisposeScript {
|
|
162
|
-
script_id: i32,
|
|
163
|
-
},
|
|
164
196
|
// Serialize a bytecode cache from a compiled handle's CURRENT compile state
|
|
165
197
|
// (Script#create_code_cache / Module#create_code_cache). Called after run/
|
|
166
198
|
// evaluate, it captures the inner functions V8 lazily compiled while running
|
|
@@ -234,102 +266,104 @@ pub(crate) fn run_source(
|
|
|
234
266
|
scope: &mut v8::PinScope<'_, '_>,
|
|
235
267
|
source: &str,
|
|
236
268
|
filename: &str,
|
|
237
|
-
|
|
269
|
+
completion: Completion,
|
|
238
270
|
) -> Result<JsVal, VmError> {
|
|
239
271
|
v8::tc_scope!(let tc, scope);
|
|
240
272
|
// Compile and run as distinct phases so a compile failure maps to
|
|
241
273
|
// ParseError and a thrown exception to RuntimeError (csim rescues both).
|
|
242
274
|
let Some(code) = v8::String::new(tc, source) else {
|
|
243
|
-
return Err(VmError::Parse("source too large".into()));
|
|
275
|
+
return Err(VmError::Parse("source too large".into(), None));
|
|
244
276
|
};
|
|
245
277
|
let origin = script_origin(tc, filename);
|
|
246
278
|
let script = match v8::Script::compile(tc, code, Some(&origin)) {
|
|
247
279
|
Some(script) => script,
|
|
248
280
|
None if tc.has_terminated() => return Err(VmError::Terminated),
|
|
249
281
|
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}")));
|
|
282
|
+
let (exc, message) = (tc.exception(), tc.message());
|
|
283
|
+
return Err(parse_error(tc, exc, message, filename, "parse error"));
|
|
267
284
|
}
|
|
268
285
|
};
|
|
269
286
|
match script.run(tc) {
|
|
270
|
-
Some(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
287
|
+
Some(value) => match completion {
|
|
288
|
+
Completion::Value => marshalled!(tc, value),
|
|
289
|
+
Completion::Primitive if value.is_object() || value.is_symbol() => Ok(JsVal::Undefined),
|
|
290
|
+
Completion::Primitive => marshalled!(tc, value),
|
|
291
|
+
Completion::Void => Ok(JsVal::Undefined),
|
|
292
|
+
},
|
|
293
|
+
None => Err(caught!(tc)),
|
|
278
294
|
}
|
|
279
295
|
}
|
|
280
296
|
|
|
281
|
-
// Resolve a dotted property path on globalThis to a
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
) -> Result<
|
|
290
|
-
|
|
291
|
-
let
|
|
292
|
-
let global = context.global(tc);
|
|
293
|
-
let mut recv: v8::Local<v8::Value> = global.into();
|
|
297
|
+
// Resolve a dotted property path on the current realm's globalThis to a
|
|
298
|
+
// function and the holder it was read off — its `this`, so `a.b.f` gets the right
|
|
299
|
+
// receiver. Each segment is an ordinary [[Get]], so a getter or Proxy trap on the
|
|
300
|
+
// path runs; when one throws, the error is Err(None): the caller's TryCatch holds
|
|
301
|
+
// it (`caught!`).
|
|
302
|
+
fn resolve_function<'s>(
|
|
303
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
304
|
+
path: &str,
|
|
305
|
+
) -> Result<(v8::Local<'s, v8::Function>, v8::Local<'s, v8::Value>), Option<VmError>> {
|
|
306
|
+
let global = scope.get_current_context().global(scope);
|
|
307
|
+
let mut holder: v8::Local<v8::Value> = global.into();
|
|
294
308
|
let mut target: v8::Local<v8::Value> = global.into();
|
|
295
|
-
for part in
|
|
296
|
-
let Some(obj) = target.to_object(
|
|
309
|
+
for part in path.split('.') {
|
|
310
|
+
let Some(obj) = target.to_object(scope) else {
|
|
297
311
|
// The holder of `part` (a preceding segment) was null/undefined, so
|
|
298
312
|
// there's nothing to read `part` from — name the holder, not `part`.
|
|
299
|
-
return Err(VmError::Runtime(format!(
|
|
300
|
-
"`{
|
|
301
|
-
)));
|
|
313
|
+
return Err(Some(VmError::Runtime(format!(
|
|
314
|
+
"`{path}`: cannot read `{part}` (a preceding path segment is not an object)"
|
|
315
|
+
))));
|
|
302
316
|
};
|
|
303
|
-
let Some(key) = v8::String::new(
|
|
304
|
-
return Err(VmError::Runtime("property name too large".into()));
|
|
317
|
+
let Some(key) = v8::String::new(scope, part) else {
|
|
318
|
+
return Err(Some(VmError::Runtime("property name too large".into())));
|
|
305
319
|
};
|
|
306
|
-
let Some(next) = obj.get(
|
|
307
|
-
|
|
308
|
-
return Err(VmError::Terminated);
|
|
309
|
-
}
|
|
310
|
-
let msg = tc
|
|
311
|
-
.exception()
|
|
312
|
-
.map(|e| e.to_rust_string_lossy(tc))
|
|
313
|
-
.unwrap_or_else(|| format!("cannot read `{part}` of `{name}`"));
|
|
314
|
-
return Err(VmError::Runtime(msg));
|
|
320
|
+
let Some(next) = obj.get(scope, key.into()) else {
|
|
321
|
+
return Err(None);
|
|
315
322
|
};
|
|
316
|
-
|
|
323
|
+
holder = target;
|
|
317
324
|
target = next;
|
|
318
325
|
}
|
|
319
|
-
|
|
320
|
-
|
|
326
|
+
match v8::Local::<v8::Function>::try_from(target) {
|
|
327
|
+
Ok(function) => Ok((function, holder)),
|
|
328
|
+
Err(_) => Err(Some(VmError::Runtime(format!(
|
|
329
|
+
"`{path}` is not a function"
|
|
330
|
+
)))),
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Context#call: resolve |name| (above), then call what it names.
|
|
335
|
+
fn call_function(
|
|
336
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
337
|
+
name: &str,
|
|
338
|
+
args: Vec<JsVal>,
|
|
339
|
+
void: bool,
|
|
340
|
+
) -> Result<JsVal, VmError> {
|
|
341
|
+
let (function, receiver) = {
|
|
342
|
+
v8::tc_scope!(let tc, scope);
|
|
343
|
+
match resolve_function(tc, name) {
|
|
344
|
+
Ok(resolved) => resolved,
|
|
345
|
+
Err(failure) => return Err(failure.unwrap_or_else(|| caught!(tc))),
|
|
346
|
+
}
|
|
321
347
|
};
|
|
348
|
+
call_resolved(scope, function, receiver, args, void)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Invoke |function| via v8::Function::call with |receiver| as `this`.
|
|
352
|
+
// Args/result marshal through the ref-preserving paths.
|
|
353
|
+
fn call_resolved(
|
|
354
|
+
scope: &mut v8::PinScope<'_, '_>,
|
|
355
|
+
function: v8::Local<v8::Function>,
|
|
356
|
+
receiver: v8::Local<v8::Value>,
|
|
357
|
+
args: Vec<JsVal>,
|
|
358
|
+
void: bool,
|
|
359
|
+
) -> Result<JsVal, VmError> {
|
|
360
|
+
v8::tc_scope!(let tc, scope);
|
|
322
361
|
let argv: Vec<v8::Local<v8::Value>> = args.into_iter().map(|a| jsval_to_js(tc, a)).collect();
|
|
323
|
-
match
|
|
362
|
+
match function.call(tc, receiver, &argv) {
|
|
324
363
|
// void: skip marshalling the return so a huge/cyclic result is never walked.
|
|
325
364
|
Some(_) if void => Ok(JsVal::Undefined),
|
|
326
365
|
Some(value) => marshalled!(tc, value),
|
|
327
|
-
None
|
|
328
|
-
None => {
|
|
329
|
-
let exc = tc.exception();
|
|
330
|
-
let stack = tc.stack_trace();
|
|
331
|
-
Err(capture_js_error(tc, exc, stack))
|
|
332
|
-
}
|
|
366
|
+
None => Err(caught!(tc)),
|
|
333
367
|
}
|
|
334
368
|
}
|
|
335
369
|
|
|
@@ -413,6 +447,7 @@ pub(crate) fn service_request(
|
|
|
413
447
|
// (the graveyard repoint inside) requires that.
|
|
414
448
|
if outermost {
|
|
415
449
|
flush_retiring(scope);
|
|
450
|
+
forget_released_handles(scope);
|
|
416
451
|
}
|
|
417
452
|
reply
|
|
418
453
|
}
|
|
@@ -423,6 +458,8 @@ fn request_realm(state: &IsolateState, request: &Request) -> Option<i32> {
|
|
|
423
458
|
match request {
|
|
424
459
|
Request::Eval { context_id, .. }
|
|
425
460
|
| Request::Call { context_id, .. }
|
|
461
|
+
| Request::ResolveFunction { context_id, .. }
|
|
462
|
+
| Request::CallFunction { context_id, .. }
|
|
426
463
|
| Request::Attach { context_id, .. }
|
|
427
464
|
| Request::AttachMany { context_id, .. }
|
|
428
465
|
| Request::CompileModule { context_id, .. }
|
|
@@ -441,8 +478,6 @@ fn request_realm(state: &IsolateState, request: &Request) -> Option<i32> {
|
|
|
441
478
|
| Request::DisposeContext { .. }
|
|
442
479
|
| Request::ModuleStatus { .. }
|
|
443
480
|
| Request::ModuleGraphAsync { .. }
|
|
444
|
-
| Request::DisposeModule { .. }
|
|
445
|
-
| Request::DisposeScript { .. }
|
|
446
481
|
| Request::ScriptCodeCache { .. }
|
|
447
482
|
| Request::ModuleCodeCache { .. }
|
|
448
483
|
| Request::HeapStatistics
|
|
@@ -465,9 +500,9 @@ fn dispatch_one(
|
|
|
465
500
|
source,
|
|
466
501
|
filename,
|
|
467
502
|
timeout_ms,
|
|
468
|
-
|
|
503
|
+
completion,
|
|
469
504
|
} => op_eval(
|
|
470
|
-
scope, context_id, source, filename, timeout_ms,
|
|
505
|
+
scope, context_id, source, filename, timeout_ms, completion, outermost,
|
|
471
506
|
),
|
|
472
507
|
Request::Call {
|
|
473
508
|
context_id,
|
|
@@ -476,13 +511,33 @@ fn dispatch_one(
|
|
|
476
511
|
void,
|
|
477
512
|
timeout_ms,
|
|
478
513
|
} => op_call(scope, context_id, name, args, void, timeout_ms, outermost),
|
|
514
|
+
Request::ResolveFunction {
|
|
515
|
+
context_id,
|
|
516
|
+
path,
|
|
517
|
+
timeout_ms,
|
|
518
|
+
} => op_resolve_function(scope, context_id, path, timeout_ms, outermost),
|
|
519
|
+
Request::CallFunction {
|
|
520
|
+
context_id,
|
|
521
|
+
function_id,
|
|
522
|
+
args,
|
|
523
|
+
void,
|
|
524
|
+
timeout_ms,
|
|
525
|
+
} => op_call_function(
|
|
526
|
+
scope,
|
|
527
|
+
context_id,
|
|
528
|
+
function_id,
|
|
529
|
+
args,
|
|
530
|
+
void,
|
|
531
|
+
timeout_ms,
|
|
532
|
+
outermost,
|
|
533
|
+
),
|
|
479
534
|
Request::DrainMicrotasks { timeout_ms } => op_drain_microtasks(scope, timeout_ms),
|
|
480
535
|
Request::Attach {
|
|
481
536
|
context_id,
|
|
482
537
|
name,
|
|
483
|
-
|
|
538
|
+
host_fn,
|
|
484
539
|
timeout_ms,
|
|
485
|
-
} => op_attach(scope, context_id, name,
|
|
540
|
+
} => op_attach(scope, context_id, name, host_fn, timeout_ms, outermost),
|
|
486
541
|
Request::AttachMany {
|
|
487
542
|
context_id,
|
|
488
543
|
entries,
|
|
@@ -511,11 +566,11 @@ fn dispatch_one(
|
|
|
511
566
|
Request::EvaluateModule {
|
|
512
567
|
module_id,
|
|
513
568
|
timeout_ms,
|
|
514
|
-
|
|
569
|
+
report,
|
|
570
|
+
} => op_evaluate_module(scope, module_id, timeout_ms, report, outermost),
|
|
515
571
|
Request::ModuleNamespace { module_id } => op_module_namespace(scope, module_id),
|
|
516
572
|
Request::ModuleStatus { module_id } => op_module_status(scope, module_id),
|
|
517
573
|
Request::ModuleGraphAsync { module_id } => op_module_graph_async(scope, module_id),
|
|
518
|
-
Request::DisposeModule { module_id } => op_dispose_module(scope, module_id),
|
|
519
574
|
Request::CompileScript {
|
|
520
575
|
context_id,
|
|
521
576
|
source,
|
|
@@ -537,7 +592,6 @@ fn dispatch_one(
|
|
|
537
592
|
timeout_ms,
|
|
538
593
|
void,
|
|
539
594
|
} => op_run_script(scope, script_id, timeout_ms, void, outermost),
|
|
540
|
-
Request::DisposeScript { script_id } => op_dispose_script(scope, script_id),
|
|
541
595
|
// Serialize the script's CURRENT compile state. The stored handle is
|
|
542
596
|
// the UnboundScript, which V8 fills in with inner-function bytecode as
|
|
543
597
|
// run() lazily compiles them — so calling this after run() captures
|
|
@@ -619,7 +673,7 @@ fn op_eval(
|
|
|
619
673
|
source: String,
|
|
620
674
|
filename: String,
|
|
621
675
|
timeout_ms: u64,
|
|
622
|
-
|
|
676
|
+
completion: Completion,
|
|
623
677
|
outermost: bool,
|
|
624
678
|
) -> VmReply {
|
|
625
679
|
let outcome = run_js_bracketed(scope, outermost, timeout_ms, "eval", |scope, outermost| {
|
|
@@ -628,7 +682,7 @@ fn op_eval(
|
|
|
628
682
|
Some(ctx) => {
|
|
629
683
|
let context = v8::Local::new(scope, &ctx);
|
|
630
684
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
631
|
-
let out = run_source(scope, &source, &filename,
|
|
685
|
+
let out = run_source(scope, &source, &filename, completion);
|
|
632
686
|
auto_drain(scope, outermost);
|
|
633
687
|
(true, out)
|
|
634
688
|
}
|
|
@@ -672,6 +726,132 @@ fn op_call(
|
|
|
672
726
|
VmReply::Done(outcome)
|
|
673
727
|
}
|
|
674
728
|
|
|
729
|
+
fn op_resolve_function(
|
|
730
|
+
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
731
|
+
context_id: i32,
|
|
732
|
+
path: String,
|
|
733
|
+
timeout_ms: u64,
|
|
734
|
+
outermost: bool,
|
|
735
|
+
) -> VmReply {
|
|
736
|
+
// The id this resolve registered, for a terminate after it (in the drain, or
|
|
737
|
+
// the watchdog's verdict over the bracket) to take back: a failed resolve hands
|
|
738
|
+
// Ruby no wrapper, so nothing else would ever release it.
|
|
739
|
+
let mut registered = None;
|
|
740
|
+
let outcome = run_js_bracketed(
|
|
741
|
+
scope,
|
|
742
|
+
outermost,
|
|
743
|
+
timeout_ms,
|
|
744
|
+
"function",
|
|
745
|
+
|scope, outermost| {
|
|
746
|
+
let Some(ctx) = context_for(istate!(scope), context_id) else {
|
|
747
|
+
return (false, Err(VmError::Disposed(REALM_GONE.into())));
|
|
748
|
+
};
|
|
749
|
+
let context = v8::Local::new(scope, &ctx);
|
|
750
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
751
|
+
let resolved = {
|
|
752
|
+
v8::tc_scope!(let tc, scope);
|
|
753
|
+
match resolve_function(tc, &path) {
|
|
754
|
+
Ok((function, receiver)) => {
|
|
755
|
+
Ok((v8::Global::new(tc, function), v8::Global::new(tc, receiver)))
|
|
756
|
+
}
|
|
757
|
+
Err(failure) => Err(failure.unwrap_or_else(|| caught!(tc))),
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
let out = resolved.map(|(function, receiver)| {
|
|
761
|
+
let functions = &mut istate!(scope).functions;
|
|
762
|
+
let id = functions.next_id;
|
|
763
|
+
functions.next_id += 1;
|
|
764
|
+
functions.by_id.insert(
|
|
765
|
+
id,
|
|
766
|
+
FunctionEntry {
|
|
767
|
+
function,
|
|
768
|
+
receiver,
|
|
769
|
+
context_id,
|
|
770
|
+
},
|
|
771
|
+
);
|
|
772
|
+
registered = Some(id);
|
|
773
|
+
JsVal::Int(id as i64)
|
|
774
|
+
});
|
|
775
|
+
auto_drain(scope, outermost);
|
|
776
|
+
(true, out)
|
|
777
|
+
},
|
|
778
|
+
);
|
|
779
|
+
if let (Err(_), Some(id)) = (&outcome, registered) {
|
|
780
|
+
istate!(scope).functions.by_id.remove(&id);
|
|
781
|
+
}
|
|
782
|
+
VmReply::Done(outcome)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// What a call or a resolve of a handle whose realm is gone raises.
|
|
786
|
+
const REALM_GONE: &str = "function's realm was reset or disposed";
|
|
787
|
+
|
|
788
|
+
// Remove the handles whose Ruby wrappers were disposed or collected
|
|
789
|
+
// (Core.released) from their registries: a wrapper could not drop its Globals
|
|
790
|
+
// itself. At the outermost request boundary, and only when one was released — a
|
|
791
|
+
// flag read, not a lock, on every other op.
|
|
792
|
+
fn forget_released_handles(scope: &mut v8::PinScope<'_, '_, ()>) {
|
|
793
|
+
let core = istate!(scope).core_ptr;
|
|
794
|
+
if core.is_null() {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
let core = unsafe { &*core };
|
|
798
|
+
if !core.handles_released.swap(false, Ordering::Relaxed) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
let released = std::mem::take(&mut *core.released.lock().unwrap());
|
|
802
|
+
let state = istate!(scope);
|
|
803
|
+
for handle in released {
|
|
804
|
+
match handle {
|
|
805
|
+
Released::Function(id) => {
|
|
806
|
+
state.functions.by_id.remove(&id);
|
|
807
|
+
}
|
|
808
|
+
Released::Module(id) => {
|
|
809
|
+
state.modules.by_id.remove(&id);
|
|
810
|
+
}
|
|
811
|
+
Released::Script(id) => {
|
|
812
|
+
state.scripts.by_id.remove(&id);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
#[allow(clippy::too_many_arguments)]
|
|
819
|
+
fn op_call_function(
|
|
820
|
+
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
821
|
+
context_id: i32,
|
|
822
|
+
function_id: u64,
|
|
823
|
+
args: Vec<JsVal>,
|
|
824
|
+
void: bool,
|
|
825
|
+
timeout_ms: u64,
|
|
826
|
+
outermost: bool,
|
|
827
|
+
) -> VmReply {
|
|
828
|
+
let outcome = run_js_bracketed(scope, outermost, timeout_ms, "call", |scope, outermost| {
|
|
829
|
+
let Some(ctx) = context_for(istate!(scope), context_id) else {
|
|
830
|
+
return (false, Err(VmError::Disposed(REALM_GONE.into())));
|
|
831
|
+
};
|
|
832
|
+
let context = v8::Local::new(scope, &ctx);
|
|
833
|
+
let scope = &mut v8::ContextScope::new(scope, context);
|
|
834
|
+
// Locals straight off the registry's Globals: a shared borrow of the slot
|
|
835
|
+
// beside the shared one Local::new takes, so nothing is cloned per call.
|
|
836
|
+
let target = scope
|
|
837
|
+
.get_slot::<IsolateState>()
|
|
838
|
+
.and_then(|state| state.functions.by_id.get(&function_id))
|
|
839
|
+
.map(|entry| {
|
|
840
|
+
(
|
|
841
|
+
v8::Local::new(scope, &entry.function),
|
|
842
|
+
v8::Local::new(scope, &entry.receiver),
|
|
843
|
+
)
|
|
844
|
+
});
|
|
845
|
+
let Some((function, receiver)) = target else {
|
|
846
|
+
return (false, Err(VmError::Disposed(REALM_GONE.into())));
|
|
847
|
+
};
|
|
848
|
+
let out = call_resolved(scope, function, receiver, args, void);
|
|
849
|
+
auto_drain(scope, outermost);
|
|
850
|
+
(true, out)
|
|
851
|
+
});
|
|
852
|
+
VmReply::Done(outcome)
|
|
853
|
+
}
|
|
854
|
+
|
|
675
855
|
fn op_drain_microtasks(scope: &mut v8::PinScope<'_, '_, ()>, timeout_ms: u64) -> VmReply {
|
|
676
856
|
// A microtask may call an attached host fn (a Promise .then ->
|
|
677
857
|
// ruby), which runs inline via host_fn_callback — no routing
|
|
@@ -699,7 +879,7 @@ fn op_attach(
|
|
|
699
879
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
700
880
|
context_id: i32,
|
|
701
881
|
name: String,
|
|
702
|
-
|
|
882
|
+
host_fn: HostFnKey,
|
|
703
883
|
timeout_ms: u64,
|
|
704
884
|
outermost: bool,
|
|
705
885
|
) -> VmReply {
|
|
@@ -719,7 +899,7 @@ fn op_attach(
|
|
|
719
899
|
Some(ctx) => {
|
|
720
900
|
let context = v8::Local::new(scope, &ctx);
|
|
721
901
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
722
|
-
let external = v8::External::new(scope,
|
|
902
|
+
let external = v8::External::new(scope, host_fn.to_external());
|
|
723
903
|
let out = match v8::Function::builder(host_fn_callback)
|
|
724
904
|
.data(external.into())
|
|
725
905
|
.build(scope)
|
|
@@ -747,7 +927,7 @@ fn op_attach(
|
|
|
747
927
|
fn op_attach_many(
|
|
748
928
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
749
929
|
context_id: i32,
|
|
750
|
-
entries: Vec<(String,
|
|
930
|
+
entries: Vec<(String, HostFnKey)>,
|
|
751
931
|
timeout_ms: u64,
|
|
752
932
|
outermost: bool,
|
|
753
933
|
) -> VmReply {
|
|
@@ -769,8 +949,8 @@ fn op_attach_many(
|
|
|
769
949
|
let context = v8::Local::new(scope, &ctx);
|
|
770
950
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
771
951
|
let mut out = Ok(JsVal::Undefined);
|
|
772
|
-
for (name,
|
|
773
|
-
let external = v8::External::new(scope,
|
|
952
|
+
for (name, host_fn) in &entries {
|
|
953
|
+
let external = v8::External::new(scope, host_fn.to_external());
|
|
774
954
|
out = match v8::Function::builder(host_fn_callback)
|
|
775
955
|
.data(external.into())
|
|
776
956
|
.build(scope)
|
|
@@ -938,13 +1118,13 @@ fn op_compile_module(
|
|
|
938
1118
|
};
|
|
939
1119
|
let hash = module.get_identity_hash().get();
|
|
940
1120
|
let g = v8::Global::new(tc, module);
|
|
1121
|
+
let weak = v8::Weak::new(tc, module);
|
|
941
1122
|
let id = {
|
|
942
1123
|
let m = &mut istate!(tc).modules;
|
|
943
1124
|
let id = m.next_id;
|
|
944
1125
|
m.next_id += 1;
|
|
945
|
-
m.by_id
|
|
946
|
-
|
|
947
|
-
m.by_hash.entry(hash).or_default().push((g, id));
|
|
1126
|
+
m.by_id.insert(id, (g, context_id));
|
|
1127
|
+
m.add_url(hash, weak, filename.clone());
|
|
948
1128
|
id
|
|
949
1129
|
};
|
|
950
1130
|
Ok(Compiled {
|
|
@@ -957,21 +1137,14 @@ fn op_compile_module(
|
|
|
957
1137
|
// A module compile failure is a parse error
|
|
958
1138
|
// (compile-time), not a thrown exception.
|
|
959
1139
|
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}")))
|
|
1140
|
+
let (exc, message) = (tc.exception(), tc.message());
|
|
1141
|
+
Err(parse_error(
|
|
1142
|
+
tc,
|
|
1143
|
+
exc,
|
|
1144
|
+
message,
|
|
1145
|
+
&filename,
|
|
1146
|
+
"module parse error",
|
|
1147
|
+
))
|
|
975
1148
|
}
|
|
976
1149
|
}
|
|
977
1150
|
}
|
|
@@ -1013,10 +1186,9 @@ fn op_instantiate_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -
|
|
|
1013
1186
|
| v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
|
|
1014
1187
|
// V8 CHECK-aborts on instantiating an errored
|
|
1015
1188
|
// module; surface its exception instead.
|
|
1016
|
-
v8::ModuleStatus::Errored =>
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
}),
|
|
1189
|
+
v8::ModuleStatus::Errored => {
|
|
1190
|
+
Err(rejected_with(scope, module.get_exception()))
|
|
1191
|
+
}
|
|
1020
1192
|
_ => {
|
|
1021
1193
|
v8::tc_scope!(let tc, scope);
|
|
1022
1194
|
match module.instantiate_module(tc, resolve_imported) {
|
|
@@ -1047,6 +1219,7 @@ fn op_evaluate_module(
|
|
|
1047
1219
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
1048
1220
|
module_id: i32,
|
|
1049
1221
|
timeout_ms: u64,
|
|
1222
|
+
report: Option<String>,
|
|
1050
1223
|
outermost: bool,
|
|
1051
1224
|
) -> VmReply {
|
|
1052
1225
|
// Top-level module code (and, under :auto, the microtasks its
|
|
@@ -1081,17 +1254,25 @@ fn op_evaluate_module(
|
|
|
1081
1254
|
// must not override their real outcome to Terminated.
|
|
1082
1255
|
let mut did_eval = false;
|
|
1083
1256
|
// V8 CHECK-aborts the process if evaluate runs on a
|
|
1084
|
-
// module that
|
|
1085
|
-
// status explicitly rather than
|
|
1257
|
+
// module that is not instantiated yet (Uninstantiated /
|
|
1258
|
+
// Instantiating), so guard status explicitly rather than
|
|
1259
|
+
// crash.
|
|
1086
1260
|
let out = match module.get_status() {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
}
|
|
1091
|
-
v8::ModuleStatus::Evaluated => Ok(JsVal::Undefined),
|
|
1092
|
-
|
|
1261
|
+
// (…one evaluated already, with no reporter: nothing more to do, or its error again)
|
|
1262
|
+
v8::ModuleStatus::Errored if report.is_none() => {
|
|
1263
|
+
Err(rejected_with(scope, module.get_exception()))
|
|
1264
|
+
}
|
|
1265
|
+
v8::ModuleStatus::Evaluated if report.is_none() => Ok(JsVal::Undefined),
|
|
1266
|
+
// (…and with one, Evaluate again, as HTML runs every script element's record — V8 hands back
|
|
1267
|
+
// the same evaluation promise, rejected with its error or still pending its top-level
|
|
1268
|
+
// await — so each run reports)
|
|
1269
|
+
v8::ModuleStatus::Instantiated
|
|
1270
|
+
| v8::ModuleStatus::Evaluated
|
|
1271
|
+
| v8::ModuleStatus::Errored => {
|
|
1093
1272
|
did_eval = true;
|
|
1094
1273
|
v8::tc_scope!(let tc, scope);
|
|
1274
|
+
let reporter =
|
|
1275
|
+
report.as_deref().and_then(|name| global_function(tc, name));
|
|
1095
1276
|
match module.evaluate(tc) {
|
|
1096
1277
|
// A synchronous top-level throw yields a
|
|
1097
1278
|
// *rejected* promise (not None); a pending
|
|
@@ -1099,12 +1280,27 @@ fn op_evaluate_module(
|
|
|
1099
1280
|
// re-checked after the drain.
|
|
1100
1281
|
Some(value) => {
|
|
1101
1282
|
match v8::Local::<v8::Promise>::try_from(value) {
|
|
1283
|
+
// (…its rejection the reporter's: a synchronous throw's at once, before
|
|
1284
|
+
// whatever its script's runner does next — its element's `load`, Chrome —
|
|
1285
|
+
// the top-level await's as it settles, a microtask)
|
|
1286
|
+
Ok(p) if reporter.is_some() => {
|
|
1287
|
+
if let Some(reporter) = reporter {
|
|
1288
|
+
if p.state() == v8::PromiseState::Rejected {
|
|
1289
|
+
p.mark_as_handled();
|
|
1290
|
+
let (receiver, reason) = (
|
|
1291
|
+
v8::undefined(tc).into(),
|
|
1292
|
+
p.result(tc),
|
|
1293
|
+
);
|
|
1294
|
+
reporter.call(tc, receiver, &[reason]);
|
|
1295
|
+
} else {
|
|
1296
|
+
p.catch(tc, reporter);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
Ok(JsVal::Undefined)
|
|
1300
|
+
}
|
|
1102
1301
|
Ok(p) if p.state() == v8::PromiseState::Rejected => {
|
|
1103
1302
|
let reason = p.result(tc);
|
|
1104
|
-
Err(
|
|
1105
|
-
message: reason.to_rust_string_lossy(tc),
|
|
1106
|
-
backtrace: vec![],
|
|
1107
|
-
})
|
|
1303
|
+
Err(rejected_with(tc, reason))
|
|
1108
1304
|
}
|
|
1109
1305
|
Ok(p) => {
|
|
1110
1306
|
eval_promise = Some(v8::Global::new(tc, p));
|
|
@@ -1113,12 +1309,7 @@ fn op_evaluate_module(
|
|
|
1113
1309
|
_ => Ok(JsVal::Undefined),
|
|
1114
1310
|
}
|
|
1115
1311
|
}
|
|
1116
|
-
None
|
|
1117
|
-
None => {
|
|
1118
|
-
let exc = tc.exception();
|
|
1119
|
-
let stack = tc.stack_trace();
|
|
1120
|
-
Err(capture_js_error(tc, exc, stack))
|
|
1121
|
-
}
|
|
1312
|
+
None => Err(caught!(tc)),
|
|
1122
1313
|
}
|
|
1123
1314
|
}
|
|
1124
1315
|
_ => Err(VmError::Runtime(
|
|
@@ -1132,10 +1323,7 @@ fn op_evaluate_module(
|
|
|
1132
1323
|
let p = v8::Local::new(scope, &g);
|
|
1133
1324
|
if p.state() == v8::PromiseState::Rejected {
|
|
1134
1325
|
let reason = p.result(scope);
|
|
1135
|
-
Err(
|
|
1136
|
-
message: reason.to_rust_string_lossy(scope),
|
|
1137
|
-
backtrace: vec![],
|
|
1138
|
-
})
|
|
1326
|
+
Err(rejected_with(scope, reason))
|
|
1139
1327
|
} else {
|
|
1140
1328
|
out
|
|
1141
1329
|
}
|
|
@@ -1151,6 +1339,16 @@ fn op_evaluate_module(
|
|
|
1151
1339
|
VmReply::Done(outcome)
|
|
1152
1340
|
}
|
|
1153
1341
|
|
|
1342
|
+
// The function the current context's global has under `name`, or None.
|
|
1343
|
+
fn global_function<'s>(
|
|
1344
|
+
scope: &mut v8::PinScope<'s, '_>,
|
|
1345
|
+
name: &str,
|
|
1346
|
+
) -> Option<v8::Local<'s, v8::Function>> {
|
|
1347
|
+
let key = v8::String::new(scope, name)?;
|
|
1348
|
+
let global = scope.get_current_context().global(scope);
|
|
1349
|
+
v8::Local::<v8::Function>::try_from(global.get(scope, key.into())?).ok()
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1154
1352
|
// get_module_namespace and is_graph_async both CHECK-abort (a release check, so
|
|
1155
1353
|
// it kills the process) below Instantiated, so both ask this first. Everything
|
|
1156
1354
|
// from Instantiated up is admissible, Errored included: a module that fails to
|
|
@@ -1179,10 +1377,7 @@ fn op_module_namespace(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) ->
|
|
|
1179
1377
|
// never initialized — and the useful answer is WHY it errored,
|
|
1180
1378
|
// the same one instantiate and evaluate give.
|
|
1181
1379
|
if module.get_status() == v8::ModuleStatus::Errored {
|
|
1182
|
-
Err(
|
|
1183
|
-
message: module.get_exception().to_rust_string_lossy(scope),
|
|
1184
|
-
backtrace: vec![],
|
|
1185
|
-
})
|
|
1380
|
+
Err(rejected_with(scope, module.get_exception()))
|
|
1186
1381
|
} else {
|
|
1187
1382
|
match require_instantiated(module, "namespace") {
|
|
1188
1383
|
Err(e) => Err(e),
|
|
@@ -1244,15 +1439,6 @@ fn op_module_graph_async(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -
|
|
|
1244
1439
|
VmReply::Done(outcome)
|
|
1245
1440
|
}
|
|
1246
1441
|
|
|
1247
|
-
fn op_dispose_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
1248
|
-
let m = &mut istate!(scope).modules;
|
|
1249
|
-
m.by_id.remove(&module_id);
|
|
1250
|
-
for bucket in m.by_hash.values_mut() {
|
|
1251
|
-
bucket.retain(|(_, id)| *id != module_id);
|
|
1252
|
-
}
|
|
1253
|
-
VmReply::Done(Ok(JsVal::Undefined))
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
1442
|
#[allow(clippy::too_many_arguments)]
|
|
1257
1443
|
fn op_compile_script(
|
|
1258
1444
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
@@ -1306,21 +1492,14 @@ fn op_compile_script(
|
|
|
1306
1492
|
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
1307
1493
|
// Compile failure = a parse error (with location).
|
|
1308
1494
|
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}")))
|
|
1495
|
+
let (exc, message) = (tc.exception(), tc.message());
|
|
1496
|
+
Err(parse_error(
|
|
1497
|
+
tc,
|
|
1498
|
+
exc,
|
|
1499
|
+
message,
|
|
1500
|
+
&filename,
|
|
1501
|
+
"script parse error",
|
|
1502
|
+
))
|
|
1324
1503
|
}
|
|
1325
1504
|
}
|
|
1326
1505
|
}
|
|
@@ -1361,12 +1540,7 @@ fn op_run_script(
|
|
|
1361
1540
|
match script.run(tc) {
|
|
1362
1541
|
Some(_) if void => Ok(JsVal::Undefined),
|
|
1363
1542
|
Some(value) => marshalled!(tc, value),
|
|
1364
|
-
None
|
|
1365
|
-
None => {
|
|
1366
|
-
let exc = tc.exception();
|
|
1367
|
-
let stack = tc.stack_trace();
|
|
1368
|
-
Err(capture_js_error(tc, exc, stack))
|
|
1369
|
-
}
|
|
1543
|
+
None => Err(caught!(tc)),
|
|
1370
1544
|
}
|
|
1371
1545
|
};
|
|
1372
1546
|
auto_drain(scope, outermost);
|
|
@@ -1379,11 +1553,6 @@ fn op_run_script(
|
|
|
1379
1553
|
VmReply::Done(outcome)
|
|
1380
1554
|
}
|
|
1381
1555
|
|
|
1382
|
-
fn op_dispose_script(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32) -> VmReply {
|
|
1383
|
-
istate!(scope).scripts.by_id.remove(&script_id);
|
|
1384
|
-
VmReply::Done(Ok(JsVal::Undefined))
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
1556
|
fn op_script_code_cache(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32) -> VmReply {
|
|
1388
1557
|
let handle = script_handle(istate!(scope), script_id);
|
|
1389
1558
|
let outcome = match handle {
|