schemurai 3.0.0 → 3.0.2

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -4
  3. data/ext/schemurai_native/Cargo.lock +55 -1
  4. data/ext/schemurai_native/Cargo.toml +4 -1
  5. data/ext/schemurai_native/extconf.rb +1 -1
  6. data/ext/schemurai_native/glue.c +1679 -62
  7. data/ext/schemurai_native/protocol.def +45 -0
  8. data/ext/schemurai_native/src/budget.rs +49 -0
  9. data/ext/schemurai_native/src/container.rs +460 -0
  10. data/ext/schemurai_native/src/content.rs +74 -27
  11. data/ext/schemurai_native/src/diagnostics/array.rs +392 -0
  12. data/ext/schemurai_native/src/diagnostics/object.rs +218 -0
  13. data/ext/schemurai_native/src/diagnostics.rs +1087 -307
  14. data/ext/schemurai_native/src/enumeration.rs +6 -1
  15. data/ext/schemurai_native/src/evaluation.rs +49 -20
  16. data/ext/schemurai_native/src/format.rs +126 -14
  17. data/ext/schemurai_native/src/graph.rs +37 -0
  18. data/ext/schemurai_native/src/instance_graph.rs +90 -0
  19. data/ext/schemurai_native/src/integer_scan.rs +132 -0
  20. data/ext/schemurai_native/src/lib.rs +917 -238
  21. data/ext/schemurai_native/src/memo.rs +11 -38
  22. data/ext/schemurai_native/src/number/arbitrary.rs +154 -0
  23. data/ext/schemurai_native/src/number.rs +250 -59
  24. data/ext/schemurai_native/src/pattern.rs +147 -109
  25. data/ext/schemurai_native/src/pointer.rs +97 -0
  26. data/ext/schemurai_native/src/runtime/properties.rs +162 -0
  27. data/ext/schemurai_native/src/runtime/reports.rs +266 -0
  28. data/ext/schemurai_native/src/runtime/scopes.rs +430 -0
  29. data/ext/schemurai_native/src/runtime.rs +1020 -0
  30. data/ext/schemurai_native/src/scope.rs +60 -16
  31. data/ext/schemurai_native/src/string.rs +68 -0
  32. data/ext/schemurai_native/src/tracking.rs +244 -174
  33. data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
  34. data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
  35. data/lib/schemurai/container_checks.rb +534 -0
  36. data/lib/schemurai/decimal.rb +11 -0
  37. data/lib/schemurai/dialect.rb +0 -12
  38. data/lib/schemurai/ecma_regexp.rb +46 -0
  39. data/lib/schemurai/evaluation_pool.rb +66 -0
  40. data/lib/schemurai/evaluator.rb +9 -48
  41. data/lib/schemurai/json_pointer.rb +11 -0
  42. data/lib/schemurai/native/diagnostics.rb +4 -3
  43. data/lib/schemurai/native/evaluator.rb +38 -10
  44. data/lib/schemurai/native/null_numeric.rb +23 -0
  45. data/lib/schemurai/number_checks.rb +128 -0
  46. data/lib/schemurai/string_checks.rb +166 -0
  47. data/lib/schemurai/type_checks.rb +59 -0
  48. data/lib/schemurai/value_equality.rb +72 -0
  49. data/lib/schemurai/version.rb +1 -1
  50. data/lib/schemurai/vm/compiler.rb +18 -26
  51. data/lib/schemurai/vm/evaluator.rb +87 -545
  52. data/lib/schemurai/vm/limited_evaluator.rb +7 -22
  53. metadata +25 -2
@@ -0,0 +1,1020 @@
1
+ //! Runner-owned search caches and observations requested outside Rust frames.
2
+ use super::*;
3
+ mod properties;
4
+ mod reports;
5
+ mod scopes;
6
+ use regex_automata::{
7
+ Input,
8
+ meta::{Cache, Regex},
9
+ };
10
+ use std::collections::HashSet;
11
+
12
+ // Each validator owns its search caches. Compiled regex/DFA data stays shared.
13
+ #[derive(Default)]
14
+ pub struct Caches {
15
+ searches: std::cell::RefCell<Vec<Option<Box<Cache>>>>,
16
+ matches: std::cell::Cell<*const c_void>,
17
+ pending: std::cell::Cell<Option<(Value, Value, i32)>>,
18
+ deadline: std::cell::Cell<Option<std::time::Instant>>,
19
+ runtime_order: std::cell::Cell<bool>,
20
+ dynamic_calls: std::cell::Cell<bool>,
21
+ reorder: std::cell::Cell<bool>,
22
+ pending_rescue: std::cell::Cell<Option<StringError>>,
23
+ rescued_strings: std::cell::RefCell<HashSet<StringError>>,
24
+ call_cursor: std::cell::Cell<usize>,
25
+ pointers: std::cell::RefCell<Pointers>,
26
+ pending_pointer: std::cell::Cell<Option<usize>>,
27
+ messages: std::cell::RefCell<HashMap<Message, Option<Value>>>,
28
+ pending_message: std::cell::Cell<Option<Message>>,
29
+ pending_scalar: std::cell::Cell<Option<ScalarArguments>>,
30
+ frame_error: std::cell::Cell<bool>,
31
+ scalar_callback: std::cell::Cell<bool>,
32
+ callback_checkpoints: std::cell::Cell<bool>,
33
+ callbacks_allowed: std::cell::Cell<bool>,
34
+ continuations_allowed: std::cell::Cell<bool>,
35
+ pending_contains: std::cell::Cell<Option<ContainsArguments>>,
36
+ scopes: scopes::Scopes,
37
+ reports: reports::Reports,
38
+ properties: properties::Properties,
39
+ report_continuations_allowed: std::cell::Cell<bool>,
40
+ }
41
+ #[repr(C)]
42
+ #[derive(Clone, Copy)]
43
+ pub struct ContainsArguments {
44
+ rules: Value,
45
+ instance: Value,
46
+ child: usize,
47
+ required: i32,
48
+ scalar_kind: i32,
49
+ scalar_flags: i32,
50
+ }
51
+ type StringError = (Value, usize, &'static str);
52
+ #[repr(C)]
53
+ #[derive(Clone, Copy)]
54
+ pub struct ScalarArguments {
55
+ ordinal: Value,
56
+ rules: Value,
57
+ instance: Value,
58
+ }
59
+ #[derive(Clone, Copy, PartialEq, Eq, Hash)]
60
+ struct Message {
61
+ ordinal: usize,
62
+ keyword: &'static str,
63
+ opcode: i32,
64
+ operand: Value,
65
+ instance: Value,
66
+ detail: usize,
67
+ frame: Value,
68
+ }
69
+ #[repr(C)]
70
+ pub struct MessageArguments {
71
+ keyword: *const u8,
72
+ keyword_len: usize,
73
+ opcode: i32,
74
+ operand: Value,
75
+ instance: Value,
76
+ detail: usize,
77
+ frame: Value,
78
+ }
79
+ #[derive(Default)]
80
+ struct Pointers {
81
+ indices: HashMap<std::sync::Arc<pointer::Path>, usize>,
82
+ requests: Vec<PointerRequest>,
83
+ }
84
+ struct PointerRequest {
85
+ path: std::sync::Arc<pointer::Path>,
86
+ result: Option<Value>,
87
+ }
88
+ unsafe extern "C" {
89
+ fn sn_runtime_ordinal(ordinal: usize) -> Value;
90
+ fn sn_cached_runtime(
91
+ matches: *const c_void,
92
+ operation: i32,
93
+ source: Value,
94
+ value: Value,
95
+ ) -> i32;
96
+ }
97
+ impl Caches {
98
+ fn reset_pass(&self) {
99
+ self.scopes.reset();
100
+ self.reports.reset();
101
+ self.properties.reset();
102
+ self.pending.set(None);
103
+ self.call_cursor.set(0);
104
+ self.pending_pointer.set(None);
105
+ self.pending_message.set(None);
106
+ self.pending_scalar.set(None);
107
+ self.pending_contains.set(None);
108
+ self.frame_error.set(false);
109
+ self.pending_rescue.set(None);
110
+ }
111
+
112
+ pub(super) fn scope_guard(&self, resource: Value, enters: bool) -> scopes::Guard<'_> {
113
+ self.scopes.enter(resource, enters)
114
+ }
115
+ pub(super) fn reference_frame(&self, program: Value, instance: Value) -> scopes::Reference {
116
+ self.scopes.reference_frame(program, instance)
117
+ }
118
+ pub(super) fn reference_guard<'a>(
119
+ &'a self,
120
+ frame: &'a scopes::Reference,
121
+ ) -> scopes::ReferenceGuard<'a> {
122
+ self.scopes.reference_guard(frame)
123
+ }
124
+ pub(super) fn reference_active(&self, program: Value, instance: Value) -> bool {
125
+ self.scopes.reference_active(program, instance)
126
+ }
127
+ pub(super) fn scopes_enabled(&self) -> bool {
128
+ self.scopes.enabled()
129
+ }
130
+ pub(super) fn scope_at(&self, index: usize) -> Option<Value> {
131
+ self.scopes.at(index)
132
+ }
133
+ pub(super) fn frame_error(&self, active: bool) {
134
+ self.frame_error.set(active);
135
+ }
136
+ pub(super) fn scalar_callback(&self) -> bool {
137
+ self.scalar_callback.get()
138
+ }
139
+ pub(super) fn callbacks_allowed(&self) -> bool {
140
+ self.callbacks_allowed.get()
141
+ }
142
+ pub(super) fn allow_callbacks(&self, allowed: bool) {
143
+ self.callbacks_allowed.set(allowed);
144
+ }
145
+ pub(super) fn allow_continuations(&self, allowed: bool) {
146
+ self.continuations_allowed.set(allowed);
147
+ }
148
+ pub(super) fn continuations_allowed(&self) -> bool {
149
+ self.continuations_allowed.get()
150
+ }
151
+ pub(super) fn allow_report_continuations(&self, allowed: bool) {
152
+ self.report_continuations_allowed.set(allowed);
153
+ }
154
+ pub(super) fn report_continuations_allowed(&self) -> bool {
155
+ self.report_continuations_allowed.get()
156
+ }
157
+ pub(super) fn admit(&self, engine: &Engine, root: usize) {
158
+ self.allow_callbacks(engine.scalar_callbacks_safe(root));
159
+ self.allow_continuations(engine.native_continuations_safe(root));
160
+ self.allow_report_continuations(engine.report_continuations_safe(root));
161
+ }
162
+ pub(super) fn initial_report(&self) -> Option<diagnostics::Report> {
163
+ self.reports.initial()
164
+ }
165
+ pub(super) fn report_child(
166
+ &self,
167
+ node: usize,
168
+ value: Value,
169
+ report: &mut diagnostics::Report,
170
+ annotations: bool,
171
+ ) -> Result<tracking::Tracked, Verdict> {
172
+ self.report_request(
173
+ ContainsArguments {
174
+ rules: unsafe { sn_nil() },
175
+ instance: value,
176
+ child: node,
177
+ required: 0,
178
+ scalar_kind: CALLBACK_RUNTIME,
179
+ scalar_flags: 0,
180
+ },
181
+ RUNTIME_NATIVE_REPORT,
182
+ report,
183
+ annotations,
184
+ None,
185
+ )
186
+ }
187
+ pub(super) fn container_report(
188
+ &self,
189
+ node: usize,
190
+ value: Value,
191
+ header: (Value, i32, bool),
192
+ report: &mut diagnostics::Report,
193
+ prior: &tracking::Tracked,
194
+ annotations: bool,
195
+ ) -> Result<tracking::Tracked, Verdict> {
196
+ self.report_request(
197
+ ContainsArguments {
198
+ rules: header.0,
199
+ instance: value,
200
+ child: node,
201
+ required: header.1,
202
+ scalar_kind: CALLBACK_RUNTIME,
203
+ scalar_flags: 0,
204
+ },
205
+ if header.2 {
206
+ RUNTIME_OBJECT_REPORT
207
+ } else {
208
+ RUNTIME_ARRAY_REPORT
209
+ },
210
+ report,
211
+ annotations,
212
+ Some(prior),
213
+ )
214
+ }
215
+ fn report_request(
216
+ &self,
217
+ arguments: ContainsArguments,
218
+ operation: i32,
219
+ report: &mut diagnostics::Report,
220
+ annotations: bool,
221
+ prior: Option<&tracking::Tracked>,
222
+ ) -> Result<tracking::Tracked, Verdict> {
223
+ let ordinal = self.next_call()?;
224
+ if self.observe(ordinal, arguments.instance, operation) == Unsupported {
225
+ let seed = match prior {
226
+ Some(prior) => report.container_continuation(annotations, prior),
227
+ None => report.continuation(annotations),
228
+ };
229
+ self.reports.request(ordinal, seed);
230
+ self.pending_contains.set(Some(arguments));
231
+ return Err(Unsupported);
232
+ }
233
+ Ok(report.adopt(self.reports.result(ordinal).ok_or(Unsupported)?))
234
+ }
235
+ pub(super) fn native_child(&self, child: usize, instance: Value) -> Verdict {
236
+ self.children_request(
237
+ ContainsArguments {
238
+ rules: unsafe { sn_nil() },
239
+ instance,
240
+ child,
241
+ required: 0,
242
+ scalar_kind: CALLBACK_RUNTIME,
243
+ scalar_flags: 0,
244
+ },
245
+ RUNTIME_NATIVE_CHILD,
246
+ )
247
+ }
248
+ pub(super) fn contains(
249
+ &self,
250
+ rules: Value,
251
+ instance: Value,
252
+ child: usize,
253
+ required: i32,
254
+ scalar: (i32, i32),
255
+ ) -> Verdict {
256
+ self.children_request(
257
+ ContainsArguments {
258
+ rules,
259
+ instance,
260
+ child,
261
+ required,
262
+ scalar_kind: scalar.0,
263
+ scalar_flags: scalar.1,
264
+ },
265
+ RUNTIME_SCALAR_CONTAINS,
266
+ )
267
+ }
268
+ pub(super) fn container_children(
269
+ &self,
270
+ rules: Value,
271
+ instance: Value,
272
+ node: usize,
273
+ required: i32,
274
+ object: bool,
275
+ ) -> Verdict {
276
+ self.children_request(
277
+ ContainsArguments {
278
+ rules,
279
+ instance,
280
+ child: node,
281
+ required,
282
+ scalar_kind: CALLBACK_RUNTIME,
283
+ scalar_flags: 0,
284
+ },
285
+ if object {
286
+ RUNTIME_OBJECT_CHILDREN
287
+ } else {
288
+ RUNTIME_ARRAY_CHILDREN
289
+ },
290
+ )
291
+ }
292
+ fn children_request(&self, arguments: ContainsArguments, operation: i32) -> Verdict {
293
+ let Ok(ordinal) = self.next_call() else {
294
+ return Unsupported;
295
+ };
296
+ let result = self.observe(ordinal, arguments.instance, operation);
297
+ if result == Unsupported {
298
+ self.pending_contains.set(Some(arguments));
299
+ }
300
+ result
301
+ }
302
+ pub(super) fn runtime_order(&self) -> bool {
303
+ self.runtime_order.get()
304
+ }
305
+ pub(super) fn budget(&self, limits: budget::Limits) -> budget::Budget {
306
+ if self.scalar_callback() {
307
+ // These children execute shared Ruby scalar primitives. Their VM
308
+ // checkpoints own the method boundaries; an entry/exit native
309
+ // deadline check could preempt an exception caught by the caller's
310
+ // Ruby block. The parent retains its native invocation deadline.
311
+ return budget::Budget::new(budget::Limits {
312
+ enabled: limits.enabled & !2,
313
+ ..limits
314
+ });
315
+ }
316
+ budget::Budget::resume(limits, &self.deadline)
317
+ }
318
+ pub(super) fn is_match(&self, index: usize, regex: &Regex, bytes: &[u8]) -> bool {
319
+ // Each runner is tied to one engine, whose regex indices are fixed at
320
+ // compilation. Allocate only the caches this validator actually uses.
321
+ let mut caches = self.searches.borrow_mut();
322
+ if caches.len() <= index {
323
+ caches.resize_with(index + 1, || None);
324
+ }
325
+ let cache = caches[index].get_or_insert_with(|| Box::new(regex.create_cache()));
326
+ regex
327
+ .search_half_with(cache, &Input::new(bytes).earliest(true))
328
+ .is_some()
329
+ }
330
+
331
+ pub(super) fn ruby_match(&self, source: Value, value: Value, assertion: bool) -> Verdict {
332
+ self.observe(
333
+ source,
334
+ value,
335
+ if assertion {
336
+ RUNTIME_REGEXP_ASSERTION
337
+ } else {
338
+ RUNTIME_REGEXP_STRICT
339
+ },
340
+ )
341
+ }
342
+ pub(super) fn json(&self, value: Value) -> Verdict {
343
+ // Qundef cannot be a Ruby pattern operand, including malformed schemas.
344
+ self.observe(unsafe { sn_undefined() }, value, RUNTIME_JSON)
345
+ }
346
+ pub(super) fn dynamic_calls(&self) -> bool {
347
+ self.dynamic_calls.get()
348
+ }
349
+ fn next_call(&self) -> Result<Value, Verdict> {
350
+ if !self.dynamic_calls.replace(true) {
351
+ self.reorder.set(true);
352
+ }
353
+ let ordinal = self.call_cursor.get();
354
+ if ordinal > (isize::MAX >> 1) as usize {
355
+ return Err(Unsupported);
356
+ }
357
+ self.call_cursor.set(ordinal + 1);
358
+ Ok(unsafe { sn_runtime_ordinal(ordinal) })
359
+ }
360
+ pub(super) fn path_pop(&self) -> Result<(), Verdict> {
361
+ let ordinal = self.next_call()?;
362
+ if self.observe(ordinal, unsafe { sn_nil() }, RUNTIME_PATH_POP) == Unsupported {
363
+ Err(Unsupported)
364
+ } else {
365
+ Ok(())
366
+ }
367
+ }
368
+ pub(super) fn callback_checkpoint(&self, value: Value) -> Verdict {
369
+ if !self.callback_checkpoints.get() {
370
+ return Valid;
371
+ }
372
+ let Ok(ordinal) = self.next_call() else {
373
+ return Unsupported;
374
+ };
375
+ self.observe(ordinal, value, RUNTIME_CALLBACK_CHECKPOINT)
376
+ }
377
+ pub(super) fn type_predicate(&self, value: Value, mask: i32, union: bool) -> Verdict {
378
+ let Ok(ordinal) = self.next_call() else {
379
+ return Unsupported;
380
+ };
381
+ self.observe(ordinal, value, RUNTIME_TYPE + mask * 2 + i32::from(union))
382
+ }
383
+ pub(super) fn number(
384
+ &self,
385
+ rules: Value,
386
+ instance: Value,
387
+ required: i32,
388
+ ) -> Result<ScalarFrame<'_>, Verdict> {
389
+ self.scalar(rules, instance, required, RUNTIME_NUMBER, 64)
390
+ }
391
+ pub(super) fn string(
392
+ &self,
393
+ rules: Value,
394
+ instance: Value,
395
+ flags: i32,
396
+ ) -> Result<ScalarFrame<'_>, Verdict> {
397
+ self.scalar(rules, instance, flags, RUNTIME_STRING, 256)
398
+ }
399
+ fn scalar(
400
+ &self,
401
+ rules: Value,
402
+ instance: Value,
403
+ required: i32,
404
+ base: i32,
405
+ stride: i32,
406
+ ) -> Result<ScalarFrame<'_>, Verdict> {
407
+ Ok(ScalarFrame {
408
+ caches: self,
409
+ arguments: ScalarArguments {
410
+ ordinal: self.next_call()?,
411
+ rules,
412
+ instance,
413
+ },
414
+ required,
415
+ base,
416
+ stride,
417
+ })
418
+ }
419
+ pub(super) fn container(
420
+ &self,
421
+ rules: Value,
422
+ instance: Value,
423
+ required: i32,
424
+ object: bool,
425
+ ) -> Result<ScalarFrame<'_>, Verdict> {
426
+ self.scalar(
427
+ rules,
428
+ instance,
429
+ required,
430
+ if object {
431
+ RUNTIME_OBJECT_HEADER
432
+ } else {
433
+ RUNTIME_ARRAY_HEADER
434
+ },
435
+ 64,
436
+ )
437
+ }
438
+ pub(super) fn equality(&self, operand: Value, instance: Value, operation: i32) -> Verdict {
439
+ let Ok(ordinal) = self.next_call() else {
440
+ return Unsupported;
441
+ };
442
+ let result = self.observe(ordinal, instance, operation);
443
+ if result == Unsupported {
444
+ self.pending_scalar.set(Some(ScalarArguments {
445
+ ordinal,
446
+ rules: operand,
447
+ instance,
448
+ }));
449
+ }
450
+ result
451
+ }
452
+ pub(super) fn required_missing(&self, name: Value, value: Value) -> Verdict {
453
+ self.observe(name, value, RUNTIME_REQUIRED_MISSING)
454
+ }
455
+ pub(super) fn error_message(
456
+ &self,
457
+ keyword: &'static str,
458
+ source: (i32, Value),
459
+ instance: Value,
460
+ detail: usize,
461
+ ordinal: usize,
462
+ ) -> Result<Value, Verdict> {
463
+ let message = Message {
464
+ ordinal,
465
+ keyword,
466
+ opcode: source.0,
467
+ operand: source.1,
468
+ instance,
469
+ detail,
470
+ frame: unsafe { sn_undefined() },
471
+ };
472
+ self.message(message)
473
+ }
474
+ pub(super) fn frame_message(
475
+ &self,
476
+ keyword: &'static str,
477
+ source: (i32, Value),
478
+ instance: Value,
479
+ frame: Value,
480
+ ordinal: usize,
481
+ rescued: bool,
482
+ ) -> Result<Value, Verdict> {
483
+ self.message(Message {
484
+ ordinal,
485
+ keyword,
486
+ opcode: source.0,
487
+ operand: source.1,
488
+ instance,
489
+ detail: usize::from(rescued),
490
+ frame,
491
+ })
492
+ }
493
+ pub(super) fn begin_string_error(
494
+ &self,
495
+ frame: Value,
496
+ ordinal: usize,
497
+ keyword: &'static str,
498
+ ) -> bool {
499
+ let key = (frame, ordinal, keyword);
500
+ if self.rescued_strings.borrow().contains(&key) {
501
+ self.apply_runtime(scopes::Key::Rescue(key));
502
+ return false;
503
+ }
504
+ self.pending_rescue.set(Some(key));
505
+ true
506
+ }
507
+ pub(super) fn end_string_error(&self) {
508
+ self.pending_rescue.set(None);
509
+ }
510
+ fn message(&self, message: Message) -> Result<Value, Verdict> {
511
+ if let Some(value) = self.messages.borrow_mut().entry(message).or_insert(None) {
512
+ self.apply_runtime(scopes::Key::Message(message));
513
+ return Ok(*value);
514
+ }
515
+ self.request_runtime(scopes::Key::Message(message));
516
+ self.pending_message.set(Some(message));
517
+ self.pending.set(Some((
518
+ unsafe { sn_undefined() },
519
+ unsafe { sn_nil() },
520
+ RUNTIME_DIAGNOSTIC_MESSAGE,
521
+ )));
522
+ Err(Unsupported)
523
+ }
524
+ pub(super) fn pointer(&self, path: &pointer::Path) -> Result<Value, Verdict> {
525
+ let mut pointers = self.pointers.borrow_mut();
526
+ let index = if let Some(&index) = pointers.indices.get(path) {
527
+ index
528
+ } else {
529
+ let path = std::sync::Arc::new(path.clone());
530
+ let index = pointers.requests.len();
531
+ pointers.indices.insert(path.clone(), index);
532
+ pointers
533
+ .requests
534
+ .push(PointerRequest { path, result: None });
535
+ index
536
+ };
537
+ if let Some(value) = pointers.requests[index].result {
538
+ self.apply_runtime(scopes::Key::Pointer(index));
539
+ return Ok(value);
540
+ }
541
+ self.request_runtime(scopes::Key::Pointer(index));
542
+ self.pending_pointer.set(Some(index));
543
+ self.pending.set(Some((
544
+ unsafe { sn_undefined() },
545
+ unsafe { sn_nil() },
546
+ RUNTIME_POINTER,
547
+ )));
548
+ Err(Unsupported)
549
+ }
550
+ pub(super) fn null_number(&self, value: Value, bit: i32, decimal: bool) -> Verdict {
551
+ self.observe(
552
+ unsafe { sn_nil() },
553
+ value,
554
+ RUNTIME_NULL_NUMBER + bit * 2 + i32::from(decimal),
555
+ )
556
+ }
557
+ pub(super) fn invalid_regexp(&self, source: Value, value: Value) -> bool {
558
+ let matches = self.matches.get();
559
+ let invalid = !matches.is_null()
560
+ && unsafe { sn_cached_runtime(matches, RUNTIME_REGEXP_ASSERTION, source, value) }
561
+ == REGEXP_INVALID;
562
+ if invalid {
563
+ self.apply_runtime(scopes::Key::Value(source, value, RUNTIME_REGEXP_ASSERTION));
564
+ }
565
+ invalid
566
+ }
567
+ pub(super) fn capture_report(&self, report: &crate::diagnostics::Report) {
568
+ self.reports.capture(report);
569
+ }
570
+ pub(super) fn apply_report_effects(&self, report: &mut crate::diagnostics::Report) {
571
+ self.reports.apply(report);
572
+ }
573
+ fn request_runtime(&self, key: scopes::Key) {
574
+ self.scopes.request(key);
575
+ self.reports.request_runtime(key);
576
+ }
577
+ fn apply_runtime(&self, key: scopes::Key) {
578
+ self.scopes.apply(key);
579
+ self.reports.apply_runtime(key);
580
+ }
581
+ fn observe(&self, source: Value, value: Value, operation: i32) -> Verdict {
582
+ let matches = self.matches.get();
583
+ if !matches.is_null() {
584
+ let cached = unsafe { sn_cached_runtime(matches, operation, source, value) };
585
+ if operation == RUNTIME_REGEXP_ASSERTION && cached == REGEXP_INVALID {
586
+ self.apply_runtime(scopes::Key::Value(source, value, operation));
587
+ return Invalid;
588
+ }
589
+ let result = decode(cached);
590
+ if result != Unsupported {
591
+ self.apply_runtime(scopes::Key::Value(source, value, operation));
592
+ return result;
593
+ }
594
+ }
595
+ self.request_runtime(scopes::Key::Value(source, value, operation));
596
+ self.pending.set(Some((source, value, operation)));
597
+ Unsupported
598
+ }
599
+ }
600
+ pub(super) struct ScalarFrame<'a> {
601
+ caches: &'a Caches,
602
+ arguments: ScalarArguments,
603
+ required: i32,
604
+ base: i32,
605
+ stride: i32,
606
+ }
607
+ impl ScalarFrame<'_> {
608
+ pub(super) fn ordinal(&self) -> Value {
609
+ self.arguments.ordinal
610
+ }
611
+ pub(super) fn step(&self, step: i32) -> Verdict {
612
+ let Some(operation) = step
613
+ .checked_mul(self.stride)
614
+ .and_then(|step| self.base.checked_add(step))
615
+ .and_then(|operation| operation.checked_add(self.required))
616
+ else {
617
+ return Unsupported;
618
+ };
619
+ let result =
620
+ self.caches
621
+ .observe(self.arguments.ordinal, self.arguments.instance, operation);
622
+ if result == Unsupported {
623
+ self.caches.pending_scalar.set(Some(self.arguments));
624
+ }
625
+ result
626
+ }
627
+ pub(super) fn predicate(&self, step: i32) -> Result<bool, Verdict> {
628
+ match self.step(step) {
629
+ Valid => Ok(true),
630
+ Invalid => Ok(false),
631
+ Unsupported => Err(Unsupported),
632
+ }
633
+ }
634
+ }
635
+ /// # Safety
636
+ /// Caches must be null or the currently active runner's caches, and klass one
637
+ /// of the exact identity scalar classes. No Ruby allocation is performed here.
638
+ #[unsafe(no_mangle)]
639
+ pub unsafe extern "C" fn sn_identity_kind(caches: *const Caches, klass: Value) -> i32 {
640
+ let Some(caches) = (unsafe { caches.as_ref() }) else {
641
+ return TYPE_UNSUPPORTED;
642
+ };
643
+ match caches.observe(klass, unsafe { sn_nil() }, RUNTIME_IDENTITY_METHODS) {
644
+ Valid => TYPE_IDENTITY_SCALAR,
645
+ Invalid | Unsupported => TYPE_UNSUPPORTED,
646
+ }
647
+ }
648
+ #[unsafe(no_mangle)]
649
+ pub extern "C" fn sn_caches_new() -> *mut Caches {
650
+ Box::into_raw(Box::new(Caches::default()))
651
+ }
652
+ /// # Safety
653
+ /// Caches must be owned, returned by sn_caches_new, and freed exactly once.
654
+ #[unsafe(no_mangle)]
655
+ pub unsafe extern "C" fn sn_caches_free(caches: *mut Caches) {
656
+ drop(unsafe { Box::from_raw(caches) });
657
+ }
658
+ /// # Safety
659
+ /// Caches must be live; call under the GVL or while all Ractors are stopped.
660
+ #[unsafe(no_mangle)]
661
+ pub unsafe extern "C" fn sn_caches_size(caches_ptr: *const Caches) -> usize {
662
+ // GC may inspect a stopped runner while its search has borrowed a cache.
663
+ let Ok(caches) = (unsafe { &*caches_ptr }).searches.try_borrow() else {
664
+ return std::mem::size_of::<Caches>();
665
+ };
666
+ let pointers = unsafe { &*caches_ptr }
667
+ .pointers
668
+ .try_borrow()
669
+ .map_or(0, |pointers| {
670
+ pointers.indices.capacity()
671
+ * std::mem::size_of::<(std::sync::Arc<pointer::Path>, usize)>()
672
+ + pointers.requests.capacity() * std::mem::size_of::<PointerRequest>()
673
+ + pointers
674
+ .requests
675
+ .iter()
676
+ .map(|r| std::mem::size_of::<pointer::Path>() + r.path.memory_usage())
677
+ .sum::<usize>()
678
+ });
679
+ let messages = unsafe { &*caches_ptr }
680
+ .messages
681
+ .try_borrow()
682
+ .map_or(0, |messages| {
683
+ messages.capacity() * std::mem::size_of::<(Message, Option<Value>)>()
684
+ });
685
+ let rescued = unsafe { &*caches_ptr }
686
+ .rescued_strings
687
+ .try_borrow()
688
+ .map_or(0, |set| set.capacity() * std::mem::size_of::<StringError>());
689
+ std::mem::size_of::<Caches>()
690
+ + unsafe { &*caches_ptr }.scopes.size()
691
+ + rescued
692
+ + messages
693
+ + unsafe { &*caches_ptr }.reports.memory_usage()
694
+ + unsafe { &*caches_ptr }.properties.memory_usage()
695
+ + pointers
696
+ + caches.capacity() * std::mem::size_of::<Option<Box<Cache>>>()
697
+ + caches
698
+ .iter()
699
+ .flatten()
700
+ .map(|cache| std::mem::size_of::<Cache>() + cache.memory_usage())
701
+ .sum::<usize>()
702
+ }
703
+
704
+ /// # Safety
705
+ /// All pointers must be live. The C table belongs to the owning runner.
706
+ #[unsafe(no_mangle)]
707
+ pub unsafe extern "C" fn sn_caches_runtime(caches: *const Caches, matches: *const c_void) {
708
+ let caches = unsafe { &*caches };
709
+ caches.matches.set(matches);
710
+ caches.reset_pass();
711
+ }
712
+ /// # Safety
713
+ /// Caches must be live and idle under the GVL.
714
+ #[unsafe(no_mangle)]
715
+ pub unsafe extern "C" fn sn_caches_frame_error(caches: *const Caches) -> i32 {
716
+ i32::from(unsafe { &*caches }.frame_error.get())
717
+ }
718
+ /// # Safety
719
+ /// Caches must be idle under the GVL. Capture only a RegexpError raised while
720
+ /// constructing a reached string error; its rescue handler must not catch again.
721
+ #[unsafe(no_mangle)]
722
+ pub unsafe extern "C" fn sn_caches_rescue_regexp(caches: *const Caches) -> i32 {
723
+ let caches = unsafe { &*caches };
724
+ if let Some(key) = caches.pending_rescue.take() {
725
+ caches.scopes.rescue(key);
726
+ caches.rescued_strings.borrow_mut().insert(key);
727
+ 1
728
+ } else {
729
+ 0
730
+ }
731
+ }
732
+ /// # Safety
733
+ /// Caches must be live and idle under the GVL. Consume a transition to ordered
734
+ /// scalar calls before C services a request with newly assigned ordinals.
735
+ #[unsafe(no_mangle)]
736
+ pub unsafe extern "C" fn sn_caches_take_reorder(caches: *const Caches) -> i32 {
737
+ i32::from(unsafe { &*caches }.reorder.replace(false))
738
+ }
739
+ /// # Safety
740
+ /// Caches must be live and called only between complete validation calls.
741
+ #[unsafe(no_mangle)]
742
+ pub unsafe extern "C" fn sn_caches_begin(caches: *const Caches) {
743
+ let caches = unsafe { &*caches };
744
+ caches.scopes.clear();
745
+ caches.reports.clear();
746
+ caches.properties.clear();
747
+ caches.report_continuations_allowed.set(false);
748
+ caches.reset_pass();
749
+ caches.deadline.set(None);
750
+ caches.runtime_order.set(false);
751
+ caches.dynamic_calls.set(false);
752
+ caches.scalar_callback.set(false);
753
+ caches.callback_checkpoints.set(false);
754
+ caches.callbacks_allowed.set(false);
755
+ caches.continuations_allowed.set(false);
756
+ caches.reorder.set(false);
757
+ caches.rescued_strings.borrow_mut().clear();
758
+ *caches.pointers.borrow_mut() = Pointers::default();
759
+ *caches.messages.borrow_mut() = HashMap::new();
760
+ }
761
+ /// # Safety
762
+ /// Caches must be live and no Rust evaluation may be active.
763
+ #[unsafe(no_mangle)]
764
+ pub unsafe extern "C" fn sn_caches_observe(caches: *const Caches) {
765
+ let caches = unsafe { &*caches };
766
+ caches.reset_pass();
767
+ caches.runtime_order.set(true);
768
+ }
769
+ /// # Safety
770
+ /// Caches and output pointers must be live under the GVL.
771
+ #[unsafe(no_mangle)]
772
+ pub unsafe extern "C" fn sn_caches_pending(
773
+ caches: *const Caches,
774
+ source: *mut Value,
775
+ value: *mut Value,
776
+ operation: *mut i32,
777
+ ) -> i32 {
778
+ if let Some((operand, instance, kind)) = unsafe { &*caches }.pending.get() {
779
+ unsafe {
780
+ *source = operand;
781
+ *value = instance;
782
+ *operation = kind;
783
+ }
784
+ 1
785
+ } else {
786
+ 0
787
+ }
788
+ }
789
+ /// # Safety
790
+ /// Both caches must be live and idle under the GVL. The parent has unwound.
791
+ #[unsafe(no_mangle)]
792
+ pub unsafe extern "C" fn sn_caches_child(
793
+ caches: *const Caches,
794
+ parent: *const Caches,
795
+ limited: i32,
796
+ scalar: i32,
797
+ ) {
798
+ let caches = unsafe { &*caches };
799
+ caches.deadline.set(unsafe { &*parent }.deadline.get());
800
+ caches.scalar_callback.set(scalar != 0);
801
+ caches.callback_checkpoints.set(scalar != 0 && limited != 0);
802
+ caches.dynamic_calls.set(true);
803
+ caches.runtime_order.set(true);
804
+ }
805
+ /// # Safety
806
+ /// Caches must be live and idle under the GVL.
807
+ #[unsafe(no_mangle)]
808
+ pub unsafe extern "C" fn sn_caches_is_scalar(caches: *const Caches) -> i32 {
809
+ i32::from(unsafe { &*caches }.scalar_callback())
810
+ }
811
+ /// # Safety
812
+ /// Caches must be live and idle under the GVL, after a Ruby scalar has returned.
813
+ #[unsafe(no_mangle)]
814
+ pub unsafe extern "C" fn sn_caches_deadline_expired(caches: *const Caches) -> i32 {
815
+ i32::from(
816
+ unsafe { &*caches }
817
+ .deadline
818
+ .get()
819
+ .is_some_and(|deadline| std::time::Instant::now() >= deadline),
820
+ )
821
+ }
822
+ /// # Safety
823
+ /// Caches must hold a pending child request and output must be writable.
824
+ #[unsafe(no_mangle)]
825
+ pub unsafe extern "C" fn sn_contains_arguments(
826
+ caches: *const Caches,
827
+ output: *mut ContainsArguments,
828
+ ) {
829
+ unsafe {
830
+ *output = (&*caches)
831
+ .pending_contains
832
+ .get()
833
+ .expect("pending contains callback")
834
+ };
835
+ }
836
+ /// # Safety
837
+ /// Call from the owning runner's GC mark callback while caches are live.
838
+ #[unsafe(no_mangle)]
839
+ pub unsafe extern "C" fn sn_caches_mark(caches: *const Caches) {
840
+ unsafe { &*caches }.scopes.mark();
841
+ unsafe { &*caches }.reports.mark();
842
+ unsafe { &*caches }.properties.mark();
843
+ if let Some(arguments) = unsafe { &*caches }.pending_contains.get() {
844
+ unsafe {
845
+ sn_pin(arguments.rules);
846
+ sn_pin(arguments.instance);
847
+ }
848
+ }
849
+ // All keys also belong to the live original instance/schema. C roots every
850
+ // completed result before recording it, including during stopped-Ractor GC.
851
+ if let Ok(pointers) = unsafe { &*caches }.pointers.try_borrow() {
852
+ for request in &pointers.requests {
853
+ for &(_, key) in &request.path.names {
854
+ unsafe { sn_pin(key) };
855
+ }
856
+ if let Some(value) = request.result {
857
+ unsafe { sn_pin(value) };
858
+ }
859
+ }
860
+ }
861
+
862
+ if let Ok(messages) = unsafe { &*caches }.messages.try_borrow() {
863
+ for (message, result) in messages.iter() {
864
+ unsafe {
865
+ sn_pin(message.operand);
866
+ sn_pin(message.instance);
867
+ if let Some(value) = result {
868
+ sn_pin(*value);
869
+ }
870
+ }
871
+ }
872
+ }
873
+ if let Some(scalar) = unsafe { &*caches }.pending_scalar.get() {
874
+ unsafe {
875
+ sn_pin(scalar.rules);
876
+ sn_pin(scalar.instance);
877
+ }
878
+ }
879
+ if let Some((source, value, _)) = unsafe { &*caches }.pending.get() {
880
+ unsafe {
881
+ sn_pin(source);
882
+ sn_pin(value);
883
+ }
884
+ }
885
+ }
886
+
887
+ /// # Safety
888
+ /// Caches must have a pending pointer request and be idle under the GVL.
889
+ #[unsafe(no_mangle)]
890
+ pub unsafe extern "C" fn sn_pointer_parts(caches: *const Caches) -> usize {
891
+ let caches = unsafe { &*caches };
892
+ caches.pointers.borrow().requests[caches.pending_pointer.get().unwrap()]
893
+ .path
894
+ .names
895
+ .len()
896
+ + 1
897
+ }
898
+ /// # Safety
899
+ /// Caches must have a pending pointer request; index must be in bounds and
900
+ /// output pointers writable. Text remains owned by the request until cleanup.
901
+ #[unsafe(no_mangle)]
902
+ pub unsafe extern "C" fn sn_pointer_part(
903
+ caches: *const Caches,
904
+ index: usize,
905
+ data: *mut *const u8,
906
+ len: *mut usize,
907
+ key: *mut Value,
908
+ ) {
909
+ let caches = unsafe { &*caches };
910
+ let pointers = caches.pointers.borrow();
911
+ let path = &pointers.requests[caches.pending_pointer.get().unwrap()].path;
912
+ let start = if index == 0 {
913
+ 0
914
+ } else {
915
+ path.names[index - 1].0
916
+ };
917
+ let (end, name) = path
918
+ .names
919
+ .get(index)
920
+ .copied()
921
+ .unwrap_or((path.text.len(), unsafe { sn_undefined() }));
922
+ unsafe {
923
+ *data = path.text.as_ptr().add(start);
924
+ *len = end - start;
925
+ *key = name;
926
+ }
927
+ }
928
+ /// # Safety
929
+ /// Caches must have a pending pointer request. Value must be rooted by the
930
+ /// owning C runner until cleanup, with no active Rust evaluation.
931
+ #[unsafe(no_mangle)]
932
+ pub unsafe extern "C" fn sn_pointer_result(caches: *const Caches, value: Value) {
933
+ let caches = unsafe { &*caches };
934
+ caches.pointers.borrow_mut().requests[caches.pending_pointer.get().unwrap()].result =
935
+ Some(value);
936
+ }
937
+
938
+ /// # Safety
939
+ /// Caches must have a pending message and the output pointer must be writable.
940
+ #[unsafe(no_mangle)]
941
+ pub unsafe extern "C" fn sn_message_arguments(
942
+ caches: *const Caches,
943
+ output: *mut MessageArguments,
944
+ ) {
945
+ let message = unsafe { &*caches }.pending_message.get().unwrap();
946
+ unsafe {
947
+ *output = MessageArguments {
948
+ keyword: message.keyword.as_ptr(),
949
+ keyword_len: message.keyword.len(),
950
+ opcode: message.opcode,
951
+ operand: message.operand,
952
+ instance: message.instance,
953
+ detail: message.detail,
954
+ frame: message.frame,
955
+ };
956
+ }
957
+ }
958
+ /// # Safety
959
+ /// Caches must have a pending message with no active Rust evaluation.
960
+ #[unsafe(no_mangle)]
961
+ pub unsafe extern "C" fn sn_message_result(caches: *const Caches, result: Value) {
962
+ let caches = unsafe { &*caches };
963
+ caches
964
+ .messages
965
+ .borrow_mut()
966
+ .insert(caches.pending_message.get().unwrap(), Some(result));
967
+ }
968
+
969
+ /// # Safety
970
+ /// Caches must have a pending scalar request and output must be writable.
971
+ #[unsafe(no_mangle)]
972
+ pub unsafe extern "C" fn sn_scalar_arguments(caches: *const Caches, output: *mut ScalarArguments) {
973
+ unsafe {
974
+ *output = (&*caches).pending_scalar.get().unwrap();
975
+ }
976
+ }
977
+
978
+ #[cfg(test)]
979
+ mod tests {
980
+ use super::*;
981
+ #[test]
982
+ fn child_modes_inherit_deadlines_without_adding_normal_scalar_checkpoints() {
983
+ let parent = Caches::default();
984
+ let child = Caches::default();
985
+ let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
986
+ parent.deadline.set(Some(deadline));
987
+ unsafe { sn_caches_child(&child, &parent, 1, 0) };
988
+ assert_eq!(child.deadline.get(), Some(deadline));
989
+ assert!(!child.scalar_callback());
990
+ assert!(!child.callback_checkpoints.get());
991
+ assert!(child.dynamic_calls());
992
+ assert!(child.runtime_order());
993
+ unsafe { sn_caches_child(&child, &parent, 1, 1) };
994
+ assert_eq!(child.deadline.get(), Some(deadline));
995
+ assert!(child.scalar_callback());
996
+ assert!(child.callback_checkpoints.get());
997
+ }
998
+
999
+ #[test]
1000
+ fn reports_memory_when_a_search_cache_is_borrowed() {
1001
+ let caches = Caches::default();
1002
+ let regex = Regex::new("value[0-9]+").unwrap();
1003
+ assert!(caches.is_match(0, &regex, b"value1"));
1004
+ assert!(unsafe { sn_caches_size(&caches) } > std::mem::size_of::<Caches>());
1005
+ let _search = caches.searches.borrow_mut();
1006
+ assert_eq!(
1007
+ unsafe { sn_caches_size(&caches) },
1008
+ std::mem::size_of::<Caches>()
1009
+ );
1010
+ }
1011
+ }
1012
+
1013
+ /// # Safety
1014
+ /// Caches are idle at the start of a public invocation with a reused Ruby pool.
1015
+ #[unsafe(no_mangle)]
1016
+ pub unsafe extern "C" fn sn_caches_reuse_buffers(caches: *const Caches) {
1017
+ let caches = unsafe { &*caches };
1018
+ caches.dynamic_calls.set(true);
1019
+ caches.runtime_order.set(true);
1020
+ }