rusty_racer 0.1.16 → 0.2.0
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 +15 -0
- data/ext/rusty_racer/Cargo.toml +1 -1
- data/ext/rusty_racer/src/lib.rs +132 -32
- data/ext/rusty_racer/src/marshal.rs +38 -3
- data/ext/rusty_racer/src/ops.rs +88 -12
- data/lib/rusty_racer/version.rb +1 -1
- data/lib/rusty_racer.rb +13 -0
- 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: f26a45e0afbed589acb2749fc1817e7d8508622c81a70fc378dc4af4a0f54714
|
|
4
|
+
data.tar.gz: 718074b8d9f468b9ab3da144391ecbeff1d00db347812e5209cd11d866f78f6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 03b227aa3eba1f597b34bca826f947c4345a8c16accebce84f267d5fcb3df2e9c72e3343e261be857387ada97000769dfca0b449130c9056bc8c6aceb569cd03
|
|
7
|
+
data.tar.gz: fbe970e400edf1041271954df3ba7f3be5d1f8f3775ca29d09398065b85d9b813bc9ca8b9ae8302bd32757884f8c47af5ba9953937ca6de0486af8bd7e50959b
|
data/README.md
CHANGED
|
@@ -79,6 +79,12 @@ rescue RustyRacer::RuntimeError => e
|
|
|
79
79
|
end
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
Everything the binding raises descends from `RustyRacer::Error`, so one rescue
|
|
83
|
+
covers the library. Under it: `ParseError`, `RuntimeError`,
|
|
84
|
+
`ScriptTerminatedError` and `V8OutOfMemoryError` for what the JS did;
|
|
85
|
+
`DisposedError` for using an isolate (or anything it handed out) after
|
|
86
|
+
disposing it; `WrongThreadError` for reaching an isolate from the wrong thread.
|
|
87
|
+
|
|
82
88
|
ES modules (the embedder owns the URL→module registry):
|
|
83
89
|
|
|
84
90
|
```ruby
|
|
@@ -86,10 +92,19 @@ dep = ctx.compile_module("export const x = 21;", filename: "/dep.js")
|
|
|
86
92
|
app = ctx.compile_module('import {x} from "./dep.js"; export const r = x * 2;',
|
|
87
93
|
filename: "/app.js")
|
|
88
94
|
app.instantiate { |specifier, referrer| dep if specifier == "./dep.js" }
|
|
95
|
+
app.graph_async? # => false
|
|
89
96
|
app.evaluate
|
|
90
97
|
app.namespace["r"] # => 42
|
|
91
98
|
```
|
|
92
99
|
|
|
100
|
+
`graph_async?` asks whether top-level `await` appears anywhere in the *linked*
|
|
101
|
+
graph, so an `await` in a dependency counts — which is why it needs an
|
|
102
|
+
instantiated module and raises before then. `false` is the answer that carries a
|
|
103
|
+
guarantee: `evaluate` ran the whole graph to completion. Since there is no event
|
|
104
|
+
loop here (see below), a `true` graph whose `await` never settles returns from
|
|
105
|
+
`evaluate` with the module body still suspended, and `status` reports
|
|
106
|
+
`:evaluated` either way — so `graph_async?` is what distinguishes the two.
|
|
107
|
+
|
|
93
108
|
Classic `<script>`s work the same way: `ctx.compile("1 + 1").run` # => 2.
|
|
94
109
|
|
|
95
110
|
### 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
|
}
|
|
@@ -2104,7 +2185,7 @@ impl Core {
|
|
|
2104
2185
|
}
|
|
2105
2186
|
if self.shared.lock().unwrap().disposed {
|
|
2106
2187
|
return Err(Error::new(
|
|
2107
|
-
ruby
|
|
2188
|
+
err_class(ruby, "DisposedError"),
|
|
2108
2189
|
"disposed context",
|
|
2109
2190
|
));
|
|
2110
2191
|
}
|
|
@@ -2280,7 +2361,7 @@ impl Core {
|
|
|
2280
2361
|
// isolate (every later op refuses) rather than risk using it.
|
|
2281
2362
|
self.shared.lock().unwrap().disposed = true;
|
|
2282
2363
|
Err(Error::new(
|
|
2283
|
-
ruby
|
|
2364
|
+
err_class(ruby, "InternalError"),
|
|
2284
2365
|
"internal error: operation panicked; the isolate has been disposed",
|
|
2285
2366
|
))
|
|
2286
2367
|
}
|
|
@@ -2302,7 +2383,7 @@ impl Core {
|
|
|
2302
2383
|
}
|
|
2303
2384
|
VmReply::Done(Err(e)) => Err(vm_err(ruby, e)),
|
|
2304
2385
|
_ => Err(Error::new(
|
|
2305
|
-
ruby
|
|
2386
|
+
err_class(ruby, "InternalError"),
|
|
2306
2387
|
"internal: unexpected reply kind",
|
|
2307
2388
|
)),
|
|
2308
2389
|
}
|
|
@@ -2402,7 +2483,7 @@ impl Core {
|
|
|
2402
2483
|
let reply = self.run(ruby, Request::HeapStatistics)?;
|
|
2403
2484
|
let VmReply::Heap(s) = reply else {
|
|
2404
2485
|
return Err(Error::new(
|
|
2405
|
-
ruby
|
|
2486
|
+
err_class(ruby, "InternalError"),
|
|
2406
2487
|
"internal: unexpected heap reply",
|
|
2407
2488
|
));
|
|
2408
2489
|
};
|
|
@@ -2576,7 +2657,7 @@ impl Core {
|
|
|
2576
2657
|
VmReply::ModuleCompiled(Ok(cm)) => Ok(cm),
|
|
2577
2658
|
VmReply::ModuleCompiled(Err(e)) => Err(vm_err(ruby, e)),
|
|
2578
2659
|
_ => Err(Error::new(
|
|
2579
|
-
ruby
|
|
2660
|
+
err_class(ruby, "InternalError"),
|
|
2580
2661
|
"internal: unexpected compile reply",
|
|
2581
2662
|
)),
|
|
2582
2663
|
}
|
|
@@ -2649,6 +2730,11 @@ impl Core {
|
|
|
2649
2730
|
self.reply_value(ruby, reply)
|
|
2650
2731
|
}
|
|
2651
2732
|
|
|
2733
|
+
fn module_graph_async(&self, ruby: &Ruby, module_id: i32) -> Result<Value, Error> {
|
|
2734
|
+
let reply = self.run(ruby, Request::ModuleGraphAsync { module_id })?;
|
|
2735
|
+
self.reply_value(ruby, reply)
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2652
2738
|
fn dispose_module(&self, ruby: &Ruby, module_id: i32) -> Result<(), Error> {
|
|
2653
2739
|
let reply = self.run(ruby, Request::DisposeModule { module_id })?;
|
|
2654
2740
|
self.reply_value(ruby, reply).map(|_| ())
|
|
@@ -2681,7 +2767,7 @@ impl Core {
|
|
|
2681
2767
|
VmReply::ScriptCompiled(Ok(cs)) => Ok(cs),
|
|
2682
2768
|
VmReply::ScriptCompiled(Err(e)) => Err(vm_err(ruby, e)),
|
|
2683
2769
|
_ => Err(Error::new(
|
|
2684
|
-
ruby
|
|
2770
|
+
err_class(ruby, "InternalError"),
|
|
2685
2771
|
"internal: unexpected compile reply",
|
|
2686
2772
|
)),
|
|
2687
2773
|
}
|
|
@@ -2794,7 +2880,7 @@ impl Core {
|
|
|
2794
2880
|
// and SEGV — refuse it, leaving the isolate usable.
|
|
2795
2881
|
if self.depth.load(Ordering::SeqCst) != 0 {
|
|
2796
2882
|
return Err(Error::new(
|
|
2797
|
-
ruby
|
|
2883
|
+
err_class(ruby, "Error"),
|
|
2798
2884
|
"RustyRacer: cannot dispose an isolate from within a running op or host callback",
|
|
2799
2885
|
));
|
|
2800
2886
|
}
|
|
@@ -2915,7 +3001,7 @@ impl Context {
|
|
|
2915
3001
|
// id 0's lifetime is the isolate's; extras also track their own dispose.
|
|
2916
3002
|
if self.disposed.load(Ordering::SeqCst) || self.core.is_disposed() {
|
|
2917
3003
|
return Err(Error::new(
|
|
2918
|
-
ruby
|
|
3004
|
+
err_class(ruby, "DisposedError"),
|
|
2919
3005
|
"disposed context",
|
|
2920
3006
|
));
|
|
2921
3007
|
}
|
|
@@ -3112,7 +3198,7 @@ impl JsModule {
|
|
|
3112
3198
|
// iso.dispose is a use-after-free.
|
|
3113
3199
|
if self.disposed.load(Ordering::SeqCst) || self.core.is_disposed() {
|
|
3114
3200
|
return Err(Error::new(
|
|
3115
|
-
ruby
|
|
3201
|
+
err_class(ruby, "DisposedError"),
|
|
3116
3202
|
"disposed module",
|
|
3117
3203
|
));
|
|
3118
3204
|
}
|
|
@@ -3140,6 +3226,19 @@ impl JsModule {
|
|
|
3140
3226
|
rb_self.check_live(ruby)?;
|
|
3141
3227
|
rb_self.core.module_status(ruby, rb_self.module_id)
|
|
3142
3228
|
}
|
|
3229
|
+
// True when top-level await appears anywhere in the module's LINKED import
|
|
3230
|
+
// graph (v8::Module::IsGraphAsync) — a dependency's await counts, which is
|
|
3231
|
+
// why no check on the source text alone can stand in for this. FALSE is the
|
|
3232
|
+
// load-bearing answer: V8 guarantees the evaluation promise is settled, so
|
|
3233
|
+
// #evaluate returning means the whole graph ran. True and it may have
|
|
3234
|
+
// returned with the body still suspended, while #status reads :evaluated
|
|
3235
|
+
// regardless (V8 folds its internal kEvaluatingAsync into kEvaluated).
|
|
3236
|
+
// Raises RustyRacer::RuntimeError unless the module is instantiated: before
|
|
3237
|
+
// linking there is no graph to walk.
|
|
3238
|
+
fn graph_async(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
3239
|
+
rb_self.check_live(ruby)?;
|
|
3240
|
+
rb_self.core.module_graph_async(ruby, rb_self.module_id)
|
|
3241
|
+
}
|
|
3143
3242
|
// The bytecode cache produced at compile (produce_cache: true), as a binary
|
|
3144
3243
|
// String, or nil. Persist it cross-process and pass back via cached_data:.
|
|
3145
3244
|
fn cached_data(ruby: &Ruby, rb_self: &Self) -> Value {
|
|
@@ -3176,7 +3275,7 @@ impl Script {
|
|
|
3176
3275
|
// Also refuse once the isolate is disposed (see JsModule::check_live).
|
|
3177
3276
|
if self.disposed.load(Ordering::SeqCst) || self.core.is_disposed() {
|
|
3178
3277
|
return Err(Error::new(
|
|
3179
|
-
ruby
|
|
3278
|
+
err_class(ruby, "DisposedError"),
|
|
3180
3279
|
"disposed script",
|
|
3181
3280
|
));
|
|
3182
3281
|
}
|
|
@@ -3231,7 +3330,7 @@ fn code_cache_from_reply(ruby: &Ruby, reply: VmReply) -> Result<Option<Vec<u8>>,
|
|
|
3231
3330
|
VmReply::CodeCache(Ok(bytes)) => Ok(bytes),
|
|
3232
3331
|
VmReply::CodeCache(Err(e)) => Err(vm_err(ruby, e)),
|
|
3233
3332
|
_ => Err(Error::new(
|
|
3234
|
-
ruby
|
|
3333
|
+
err_class(ruby, "InternalError"),
|
|
3235
3334
|
"internal: unexpected code-cache reply",
|
|
3236
3335
|
)),
|
|
3237
3336
|
}
|
|
@@ -3367,7 +3466,7 @@ fn resolve_module_via_ruby(
|
|
|
3367
3466
|
})?;
|
|
3368
3467
|
if !std::ptr::eq(Arc::as_ptr(&obj.core), core as *const Core) {
|
|
3369
3468
|
return Err(Error::new(
|
|
3370
|
-
ruby
|
|
3469
|
+
err_class(&ruby, "Error"),
|
|
3371
3470
|
"module resolver returned a Module from a different Context",
|
|
3372
3471
|
));
|
|
3373
3472
|
}
|
|
@@ -3444,6 +3543,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
3444
3543
|
jsmodule.define_method("evaluate", method!(JsModule::evaluate, 0))?;
|
|
3445
3544
|
jsmodule.define_method("namespace", method!(JsModule::namespace, 0))?;
|
|
3446
3545
|
jsmodule.define_method("_status", method!(JsModule::status, 0))?;
|
|
3546
|
+
jsmodule.define_method("graph_async?", method!(JsModule::graph_async, 0))?;
|
|
3447
3547
|
jsmodule.define_method("cached_data", method!(JsModule::cached_data, 0))?;
|
|
3448
3548
|
jsmodule.define_method("cache_rejected?", method!(JsModule::cache_rejected, 0))?;
|
|
3449
3549
|
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),
|
|
@@ -104,6 +127,11 @@ pub(crate) enum Request {
|
|
|
104
127
|
ModuleStatus {
|
|
105
128
|
module_id: i32,
|
|
106
129
|
},
|
|
130
|
+
// Whether top-level await appears anywhere in the module's linked import
|
|
131
|
+
// graph (v8::Module::IsGraphAsync), as a bool.
|
|
132
|
+
ModuleGraphAsync {
|
|
133
|
+
module_id: i32,
|
|
134
|
+
},
|
|
107
135
|
DisposeModule {
|
|
108
136
|
module_id: i32,
|
|
109
137
|
},
|
|
@@ -224,7 +252,7 @@ pub(crate) fn run_source(
|
|
|
224
252
|
}
|
|
225
253
|
};
|
|
226
254
|
match script.run(tc) {
|
|
227
|
-
Some(value) =>
|
|
255
|
+
Some(value) => marshalled!(tc, value),
|
|
228
256
|
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
229
257
|
None => {
|
|
230
258
|
let exc = tc.exception();
|
|
@@ -279,7 +307,7 @@ fn call_function(
|
|
|
279
307
|
match func.call(tc, recv, &argv) {
|
|
280
308
|
// void: skip marshalling the return so a huge/cyclic result is never walked.
|
|
281
309
|
Some(_) if void => Ok(JsVal::Undefined),
|
|
282
|
-
Some(value) =>
|
|
310
|
+
Some(value) => marshalled!(tc, value),
|
|
283
311
|
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
284
312
|
None => {
|
|
285
313
|
let exc = tc.exception();
|
|
@@ -396,6 +424,7 @@ fn request_realm(state: &IsolateState, request: &Request) -> Option<i32> {
|
|
|
396
424
|
| Request::CreateContext
|
|
397
425
|
| Request::DisposeContext { .. }
|
|
398
426
|
| Request::ModuleStatus { .. }
|
|
427
|
+
| Request::ModuleGraphAsync { .. }
|
|
399
428
|
| Request::DisposeModule { .. }
|
|
400
429
|
| Request::DisposeScript { .. }
|
|
401
430
|
| Request::ScriptCodeCache { .. }
|
|
@@ -465,6 +494,7 @@ fn dispatch_one(
|
|
|
465
494
|
} => op_evaluate_module(scope, module_id, timeout_ms, outermost),
|
|
466
495
|
Request::ModuleNamespace { module_id } => op_module_namespace(scope, module_id),
|
|
467
496
|
Request::ModuleStatus { module_id } => op_module_status(scope, module_id),
|
|
497
|
+
Request::ModuleGraphAsync { module_id } => op_module_graph_async(scope, module_id),
|
|
468
498
|
Request::DisposeModule { module_id } => op_dispose_module(scope, module_id),
|
|
469
499
|
Request::CompileScript {
|
|
470
500
|
context_id,
|
|
@@ -1065,6 +1095,20 @@ fn op_evaluate_module(
|
|
|
1065
1095
|
VmReply::Done(outcome)
|
|
1066
1096
|
}
|
|
1067
1097
|
|
|
1098
|
+
// get_module_namespace and is_graph_async both CHECK-abort (a release check, so
|
|
1099
|
+
// it kills the process) below Instantiated, so both ask this first. Everything
|
|
1100
|
+
// from Instantiated up is admissible, Errored included: a module that fails to
|
|
1101
|
+
// LINK is reset to Uninstantiated by V8's ResetGraph, so Errored can only mean
|
|
1102
|
+
// "linked, then evaluation failed" — the graph is still there to read.
|
|
1103
|
+
fn require_instantiated(module: v8::Local<v8::Module>, what: &str) -> Result<(), VmError> {
|
|
1104
|
+
match module.get_status() {
|
|
1105
|
+
v8::ModuleStatus::Uninstantiated | v8::ModuleStatus::Instantiating => Err(
|
|
1106
|
+
VmError::Runtime(format!("module must be instantiated before {what}")),
|
|
1107
|
+
),
|
|
1108
|
+
_ => Ok(()),
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1068
1112
|
fn op_module_namespace(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
1069
1113
|
let handle = module_handle(istate!(scope), module_id);
|
|
1070
1114
|
let outcome = match handle {
|
|
@@ -1075,15 +1119,27 @@ fn op_module_namespace(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) ->
|
|
|
1075
1119
|
let context = v8::Local::new(scope, &cx);
|
|
1076
1120
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
1077
1121
|
let module = v8::Local::new(scope, &g);
|
|
1078
|
-
//
|
|
1079
|
-
// is
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1122
|
+
// An errored module's namespace is unreadable — its bindings
|
|
1123
|
+
// never initialized — and the useful answer is WHY it errored,
|
|
1124
|
+
// the same one instantiate and evaluate give.
|
|
1125
|
+
if module.get_status() == v8::ModuleStatus::Errored {
|
|
1126
|
+
Err(VmError::JsError {
|
|
1127
|
+
message: module.get_exception().to_rust_string_lossy(scope),
|
|
1128
|
+
backtrace: vec![],
|
|
1129
|
+
})
|
|
1130
|
+
} else {
|
|
1131
|
+
match require_instantiated(module, "namespace") {
|
|
1132
|
+
Err(e) => Err(e),
|
|
1133
|
+
Ok(()) => {
|
|
1134
|
+
let ns = module.get_module_namespace();
|
|
1135
|
+
// Reading the namespace RUNS JS: every export is an
|
|
1136
|
+
// accessor, and one whose binding is still
|
|
1137
|
+
// uninitialized throws. Unwatched, that throw made
|
|
1138
|
+
// enumeration fail and the caller silently got the
|
|
1139
|
+
// stringified object in place of their exports.
|
|
1140
|
+
v8::tc_scope!(let tc, scope);
|
|
1141
|
+
marshalled!(tc, ns)
|
|
1142
|
+
}
|
|
1087
1143
|
}
|
|
1088
1144
|
}
|
|
1089
1145
|
}
|
|
@@ -1112,6 +1168,26 @@ fn op_module_status(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmR
|
|
|
1112
1168
|
VmReply::Done(outcome)
|
|
1113
1169
|
}
|
|
1114
1170
|
|
|
1171
|
+
// Does top-level await appear anywhere in this module's import graph? V8 walks
|
|
1172
|
+
// the linked graph from here, so an `await` in a dependency counts too — which
|
|
1173
|
+
// is why no amount of looking at one source text can answer it, and why it needs
|
|
1174
|
+
// an instantiated module: before linking there is no graph to walk. What V8
|
|
1175
|
+
// guarantees is the false case — "if IsGraphAsync() is false, the returned
|
|
1176
|
+
// Promise is settled" — i.e. false means #evaluate ran the whole graph to
|
|
1177
|
+
// completion. No context is entered: the walk reads module slots, runs no JS.
|
|
1178
|
+
fn op_module_graph_async(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
1179
|
+
let handle = module_handle(istate!(scope), module_id);
|
|
1180
|
+
let outcome = match handle {
|
|
1181
|
+
None => Err(VmError::Runtime("unknown module".into())),
|
|
1182
|
+
Some((g, _cid)) => {
|
|
1183
|
+
let module = v8::Local::new(scope, &g);
|
|
1184
|
+
require_instantiated(module, "graph_async?")
|
|
1185
|
+
.map(|()| JsVal::Bool(module.is_graph_async()))
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
VmReply::Done(outcome)
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1115
1191
|
fn op_dispose_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
|
|
1116
1192
|
let m = &mut istate!(scope).modules;
|
|
1117
1193
|
m.by_id.remove(&module_id);
|
|
@@ -1226,7 +1302,7 @@ fn op_run_script(
|
|
|
1226
1302
|
let out = {
|
|
1227
1303
|
v8::tc_scope!(let tc, scope);
|
|
1228
1304
|
match script.run(tc) {
|
|
1229
|
-
Some(value) =>
|
|
1305
|
+
Some(value) => marshalled!(tc, value),
|
|
1230
1306
|
None if tc.has_terminated() => Err(VmError::Terminated),
|
|
1231
1307
|
None => {
|
|
1232
1308
|
let exc = tc.exception();
|
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
|
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.
|
|
4
|
+
version: 0.2.0
|
|
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
|