schemurai 2.2.2 → 2.3.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.
@@ -0,0 +1,532 @@
1
+ //! Evaluated locations stay in native buffers, scoped to one instance location.
2
+ //! Keys remain borrowed VALUEs; no Ruby objects or instance trees are allocated.
3
+ use super::*;
4
+ use std::collections::HashSet;
5
+ use std::hash::Hash;
6
+
7
+ #[derive(Clone, Copy, Debug)]
8
+ pub(super) struct JsonKey(pub Value, u64);
9
+ impl JsonKey {
10
+ pub(super) fn new(value: Value) -> Self {
11
+ Self(value, string_fingerprint(value))
12
+ }
13
+ }
14
+ impl PartialEq for JsonKey {
15
+ fn eq(&self, other: &Self) -> bool {
16
+ self.0 == other.0 || unsafe { sn_string_equal(self.0, other.0) != 0 }
17
+ }
18
+ }
19
+ impl Eq for JsonKey {}
20
+ impl Hash for JsonKey {
21
+ fn hash<H: Hasher>(&self, state: &mut H) {
22
+ state.write_u64(self.1);
23
+ }
24
+ }
25
+ pub(super) type KeySet = HashSet<JsonKey, BuildHasherDefault<BucketHasher>>;
26
+
27
+ pub(super) struct Tracked {
28
+ pub status: Verdict,
29
+ properties: KeySet,
30
+ known_properties: Vec<u64>,
31
+ items: Vec<u64>,
32
+ }
33
+ impl Tracked {
34
+ pub(super) fn new(status: Verdict) -> Self {
35
+ Self {
36
+ status,
37
+ properties: KeySet::default(),
38
+ known_properties: vec![],
39
+ items: vec![],
40
+ }
41
+ }
42
+ pub(super) fn item(&mut self, index: usize) {
43
+ let word = index / 64;
44
+ if self.items.len() <= word {
45
+ self.items.resize(word + 1, 0);
46
+ }
47
+ self.items[word] |= 1_u64 << (index % 64);
48
+ }
49
+ pub(super) fn contains_item(&self, index: usize) -> bool {
50
+ self.items
51
+ .get(index / 64)
52
+ .is_some_and(|word| word & (1_u64 << (index % 64)) != 0)
53
+ }
54
+ fn known_property(&mut self, index: usize) {
55
+ let word = index / 64;
56
+ if self.known_properties.len() <= word {
57
+ self.known_properties.resize(word + 1, 0);
58
+ }
59
+ self.known_properties[word] |= 1_u64 << (index % 64);
60
+ }
61
+ fn contains_known_property(&self, index: usize) -> bool {
62
+ self.known_properties
63
+ .get(index / 64)
64
+ .is_some_and(|word| word & (1_u64 << (index % 64)) != 0)
65
+ }
66
+ pub(super) fn property_count(&self) -> usize {
67
+ self.properties.len()
68
+ + self
69
+ .known_properties
70
+ .iter()
71
+ .map(|word| word.count_ones() as usize)
72
+ .sum::<usize>()
73
+ }
74
+ pub(super) fn item_count(&self) -> usize {
75
+ self.items
76
+ .iter()
77
+ .map(|word| word.count_ones() as usize)
78
+ .sum()
79
+ }
80
+ pub(super) fn merge(&mut self, other: Self) -> Verdict {
81
+ if other.status != Valid {
82
+ return other.status;
83
+ }
84
+ if self.properties.is_empty() {
85
+ self.properties = other.properties;
86
+ } else {
87
+ self.properties.extend(other.properties);
88
+ }
89
+ merge_bits(&mut self.known_properties, other.known_properties);
90
+ merge_bits(&mut self.items, other.items);
91
+ Valid
92
+ }
93
+ pub(super) fn merge_diagnostics(&mut self, other: Self) -> Result<(), Verdict> {
94
+ if other.status == Unsupported {
95
+ return Err(Unsupported);
96
+ }
97
+ if self.status != Valid || other.status != Valid {
98
+ *self = Self::new(Invalid);
99
+ } else {
100
+ self.merge(other);
101
+ }
102
+ Ok(())
103
+ }
104
+ pub(super) fn property(&mut self, engine: &Engine, value: Value) {
105
+ let key = JsonKey::new(value);
106
+ if let Some(&index) = engine.property_ids.get(&key) {
107
+ self.known_property(index);
108
+ } else {
109
+ self.properties.insert(key);
110
+ }
111
+ }
112
+ pub(super) fn contains_property(&self, engine: &Engine, value: Value) -> bool {
113
+ let key = JsonKey::new(value);
114
+ engine
115
+ .property_ids
116
+ .get(&key)
117
+ .is_some_and(|&index| self.contains_known_property(index))
118
+ || self.properties.contains(&key)
119
+ }
120
+ }
121
+ fn merge_bits(target: &mut Vec<u64>, source: Vec<u64>) {
122
+ // Branch annotations own native buffers of location bits. Transfer an
123
+ // existing buffer into an empty accumulator instead of allocating a copy.
124
+ if target.is_empty() {
125
+ *target = source;
126
+ } else {
127
+ if target.len() < source.len() {
128
+ target.resize(source.len(), 0);
129
+ }
130
+ for (target, source) in target.iter_mut().zip(source) {
131
+ *target |= source;
132
+ }
133
+ }
134
+ }
135
+ impl Engine {
136
+ pub(super) fn eval_tracked(
137
+ &self,
138
+ node: usize,
139
+ value: Value,
140
+ depth: usize,
141
+ active: scope::Context<'_>,
142
+ ) -> Tracked {
143
+ if depth > MAX_EVALUATION_DEPTH {
144
+ return Tracked::new(Unsupported);
145
+ }
146
+ let kind = if value & self.fixnum_flag != 0 {
147
+ TYPE_INTEGRAL_NUMBER
148
+ } else {
149
+ unsafe { sn_kind(value) }
150
+ };
151
+ self.eval_tracked_kind(node, value, depth, active, kind)
152
+ }
153
+ // Applicator branches evaluate the same borrowed instance under the GVL.
154
+ // No Ruby callback can mutate it, so its guarded kind can be reused while
155
+ // each branch still enters its own resource scope and checks depth.
156
+ pub(super) fn eval_tracked_kind(
157
+ &self,
158
+ node: usize,
159
+ value: Value,
160
+ depth: usize,
161
+ active: scope::Context<'_>,
162
+ kind: i32,
163
+ ) -> Tracked {
164
+ let frame = scope::ScopeFrame {
165
+ resource: self.metadata[node].resource,
166
+ parent: active.scope,
167
+ primary_binding: active
168
+ .scope
169
+ .and_then(|frame| frame.primary_binding)
170
+ .or(self.metadata[node].primary_binding),
171
+ };
172
+ let active = self.enter_scope(node, active, &frame);
173
+ if depth > MAX_EVALUATION_DEPTH {
174
+ return Tracked::new(Unsupported);
175
+ }
176
+ if kind == TYPE_UNSUPPORTED {
177
+ return Tracked::new(Unsupported);
178
+ }
179
+ let mut evaluation = Tracked::new(Valid);
180
+ for op in &self.nodes[node] {
181
+ let result = match op {
182
+ Op::Ref(target) => {
183
+ if RefFrame::contains(active.active, node, value) {
184
+ Valid
185
+ } else {
186
+ let frame = RefFrame {
187
+ source: node,
188
+ value,
189
+ parent: active.active,
190
+ };
191
+ evaluation.merge(self.eval_tracked_kind(
192
+ self.reference_target(*target, active),
193
+ value,
194
+ depth + 1,
195
+ scope::Context {
196
+ active: Some(&frame),
197
+ ..active
198
+ },
199
+ kind,
200
+ ))
201
+ }
202
+ }
203
+ Op::All(nodes) => {
204
+ let mut result = Valid;
205
+ for &child in nodes {
206
+ result = evaluation.merge(self.eval_tracked_kind(
207
+ child,
208
+ value,
209
+ depth + 1,
210
+ active,
211
+ kind,
212
+ ));
213
+ if result != Valid {
214
+ break;
215
+ }
216
+ }
217
+ result
218
+ }
219
+ Op::Any(nodes) | Op::One(nodes) => {
220
+ let mut matched = Tracked::new(Valid);
221
+ let mut count = 0;
222
+ let mut result = Valid;
223
+ for &(child, multiplicity) in nodes {
224
+ let branch = self.eval_tracked_kind(child, value, depth + 1, active, kind);
225
+ if branch.status == Unsupported {
226
+ result = Unsupported;
227
+ break;
228
+ }
229
+ if branch.status == Valid {
230
+ count += multiplicity;
231
+ matched.merge(branch);
232
+ }
233
+ }
234
+ if result != Valid {
235
+ result
236
+ } else if count == 0 || (matches!(op, Op::One(_)) && count != 1) {
237
+ Invalid
238
+ } else {
239
+ evaluation.merge(matched)
240
+ }
241
+ }
242
+ Op::Not(child) => match self.eval(*child, value, depth + 1, active) {
243
+ Valid => Invalid,
244
+ Invalid => Valid,
245
+ Unsupported => Unsupported,
246
+ },
247
+ Op::Conditional(condition, yes, no) => {
248
+ let condition =
249
+ self.eval_tracked_kind(*condition, value, depth + 1, active, kind);
250
+ let status = condition.status;
251
+ if status == Unsupported {
252
+ Unsupported
253
+ } else {
254
+ let branch = if status == Valid {
255
+ evaluation.merge(condition);
256
+ yes
257
+ } else {
258
+ no
259
+ };
260
+ if let Some(child) = branch {
261
+ evaluation.merge(self.eval_tracked_kind(
262
+ *child,
263
+ value,
264
+ depth + 1,
265
+ active,
266
+ kind,
267
+ ))
268
+ } else {
269
+ Valid
270
+ }
271
+ }
272
+ }
273
+ Op::Array(required, rules) => {
274
+ if *required != 0 && kind != TYPE_ARRAY {
275
+ Invalid
276
+ } else if kind != TYPE_ARRAY {
277
+ Valid
278
+ } else {
279
+ self.array_tracked(rules, value, depth + 1, active, &mut evaluation)
280
+ }
281
+ }
282
+ Op::Object(required, rules) => {
283
+ if *required != 0 && kind != TYPE_OBJECT {
284
+ Invalid
285
+ } else if kind != TYPE_OBJECT {
286
+ Valid
287
+ } else {
288
+ self.object_tracked(rules, value, depth + 1, active, &mut evaluation)
289
+ }
290
+ }
291
+ _ => self.leaf(op, kind, value, depth),
292
+ };
293
+ if result != Valid {
294
+ return Tracked::new(result);
295
+ }
296
+ }
297
+ evaluation
298
+ }
299
+ fn array_tracked(
300
+ &self,
301
+ r: &Array,
302
+ value: Value,
303
+ depth: usize,
304
+ active: scope::Context<'_>,
305
+ evaluation: &mut Tracked,
306
+ ) -> Verdict {
307
+ let len = unsafe { sn_len(value) };
308
+ if bounds(len, r.min, r.max) == Invalid {
309
+ return Invalid;
310
+ }
311
+ if r.unique {
312
+ let result = unique(value, depth, self.fixnum_flag);
313
+ if result != Valid {
314
+ return result;
315
+ }
316
+ }
317
+ for (index, &child) in r.prefix.iter().take(len).enumerate() {
318
+ let result = self.eval(child, unsafe { sn_entry(value, index) }, depth, active);
319
+ if result != Valid {
320
+ return result;
321
+ }
322
+ evaluation.item(index);
323
+ }
324
+ for index in 0..len {
325
+ let child = if r.list {
326
+ r.items.get(index).copied().or(r.additional)
327
+ } else if index >= r.prefix.len() {
328
+ r.items.first().copied()
329
+ } else {
330
+ None
331
+ };
332
+ if let Some(child) = child {
333
+ let result = self.eval(child, unsafe { sn_entry(value, index) }, depth, active);
334
+ if result != Valid {
335
+ return result;
336
+ }
337
+ evaluation.item(index);
338
+ }
339
+ }
340
+ if let Some(child) = r.contains {
341
+ let mut count = 0;
342
+ for index in 0..len {
343
+ match self.eval(child, unsafe { sn_entry(value, index) }, depth, active) {
344
+ Valid => {
345
+ count += 1;
346
+ evaluation.item(index);
347
+ }
348
+ Invalid => {}
349
+ Unsupported => return Unsupported,
350
+ }
351
+ }
352
+ if bounds(count, Some(r.min_contains), r.max_contains) == Invalid {
353
+ return Invalid;
354
+ }
355
+ }
356
+ if let Some(child) = r.unevaluated.filter(|_| evaluation.item_count() < len) {
357
+ if matches!(self.nodes[child].as_slice(), [Op::Boolean(false)]) {
358
+ return Invalid;
359
+ }
360
+ for index in 0..len {
361
+ if evaluation.contains_item(index) {
362
+ continue;
363
+ }
364
+ let result = self.eval(child, unsafe { sn_entry(value, index) }, depth, active);
365
+ if result != Valid {
366
+ return result;
367
+ }
368
+ evaluation.item(index);
369
+ }
370
+ }
371
+ Valid
372
+ }
373
+ fn object_tracked(
374
+ &self,
375
+ r: &Object,
376
+ value: Value,
377
+ depth: usize,
378
+ active: scope::Context<'_>,
379
+ evaluation: &mut Tracked,
380
+ ) -> Verdict {
381
+ let len = unsafe { sn_len(value) };
382
+ if bounds(len, r.min, r.max) == Invalid {
383
+ return Invalid;
384
+ }
385
+ for &key in &r.required {
386
+ if missing(value, key, self.undefined) {
387
+ return Invalid;
388
+ }
389
+ }
390
+ let mut found = 0;
391
+ for (index, &(key, child)) in r.properties.iter().enumerate() {
392
+ let item = unsafe { sn_lookup(value, key) };
393
+ if item == self.undefined {
394
+ if r.required_properties[index] {
395
+ return Invalid;
396
+ }
397
+ continue;
398
+ }
399
+ found += 1;
400
+ let result = self.record_value(child, item, depth, active);
401
+ if result != Valid {
402
+ return result;
403
+ }
404
+ evaluation.known_property(r.property_indices[index]);
405
+ }
406
+ if r.names.is_some() || !r.patterns.is_empty() || (r.additional.is_some() && found < len) {
407
+ let result = self.tracked_object_walk(r, value, depth, active, evaluation, false);
408
+ if result != Valid {
409
+ return result;
410
+ }
411
+ }
412
+ for (name, required) in &r.dependent_required {
413
+ if !missing(value, *name, self.undefined) {
414
+ for &key in required {
415
+ if missing(value, key, self.undefined) {
416
+ return Invalid;
417
+ }
418
+ }
419
+ }
420
+ }
421
+ for &(key, child) in &r.dependent_schemas {
422
+ if !missing(value, key, self.undefined) {
423
+ let result = evaluation.merge(self.eval_tracked(child, value, depth, active));
424
+ if result != Valid {
425
+ return result;
426
+ }
427
+ }
428
+ }
429
+ if let Some(child) = r.unevaluated.filter(|_| evaluation.property_count() < len) {
430
+ if matches!(self.nodes[child].as_slice(), [Op::Boolean(false)]) {
431
+ return Invalid;
432
+ }
433
+ let result = self.tracked_object_walk(r, value, depth, active, evaluation, true);
434
+ if result != Valid {
435
+ return result;
436
+ }
437
+ }
438
+ Valid
439
+ }
440
+ fn tracked_object_walk(
441
+ &self,
442
+ rules: &Object,
443
+ value: Value,
444
+ depth: usize,
445
+ active: scope::Context<'_>,
446
+ evaluation: &mut Tracked,
447
+ unevaluated: bool,
448
+ ) -> Verdict {
449
+ let mut c = TrackingContext {
450
+ engine: self,
451
+ rules,
452
+ depth,
453
+ active,
454
+ evaluation,
455
+ unevaluated,
456
+ };
457
+ decode(unsafe {
458
+ sn_hash_each(
459
+ value,
460
+ (&mut c as *mut TrackingContext).cast(),
461
+ tracking_entry,
462
+ )
463
+ })
464
+ }
465
+ }
466
+ struct TrackingContext<'a> {
467
+ engine: &'a Engine,
468
+ rules: &'a Object,
469
+ depth: usize,
470
+ active: scope::Context<'a>,
471
+ evaluation: &'a mut Tracked,
472
+ unevaluated: bool,
473
+ }
474
+ unsafe extern "C" fn tracking_entry(context: *mut c_void, key: Value, value: Value) -> i32 {
475
+ catch_unwind(AssertUnwindSafe(|| {
476
+ let c = unsafe { &mut *context.cast::<TrackingContext>() };
477
+ if unsafe { sn_kind(key) } != TYPE_STRING {
478
+ return Unsupported as i32;
479
+ }
480
+ let key_location = JsonKey::new(key);
481
+ let known = c.engine.property_ids.get(&key_location).copied();
482
+ let child = if c.unevaluated {
483
+ if known.map_or_else(
484
+ || c.evaluation.properties.contains(&key_location),
485
+ |index| c.evaluation.contains_known_property(index),
486
+ ) {
487
+ return Valid as i32;
488
+ }
489
+ c.rules.unevaluated
490
+ } else {
491
+ if let Some(child) = c.rules.names {
492
+ let result = c.engine.eval(child, key, c.depth, c.active);
493
+ if result != Valid {
494
+ return result as i32;
495
+ }
496
+ }
497
+ let mut matched = c.rules.property_keys.contains(&key_location);
498
+ for (pattern, child) in &c.rules.patterns {
499
+ match pattern.eval(key) {
500
+ Unsupported => return Unsupported as i32,
501
+ Invalid => {}
502
+ Valid => {
503
+ matched = true;
504
+ let result = c.engine.eval(*child, value, c.depth, c.active);
505
+ if result != Valid {
506
+ return result as i32;
507
+ }
508
+ if let Some(index) = known {
509
+ c.evaluation.known_property(index);
510
+ } else {
511
+ c.evaluation.properties.insert(key_location);
512
+ }
513
+ }
514
+ }
515
+ }
516
+ if matched { None } else { c.rules.additional }
517
+ };
518
+ if let Some(child) = child {
519
+ let result = c.engine.eval(child, value, c.depth, c.active);
520
+ if result != Valid {
521
+ return result as i32;
522
+ }
523
+ if let Some(index) = known {
524
+ c.evaluation.known_property(index);
525
+ } else {
526
+ c.evaluation.properties.insert(key_location);
527
+ }
528
+ }
529
+ Valid as i32
530
+ }))
531
+ .unwrap_or(Unsupported as i32)
532
+ }
@@ -3,7 +3,7 @@
3
3
  module Schemurai
4
4
  module Backend
5
5
  VM_FEATURE = "vm/evaluator"
6
- CHOICES = %i[default ruby vm].freeze
6
+ CHOICES = %i[default ruby vm native].freeze
7
7
 
8
8
  module_function def requested
9
9
  value = ENV.fetch("SCHEMURAI_BACKEND", "default").to_sym
@@ -18,6 +18,7 @@ module Schemurai
18
18
 
19
19
  selection = production_default if selection == :default
20
20
  load_vm! if selection == :vm
21
+ require_relative "native/evaluator" if selection == :native
21
22
  selection
22
23
  end
23
24
 
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Native
5
+ # Render native diagnostic output using the messages shared by every backend.
6
+ # The instance remains an original Ruby object; this layer never traverses it.
7
+ module Diagnostics
8
+ TYPE_NAMES = {Protocol::OP_TYPE_NULL => "null", Protocol::OP_TYPE_BOOLEAN => "boolean", Protocol::OP_TYPE_OBJECT => "object",
9
+ Protocol::OP_TYPE_ARRAY => "array", Protocol::OP_TYPE_NUMBER => "number", Protocol::OP_TYPE_INTEGER => "integer",
10
+ Protocol::OP_TYPE_STRING => "string", Protocol::OP_TYPED_NUMBER => "number", Protocol::OP_TYPED_INTEGER => "integer",
11
+ Protocol::OP_TYPED_STRING => "string", Protocol::OP_TYPED_ARRAY => "array", Protocol::OP_TYPED_OBJECT => "object"}.freeze
12
+ ATTRIBUTES = {"maximum" => :maximum, "minimum" => :minimum, "exclusiveMaximum" => :exclusive_maximum, "exclusiveMinimum" => :exclusive_minimum,
13
+ "maxLength" => :max_length, "minLength" => :min_length, "maxItems" => :max_items, "minItems" => :min_items,
14
+ "maxProperties" => :max_properties, "minProperties" => :min_properties}.freeze
15
+ private_constant :TYPE_NAMES, :ATTRIBUTES
16
+
17
+ module_function def error(row)
18
+ keyword, instance_path, schema_path, opcode, operand, instance, detail = row
19
+ ValidationError.new(keyword: keyword, instance_path: instance_path, schema_path: schema_path,
20
+ message: message(keyword, opcode, operand, instance, detail))
21
+ end
22
+
23
+ module_function def message(keyword, opcode, operand, instance, detail) # rubocop:disable Metrics/MethodLength
24
+ messages = Internal::ErrorMessage
25
+ case keyword
26
+ when "falseSchema" then messages.false_schema
27
+ when "type" then messages.type((opcode == Protocol::OP_TYPES) ? operand.names : TYPE_NAMES.fetch(opcode), instance)
28
+ when "const" then messages.const
29
+ when "enum" then messages.enum
30
+ when "maximum", "minimum", "exclusiveMaximum", "exclusiveMinimum"
31
+ messages.numeric_limit(keyword, operand.public_send(ATTRIBUTES.fetch(keyword)))
32
+ when "multipleOf" then messages.multiple_of(operand.multiple_of)
33
+ when "maxLength", "minLength", "maxItems", "minItems", "maxProperties", "minProperties"
34
+ messages.size(keyword, operand.public_send(ATTRIBUTES.fetch(keyword)), detail)
35
+ when "pattern" then messages.pattern(operand.pattern)
36
+ when "format" then messages.format(operand.format.name)
37
+ when "contentEncoding" then messages.content_encoding
38
+ when "contentMediaType" then messages.content_media_type
39
+ when "uniqueItems" then messages.unique_items
40
+ when "required" then messages.required(instance)
41
+ when "dependencies", "dependentRequired" then messages.dependent_required(operand, instance)
42
+ when "contains" then messages.contains(detail, operand.min_contains, operand.max_contains)
43
+ when "anyOf" then messages.any_of
44
+ when "oneOf" then messages.one_of(detail)
45
+ when "not" then messages.not
46
+ else raise "unknown native diagnostic #{keyword.inspect}"
47
+ end
48
+ end
49
+ private_class_method :message
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../vm/evaluator"
4
+ require "schemurai_native"
5
+ require_relative "diagnostics"
6
+
7
+ module Schemurai
8
+ module Native
9
+ class Evaluator
10
+ def backend = :native
11
+
12
+ def initialize(graph, compiler, root, content: false, format: false)
13
+ @program = Program.new(root, compiler, format, content)
14
+ @vm = VM::Evaluator.new(graph, compiler, root, content: content, format: format)
15
+ end
16
+
17
+ def valid?(instance)
18
+ result = @program.valid?(instance)
19
+ result.nil? ? @vm.valid?(instance) : result
20
+ end
21
+
22
+ def validate(instance)
23
+ rows = @program.diagnostics(instance)
24
+ return @vm.validate(instance) if rows.nil?
25
+
26
+ Result.new(rows.empty? ? rows : rows.map { |row| Diagnostics.error(row) })
27
+ end
28
+ end
29
+ end
30
+
31
+ private_constant :Native
32
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Schemurai
4
4
  # The installed Schemurai version.
5
- VERSION = "2.2.2"
5
+ VERSION = "2.3.0"
6
6
  end