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
@@ -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,42 @@ 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
  }
196
716
  match op {
197
717
  Op::Unsupported => return Err(Unsupported),
718
+ Op::UnresolvedRef(message) => {
719
+ let keyword = reference_keyword(source.0);
720
+ report.add(self.caches, keyword, (source.0, *message), value, 0)?;
721
+ prior = Tracked::new(Invalid);
722
+ }
198
723
  Op::Boolean(false) => {
199
- report.add("falseSchema", source, value, 0)?;
724
+ report.add(self.caches, "falseSchema", source, value, 0)?;
200
725
  prior = Tracked::new(Invalid);
201
726
  }
202
727
  Op::Boolean(true) => {}
203
- Op::Type(mask) => {
204
- if kind & mask == 0 {
205
- report.add("type", source, value, 0)?;
728
+ Op::Type(mask, union) => match self.type_verdict(*mask, *union, kind, value) {
729
+ Unsupported => return Err(Unsupported),
730
+ Valid => {}
731
+ Invalid => {
732
+ report.add(self.caches, "type", source, value, 0)?;
206
733
  }
207
- }
734
+ },
208
735
  Op::Const(..) | Op::Enum(..) => match self.leaf(op, kind, value, depth) {
209
736
  Unsupported => return Err(Unsupported),
210
737
  Invalid => report.add(
738
+ self.caches,
211
739
  if matches!(op, Op::Const(_)) {
212
740
  "const"
213
741
  } else {
@@ -220,30 +748,42 @@ impl Execution<'_> {
220
748
  Valid => {}
221
749
  },
222
750
  Op::Number(required, rules) => {
751
+ if kind == TYPE_UNSUPPORTED || self.caches.dynamic_calls() {
752
+ rules.runtime_failures(self.caches, *required, value, |keyword| {
753
+ report.add(self.caches, keyword, source, value, 0)
754
+ })?;
755
+ continue;
756
+ }
223
757
  if *required != 0 && kind & required == 0 {
224
- report.add("type", source, value, 0)?;
758
+ report.add(self.caches, "type", source, value, 0)?;
225
759
  }
226
760
  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
- }
761
+ rules.visit_failures(self.caches, value, &self.budget, |keyword| {
762
+ report.add(self.caches, keyword, source, value, 0)
763
+ })?;
242
764
  }
243
765
  }
244
766
  Op::String(required, rules) => {
767
+ if kind == TYPE_UNSUPPORTED || self.caches.dynamic_calls() {
768
+ rules.runtime_failures(
769
+ self.caches,
770
+ *required,
771
+ value,
772
+ |keyword, frame, rescued| {
773
+ report.add_string(
774
+ self.caches,
775
+ keyword,
776
+ source,
777
+ value,
778
+ frame,
779
+ rescued,
780
+ )
781
+ },
782
+ )?;
783
+ continue;
784
+ }
245
785
  if *required != 0 && kind != TYPE_STRING {
246
- report.add("type", source, value, 0)?;
786
+ report.add(self.caches, "type", source, value, 0)?;
247
787
  }
248
788
  if kind == TYPE_STRING {
249
789
  let len = if rules.min.is_some() || rules.max.is_some() {
@@ -252,29 +792,42 @@ impl Execution<'_> {
252
792
  0
253
793
  };
254
794
  if rules.max.is_some_and(|max| len > max) {
255
- report.add("maxLength", source, value, len)?;
795
+ report.add(self.caches, "maxLength", source, value, len)?;
256
796
  }
257
797
  if rules.min.is_some_and(|min| len < min) {
258
- report.add("minLength", source, value, len)?;
798
+ report.add(self.caches, "minLength", source, value, len)?;
259
799
  }
260
800
  if let Some(pattern) = &rules.pattern {
261
- match pattern.eval(self.caches, value) {
801
+ match pattern.assertion(self.caches, value) {
262
802
  Unsupported => return Err(Unsupported),
263
- Invalid => report.add("pattern", source, value, 0)?,
803
+ Invalid => {
804
+ let invalid = pattern.invalid(self.caches, value);
805
+ report.add(
806
+ self.caches,
807
+ "pattern",
808
+ source,
809
+ value,
810
+ usize::from(invalid),
811
+ )?;
812
+ if invalid {
813
+ continue;
814
+ }
815
+ }
264
816
  Valid => {}
265
817
  }
266
818
  }
267
819
  if let Some(format) = &rules.format {
268
820
  match format.eval(value) {
269
821
  Unsupported => return Err(Unsupported),
270
- Invalid => report.add("format", source, value, 0)?,
822
+ Invalid => report.add(self.caches, "format", source, value, 0)?,
271
823
  Valid => {}
272
824
  }
273
825
  }
274
826
  if let Some(content) = &rules.content {
275
- match content.eval(value) {
827
+ match content.eval(self.caches, value) {
276
828
  Unsupported => return Err(Unsupported),
277
829
  Invalid => report.add(
830
+ self.caches,
278
831
  if content.base64 {
279
832
  "contentEncoding"
280
833
  } else {
@@ -290,27 +843,73 @@ impl Execution<'_> {
290
843
  }
291
844
  }
292
845
  Op::Array(required, rules) => {
846
+ if self.caches.report_continuations_allowed()
847
+ && !rules.header_only()
848
+ && (kind == TYPE_UNSUPPORTED
849
+ || self.caches.dynamic_calls()
850
+ || prior.has_runtime_properties())
851
+ {
852
+ let result = self.caches.container_report(
853
+ node,
854
+ value,
855
+ (source.1, *required, false),
856
+ report,
857
+ &prior,
858
+ tracked,
859
+ )?;
860
+ if tracked {
861
+ prior.merge_diagnostics(self, result)?;
862
+ }
863
+ if report.silent && report.failed {
864
+ break;
865
+ }
866
+ continue;
867
+ }
868
+ if rules.header_only() && rules.header.runtime_required(self.caches, kind) {
869
+ if report.collect_header(
870
+ self.caches,
871
+ &rules.header,
872
+ *required,
873
+ value,
874
+ source,
875
+ )? {
876
+ break;
877
+ }
878
+ continue;
879
+ }
293
880
  if kind == TYPE_ARRAY && !self.budget.checkpoint() {
294
881
  return Err(Unsupported);
295
882
  }
296
883
  if *required != 0 && kind != TYPE_ARRAY {
297
- report.add("type", source, value, 0)?;
884
+ report.add(self.caches, "type", source, value, 0)?;
298
885
  }
299
886
  if kind != TYPE_ARRAY {
300
887
  continue;
301
888
  }
889
+ if report.silent {
890
+ let status =
891
+ self.array_tracked(rules, value, depth + 1, active, &mut prior);
892
+ if status == Unsupported {
893
+ return Err(Unsupported);
894
+ }
895
+ report.failed |= status == Invalid;
896
+ if report.failed {
897
+ break;
898
+ }
899
+ continue;
900
+ }
302
901
  let mut coverage = Tracked::new(Valid);
303
902
  let len = unsafe { sn_len(value) };
304
903
  if rules.max.is_some_and(|max| len > max) {
305
- report.add("maxItems", source, value, len)?;
904
+ report.add(self.caches, "maxItems", source, value, len)?;
306
905
  }
307
906
  if rules.min.is_some_and(|min| len < min) {
308
- report.add("minItems", source, value, len)?;
907
+ report.add(self.caches, "minItems", source, value, len)?;
309
908
  }
310
909
  if rules.unique {
311
- match unique(value, depth, self.fixnum_flag, &self.budget) {
910
+ match self.unique_values(value, depth) {
312
911
  Unsupported => return Err(Unsupported),
313
- Invalid => report.add("uniqueItems", source, value, 0)?,
912
+ Invalid => report.add(self.caches, "uniqueItems", source, value, 0)?,
314
913
  Valid => {}
315
914
  }
316
915
  }
@@ -321,15 +920,20 @@ impl Execution<'_> {
321
920
  if tracked {
322
921
  coverage.item(index);
323
922
  }
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
- })?;
923
+ report.scoped(
924
+ self.caches,
925
+ &format!("prefixItems/{index}"),
926
+ Some(index),
927
+ |r| {
928
+ self.collect::<false>(
929
+ r,
930
+ child,
931
+ unsafe { sn_entry(value, index) },
932
+ depth + 1,
933
+ active,
934
+ )
935
+ },
936
+ )?;
333
937
  }
334
938
  if rules.list {
335
939
  for (index, &child) in rules.items.iter().take(len).enumerate() {
@@ -339,15 +943,20 @@ impl Execution<'_> {
339
943
  if tracked {
340
944
  coverage.item(index);
341
945
  }
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
- })?;
946
+ report.scoped(
947
+ self.caches,
948
+ &format!("items/{index}"),
949
+ Some(index),
950
+ |r| {
951
+ self.collect::<false>(
952
+ r,
953
+ child,
954
+ unsafe { sn_entry(value, index) },
955
+ depth + 1,
956
+ active,
957
+ )
958
+ },
959
+ )?;
351
960
  }
352
961
  if let Some(child) = rules.additional {
353
962
  for index in rules.items.len()..len {
@@ -357,20 +966,25 @@ impl Execution<'_> {
357
966
  if tracked {
358
967
  coverage.item(index);
359
968
  }
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
- })?;
969
+ report.scoped(
970
+ self.caches,
971
+ "additionalItems",
972
+ Some(index),
973
+ |r| {
974
+ self.collect::<false>(
975
+ r,
976
+ child,
977
+ unsafe { sn_entry(value, index) },
978
+ depth + 1,
979
+ active,
980
+ )
981
+ },
982
+ )?;
369
983
  }
370
984
  }
371
985
  } else if let Some(&child) = rules.items.first() {
372
986
  let primitive = match self.nodes[child].as_slice() {
373
- [Op::Type(mask)]
987
+ [Op::Type(mask, _)]
374
988
  if mask & TYPE_NUMBER != 0 || mask & TYPE_INTEGER != 0 =>
375
989
  {
376
990
  Some(&[][..])
@@ -378,6 +992,7 @@ impl Execution<'_> {
378
992
  [Op::Number(_, rules)] => rules.fast_limits.as_deref(),
379
993
  _ => None,
380
994
  };
995
+ let primitive = primitive.filter(|_| !self.caches.dynamic_calls());
381
996
  let values = if len == 0 {
382
997
  &[]
383
998
  } else {
@@ -389,9 +1004,12 @@ impl Execution<'_> {
389
1004
  }
390
1005
  _ => None,
391
1006
  };
392
- let pattern = pattern.filter(|_| !self.budget.timed());
1007
+ let pattern = pattern
1008
+ .filter(|_| !self.budget.timed() && !self.caches.dynamic_calls());
393
1009
  let number = match self.nodes[child].as_slice() {
394
- [Op::Number(required, number)] if depth < MAX_EVALUATION_DEPTH => {
1010
+ [Op::Number(required, number)]
1011
+ if depth < MAX_EVALUATION_DEPTH && !self.caches.dynamic_calls() =>
1012
+ {
395
1013
  Some((*required, number))
396
1014
  }
397
1015
  _ => None,
@@ -402,14 +1020,16 @@ impl Execution<'_> {
402
1020
  }
403
1021
  _ => None,
404
1022
  };
405
- let format = format.filter(|_| !self.budget.timed());
1023
+ let format =
1024
+ format.filter(|_| !self.budget.timed() && !self.caches.dynamic_calls());
406
1025
  let content = match self.nodes[child].as_slice() {
407
1026
  [Op::String(required, string)] if depth < MAX_EVALUATION_DEPTH => {
408
1027
  string.content_only().map(|content| (*required, content))
409
1028
  }
410
1029
  _ => None,
411
1030
  };
412
- let content = content.filter(|_| !self.budget.timed());
1031
+ let content = content
1032
+ .filter(|_| !self.budget.timed() && !self.caches.dynamic_calls());
413
1033
  if pattern.is_some() || format.is_some() || content.is_some() {
414
1034
  let start = rules.prefix.len().min(len);
415
1035
  if tracked {
@@ -423,6 +1043,7 @@ impl Execution<'_> {
423
1043
  // The invalid callback is shared, but each scanner retains
424
1044
  // its statically selected parser and guarded byte views.
425
1045
  let invalid = |index, item| match report.scoped(
1046
+ self.caches,
426
1047
  "items",
427
1048
  Some(start + index),
428
1049
  |r| self.collect::<false>(r, child, item, depth + 1, active),
@@ -440,9 +1061,12 @@ impl Execution<'_> {
440
1061
  (_, Some((required, format)), _) => {
441
1062
  format.visit_invalid_items(required, &values[start..], invalid)
442
1063
  }
443
- (_, _, Some((required, content))) => {
444
- content.visit_invalid_items(required, &values[start..], invalid)
445
- }
1064
+ (_, _, Some((required, content))) => content.visit_invalid_items(
1065
+ self.caches,
1066
+ required,
1067
+ &values[start..],
1068
+ invalid,
1069
+ ),
446
1070
  _ => unreachable!("a scalar item scanner was selected"),
447
1071
  };
448
1072
  if status != Valid {
@@ -472,18 +1096,25 @@ impl Execution<'_> {
472
1096
  _ => n > limit,
473
1097
  })
474
1098
  } else if let Some((required, number)) = number {
475
- match number.eval_guarded(required, item) {
1099
+ match number.eval_guarded(
1100
+ self.caches,
1101
+ required,
1102
+ item,
1103
+ &self.budget,
1104
+ ) {
476
1105
  Valid => false,
477
1106
  Invalid => true,
478
- Unsupported => return Err(Unsupported),
1107
+ Unsupported => {
1108
+ self.needs_diagnostics(child, item, depth + 1)?
1109
+ }
479
1110
  }
480
1111
  } else {
481
- self.needs_diagnostics(child, item, depth + 1, active)?
1112
+ self.needs_diagnostics(child, item, depth + 1)?
482
1113
  };
483
1114
  if !needs {
484
1115
  continue;
485
1116
  }
486
- report.scoped("items", Some(index), |r| {
1117
+ report.scoped(self.caches, "items", Some(index), |r| {
487
1118
  self.collect::<false>(r, child, item, depth + 1, active)
488
1119
  })?;
489
1120
  }
@@ -497,15 +1128,8 @@ impl Execution<'_> {
497
1128
  };
498
1129
  let matches = evaluation::scan_contains(
499
1130
  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
- },
1131
+ evaluation::ContainsPolicy::AllMatches,
1132
+ |index| self.trial_status(child, values[index], depth + 1, active),
509
1133
  |index| {
510
1134
  if tracked {
511
1135
  coverage.item(index);
@@ -514,7 +1138,7 @@ impl Execution<'_> {
514
1138
  )?;
515
1139
  if bounds(matches, Some(rules.min_contains), rules.max_contains) == Invalid
516
1140
  {
517
- report.add("contains", source, value, matches)?;
1141
+ report.add(self.caches, "contains", source, value, matches)?;
518
1142
  }
519
1143
  }
520
1144
  if let Some(child) = rules
@@ -528,7 +1152,7 @@ impl Execution<'_> {
528
1152
  if prior.contains_item(index) || coverage.contains_item(index) {
529
1153
  continue;
530
1154
  }
531
- report.scoped("unevaluatedItems", Some(index), |r| {
1155
+ report.scoped(self.caches, "unevaluatedItems", Some(index), |r| {
532
1156
  self.collect::<false>(
533
1157
  r,
534
1158
  child,
@@ -541,33 +1165,79 @@ impl Execution<'_> {
541
1165
  }
542
1166
  }
543
1167
  if tracked {
544
- prior.merge_diagnostics(coverage)?;
1168
+ prior.merge_diagnostics(self, coverage)?;
545
1169
  }
546
1170
  }
547
1171
  Op::Object(required, rules) => {
1172
+ if self.caches.report_continuations_allowed()
1173
+ && !rules.header_only()
1174
+ && (kind == TYPE_UNSUPPORTED
1175
+ || self.caches.dynamic_calls()
1176
+ || prior.has_runtime_properties())
1177
+ {
1178
+ let result = self.caches.container_report(
1179
+ node,
1180
+ value,
1181
+ (source.1, *required, true),
1182
+ report,
1183
+ &prior,
1184
+ tracked,
1185
+ )?;
1186
+ if tracked {
1187
+ prior.merge_diagnostics(self, result)?;
1188
+ }
1189
+ if report.silent && report.failed {
1190
+ break;
1191
+ }
1192
+ continue;
1193
+ }
1194
+ if rules.header_only() && rules.header.runtime_required(self.caches, kind) {
1195
+ if report.collect_header(
1196
+ self.caches,
1197
+ &rules.header,
1198
+ *required,
1199
+ value,
1200
+ source,
1201
+ )? {
1202
+ break;
1203
+ }
1204
+ continue;
1205
+ }
548
1206
  if kind == TYPE_OBJECT && !self.budget.checkpoint() {
549
1207
  return Err(Unsupported);
550
1208
  }
551
1209
  if *required != 0 && kind != TYPE_OBJECT {
552
- report.add("type", source, value, 0)?;
1210
+ report.add(self.caches, "type", source, value, 0)?;
553
1211
  }
554
1212
  if kind != TYPE_OBJECT {
555
1213
  continue;
556
1214
  }
1215
+ if report.silent {
1216
+ let status =
1217
+ self.object_tracked(rules, value, depth + 1, active, &mut prior);
1218
+ if status == Unsupported {
1219
+ return Err(Unsupported);
1220
+ }
1221
+ report.failed |= status == Invalid;
1222
+ if report.failed {
1223
+ break;
1224
+ }
1225
+ continue;
1226
+ }
557
1227
  let mut coverage = Tracked::new(Valid);
558
1228
  let len = unsafe { sn_len(value) };
559
1229
  if rules.max.is_some_and(|max| len > max) {
560
- report.add("maxProperties", source, value, len)?;
1230
+ report.add(self.caches, "maxProperties", source, value, len)?;
561
1231
  }
562
1232
  if rules.min.is_some_and(|min| len < min) {
563
- report.add("minProperties", source, value, len)?;
1233
+ report.add(self.caches, "minProperties", source, value, len)?;
564
1234
  }
565
1235
  for &name in &rules.required_order {
566
1236
  if !self.budget.checkpoint() {
567
1237
  return Err(Unsupported);
568
1238
  }
569
- if missing(value, name, self.undefined) {
570
- report.add("required", source, name, 0)?;
1239
+ if name.missing(self.caches, value, self.undefined)? {
1240
+ report.add(self.caches, "required", source, name.name, 0)?;
571
1241
  }
572
1242
  }
573
1243
  let mut context = ObjectDiagnostics {
@@ -621,8 +1291,14 @@ impl Execution<'_> {
621
1291
  if !self.budget.checkpoint() {
622
1292
  return Err(Unsupported);
623
1293
  }
624
- if missing(value, required, self.undefined) {
625
- report.add(keyword, (source.0, *name), required, 0)?;
1294
+ if required.missing(self.caches, value, self.undefined)? {
1295
+ report.add(
1296
+ self.caches,
1297
+ keyword,
1298
+ (source.0, *name),
1299
+ required.name,
1300
+ 0,
1301
+ )?;
626
1302
  }
627
1303
  }
628
1304
  }
@@ -631,10 +1307,8 @@ impl Execution<'_> {
631
1307
  continue;
632
1308
  }
633
1309
  if tracked {
634
- let result = report.scoped(
635
- &format!("{keyword}/{}", pointer_name(*name)?),
636
- None,
637
- |r| {
1310
+ let result =
1311
+ report.schema_name(self.caches, keyword, *name, |r| {
638
1312
  self.collect::<true>(
639
1313
  r,
640
1314
  *child,
@@ -642,30 +1316,16 @@ impl Execution<'_> {
642
1316
  depth + 1,
643
1317
  active,
644
1318
  )
645
- },
646
- )?;
647
- if result.status == Valid {
648
- coverage.merge(result);
1319
+ })?;
1320
+ if result.status == Valid
1321
+ && coverage.merge_runtime(self, result) == Unsupported
1322
+ {
1323
+ return Err(Unsupported);
649
1324
  }
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
- )?;
1325
+ } else if self.needs_diagnostics(*child, value, depth + 1)? {
1326
+ report.schema_name(self.caches, keyword, *name, |r| {
1327
+ self.collect::<false>(r, *child, value, depth + 1, active)
1328
+ })?;
669
1329
  }
670
1330
  }
671
1331
  }
@@ -698,7 +1358,7 @@ impl Execution<'_> {
698
1358
  }
699
1359
  }
700
1360
  if tracked {
701
- prior.merge_diagnostics(coverage)?;
1361
+ prior.merge_diagnostics(self, coverage)?;
702
1362
  }
703
1363
  }
704
1364
  Op::All(nodes) => {
@@ -707,19 +1367,34 @@ impl Execution<'_> {
707
1367
  if !self.budget.checkpoint() {
708
1368
  return Err(Unsupported);
709
1369
  }
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
- })?;
1370
+ let result =
1371
+ report.scoped(self.caches, &format!("allOf/{index}"), None, |r| {
1372
+ if tracked {
1373
+ self.collect_kind::<true>(
1374
+ r,
1375
+ child,
1376
+ value,
1377
+ depth + 1,
1378
+ active,
1379
+ kind_state,
1380
+ )
1381
+ } else {
1382
+ self.collect_kind::<false>(
1383
+ r,
1384
+ child,
1385
+ value,
1386
+ depth + 1,
1387
+ active,
1388
+ kind_state,
1389
+ )
1390
+ }
1391
+ })?;
717
1392
  if tracked {
718
- coverage.merge_diagnostics(result)?;
1393
+ coverage.merge_diagnostics(self, result)?;
719
1394
  }
720
1395
  }
721
1396
  if tracked {
722
- prior.merge_diagnostics(coverage)?;
1397
+ prior.merge_diagnostics(self, coverage)?;
723
1398
  }
724
1399
  }
725
1400
  Op::Any(nodes) | Op::One(nodes) => {
@@ -728,67 +1403,68 @@ impl Execution<'_> {
728
1403
  } else {
729
1404
  evaluation::BranchKind::One
730
1405
  };
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
- };
1406
+ // Trials execute complete scalar checks without retaining
1407
+ // errors or paths; ordinary validity may stop earlier.
1408
+ let result = evaluation::scan_branches(
1409
+ nodes,
1410
+ evaluation::BranchPolicy::Complete,
1411
+ self.caches.dynamic_calls(),
1412
+ |child| self.eval_tracked_kind(child, value, depth + 1, active, kind_state),
1413
+ |coverage, branch| {
1414
+ if coverage.merge_runtime(self, branch) == Unsupported {
1415
+ Err(Unsupported)
1416
+ } else {
1417
+ Ok(())
1418
+ }
1419
+ },
1420
+ )?;
752
1421
  if !branch_kind.accepts(result.matches) {
753
- report.add(branch_kind.keyword(), source, value, result.matches)?;
1422
+ report.add(
1423
+ self.caches,
1424
+ branch_kind.keyword(),
1425
+ source,
1426
+ value,
1427
+ result.matches,
1428
+ )?;
754
1429
  } else if tracked {
755
- prior.merge_diagnostics(result.coverage)?;
1430
+ prior.merge_diagnostics(self, result.coverage)?;
756
1431
  }
757
1432
  }
758
1433
  Op::Not(child) => {
759
- match self.eval_with_kind(*child, value, depth + 1, active, Some(kind)) {
1434
+ match self
1435
+ .eval_tracked_kind(*child, value, depth + 1, active, kind_state)
1436
+ .status
1437
+ {
760
1438
  Unsupported => return Err(Unsupported),
761
- Valid => report.add("not", source, value, 0)?,
1439
+ Valid => report.add(self.caches, "not", source, value, 0)?,
762
1440
  Invalid => {}
763
1441
  }
764
1442
  }
765
1443
  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
- };
1444
+ let condition =
1445
+ self.eval_tracked_kind(*condition, value, depth + 1, active, kind_state);
777
1446
  let mut coverage = Tracked::new(Valid);
778
1447
  let (child, segment) = match condition.status {
779
1448
  Unsupported => return Err(Unsupported),
780
1449
  Valid => {
781
- if tracked {
782
- coverage.merge(condition);
1450
+ if tracked && coverage.merge_runtime(self, condition) == Unsupported {
1451
+ return Err(Unsupported);
783
1452
  }
784
1453
  (yes, "then")
785
1454
  }
786
1455
  Invalid => (no, "else"),
787
1456
  };
788
1457
  if let Some(child) = child {
789
- let result = report.scoped(segment, None, |r| {
1458
+ let result = report.scoped(self.caches, segment, None, |r| {
790
1459
  if tracked {
791
- self.collect_kind::<true>(r, *child, value, depth + 1, active, kind)
1460
+ self.collect_kind::<true>(
1461
+ r,
1462
+ *child,
1463
+ value,
1464
+ depth + 1,
1465
+ active,
1466
+ kind_state,
1467
+ )
792
1468
  } else {
793
1469
  self.collect_kind::<false>(
794
1470
  r,
@@ -796,20 +1472,24 @@ impl Execution<'_> {
796
1472
  value,
797
1473
  depth + 1,
798
1474
  active,
799
- kind,
1475
+ kind_state,
800
1476
  )
801
1477
  }
802
1478
  })?;
803
1479
  if tracked {
804
- coverage.merge_diagnostics(result)?;
1480
+ coverage.merge_diagnostics(self, result)?;
805
1481
  }
806
1482
  }
807
1483
  if tracked {
808
- prior.merge_diagnostics(coverage)?;
1484
+ prior.merge_diagnostics(self, coverage)?;
809
1485
  }
810
1486
  }
811
1487
  Op::Ref(target) => {
812
- if RefFrame::contains(active.active, node, value) {
1488
+ let target = self.resolve_reference(*target, value, active)?;
1489
+ let program = self.programs[node];
1490
+ if RefFrame::contains(active.active, node, value)
1491
+ || self.caches.reference_active(program, value)
1492
+ {
813
1493
  continue;
814
1494
  }
815
1495
  let frame = RefFrame {
@@ -817,29 +1497,51 @@ impl Execution<'_> {
817
1497
  value,
818
1498
  parent: active.active,
819
1499
  };
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);
1500
+ let keyword = reference_keyword(source.0);
1501
+ let reference = self.caches.reference_frame(program, value);
1502
+ let _guard = self.caches.reference_guard(&reference);
826
1503
  let active = scope::Context {
827
1504
  active: Some(&frame),
828
1505
  ..active
829
1506
  };
830
- let result = report.scoped(keyword, None, |r| {
1507
+ let result = report.scoped(self.caches, keyword, None, |r| {
831
1508
  if tracked {
832
- self.collect_kind::<true>(r, target, value, depth + 1, active, kind)
1509
+ self.collect_kind::<true>(
1510
+ r,
1511
+ target,
1512
+ value,
1513
+ depth + 1,
1514
+ active,
1515
+ kind_state,
1516
+ )
833
1517
  } else {
834
- self.collect_kind::<false>(r, target, value, depth + 1, active, kind)
1518
+ self.collect_kind::<false>(
1519
+ r,
1520
+ target,
1521
+ value,
1522
+ depth + 1,
1523
+ active,
1524
+ kind_state,
1525
+ )
835
1526
  }
836
1527
  })?;
837
1528
  if tracked {
838
- prior.merge_diagnostics(result)?;
1529
+ prior.merge_diagnostics(self, result)?;
839
1530
  }
840
1531
  }
841
1532
  }
1533
+ self.caches.apply_report_effects(report);
1534
+ if report.silent
1535
+ && report.failed
1536
+ && matches!(
1537
+ op,
1538
+ Op::All(_) | Op::Any(_) | Op::One(_) | Op::Not(_) | Op::Conditional(..)
1539
+ )
1540
+ {
1541
+ break;
1542
+ }
842
1543
  }
1544
+ self.caches.apply_report_effects(report);
843
1545
  let failed = report.failed;
844
1546
  report.failed |= parent_failed;
845
1547
  // Diagnostic coverage keeps local attempted locations for subsequent
@@ -849,33 +1551,61 @@ impl Execution<'_> {
849
1551
 
850
1552
  fn needs_diagnostics(
851
1553
  &self,
852
- node: usize,
1554
+ mut node: usize,
853
1555
  value: Value,
854
- depth: usize,
855
- active: scope::Context<'_>,
1556
+ mut depth: usize,
856
1557
  ) -> Result<bool, Verdict> {
857
- if !self.budget.checkpoint() {
858
- return Err(Unsupported);
1558
+ loop {
1559
+ if !self.budget.checkpoint() {
1560
+ return Err(Unsupported);
1561
+ }
1562
+ if depth > MAX_EVALUATION_DEPTH {
1563
+ return Ok(true);
1564
+ }
1565
+ let [Op::Ref(reference)] = self.nodes[node].as_slice() else {
1566
+ break;
1567
+ };
1568
+ let Some(target) = reference.static_target() else {
1569
+ break;
1570
+ };
1571
+ node = target;
1572
+ depth += 1;
859
1573
  }
860
1574
  let status = match self.nodes[node].as_slice() {
861
1575
  [Op::Number(required, rules)] if depth <= MAX_EVALUATION_DEPTH => {
862
- rules.eval_guarded(*required, value)
1576
+ if self.caches.dynamic_calls()
1577
+ || self.diagnostic_kind(node, value, &InstanceKind::new(TYPE_UNCHECKED))
1578
+ == TYPE_UNSUPPORTED
1579
+ {
1580
+ return Ok(true);
1581
+ }
1582
+ rules.eval_guarded(self.caches, *required, value, &self.budget)
863
1583
  }
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 {
1584
+ [
1585
+ op @ (Op::Type(..) | Op::Boolean(_) | Op::String(..) | Op::Const(_) | Op::Enum(_)),
1586
+ ] if depth <= MAX_EVALUATION_DEPTH => {
1587
+ let kind = self.diagnostic_kind(node, value, &InstanceKind::new(TYPE_UNCHECKED));
1588
+ if (kind == TYPE_UNSUPPORTED
1589
+ && matches!(
1590
+ op,
1591
+ Op::Type(..) | Op::String(..) | Op::Const(_) | Op::Enum(_)
1592
+ ))
1593
+ || (self.caches.dynamic_calls()
1594
+ && matches!(op, Op::String(..) | Op::Const(_) | Op::Enum(_)))
1595
+ {
1596
+ // Collection must execute a dynamic predicate once, rather
1597
+ // than once here and again while recording its failure.
1598
+ return Ok(true);
1599
+ }
1600
+ if self.unsupported_kind(node, kind) {
873
1601
  Unsupported
874
1602
  } else {
875
1603
  self.leaf(op, kind, value, depth)
876
1604
  }
877
1605
  }
878
- _ => self.eval(node, value, depth, active),
1606
+ // A successful validity/trial scan can omit diagnostic-only work
1607
+ // in descendants. Only complete collection proves their success.
1608
+ _ => return Ok(true),
879
1609
  };
880
1610
  match status {
881
1611
  Valid => Ok(false),
@@ -904,8 +1634,8 @@ unsafe extern "C" fn object_diagnostic(context: *mut c_void, key: Value, value:
904
1634
  return Unsupported as i32;
905
1635
  }
906
1636
  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| {
1637
+ if c.engine.needs_diagnostics(child, item, c.depth)? {
1638
+ report.property(c.engine.caches, segment, key, schema_key, |r| {
909
1639
  c.engine.collect::<false>(r, child, item, c.depth, c.active)
910
1640
  })?;
911
1641
  }
@@ -934,9 +1664,10 @@ unsafe extern "C" fn object_diagnostic(context: *mut c_void, key: Value, value:
934
1664
  c.coverage.property(c.engine, key);
935
1665
  return Ok(());
936
1666
  }
937
- let evaluated = c.rules.visit_value_rules::<true>(
1667
+ let evaluated = c.rules.visit_value_rules(
938
1668
  c.engine.caches,
939
1669
  &c.engine.budget,
1670
+ true,
940
1671
  key,
941
1672
  None,
942
1673
  |segment, name, child| visit(c.report, child, value, segment, name),
@@ -962,10 +1693,20 @@ pub struct Record {
962
1693
  instance_len: usize,
963
1694
  schema_path: *const u8,
964
1695
  schema_len: usize,
1696
+ instance_pointer: Value,
1697
+ schema_pointer: Value,
965
1698
  opcode: i32,
966
1699
  operand: Value,
967
1700
  instance: Value,
968
1701
  detail: usize,
1702
+ message: Value,
1703
+ }
1704
+ fn report_outcome(result: std::thread::Result<Result<*mut Report, i32>>) -> (*mut Report, i32) {
1705
+ match result {
1706
+ Ok(Ok(report)) => (report, VERDICT_VALID),
1707
+ Ok(Err(code)) => (std::ptr::null_mut(), code),
1708
+ Err(_) => (std::ptr::null_mut(), STATUS_PANIC),
1709
+ }
969
1710
  }
970
1711
  /// # Safety
971
1712
  /// Engine/status must be live under the GVL; root must be in bounds and
@@ -973,14 +1714,14 @@ pub struct Record {
973
1714
  #[unsafe(no_mangle)]
974
1715
  pub unsafe extern "C" fn sn_diagnose(
975
1716
  engine: *const Engine,
976
- caches: *const pattern::Caches,
1717
+ caches: *const runtime::Caches,
977
1718
  root: usize,
978
1719
  instance: Value,
979
1720
  status: *mut i32,
980
1721
  ) -> *mut Report {
981
1722
  let result = catch_unwind(AssertUnwindSafe(|| {
982
1723
  let engine = unsafe { &*engine };
983
- let local = pattern::Caches::default();
1724
+ let local = runtime::Caches::default();
984
1725
  let caches = unsafe { caches.as_ref() }.unwrap_or(&local);
985
1726
  let execution = Execution {
986
1727
  engine,
@@ -988,33 +1729,29 @@ pub unsafe extern "C" fn sn_diagnose(
988
1729
  memo: memo::Memo::default(),
989
1730
  budget: budget::Budget::default(),
990
1731
  };
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)))
1732
+ caches.admit(engine, root);
1733
+ let inherited = caches.initial_report();
1734
+ let resumed = inherited.is_some();
1735
+ let mut report = inherited.unwrap_or_else(|| Report {
1736
+ render_messages: engine.runtime_messages,
1737
+ ..Report::default()
1738
+ });
1739
+ let result = execution
1740
+ .collect_report(&mut report, root, instance)
1741
+ .map_err(|verdict| execution.budget.status(verdict))?;
1742
+ report.evaluation = if resumed { Some(result) } else { None };
1743
+ Ok::<_, i32>(Box::into_raw(Box::new(report)))
994
1744
  }));
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
- }
1745
+ let (report, code) = report_outcome(result);
1746
+ unsafe { *status = code };
1747
+ report
1011
1748
  }
1012
1749
  /// # Safety
1013
1750
  /// Same requirements as sn_diagnose; all limit/output pointers must be live.
1014
1751
  #[unsafe(no_mangle)]
1015
1752
  pub unsafe extern "C" fn sn_diagnose_with_limits(
1016
1753
  engine: *const Engine,
1017
- caches: *const pattern::Caches,
1754
+ caches: *const runtime::Caches,
1018
1755
  root: usize,
1019
1756
  instance: Value,
1020
1757
  status: *mut i32,
@@ -1022,21 +1759,25 @@ pub unsafe extern "C" fn sn_diagnose_with_limits(
1022
1759
  outcome: *mut budget::Outcome,
1023
1760
  ) -> *mut Report {
1024
1761
  let result = catch_unwind(AssertUnwindSafe(|| {
1025
- let local = pattern::Caches::default();
1762
+ let local = runtime::Caches::default();
1026
1763
  let caches = unsafe { caches.as_ref() }.unwrap_or(&local);
1027
1764
  let limits = unsafe { *limits };
1028
1765
  let execution = Execution {
1029
1766
  engine: unsafe { &*engine },
1030
1767
  caches,
1031
1768
  memo: memo::Memo::default(),
1032
- budget: budget::Budget::new(limits),
1769
+ budget: caches.budget(limits),
1033
1770
  };
1034
- let mut report = Report {
1771
+ caches.admit(execution.engine, root);
1772
+ let inherited = caches.initial_report();
1773
+ let resumed = inherited.is_some();
1774
+ let mut report = inherited.unwrap_or_else(|| Report {
1035
1775
  limits,
1776
+ render_messages: execution.runtime_messages,
1036
1777
  ..Report::default()
1037
- };
1778
+ });
1038
1779
  let result = if execution.budget.check_deadline() {
1039
- execution.collect::<false>(&mut report, root, instance, 0, scope::Context::default())
1780
+ execution.collect_report(&mut report, root, instance)
1040
1781
  } else {
1041
1782
  Err(Unsupported)
1042
1783
  };
@@ -1053,24 +1794,16 @@ pub unsafe extern "C" fn sn_diagnose_with_limits(
1053
1794
  unsafe {
1054
1795
  ((*outcome).kind, (*outcome).attempted) = hit;
1055
1796
  }
1056
- result.map(|_| Box::into_raw(Box::new(report)))
1797
+ result
1798
+ .map(|evaluation| {
1799
+ report.evaluation = if resumed { Some(evaluation) } else { None };
1800
+ Box::into_raw(Box::new(report))
1801
+ })
1802
+ .map_err(|verdict| execution.budget.status(verdict))
1057
1803
  }));
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
- }
1804
+ let (report, code) = report_outcome(result);
1805
+ unsafe { *status = code };
1806
+ report
1074
1807
  }
1075
1808
  /// # Safety
1076
1809
  /// Report must be live under the GVL.
@@ -1082,15 +1815,7 @@ pub unsafe extern "C" fn sn_report_len(report: *const Report) -> usize {
1082
1815
  /// Report must be live under the GVL.
1083
1816
  #[unsafe(no_mangle)]
1084
1817
  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>()
1818
+ std::mem::size_of::<Report>() + unsafe { &*report }.memory_usage()
1094
1819
  }
1095
1820
  /// # Safety
1096
1821
  /// Report must be live, index in bounds, and record a writable pointer.
@@ -1105,14 +1830,17 @@ pub unsafe extern "C" fn sn_report_record(
1105
1830
  *record = Record {
1106
1831
  keyword: e.keyword.as_ptr(),
1107
1832
  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(),
1833
+ instance_path: e.instance_path.bytes().as_ptr(),
1834
+ instance_len: e.instance_path.bytes().len(),
1835
+ schema_path: e.schema_path.bytes().as_ptr(),
1836
+ schema_len: e.schema_path.bytes().len(),
1837
+ instance_pointer: e.instance_path.value(),
1838
+ schema_pointer: e.schema_path.value(),
1112
1839
  opcode: e.opcode,
1113
1840
  operand: e.operand,
1114
1841
  instance: e.instance,
1115
1842
  detail: e.detail,
1843
+ message: e.message,
1116
1844
  }
1117
1845
  };
1118
1846
  }
@@ -1120,12 +1848,7 @@ pub unsafe extern "C" fn sn_report_record(
1120
1848
  /// Report must be live; call only from its synchronous Ruby GC mark callback.
1121
1849
  #[unsafe(no_mangle)]
1122
1850
  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
- }
1851
+ unsafe { &*report }.mark();
1129
1852
  }
1130
1853
  /// # Safety
1131
1854
  /// Report must be owned, returned by sn_diagnose, and freed exactly once.
@@ -1133,3 +1856,60 @@ pub unsafe extern "C" fn sn_report_mark(report: *const Report) {
1133
1856
  pub unsafe extern "C" fn sn_report_free(report: *mut Report) {
1134
1857
  drop(unsafe { Box::from_raw(report) });
1135
1858
  }
1859
+
1860
+ #[cfg(test)]
1861
+ mod tests {
1862
+ use super::*;
1863
+
1864
+ #[test]
1865
+ fn report_seeds_and_results_own_error_paths_limits_and_locations() {
1866
+ let mut report = Report {
1867
+ attempts: 2,
1868
+ failed: true,
1869
+ ..Report::default()
1870
+ };
1871
+ report.instance_path.text.push_str("/item");
1872
+ report.schema_path.text.push_str("/allOf/1");
1873
+ report.limits = budget::Limits {
1874
+ enabled: 1,
1875
+ max_errors: 3,
1876
+ ..budget::Limits::default()
1877
+ };
1878
+ report.errors.push(Error {
1879
+ keyword: "type",
1880
+ instance_path: pointer::Rendered::Native("/item".into()),
1881
+ schema_path: pointer::Rendered::Native("/allOf/0/type".into()),
1882
+ opcode: OP_TYPE_INTEGER,
1883
+ operand: 0,
1884
+ instance: 0,
1885
+ detail: 0,
1886
+ message: 0,
1887
+ });
1888
+ let mut locations = Tracked::new(Valid);
1889
+ locations.item(4);
1890
+ locations.item(65);
1891
+ report.evaluation = Some(locations);
1892
+ let mut seed = report.continuation(true);
1893
+ assert!(seed.annotations);
1894
+ assert!(seed.evaluation.is_none());
1895
+ assert_eq!(seed.attempts, 2);
1896
+ assert!(seed.failed);
1897
+ assert_eq!(seed.limits.max_errors, 3);
1898
+ seed.instance_path.text.push_str("/child");
1899
+ seed.errors.clear();
1900
+ assert_eq!(report.instance_path.text, "/item");
1901
+ assert_eq!(report.errors.len(), 1);
1902
+ assert!(report.memory_usage() > 0);
1903
+ let mut adopted = Report::default();
1904
+ let mut completed = report.clone();
1905
+ completed.annotations = true;
1906
+ let mut result = adopted.adopt(completed);
1907
+ result.item(8);
1908
+ assert_eq!(result.item_count(), 3);
1909
+ assert_eq!(report.evaluation.as_ref().unwrap().item_count(), 2);
1910
+ assert!(report.evaluation.as_ref().unwrap().contains_item(65));
1911
+ assert!(adopted.evaluation.is_none());
1912
+ assert!(!adopted.annotations);
1913
+ assert_eq!(adopted.attempts, 2);
1914
+ }
1915
+ }