schemurai 3.0.1 → 3.1.0

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -7
  3. data/ext/schemurai_native/Cargo.lock +59 -1
  4. data/ext/schemurai_native/Cargo.toml +4 -1
  5. data/ext/schemurai_native/extconf.rb +5 -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 +1064 -325
  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 +210 -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_limits.rb +65 -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/limited_evaluator.rb +2 -59
  43. data/lib/schemurai/native/diagnostics.rb +4 -3
  44. data/lib/schemurai/native/evaluator.rb +38 -10
  45. data/lib/schemurai/native/null_numeric.rb +23 -0
  46. data/lib/schemurai/number_checks.rb +128 -0
  47. data/lib/schemurai/string_checks.rb +166 -0
  48. data/lib/schemurai/type_checks.rb +59 -0
  49. data/lib/schemurai/value_equality.rb +72 -0
  50. data/lib/schemurai/version.rb +1 -1
  51. data/lib/schemurai/vm/compiler.rb +18 -26
  52. data/lib/schemurai/vm/evaluator.rb +87 -545
  53. data/lib/schemurai/vm/limited_evaluator.rb +8 -78
  54. metadata +25 -2
@@ -2,113 +2,569 @@
2
2
  //! returning to C are the output records materialized as Ruby objects.
3
3
  use super::*;
4
4
  use tracking::Tracked;
5
+ mod array;
6
+ mod object;
5
7
 
8
+ fn reference_keyword(opcode: i32) -> &'static str {
9
+ match opcode {
10
+ OP_RECURSIVE_REF => "$recursiveRef",
11
+ OP_DYNAMIC_REF => "$dynamicRef",
12
+ _ => "$ref",
13
+ }
14
+ }
15
+
16
+ #[derive(Clone)]
6
17
  struct Error {
7
18
  keyword: &'static str,
8
- instance_path: String,
9
- schema_path: String,
19
+ instance_path: pointer::Rendered,
20
+ schema_path: pointer::Rendered,
10
21
  opcode: i32,
11
22
  operand: Value,
12
23
  instance: Value,
13
24
  detail: usize,
25
+ message: Value,
14
26
  }
15
- #[derive(Default)]
27
+ #[derive(Clone)]
28
+ struct ErrorEntry {
29
+ source: (i32, Value),
30
+ instance: Value,
31
+ keyword: &'static str,
32
+ frame: Value,
33
+ }
34
+ #[derive(Clone, Default)]
16
35
  pub struct Report {
17
36
  errors: Vec<Error>,
18
- instance_path: String,
19
- schema_path: String,
37
+ annotations: bool,
38
+ evaluation: Option<Tracked>,
39
+ container_prior: Option<Tracked>,
40
+ entry: Option<ErrorEntry>,
41
+ attempts: usize,
42
+ collection_reset: usize,
43
+ reset_path_pops: bool,
44
+ instance_path: pointer::Path,
45
+ schema_path: pointer::Path,
20
46
  // Explicit failure signal for the current evaluation, independent of the
21
47
  // number or rendering of diagnostic records. Child evaluations isolate it.
22
48
  failed: bool,
49
+ silent: bool,
50
+ render_messages: bool,
23
51
  limits: budget::Limits,
24
52
  limit_hit: (u32, usize),
25
53
  }
26
54
  impl Report {
55
+ pub(super) fn same_collection_policy(&self, other: &Self) -> bool {
56
+ self.silent == other.silent
57
+ }
58
+
59
+ fn finish_runtime<T>(
60
+ &mut self,
61
+ caches: &runtime::Caches,
62
+ result: Result<T, Verdict>,
63
+ ) -> Result<T, Verdict> {
64
+ caches.apply_report_effects(self);
65
+ if matches!(result, Err(Unsupported)) {
66
+ caches.capture_report(self);
67
+ }
68
+ result
69
+ }
70
+
71
+ pub(super) fn complete_callback(&mut self, valid: bool) {
72
+ self.evaluation = Some(Tracked::new(if valid { Valid } else { Invalid }));
73
+ }
74
+
75
+ pub(super) fn clear_collection(&mut self, paths_nil: bool) {
76
+ self.collection_reset = self.collection_reset.saturating_add(1);
77
+ self.reset_path_pops = paths_nil;
78
+ self.errors.clear();
79
+ self.attempts = 0;
80
+ self.failed = false;
81
+ self.silent = true;
82
+ self.instance_path = pointer::Path::default();
83
+ self.schema_path = pointer::Path::default();
84
+ }
85
+ pub(super) fn effect_since(&self, before: &Self) -> Option<Self> {
86
+ if self.collection_reset != before.collection_reset {
87
+ return Some(Self {
88
+ errors: self.errors.clone(),
89
+ attempts: self.attempts,
90
+ collection_reset: self.collection_reset,
91
+ reset_path_pops: self.reset_path_pops,
92
+ failed: self.failed,
93
+ silent: self.silent,
94
+ instance_path: self.instance_path.clone(),
95
+ schema_path: self.schema_path.clone(),
96
+ ..Self::default()
97
+ });
98
+ }
99
+ let errors = self.errors.get(before.errors.len()..)?.to_vec();
100
+ let attempts = self.attempts.saturating_sub(before.attempts);
101
+ let failed = self.failed && !before.failed;
102
+ (!errors.is_empty() || attempts != 0 || failed).then_some(Self {
103
+ errors,
104
+ attempts,
105
+ failed,
106
+ ..Self::default()
107
+ })
108
+ }
109
+ pub(super) fn apply_effect(&mut self, effect: Self) {
110
+ if effect.collection_reset != 0 {
111
+ self.collection_reset = effect.collection_reset;
112
+ self.reset_path_pops = effect.reset_path_pops;
113
+ self.errors = effect.errors;
114
+ self.attempts = effect.attempts;
115
+ self.failed = effect.failed;
116
+ self.silent = effect.silent;
117
+ self.instance_path = effect.instance_path;
118
+ self.schema_path = effect.schema_path;
119
+ return;
120
+ }
121
+ self.errors.extend(effect.errors);
122
+ self.attempts = self.attempts.saturating_add(effect.attempts);
123
+ self.failed |= effect.failed;
124
+ }
125
+
126
+ pub(super) fn continuation(&self, annotations: bool) -> Self {
127
+ let mut report = self.clone();
128
+ report.annotations = annotations;
129
+ report.evaluation = None;
130
+ report.container_prior = None;
131
+ report.entry = None;
132
+ report
133
+ }
134
+ pub(super) fn adopt(&mut self, mut completed: Self) -> Tracked {
135
+ let evaluation = completed
136
+ .evaluation
137
+ .take()
138
+ .expect("completed native report");
139
+ completed.annotations = self.annotations;
140
+ completed.evaluation = self.evaluation.take();
141
+ completed.container_prior = self.container_prior.take();
142
+ let instance_path = std::mem::take(&mut self.instance_path);
143
+ let schema_path = std::mem::take(&mut self.schema_path);
144
+ *self = completed;
145
+ self.instance_path = instance_path;
146
+ self.schema_path = schema_path;
147
+ evaluation
148
+ }
149
+ pub(super) fn memory_usage(&self) -> usize {
150
+ self.errors.capacity() * std::mem::size_of::<Error>()
151
+ + self.instance_path.memory_usage()
152
+ + self.schema_path.memory_usage()
153
+ + self
154
+ .errors
155
+ .iter()
156
+ .map(|e| e.instance_path.memory_usage() + e.schema_path.memory_usage())
157
+ .sum::<usize>()
158
+ + self.evaluation.as_ref().map_or(0, Tracked::memory_usage)
159
+ + self
160
+ .container_prior
161
+ .as_ref()
162
+ .map_or(0, Tracked::memory_usage)
163
+ }
164
+ pub(super) fn mark(&self) {
165
+ for path in [&self.instance_path, &self.schema_path] {
166
+ for &(_, value) in &path.names {
167
+ unsafe {
168
+ sn_pin(value);
169
+ }
170
+ }
171
+ }
172
+ for error in &self.errors {
173
+ unsafe {
174
+ sn_pin(error.instance_path.value());
175
+ sn_pin(error.schema_path.value());
176
+ sn_pin(error.operand);
177
+ sn_pin(error.instance);
178
+ sn_pin(error.message);
179
+ }
180
+ }
181
+ if let Some(evaluation) = &self.evaluation {
182
+ evaluation.mark();
183
+ }
184
+ if let Some(prior) = &self.container_prior {
185
+ prior.mark();
186
+ }
187
+ if let Some(entry) = &self.entry {
188
+ unsafe {
189
+ sn_pin(entry.source.1);
190
+ sn_pin(entry.instance);
191
+ sn_pin(entry.frame);
192
+ }
193
+ }
194
+ }
195
+ fn container_error(
196
+ &self,
197
+ engine: &Engine,
198
+ node: usize,
199
+ instance: Value,
200
+ keyword: &'static str,
201
+ frame: Value,
202
+ object: bool,
203
+ ) -> Self {
204
+ let source = engine.nodes[node]
205
+ .iter()
206
+ .zip(&engine.error_operands[node])
207
+ .find_map(|(op, &source)| {
208
+ (if object {
209
+ matches!(op, Op::Object(..))
210
+ } else {
211
+ matches!(op, Op::Array(..))
212
+ })
213
+ .then_some(source)
214
+ })
215
+ .expect("container source");
216
+ let mut seed = self.continuation(false);
217
+ seed.entry = Some(ErrorEntry {
218
+ source,
219
+ instance,
220
+ keyword,
221
+ frame,
222
+ });
223
+ seed
224
+ }
225
+ pub(super) fn container_continuation(&self, annotations: bool, prior: &Tracked) -> Self {
226
+ let mut report = self.continuation(annotations);
227
+ report.container_prior = Some(prior.clone());
228
+ report.evaluation = Some(Tracked::new(Valid));
229
+ report
230
+ }
27
231
  fn add(
28
232
  &mut self,
233
+ caches: &runtime::Caches,
29
234
  keyword: &'static str,
30
235
  source: (i32, Value),
31
236
  instance: Value,
32
237
  detail: usize,
33
238
  ) -> Result<(), Verdict> {
34
- if self.limits.enabled & 1 != 0 && self.errors.len() >= self.limits.max_errors {
35
- self.limit_hit = (1, self.errors.len().saturating_add(1));
239
+ self.add_with_frame(caches, keyword, source, instance, detail, None)
240
+ }
241
+ fn collect_header(
242
+ &mut self,
243
+ caches: &runtime::Caches,
244
+ header: &container::Header,
245
+ required: i32,
246
+ value: Value,
247
+ source: (i32, Value),
248
+ ) -> Result<bool, Verdict> {
249
+ let silent = self.silent;
250
+ let applied = header.failures(caches, required, value, silent, |keyword, frame| {
251
+ self.add_frame(caches, keyword, source, value, frame)
252
+ })?;
253
+ Ok(applied && silent && self.failed)
254
+ }
255
+ fn add_frame(
256
+ &mut self,
257
+ caches: &runtime::Caches,
258
+ keyword: &'static str,
259
+ source: (i32, Value),
260
+ instance: Value,
261
+ frame: Value,
262
+ ) -> Result<(), Verdict> {
263
+ self.add_with_frame(caches, keyword, source, instance, 0, Some(frame))
264
+ }
265
+ fn add_string(
266
+ &mut self,
267
+ caches: &runtime::Caches,
268
+ keyword: &'static str,
269
+ source: (i32, Value),
270
+ instance: Value,
271
+ frame: Value,
272
+ rescued: bool,
273
+ ) -> Result<(), Verdict> {
274
+ if !self.silent && !rescued && keyword != "type" {
275
+ if !caches.begin_string_error(frame, self.errors.len(), keyword) {
276
+ self.reserve_error()?;
277
+ return Err(Invalid);
278
+ }
279
+ } else {
280
+ caches.end_string_error();
281
+ }
282
+ let result = self.add_with_frame(
283
+ caches,
284
+ keyword,
285
+ source,
286
+ instance,
287
+ usize::from(rescued),
288
+ Some(frame),
289
+ );
290
+ if result != Err(Unsupported) {
291
+ caches.end_string_error();
292
+ }
293
+ result
294
+ }
295
+ fn reserve_error(&mut self) -> Result<(), Verdict> {
296
+ if self.limits.enabled & 1 != 0 && self.attempts >= self.limits.max_errors {
297
+ self.limit_hit = (1, self.attempts.saturating_add(1));
36
298
  return Err(Unsupported);
37
299
  }
300
+ self.attempts = self.attempts.saturating_add(1);
38
301
  self.failed = true;
39
- let schema_path = if keyword == "falseSchema" {
40
- self.schema_path.clone()
302
+ Ok(())
303
+ }
304
+ fn add_with_frame(
305
+ &mut self,
306
+ caches: &runtime::Caches,
307
+ keyword: &'static str,
308
+ source: (i32, Value),
309
+ instance: Value,
310
+ detail: usize,
311
+ frame: Option<Value>,
312
+ ) -> Result<(), Verdict> {
313
+ caches.apply_report_effects(self);
314
+ if self.silent {
315
+ self.failed = true;
316
+ return Ok(());
317
+ }
318
+ self.reserve_error()?;
319
+ caches.frame_error(frame.is_some());
320
+ let instance_path = self.instance_path.render(caches, None)?;
321
+ caches.apply_report_effects(self);
322
+ let keyword_segment = (!matches!(
323
+ keyword,
324
+ "falseSchema" | "$ref" | "$recursiveRef" | "$dynamicRef"
325
+ ))
326
+ .then_some(keyword);
327
+ let schema_path = self.schema_path.render(caches, keyword_segment)?;
328
+ caches.apply_report_effects(self);
329
+ let message = if self.render_messages || caches.dynamic_calls() {
330
+ if let Some(frame) = frame {
331
+ caches.frame_message(
332
+ keyword,
333
+ source,
334
+ instance,
335
+ frame,
336
+ self.errors.len(),
337
+ detail != 0,
338
+ )?
339
+ } else {
340
+ caches.error_message(keyword, source, instance, detail, self.errors.len())?
341
+ }
41
342
  } else {
42
- format!("{}/{keyword}", self.schema_path)
343
+ unsafe { sn_nil() }
43
344
  };
345
+ caches.apply_report_effects(self);
44
346
  self.errors.push(Error {
45
347
  keyword,
46
- instance_path: self.instance_path.clone(),
348
+ instance_path,
47
349
  schema_path,
48
350
  opcode: source.0,
49
351
  operand: source.1,
50
352
  instance,
51
353
  detail,
354
+ message,
52
355
  });
356
+ caches.frame_error(false);
53
357
  Ok(())
54
358
  }
359
+ fn cleanup_reset_paths<T>(
360
+ &mut self,
361
+ caches: &runtime::Caches,
362
+ reset: usize,
363
+ result: Result<T, Verdict>,
364
+ count: usize,
365
+ ) -> Result<T, Verdict> {
366
+ if self.collection_reset != reset
367
+ && self.reset_path_pops
368
+ && !matches!(result, Err(Unsupported))
369
+ {
370
+ for _ in 0..count {
371
+ caches.path_pop()?;
372
+ }
373
+ }
374
+ result
375
+ }
55
376
  fn scoped<T>(
56
377
  &mut self,
378
+ caches: &runtime::Caches,
57
379
  segment: &str,
58
380
  instance: Option<usize>,
59
381
  visit: impl FnOnce(&mut Self) -> Result<T, Verdict>,
60
382
  ) -> Result<T, Verdict> {
61
- use std::fmt::Write;
62
- let schema_len = self.schema_path.len();
63
- let instance_len = self.instance_path.len();
64
- self.schema_path.push('/');
383
+ if self.silent {
384
+ return visit(self);
385
+ }
386
+
387
+ let reset = self.collection_reset;
388
+ let schema_len = self.schema_path.checkpoint();
389
+ let instance_len = self.instance_path.checkpoint();
390
+ self.schema_path.push_str("/");
65
391
  self.schema_path.push_str(segment);
66
392
  if let Some(index) = instance {
67
- write!(self.instance_path, "/{index}").unwrap();
393
+ self.instance_path.index(index);
68
394
  }
69
395
  let result = visit(self);
396
+ let result = self.cleanup_reset_paths(
397
+ caches,
398
+ reset,
399
+ result,
400
+ segment.split('/').count() + usize::from(instance.is_some()),
401
+ );
70
402
  self.schema_path.truncate(schema_len);
71
403
  self.instance_path.truncate(instance_len);
72
404
  result
73
405
  }
74
406
  fn property<T>(
75
407
  &mut self,
408
+ caches: &runtime::Caches,
76
409
  segment: &str,
77
410
  key: Value,
78
411
  schema_key: Option<Value>,
79
412
  visit: impl FnOnce(&mut Self) -> Result<T, Verdict>,
80
413
  ) -> Result<T, Verdict> {
81
- let name = pointer_name(key)?;
82
- let segment = if let Some(key) = schema_key {
83
- format!("{segment}/{}", pointer_name(key)?)
414
+ if self.silent {
415
+ return visit(self);
416
+ }
417
+
418
+ let reset = self.collection_reset;
419
+ let instance_len = self.instance_path.checkpoint();
420
+ self.instance_path.name(key)?;
421
+ let result = if let Some(key) = schema_key {
422
+ self.schema_name(caches, segment, key, visit)
84
423
  } else {
85
- segment.to_owned()
424
+ self.scoped(caches, segment, None, visit)
86
425
  };
87
- let instance_len = self.instance_path.len();
88
- self.instance_path.push('/');
89
- self.instance_path.push_str(&name);
90
- let result = self.scoped(&segment, None, visit);
426
+ let result = self.cleanup_reset_paths(caches, reset, result, 1);
91
427
  self.instance_path.truncate(instance_len);
92
428
  result
93
429
  }
94
- }
95
430
 
96
- fn pointer_name(value: Value) -> Result<String, Verdict> {
97
- if unsafe { sn_kind(value) } != TYPE_STRING {
98
- return Err(Unsupported);
99
- }
100
- let mut data = std::ptr::null();
101
- let mut len = 0;
102
- let encoding = unsafe { sn_string_view(value, &mut data, &mut len) };
103
- let bytes = unsafe { std::slice::from_raw_parts(data, len) };
104
- if encoding == ENCODING_OTHER || (encoding == ENCODING_ASCII && !bytes.is_ascii()) {
105
- return Err(Unsupported);
431
+ fn schema_name<T>(
432
+ &mut self,
433
+ caches: &runtime::Caches,
434
+ segment: &str,
435
+ key: Value,
436
+ visit: impl FnOnce(&mut Self) -> Result<T, Verdict>,
437
+ ) -> Result<T, Verdict> {
438
+ if self.silent {
439
+ return visit(self);
440
+ }
441
+
442
+ self.scoped(caches, segment, None, |report| {
443
+ report.schema_path.name(key)?;
444
+ let reset = report.collection_reset;
445
+ let result = visit(report);
446
+ report.cleanup_reset_paths(caches, reset, result, 1)
447
+ })
106
448
  }
107
- let name = std::str::from_utf8(bytes).map_err(|_| Unsupported)?;
108
- Ok(name.replace('~', "~0").replace('/', "~1"))
109
449
  }
110
450
 
111
451
  impl Execution<'_> {
452
+ pub(super) fn should_continue_report(&self, depth: usize) -> bool {
453
+ depth >= MAX_EVALUATION_DEPTH && self.caches.report_continuations_allowed()
454
+ }
455
+ fn collect_report(
456
+ &self,
457
+ report: &mut Report,
458
+ root: usize,
459
+ instance: Value,
460
+ ) -> Result<Tracked, Verdict> {
461
+ let result = self.collect_report_inner(report, root, instance);
462
+ report.finish_runtime(self.caches, result)
463
+ }
464
+ fn collect_report_inner(
465
+ &self,
466
+ report: &mut Report,
467
+ root: usize,
468
+ instance: Value,
469
+ ) -> Result<Tracked, Verdict> {
470
+ if let Some(entry) = report.entry.take() {
471
+ report.add_with_frame(
472
+ self.caches,
473
+ entry.keyword,
474
+ entry.source,
475
+ entry.instance,
476
+ 0,
477
+ Some(entry.frame),
478
+ )?;
479
+ return Ok(Tracked::new(Invalid));
480
+ }
481
+ if report.annotations {
482
+ self.collect::<true>(report, root, instance, 0, scope::Context::default())
483
+ } else {
484
+ self.collect::<false>(report, root, instance, 0, scope::Context::default())
485
+ }
486
+ }
487
+ // Scalar trials export no annotations and need no evaluation memo entry.
488
+ // Keep this before memo admission, especially for contains over scalar arrays.
489
+ pub(super) fn trial_scalar(
490
+ &self,
491
+ node: usize,
492
+ value: Value,
493
+ depth: usize,
494
+ kind: i32,
495
+ ) -> Option<Verdict> {
496
+ if self.caches.scalar_callback() {
497
+ return None;
498
+ }
499
+ match self.nodes[node].as_slice() {
500
+ [Op::Number(required, rules)]
501
+ if kind != TYPE_UNSUPPORTED
502
+ && !self.caches.dynamic_calls()
503
+ && !rules.runtime_operands() =>
504
+ {
505
+ // A typed-integer failure still performs numeric conversion in
506
+ // a trial, so preserve nonfinite decimal exceptions as well.
507
+ let status = if kind & TYPE_NUMBER != 0 {
508
+ let status = rules.eval(self.caches, value, &self.budget);
509
+ if status != Unsupported && *required != 0 && kind & required == 0 {
510
+ Invalid
511
+ } else {
512
+ status
513
+ }
514
+ } else {
515
+ verdict(*required == 0)
516
+ };
517
+ Some(status)
518
+ }
519
+ [op @ (Op::Boolean(_) | Op::Type(..))] => Some(self.leaf(op, kind, value, depth)),
520
+ [op @ Op::String(_, rules)]
521
+ if kind != TYPE_UNSUPPORTED
522
+ && !self.caches.dynamic_calls()
523
+ && (kind != TYPE_STRING
524
+ || rules.length_only()
525
+ || rules.pattern_only().is_some()
526
+ || rules.format_only().is_some()
527
+ || rules.content_only().is_some()) =>
528
+ {
529
+ Some(self.leaf(op, kind, value, depth))
530
+ }
531
+ _ => None,
532
+ }
533
+ }
534
+ pub(super) fn collect_trial(
535
+ &self,
536
+ node: usize,
537
+ value: Value,
538
+ depth: usize,
539
+ active: scope::Context<'_>,
540
+ kind_state: &InstanceKind,
541
+ ) -> Tracked {
542
+ if self.should_continue_report(depth) {
543
+ let mut report = Report {
544
+ silent: true,
545
+ ..Report::default()
546
+ };
547
+ return self
548
+ .caches
549
+ .report_child(node, value, &mut report, true)
550
+ .unwrap_or_else(Tracked::new);
551
+ }
552
+ let kind = kind_state.get();
553
+ if depth > MAX_EVALUATION_DEPTH || self.unsupported_kind(node, kind) {
554
+ return Tracked::new(Unsupported);
555
+ }
556
+ if !self.caches.scalar_callback()
557
+ && let [op @ (Op::Const(_) | Op::Enum(_))] = self.nodes[node].as_slice()
558
+ {
559
+ return Tracked::new(self.leaf(op, kind, value, depth));
560
+ }
561
+ let mut report = Report {
562
+ silent: true,
563
+ ..Report::default()
564
+ };
565
+ self.collect_uncached::<true>(&mut report, node, value, depth, active, kind_state)
566
+ .unwrap_or_else(Tracked::new)
567
+ }
112
568
  fn collect<const ANNOTATIONS: bool>(
113
569
  &self,
114
570
  report: &mut Report,
@@ -117,17 +573,21 @@ impl Execution<'_> {
117
573
  depth: usize,
118
574
  active: scope::Context<'_>,
119
575
  ) -> Result<Tracked, Verdict> {
576
+ if self.should_continue_report(depth) {
577
+ return self.caches.report_child(node, value, report, ANNOTATIONS);
578
+ }
120
579
  if depth > MAX_EVALUATION_DEPTH {
121
580
  return Err(Unsupported);
122
581
  }
123
582
  if !self.budget.checkpoint() {
124
583
  return Err(Unsupported);
125
584
  }
126
- let kind = unsafe { sn_kind(value) };
127
- if kind == TYPE_UNSUPPORTED {
585
+ let kind_state = InstanceKind::new(TYPE_UNCHECKED);
586
+ let kind = self.diagnostic_kind(node, value, &kind_state);
587
+ if self.unsupported_kind(node, kind) {
128
588
  return Err(Unsupported);
129
589
  }
130
- self.collect_kind::<ANNOTATIONS>(report, node, value, depth, active, kind)
590
+ self.collect_kind::<ANNOTATIONS>(report, node, value, depth, active, &kind_state)
131
591
  }
132
592
  fn collect_kind<const ANNOTATIONS: bool>(
133
593
  &self,
@@ -136,8 +596,31 @@ impl Execution<'_> {
136
596
  value: Value,
137
597
  depth: usize,
138
598
  active: scope::Context<'_>,
139
- kind: i32,
599
+ kind_state: &InstanceKind,
140
600
  ) -> Result<Tracked, Verdict> {
601
+ let result =
602
+ self.collect_kind_inner::<ANNOTATIONS>(report, node, value, depth, active, kind_state);
603
+ report.finish_runtime(self.caches, result)
604
+ }
605
+ fn collect_kind_inner<const ANNOTATIONS: bool>(
606
+ &self,
607
+ report: &mut Report,
608
+ node: usize,
609
+ value: Value,
610
+ depth: usize,
611
+ active: scope::Context<'_>,
612
+ kind_state: &InstanceKind,
613
+ ) -> Result<Tracked, Verdict> {
614
+ if self.should_continue_report(depth) {
615
+ return self.caches.report_child(node, value, report, ANNOTATIONS);
616
+ }
617
+ let kind = self.diagnostic_kind(node, value, kind_state);
618
+ if self.unsupported_kind(node, kind) {
619
+ return Err(Unsupported);
620
+ }
621
+ // Silent children share this collection's failure state. Only actual
622
+ // trials create a fresh report in collect_trial; another isolated trial
623
+ // here would discard earlier failures when a Ruby caller rescues a raise.
141
624
  // A diagnostic success is established by complete traversal, never by
142
625
  // a short-circuit validity result. Keep exported annotations and the
143
626
  // collection policy separate; failures retain their path-specific errors.
@@ -155,7 +638,8 @@ impl Execution<'_> {
155
638
  {
156
639
  return Ok(result);
157
640
  }
158
- let result = self.collect_uncached::<ANNOTATIONS>(report, node, value, depth, active, kind);
641
+ let result =
642
+ self.collect_uncached::<ANNOTATIONS>(report, node, value, depth, active, kind_state);
159
643
  if let Some(key) = key
160
644
  && let Ok(coverage) = &result
161
645
  && coverage.status == Valid
@@ -172,11 +656,41 @@ impl Execution<'_> {
172
656
  value: Value,
173
657
  depth: usize,
174
658
  active: scope::Context<'_>,
175
- kind: i32,
659
+ kind_state: &InstanceKind,
660
+ ) -> Result<Tracked, Verdict> {
661
+ let result = self
662
+ .collect_uncached_inner::<ANNOTATIONS>(report, node, value, depth, active, kind_state);
663
+ report.finish_runtime(self.caches, result)
664
+ }
665
+ fn collect_uncached_inner<const ANNOTATIONS: bool>(
666
+ &self,
667
+ report: &mut Report,
668
+ node: usize,
669
+ value: Value,
670
+ depth: usize,
671
+ active: scope::Context<'_>,
672
+ kind_state: &InstanceKind,
176
673
  ) -> Result<Tracked, Verdict> {
674
+ let kind = kind_state.get();
675
+ // Callback results currently carry validity only. Preserve the existing
676
+ // diagnostic guard before any other assertion invokes Ruby methods.
677
+ if kind == TYPE_UNSUPPORTED
678
+ && (self.callback_container_node(node)
679
+ || self.nodes[node].iter().any(|op| match op {
680
+ Op::Array(_, rules) => !rules.header_only(),
681
+ Op::Object(_, rules) => !rules.header_only(),
682
+ _ => false,
683
+ }))
684
+ && !self.caches.report_continuations_allowed()
685
+ {
686
+ return Err(Unsupported);
687
+ }
177
688
  if depth > MAX_EVALUATION_DEPTH {
178
689
  return Err(Unsupported);
179
690
  }
691
+ if self.caches.callback_checkpoint(value) != Valid {
692
+ return Err(Unsupported);
693
+ }
180
694
  let frame = scope::ScopeFrame {
181
695
  resource: self.metadata[node].resource,
182
696
  parent: active.scope,
@@ -186,28 +700,111 @@ impl Execution<'_> {
186
700
  .or(self.metadata[node].primary_binding),
187
701
  };
188
702
  let active = self.enter_scope(node, active, &frame);
703
+ let _scope = self
704
+ .caches
705
+ .scope_guard(frame.resource, self.metadata[node].enters_scope);
189
706
  let tracked = ANNOTATIONS || self.tracks[node];
190
707
  let parent_failed = std::mem::replace(&mut report.failed, false);
191
708
  let mut prior = Tracked::new(Valid);
192
709
  for (op, &source) in self.nodes[node].iter().zip(&self.error_operands[node]) {
710
+ if self.caches.callback_checkpoint(value) != Valid {
711
+ return Err(Unsupported);
712
+ }
193
713
  if !self.budget.checkpoint() {
194
714
  return Err(Unsupported);
195
715
  }
716
+ // Container entry policy is shared; child traversal stays specific
717
+ // to each container's paths, assertions and annotation coverage.
718
+ let container = match op {
719
+ Op::Array(required, rules) => {
720
+ Some((*required, &rules.header, rules.header_only(), TYPE_ARRAY))
721
+ }
722
+ Op::Object(required, rules) => {
723
+ Some((*required, &rules.header, rules.header_only(), TYPE_OBJECT))
724
+ }
725
+ _ => None,
726
+ };
727
+ if let Some((required, header, header_only, expected_kind)) = container {
728
+ if self.caches.report_continuations_allowed()
729
+ && !header_only
730
+ && (kind == TYPE_UNSUPPORTED
731
+ || self.caches.dynamic_calls()
732
+ || prior.has_runtime_properties())
733
+ {
734
+ let result = self.caches.container_report(
735
+ node,
736
+ value,
737
+ (source.1, required, expected_kind == TYPE_OBJECT),
738
+ report,
739
+ &prior,
740
+ tracked,
741
+ )?;
742
+ if tracked {
743
+ prior.merge_diagnostics(self, result)?;
744
+ }
745
+ if report.silent && report.failed {
746
+ break;
747
+ }
748
+ continue;
749
+ }
750
+ if header_only && header.runtime_required(self.caches, kind) {
751
+ if report.collect_header(self.caches, header, required, value, source)? {
752
+ break;
753
+ }
754
+ continue;
755
+ }
756
+ if kind == expected_kind && !self.budget.checkpoint() {
757
+ return Err(Unsupported);
758
+ }
759
+ if required != 0 && kind != expected_kind {
760
+ report.add(self.caches, "type", source, value, 0)?;
761
+ }
762
+ if kind != expected_kind {
763
+ continue;
764
+ }
765
+ if report.silent {
766
+ let status = match op {
767
+ Op::Array(_, rules) => {
768
+ self.array_tracked(rules, value, depth + 1, active, &mut prior)
769
+ }
770
+ Op::Object(_, rules) => {
771
+ self.object_tracked(rules, value, depth + 1, active, &mut prior)
772
+ }
773
+ _ => unreachable!("container entry"),
774
+ };
775
+ if status == Unsupported {
776
+ return Err(Unsupported);
777
+ }
778
+ report.failed |= status == Invalid;
779
+ if report.failed {
780
+ break;
781
+ }
782
+ continue;
783
+ }
784
+ }
196
785
  match op {
197
786
  Op::Unsupported => return Err(Unsupported),
787
+ Op::UnresolvedRef(message) => {
788
+ let keyword = reference_keyword(source.0);
789
+ report.add(self.caches, keyword, (source.0, *message), value, 0)?;
790
+ prior = Tracked::new(Invalid);
791
+ }
198
792
  Op::Boolean(false) => {
199
- report.add("falseSchema", source, value, 0)?;
793
+ report.add(self.caches, "falseSchema", source, value, 0)?;
200
794
  prior = Tracked::new(Invalid);
201
795
  }
202
796
  Op::Boolean(true) => {}
203
- Op::Type(mask) => {
204
- if kind & mask == 0 {
205
- report.add("type", source, value, 0)?;
797
+ Op::Type(mask, union) => match self.type_verdict(*mask, *union, kind, value) {
798
+ Unsupported => return Err(Unsupported),
799
+ Valid => {}
800
+ Invalid => {
801
+ report.add(self.caches, "type", source, value, 0)?;
206
802
  }
207
- }
803
+ },
208
804
  Op::Const(..) | Op::Enum(..) => match self.leaf(op, kind, value, depth) {
209
805
  Unsupported => return Err(Unsupported),
210
806
  Invalid => report.add(
807
+ self.caches,
211
808
  if matches!(op, Op::Const(_)) {
212
809
  "const"
213
810
  } else {
@@ -220,30 +817,42 @@ impl Execution<'_> {
220
817
  Valid => {}
221
818
  },
222
819
  Op::Number(required, rules) => {
820
+ if kind == TYPE_UNSUPPORTED || self.caches.dynamic_calls() {
821
+ rules.runtime_failures(self.caches, *required, value, |keyword| {
822
+ report.add(self.caches, keyword, source, value, 0)
823
+ })?;
824
+ continue;
825
+ }
223
826
  if *required != 0 && kind & required == 0 {
224
- report.add("type", source, value, 0)?;
827
+ report.add(self.caches, "type", source, value, 0)?;
225
828
  }
226
829
  if kind & TYPE_NUMBER != 0 {
227
- let bits = rules.failures(value)?;
228
- for (bit, keyword) in [
229
- (MAXIMUM, "maximum"),
230
- (MINIMUM, "minimum"),
231
- (EXCLUSIVE_MAXIMUM, "exclusiveMaximum"),
232
- (EXCLUSIVE_MINIMUM, "exclusiveMinimum"),
233
- (MULTIPLE_OF, "multipleOf"),
234
- ] {
235
- if !self.budget.checkpoint() {
236
- return Err(Unsupported);
237
- }
238
- if bits & bit != 0 {
239
- report.add(keyword, source, value, 0)?;
240
- }
241
- }
830
+ rules.visit_failures(self.caches, value, &self.budget, |keyword| {
831
+ report.add(self.caches, keyword, source, value, 0)
832
+ })?;
242
833
  }
243
834
  }
244
835
  Op::String(required, rules) => {
836
+ if kind == TYPE_UNSUPPORTED || self.caches.dynamic_calls() {
837
+ rules.runtime_failures(
838
+ self.caches,
839
+ *required,
840
+ value,
841
+ |keyword, frame, rescued| {
842
+ report.add_string(
843
+ self.caches,
844
+ keyword,
845
+ source,
846
+ value,
847
+ frame,
848
+ rescued,
849
+ )
850
+ },
851
+ )?;
852
+ continue;
853
+ }
245
854
  if *required != 0 && kind != TYPE_STRING {
246
- report.add("type", source, value, 0)?;
855
+ report.add(self.caches, "type", source, value, 0)?;
247
856
  }
248
857
  if kind == TYPE_STRING {
249
858
  let len = if rules.min.is_some() || rules.max.is_some() {
@@ -252,29 +861,42 @@ impl Execution<'_> {
252
861
  0
253
862
  };
254
863
  if rules.max.is_some_and(|max| len > max) {
255
- report.add("maxLength", source, value, len)?;
864
+ report.add(self.caches, "maxLength", source, value, len)?;
256
865
  }
257
866
  if rules.min.is_some_and(|min| len < min) {
258
- report.add("minLength", source, value, len)?;
867
+ report.add(self.caches, "minLength", source, value, len)?;
259
868
  }
260
869
  if let Some(pattern) = &rules.pattern {
261
- match pattern.eval(self.caches, value) {
870
+ match pattern.assertion(self.caches, value) {
262
871
  Unsupported => return Err(Unsupported),
263
- Invalid => report.add("pattern", source, value, 0)?,
872
+ Invalid => {
873
+ let invalid = pattern.invalid(self.caches, value);
874
+ report.add(
875
+ self.caches,
876
+ "pattern",
877
+ source,
878
+ value,
879
+ usize::from(invalid),
880
+ )?;
881
+ if invalid {
882
+ continue;
883
+ }
884
+ }
264
885
  Valid => {}
265
886
  }
266
887
  }
267
888
  if let Some(format) = &rules.format {
268
889
  match format.eval(value) {
269
890
  Unsupported => return Err(Unsupported),
270
- Invalid => report.add("format", source, value, 0)?,
891
+ Invalid => report.add(self.caches, "format", source, value, 0)?,
271
892
  Valid => {}
272
893
  }
273
894
  }
274
895
  if let Some(content) = &rules.content {
275
- match content.eval(value) {
896
+ match content.eval(self.caches, value) {
276
897
  Unsupported => return Err(Unsupported),
277
898
  Invalid => report.add(
899
+ self.caches,
278
900
  if content.base64 {
279
901
  "contentEncoding"
280
902
  } else {
@@ -289,28 +911,19 @@ impl Execution<'_> {
289
911
  }
290
912
  }
291
913
  }
292
- Op::Array(required, rules) => {
293
- if kind == TYPE_ARRAY && !self.budget.checkpoint() {
294
- return Err(Unsupported);
295
- }
296
- if *required != 0 && kind != TYPE_ARRAY {
297
- report.add("type", source, value, 0)?;
298
- }
299
- if kind != TYPE_ARRAY {
300
- continue;
301
- }
914
+ Op::Array(_, rules) => {
302
915
  let mut coverage = Tracked::new(Valid);
303
916
  let len = unsafe { sn_len(value) };
304
917
  if rules.max.is_some_and(|max| len > max) {
305
- report.add("maxItems", source, value, len)?;
918
+ report.add(self.caches, "maxItems", source, value, len)?;
306
919
  }
307
920
  if rules.min.is_some_and(|min| len < min) {
308
- report.add("minItems", source, value, len)?;
921
+ report.add(self.caches, "minItems", source, value, len)?;
309
922
  }
310
923
  if rules.unique {
311
- match unique(value, depth, self.fixnum_flag, &self.budget) {
924
+ match self.unique_values(value, depth) {
312
925
  Unsupported => return Err(Unsupported),
313
- Invalid => report.add("uniqueItems", source, value, 0)?,
926
+ Invalid => report.add(self.caches, "uniqueItems", source, value, 0)?,
314
927
  Valid => {}
315
928
  }
316
929
  }
@@ -321,15 +934,20 @@ impl Execution<'_> {
321
934
  if tracked {
322
935
  coverage.item(index);
323
936
  }
324
- report.scoped(&format!("prefixItems/{index}"), Some(index), |r| {
325
- self.collect::<false>(
326
- r,
327
- child,
328
- unsafe { sn_entry(value, index) },
329
- depth + 1,
330
- active,
331
- )
332
- })?;
937
+ report.scoped(
938
+ self.caches,
939
+ &format!("prefixItems/{index}"),
940
+ Some(index),
941
+ |r| {
942
+ self.collect::<false>(
943
+ r,
944
+ child,
945
+ unsafe { sn_entry(value, index) },
946
+ depth + 1,
947
+ active,
948
+ )
949
+ },
950
+ )?;
333
951
  }
334
952
  if rules.list {
335
953
  for (index, &child) in rules.items.iter().take(len).enumerate() {
@@ -339,15 +957,20 @@ impl Execution<'_> {
339
957
  if tracked {
340
958
  coverage.item(index);
341
959
  }
342
- report.scoped(&format!("items/{index}"), Some(index), |r| {
343
- self.collect::<false>(
344
- r,
345
- child,
346
- unsafe { sn_entry(value, index) },
347
- depth + 1,
348
- active,
349
- )
350
- })?;
960
+ report.scoped(
961
+ self.caches,
962
+ &format!("items/{index}"),
963
+ Some(index),
964
+ |r| {
965
+ self.collect::<false>(
966
+ r,
967
+ child,
968
+ unsafe { sn_entry(value, index) },
969
+ depth + 1,
970
+ active,
971
+ )
972
+ },
973
+ )?;
351
974
  }
352
975
  if let Some(child) = rules.additional {
353
976
  for index in rules.items.len()..len {
@@ -357,20 +980,25 @@ impl Execution<'_> {
357
980
  if tracked {
358
981
  coverage.item(index);
359
982
  }
360
- report.scoped("additionalItems", Some(index), |r| {
361
- self.collect::<false>(
362
- r,
363
- child,
364
- unsafe { sn_entry(value, index) },
365
- depth + 1,
366
- active,
367
- )
368
- })?;
983
+ report.scoped(
984
+ self.caches,
985
+ "additionalItems",
986
+ Some(index),
987
+ |r| {
988
+ self.collect::<false>(
989
+ r,
990
+ child,
991
+ unsafe { sn_entry(value, index) },
992
+ depth + 1,
993
+ active,
994
+ )
995
+ },
996
+ )?;
369
997
  }
370
998
  }
371
999
  } else if let Some(&child) = rules.items.first() {
372
1000
  let primitive = match self.nodes[child].as_slice() {
373
- [Op::Type(mask)]
1001
+ [Op::Type(mask, _)]
374
1002
  if mask & TYPE_NUMBER != 0 || mask & TYPE_INTEGER != 0 =>
375
1003
  {
376
1004
  Some(&[][..])
@@ -378,6 +1006,7 @@ impl Execution<'_> {
378
1006
  [Op::Number(_, rules)] => rules.fast_limits.as_deref(),
379
1007
  _ => None,
380
1008
  };
1009
+ let primitive = primitive.filter(|_| !self.caches.dynamic_calls());
381
1010
  let values = if len == 0 {
382
1011
  &[]
383
1012
  } else {
@@ -389,9 +1018,12 @@ impl Execution<'_> {
389
1018
  }
390
1019
  _ => None,
391
1020
  };
392
- let pattern = pattern.filter(|_| !self.budget.timed());
1021
+ let pattern = pattern
1022
+ .filter(|_| !self.budget.timed() && !self.caches.dynamic_calls());
393
1023
  let number = match self.nodes[child].as_slice() {
394
- [Op::Number(required, number)] if depth < MAX_EVALUATION_DEPTH => {
1024
+ [Op::Number(required, number)]
1025
+ if depth < MAX_EVALUATION_DEPTH && !self.caches.dynamic_calls() =>
1026
+ {
395
1027
  Some((*required, number))
396
1028
  }
397
1029
  _ => None,
@@ -402,14 +1034,16 @@ impl Execution<'_> {
402
1034
  }
403
1035
  _ => None,
404
1036
  };
405
- let format = format.filter(|_| !self.budget.timed());
1037
+ let format =
1038
+ format.filter(|_| !self.budget.timed() && !self.caches.dynamic_calls());
406
1039
  let content = match self.nodes[child].as_slice() {
407
1040
  [Op::String(required, string)] if depth < MAX_EVALUATION_DEPTH => {
408
1041
  string.content_only().map(|content| (*required, content))
409
1042
  }
410
1043
  _ => None,
411
1044
  };
412
- let content = content.filter(|_| !self.budget.timed());
1045
+ let content = content
1046
+ .filter(|_| !self.budget.timed() && !self.caches.dynamic_calls());
413
1047
  if pattern.is_some() || format.is_some() || content.is_some() {
414
1048
  let start = rules.prefix.len().min(len);
415
1049
  if tracked {
@@ -423,6 +1057,7 @@ impl Execution<'_> {
423
1057
  // The invalid callback is shared, but each scanner retains
424
1058
  // its statically selected parser and guarded byte views.
425
1059
  let invalid = |index, item| match report.scoped(
1060
+ self.caches,
426
1061
  "items",
427
1062
  Some(start + index),
428
1063
  |r| self.collect::<false>(r, child, item, depth + 1, active),
@@ -440,9 +1075,12 @@ impl Execution<'_> {
440
1075
  (_, Some((required, format)), _) => {
441
1076
  format.visit_invalid_items(required, &values[start..], invalid)
442
1077
  }
443
- (_, _, Some((required, content))) => {
444
- content.visit_invalid_items(required, &values[start..], invalid)
445
- }
1078
+ (_, _, Some((required, content))) => content.visit_invalid_items(
1079
+ self.caches,
1080
+ required,
1081
+ &values[start..],
1082
+ invalid,
1083
+ ),
446
1084
  _ => unreachable!("a scalar item scanner was selected"),
447
1085
  };
448
1086
  if status != Valid {
@@ -472,18 +1110,25 @@ impl Execution<'_> {
472
1110
  _ => n > limit,
473
1111
  })
474
1112
  } else if let Some((required, number)) = number {
475
- match number.eval_guarded(required, item) {
1113
+ match number.eval_guarded(
1114
+ self.caches,
1115
+ required,
1116
+ item,
1117
+ &self.budget,
1118
+ ) {
476
1119
  Valid => false,
477
1120
  Invalid => true,
478
- Unsupported => return Err(Unsupported),
1121
+ Unsupported => {
1122
+ self.needs_diagnostics(child, item, depth + 1)?
1123
+ }
479
1124
  }
480
1125
  } else {
481
- self.needs_diagnostics(child, item, depth + 1, active)?
1126
+ self.needs_diagnostics(child, item, depth + 1)?
482
1127
  };
483
1128
  if !needs {
484
1129
  continue;
485
1130
  }
486
- report.scoped("items", Some(index), |r| {
1131
+ report.scoped(self.caches, "items", Some(index), |r| {
487
1132
  self.collect::<false>(r, child, item, depth + 1, active)
488
1133
  })?;
489
1134
  }
@@ -497,15 +1142,8 @@ impl Execution<'_> {
497
1142
  };
498
1143
  let matches = evaluation::scan_contains(
499
1144
  len,
500
- |index| match self.needs_diagnostics(
501
- child,
502
- values[index],
503
- depth + 1,
504
- active,
505
- ) {
506
- Ok(needs) => verdict(!needs),
507
- Err(status) => status,
508
- },
1145
+ evaluation::ContainsPolicy::AllMatches,
1146
+ |index| self.trial_status(child, values[index], depth + 1, active),
509
1147
  |index| {
510
1148
  if tracked {
511
1149
  coverage.item(index);
@@ -514,7 +1152,7 @@ impl Execution<'_> {
514
1152
  )?;
515
1153
  if bounds(matches, Some(rules.min_contains), rules.max_contains) == Invalid
516
1154
  {
517
- report.add("contains", source, value, matches)?;
1155
+ report.add(self.caches, "contains", source, value, matches)?;
518
1156
  }
519
1157
  }
520
1158
  if let Some(child) = rules
@@ -528,7 +1166,7 @@ impl Execution<'_> {
528
1166
  if prior.contains_item(index) || coverage.contains_item(index) {
529
1167
  continue;
530
1168
  }
531
- report.scoped("unevaluatedItems", Some(index), |r| {
1169
+ report.scoped(self.caches, "unevaluatedItems", Some(index), |r| {
532
1170
  self.collect::<false>(
533
1171
  r,
534
1172
  child,
@@ -541,33 +1179,24 @@ impl Execution<'_> {
541
1179
  }
542
1180
  }
543
1181
  if tracked {
544
- prior.merge_diagnostics(coverage)?;
1182
+ prior.merge_diagnostics(self, coverage)?;
545
1183
  }
546
1184
  }
547
- Op::Object(required, rules) => {
548
- if kind == TYPE_OBJECT && !self.budget.checkpoint() {
549
- return Err(Unsupported);
550
- }
551
- if *required != 0 && kind != TYPE_OBJECT {
552
- report.add("type", source, value, 0)?;
553
- }
554
- if kind != TYPE_OBJECT {
555
- continue;
556
- }
1185
+ Op::Object(_, rules) => {
557
1186
  let mut coverage = Tracked::new(Valid);
558
1187
  let len = unsafe { sn_len(value) };
559
1188
  if rules.max.is_some_and(|max| len > max) {
560
- report.add("maxProperties", source, value, len)?;
1189
+ report.add(self.caches, "maxProperties", source, value, len)?;
561
1190
  }
562
1191
  if rules.min.is_some_and(|min| len < min) {
563
- report.add("minProperties", source, value, len)?;
1192
+ report.add(self.caches, "minProperties", source, value, len)?;
564
1193
  }
565
1194
  for &name in &rules.required_order {
566
1195
  if !self.budget.checkpoint() {
567
1196
  return Err(Unsupported);
568
1197
  }
569
- if missing(value, name, self.undefined) {
570
- report.add("required", source, name, 0)?;
1198
+ if name.missing(self.caches, value, self.undefined)? {
1199
+ report.add(self.caches, "required", source, name.name, 0)?;
571
1200
  }
572
1201
  }
573
1202
  let mut context = ObjectDiagnostics {
@@ -621,8 +1250,14 @@ impl Execution<'_> {
621
1250
  if !self.budget.checkpoint() {
622
1251
  return Err(Unsupported);
623
1252
  }
624
- if missing(value, required, self.undefined) {
625
- report.add(keyword, (source.0, *name), required, 0)?;
1253
+ if required.missing(self.caches, value, self.undefined)? {
1254
+ report.add(
1255
+ self.caches,
1256
+ keyword,
1257
+ (source.0, *name),
1258
+ required.name,
1259
+ 0,
1260
+ )?;
626
1261
  }
627
1262
  }
628
1263
  }
@@ -631,10 +1266,8 @@ impl Execution<'_> {
631
1266
  continue;
632
1267
  }
633
1268
  if tracked {
634
- let result = report.scoped(
635
- &format!("{keyword}/{}", pointer_name(*name)?),
636
- None,
637
- |r| {
1269
+ let result =
1270
+ report.schema_name(self.caches, keyword, *name, |r| {
638
1271
  self.collect::<true>(
639
1272
  r,
640
1273
  *child,
@@ -642,30 +1275,16 @@ impl Execution<'_> {
642
1275
  depth + 1,
643
1276
  active,
644
1277
  )
645
- },
646
- )?;
647
- if result.status == Valid {
648
- coverage.merge(result);
1278
+ })?;
1279
+ if result.status == Valid
1280
+ && coverage.merge_runtime(self, result) == Unsupported
1281
+ {
1282
+ return Err(Unsupported);
649
1283
  }
650
- } else if self.needs_diagnostics(
651
- *child,
652
- value,
653
- depth + 1,
654
- active,
655
- )? {
656
- report.scoped(
657
- &format!("{keyword}/{}", pointer_name(*name)?),
658
- None,
659
- |r| {
660
- self.collect::<false>(
661
- r,
662
- *child,
663
- value,
664
- depth + 1,
665
- active,
666
- )
667
- },
668
- )?;
1284
+ } else if self.needs_diagnostics(*child, value, depth + 1)? {
1285
+ report.schema_name(self.caches, keyword, *name, |r| {
1286
+ self.collect::<false>(r, *child, value, depth + 1, active)
1287
+ })?;
669
1288
  }
670
1289
  }
671
1290
  }
@@ -698,7 +1317,7 @@ impl Execution<'_> {
698
1317
  }
699
1318
  }
700
1319
  if tracked {
701
- prior.merge_diagnostics(coverage)?;
1320
+ prior.merge_diagnostics(self, coverage)?;
702
1321
  }
703
1322
  }
704
1323
  Op::All(nodes) => {
@@ -707,19 +1326,34 @@ impl Execution<'_> {
707
1326
  if !self.budget.checkpoint() {
708
1327
  return Err(Unsupported);
709
1328
  }
710
- let result = report.scoped(&format!("allOf/{index}"), None, |r| {
711
- if tracked {
712
- self.collect_kind::<true>(r, child, value, depth + 1, active, kind)
713
- } else {
714
- self.collect_kind::<false>(r, child, value, depth + 1, active, kind)
715
- }
716
- })?;
1329
+ let result =
1330
+ report.scoped(self.caches, &format!("allOf/{index}"), None, |r| {
1331
+ if tracked {
1332
+ self.collect_kind::<true>(
1333
+ r,
1334
+ child,
1335
+ value,
1336
+ depth + 1,
1337
+ active,
1338
+ kind_state,
1339
+ )
1340
+ } else {
1341
+ self.collect_kind::<false>(
1342
+ r,
1343
+ child,
1344
+ value,
1345
+ depth + 1,
1346
+ active,
1347
+ kind_state,
1348
+ )
1349
+ }
1350
+ })?;
717
1351
  if tracked {
718
- coverage.merge_diagnostics(result)?;
1352
+ coverage.merge_diagnostics(self, result)?;
719
1353
  }
720
1354
  }
721
1355
  if tracked {
722
- prior.merge_diagnostics(coverage)?;
1356
+ prior.merge_diagnostics(self, coverage)?;
723
1357
  }
724
1358
  }
725
1359
  Op::Any(nodes) | Op::One(nodes) => {
@@ -728,67 +1362,68 @@ impl Execution<'_> {
728
1362
  } else {
729
1363
  evaluation::BranchKind::One
730
1364
  };
731
- // Complete scanning preserves exact oneOf counts and VM
732
- // exceptions from branches after an anyOf success.
733
- let result = if tracked {
734
- evaluation::scan_branches(
735
- nodes,
736
- evaluation::BranchPolicy::Complete,
737
- |child| self.eval_tracked_kind(child, value, depth + 1, active, kind),
738
- )?
739
- } else {
740
- let result = evaluation::scan_branches(
741
- nodes,
742
- evaluation::BranchPolicy::Complete,
743
- |child| {
744
- self.eval_with_kind(child, value, depth + 1, active, Some(kind))
745
- },
746
- )?;
747
- evaluation::BranchMatches {
748
- matches: result.matches,
749
- coverage: Tracked::new(Valid),
750
- }
751
- };
1365
+ // Trials execute complete scalar checks without retaining
1366
+ // errors or paths; ordinary validity may stop earlier.
1367
+ let result = evaluation::scan_branches(
1368
+ nodes,
1369
+ evaluation::BranchPolicy::Complete,
1370
+ self.caches.dynamic_calls(),
1371
+ |child| self.eval_tracked_kind(child, value, depth + 1, active, kind_state),
1372
+ |coverage, branch| {
1373
+ if coverage.merge_runtime(self, branch) == Unsupported {
1374
+ Err(Unsupported)
1375
+ } else {
1376
+ Ok(())
1377
+ }
1378
+ },
1379
+ )?;
752
1380
  if !branch_kind.accepts(result.matches) {
753
- report.add(branch_kind.keyword(), source, value, result.matches)?;
1381
+ report.add(
1382
+ self.caches,
1383
+ branch_kind.keyword(),
1384
+ source,
1385
+ value,
1386
+ result.matches,
1387
+ )?;
754
1388
  } else if tracked {
755
- prior.merge_diagnostics(result.coverage)?;
1389
+ prior.merge_diagnostics(self, result.coverage)?;
756
1390
  }
757
1391
  }
758
1392
  Op::Not(child) => {
759
- match self.eval_with_kind(*child, value, depth + 1, active, Some(kind)) {
1393
+ match self
1394
+ .eval_tracked_kind(*child, value, depth + 1, active, kind_state)
1395
+ .status
1396
+ {
760
1397
  Unsupported => return Err(Unsupported),
761
- Valid => report.add("not", source, value, 0)?,
1398
+ Valid => report.add(self.caches, "not", source, value, 0)?,
762
1399
  Invalid => {}
763
1400
  }
764
1401
  }
765
1402
  Op::Conditional(condition, yes, no) => {
766
- let condition = if tracked {
767
- self.eval_tracked_kind(*condition, value, depth + 1, active, kind)
768
- } else {
769
- Tracked::new(self.eval_with_kind(
770
- *condition,
771
- value,
772
- depth + 1,
773
- active,
774
- Some(kind),
775
- ))
776
- };
1403
+ let condition =
1404
+ self.eval_tracked_kind(*condition, value, depth + 1, active, kind_state);
777
1405
  let mut coverage = Tracked::new(Valid);
778
1406
  let (child, segment) = match condition.status {
779
1407
  Unsupported => return Err(Unsupported),
780
1408
  Valid => {
781
- if tracked {
782
- coverage.merge(condition);
1409
+ if tracked && coverage.merge_runtime(self, condition) == Unsupported {
1410
+ return Err(Unsupported);
783
1411
  }
784
1412
  (yes, "then")
785
1413
  }
786
1414
  Invalid => (no, "else"),
787
1415
  };
788
1416
  if let Some(child) = child {
789
- let result = report.scoped(segment, None, |r| {
1417
+ let result = report.scoped(self.caches, segment, None, |r| {
790
1418
  if tracked {
791
- self.collect_kind::<true>(r, *child, value, depth + 1, active, kind)
1419
+ self.collect_kind::<true>(
1420
+ r,
1421
+ *child,
1422
+ value,
1423
+ depth + 1,
1424
+ active,
1425
+ kind_state,
1426
+ )
792
1427
  } else {
793
1428
  self.collect_kind::<false>(
794
1429
  r,
@@ -796,20 +1431,24 @@ impl Execution<'_> {
796
1431
  value,
797
1432
  depth + 1,
798
1433
  active,
799
- kind,
1434
+ kind_state,
800
1435
  )
801
1436
  }
802
1437
  })?;
803
1438
  if tracked {
804
- coverage.merge_diagnostics(result)?;
1439
+ coverage.merge_diagnostics(self, result)?;
805
1440
  }
806
1441
  }
807
1442
  if tracked {
808
- prior.merge_diagnostics(coverage)?;
1443
+ prior.merge_diagnostics(self, coverage)?;
809
1444
  }
810
1445
  }
811
1446
  Op::Ref(target) => {
812
- if RefFrame::contains(active.active, node, value) {
1447
+ let target = self.resolve_reference(*target, value, active)?;
1448
+ let program = self.programs[node];
1449
+ if RefFrame::contains(active.active, node, value)
1450
+ || self.caches.reference_active(program, value)
1451
+ {
813
1452
  continue;
814
1453
  }
815
1454
  let frame = RefFrame {
@@ -817,29 +1456,51 @@ impl Execution<'_> {
817
1456
  value,
818
1457
  parent: active.active,
819
1458
  };
820
- let keyword = match source.0 {
821
- OP_RECURSIVE_REF => "$recursiveRef",
822
- OP_DYNAMIC_REF => "$dynamicRef",
823
- _ => "$ref",
824
- };
825
- let target = self.reference_target(*target, active);
1459
+ let keyword = reference_keyword(source.0);
1460
+ let reference = self.caches.reference_frame(program, value);
1461
+ let _guard = self.caches.reference_guard(&reference);
826
1462
  let active = scope::Context {
827
1463
  active: Some(&frame),
828
1464
  ..active
829
1465
  };
830
- let result = report.scoped(keyword, None, |r| {
1466
+ let result = report.scoped(self.caches, keyword, None, |r| {
831
1467
  if tracked {
832
- self.collect_kind::<true>(r, target, value, depth + 1, active, kind)
1468
+ self.collect_kind::<true>(
1469
+ r,
1470
+ target,
1471
+ value,
1472
+ depth + 1,
1473
+ active,
1474
+ kind_state,
1475
+ )
833
1476
  } else {
834
- self.collect_kind::<false>(r, target, value, depth + 1, active, kind)
1477
+ self.collect_kind::<false>(
1478
+ r,
1479
+ target,
1480
+ value,
1481
+ depth + 1,
1482
+ active,
1483
+ kind_state,
1484
+ )
835
1485
  }
836
1486
  })?;
837
1487
  if tracked {
838
- prior.merge_diagnostics(result)?;
1488
+ prior.merge_diagnostics(self, result)?;
839
1489
  }
840
1490
  }
841
1491
  }
1492
+ self.caches.apply_report_effects(report);
1493
+ if report.silent
1494
+ && report.failed
1495
+ && matches!(
1496
+ op,
1497
+ Op::All(_) | Op::Any(_) | Op::One(_) | Op::Not(_) | Op::Conditional(..)
1498
+ )
1499
+ {
1500
+ break;
1501
+ }
842
1502
  }
1503
+ self.caches.apply_report_effects(report);
843
1504
  let failed = report.failed;
844
1505
  report.failed |= parent_failed;
845
1506
  // Diagnostic coverage keeps local attempted locations for subsequent
@@ -849,33 +1510,61 @@ impl Execution<'_> {
849
1510
 
850
1511
  fn needs_diagnostics(
851
1512
  &self,
852
- node: usize,
1513
+ mut node: usize,
853
1514
  value: Value,
854
- depth: usize,
855
- active: scope::Context<'_>,
1515
+ mut depth: usize,
856
1516
  ) -> Result<bool, Verdict> {
857
- if !self.budget.checkpoint() {
858
- return Err(Unsupported);
1517
+ loop {
1518
+ if !self.budget.checkpoint() {
1519
+ return Err(Unsupported);
1520
+ }
1521
+ if depth > MAX_EVALUATION_DEPTH {
1522
+ return Ok(true);
1523
+ }
1524
+ let [Op::Ref(reference)] = self.nodes[node].as_slice() else {
1525
+ break;
1526
+ };
1527
+ let Some(target) = reference.static_target() else {
1528
+ break;
1529
+ };
1530
+ node = target;
1531
+ depth += 1;
859
1532
  }
860
1533
  let status = match self.nodes[node].as_slice() {
861
1534
  [Op::Number(required, rules)] if depth <= MAX_EVALUATION_DEPTH => {
862
- rules.eval_guarded(*required, value)
1535
+ if self.caches.dynamic_calls()
1536
+ || self.diagnostic_kind(node, value, &InstanceKind::new(TYPE_UNCHECKED))
1537
+ == TYPE_UNSUPPORTED
1538
+ {
1539
+ return Ok(true);
1540
+ }
1541
+ rules.eval_guarded(self.caches, *required, value, &self.budget)
863
1542
  }
864
- [op @ (Op::Type(_) | Op::Boolean(_) | Op::String(..) | Op::Const(_) | Op::Enum(_))]
865
- if depth <= MAX_EVALUATION_DEPTH =>
866
- {
867
- let kind = if value & self.fixnum_flag != 0 {
868
- TYPE_INTEGRAL_NUMBER
869
- } else {
870
- unsafe { sn_kind(value) }
871
- };
872
- if kind == TYPE_UNSUPPORTED {
1543
+ [
1544
+ op @ (Op::Type(..) | Op::Boolean(_) | Op::String(..) | Op::Const(_) | Op::Enum(_)),
1545
+ ] if depth <= MAX_EVALUATION_DEPTH => {
1546
+ let kind = self.diagnostic_kind(node, value, &InstanceKind::new(TYPE_UNCHECKED));
1547
+ if (kind == TYPE_UNSUPPORTED
1548
+ && matches!(
1549
+ op,
1550
+ Op::Type(..) | Op::String(..) | Op::Const(_) | Op::Enum(_)
1551
+ ))
1552
+ || (self.caches.dynamic_calls()
1553
+ && matches!(op, Op::String(..) | Op::Const(_) | Op::Enum(_)))
1554
+ {
1555
+ // Collection must execute a dynamic predicate once, rather
1556
+ // than once here and again while recording its failure.
1557
+ return Ok(true);
1558
+ }
1559
+ if self.unsupported_kind(node, kind) {
873
1560
  Unsupported
874
1561
  } else {
875
1562
  self.leaf(op, kind, value, depth)
876
1563
  }
877
1564
  }
878
- _ => self.eval(node, value, depth, active),
1565
+ // A successful validity/trial scan can omit diagnostic-only work
1566
+ // in descendants. Only complete collection proves their success.
1567
+ _ => return Ok(true),
879
1568
  };
880
1569
  match status {
881
1570
  Valid => Ok(false),
@@ -904,8 +1593,8 @@ unsafe extern "C" fn object_diagnostic(context: *mut c_void, key: Value, value:
904
1593
  return Unsupported as i32;
905
1594
  }
906
1595
  let visit = |report: &mut Report, child: usize, item: Value, segment: &str, schema_key| {
907
- if c.engine.needs_diagnostics(child, item, c.depth, c.active)? {
908
- report.property(segment, key, schema_key, |r| {
1596
+ if c.engine.needs_diagnostics(child, item, c.depth)? {
1597
+ report.property(c.engine.caches, segment, key, schema_key, |r| {
909
1598
  c.engine.collect::<false>(r, child, item, c.depth, c.active)
910
1599
  })?;
911
1600
  }
@@ -934,9 +1623,10 @@ unsafe extern "C" fn object_diagnostic(context: *mut c_void, key: Value, value:
934
1623
  c.coverage.property(c.engine, key);
935
1624
  return Ok(());
936
1625
  }
937
- let evaluated = c.rules.visit_value_rules::<true>(
1626
+ let evaluated = c.rules.visit_value_rules(
938
1627
  c.engine.caches,
939
1628
  &c.engine.budget,
1629
+ true,
940
1630
  key,
941
1631
  None,
942
1632
  |segment, name, child| visit(c.report, child, value, segment, name),
@@ -962,10 +1652,20 @@ pub struct Record {
962
1652
  instance_len: usize,
963
1653
  schema_path: *const u8,
964
1654
  schema_len: usize,
1655
+ instance_pointer: Value,
1656
+ schema_pointer: Value,
965
1657
  opcode: i32,
966
1658
  operand: Value,
967
1659
  instance: Value,
968
1660
  detail: usize,
1661
+ message: Value,
1662
+ }
1663
+ fn report_outcome(result: std::thread::Result<Result<*mut Report, i32>>) -> (*mut Report, i32) {
1664
+ match result {
1665
+ Ok(Ok(report)) => (report, VERDICT_VALID),
1666
+ Ok(Err(code)) => (std::ptr::null_mut(), code),
1667
+ Err(_) => (std::ptr::null_mut(), STATUS_PANIC),
1668
+ }
969
1669
  }
970
1670
  /// # Safety
971
1671
  /// Engine/status must be live under the GVL; root must be in bounds and
@@ -973,14 +1673,14 @@ pub struct Record {
973
1673
  #[unsafe(no_mangle)]
974
1674
  pub unsafe extern "C" fn sn_diagnose(
975
1675
  engine: *const Engine,
976
- caches: *const pattern::Caches,
1676
+ caches: *const runtime::Caches,
977
1677
  root: usize,
978
1678
  instance: Value,
979
1679
  status: *mut i32,
980
1680
  ) -> *mut Report {
981
1681
  let result = catch_unwind(AssertUnwindSafe(|| {
982
1682
  let engine = unsafe { &*engine };
983
- let local = pattern::Caches::default();
1683
+ let local = runtime::Caches::default();
984
1684
  let caches = unsafe { caches.as_ref() }.unwrap_or(&local);
985
1685
  let execution = Execution {
986
1686
  engine,
@@ -988,33 +1688,29 @@ pub unsafe extern "C" fn sn_diagnose(
988
1688
  memo: memo::Memo::default(),
989
1689
  budget: budget::Budget::default(),
990
1690
  };
991
- let mut report = Report::default();
992
- execution.collect::<false>(&mut report, root, instance, 0, scope::Context::default())?;
993
- Ok::<_, Verdict>(Box::into_raw(Box::new(report)))
1691
+ caches.admit(engine, root);
1692
+ let inherited = caches.initial_report();
1693
+ let resumed = inherited.is_some();
1694
+ let mut report = inherited.unwrap_or_else(|| Report {
1695
+ render_messages: engine.runtime_messages,
1696
+ ..Report::default()
1697
+ });
1698
+ let result = execution
1699
+ .collect_report(&mut report, root, instance)
1700
+ .map_err(|verdict| execution.budget.status(verdict))?;
1701
+ report.evaluation = if resumed { Some(result) } else { None };
1702
+ Ok::<_, i32>(Box::into_raw(Box::new(report)))
994
1703
  }));
995
- match result {
996
- Ok(Ok(report)) => {
997
- unsafe { *status = VERDICT_VALID };
998
- report
999
- }
1000
- other => {
1001
- unsafe {
1002
- *status = if other.is_err() {
1003
- STATUS_PANIC
1004
- } else {
1005
- VERDICT_UNSUPPORTED
1006
- }
1007
- };
1008
- std::ptr::null_mut()
1009
- }
1010
- }
1704
+ let (report, code) = report_outcome(result);
1705
+ unsafe { *status = code };
1706
+ report
1011
1707
  }
1012
1708
  /// # Safety
1013
1709
  /// Same requirements as sn_diagnose; all limit/output pointers must be live.
1014
1710
  #[unsafe(no_mangle)]
1015
1711
  pub unsafe extern "C" fn sn_diagnose_with_limits(
1016
1712
  engine: *const Engine,
1017
- caches: *const pattern::Caches,
1713
+ caches: *const runtime::Caches,
1018
1714
  root: usize,
1019
1715
  instance: Value,
1020
1716
  status: *mut i32,
@@ -1022,21 +1718,25 @@ pub unsafe extern "C" fn sn_diagnose_with_limits(
1022
1718
  outcome: *mut budget::Outcome,
1023
1719
  ) -> *mut Report {
1024
1720
  let result = catch_unwind(AssertUnwindSafe(|| {
1025
- let local = pattern::Caches::default();
1721
+ let local = runtime::Caches::default();
1026
1722
  let caches = unsafe { caches.as_ref() }.unwrap_or(&local);
1027
1723
  let limits = unsafe { *limits };
1028
1724
  let execution = Execution {
1029
1725
  engine: unsafe { &*engine },
1030
1726
  caches,
1031
1727
  memo: memo::Memo::default(),
1032
- budget: budget::Budget::new(limits),
1728
+ budget: caches.budget(limits),
1033
1729
  };
1034
- let mut report = Report {
1730
+ caches.admit(execution.engine, root);
1731
+ let inherited = caches.initial_report();
1732
+ let resumed = inherited.is_some();
1733
+ let mut report = inherited.unwrap_or_else(|| Report {
1035
1734
  limits,
1735
+ render_messages: execution.runtime_messages,
1036
1736
  ..Report::default()
1037
- };
1737
+ });
1038
1738
  let result = if execution.budget.check_deadline() {
1039
- execution.collect::<false>(&mut report, root, instance, 0, scope::Context::default())
1739
+ execution.collect_report(&mut report, root, instance)
1040
1740
  } else {
1041
1741
  Err(Unsupported)
1042
1742
  };
@@ -1053,24 +1753,16 @@ pub unsafe extern "C" fn sn_diagnose_with_limits(
1053
1753
  unsafe {
1054
1754
  ((*outcome).kind, (*outcome).attempted) = hit;
1055
1755
  }
1056
- result.map(|_| Box::into_raw(Box::new(report)))
1756
+ result
1757
+ .map(|evaluation| {
1758
+ report.evaluation = if resumed { Some(evaluation) } else { None };
1759
+ Box::into_raw(Box::new(report))
1760
+ })
1761
+ .map_err(|verdict| execution.budget.status(verdict))
1057
1762
  }));
1058
- match result {
1059
- Ok(Ok(report)) => {
1060
- unsafe { *status = VERDICT_VALID };
1061
- report
1062
- }
1063
- other => {
1064
- unsafe {
1065
- *status = if other.is_err() {
1066
- STATUS_PANIC
1067
- } else {
1068
- VERDICT_UNSUPPORTED
1069
- }
1070
- };
1071
- std::ptr::null_mut()
1072
- }
1073
- }
1763
+ let (report, code) = report_outcome(result);
1764
+ unsafe { *status = code };
1765
+ report
1074
1766
  }
1075
1767
  /// # Safety
1076
1768
  /// Report must be live under the GVL.
@@ -1082,15 +1774,7 @@ pub unsafe extern "C" fn sn_report_len(report: *const Report) -> usize {
1082
1774
  /// Report must be live under the GVL.
1083
1775
  #[unsafe(no_mangle)]
1084
1776
  pub unsafe extern "C" fn sn_report_size(report: *const Report) -> usize {
1085
- let r = unsafe { &*report };
1086
- std::mem::size_of::<Report>()
1087
- + r.errors.capacity() * std::mem::size_of::<Error>()
1088
- + r.instance_path.capacity()
1089
- + r.schema_path.capacity()
1090
- + r.errors
1091
- .iter()
1092
- .map(|e| e.instance_path.capacity() + e.schema_path.capacity())
1093
- .sum::<usize>()
1777
+ std::mem::size_of::<Report>() + unsafe { &*report }.memory_usage()
1094
1778
  }
1095
1779
  /// # Safety
1096
1780
  /// Report must be live, index in bounds, and record a writable pointer.
@@ -1105,14 +1789,17 @@ pub unsafe extern "C" fn sn_report_record(
1105
1789
  *record = Record {
1106
1790
  keyword: e.keyword.as_ptr(),
1107
1791
  keyword_len: e.keyword.len(),
1108
- instance_path: e.instance_path.as_ptr(),
1109
- instance_len: e.instance_path.len(),
1110
- schema_path: e.schema_path.as_ptr(),
1111
- schema_len: e.schema_path.len(),
1792
+ instance_path: e.instance_path.bytes().as_ptr(),
1793
+ instance_len: e.instance_path.bytes().len(),
1794
+ schema_path: e.schema_path.bytes().as_ptr(),
1795
+ schema_len: e.schema_path.bytes().len(),
1796
+ instance_pointer: e.instance_path.value(),
1797
+ schema_pointer: e.schema_path.value(),
1112
1798
  opcode: e.opcode,
1113
1799
  operand: e.operand,
1114
1800
  instance: e.instance,
1115
1801
  detail: e.detail,
1802
+ message: e.message,
1116
1803
  }
1117
1804
  };
1118
1805
  }
@@ -1120,12 +1807,7 @@ pub unsafe extern "C" fn sn_report_record(
1120
1807
  /// Report must be live; call only from its synchronous Ruby GC mark callback.
1121
1808
  #[unsafe(no_mangle)]
1122
1809
  pub unsafe extern "C" fn sn_report_mark(report: *const Report) {
1123
- for error in &unsafe { &*report }.errors {
1124
- unsafe {
1125
- sn_pin(error.operand);
1126
- sn_pin(error.instance);
1127
- }
1128
- }
1810
+ unsafe { &*report }.mark();
1129
1811
  }
1130
1812
  /// # Safety
1131
1813
  /// Report must be owned, returned by sn_diagnose, and freed exactly once.
@@ -1133,3 +1815,60 @@ pub unsafe extern "C" fn sn_report_mark(report: *const Report) {
1133
1815
  pub unsafe extern "C" fn sn_report_free(report: *mut Report) {
1134
1816
  drop(unsafe { Box::from_raw(report) });
1135
1817
  }
1818
+
1819
+ #[cfg(test)]
1820
+ mod tests {
1821
+ use super::*;
1822
+
1823
+ #[test]
1824
+ fn report_seeds_and_results_own_error_paths_limits_and_locations() {
1825
+ let mut report = Report {
1826
+ attempts: 2,
1827
+ failed: true,
1828
+ ..Report::default()
1829
+ };
1830
+ report.instance_path.text.push_str("/item");
1831
+ report.schema_path.text.push_str("/allOf/1");
1832
+ report.limits = budget::Limits {
1833
+ enabled: 1,
1834
+ max_errors: 3,
1835
+ ..budget::Limits::default()
1836
+ };
1837
+ report.errors.push(Error {
1838
+ keyword: "type",
1839
+ instance_path: pointer::Rendered::Native("/item".into()),
1840
+ schema_path: pointer::Rendered::Native("/allOf/0/type".into()),
1841
+ opcode: OP_TYPE_INTEGER,
1842
+ operand: 0,
1843
+ instance: 0,
1844
+ detail: 0,
1845
+ message: 0,
1846
+ });
1847
+ let mut locations = Tracked::new(Valid);
1848
+ locations.item(4);
1849
+ locations.item(65);
1850
+ report.evaluation = Some(locations);
1851
+ let mut seed = report.continuation(true);
1852
+ assert!(seed.annotations);
1853
+ assert!(seed.evaluation.is_none());
1854
+ assert_eq!(seed.attempts, 2);
1855
+ assert!(seed.failed);
1856
+ assert_eq!(seed.limits.max_errors, 3);
1857
+ seed.instance_path.text.push_str("/child");
1858
+ seed.errors.clear();
1859
+ assert_eq!(report.instance_path.text, "/item");
1860
+ assert_eq!(report.errors.len(), 1);
1861
+ assert!(report.memory_usage() > 0);
1862
+ let mut adopted = Report::default();
1863
+ let mut completed = report.clone();
1864
+ completed.annotations = true;
1865
+ let mut result = adopted.adopt(completed);
1866
+ result.item(8);
1867
+ assert_eq!(result.item_count(), 3);
1868
+ assert_eq!(report.evaluation.as_ref().unwrap().item_count(), 2);
1869
+ assert!(report.evaluation.as_ref().unwrap().contains_item(65));
1870
+ assert!(adopted.evaluation.is_none());
1871
+ assert!(!adopted.annotations);
1872
+ assert_eq!(adopted.attempts, 2);
1873
+ }
1874
+ }