rusty_racer 0.1.16 → 0.2.1
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 +24 -0
- data/ext/rusty_racer/Cargo.toml +1 -1
- data/ext/rusty_racer/src/lib.rs +160 -39
- data/ext/rusty_racer/src/marshal.rs +38 -3
- data/ext/rusty_racer/src/ops.rs +109 -16
- data/lib/rusty_racer/execjs.rb +8 -2
- data/lib/rusty_racer/version.rb +1 -1
- data/lib/rusty_racer.rb +25 -1
- 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: 074b269481c924fc8e99f129e361d29fb4847c9c91ac4afee96378721fe9eebd
|
|
4
|
+
data.tar.gz: 68b312cba649752c393c0736e10efd8280f87ae0406ef7826b8d9fe3ec5505ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3ff8f690a9ad98912f2328d7be4bc10c3d1f38499365f628fbe4dac5e782832323bfc86976ac05fbe9406f288b75191fac11f1eb4f920d1caae056561d0eceb
|
|
7
|
+
data.tar.gz: a13dad93b256b9f15eef3baa27e79a97e2d7da61ec20c29aefc66907cd1cef4916fb0c84d65742233d182d48fed1fa369b48e86105bba7fede8a6afdaa008133
|
data/README.md
CHANGED
|
@@ -65,6 +65,7 @@ ctx.eval("({a: 1, b: [true, 'x']})") # => {"a"=>1, "b"=>[true, "x"]}
|
|
|
65
65
|
ctx.eval("function add(a, b) { return a + b }")
|
|
66
66
|
ctx.call("add", 20, 22) # => 42
|
|
67
67
|
ctx.call_void("doSideEffect") # runs it; never marshals the return
|
|
68
|
+
ctx.eval_void("globalThis.app = boot()") # ditto for the completion value
|
|
68
69
|
|
|
69
70
|
# Ruby callbacks into JS; a raised Ruby exception becomes a JS exception.
|
|
70
71
|
ctx.attach("rubyUpcase", ->(s) { s.upcase })
|
|
@@ -79,6 +80,20 @@ rescue RustyRacer::RuntimeError => e
|
|
|
79
80
|
end
|
|
80
81
|
```
|
|
81
82
|
|
|
83
|
+
Everything the binding raises descends from `RustyRacer::Error`, so one rescue
|
|
84
|
+
covers the library. Under it: `ParseError`, `RuntimeError`,
|
|
85
|
+
`ScriptTerminatedError` and `V8OutOfMemoryError` for what the JS did;
|
|
86
|
+
`DisposedError` for using an isolate (or anything it handed out) after
|
|
87
|
+
disposing it; `WrongThreadError` for reaching an isolate from the wrong thread.
|
|
88
|
+
|
|
89
|
+
Reading a result is not passive — marshalling runs JS (getters, `Proxy` traps,
|
|
90
|
+
`toString`), and a throw there fails the operation even though the code itself
|
|
91
|
+
ran to completion. When the value is incidental, say so: `#eval_void`,
|
|
92
|
+
`Script#run_void` and `#call_void` run for the effect and never touch the
|
|
93
|
+
result. A statement list has a completion value whether you want one or not, so
|
|
94
|
+
plumbing an object into a global (`globalThis.win = someProxy`) is enough to
|
|
95
|
+
make a plain `#eval` fail after every one of its writes has landed.
|
|
96
|
+
|
|
82
97
|
ES modules (the embedder owns the URL→module registry):
|
|
83
98
|
|
|
84
99
|
```ruby
|
|
@@ -86,10 +101,19 @@ dep = ctx.compile_module("export const x = 21;", filename: "/dep.js")
|
|
|
86
101
|
app = ctx.compile_module('import {x} from "./dep.js"; export const r = x * 2;',
|
|
87
102
|
filename: "/app.js")
|
|
88
103
|
app.instantiate { |specifier, referrer| dep if specifier == "./dep.js" }
|
|
104
|
+
app.graph_async? # => false
|
|
89
105
|
app.evaluate
|
|
90
106
|
app.namespace["r"] # => 42
|
|
91
107
|
```
|
|
92
108
|
|
|
109
|
+
`graph_async?` asks whether top-level `await` appears anywhere in the *linked*
|
|
110
|
+
graph, so an `await` in a dependency counts — which is why it needs an
|
|
111
|
+
instantiated module and raises before then. `false` is the answer that carries a
|
|
112
|
+
guarantee: `evaluate` ran the whole graph to completion. Since there is no event
|
|
113
|
+
loop here (see below), a `true` graph whose `await` never settles returns from
|
|
114
|
+
`evaluate` with the module body still suspended, and `status` reports
|
|
115
|
+
`:evaluated` either way — so `graph_async?` is what distinguishes the two.
|
|
116
|
+
|
|
93
117
|
Classic `<script>`s work the same way: `ctx.compile("1 + 1").run` # => 2.
|
|
94
118
|
|
|
95
119
|
### Resource limits
|
data/ext/rusty_racer/Cargo.toml
CHANGED
data/ext/rusty_racer/src/lib.rs
CHANGED
|
@@ -326,13 +326,21 @@ fn current_ruby_thread() -> usize {
|
|
|
326
326
|
// that class carrying the message. Must be called with the GVL held.
|
|
327
327
|
fn error_to_exception(e: &Error) -> Option<Exception> {
|
|
328
328
|
let v = e.value()?;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
329
|
+
// ExceptionClass FIRST. Error::new(class, msg) stores the CLASS, and
|
|
330
|
+
// Exception::try_convert would happily accept it — it falls back to calling
|
|
331
|
+
// #exception on the value, which hands back a fresh instance carrying only
|
|
332
|
+
// the class name, silently dropping our message.
|
|
332
333
|
if let Ok(class) = ExceptionClass::try_convert(v) {
|
|
333
|
-
|
|
334
|
+
// magnus renders such an Error as "Class: message"; the instance carries
|
|
335
|
+
// the class already, so strip it rather than say it twice.
|
|
336
|
+
let text = e.to_string();
|
|
337
|
+
let message = text
|
|
338
|
+
.strip_prefix(&format!("{class}: "))
|
|
339
|
+
.unwrap_or(&text)
|
|
340
|
+
.to_owned();
|
|
341
|
+
return class.new_instance((message,)).ok();
|
|
334
342
|
}
|
|
335
|
-
|
|
343
|
+
Exception::try_convert(v).ok()
|
|
336
344
|
}
|
|
337
345
|
|
|
338
346
|
// JS called a host function. We are on the owner thread with the GVL RELEASED
|
|
@@ -351,8 +359,20 @@ fn host_fn_callback(
|
|
|
351
359
|
Err(_) => return,
|
|
352
360
|
};
|
|
353
361
|
let mut js_args = Vec::with_capacity(args.length() as usize);
|
|
354
|
-
|
|
355
|
-
|
|
362
|
+
{
|
|
363
|
+
// Marshalling an argument RUNS JS — accessors, Proxy traps — and a throw
|
|
364
|
+
// there has to fail the host call, not hand the Ruby proc a value that
|
|
365
|
+
// was never read. Without a TryCatch of our own the exception simply
|
|
366
|
+
// rides along until something happens to clear it, so whether the caller
|
|
367
|
+
// ever saw it came down to whether the proc re-entered V8.
|
|
368
|
+
v8::tc_scope!(let tc, scope);
|
|
369
|
+
for i in 0..args.length() {
|
|
370
|
+
js_args.push(js_to_jsval(tc, args.get(i)));
|
|
371
|
+
}
|
|
372
|
+
if tc.has_caught() {
|
|
373
|
+
tc.rethrow();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
356
376
|
}
|
|
357
377
|
// Reach the owning Core through the slot back-pointer (the callback holds
|
|
358
378
|
// only a scope). Null only before wiring, which is before any JS can run.
|
|
@@ -924,19 +944,22 @@ fn resolve_imported<'s>(
|
|
|
924
944
|
// import's auto-link (None -> dynamic_import_resolver, with the initiating
|
|
925
945
|
// realm so it can resolve per-realm).
|
|
926
946
|
let instantiate = istate!(scope).instantiate_resolve.as_ref().map(|r| r.get());
|
|
947
|
+
let mut dynamic_err = None;
|
|
927
948
|
let dep_id = match instantiate {
|
|
928
949
|
Some(resolve) => {
|
|
950
|
+
// The error conversion happens INSIDE with_gvl: error_to_exception
|
|
951
|
+
// calls into Ruby (so it can allocate, so it can GC) and BoxValue::new
|
|
952
|
+
// registers a GC root — neither is safe with the GVL released.
|
|
929
953
|
match with_gvl(|| {
|
|
930
954
|
resolve_module_via_ruby(unsafe { &*core_ptr }, resolve, &spec, &ref_url, None)
|
|
955
|
+
.map_err(|e| error_to_exception(&e).map(BoxValue::new))
|
|
931
956
|
}) {
|
|
932
957
|
Ok(id) => id,
|
|
933
958
|
// Stash the resolver's own raised exception (GC-rooted) so the
|
|
934
959
|
// InstantiateModule op can re-raise it with its original class
|
|
935
960
|
// instead of a generic "failed to link".
|
|
936
|
-
Err(
|
|
937
|
-
|
|
938
|
-
istate!(scope).instantiate_resolve_err = Some(BoxValue::new(exc));
|
|
939
|
-
}
|
|
961
|
+
Err(exc) => {
|
|
962
|
+
istate!(scope).instantiate_resolve_err = exc;
|
|
940
963
|
None
|
|
941
964
|
}
|
|
942
965
|
}
|
|
@@ -949,7 +972,7 @@ fn resolve_imported<'s>(
|
|
|
949
972
|
.as_ref()
|
|
950
973
|
.map(|r| r.get());
|
|
951
974
|
match resolver {
|
|
952
|
-
Some(p) => with_gvl(|| {
|
|
975
|
+
Some(p) => match with_gvl(|| {
|
|
953
976
|
resolve_module_via_ruby(
|
|
954
977
|
unsafe { &*core_ptr },
|
|
955
978
|
p,
|
|
@@ -957,23 +980,81 @@ fn resolve_imported<'s>(
|
|
|
957
980
|
&ref_url,
|
|
958
981
|
Some(here.unwrap_or(0)),
|
|
959
982
|
)
|
|
960
|
-
|
|
961
|
-
|
|
983
|
+
// Rendering the message reads the Ruby exception, so it too
|
|
984
|
+
// stays inside with_gvl.
|
|
985
|
+
.map_err(|e| e.to_string())
|
|
986
|
+
}) {
|
|
987
|
+
Ok(id) => id,
|
|
988
|
+
// Unlike the static branch there is no Ruby frame waiting to
|
|
989
|
+
// re-raise into — this failure becomes the import()'s promise
|
|
990
|
+
// rejection — so the resolver's own message has to travel out
|
|
991
|
+
// as the JS error or it is lost.
|
|
992
|
+
Err(why) => {
|
|
993
|
+
dynamic_err = Some(why);
|
|
994
|
+
None
|
|
995
|
+
}
|
|
996
|
+
},
|
|
962
997
|
None => None,
|
|
963
998
|
}
|
|
964
999
|
}
|
|
965
1000
|
};
|
|
966
|
-
|
|
1001
|
+
// From here on, every exit that isn't a module throws first. V8 has no
|
|
1002
|
+
// channel for "why" — it just sees an empty return, installs no error of its
|
|
1003
|
+
// own and resets the graph — so without a thrown exception the failure
|
|
1004
|
+
// reaches Ruby as a bare "unexpected failure" naming neither the import nor
|
|
1005
|
+
// where it came from, which is the single most common module wiring mistake.
|
|
1006
|
+
// (The two bails above stay silent: both are broken internal state, not
|
|
1007
|
+
// anything the caller did.) The one path that must also stay silent is a
|
|
1008
|
+
// STATIC resolver that raised — its own Ruby exception is already stashed
|
|
1009
|
+
// for instantiate_module to re-raise, and ours would mask it. The dynamic
|
|
1010
|
+
// branch has no such frame, so it carries its message in dynamic_err.
|
|
1011
|
+
let raised = istate!(scope).instantiate_resolve_err.is_some();
|
|
1012
|
+
let fail = |scope: &mut v8::PinScope<'_, '_>, message: String| {
|
|
1013
|
+
if !raised {
|
|
1014
|
+
throw_js_error(scope, &message);
|
|
1015
|
+
}
|
|
1016
|
+
None
|
|
1017
|
+
};
|
|
1018
|
+
let import = format!("{spec:?} imported from {ref_url}");
|
|
1019
|
+
let Some(dep_id) = dep_id else {
|
|
1020
|
+
return match dynamic_err {
|
|
1021
|
+
Some(why) => fail(scope, format!("{why} (resolving {import})")),
|
|
1022
|
+
None => fail(
|
|
1023
|
+
scope,
|
|
1024
|
+
format!("failed to resolve module specifier {import}"),
|
|
1025
|
+
),
|
|
1026
|
+
};
|
|
1027
|
+
};
|
|
967
1028
|
// The dep must live in the context actually being linked — the auto-link of
|
|
968
1029
|
// a dynamic import runs in whatever realm import() fired in, which kAuto can
|
|
969
1030
|
// detach from the request that started it. A foreign-context module would
|
|
970
1031
|
// V8-CHECK-abort.
|
|
971
|
-
let
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1032
|
+
let found = istate!(scope)
|
|
1033
|
+
.modules
|
|
1034
|
+
.by_id
|
|
1035
|
+
.get(&dep_id)
|
|
1036
|
+
.map(|(g, _, cid)| (g.clone(), *cid));
|
|
1037
|
+
let g = match (here, found) {
|
|
1038
|
+
(Some(here), Some((g, cid))) if cid == here => g,
|
|
1039
|
+
(Some(_), Some(_)) => {
|
|
1040
|
+
return fail(
|
|
1041
|
+
scope,
|
|
1042
|
+
format!("resolver returned a module from another realm for {import}"),
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
// The realm being linked is gone. Not reachable while a module is
|
|
1046
|
+
// linking (see |here| above), but reporting it as a realm mismatch
|
|
1047
|
+
// would name the wrong cause.
|
|
1048
|
+
(None, _) => return fail(scope, format!("the realm being linked is gone ({import})")),
|
|
1049
|
+
// Only reachable if the module went away between the resolver handing it
|
|
1050
|
+
// over and this lookup; to the caller that reads the same as never
|
|
1051
|
+
// having resolved it.
|
|
1052
|
+
(_, None) => {
|
|
1053
|
+
return fail(
|
|
1054
|
+
scope,
|
|
1055
|
+
format!("failed to resolve module specifier {import}"),
|
|
1056
|
+
);
|
|
975
1057
|
}
|
|
976
|
-
g.clone()
|
|
977
1058
|
};
|
|
978
1059
|
Some(v8::Local::new(scope, &g))
|
|
979
1060
|
}
|
|
@@ -1916,8 +1997,16 @@ fn build_snapshot(code: &str, base: Option<Vec<u8>>, warmup: bool) -> Result<Vec
|
|
|
1916
1997
|
{
|
|
1917
1998
|
let cscope = &mut v8::ContextScope::new(scope, context);
|
|
1918
1999
|
if !code.is_empty()
|
|
1919
|
-
&& let Err(e) =
|
|
1920
|
-
|
|
2000
|
+
&& let Err(e) = run_source(
|
|
2001
|
+
cscope,
|
|
2002
|
+
code,
|
|
2003
|
+
if warmup { "<warmup>" } else { "<snapshot>" },
|
|
2004
|
+
// Snapshot/warmup code runs for its EFFECT (the heap it
|
|
2005
|
+
// leaves behind); nothing reads its completion value, so
|
|
2006
|
+
// don't marshal it — a bundle ending in an expression must
|
|
2007
|
+
// not fail the snapshot over a value no one wants.
|
|
2008
|
+
true,
|
|
2009
|
+
)
|
|
1921
2010
|
{
|
|
1922
2011
|
err = Some(match e {
|
|
1923
2012
|
VmError::Parse(m) | VmError::Runtime(m) => m,
|
|
@@ -2104,7 +2193,7 @@ impl Core {
|
|
|
2104
2193
|
}
|
|
2105
2194
|
if self.shared.lock().unwrap().disposed {
|
|
2106
2195
|
return Err(Error::new(
|
|
2107
|
-
ruby
|
|
2196
|
+
err_class(ruby, "DisposedError"),
|
|
2108
2197
|
"disposed context",
|
|
2109
2198
|
));
|
|
2110
2199
|
}
|
|
@@ -2280,7 +2369,7 @@ impl Core {
|
|
|
2280
2369
|
// isolate (every later op refuses) rather than risk using it.
|
|
2281
2370
|
self.shared.lock().unwrap().disposed = true;
|
|
2282
2371
|
Err(Error::new(
|
|
2283
|
-
ruby
|
|
2372
|
+
err_class(ruby, "InternalError"),
|
|
2284
2373
|
"internal error: operation panicked; the isolate has been disposed",
|
|
2285
2374
|
))
|
|
2286
2375
|
}
|
|
@@ -2302,7 +2391,7 @@ impl Core {
|
|
|
2302
2391
|
}
|
|
2303
2392
|
VmReply::Done(Err(e)) => Err(vm_err(ruby, e)),
|
|
2304
2393
|
_ => Err(Error::new(
|
|
2305
|
-
ruby
|
|
2394
|
+
err_class(ruby, "InternalError"),
|
|
2306
2395
|
"internal: unexpected reply kind",
|
|
2307
2396
|
)),
|
|
2308
2397
|
}
|
|
@@ -2402,7 +2491,7 @@ impl Core {
|
|
|
2402
2491
|
let reply = self.run(ruby, Request::HeapStatistics)?;
|
|
2403
2492
|
let VmReply::Heap(s) = reply else {
|
|
2404
2493
|
return Err(Error::new(
|
|
2405
|
-
ruby
|
|
2494
|
+
err_class(ruby, "InternalError"),
|
|
2406
2495
|
"internal: unexpected heap reply",
|
|
2407
2496
|
));
|
|
2408
2497
|
};
|
|
@@ -2441,6 +2530,7 @@ impl Core {
|
|
|
2441
2530
|
source: String,
|
|
2442
2531
|
filename: String,
|
|
2443
2532
|
timeout_ms: u64,
|
|
2533
|
+
void: bool,
|
|
2444
2534
|
) -> Result<Value, Error> {
|
|
2445
2535
|
let reply = self.run(
|
|
2446
2536
|
ruby,
|
|
@@ -2449,6 +2539,7 @@ impl Core {
|
|
|
2449
2539
|
source,
|
|
2450
2540
|
filename,
|
|
2451
2541
|
timeout_ms,
|
|
2542
|
+
void,
|
|
2452
2543
|
},
|
|
2453
2544
|
)?;
|
|
2454
2545
|
self.reply_value(ruby, reply)
|
|
@@ -2576,7 +2667,7 @@ impl Core {
|
|
|
2576
2667
|
VmReply::ModuleCompiled(Ok(cm)) => Ok(cm),
|
|
2577
2668
|
VmReply::ModuleCompiled(Err(e)) => Err(vm_err(ruby, e)),
|
|
2578
2669
|
_ => Err(Error::new(
|
|
2579
|
-
ruby
|
|
2670
|
+
err_class(ruby, "InternalError"),
|
|
2580
2671
|
"internal: unexpected compile reply",
|
|
2581
2672
|
)),
|
|
2582
2673
|
}
|
|
@@ -2649,6 +2740,11 @@ impl Core {
|
|
|
2649
2740
|
self.reply_value(ruby, reply)
|
|
2650
2741
|
}
|
|
2651
2742
|
|
|
2743
|
+
fn module_graph_async(&self, ruby: &Ruby, module_id: i32) -> Result<Value, Error> {
|
|
2744
|
+
let reply = self.run(ruby, Request::ModuleGraphAsync { module_id })?;
|
|
2745
|
+
self.reply_value(ruby, reply)
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2652
2748
|
fn dispose_module(&self, ruby: &Ruby, module_id: i32) -> Result<(), Error> {
|
|
2653
2749
|
let reply = self.run(ruby, Request::DisposeModule { module_id })?;
|
|
2654
2750
|
self.reply_value(ruby, reply).map(|_| ())
|
|
@@ -2681,18 +2777,19 @@ impl Core {
|
|
|
2681
2777
|
VmReply::ScriptCompiled(Ok(cs)) => Ok(cs),
|
|
2682
2778
|
VmReply::ScriptCompiled(Err(e)) => Err(vm_err(ruby, e)),
|
|
2683
2779
|
_ => Err(Error::new(
|
|
2684
|
-
ruby
|
|
2780
|
+
err_class(ruby, "InternalError"),
|
|
2685
2781
|
"internal: unexpected compile reply",
|
|
2686
2782
|
)),
|
|
2687
2783
|
}
|
|
2688
2784
|
}
|
|
2689
2785
|
|
|
2690
|
-
fn run_script(&self, ruby: &Ruby, script_id: i32) -> Result<Value, Error> {
|
|
2786
|
+
fn run_script(&self, ruby: &Ruby, script_id: i32, void: bool) -> Result<Value, Error> {
|
|
2691
2787
|
let reply = self.run(
|
|
2692
2788
|
ruby,
|
|
2693
2789
|
Request::RunScript {
|
|
2694
2790
|
script_id,
|
|
2695
2791
|
timeout_ms: self.default_timeout_ms,
|
|
2792
|
+
void,
|
|
2696
2793
|
},
|
|
2697
2794
|
)?;
|
|
2698
2795
|
self.reply_value(ruby, reply)
|
|
@@ -2794,7 +2891,7 @@ impl Core {
|
|
|
2794
2891
|
// and SEGV — refuse it, leaving the isolate usable.
|
|
2795
2892
|
if self.depth.load(Ordering::SeqCst) != 0 {
|
|
2796
2893
|
return Err(Error::new(
|
|
2797
|
-
ruby
|
|
2894
|
+
err_class(ruby, "Error"),
|
|
2798
2895
|
"RustyRacer: cannot dispose an isolate from within a running op or host callback",
|
|
2799
2896
|
));
|
|
2800
2897
|
}
|
|
@@ -2915,19 +3012,21 @@ impl Context {
|
|
|
2915
3012
|
// id 0's lifetime is the isolate's; extras also track their own dispose.
|
|
2916
3013
|
if self.disposed.load(Ordering::SeqCst) || self.core.is_disposed() {
|
|
2917
3014
|
return Err(Error::new(
|
|
2918
|
-
ruby
|
|
3015
|
+
err_class(ruby, "DisposedError"),
|
|
2919
3016
|
"disposed context",
|
|
2920
3017
|
));
|
|
2921
3018
|
}
|
|
2922
3019
|
Ok(())
|
|
2923
3020
|
}
|
|
2924
3021
|
// timeout_ms 0 = use the isolate's default; an explicit value overrides it.
|
|
3022
|
+
// |void| discards the completion value instead of marshalling it.
|
|
2925
3023
|
fn eval(
|
|
2926
3024
|
ruby: &Ruby,
|
|
2927
3025
|
rb_self: &Self,
|
|
2928
3026
|
source: String,
|
|
2929
3027
|
timeout_ms: u64,
|
|
2930
3028
|
filename: String,
|
|
3029
|
+
void: bool,
|
|
2931
3030
|
) -> Result<Value, Error> {
|
|
2932
3031
|
rb_self.check_live(ruby)?;
|
|
2933
3032
|
let timeout = if timeout_ms == 0 {
|
|
@@ -2937,7 +3036,7 @@ impl Context {
|
|
|
2937
3036
|
};
|
|
2938
3037
|
rb_self
|
|
2939
3038
|
.core
|
|
2940
|
-
.eval_t(ruby, rb_self.id, source, filename, timeout)
|
|
3039
|
+
.eval_t(ruby, rb_self.id, source, filename, timeout, void)
|
|
2941
3040
|
}
|
|
2942
3041
|
fn call(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
|
|
2943
3042
|
rb_self.check_live(ruby)?;
|
|
@@ -3112,7 +3211,7 @@ impl JsModule {
|
|
|
3112
3211
|
// iso.dispose is a use-after-free.
|
|
3113
3212
|
if self.disposed.load(Ordering::SeqCst) || self.core.is_disposed() {
|
|
3114
3213
|
return Err(Error::new(
|
|
3115
|
-
ruby
|
|
3214
|
+
err_class(ruby, "DisposedError"),
|
|
3116
3215
|
"disposed module",
|
|
3117
3216
|
));
|
|
3118
3217
|
}
|
|
@@ -3140,6 +3239,19 @@ impl JsModule {
|
|
|
3140
3239
|
rb_self.check_live(ruby)?;
|
|
3141
3240
|
rb_self.core.module_status(ruby, rb_self.module_id)
|
|
3142
3241
|
}
|
|
3242
|
+
// True when top-level await appears anywhere in the module's LINKED import
|
|
3243
|
+
// graph (v8::Module::IsGraphAsync) — a dependency's await counts, which is
|
|
3244
|
+
// why no check on the source text alone can stand in for this. FALSE is the
|
|
3245
|
+
// load-bearing answer: V8 guarantees the evaluation promise is settled, so
|
|
3246
|
+
// #evaluate returning means the whole graph ran. True and it may have
|
|
3247
|
+
// returned with the body still suspended, while #status reads :evaluated
|
|
3248
|
+
// regardless (V8 folds its internal kEvaluatingAsync into kEvaluated).
|
|
3249
|
+
// Raises RustyRacer::RuntimeError unless the module is instantiated: before
|
|
3250
|
+
// linking there is no graph to walk.
|
|
3251
|
+
fn graph_async(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
3252
|
+
rb_self.check_live(ruby)?;
|
|
3253
|
+
rb_self.core.module_graph_async(ruby, rb_self.module_id)
|
|
3254
|
+
}
|
|
3143
3255
|
// The bytecode cache produced at compile (produce_cache: true), as a binary
|
|
3144
3256
|
// String, or nil. Persist it cross-process and pass back via cached_data:.
|
|
3145
3257
|
fn cached_data(ruby: &Ruby, rb_self: &Self) -> Value {
|
|
@@ -3176,7 +3288,7 @@ impl Script {
|
|
|
3176
3288
|
// Also refuse once the isolate is disposed (see JsModule::check_live).
|
|
3177
3289
|
if self.disposed.load(Ordering::SeqCst) || self.core.is_disposed() {
|
|
3178
3290
|
return Err(Error::new(
|
|
3179
|
-
ruby
|
|
3291
|
+
err_class(ruby, "DisposedError"),
|
|
3180
3292
|
"disposed script",
|
|
3181
3293
|
));
|
|
3182
3294
|
}
|
|
@@ -3186,7 +3298,13 @@ impl Script {
|
|
|
3186
3298
|
// thrown exception is a RuntimeError; a timeout/stop a ScriptTerminatedError.
|
|
3187
3299
|
fn run(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
3188
3300
|
rb_self.check_live(ruby)?;
|
|
3189
|
-
rb_self.core.run_script(ruby, rb_self.script_id)
|
|
3301
|
+
rb_self.core.run_script(ruby, rb_self.script_id, false)
|
|
3302
|
+
}
|
|
3303
|
+
// Run it for its effect only, discarding the completion value (see
|
|
3304
|
+
// Context#eval_void) — what a <script> tag does.
|
|
3305
|
+
fn run_void(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
3306
|
+
rb_self.check_live(ruby)?;
|
|
3307
|
+
rb_self.core.run_script(ruby, rb_self.script_id, true)
|
|
3190
3308
|
}
|
|
3191
3309
|
fn cached_data(ruby: &Ruby, rb_self: &Self) -> Value {
|
|
3192
3310
|
code_cache_value(ruby, rb_self.cached_data.as_ref())
|
|
@@ -3231,7 +3349,7 @@ fn code_cache_from_reply(ruby: &Ruby, reply: VmReply) -> Result<Option<Vec<u8>>,
|
|
|
3231
3349
|
VmReply::CodeCache(Ok(bytes)) => Ok(bytes),
|
|
3232
3350
|
VmReply::CodeCache(Err(e)) => Err(vm_err(ruby, e)),
|
|
3233
3351
|
_ => Err(Error::new(
|
|
3234
|
-
ruby
|
|
3352
|
+
err_class(ruby, "InternalError"),
|
|
3235
3353
|
"internal: unexpected code-cache reply",
|
|
3236
3354
|
)),
|
|
3237
3355
|
}
|
|
@@ -3367,7 +3485,7 @@ fn resolve_module_via_ruby(
|
|
|
3367
3485
|
})?;
|
|
3368
3486
|
if !std::ptr::eq(Arc::as_ptr(&obj.core), core as *const Core) {
|
|
3369
3487
|
return Err(Error::new(
|
|
3370
|
-
ruby
|
|
3488
|
+
err_class(&ruby, "Error"),
|
|
3371
3489
|
"module resolver returned a Module from a different Context",
|
|
3372
3490
|
));
|
|
3373
3491
|
}
|
|
@@ -3407,8 +3525,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
3407
3525
|
|
|
3408
3526
|
// A v8::Context (realm): eval/call/attach/compile_module.
|
|
3409
3527
|
let context = module.define_class("Context", ruby.class_object())?;
|
|
3410
|
-
// keyword-arg
|
|
3411
|
-
|
|
3528
|
+
// Backs both keyword-arg wrappers in lib: Context#eval(source, timeout_ms:,
|
|
3529
|
+
// filename:) and #eval_void, which differ only in the 4th arg (the void flag).
|
|
3530
|
+
context.define_method("_eval", method!(Context::eval, 4))?;
|
|
3412
3531
|
context.define_method("call", method!(Context::call, -1))?;
|
|
3413
3532
|
context.define_method("call_void", method!(Context::call_void, -1))?;
|
|
3414
3533
|
context.define_method("attach", method!(Context::attach, 2))?;
|
|
@@ -3424,6 +3543,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
3424
3543
|
// Classic compiled script: Context#compile -> #run / #cached_data.
|
|
3425
3544
|
let script = module.define_class("Script", ruby.class_object())?;
|
|
3426
3545
|
script.define_method("run", method!(Script::run, 0))?;
|
|
3546
|
+
script.define_method("run_void", method!(Script::run_void, 0))?;
|
|
3427
3547
|
script.define_method("cached_data", method!(Script::cached_data, 0))?;
|
|
3428
3548
|
script.define_method("cache_rejected?", method!(Script::cache_rejected, 0))?;
|
|
3429
3549
|
script.define_method("create_code_cache", method!(Script::create_code_cache, 0))?;
|
|
@@ -3444,6 +3564,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
3444
3564
|
jsmodule.define_method("evaluate", method!(JsModule::evaluate, 0))?;
|
|
3445
3565
|
jsmodule.define_method("namespace", method!(JsModule::namespace, 0))?;
|
|
3446
3566
|
jsmodule.define_method("_status", method!(JsModule::status, 0))?;
|
|
3567
|
+
jsmodule.define_method("graph_async?", method!(JsModule::graph_async, 0))?;
|
|
3447
3568
|
jsmodule.define_method("cached_data", method!(JsModule::cached_data, 0))?;
|
|
3448
3569
|
jsmodule.define_method("cache_rejected?", method!(JsModule::cache_rejected, 0))?;
|
|
3449
3570
|
jsmodule.define_method("create_code_cache", method!(JsModule::create_code_cache, 0))?;
|
|
@@ -149,7 +149,7 @@ fn js_container_id(
|
|
|
149
149
|
// First sighting but too deep: truncate WITHOUT registering, so no later
|
|
150
150
|
// Ref can target a container that was never emitted.
|
|
151
151
|
if depth >= MAX_MARSHAL_DEPTH {
|
|
152
|
-
return Err(JsVal::Str(
|
|
152
|
+
return Err(JsVal::Str(stringify(scope, value)));
|
|
153
153
|
}
|
|
154
154
|
let id = seen.next_id;
|
|
155
155
|
seen.next_id += 1;
|
|
@@ -342,7 +342,7 @@ fn js_to_jsval_d(
|
|
|
342
342
|
let Some(key) = names.get_index(scope, i) else {
|
|
343
343
|
continue;
|
|
344
344
|
};
|
|
345
|
-
let key_str =
|
|
345
|
+
let key_str = stringify(scope, key);
|
|
346
346
|
let val = obj
|
|
347
347
|
.get(scope, key)
|
|
348
348
|
.unwrap_or_else(|| v8::undefined(scope).into());
|
|
@@ -350,8 +350,43 @@ fn js_to_jsval_d(
|
|
|
350
350
|
}
|
|
351
351
|
return JsVal::Obj { id, entries };
|
|
352
352
|
}
|
|
353
|
+
// Enumeration THREW — a module namespace whose bindings aren't
|
|
354
|
+
// initialized yet, a Proxy with a hostile ownKeys trap. Falling through
|
|
355
|
+
// to the stringify below would hand the caller a plausible-looking
|
|
356
|
+
// "[object Object]" for an object we never actually read. Leave the
|
|
357
|
+
// pending exception alone instead and let the op's TryCatch report it —
|
|
358
|
+
// and give back the id, or a sibling Ref to this same object would point
|
|
359
|
+
// at a container that is never emitted.
|
|
360
|
+
unregister_container(seen, obj, id);
|
|
361
|
+
return JsVal::Undefined;
|
|
362
|
+
}
|
|
363
|
+
JsVal::Str(stringify(scope, value))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// The escape hatch for values with no structural representation — Function,
|
|
367
|
+
// Date, RegExp, Symbol. to_rust_string_lossy is Value::ToString, which THROWS on
|
|
368
|
+
// a Symbol, and a Symbol sitting anywhere in a graph (every React element has
|
|
369
|
+
// one) would then leave a pending exception behind and fail the whole op over a
|
|
370
|
+
// value that marshalled fine. to_detail_string is V8's no-side-effects renderer
|
|
371
|
+
// and cannot throw, so Symbols go through it.
|
|
372
|
+
fn stringify(scope: &mut v8::PinScope<'_, '_>, value: v8::Local<v8::Value>) -> String {
|
|
373
|
+
if value.is_symbol() {
|
|
374
|
+
return value
|
|
375
|
+
.to_detail_string(scope)
|
|
376
|
+
.map(|s| s.to_rust_string_lossy(scope))
|
|
377
|
+
.unwrap_or_default();
|
|
378
|
+
}
|
|
379
|
+
value.to_rust_string_lossy(scope)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Undo js_container_id's registration for a container we end up NOT emitting.
|
|
383
|
+
// The id is always the last one pushed to its bucket, since nothing else can
|
|
384
|
+
// register while we are inside this object.
|
|
385
|
+
fn unregister_container(seen: &mut JsSeen, obj: v8::Local<v8::Object>, id: u32) {
|
|
386
|
+
let hash = obj.get_identity_hash().get();
|
|
387
|
+
if let Some(bucket) = seen.map.get_mut(&hash) {
|
|
388
|
+
bucket.retain(|(_, existing)| *existing != id);
|
|
353
389
|
}
|
|
354
|
-
JsVal::Str(value.to_rust_string_lossy(scope))
|
|
355
390
|
}
|
|
356
391
|
|
|
357
392
|
// Owned-by-value (not &JsVal): a JsVal::Bytes hands its Vec straight to V8's
|
data/ext/rusty_racer/src/ops.rs
CHANGED
|
@@ -18,6 +18,29 @@ use crate::istate;
|
|
|
18
18
|
use crate::marshal::{JsVal, js_to_jsval, jsval_to_js};
|
|
19
19
|
use crate::*;
|
|
20
20
|
|
|
21
|
+
// Marshal an op's result, treating a throw on the way out as the op's outcome.
|
|
22
|
+
// Marshalling RUNS JS — accessors, Proxy traps, toString — and an exception
|
|
23
|
+
// raised there is as real as one from the script itself, but it lands after the
|
|
24
|
+
// call has already succeeded, so nothing else looks at it: the TryCatch drops it
|
|
25
|
+
// on unwind and the caller keeps a value that no longer means anything (an
|
|
26
|
+
// object whose enumeration threw marshals to undefined). A macro rather than a
|
|
27
|
+
// fn because the tc_scope! binding has no nameable type here.
|
|
28
|
+
macro_rules! marshalled {
|
|
29
|
+
($tc:expr, $value:expr) => {{
|
|
30
|
+
let tc = $tc;
|
|
31
|
+
let out = js_to_jsval(tc, $value);
|
|
32
|
+
let exc = tc.exception();
|
|
33
|
+
if exc.is_none() {
|
|
34
|
+
Ok(out)
|
|
35
|
+
} else if tc.has_terminated() {
|
|
36
|
+
Err(VmError::Terminated)
|
|
37
|
+
} else {
|
|
38
|
+
let stack = tc.stack_trace();
|
|
39
|
+
Err(capture_js_error(tc, exc, stack))
|
|
40
|
+
}
|
|
41
|
+
}};
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
// One VM operation, built by a magnus method and run inline by Core::run ->
|
|
22
45
|
// service_request -> dispatch_one. |context_id| selects which realm the op runs
|
|
23
46
|
// in: 0 = the main realm (Context's own globalThis, swappable by reset_realm),
|
|
@@ -28,6 +51,11 @@ pub(crate) enum Request {
|
|
|
28
51
|
source: String,
|
|
29
52
|
filename: String,
|
|
30
53
|
timeout_ms: u64,
|
|
54
|
+
// void = don't marshal the completion value (see Call's |void|). A
|
|
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,
|
|
31
59
|
},
|
|
32
60
|
// Resolve a dotted function path on globalThis and invoke it with marshalled
|
|
33
61
|
// args (v8::Function::call), preserving the holder as `this`. Distinct from
|
|
@@ -104,6 +132,11 @@ pub(crate) enum Request {
|
|
|
104
132
|
ModuleStatus {
|
|
105
133
|
module_id: i32,
|
|
106
134
|
},
|
|
135
|
+
// Whether top-level await appears anywhere in the module's linked import
|
|
136
|
+
// graph (v8::Module::IsGraphAsync), as a bool.
|
|
137
|
+
ModuleGraphAsync {
|
|
138
|
+
module_id: i32,
|
|
139
|
+
},
|
|
107
140
|
DisposeModule {
|
|
108
141
|
module_id: i32,
|
|
109
142
|
},
|
|
@@ -118,10 +151,12 @@ pub(crate) enum Request {
|
|
|
118
151
|
produce_cache: bool,
|
|
119
152
|
eager: bool,
|
|
120
153
|
},
|
|
121
|
-
// Bind the script to its context and run it; returns the completion value
|
|
154
|
+
// Bind the script to its context and run it; returns the completion value
|
|
155
|
+
// (unless |void| — see Eval).
|
|
122
156
|
RunScript {
|
|
123
157
|
script_id: i32,
|
|
124
158
|
timeout_ms: u64,
|
|
159
|
+
void: bool,
|
|
125
160
|
},
|
|
126
161
|
DisposeScript {
|
|
127
162
|
script_id: i32,
|
|
@@ -192,6 +227,7 @@ pub(crate) fn run_source(
|
|
|
192
227
|
scope: &mut v8::PinScope<'_, '_>,
|
|
193
228
|
source: &str,
|
|
194
229
|
filename: &str,
|
|
230
|
+
void: bool,
|
|
195
231
|
) -> Result<JsVal, VmError> {
|
|
196
232
|
v8::tc_scope!(let tc, scope);
|
|
197
233
|
// Compile and run as distinct phases so a compile failure maps to
|
|
@@ -224,7 +260,8 @@ pub(crate) fn run_source(
|
|
|
224
260
|
}
|
|
225
261
|
};
|
|
226
262
|
match script.run(tc) {
|
|
227
|
-
Some(
|
|
263
|
+
Some(_) if void => Ok(JsVal::Undefined),
|
|
264
|
+
Some(value) => marshalled!(tc, value),
|
|
228
265
|
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
229
266
|
None => {
|
|
230
267
|
let exc = tc.exception();
|
|
@@ -279,7 +316,7 @@ fn call_function(
|
|
|
279
316
|
match func.call(tc, recv, &argv) {
|
|
280
317
|
// void: skip marshalling the return so a huge/cyclic result is never walked.
|
|
281
318
|
Some(_) if void => Ok(JsVal::Undefined),
|
|
282
|
-
Some(value) =>
|
|
319
|
+
Some(value) => marshalled!(tc, value),
|
|
283
320
|
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
284
321
|
None => {
|
|
285
322
|
let exc = tc.exception();
|
|
@@ -396,6 +433,7 @@ fn request_realm(state: &IsolateState, request: &Request) -> Option<i32> {
|
|
|
396
433
|
| Request::CreateContext
|
|
397
434
|
| Request::DisposeContext { .. }
|
|
398
435
|
| Request::ModuleStatus { .. }
|
|
436
|
+
| Request::ModuleGraphAsync { .. }
|
|
399
437
|
| Request::DisposeModule { .. }
|
|
400
438
|
| Request::DisposeScript { .. }
|
|
401
439
|
| Request::ScriptCodeCache { .. }
|
|
@@ -419,7 +457,10 @@ fn dispatch_one(
|
|
|
419
457
|
source,
|
|
420
458
|
filename,
|
|
421
459
|
timeout_ms,
|
|
422
|
-
|
|
460
|
+
void,
|
|
461
|
+
} => op_eval(
|
|
462
|
+
scope, context_id, source, filename, timeout_ms, void, outermost,
|
|
463
|
+
),
|
|
423
464
|
Request::Call {
|
|
424
465
|
context_id,
|
|
425
466
|
name,
|
|
@@ -465,6 +506,7 @@ fn dispatch_one(
|
|
|
465
506
|
} => op_evaluate_module(scope, module_id, timeout_ms, outermost),
|
|
466
507
|
Request::ModuleNamespace { module_id } => op_module_namespace(scope, module_id),
|
|
467
508
|
Request::ModuleStatus { module_id } => op_module_status(scope, module_id),
|
|
509
|
+
Request::ModuleGraphAsync { module_id } => op_module_graph_async(scope, module_id),
|
|
468
510
|
Request::DisposeModule { module_id } => op_dispose_module(scope, module_id),
|
|
469
511
|
Request::CompileScript {
|
|
470
512
|
context_id,
|
|
@@ -485,7 +527,8 @@ fn dispatch_one(
|
|
|
485
527
|
Request::RunScript {
|
|
486
528
|
script_id,
|
|
487
529
|
timeout_ms,
|
|
488
|
-
|
|
530
|
+
void,
|
|
531
|
+
} => op_run_script(scope, script_id, timeout_ms, void, outermost),
|
|
489
532
|
Request::DisposeScript { script_id } => op_dispose_script(scope, script_id),
|
|
490
533
|
// Serialize the script's CURRENT compile state. The stored handle is
|
|
491
534
|
// the UnboundScript, which V8 fills in with inner-function bytecode as
|
|
@@ -528,12 +571,14 @@ fn op_low_memory_notification(scope: &mut v8::PinScope<'_, '_, ()>) -> VmReply {
|
|
|
528
571
|
VmReply::Done(Ok(JsVal::Undefined))
|
|
529
572
|
}
|
|
530
573
|
|
|
574
|
+
#[allow(clippy::too_many_arguments)]
|
|
531
575
|
fn op_eval(
|
|
532
576
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
533
577
|
context_id: i32,
|
|
534
578
|
source: String,
|
|
535
579
|
filename: String,
|
|
536
580
|
timeout_ms: u64,
|
|
581
|
+
void: bool,
|
|
537
582
|
outermost: bool,
|
|
538
583
|
) -> VmReply {
|
|
539
584
|
let outcome = run_js_bracketed(scope, outermost, timeout_ms, "eval", |scope, outermost| {
|
|
@@ -542,7 +587,7 @@ fn op_eval(
|
|
|
542
587
|
Some(ctx) => {
|
|
543
588
|
let context = v8::Local::new(scope, &ctx);
|
|
544
589
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
545
|
-
let out = run_source(scope, &source, &filename);
|
|
590
|
+
let out = run_source(scope, &source, &filename, void);
|
|
546
591
|
auto_drain(scope, outermost);
|
|
547
592
|
(true, out)
|
|
548
593
|
}
|
|
@@ -1065,6 +1110,20 @@ fn op_evaluate_module(
|
|
|
1065
1110
|
VmReply::Done(outcome)
|
|
1066
1111
|
}
|
|
1067
1112
|
|
|
1113
|
+
// get_module_namespace and is_graph_async both CHECK-abort (a release check, so
|
|
1114
|
+
// it kills the process) below Instantiated, so both ask this first. Everything
|
|
1115
|
+
// from Instantiated up is admissible, Errored included: a module that fails to
|
|
1116
|
+
// LINK is reset to Uninstantiated by V8's ResetGraph, so Errored can only mean
|
|
1117
|
+
// "linked, then evaluation failed" — the graph is still there to read.
|
|
1118
|
+
fn require_instantiated(module: v8::Local<v8::Module>, what: &str) -> Result<(), VmError> {
|
|
1119
|
+
match module.get_status() {
|
|
1120
|
+
v8::ModuleStatus::Uninstantiated | v8::ModuleStatus::Instantiating => Err(
|
|
1121
|
+
VmError::Runtime(format!("module must be instantiated before {what}")),
|
|
1122
|
+
),
|
|
1123
|
+
_ => Ok(()),
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1068
1127
|
fn op_module_namespace(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
1069
1128
|
let handle = module_handle(istate!(scope), module_id);
|
|
1070
1129
|
let outcome = match handle {
|
|
@@ -1075,15 +1134,27 @@ fn op_module_namespace(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) ->
|
|
|
1075
1134
|
let context = v8::Local::new(scope, &cx);
|
|
1076
1135
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
1077
1136
|
let module = v8::Local::new(scope, &g);
|
|
1078
|
-
//
|
|
1079
|
-
// is
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1137
|
+
// An errored module's namespace is unreadable — its bindings
|
|
1138
|
+
// never initialized — and the useful answer is WHY it errored,
|
|
1139
|
+
// the same one instantiate and evaluate give.
|
|
1140
|
+
if module.get_status() == v8::ModuleStatus::Errored {
|
|
1141
|
+
Err(VmError::JsError {
|
|
1142
|
+
message: module.get_exception().to_rust_string_lossy(scope),
|
|
1143
|
+
backtrace: vec![],
|
|
1144
|
+
})
|
|
1145
|
+
} else {
|
|
1146
|
+
match require_instantiated(module, "namespace") {
|
|
1147
|
+
Err(e) => Err(e),
|
|
1148
|
+
Ok(()) => {
|
|
1149
|
+
let ns = module.get_module_namespace();
|
|
1150
|
+
// Reading the namespace RUNS JS: every export is an
|
|
1151
|
+
// accessor, and one whose binding is still
|
|
1152
|
+
// uninitialized throws. Unwatched, that throw made
|
|
1153
|
+
// enumeration fail and the caller silently got the
|
|
1154
|
+
// stringified object in place of their exports.
|
|
1155
|
+
v8::tc_scope!(let tc, scope);
|
|
1156
|
+
marshalled!(tc, ns)
|
|
1157
|
+
}
|
|
1087
1158
|
}
|
|
1088
1159
|
}
|
|
1089
1160
|
}
|
|
@@ -1112,6 +1183,26 @@ fn op_module_status(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmR
|
|
|
1112
1183
|
VmReply::Done(outcome)
|
|
1113
1184
|
}
|
|
1114
1185
|
|
|
1186
|
+
// Does top-level await appear anywhere in this module's import graph? V8 walks
|
|
1187
|
+
// the linked graph from here, so an `await` in a dependency counts too — which
|
|
1188
|
+
// is why no amount of looking at one source text can answer it, and why it needs
|
|
1189
|
+
// an instantiated module: before linking there is no graph to walk. What V8
|
|
1190
|
+
// guarantees is the false case — "if IsGraphAsync() is false, the returned
|
|
1191
|
+
// Promise is settled" — i.e. false means #evaluate ran the whole graph to
|
|
1192
|
+
// completion. No context is entered: the walk reads module slots, runs no JS.
|
|
1193
|
+
fn op_module_graph_async(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
1194
|
+
let handle = module_handle(istate!(scope), module_id);
|
|
1195
|
+
let outcome = match handle {
|
|
1196
|
+
None => Err(VmError::Runtime("unknown module".into())),
|
|
1197
|
+
Some((g, _cid)) => {
|
|
1198
|
+
let module = v8::Local::new(scope, &g);
|
|
1199
|
+
require_instantiated(module, "graph_async?")
|
|
1200
|
+
.map(|()| JsVal::Bool(module.is_graph_async()))
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
VmReply::Done(outcome)
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1115
1206
|
fn op_dispose_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
1116
1207
|
let m = &mut istate!(scope).modules;
|
|
1117
1208
|
m.by_id.remove(&module_id);
|
|
@@ -1202,6 +1293,7 @@ fn op_run_script(
|
|
|
1202
1293
|
scope: &mut v8::PinScope<'_, '_, ()>,
|
|
1203
1294
|
script_id: i32,
|
|
1204
1295
|
timeout_ms: u64,
|
|
1296
|
+
void: bool,
|
|
1205
1297
|
outermost: bool,
|
|
1206
1298
|
) -> VmReply {
|
|
1207
1299
|
let outcome = run_js_bracketed(
|
|
@@ -1226,7 +1318,8 @@ fn op_run_script(
|
|
|
1226
1318
|
let out = {
|
|
1227
1319
|
v8::tc_scope!(let tc, scope);
|
|
1228
1320
|
match script.run(tc) {
|
|
1229
|
-
Some(
|
|
1321
|
+
Some(_) if void => Ok(JsVal::Undefined),
|
|
1322
|
+
Some(value) => marshalled!(tc, value),
|
|
1230
1323
|
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
1231
1324
|
None => {
|
|
1232
1325
|
let exc = tc.exception();
|
data/lib/rusty_racer/execjs.rb
CHANGED
|
@@ -35,9 +35,15 @@ module RustyRacer
|
|
|
35
35
|
# ExecJS guarantees a bare global (no browser/Node ambient): V8 installs a
|
|
36
36
|
# default `console`, so drop it to match the contract (consumers attach
|
|
37
37
|
# their own if needed), exactly as the mini_racer runtime does.
|
|
38
|
-
@context.
|
|
38
|
+
@context.eval_void('delete globalThis.console')
|
|
39
39
|
source = encode(source)
|
|
40
|
-
|
|
40
|
+
# eval_void: a bundle is run to POPULATE the context, and its completion
|
|
41
|
+
# value — the last thing a UMD wrapper happened to evaluate — is nobody's
|
|
42
|
+
# answer. Marshalling it would walk the whole exports graph on every
|
|
43
|
+
# context creation, and a lazy/hostile property there would fail the
|
|
44
|
+
# constructor over a value ExecJS discards. #exec/#eval below must NOT
|
|
45
|
+
# use it: they read the JSON their wrapper returns.
|
|
46
|
+
translate { @context.eval_void(source, filename: LOCATION) } if /\S/.match?(source)
|
|
41
47
|
end
|
|
42
48
|
|
|
43
49
|
# Run statements in a function body and return what they `return` (nil when
|
data/lib/rusty_racer/version.rb
CHANGED
data/lib/rusty_racer.rb
CHANGED
|
@@ -42,6 +42,19 @@ module RustyRacer
|
|
|
42
42
|
class SnapshotError < Error; end
|
|
43
43
|
class PlatformAlreadyInitialized < Error; end
|
|
44
44
|
|
|
45
|
+
# Raised when a disposed Isolate, Context, Module or Script is used — or one
|
|
46
|
+
# whose Isolate has been disposed, which takes everything it handed out with
|
|
47
|
+
# it. Disposal is deliberate, so this says "you used it after you released
|
|
48
|
+
# it", not "something went wrong".
|
|
49
|
+
class DisposedError < Error; end
|
|
50
|
+
|
|
51
|
+
# A broken invariant inside the binding rather than anything the caller did:
|
|
52
|
+
# an unexpected reply kind, or an operation that panicked. Reaching one is a
|
|
53
|
+
# bug in rusty_racer and worth reporting. It exists so that EVERY error the
|
|
54
|
+
# extension raises is a RustyRacer::Error, and one rescue can cover the
|
|
55
|
+
# library.
|
|
56
|
+
class InternalError < Error; end
|
|
57
|
+
|
|
45
58
|
# Raised when an Isolate (or a Context/Module/Script it handed out) is used
|
|
46
59
|
# from a thread other than the one that created it. An isolate is
|
|
47
60
|
# thread-confined: every operation must run on its owner thread. The lone
|
|
@@ -99,7 +112,18 @@ module RustyRacer
|
|
|
99
112
|
# `timeout_ms` (0 = the isolate default) caps this eval; `filename` names the
|
|
100
113
|
# script in stack traces and parse-error locations.
|
|
101
114
|
def eval(source, timeout_ms: 0, filename: '<eval>')
|
|
102
|
-
_eval(source, timeout_ms, filename)
|
|
115
|
+
_eval(source, timeout_ms, filename, false)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Run `source` for its EFFECT and return nil, without marshalling its
|
|
119
|
+
# completion value — the counterpart of #call_void, and what a <script> tag
|
|
120
|
+
# does. Worth reaching for whenever the value is incidental: a statement
|
|
121
|
+
# list still has one (its last statement's), and marshalling reads the
|
|
122
|
+
# value, which runs JS — a getter or a Proxy trap can throw, failing an eval
|
|
123
|
+
# whose writes all landed. `globalThis.win = someProxy` is the shape that
|
|
124
|
+
# bites.
|
|
125
|
+
def eval_void(source, timeout_ms: 0, filename: '<eval>')
|
|
126
|
+
_eval(source, timeout_ms, filename, true)
|
|
103
127
|
end
|
|
104
128
|
|
|
105
129
|
# Compile a classic <script>; returns a RustyRacer::Script to #run.
|
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.1
|
|
4
|
+
version: 0.2.1
|
|
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-08-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|