rusty_racer 0.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f26a45e0afbed589acb2749fc1817e7d8508622c81a70fc378dc4af4a0f54714
4
- data.tar.gz: 718074b8d9f468b9ab3da144391ecbeff1d00db347812e5209cd11d866f78f6e
3
+ metadata.gz: 074b269481c924fc8e99f129e361d29fb4847c9c91ac4afee96378721fe9eebd
4
+ data.tar.gz: 68b312cba649752c393c0736e10efd8280f87ae0406ef7826b8d9fe3ec5505ce
5
5
  SHA512:
6
- metadata.gz: 03b227aa3eba1f597b34bca826f947c4345a8c16accebce84f267d5fcb3df2e9c72e3343e261be857387ada97000769dfca0b449130c9056bc8c6aceb569cd03
7
- data.tar.gz: fbe970e400edf1041271954df3ba7f3be5d1f8f3775ca29d09398065b85d9b813bc9ca8b9ae8302bd32757884f8c47af5ba9953937ca6de0486af8bd7e50959b
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 })
@@ -85,6 +86,14 @@ covers the library. Under it: `ParseError`, `RuntimeError`,
85
86
  `DisposedError` for using an isolate (or anything it handed out) after
86
87
  disposing it; `WrongThreadError` for reaching an isolate from the wrong thread.
87
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
+
88
97
  ES modules (the embedder owns the URL→module registry):
89
98
 
90
99
  ```ruby
@@ -5,7 +5,7 @@
5
5
  # links and loads with no custom archive.
6
6
  [package]
7
7
  name = "rusty_racer"
8
- version = "0.2.0"
8
+ version = "0.2.1"
9
9
  edition = "2024"
10
10
  publish = false
11
11
 
@@ -1997,8 +1997,16 @@ fn build_snapshot(code: &str, base: Option<Vec<u8>>, warmup: bool) -> Result<Vec
1997
1997
  {
1998
1998
  let cscope = &mut v8::ContextScope::new(scope, context);
1999
1999
  if !code.is_empty()
2000
- && let Err(e) =
2001
- run_source(cscope, code, if warmup { "<warmup>" } else { "<snapshot>" })
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
+ )
2002
2010
  {
2003
2011
  err = Some(match e {
2004
2012
  VmError::Parse(m) | VmError::Runtime(m) => m,
@@ -2522,6 +2530,7 @@ impl Core {
2522
2530
  source: String,
2523
2531
  filename: String,
2524
2532
  timeout_ms: u64,
2533
+ void: bool,
2525
2534
  ) -> Result<Value, Error> {
2526
2535
  let reply = self.run(
2527
2536
  ruby,
@@ -2530,6 +2539,7 @@ impl Core {
2530
2539
  source,
2531
2540
  filename,
2532
2541
  timeout_ms,
2542
+ void,
2533
2543
  },
2534
2544
  )?;
2535
2545
  self.reply_value(ruby, reply)
@@ -2773,12 +2783,13 @@ impl Core {
2773
2783
  }
2774
2784
  }
2775
2785
 
2776
- 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> {
2777
2787
  let reply = self.run(
2778
2788
  ruby,
2779
2789
  Request::RunScript {
2780
2790
  script_id,
2781
2791
  timeout_ms: self.default_timeout_ms,
2792
+ void,
2782
2793
  },
2783
2794
  )?;
2784
2795
  self.reply_value(ruby, reply)
@@ -3008,12 +3019,14 @@ impl Context {
3008
3019
  Ok(())
3009
3020
  }
3010
3021
  // timeout_ms 0 = use the isolate's default; an explicit value overrides it.
3022
+ // |void| discards the completion value instead of marshalling it.
3011
3023
  fn eval(
3012
3024
  ruby: &Ruby,
3013
3025
  rb_self: &Self,
3014
3026
  source: String,
3015
3027
  timeout_ms: u64,
3016
3028
  filename: String,
3029
+ void: bool,
3017
3030
  ) -> Result<Value, Error> {
3018
3031
  rb_self.check_live(ruby)?;
3019
3032
  let timeout = if timeout_ms == 0 {
@@ -3023,7 +3036,7 @@ impl Context {
3023
3036
  };
3024
3037
  rb_self
3025
3038
  .core
3026
- .eval_t(ruby, rb_self.id, source, filename, timeout)
3039
+ .eval_t(ruby, rb_self.id, source, filename, timeout, void)
3027
3040
  }
3028
3041
  fn call(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
3029
3042
  rb_self.check_live(ruby)?;
@@ -3285,7 +3298,13 @@ impl Script {
3285
3298
  // thrown exception is a RuntimeError; a timeout/stop a ScriptTerminatedError.
3286
3299
  fn run(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
3287
3300
  rb_self.check_live(ruby)?;
3288
- 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)
3289
3308
  }
3290
3309
  fn cached_data(ruby: &Ruby, rb_self: &Self) -> Value {
3291
3310
  code_cache_value(ruby, rb_self.cached_data.as_ref())
@@ -3506,8 +3525,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
3506
3525
 
3507
3526
  // A v8::Context (realm): eval/call/attach/compile_module.
3508
3527
  let context = module.define_class("Context", ruby.class_object())?;
3509
- // keyword-arg wrapper Context#eval(source, timeout_ms:, filename:) in lib.
3510
- context.define_method("_eval", method!(Context::eval, 3))?;
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))?;
3511
3531
  context.define_method("call", method!(Context::call, -1))?;
3512
3532
  context.define_method("call_void", method!(Context::call_void, -1))?;
3513
3533
  context.define_method("attach", method!(Context::attach, 2))?;
@@ -3523,6 +3543,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
3523
3543
  // Classic compiled script: Context#compile -> #run / #cached_data.
3524
3544
  let script = module.define_class("Script", ruby.class_object())?;
3525
3545
  script.define_method("run", method!(Script::run, 0))?;
3546
+ script.define_method("run_void", method!(Script::run_void, 0))?;
3526
3547
  script.define_method("cached_data", method!(Script::cached_data, 0))?;
3527
3548
  script.define_method("cache_rejected?", method!(Script::cache_rejected, 0))?;
3528
3549
  script.define_method("create_code_cache", method!(Script::create_code_cache, 0))?;
@@ -51,6 +51,11 @@ pub(crate) enum Request {
51
51
  source: String,
52
52
  filename: String,
53
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,
54
59
  },
55
60
  // Resolve a dotted function path on globalThis and invoke it with marshalled
56
61
  // args (v8::Function::call), preserving the holder as `this`. Distinct from
@@ -146,10 +151,12 @@ pub(crate) enum Request {
146
151
  produce_cache: bool,
147
152
  eager: bool,
148
153
  },
149
- // 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).
150
156
  RunScript {
151
157
  script_id: i32,
152
158
  timeout_ms: u64,
159
+ void: bool,
153
160
  },
154
161
  DisposeScript {
155
162
  script_id: i32,
@@ -220,6 +227,7 @@ pub(crate) fn run_source(
220
227
  scope: &mut v8::PinScope<'_, '_>,
221
228
  source: &str,
222
229
  filename: &str,
230
+ void: bool,
223
231
  ) -> Result<JsVal, VmError> {
224
232
  v8::tc_scope!(let tc, scope);
225
233
  // Compile and run as distinct phases so a compile failure maps to
@@ -252,6 +260,7 @@ pub(crate) fn run_source(
252
260
  }
253
261
  };
254
262
  match script.run(tc) {
263
+ Some(_) if void => Ok(JsVal::Undefined),
255
264
  Some(value) => marshalled!(tc, value),
256
265
  None if tc.has_terminated() => Err(VmError::Terminated),
257
266
  None => {
@@ -448,7 +457,10 @@ fn dispatch_one(
448
457
  source,
449
458
  filename,
450
459
  timeout_ms,
451
- } => op_eval(scope, context_id, source, filename, timeout_ms, outermost),
460
+ void,
461
+ } => op_eval(
462
+ scope, context_id, source, filename, timeout_ms, void, outermost,
463
+ ),
452
464
  Request::Call {
453
465
  context_id,
454
466
  name,
@@ -515,7 +527,8 @@ fn dispatch_one(
515
527
  Request::RunScript {
516
528
  script_id,
517
529
  timeout_ms,
518
- } => op_run_script(scope, script_id, timeout_ms, outermost),
530
+ void,
531
+ } => op_run_script(scope, script_id, timeout_ms, void, outermost),
519
532
  Request::DisposeScript { script_id } => op_dispose_script(scope, script_id),
520
533
  // Serialize the script's CURRENT compile state. The stored handle is
521
534
  // the UnboundScript, which V8 fills in with inner-function bytecode as
@@ -558,12 +571,14 @@ fn op_low_memory_notification(scope: &mut v8::PinScope<'_, '_, ()>) -> VmReply {
558
571
  VmReply::Done(Ok(JsVal::Undefined))
559
572
  }
560
573
 
574
+ #[allow(clippy::too_many_arguments)]
561
575
  fn op_eval(
562
576
  scope: &mut v8::PinScope<'_, '_, ()>,
563
577
  context_id: i32,
564
578
  source: String,
565
579
  filename: String,
566
580
  timeout_ms: u64,
581
+ void: bool,
567
582
  outermost: bool,
568
583
  ) -> VmReply {
569
584
  let outcome = run_js_bracketed(scope, outermost, timeout_ms, "eval", |scope, outermost| {
@@ -572,7 +587,7 @@ fn op_eval(
572
587
  Some(ctx) => {
573
588
  let context = v8::Local::new(scope, &ctx);
574
589
  let scope = &mut v8::ContextScope::new(scope, context);
575
- let out = run_source(scope, &source, &filename);
590
+ let out = run_source(scope, &source, &filename, void);
576
591
  auto_drain(scope, outermost);
577
592
  (true, out)
578
593
  }
@@ -1278,6 +1293,7 @@ fn op_run_script(
1278
1293
  scope: &mut v8::PinScope<'_, '_, ()>,
1279
1294
  script_id: i32,
1280
1295
  timeout_ms: u64,
1296
+ void: bool,
1281
1297
  outermost: bool,
1282
1298
  ) -> VmReply {
1283
1299
  let outcome = run_js_bracketed(
@@ -1302,6 +1318,7 @@ fn op_run_script(
1302
1318
  let out = {
1303
1319
  v8::tc_scope!(let tc, scope);
1304
1320
  match script.run(tc) {
1321
+ Some(_) if void => Ok(JsVal::Undefined),
1305
1322
  Some(value) => marshalled!(tc, value),
1306
1323
  None if tc.has_terminated() => Err(VmError::Terminated),
1307
1324
  None => {
@@ -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.eval('delete globalThis.console')
38
+ @context.eval_void('delete globalThis.console')
39
39
  source = encode(source)
40
- translate { @context.eval(source, filename: LOCATION) } if /\S/.match?(source)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RustyRacer
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/rusty_racer.rb CHANGED
@@ -112,7 +112,18 @@ module RustyRacer
112
112
  # `timeout_ms` (0 = the isolate default) caps this eval; `filename` names the
113
113
  # script in stack traces and parse-error locations.
114
114
  def eval(source, timeout_ms: 0, filename: '<eval>')
115
- _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)
116
127
  end
117
128
 
118
129
  # Compile a classic <script>; returns a RustyRacer::Script to #run.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rusty_racer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keita Urashima