rusty_racer 0.2.5 → 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.
@@ -29,14 +29,25 @@ macro_rules! marshalled {
29
29
  ($tc:expr, $value:expr) => {{
30
30
  let tc = $tc;
31
31
  let out = js_to_jsval(tc, $value);
32
- let exc = tc.exception();
33
- if exc.is_none() {
32
+ if tc.has_caught() {
33
+ Err(caught!(tc))
34
+ } else {
34
35
  Ok(out)
35
- } else if tc.has_terminated() {
36
- Err(VmError::Terminated)
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
- Err(capture_js_error(tc, exc, stack))
50
+ capture_js_error(tc, exc, stack)
40
51
  }
41
52
  }};
42
53
  }
@@ -76,6 +87,22 @@ pub(crate) enum Request {
76
87
  void: bool,
77
88
  timeout_ms: u64,
78
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
+ },
79
106
  // Drain the isolate's microtask queue once (no auto event loop).
80
107
  DrainMicrotasks {
81
108
  timeout_ms: u64,
@@ -148,9 +175,6 @@ pub(crate) enum Request {
148
175
  ModuleGraphAsync {
149
176
  module_id: i32,
150
177
  },
151
- DisposeModule {
152
- module_id: i32,
153
- },
154
178
  // Classic <script> primitives (V8 ScriptCompiler::CompileUnboundScript): an
155
179
  // unbound script, compiled in a context, runnable repeatedly, with the same
156
180
  // bytecode-cache options as modules. Addressed by id like a module.
@@ -169,9 +193,6 @@ pub(crate) enum Request {
169
193
  timeout_ms: u64,
170
194
  void: bool,
171
195
  },
172
- DisposeScript {
173
- script_id: i32,
174
- },
175
196
  // Serialize a bytecode cache from a compiled handle's CURRENT compile state
176
197
  // (Script#create_code_cache / Module#create_code_cache). Called after run/
177
198
  // evaluate, it captures the inner functions V8 lazily compiled while running
@@ -269,67 +290,80 @@ pub(crate) fn run_source(
269
290
  Completion::Primitive => marshalled!(tc, value),
270
291
  Completion::Void => Ok(JsVal::Undefined),
271
292
  },
272
- None if tc.has_terminated() => Err(VmError::Terminated),
273
- None => {
274
- let exc = tc.exception();
275
- let stack = tc.stack_trace();
276
- Err(capture_js_error(tc, exc, stack))
277
- }
293
+ None => Err(caught!(tc)),
278
294
  }
279
295
  }
280
296
 
281
- // Resolve a dotted property path on globalThis to a function and invoke it via
282
- // v8::Function::call, with the property's holder as `this` (so `a.b.f` gets the
283
- // right receiver). Args/result marshal through the ref-preserving paths.
284
- fn call_function(
285
- scope: &mut v8::PinScope<'_, '_>,
286
- name: &str,
287
- args: Vec<JsVal>,
288
- void: bool,
289
- ) -> Result<JsVal, VmError> {
290
- v8::tc_scope!(let tc, scope);
291
- let context = tc.get_current_context();
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 name.split('.') {
296
- let Some(obj) = target.to_object(tc) else {
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
- "`{name}`: cannot read `{part}` (a preceding path segment is not an object)"
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(tc, part) else {
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(tc, key.into()) else {
307
- if tc.has_terminated() {
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
- recv = target;
323
+ holder = target;
317
324
  target = next;
318
325
  }
319
- let Ok(func) = v8::Local::<v8::Function>::try_from(target) else {
320
- return Err(VmError::Runtime(format!("`{name}` is not a function")));
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 func.call(tc, recv, &argv) {
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 if tc.has_terminated() => Err(VmError::Terminated),
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
@@ -476,6 +511,26 @@ 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,
@@ -516,7 +571,6 @@ fn dispatch_one(
516
571
  Request::ModuleNamespace { module_id } => op_module_namespace(scope, module_id),
517
572
  Request::ModuleStatus { module_id } => op_module_status(scope, module_id),
518
573
  Request::ModuleGraphAsync { module_id } => op_module_graph_async(scope, module_id),
519
- Request::DisposeModule { module_id } => op_dispose_module(scope, module_id),
520
574
  Request::CompileScript {
521
575
  context_id,
522
576
  source,
@@ -538,7 +592,6 @@ fn dispatch_one(
538
592
  timeout_ms,
539
593
  void,
540
594
  } => op_run_script(scope, script_id, timeout_ms, void, outermost),
541
- Request::DisposeScript { script_id } => op_dispose_script(scope, script_id),
542
595
  // Serialize the script's CURRENT compile state. The stored handle is
543
596
  // the UnboundScript, which V8 fills in with inner-function bytecode as
544
597
  // run() lazily compiles them — so calling this after run() captures
@@ -673,6 +726,132 @@ fn op_call(
673
726
  VmReply::Done(outcome)
674
727
  }
675
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
+
676
855
  fn op_drain_microtasks(scope: &mut v8::PinScope<'_, '_, ()>, timeout_ms: u64) -> VmReply {
677
856
  // A microtask may call an attached host fn (a Promise .then ->
678
857
  // ruby), which runs inline via host_fn_callback — no routing
@@ -939,13 +1118,13 @@ fn op_compile_module(
939
1118
  };
940
1119
  let hash = module.get_identity_hash().get();
941
1120
  let g = v8::Global::new(tc, module);
1121
+ let weak = v8::Weak::new(tc, module);
942
1122
  let id = {
943
1123
  let m = &mut istate!(tc).modules;
944
1124
  let id = m.next_id;
945
1125
  m.next_id += 1;
946
- m.by_id
947
- .insert(id, (g.clone(), filename.clone(), context_id));
948
- 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());
949
1128
  id
950
1129
  };
951
1130
  Ok(Compiled {
@@ -1130,12 +1309,7 @@ fn op_evaluate_module(
1130
1309
  _ => Ok(JsVal::Undefined),
1131
1310
  }
1132
1311
  }
1133
- None if tc.has_terminated() => Err(VmError::Terminated),
1134
- None => {
1135
- let exc = tc.exception();
1136
- let stack = tc.stack_trace();
1137
- Err(capture_js_error(tc, exc, stack))
1138
- }
1312
+ None => Err(caught!(tc)),
1139
1313
  }
1140
1314
  }
1141
1315
  _ => Err(VmError::Runtime(
@@ -1265,15 +1439,6 @@ fn op_module_graph_async(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -
1265
1439
  VmReply::Done(outcome)
1266
1440
  }
1267
1441
 
1268
- fn op_dispose_module(scope: &mut v8::PinScope<'_, '_, ()>, module_id: i32) -> VmReply {
1269
- let m = &mut istate!(scope).modules;
1270
- m.by_id.remove(&module_id);
1271
- for bucket in m.by_hash.values_mut() {
1272
- bucket.retain(|(_, id)| *id != module_id);
1273
- }
1274
- VmReply::Done(Ok(JsVal::Undefined))
1275
- }
1276
-
1277
1442
  #[allow(clippy::too_many_arguments)]
1278
1443
  fn op_compile_script(
1279
1444
  scope: &mut v8::PinScope<'_, '_, ()>,
@@ -1375,12 +1540,7 @@ fn op_run_script(
1375
1540
  match script.run(tc) {
1376
1541
  Some(_) if void => Ok(JsVal::Undefined),
1377
1542
  Some(value) => marshalled!(tc, value),
1378
- None if tc.has_terminated() => Err(VmError::Terminated),
1379
- None => {
1380
- let exc = tc.exception();
1381
- let stack = tc.stack_trace();
1382
- Err(capture_js_error(tc, exc, stack))
1383
- }
1543
+ None => Err(caught!(tc)),
1384
1544
  }
1385
1545
  };
1386
1546
  auto_drain(scope, outermost);
@@ -1393,11 +1553,6 @@ fn op_run_script(
1393
1553
  VmReply::Done(outcome)
1394
1554
  }
1395
1555
 
1396
- fn op_dispose_script(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32) -> VmReply {
1397
- istate!(scope).scripts.by_id.remove(&script_id);
1398
- VmReply::Done(Ok(JsVal::Undefined))
1399
- }
1400
-
1401
1556
  fn op_script_code_cache(scope: &mut v8::PinScope<'_, '_, ()>, script_id: i32) -> VmReply {
1402
1557
  let handle = script_handle(istate!(scope), script_id);
1403
1558
  let outcome = match handle {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RustyRacer
4
- VERSION = "0.2.5"
4
+ VERSION = "0.2.6"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rusty_racer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.6
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-10-28 00:00:00.000000000 Z
11
+ date: 2026-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -34,6 +34,7 @@ extensions:
34
34
  - ext/rusty_racer/extconf.rb
35
35
  extra_rdoc_files: []
36
36
  files:
37
+ - CHANGELOG.md
37
38
  - Cargo.toml
38
39
  - README.md
39
40
  - ext/rusty_racer/Cargo.lock