1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
//! Pre-dispatch normalization and cover phases for subset analysis.
//!
//! These helpers run before the large structural `SchemaNodeKind` match. They
//! are kept in source order because many rules intentionally shadow more
//! general branch-wise reasoning.
use super::*;
/// Local contradictions that make the subset side empty before type dispatch.
pub(super) fn subset_is_locally_vacuous_before_dispatch(schema: &SchemaNode) -> bool {
array_schema_is_locally_impossible(schema) || object_schema_is_locally_impossible(schema)
}
/// Normalize degenerate `oneOf` forms before general branch-wise reasoning.
///
/// This handles only exact/local-empty XOR identities; broader oneOf reasoning
/// stays in the dispatcher so it remains ordered with the other partition rules.
pub(super) fn try_normalize_trivial_one_of(
sub: &SchemaNode,
sup: &SchemaNode,
context: &mut SubschemaCheckContext,
mode: ExplanationMode,
) -> Option<SubschemaAnalysis> {
if let SchemaNodeKind::OneOf(branches) = sub.kind() {
if one_of_pair_is_syntactically_empty(branches) {
return Some(SubschemaAnalysis::compatible());
}
if let Some(live) = one_of_trivial_live_branch(branches) {
return Some(if let Some(live) = live {
analyze_subschema_with_context(live, sup, context, mode)
} else {
SubschemaAnalysis::compatible()
});
}
if let Some(live) = one_of_pair_single_live_branch(branches) {
return Some(analyze_subschema_with_context(live, sup, context, mode));
}
}
if let SchemaNodeKind::OneOf(branches) = sup.kind() {
if let Some(Some(live)) = one_of_trivial_live_branch(branches) {
return Some(analyze_subschema_with_context(sub, live, context, mode));
}
// An empty oneOf on the right only contains an empty subset; leave that
// to the ordinary machinery unless the subset was handled above.
if let Some(live) = one_of_pair_single_live_branch(branches) {
return Some(analyze_subschema_with_context(sub, live, context, mode));
}
}
None
}
/// Peel exact wrapper identities (double negation and constant-guard conditionals).
pub(super) fn try_peel_exact_wrappers(
sub: &SchemaNode,
sup: &SchemaNode,
context: &mut SubschemaCheckContext,
mode: ExplanationMode,
) -> Option<SubschemaAnalysis> {
if let SchemaNodeKind::Not(outer) = sub.kind()
&& let SchemaNodeKind::Not(inner) = unwrap_singleton_applicators(outer).kind()
{
return Some(analyze_subschema_with_context(inner, sup, context, mode));
}
if let SchemaNodeKind::Not(outer) = sup.kind()
&& let SchemaNodeKind::Not(inner) = unwrap_singleton_applicators(outer).kind()
{
return Some(analyze_subschema_with_context(sub, inner, context, mode));
}
if let SchemaNodeKind::IfThenElse {
if_schema,
then_schema,
else_schema,
} = sup.kind()
&& let Some(live) =
constant_guard_conditional_branch(if_schema, then_schema.as_ref(), else_schema.as_ref())
{
match live {
LiveConditionalBranch::Universal => return Some(SubschemaAnalysis::compatible()),
LiveConditionalBranch::Schema(branch) if branch.id() != sup.id() => {
return Some(analyze_subschema_with_context(sub, branch, context, mode));
}
LiveConditionalBranch::Schema(_) => {}
}
}
if let SchemaNodeKind::IfThenElse {
if_schema,
then_schema,
else_schema,
} = sub.kind()
&& let Some(live) =
constant_guard_conditional_branch(if_schema, then_schema.as_ref(), else_schema.as_ref())
{
match live {
LiveConditionalBranch::Universal => {
let is_subschema = schema_is_trivially_universal(sup);
return Some(SubschemaAnalysis::from_check(is_subschema, mode, || {
explain_schema_kind_gap(sub, sup)
}));
}
LiveConditionalBranch::Schema(branch) if branch.id() != sub.id() => {
return Some(analyze_subschema_with_context(branch, sup, context, mode));
}
LiveConditionalBranch::Schema(_) => {}
}
}
None
}
/// Try pre-dispatch complement, partition, and cover identities that only prove success.
///
/// These rules are deliberately kept before the structural match: many of them
/// recognize normalized boolean encodings that would otherwise be treated as
/// ordinary branch-wise `oneOf`/`anyOf` relations.
pub(super) fn predispatch_cover_proves_subset(
sub: &SchemaNode,
sup: &SchemaNode,
context: &mut SubschemaCheckContext,
) -> bool {
// The complement of a syntactically empty schema is universal. This
// catches common normalized contradictions such as `not(allOf[type:string,
// type:array])` before the ordinary negation arm asks for contravariant
// implication facts.
if let SchemaNodeKind::Not(excluded) = sup.kind()
&& schema_is_locally_impossible_for_negation(unwrap_singleton_applicators(excluded))
{
return true;
}
// Push the same constant-guard conditional normalization through a
// negated target. `not(if true then A else B)` is just `not A`, so a
// disjointness proof against the live branch is sufficient. Do not
// synthesize a temporary `Not` node; the existing negated-target
// disjointness prover already has the right conservative structure.
if let SchemaNodeKind::Not(excluded) = sup.kind()
&& let SchemaNodeKind::IfThenElse {
if_schema,
then_schema,
else_schema,
} = unwrap_singleton_applicators(excluded).kind()
&& let Some(live) =
constant_guard_conditional_branch(if_schema, then_schema.as_ref(), else_schema.as_ref())
{
match live {
LiveConditionalBranch::Schema(branch)
if schemas_definitely_disjoint_for_negation(sub, branch, context) =>
{
return true;
}
LiveConditionalBranch::Universal
if schema_is_locally_empty_for_finite_enumeration(sub) =>
{
return true;
}
_ => {}
}
}
// A conditional can be syntactically universal even when it is not the
// literal `true` schema: if every value satisfying the guard is accepted
// by the `then` branch, and the `else` branch is unconstrained (or absent),
// the conditional accepts all instances. Recognize this before the main
// structural match so such generated guards behave like a true superset.
if let SchemaNodeKind::IfThenElse {
if_schema,
then_schema,
else_schema,
} = sup.kind()
&& conditional_is_known_universal(
if_schema,
then_schema.as_ref(),
else_schema.as_ref(),
context,
)
{
return true;
}
// A union containing both `A` and `not B` is universal whenever B <= A.
// This complement-cover pattern is common in generated partition schemas
// and is safe to recognize before the ordinary branch-wise AnyOf logic.
if let SchemaNodeKind::AnyOf(branches) = sup.kind()
&& (any_of_contains_known_universal_branch(branches, context)
|| any_of_complement_cover_is_universal(branches, context)
|| any_of_complement_union_cover_is_universal(branches, context)
|| any_of_property_presence_cover_is_universal(branches)
|| any_of_single_property_name_partition_is_universal(branches)
|| any_of_required_property_value_partition_is_universal(branches)
|| any_of_prefix_item_partition_is_universal(branches)
|| any_of_items_contains_partition_is_universal(branches)
|| any_of_integer_partition_cover_is_universal(branches)
|| any_of_numeric_range_cover_is_universal(branches)
|| any_of_string_length_cover_is_universal(branches)
|| any_of_array_count_cover_is_universal(branches)
|| any_of_object_count_cover_is_universal(branches))
{
return true;
}
// A two-arm `oneOf` of a schema and its complement is universal.
if let SchemaNodeKind::OneOf(branches) = sup.kind()
&& one_of_complement_pair_is_universal(branches, context)
{
return true;
}
// A `oneOf` with one universal arm behaves like the complement of the
// other arm. Use local disjointness to prove a subset lands on exactly
// the universal side.
if let SchemaNodeKind::OneOf(branches) = sup.kind()
&& one_of_universal_arm_contains_subset(sub, branches, context)
{
return true;
}
// Likewise, `oneOf: [A, B, not(anyOf[A, B])]` is universal when the
// positive siblings are known to be an exact disjoint cover of the union.
if let SchemaNodeKind::OneOf(branches) = sup.kind()
&& one_of_complement_union_partition_is_universal(branches, context)
{
return true;
}
// The same finite complement partition can be spelled with `oneOf` when
// the finite side is split into mutually exclusive sibling branches.
if let SchemaNodeKind::OneOf(branches) = sup.kind()
&& one_of_finite_complement_partition_is_universal(branches, context)
{
return true;
}
// A two-arm xor of complements is contained in the union of its excluded
// sides; prove each excluded side against the target directly.
if let SchemaNodeKind::OneOf(branches) = sub.kind()
&& complement_only_oneof_subset_of(branches, sup, context)
{
return true;
}
// A two-arm xor with comparable positive arms is a set difference.
// If every excluded target arm is either inside the removed side or
// disjoint from the retained side, the difference fits the negation.
if let SchemaNodeKind::OneOf(branches) = sub.kind()
&& let SchemaNodeKind::Not(excluded) = sup.kind()
&& comparable_oneof_difference_subset_of_negation(branches, excluded, context)
{
return true;
}
// A mixed xor `oneOf[A, not B]` collapses to `not(A ∪ B)` when A and B
// are disjoint. In that case it is certainly contained by `not T` for
// any T known to fit inside either excluded side. This catches compact
// encodings of "everything except these two disjoint regions" without
// constructing a synthetic union node.
if let SchemaNodeKind::OneOf(branches) = sub.kind()
&& mixed_oneof_disjoint_complement_subset_of_target(branches, sup, context)
{
return true;
}
// Dual De Morgan shape: the complement of an intersection of explicit
// complements is the union of their positive inners. If each positive
// inner is contained by the target, the whole negated intersection is too.
if let SchemaNodeKind::Not(excluded_intersection) = sub.kind()
&& let SchemaNodeKind::AllOf(conjuncts) =
unwrap_singleton_applicators(excluded_intersection).kind()
&& conjuncts
.iter()
.all(|conjunct| matches!(conjunct.kind(), SchemaNodeKind::Not(_)))
&& conjuncts.iter().all(|conjunct| {
if let SchemaNodeKind::Not(inner) = conjunct.kind() {
analyze_subschema_with_context(inner, sup, context, ExplanationMode::VerdictOnly)
.is_subschema
} else {
false
}
})
{
return true;
}
// If the target mixed xor rejects only a finite comparable gap, it is
// enough for a negated subset to exclude each gap value concretely.
if let SchemaNodeKind::Not(excluded) = sub.kind()
&& negated_schema_excludes_mixed_finite_gap(
unwrap_singleton_applicators(excluded),
sup,
context,
)
{
return true;
}
// Complemented xor normalization for `not(oneOf[A, not B])` in the
// conservative comparable/disjoint cases handled by the helper.
if let SchemaNodeKind::Not(excluded_xor) = sub.kind()
&& let SchemaNodeKind::OneOf(children) = unwrap_singleton_applicators(excluded_xor).kind()
&& (negated_oneof_complement_pair_subset_of(children, sup, context)
|| negated_complement_pair_subset_of_mixed_difference(children, sup, context)
|| negated_xor_covers_mixed_comparable_gap(children, sup, context))
{
return true;
}
// For a two-arm xor, its complement accepts values that satisfy both
// arms (or neither arm). Prove either side directly before falling back
// to more specialized complement-arm identities.
if let SchemaNodeKind::Not(excluded_xor) = sup.kind()
&& let SchemaNodeKind::OneOf(children) = unwrap_singleton_applicators(excluded_xor).kind()
&& negated_two_arm_oneof_contains(sub, children, context)
{
return true;
}
// The complement of `oneOf[not A, B]` contains both A and B when
// A and B are definitely disjoint: outside the overlap, the xor is false
// exactly on those two regions. This is a common spelling of a two-way
// partition with one arm negated.
if let SchemaNodeKind::Not(excluded_xor) = sup.kind()
&& let SchemaNodeKind::OneOf(children) = unwrap_singleton_applicators(excluded_xor).kind()
&& negated_oneof_complement_pair_contains(sub, children, context)
{
return true;
}
// Untyped array/object count assertions canonicalize the same way as
// string lengths: all other JSON types plus one count half-line.
if let SchemaNodeKind::Not(excluded_union) = sub.kind()
&& let SchemaNodeKind::AnyOf(children) = unwrap_singleton_applicators(excluded_union).kind()
&& (negated_untyped_array_count_halfline_subset_of(children, sup)
|| negated_untyped_object_count_halfline_subset_of(children, sup))
{
return true;
}
// Analogous canonicalization for untyped string length assertions:
// all non-strings are accepted by the positive union, so its negation is
// the opposite string-length half-line.
if let SchemaNodeKind::Not(excluded_union) = sub.kind()
&& let SchemaNodeKind::AnyOf(children) = unwrap_singleton_applicators(excluded_union).kind()
&& negated_untyped_string_length_halfline_subset_of(children, sup)
{
return true;
}
// A negated canonicalized untyped numeric assertion has the shape
// `not(anyOf[<all non-number types>, <numeric half-line>])`. Once every
// non-number type is covered, the complement is a numeric half-line with
// the endpoint flipped; compare that interval directly to numeric targets.
if let SchemaNodeKind::Not(excluded_union) = sub.kind()
&& let SchemaNodeKind::AnyOf(children) = unwrap_singleton_applicators(excluded_union).kind()
&& negated_untyped_numeric_halfline_subset_of(children, sup)
{
return true;
}
// One-sided De Morgan normalization: `not(anyOf[..., not A, ...])`
// implies `A`. Do this before the main match because finite/enum targets
// have early arms that would otherwise hide the negated-subset case.
if let SchemaNodeKind::Not(excluded_union) = sub.kind()
&& let SchemaNodeKind::AnyOf(children) = unwrap_singleton_applicators(excluded_union).kind()
&& (negated_union_type_remainder_subset_of(children, sup)
|| negated_anyof_finite_complement_arm_subset_of(children, sup, context)
|| negated_union_subset_of_mixed_difference(children, sup, context)
|| children.iter().any(|child| {
if let SchemaNodeKind::Not(required) = child.kind() {
analyze_subschema_with_context(
required,
sup,
context,
ExplanationMode::VerdictOnly,
)
.is_subschema
} else {
false
}
}))
{
return true;
}
// A very common xor spelling for "objects without property p" is
// `oneOf: [{type: object}, {type: object, required: [p]}]`. The generic
// oneOf rule below checks each arm independently, which is too strong: the
// second arm is precisely removed by the xor. Recognize the narrow
// presence-partition form before falling back to branch-wise reasoning.
if let SchemaNodeKind::OneOf(children) = sub.kind()
&& (oneof_object_absence_partition_subset_of(children, sup)
|| oneof_array_empty_partition_subset_of(children, sup)
|| oneof_string_empty_partition_subset_of(children, sup)
|| oneof_object_empty_partition_subset_of(children, sup))
{
return true;
}
false
}