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
@@ -1,61 +1,25 @@
1
1
  //! Crate-backed matching over borrowed Ruby string bytes.
2
2
  //! The compatibility adapter translates VM shorthands and absolute anchors;
3
- //! regex-syntax parses syntax, and regex-automata performs every search.
3
+ //! regex-syntax parses syntax and regex-automata handles the fast searches.
4
+ //! Other cases request a libruby match after the Rust evaluation returns.
4
5
  use super::*;
5
6
  use regex_automata::{
6
7
  Anchored, Input, MatchKind,
7
8
  dfa::{Automaton, OverlappingState, StartKind, dense},
8
- meta::{Cache, Regex},
9
+ meta::Regex,
9
10
  nfa::thompson,
10
11
  };
11
12
  use regex_syntax::ast::{Ast, RepetitionKind, RepetitionRange};
12
13
 
13
- // Each validator owns its search caches. Compiled regex/DFA data stays shared.
14
- #[derive(Default)]
15
- pub struct Caches(std::cell::RefCell<Vec<Option<Box<Cache>>>>);
16
- impl Caches {
17
- fn is_match(&self, index: usize, regex: &Regex, bytes: &[u8]) -> bool {
18
- // Each runner is tied to one engine, whose regex indices are fixed at
19
- // compilation. Allocate only the caches this validator actually uses.
20
- let mut caches = self.0.borrow_mut();
21
- if caches.len() <= index {
22
- caches.resize_with(index + 1, || None);
23
- }
24
- let cache = caches[index].get_or_insert_with(|| Box::new(regex.create_cache()));
25
- regex
26
- .search_half_with(cache, &Input::new(bytes).earliest(true))
27
- .is_some()
28
- }
29
- }
30
- #[unsafe(no_mangle)]
31
- pub extern "C" fn sn_caches_new() -> *mut Caches {
32
- Box::into_raw(Box::new(Caches::default()))
33
- }
34
- /// # Safety
35
- /// Caches must be owned, returned by sn_caches_new, and freed exactly once.
36
- #[unsafe(no_mangle)]
37
- pub unsafe extern "C" fn sn_caches_free(caches: *mut Caches) {
38
- drop(unsafe { Box::from_raw(caches) });
39
- }
40
- /// # Safety
41
- /// Caches must be live; call under the GVL or while all Ractors are stopped.
42
- #[unsafe(no_mangle)]
43
- pub unsafe extern "C" fn sn_caches_size(caches: *const Caches) -> usize {
44
- // GC may inspect a stopped runner while its search has borrowed a cache.
45
- let Ok(caches) = (unsafe { &*caches }).0.try_borrow() else {
46
- return std::mem::size_of::<Caches>();
47
- };
48
- std::mem::size_of::<Caches>()
49
- + caches.capacity() * std::mem::size_of::<Option<Box<Cache>>>()
50
- + caches
51
- .iter()
52
- .flatten()
53
- .map(|cache| std::mem::size_of::<Cache>() + cache.memory_usage())
54
- .sum::<usize>()
55
- }
14
+ use super::runtime::Caches;
56
15
 
57
16
  #[derive(Debug)]
58
- pub(super) enum Pattern {
17
+ pub(super) struct Pattern {
18
+ source: Value,
19
+ matcher: Matcher,
20
+ }
21
+ #[derive(Debug)]
22
+ enum Matcher {
59
23
  Unsupported,
60
24
  Dfa(Box<dense::DFA<Vec<u32>>>),
61
25
  Compiled { regex: Regex, index: usize },
@@ -73,7 +37,7 @@ impl PatternGroup {
73
37
  if !(2..=64).contains(&patterns.len())
74
38
  || !patterns
75
39
  .iter()
76
- .all(|(pattern, _)| matches!(pattern, Pattern::Dfa(_)))
40
+ .all(|(pattern, _)| matches!(pattern.matcher, Matcher::Dfa(_)))
77
41
  {
78
42
  return None;
79
43
  }
@@ -109,10 +73,7 @@ impl PatternGroup {
109
73
  return None;
110
74
  }
111
75
  let bytes = unsafe { byte_view(data, len) };
112
- if encoding != ENCODING_SEVEN_BIT
113
- && ((encoding == ENCODING_ASCII && !bytes.is_ascii())
114
- || std::str::from_utf8(bytes).is_err())
115
- {
76
+ if !valid_encoding(bytes, encoding) {
116
77
  return None;
117
78
  }
118
79
  let input = Input::new(bytes).anchored(Anchored::Yes);
@@ -141,10 +102,6 @@ impl Pattern {
141
102
  // Select the crate matcher once for a homogeneous array. The borrowed
142
103
  // Ruby slices are used synchronously; classification and encoding are
143
104
  // still checked on every element, including non-string instances.
144
- pub(super) fn eval_items(&self, caches: &Caches, required: i32, values: &[Value]) -> Verdict {
145
- self.visit_invalid_items(caches, required, values, |_, _| Invalid)
146
- }
147
-
148
105
  pub(super) fn visit_invalid_items(
149
106
  &self,
150
107
  caches: &Caches,
@@ -152,30 +109,97 @@ impl Pattern {
152
109
  values: &[Value],
153
110
  invalid: impl FnMut(usize, Value) -> Verdict,
154
111
  ) -> Verdict {
155
- match self {
156
- Self::Dfa(dfa) => scan_items(required, values, invalid, |bytes| {
157
- match dfa.try_search_fwd(&Input::new(bytes).anchored(Anchored::Yes).earliest(true))
112
+ match &self.matcher {
113
+ Matcher::Dfa(dfa) => scan_items(
114
+ required,
115
+ values,
116
+ invalid,
117
+ |bytes| match dfa
118
+ .try_search_fwd(&Input::new(bytes).anchored(Anchored::Yes).earliest(true))
158
119
  {
159
120
  Ok(matched) => verdict(matched.is_some()),
160
121
  Err(_) => Unsupported,
161
- }
162
- }),
163
- Self::Compiled { regex, index } => scan_items(required, values, invalid, |bytes| {
164
- verdict(caches.is_match(*index, regex, bytes))
165
- }),
166
- Self::Unsupported => scan_items(required, values, invalid, |_| Unsupported),
122
+ },
123
+ |value| caches.ruby_match(self.source, value, true),
124
+ ),
125
+ Matcher::Compiled { regex, index } => scan_items(
126
+ required,
127
+ values,
128
+ invalid,
129
+ |bytes| verdict(caches.is_match(*index, regex, bytes)),
130
+ |value| caches.ruby_match(self.source, value, true),
131
+ ),
132
+ Matcher::Unsupported => scan_items(
133
+ required,
134
+ values,
135
+ invalid,
136
+ |_| Unsupported,
137
+ |value| caches.ruby_match(self.source, value, true),
138
+ ),
167
139
  }
168
140
  }
169
141
 
170
142
  pub(super) fn memory_usage(&self) -> usize {
171
- match self {
172
- Self::Dfa(dfa) => std::mem::size_of_val(&**dfa) + dfa.memory_usage(),
173
- Self::Compiled { regex, .. } => regex.memory_usage(),
174
- Self::Unsupported => 0,
143
+ match &self.matcher {
144
+ Matcher::Dfa(dfa) => std::mem::size_of_val(&**dfa) + dfa.memory_usage(),
145
+ Matcher::Compiled { regex, .. } => regex.memory_usage(),
146
+ Matcher::Unsupported => 0,
175
147
  }
176
148
  }
177
149
 
178
150
  pub(super) fn compile(value: Value, cache_count: &mut usize) -> Self {
151
+ Self {
152
+ source: value,
153
+ matcher: Matcher::compile(value, cache_count),
154
+ }
155
+ }
156
+
157
+ pub(super) fn eval(&self, caches: &Caches, value: Value) -> Verdict {
158
+ self.eval_value(caches, value, false)
159
+ }
160
+ pub(super) fn assertion(&self, caches: &Caches, value: Value) -> Verdict {
161
+ self.eval_value(caches, value, true)
162
+ }
163
+ fn eval_value(&self, caches: &Caches, value: Value, assertion: bool) -> Verdict {
164
+ if matches!(self.matcher, Matcher::Unsupported) {
165
+ return caches.ruby_match(self.source, value, assertion);
166
+ }
167
+ let mut data = std::ptr::null();
168
+ let mut len = 0;
169
+ let encoding = unsafe { sn_pattern_view(value, &mut data, &mut len) };
170
+ if encoding == ENCODING_OTHER {
171
+ return caches.ruby_match(self.source, value, assertion);
172
+ }
173
+ let bytes = unsafe { byte_view(data, len) };
174
+ let result = self.matcher.eval_bytes(caches, bytes, encoding);
175
+ if result == Unsupported {
176
+ caches.ruby_match(self.source, value, assertion)
177
+ } else {
178
+ result
179
+ }
180
+ }
181
+
182
+ pub(super) fn eval_bytes(
183
+ &self,
184
+ caches: &Caches,
185
+ value: Value,
186
+ bytes: &[u8],
187
+ encoding: i32,
188
+ ) -> Verdict {
189
+ let result = self.matcher.eval_bytes(caches, bytes, encoding);
190
+ if result == Unsupported {
191
+ caches.ruby_match(self.source, value, true)
192
+ } else {
193
+ result
194
+ }
195
+ }
196
+ pub(super) fn invalid(&self, caches: &Caches, value: Value) -> bool {
197
+ caches.invalid_regexp(self.source, value)
198
+ }
199
+ }
200
+
201
+ impl Matcher {
202
+ fn compile(value: Value, cache_count: &mut usize) -> Self {
179
203
  if unsafe { sn_kind(value) } != TYPE_STRING {
180
204
  return Self::Unsupported;
181
205
  }
@@ -234,28 +258,11 @@ impl Pattern {
234
258
  .unwrap_or(Self::Unsupported)
235
259
  }
236
260
 
237
- pub(super) fn eval(&self, caches: &Caches, value: Value) -> Verdict {
238
- if matches!(self, Self::Unsupported) {
239
- return Unsupported;
240
- }
241
- let mut data = std::ptr::null();
242
- let mut len = 0;
243
- let encoding = unsafe { sn_pattern_view(value, &mut data, &mut len) };
244
- if encoding == ENCODING_OTHER {
245
- return Unsupported;
246
- }
247
- let bytes = unsafe { std::slice::from_raw_parts(data, len) };
248
- self.eval_bytes(caches, bytes, encoding)
249
- }
250
-
251
- pub(super) fn eval_bytes(&self, caches: &Caches, bytes: &[u8], encoding: i32) -> Verdict {
261
+ fn eval_bytes(&self, caches: &Caches, bytes: &[u8], encoding: i32) -> Verdict {
252
262
  if encoding == ENCODING_OTHER || matches!(self, Self::Unsupported) {
253
263
  return Unsupported;
254
264
  }
255
- if encoding != ENCODING_SEVEN_BIT
256
- && ((encoding == ENCODING_ASCII && !bytes.is_ascii())
257
- || std::str::from_utf8(bytes).is_err())
258
- {
265
+ if !valid_encoding(bytes, encoding) {
259
266
  return Unsupported;
260
267
  }
261
268
  let matched = match self {
@@ -272,18 +279,37 @@ impl Pattern {
272
279
  }
273
280
  }
274
281
 
282
+ // Keep short checks in std and skip scanning when Ruby already proved ASCII.
283
+ // The compatibility SIMD API retains early rejection of malformed UTF-8.
284
+ fn valid_encoding(bytes: &[u8], encoding: i32) -> bool {
285
+ if encoding == ENCODING_SEVEN_BIT {
286
+ true
287
+ } else if encoding == ENCODING_ASCII {
288
+ bytes.is_ascii()
289
+ } else if bytes.len() < 64 {
290
+ std::str::from_utf8(bytes).is_ok()
291
+ } else {
292
+ simdutf8::compat::from_utf8(bytes).is_ok()
293
+ }
294
+ }
295
+
275
296
  fn scan_items(
276
297
  required: i32,
277
298
  values: &[Value],
278
299
  mut invalid: impl FnMut(usize, Value) -> Verdict,
279
300
  mut matches: impl FnMut(&[u8]) -> Verdict,
301
+ mut fallback: impl FnMut(Value) -> Verdict,
280
302
  ) -> Verdict {
281
303
  for (index, &value) in values.iter().enumerate() {
282
304
  let mut data = std::ptr::null();
283
305
  let mut len = 0;
284
306
  let encoding = unsafe { sn_pattern_item_view(value, &mut data, &mut len) };
285
307
  if encoding == -TYPE_UNSUPPORTED {
286
- return Unsupported;
308
+ let result = invalid(index, value);
309
+ if result != Valid {
310
+ return result;
311
+ }
312
+ continue;
287
313
  }
288
314
  if encoding < 0 {
289
315
  if required != 0 {
@@ -294,17 +320,21 @@ fn scan_items(
294
320
  }
295
321
  continue;
296
322
  }
297
- if encoding == ENCODING_OTHER {
298
- return Unsupported;
299
- }
300
- let bytes = unsafe { std::slice::from_raw_parts(data, len) };
301
- if encoding != ENCODING_SEVEN_BIT
302
- && ((encoding == ENCODING_ASCII && !bytes.is_ascii())
303
- || std::str::from_utf8(bytes).is_err())
304
- {
305
- return Unsupported;
306
- }
307
- let result = matches(bytes);
323
+ let result = if encoding == ENCODING_OTHER {
324
+ Unsupported
325
+ } else {
326
+ let bytes = unsafe { byte_view(data, len) };
327
+ if valid_encoding(bytes, encoding) {
328
+ matches(bytes)
329
+ } else {
330
+ Unsupported
331
+ }
332
+ };
333
+ let result = if result == Unsupported {
334
+ fallback(value)
335
+ } else {
336
+ result
337
+ };
308
338
  match result {
309
339
  Unsupported => return Unsupported,
310
340
  Invalid => {
@@ -417,15 +447,23 @@ mod tests {
417
447
  use super::*;
418
448
 
419
449
  #[test]
420
- fn reports_memory_when_a_search_cache_is_borrowed() {
421
- let caches = Caches::default();
422
- let regex = Regex::new("value[0-9]+").unwrap();
423
- assert!(caches.is_match(0, &regex, b"value1"));
424
- assert!(unsafe { sn_caches_size(&caches) } > std::mem::size_of::<Caches>());
425
- let _search = caches.0.borrow_mut();
426
- assert_eq!(
427
- unsafe { sn_caches_size(&caches) },
428
- std::mem::size_of::<Caches>()
429
- );
450
+ fn encoding_validation_matches_std_across_simd_blocks_and_tails() {
451
+ let original = "日𐀀é".repeat(32).into_bytes();
452
+ for length in [63, 64, 65, 127, 128, 129, original.len()] {
453
+ for at in 0..length {
454
+ for byte in 0..=255 {
455
+ let mut input = original[..length].to_vec();
456
+ input[at] = byte;
457
+ assert_eq!(
458
+ valid_encoding(&input, ENCODING_UTF8),
459
+ std::str::from_utf8(&input).is_ok(),
460
+ "length {length}, position {at}, byte {byte}"
461
+ );
462
+ assert_eq!(valid_encoding(&input, ENCODING_ASCII), input.is_ascii());
463
+ }
464
+ }
465
+ }
466
+ assert!(valid_encoding(&original, ENCODING_UTF8));
467
+ assert!(valid_encoding(b"already classified", ENCODING_SEVEN_BIT));
430
468
  }
431
469
  }
@@ -0,0 +1,97 @@
1
+ //! Native UTF-8 pointer text with deferred names that need Ruby string semantics.
2
+ use super::*;
3
+
4
+ #[derive(Clone, Default, Eq, Hash, PartialEq)]
5
+ pub(super) struct Path {
6
+ pub(super) text: String,
7
+ pub(super) names: Vec<(usize, Value)>,
8
+ }
9
+ impl Path {
10
+ pub(super) fn checkpoint(&self) -> (usize, usize) {
11
+ (self.text.len(), self.names.len())
12
+ }
13
+ pub(super) fn truncate(&mut self, checkpoint: (usize, usize)) {
14
+ self.text.truncate(checkpoint.0);
15
+ self.names.truncate(checkpoint.1);
16
+ }
17
+ pub(super) fn push_str(&mut self, text: &str) {
18
+ self.text.push_str(text);
19
+ }
20
+ pub(super) fn index(&mut self, index: usize) {
21
+ use std::fmt::Write;
22
+ write!(self.text, "/{index}").unwrap();
23
+ }
24
+ pub(super) fn name(&mut self, value: Value) -> Result<(), Verdict> {
25
+ if unsafe { sn_kind(value) } != TYPE_STRING {
26
+ return Err(Unsupported);
27
+ }
28
+ self.text.push('/');
29
+ let mut data = std::ptr::null();
30
+ let mut len = 0;
31
+ let encoding = unsafe { sn_string_view(value, &mut data, &mut len) };
32
+ let bytes = unsafe { byte_view(data, len) };
33
+ if matches!(encoding, ENCODING_UTF8 | ENCODING_ASCII)
34
+ && (encoding != ENCODING_ASCII || bytes.is_ascii())
35
+ && let Ok(name) = std::str::from_utf8(bytes)
36
+ {
37
+ for character in name.chars() {
38
+ match character {
39
+ '~' => self.text.push_str("~0"),
40
+ '/' => self.text.push_str("~1"),
41
+ _ => self.text.push(character),
42
+ }
43
+ }
44
+ } else {
45
+ self.names.push((self.text.len(), value));
46
+ }
47
+ Ok(())
48
+ }
49
+ pub(super) fn render(
50
+ &self,
51
+ caches: &runtime::Caches,
52
+ keyword: Option<&str>,
53
+ ) -> Result<Rendered, Verdict> {
54
+ if self.names.is_empty() {
55
+ let text = keyword.map_or_else(
56
+ || self.text.clone(),
57
+ |keyword| format!("{}/{keyword}", self.text),
58
+ );
59
+ Ok(Rendered::Native(text))
60
+ } else if let Some(keyword) = keyword {
61
+ let mut path = self.clone();
62
+ path.push_str("/");
63
+ path.push_str(keyword);
64
+ caches.pointer(&path).map(Rendered::Ruby)
65
+ } else {
66
+ caches.pointer(self).map(Rendered::Ruby)
67
+ }
68
+ }
69
+ pub(super) fn memory_usage(&self) -> usize {
70
+ self.text.capacity() + self.names.capacity() * std::mem::size_of::<(usize, Value)>()
71
+ }
72
+ }
73
+ #[derive(Clone)]
74
+ pub(super) enum Rendered {
75
+ Native(String),
76
+ Ruby(Value),
77
+ }
78
+ impl Rendered {
79
+ pub(super) fn bytes(&self) -> &[u8] {
80
+ match self {
81
+ Self::Native(text) => text.as_bytes(),
82
+ Self::Ruby(_) => &[],
83
+ }
84
+ }
85
+ pub(super) fn value(&self) -> Value {
86
+ match self {
87
+ Self::Native(_) => unsafe { sn_nil() },
88
+ Self::Ruby(value) => *value,
89
+ }
90
+ }
91
+ pub(super) fn memory_usage(&self) -> usize {
92
+ match self {
93
+ Self::Native(text) => text.capacity(),
94
+ Self::Ruby(_) => 0,
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,162 @@
1
+ //! Opaque evaluation buffers for annotation merges that require Ruby semantics.
2
+ use super::*;
3
+ use crate::tracking::{RuntimeLocations, Tracked};
4
+ use std::cell::RefCell;
5
+
6
+ struct Part {
7
+ runtime: Option<RuntimeLocations>,
8
+ keys: Vec<Value>,
9
+ items: Vec<usize>,
10
+ }
11
+ impl Part {
12
+ fn new(engine: &Engine, tracked: &Tracked) -> Self {
13
+ Self {
14
+ runtime: tracked.runtime_locations(),
15
+ keys: tracked.native_properties(engine),
16
+ items: tracked.native_items(),
17
+ }
18
+ }
19
+ fn mark(&self) {
20
+ if let Some(runtime) = self.runtime {
21
+ runtime.mark();
22
+ }
23
+ for &key in &self.keys {
24
+ unsafe {
25
+ sn_pin(key);
26
+ }
27
+ }
28
+ }
29
+ fn memory_usage(&self) -> usize {
30
+ self.keys.capacity() * std::mem::size_of::<Value>()
31
+ + self.items.capacity() * std::mem::size_of::<usize>()
32
+ }
33
+ }
34
+ #[derive(Default)]
35
+ pub(super) struct Properties {
36
+ pending: RefCell<Option<(Value, [Part; 2])>>,
37
+ completed: RefCell<HashMap<Value, RuntimeLocations>>,
38
+ }
39
+ impl Properties {
40
+ pub(super) fn reset(&self) {
41
+ self.pending.borrow_mut().take();
42
+ }
43
+ pub(super) fn clear(&self) {
44
+ self.reset();
45
+ *self.completed.borrow_mut() = HashMap::new();
46
+ }
47
+ pub(super) fn memory_usage(&self) -> usize {
48
+ self.pending.try_borrow().map_or(0, |pending| {
49
+ pending.as_ref().map_or(0, |(_, parts)| {
50
+ parts.iter().map(Part::memory_usage).sum::<usize>()
51
+ })
52
+ }) + self.completed.try_borrow().map_or(0, |results| {
53
+ results.capacity() * std::mem::size_of::<(Value, RuntimeLocations)>()
54
+ })
55
+ }
56
+ pub(super) fn mark(&self) {
57
+ if let Ok(pending) = self.pending.try_borrow()
58
+ && let Some((_, parts)) = &*pending
59
+ {
60
+ for part in parts {
61
+ part.mark();
62
+ }
63
+ }
64
+ if let Ok(completed) = self.completed.try_borrow() {
65
+ for runtime in completed.values() {
66
+ runtime.mark();
67
+ }
68
+ }
69
+ }
70
+ }
71
+ impl Caches {
72
+ pub(crate) fn merge_properties(
73
+ &self,
74
+ engine: &Engine,
75
+ left: &Tracked,
76
+ right: &Tracked,
77
+ ) -> Result<RuntimeLocations, Verdict> {
78
+ let ordinal = self.next_call()?;
79
+ if self.observe(ordinal, unsafe { sn_nil() }, RUNTIME_PROPERTY_MERGE) == Unsupported {
80
+ *self.properties.pending.borrow_mut() =
81
+ Some((ordinal, [Part::new(engine, left), Part::new(engine, right)]));
82
+ return Err(Unsupported);
83
+ }
84
+ self.properties
85
+ .completed
86
+ .borrow()
87
+ .get(&ordinal)
88
+ .copied()
89
+ .ok_or(Unsupported)
90
+ }
91
+ }
92
+ /// # Safety
93
+ /// Caches are idle with a pending merge. Side is 0/1 and outputs are writable.
94
+ #[unsafe(no_mangle)]
95
+ pub unsafe extern "C" fn sn_properties_part(
96
+ caches: *const Caches,
97
+ side: usize,
98
+ properties: *mut usize,
99
+ items: *mut usize,
100
+ ) -> Value {
101
+ let pending = unsafe { &*caches }.properties.pending.borrow();
102
+ let part = &pending.as_ref().expect("property merge").1[side];
103
+ unsafe {
104
+ *properties = part.keys.len();
105
+ *items = part.items.len();
106
+ }
107
+ part.runtime
108
+ .map_or_else(|| unsafe { sn_nil() }, |runtime| runtime.buffer)
109
+ }
110
+ /// # Safety
111
+ /// Same pending merge requirements as sn_properties_part; index is in bounds.
112
+ #[unsafe(no_mangle)]
113
+ pub unsafe extern "C" fn sn_properties_key(
114
+ caches: *const Caches,
115
+ side: usize,
116
+ index: usize,
117
+ ) -> Value {
118
+ unsafe { &*caches }
119
+ .properties
120
+ .pending
121
+ .borrow()
122
+ .as_ref()
123
+ .expect("property merge")
124
+ .1[side]
125
+ .keys[index]
126
+ }
127
+ /// # Safety
128
+ /// Same pending merge requirements as sn_properties_part; index is in bounds.
129
+ #[unsafe(no_mangle)]
130
+ pub unsafe extern "C" fn sn_properties_item(
131
+ caches: *const Caches,
132
+ side: usize,
133
+ index: usize,
134
+ ) -> usize {
135
+ unsafe { &*caches }
136
+ .properties
137
+ .pending
138
+ .borrow()
139
+ .as_ref()
140
+ .expect("property merge")
141
+ .1[side]
142
+ .items[index]
143
+ }
144
+ /// # Safety
145
+ /// Caches are idle; locations is readable and all handles are rooted by C first.
146
+ #[unsafe(no_mangle)]
147
+ pub unsafe extern "C" fn sn_properties_result(
148
+ caches: *const Caches,
149
+ locations: *const RuntimeLocations,
150
+ ) {
151
+ let properties = &unsafe { &*caches }.properties;
152
+ let ordinal = properties
153
+ .pending
154
+ .borrow()
155
+ .as_ref()
156
+ .expect("property merge")
157
+ .0;
158
+ properties
159
+ .completed
160
+ .borrow_mut()
161
+ .insert(ordinal, unsafe { *locations });
162
+ }