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,5 +1,6 @@
1
1
  //! Content validation borrows Ruby string storage. Base64 decoding uses bounded
2
- //! stack scratch; JSON scanning never constructs a tree.
2
+ //! stack scratch; native JSON scanning never constructs a tree. Other source
3
+ //! encodings request Ruby parsing after Rust evaluation returns.
3
4
  use super::*;
4
5
  use std::mem::MaybeUninit;
5
6
 
@@ -14,23 +15,28 @@ pub(super) struct Content {
14
15
  pub(super) json: bool,
15
16
  }
16
17
  impl Content {
17
- pub(super) fn eval_items(&self, required: i32, values: &[Value]) -> Verdict {
18
- self.visit_invalid_items(required, values, |_, _| Invalid)
19
- }
20
18
  pub(super) fn visit_invalid_items(
21
19
  &self,
20
+ caches: &runtime::Caches,
22
21
  required: i32,
23
22
  values: &[Value],
24
23
  invalid: impl FnMut(usize, Value) -> Verdict,
25
24
  ) -> Verdict {
26
25
  match (self.base64, self.json) {
27
- (true, true) => Self::visit_items_impl::<true, true>(required, values, invalid),
28
- (true, false) => Self::visit_items_impl::<true, false>(required, values, invalid),
29
- (false, true) => Self::visit_items_impl::<false, true>(required, values, invalid),
30
- (false, false) => Self::visit_items_impl::<false, false>(required, values, invalid),
26
+ (true, true) => Self::visit_items_impl::<true, true>(caches, required, values, invalid),
27
+ (true, false) => {
28
+ Self::visit_items_impl::<true, false>(caches, required, values, invalid)
29
+ }
30
+ (false, true) => {
31
+ Self::visit_items_impl::<false, true>(caches, required, values, invalid)
32
+ }
33
+ (false, false) => {
34
+ Self::visit_items_impl::<false, false>(caches, required, values, invalid)
35
+ }
31
36
  }
32
37
  }
33
38
  fn visit_items_impl<const BASE64: bool, const JSON: bool>(
39
+ caches: &runtime::Caches,
34
40
  required: i32,
35
41
  values: &[Value],
36
42
  mut invalid: impl FnMut(usize, Value) -> Verdict,
@@ -45,9 +51,15 @@ impl Content {
45
51
  } else {
46
52
  unsafe { sn_format_item_view(value, data.as_mut_ptr(), len.as_mut_ptr()) }
47
53
  };
48
- let result = if kind == -TYPE_UNSUPPORTED || (JSON && !BASE64 && kind == ENCODING_OTHER)
49
- {
50
- Unsupported
54
+ if kind == -TYPE_UNSUPPORTED {
55
+ let status = invalid(index, value);
56
+ if status != Valid {
57
+ return status;
58
+ }
59
+ continue;
60
+ }
61
+ let result = if JSON && !BASE64 && kind == ENCODING_OTHER {
62
+ caches.json(value)
51
63
  } else if kind < 0 {
52
64
  verdict(required == 0)
53
65
  } else {
@@ -55,12 +67,12 @@ impl Content {
55
67
  let bytes = unsafe { byte_view(data.assume_init(), len.assume_init()) };
56
68
  if JSON && !BASE64 {
57
69
  if kind == ENCODING_ASCII && !bytes.is_ascii() {
58
- Unsupported
70
+ caches.json(value)
59
71
  } else {
60
72
  json(bytes)
61
73
  }
62
74
  } else {
63
- Self::eval_impl::<true, BASE64, JSON>(value, bytes, 0)
75
+ Self::eval_impl::<true, BASE64, JSON>(caches, value, bytes, 0)
64
76
  }
65
77
  };
66
78
  match result {
@@ -76,26 +88,40 @@ impl Content {
76
88
  }
77
89
  Valid
78
90
  }
79
- pub(super) fn eval(&self, value: Value) -> Verdict {
80
- self.eval_selected::<false>(value, &[], 0)
91
+ pub(super) fn eval(&self, caches: &runtime::Caches, value: Value) -> Verdict {
92
+ self.eval_selected::<false>(caches, value, &[], 0)
81
93
  }
82
- pub(super) fn eval_borrowed(&self, value: Value, bytes: &[u8], encoding: i32) -> Verdict {
83
- self.eval_selected::<true>(value, bytes, encoding)
94
+ pub(super) fn eval_borrowed(
95
+ &self,
96
+ caches: &runtime::Caches,
97
+ value: Value,
98
+ bytes: &[u8],
99
+ encoding: i32,
100
+ ) -> Verdict {
101
+ self.eval_selected::<true>(caches, value, bytes, encoding)
84
102
  }
85
103
  fn eval_selected<const BORROWED: bool>(
86
104
  &self,
105
+ caches: &runtime::Caches,
87
106
  value: Value,
88
107
  bytes: &[u8],
89
108
  encoding: i32,
90
109
  ) -> Verdict {
91
110
  match (self.base64, self.json) {
92
- (true, true) => Self::eval_impl::<BORROWED, true, true>(value, bytes, encoding),
93
- (true, false) => Self::eval_impl::<BORROWED, true, false>(value, bytes, encoding),
94
- (false, true) => Self::eval_impl::<BORROWED, false, true>(value, bytes, encoding),
95
- (false, false) => Self::eval_impl::<BORROWED, false, false>(value, bytes, encoding),
111
+ (true, true) => Self::eval_impl::<BORROWED, true, true>(caches, value, bytes, encoding),
112
+ (true, false) => {
113
+ Self::eval_impl::<BORROWED, true, false>(caches, value, bytes, encoding)
114
+ }
115
+ (false, true) => {
116
+ Self::eval_impl::<BORROWED, false, true>(caches, value, bytes, encoding)
117
+ }
118
+ (false, false) => {
119
+ Self::eval_impl::<BORROWED, false, false>(caches, value, bytes, encoding)
120
+ }
96
121
  }
97
122
  }
98
123
  fn eval_impl<const BORROWED: bool, const BASE64: bool, const JSON: bool>(
124
+ caches: &runtime::Caches,
99
125
  value: Value,
100
126
  bytes: &[u8],
101
127
  encoding: i32,
@@ -142,7 +168,7 @@ impl Content {
142
168
  _ => unsafe { sn_json_encoding(value) },
143
169
  };
144
170
  if encoding == ENCODING_OTHER || (encoding == ENCODING_ASCII && !bytes.is_ascii()) {
145
- return Unsupported;
171
+ return caches.json(value);
146
172
  }
147
173
  }
148
174
  if !JSON {
@@ -161,12 +187,24 @@ fn decode_json_medium(bytes: &[u8], length: usize) -> Verdict {
161
187
  decode_json_turbo(bytes, length, &mut scratch)
162
188
  }
163
189
 
190
+ // Keep the 8 KiB scratch frame out of shorter decoded-content paths.
164
191
  #[inline(never)]
165
192
  fn decode_json_direct(bytes: &[u8], length: usize) -> Verdict {
166
193
  let mut scratch = [0; DIRECT_BYTES + 2];
167
194
  decode_json_turbo(bytes, length, &mut scratch)
168
195
  }
169
196
 
197
+ #[cfg(target_arch = "aarch64")]
198
+ fn decode_json_turbo(bytes: &[u8], length: usize, scratch: &mut [u8]) -> Verdict {
199
+ let capacity = base64_turbo::STANDARD.decoded_len_estimate(bytes.len());
200
+ if decode_groups(bytes, &mut scratch[..capacity], length) {
201
+ json(&scratch[..length])
202
+ } else {
203
+ Invalid
204
+ }
205
+ }
206
+
207
+ #[cfg(not(target_arch = "aarch64"))]
170
208
  fn decode_json_turbo(bytes: &[u8], length: usize, scratch: &mut [u8]) -> Verdict {
171
209
  // The slice API requires an initialized upper-bound buffer, including
172
210
  // up to two padding bytes beyond the exact decoded length. Only the
@@ -195,6 +233,13 @@ fn decode_json_stream(bytes: &[u8], length: usize) -> Verdict {
195
233
 
196
234
  // Decode borrowed bytes into the caller's fixed stack scratch. The STANDARD
197
235
  // variant also checks each chunk's alphabet, padding and unused trailing bits.
236
+ #[cfg(target_arch = "aarch64")]
237
+ fn decode_groups(bytes: &[u8], output: &mut [u8], expected: usize) -> bool {
238
+ use base64_simd::AsOut;
239
+ matches!(base64_simd::STANDARD.decode(bytes, output.as_out()), Ok(decoded) if decoded.len() == expected)
240
+ }
241
+
242
+ #[cfg(not(target_arch = "aarch64"))]
198
243
  fn decode_groups(bytes: &[u8], output: &mut [u8], expected: usize) -> bool {
199
244
  matches!(
200
245
  base64_turbo::STANDARD.decode_slice(bytes, output),
@@ -210,6 +255,9 @@ trait Input {
210
255
  }
211
256
  }
212
257
  impl Input for &[u8] {
258
+ // Retain byte/take/whitespace specialization together: removing their
259
+ // hints changed parser register/state handling and slowed numeric/literal
260
+ // and nested JSON controls. See benchmark/README.md's inline audit.
213
261
  #[inline(always)]
214
262
  fn byte(&self, at: usize) -> Option<u8> {
215
263
  self.get(at).copied()
@@ -309,7 +357,6 @@ impl Input for &[u8] {
309
357
  .count()
310
358
  }
311
359
  }
312
- #[inline]
313
360
  fn string_special_word(bytes: [u8; 8]) -> u64 {
314
361
  let word = u64::from_le_bytes(bytes);
315
362
  let quote = word ^ 0x2222_2222_2222_2222;
@@ -380,11 +427,9 @@ impl BufferedBase64<'_> {
380
427
  }
381
428
  }
382
429
  impl Input for BufferedBase64<'_> {
383
- #[inline(always)]
384
430
  fn byte(&self, _at: usize) -> Option<u8> {
385
431
  self.decoded[..self.len].get(self.at).copied()
386
432
  }
387
- #[inline(always)]
388
433
  fn advance(&mut self) {
389
434
  self.at += 1;
390
435
  if self.at == self.len {
@@ -414,15 +459,14 @@ struct Parser<I> {
414
459
  at: usize,
415
460
  }
416
461
  impl<I: Input> Parser<I> {
417
- #[inline(always)]
418
462
  fn peek(&self) -> Option<u8> {
419
463
  self.input.byte(self.at)
420
464
  }
421
- #[inline(always)]
422
465
  fn advance(&mut self) {
423
466
  self.at += 1;
424
467
  self.input.advance();
425
468
  }
469
+ // Part of the measured byte/take/whitespace specialization above.
426
470
  #[inline(always)]
427
471
  fn take(&mut self, byte: u8) -> bool {
428
472
  if self.peek() == Some(byte) {
@@ -432,12 +476,15 @@ impl<I: Input> Parser<I> {
432
476
  false
433
477
  }
434
478
  }
479
+ // Keep whitespace state visible to value/container dispatch (inline audit).
435
480
  #[inline(always)]
436
481
  fn whitespace(&mut self) {
437
482
  while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
438
483
  self.advance();
439
484
  }
440
485
  }
486
+ // Paired JSON controls improved 5–6% with dispatch in the callers; see
487
+ // benchmark/README.md, "Full audit after JSON value dispatch inlining".
441
488
  #[inline(always)]
442
489
  fn value(&mut self, depth: usize) -> bool {
443
490
  self.whitespace();
@@ -0,0 +1,392 @@
1
+ //! Array report collection through C-owned handles, outside native schema frames.
2
+ use super::*;
3
+
4
+ /// # Safety
5
+ /// Report is live, immutable and rooted by C; the returned clone is owned by C.
6
+ #[unsafe(no_mangle)]
7
+ pub unsafe extern "C" fn sn_report_clone(report: *const Report) -> *mut Report {
8
+ Box::into_raw(Box::new(unsafe { &*report }.clone()))
9
+ }
10
+ /// # Safety
11
+ /// Report is the live callback collection, immutable and rooted by C.
12
+ #[unsafe(no_mangle)]
13
+ pub unsafe extern "C" fn sn_report_callback_seed(report: *const Report) -> *mut Report {
14
+ Box::into_raw(Box::new(unsafe { &*report }.continuation(true)))
15
+ }
16
+ /// # Safety
17
+ /// Report is live and C-rooted. Keyword is a valid library-owned UTF-8 segment.
18
+ /// The returned seed is owned by C and freed with sn_report_free.
19
+ #[unsafe(no_mangle)]
20
+ pub unsafe extern "C" fn sn_array_report_child(
21
+ report: *const Report,
22
+ index: usize,
23
+ keyword: *const u8,
24
+ len: usize,
25
+ child: usize,
26
+ trial: i32,
27
+ ) -> *mut Report {
28
+ let mut seed = if trial != 0 {
29
+ Report {
30
+ silent: true,
31
+ annotations: true,
32
+ ..Report::default()
33
+ }
34
+ } else {
35
+ unsafe { &*report }.continuation(false)
36
+ };
37
+ if trial == 0 {
38
+ seed.instance_path.index(index);
39
+ seed.schema_path.push_str("/");
40
+ seed.schema_path.push_str(
41
+ std::str::from_utf8(unsafe { byte_view(keyword, len) }).expect("array keyword"),
42
+ );
43
+ if child != usize::MAX {
44
+ seed.schema_path.index(child);
45
+ }
46
+ }
47
+ Box::into_raw(Box::new(seed))
48
+ }
49
+ /// # Safety
50
+ /// Report/engine are live and rooted, node contains the current Array instruction,
51
+ /// keyword is one of the library's array diagnostic IDs, and frame is C-rooted.
52
+ #[unsafe(no_mangle)]
53
+ pub unsafe extern "C" fn sn_array_report_error(
54
+ report: *const Report,
55
+ engine: *const Engine,
56
+ node: usize,
57
+ instance: Value,
58
+ keyword: i32,
59
+ frame: Value,
60
+ ) -> *mut Report {
61
+ let keyword = match keyword {
62
+ 0 => "type",
63
+ 1 => "maxItems",
64
+ 2 => "minItems",
65
+ 3 => "uniqueItems",
66
+ 4 => "contains",
67
+ _ => unreachable!(),
68
+ };
69
+ let seed = unsafe { &*report }.container_error(
70
+ unsafe { &*engine },
71
+ node,
72
+ instance,
73
+ keyword,
74
+ frame,
75
+ false,
76
+ );
77
+ Box::into_raw(Box::new(seed))
78
+ }
79
+ /// # Safety
80
+ /// Collector report is live and exclusively borrowed. C marks its immutable
81
+ /// seed and all completed results instead while this mutation is in progress.
82
+ #[unsafe(no_mangle)]
83
+ pub unsafe extern "C" fn sn_array_report_adopt(
84
+ report: *mut Report,
85
+ completed: *const Report,
86
+ ) -> i32 {
87
+ unsafe { &mut *report }
88
+ .adopt(unsafe { &*completed }.clone())
89
+ .status as i32
90
+ }
91
+ /// # Safety
92
+ /// Report is live and holds an active array accumulator; C uses the same mutation
93
+ /// root guard as sn_array_report_adopt. Index is produced by library traversal.
94
+ #[unsafe(no_mangle)]
95
+ pub unsafe extern "C" fn sn_array_report_record(report: *mut Report, index: usize) {
96
+ unsafe { &mut *report }
97
+ .evaluation
98
+ .as_mut()
99
+ .expect("array locations")
100
+ .item(index);
101
+ }
102
+ /// # Safety
103
+ /// Report is live and immutable during this call; index is a library index.
104
+ #[unsafe(no_mangle)]
105
+ pub unsafe extern "C" fn sn_array_report_contains(
106
+ report: *const Report,
107
+ index: usize,
108
+ prior: i32,
109
+ ) -> i32 {
110
+ let report = unsafe { &*report };
111
+ i32::from(
112
+ if prior != 0 {
113
+ &report.container_prior
114
+ } else {
115
+ &report.evaluation
116
+ }
117
+ .as_ref()
118
+ .is_some_and(|locations| locations.contains_item(index)),
119
+ )
120
+ }
121
+ /// # Safety
122
+ /// Report is live and immutable during this call.
123
+ #[unsafe(no_mangle)]
124
+ pub unsafe extern "C" fn sn_report_silent(report: *const Report) -> i32 {
125
+ i32::from(unsafe { &*report }.silent)
126
+ }
127
+ /// # Safety
128
+ /// Report is live, complete and immutable during this call.
129
+ #[unsafe(no_mangle)]
130
+ pub unsafe extern "C" fn sn_report_verdict(report: *const Report) -> i32 {
131
+ unsafe { &*report }
132
+ .evaluation
133
+ .as_ref()
134
+ .expect("completed report")
135
+ .status as i32
136
+ }
137
+ /// # Safety
138
+ /// Report is live with an active array accumulator and C guards mutation as for
139
+ /// sn_array_report_adopt. Only the library completes an array traversal.
140
+ #[unsafe(no_mangle)]
141
+ pub unsafe extern "C" fn sn_array_report_finish(report: *mut Report, valid: i32) {
142
+ let report = unsafe { &mut *report };
143
+ report.container_prior = None;
144
+ if valid == 0 {
145
+ report.failed = true;
146
+ report.evaluation = Some(Tracked::new(Invalid));
147
+ }
148
+ }
149
+ #[cfg(test)]
150
+ mod tests {
151
+ use super::*;
152
+
153
+ #[test]
154
+ fn runtime_effects_keep_the_receiving_path_and_detect_silent_failure() {
155
+ let mut report = Report {
156
+ attempts: 2,
157
+ ..Report::default()
158
+ };
159
+ report.instance_path.index(7);
160
+ report.schema_path.push_str("/items");
161
+ let before = report.continuation(true);
162
+ assert!(before.effect_since(&before).is_none());
163
+ let mut completed = before.clone();
164
+ completed.reserve_error().unwrap();
165
+ completed.errors.push(Error {
166
+ keyword: "type",
167
+ instance_path: pointer::Rendered::Native("/7/0".into()),
168
+ schema_path: pointer::Rendered::Native("/items/items/type".into()),
169
+ opcode: OP_TYPE_INTEGER,
170
+ operand: 0,
171
+ instance: 0,
172
+ detail: 0,
173
+ message: 0,
174
+ });
175
+ let effect = completed.effect_since(&before).unwrap();
176
+ report.apply_effect(effect);
177
+ assert_eq!(report.attempts, 3);
178
+ assert!(report.failed);
179
+ assert_eq!(report.instance_path.text, "/7");
180
+ assert_eq!(report.schema_path.text, "/items");
181
+ assert_eq!(report.errors.len(), 1);
182
+ assert_eq!(report.errors[0].instance_path.bytes(), b"/7/0");
183
+ assert!(!report.annotations);
184
+ let silent = Report {
185
+ silent: true,
186
+ ..Report::default()
187
+ };
188
+ let mut completed = silent.clone();
189
+ completed.failed = true;
190
+ let effect = completed.effect_since(&silent).unwrap();
191
+ assert!(effect.failed);
192
+ assert_eq!(effect.attempts, 0);
193
+ assert!(effect.errors.is_empty());
194
+ }
195
+
196
+ #[test]
197
+ fn collection_reset_replaces_prior_errors_and_counts_without_exporting_trial_policy() {
198
+ let before = Report {
199
+ attempts: 2,
200
+ failed: true,
201
+ ..Report::default()
202
+ };
203
+ let mut completed = before.clone();
204
+ completed.instance_path.index(7);
205
+ completed.clear_collection(true);
206
+ assert_eq!(completed.attempts, 0);
207
+ assert!(!completed.failed);
208
+ assert!(completed.silent);
209
+ let effect = completed.effect_since(&before).unwrap();
210
+ let mut receiver = before.container_continuation(true, &Tracked::new(Valid));
211
+ receiver.instance_path.index(5);
212
+ receiver.apply_effect(effect);
213
+ assert!(receiver.annotations);
214
+ assert!(receiver.silent);
215
+ assert_eq!(receiver.attempts, 0);
216
+ assert!(!receiver.failed);
217
+ assert!(receiver.instance_path.text.is_empty());
218
+ assert_eq!(receiver.collection_reset, 1);
219
+ assert!(receiver.evaluation.is_some());
220
+ let mut outer = before.clone();
221
+ let trial = outer.continuation(true);
222
+ let mut trial_result = trial.clone();
223
+ trial_result.clear_collection(true);
224
+ assert!(!outer.same_collection_policy(&trial_result));
225
+ outer.apply_effect(Report {
226
+ attempts: 1,
227
+ failed: true,
228
+ ..Report::default()
229
+ });
230
+ assert_eq!(outer.attempts, 3);
231
+ assert!(!outer.silent);
232
+ }
233
+
234
+ #[test]
235
+ fn item_results_preserve_accumulators_paths_and_independent_trial_policy() {
236
+ let mut base = Report {
237
+ attempts: 1,
238
+ ..Report::default()
239
+ };
240
+ base.instance_path.text.push_str("/outer");
241
+ base.schema_path.text.push_str("/allOf/1");
242
+ let mut prior = Tracked::new(Valid);
243
+ prior.item(65);
244
+ let mut report = base.container_continuation(true, &prior);
245
+ unsafe {
246
+ sn_array_report_record(&mut report, 4);
247
+ }
248
+ assert_eq!(unsafe { sn_array_report_contains(&report, 65, 1) }, 1);
249
+ assert_eq!(unsafe { sn_array_report_contains(&report, 4, 0) }, 1);
250
+ assert_eq!(unsafe { sn_array_report_contains(&report, 4, 1) }, 0);
251
+ let keyword = b"prefixItems";
252
+ let mut completed = unsafe {
253
+ Box::from_raw(sn_array_report_child(
254
+ &report,
255
+ 4,
256
+ keyword.as_ptr(),
257
+ keyword.len(),
258
+ 4,
259
+ 0,
260
+ ))
261
+ };
262
+ assert_eq!(completed.instance_path.text, "/outer/4");
263
+ assert_eq!(completed.schema_path.text, "/allOf/1/prefixItems/4");
264
+ completed.attempts = 2;
265
+ completed.failed = true;
266
+ completed.evaluation = Some(Tracked::new(Invalid));
267
+ assert_eq!(
268
+ unsafe { sn_array_report_adopt(&mut report, &*completed) },
269
+ VERDICT_INVALID
270
+ );
271
+ assert_eq!(report.instance_path.text, "/outer");
272
+ assert_eq!(report.schema_path.text, "/allOf/1");
273
+ assert_eq!(unsafe { sn_array_report_contains(&report, 65, 1) }, 1);
274
+ assert_eq!(unsafe { sn_array_report_contains(&report, 4, 0) }, 1);
275
+ assert_eq!(report.attempts, 2);
276
+ assert!(report.annotations);
277
+ let trial = unsafe {
278
+ Box::from_raw(sn_array_report_child(
279
+ &report,
280
+ 0,
281
+ std::ptr::null(),
282
+ 0,
283
+ usize::MAX,
284
+ 1,
285
+ ))
286
+ };
287
+ assert!(trial.silent && trial.annotations);
288
+ assert_eq!(trial.attempts, 0);
289
+ assert!(trial.instance_path.text.is_empty());
290
+ assert!(trial.schema_path.text.is_empty());
291
+ unsafe {
292
+ sn_array_report_finish(&mut report, 1);
293
+ }
294
+ assert_eq!(unsafe { sn_report_verdict(&report) }, VERDICT_VALID);
295
+ assert!(report.failed);
296
+ assert!(report.container_prior.is_none());
297
+ }
298
+ }
299
+
300
+ /// # Safety
301
+ /// Report is the current idle C-owned collection and C guards mutation as for adoption.
302
+ #[unsafe(no_mangle)]
303
+ pub unsafe extern "C" fn sn_report_clear_collection(report: *mut Report, paths_nil: i32) {
304
+ unsafe { &mut *report }.clear_collection(paths_nil != 0);
305
+ }
306
+
307
+ /// # Safety
308
+ /// Path arguments are nil or library-owned Arrays rooted by C. No Ruby calls or
309
+ /// allocation occur before the completed owned seed is returned to C.
310
+ #[unsafe(no_mangle)]
311
+ pub unsafe extern "C" fn sn_report_context(
312
+ silent: i32,
313
+ attempts: usize,
314
+ instance_path: Value,
315
+ schema_path: Value,
316
+ ) -> *mut Report {
317
+ let mut report = Report {
318
+ silent: silent != 0,
319
+ attempts,
320
+ annotations: true,
321
+ render_messages: true,
322
+ ..Report::default()
323
+ };
324
+ for (value, path) in [
325
+ (instance_path, &mut report.instance_path),
326
+ (schema_path, &mut report.schema_path),
327
+ ] {
328
+ if value == unsafe { sn_nil() } {
329
+ continue;
330
+ }
331
+ for i in 0..unsafe { sn_len(value) } {
332
+ path.text.push('/');
333
+ path.names
334
+ .push((path.text.len(), unsafe { sn_entry(value, i) }));
335
+ }
336
+ }
337
+ Box::into_raw(Box::new(report))
338
+ }
339
+ /// # Safety
340
+ /// Report is live, idle and C-rooted. Baseline is its original collection count.
341
+ #[unsafe(no_mangle)]
342
+ pub unsafe extern "C" fn sn_report_context_count(report: *const Report, baseline: usize) -> usize {
343
+ let report = unsafe { &*report };
344
+ let count = if report.collection_reset == 0 {
345
+ report.attempts.saturating_sub(baseline)
346
+ } else {
347
+ report.attempts
348
+ };
349
+ if report.silent && report.failed {
350
+ count.max(1)
351
+ } else {
352
+ count
353
+ }
354
+ }
355
+ /// # Safety
356
+ /// Report is live, idle and C-rooted.
357
+ #[unsafe(no_mangle)]
358
+ pub unsafe extern "C" fn sn_report_context_reset(report: *const Report) -> i32 {
359
+ let report = unsafe { &*report };
360
+ if report.collection_reset == 0 {
361
+ 0
362
+ } else if report.reset_path_pops {
363
+ 1
364
+ } else {
365
+ 2
366
+ }
367
+ }
368
+
369
+ /// # Safety
370
+ /// Report is live, immutable and rooted by C. C owns the partial result.
371
+ #[unsafe(no_mangle)]
372
+ pub unsafe extern "C" fn sn_report_partial(report: *const Report) -> *mut Report {
373
+ let mut report = unsafe { &*report }.clone();
374
+ report.complete_callback(false);
375
+ Box::into_raw(Box::new(report))
376
+ }
377
+
378
+ /// # Safety
379
+ /// Report is live, immutable and C-rooted. Prior selects inherited locations.
380
+ #[unsafe(no_mangle)]
381
+ pub unsafe extern "C" fn sn_report_items_set(report: *const Report, prior: i32) -> Value {
382
+ let report = unsafe { &*report };
383
+ let locations = if prior != 0 {
384
+ &report.container_prior
385
+ } else {
386
+ &report.evaluation
387
+ };
388
+ locations
389
+ .as_ref()
390
+ .and_then(Tracked::items_set)
391
+ .unwrap_or_else(|| unsafe { sn_nil() })
392
+ }