schemurai 3.0.1 → 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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -4
  3. data/ext/schemurai_native/Cargo.lock +48 -1
  4. data/ext/schemurai_native/Cargo.toml +3 -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 +56 -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 +8 -6
  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/lib.rs +910 -237
  20. data/ext/schemurai_native/src/memo.rs +11 -38
  21. data/ext/schemurai_native/src/number/arbitrary.rs +154 -0
  22. data/ext/schemurai_native/src/number.rs +250 -59
  23. data/ext/schemurai_native/src/pattern.rs +113 -101
  24. data/ext/schemurai_native/src/pointer.rs +97 -0
  25. data/ext/schemurai_native/src/runtime/properties.rs +162 -0
  26. data/ext/schemurai_native/src/runtime/reports.rs +266 -0
  27. data/ext/schemurai_native/src/runtime/scopes.rs +430 -0
  28. data/ext/schemurai_native/src/runtime.rs +1020 -0
  29. data/ext/schemurai_native/src/scope.rs +60 -16
  30. data/ext/schemurai_native/src/string.rs +68 -0
  31. data/ext/schemurai_native/src/tracking.rs +244 -174
  32. data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
  33. data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
  34. data/lib/schemurai/container_checks.rb +534 -0
  35. data/lib/schemurai/decimal.rb +11 -0
  36. data/lib/schemurai/dialect.rb +0 -12
  37. data/lib/schemurai/ecma_regexp.rb +46 -0
  38. data/lib/schemurai/evaluation_pool.rb +66 -0
  39. data/lib/schemurai/evaluator.rb +9 -48
  40. data/lib/schemurai/json_pointer.rb +11 -0
  41. data/lib/schemurai/native/diagnostics.rb +4 -3
  42. data/lib/schemurai/native/evaluator.rb +38 -10
  43. data/lib/schemurai/native/null_numeric.rb +23 -0
  44. data/lib/schemurai/number_checks.rb +128 -0
  45. data/lib/schemurai/string_checks.rb +166 -0
  46. data/lib/schemurai/type_checks.rb +59 -0
  47. data/lib/schemurai/value_equality.rb +72 -0
  48. data/lib/schemurai/version.rb +1 -1
  49. data/lib/schemurai/vm/compiler.rb +18 -26
  50. data/lib/schemurai/vm/evaluator.rb +87 -545
  51. data/lib/schemurai/vm/limited_evaluator.rb +7 -22
  52. metadata +24 -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
  }
@@ -138,10 +102,6 @@ impl Pattern {
138
102
  // Select the crate matcher once for a homogeneous array. The borrowed
139
103
  // Ruby slices are used synchronously; classification and encoding are
140
104
  // still checked on every element, including non-string instances.
141
- pub(super) fn eval_items(&self, caches: &Caches, required: i32, values: &[Value]) -> Verdict {
142
- self.visit_invalid_items(caches, required, values, |_, _| Invalid)
143
- }
144
-
145
105
  pub(super) fn visit_invalid_items(
146
106
  &self,
147
107
  caches: &Caches,
@@ -149,30 +109,97 @@ impl Pattern {
149
109
  values: &[Value],
150
110
  invalid: impl FnMut(usize, Value) -> Verdict,
151
111
  ) -> Verdict {
152
- match self {
153
- Self::Dfa(dfa) => scan_items(required, values, invalid, |bytes| {
154
- 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))
155
119
  {
156
120
  Ok(matched) => verdict(matched.is_some()),
157
121
  Err(_) => Unsupported,
158
- }
159
- }),
160
- Self::Compiled { regex, index } => scan_items(required, values, invalid, |bytes| {
161
- verdict(caches.is_match(*index, regex, bytes))
162
- }),
163
- 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
+ ),
164
139
  }
165
140
  }
166
141
 
167
142
  pub(super) fn memory_usage(&self) -> usize {
168
- match self {
169
- Self::Dfa(dfa) => std::mem::size_of_val(&**dfa) + dfa.memory_usage(),
170
- Self::Compiled { regex, .. } => regex.memory_usage(),
171
- 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,
172
147
  }
173
148
  }
174
149
 
175
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 {
176
203
  if unsafe { sn_kind(value) } != TYPE_STRING {
177
204
  return Self::Unsupported;
178
205
  }
@@ -231,21 +258,7 @@ impl Pattern {
231
258
  .unwrap_or(Self::Unsupported)
232
259
  }
233
260
 
234
- pub(super) fn eval(&self, caches: &Caches, value: Value) -> Verdict {
235
- if matches!(self, Self::Unsupported) {
236
- return Unsupported;
237
- }
238
- let mut data = std::ptr::null();
239
- let mut len = 0;
240
- let encoding = unsafe { sn_pattern_view(value, &mut data, &mut len) };
241
- if encoding == ENCODING_OTHER {
242
- return Unsupported;
243
- }
244
- let bytes = unsafe { std::slice::from_raw_parts(data, len) };
245
- self.eval_bytes(caches, bytes, encoding)
246
- }
247
-
248
- 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 {
249
262
  if encoding == ENCODING_OTHER || matches!(self, Self::Unsupported) {
250
263
  return Unsupported;
251
264
  }
@@ -285,13 +298,18 @@ fn scan_items(
285
298
  values: &[Value],
286
299
  mut invalid: impl FnMut(usize, Value) -> Verdict,
287
300
  mut matches: impl FnMut(&[u8]) -> Verdict,
301
+ mut fallback: impl FnMut(Value) -> Verdict,
288
302
  ) -> Verdict {
289
303
  for (index, &value) in values.iter().enumerate() {
290
304
  let mut data = std::ptr::null();
291
305
  let mut len = 0;
292
306
  let encoding = unsafe { sn_pattern_item_view(value, &mut data, &mut len) };
293
307
  if encoding == -TYPE_UNSUPPORTED {
294
- return Unsupported;
308
+ let result = invalid(index, value);
309
+ if result != Valid {
310
+ return result;
311
+ }
312
+ continue;
295
313
  }
296
314
  if encoding < 0 {
297
315
  if required != 0 {
@@ -302,14 +320,21 @@ fn scan_items(
302
320
  }
303
321
  continue;
304
322
  }
305
- if encoding == ENCODING_OTHER {
306
- return Unsupported;
307
- }
308
- let bytes = unsafe { std::slice::from_raw_parts(data, len) };
309
- if !valid_encoding(bytes, encoding) {
310
- return Unsupported;
311
- }
312
- 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
+ };
313
338
  match result {
314
339
  Unsupported => return Unsupported,
315
340
  Invalid => {
@@ -441,17 +466,4 @@ mod tests {
441
466
  assert!(valid_encoding(&original, ENCODING_UTF8));
442
467
  assert!(valid_encoding(b"already classified", ENCODING_SEVEN_BIT));
443
468
  }
444
-
445
- #[test]
446
- fn reports_memory_when_a_search_cache_is_borrowed() {
447
- let caches = Caches::default();
448
- let regex = Regex::new("value[0-9]+").unwrap();
449
- assert!(caches.is_match(0, &regex, b"value1"));
450
- assert!(unsafe { sn_caches_size(&caches) } > std::mem::size_of::<Caches>());
451
- let _search = caches.0.borrow_mut();
452
- assert_eq!(
453
- unsafe { sn_caches_size(&caches) },
454
- std::mem::size_of::<Caches>()
455
- );
456
- }
457
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
+ }